mirror of https://github.com/radex/radmatrix.git
Compare commits
7 Commits
3880a58137
...
c16581103d
Author | SHA1 | Date |
---|---|---|
radex | c16581103d | |
radex | 91b144ff83 | |
radex | 9cd5e00309 | |
radex | 3c42453975 | |
radex | be5f1abb08 | |
radex | dfaf256926 | |
radex | d43bdba36e |
65
README.md
65
README.md
|
@ -1,11 +1,62 @@
|
|||
# pico-led-matrix
|
||||
# radmatrix
|
||||
|
||||
Tiny modular LED matrix. Idea is to have a matrix of 50x50mm modules:
|
||||
Smol modular LED matrix, consisting of 3 types of 50x50mm modules:
|
||||
|
||||
- LED modules with a 20x20 matrix of 0603 red LEDs.
|
||||
- Multiplexer/driver modules at the end of rows and columns
|
||||
- Microcontroller (RP2040) board in the corner.
|
||||
- LED modules with a 20x20 matrix of 0603 red LEDs
|
||||
- Driver modules at the end of rows and columns (bottom and right)
|
||||
- Microcontroller (RP2040) board in the bottom-right corner
|
||||
|
||||
JLCPCB Assembly optimized.
|
||||
This was built as my first "serious" electronics project and its main purpose was to learn electronics a bit better. Some RP2040 embedded knowledge and a blinky lights object are nice side effects.
|
||||
|
||||
This is an electronics learning experiment, will likely fail. I do not know what I'm doing
|
||||
## Results
|
||||
|
||||
TODO: Photos, video, PCB renders
|
||||
|
||||
## Design details
|
||||
|
||||
The LED matrix and driver module PCB design were optimized for JLCPCB Assembly - i.e. only using parts available in its (LCSC) catalog and strongly preferring Basic parts to avoid paying feeder fees. The LEDs chosen were the cheapest available in JLC's catalog (they work fine btw).
|
||||
|
||||
The microcontroller board was optimized for JLC PCBA to a lesser extent -- I used multiple parts that would incur a feeder fee if not for the fact that I hand-soldered them.
|
||||
|
||||
The display is capable of:
|
||||
|
||||
- 8 bit (TODO, 5-bit demonstrated) red color
|
||||
- 60fps (TODO, 30fps demonstrated)
|
||||
|
||||
Additionally, the MCU board can:
|
||||
|
||||
- Read multiple "videos" (specially encoded for this firmware) from an SD card
|
||||
- Pipe in 8-bit 44kHz audio (TODO, lower quality demonstrated due to SD performance issues)
|
||||
- do CAN (TODO)
|
||||
|
||||
## v2
|
||||
|
||||
Now that I've leraned from my mistakes, here's what I would do if I were to build a v2 of this display (I probably won't):
|
||||
|
||||
- Use square, preferably RGB LED "pixels"
|
||||
- Fit them in a grid of **standard** spacing (i.e. 1, 1.27, 2, or 2.54mm)
|
||||
- Put pixels edge to edge
|
||||
- Current design simplfiied connections and JLC assembly, but it's ugly
|
||||
- Use a black PCB for the LED matrix for increased contrast
|
||||
- Move driver modules from column/row ends to the backs of LED modules
|
||||
- The current design is kinda neat, but it doesn't scale, as with each additional row of modules, the perceptual brightness of LEDs goes down
|
||||
- A 2mm-spacing 32x32 standard module seems reasonable
|
||||
- Connection between LED module and driver module would be done by placing 1x16 SMD pin headers alongside corners (split in half, say, bottom-left and top-right for rows, and likewise for columns to allow edge-to-edge clearance)
|
||||
- Possibly some additional pin headers for added stability, maybe some solderable nuts - some mechanical connection between modules would be nice
|
||||
- Driver modules
|
||||
- Would consider constant-current drivers, although resistor-based approach seems to work fine and appeared cheaper
|
||||
- If resistor-based approach is still used, I would consider a buck converter for creating a variable LED power supply
|
||||
- Would consider not skipping any of the shift register stages as that makes programming more annoying, and in this approach, it shouldn't make layout too difficult
|
||||
- Fix v1 bugs of course
|
||||
- Would consider faster shift registers. Perhaps 74HCT (5V supply to '595 gives best performance, and HCT can handle 3v3 input), or LV*
|
||||
- Would consider a decoder instead of shift registers for selecting rows. The current method is risky in that if programming is not done correctly, multiple LED rows could be lighting up at the same time (with one resistor for multiple LEDs), overstressing them. It's likely that current approach is cheaper and requires fewer signal lines though
|
||||
- Driving the display
|
||||
- Each LED/driver module would have a separate connection to the MCU board - sharing row selection, OE, SRCLK, RCLK, lines, but each having its own SER line
|
||||
- With PIO, this would have minimal impact on driving performance, and RP2040 IO counts would likely allow for 16-20 module to be connected to a single MCU board
|
||||
- This could in principle be extended further with multiple MCU boards talking to a master
|
||||
- The display would become /32, so would be brighter and that would not depend on its height
|
||||
- Each driver module should have a buffer before/after it to improve the signal
|
||||
|
||||
Note that while most people do projects like this with an FPGA, in my opinion the RP2040, thanks to PIO, is more than capable enough for this job at a better price, and can do other stuff in addition to just driving the display. The main downside vs an FPGA is limited IO (30 pins), which limits how big of a display can be driven. With the design above, likely ~20kpx (though this could be optimized further)
|
||||
|
||||
Noting the above so I don't forget or in case someone else wants to take this project further. To me, this project has fulfilled its educational goal, and making large LED displays this way is impractical given that you can get HUB75 modules for less than just the LED cost off LCSC.
|
||||
|
|
|
@ -40,49 +40,8 @@ int32_t gfx_decoder_loadNextFrame() {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Convert framebuffer into raw shift register data for fast PIO pixel pushing
|
||||
// Data will be held in buffers, one per pixel's depth bit (aka brightness stage),
|
||||
// with each row split into 32-bit chunks, one per module
|
||||
// (20 pixels, 24 shift register stages, 8 unused bits)
|
||||
// Rows are inverted, because that's how they're fed to the shift registers
|
||||
// TODO: Move this to leds.cpp
|
||||
// TODO: Use a separate buffer, then copy to ledsBuffer to avoid tearing
|
||||
for (int bi = 0; bi < 8; bi++) {
|
||||
uint8_t bitPosition = 1 << bi;
|
||||
for (int y = 0; y < ROW_COUNT; y++) {
|
||||
auto yOffset = y * COL_COUNT;
|
||||
for (int xModule = 0; xModule < COL_MODULES; xModule++) {
|
||||
auto bufferXOffset = yOffset + xModule * 20;
|
||||
uint32_t sample = 0;
|
||||
leds_set_framebuffer(buffer);
|
||||
|
||||
for (int x = 0; x < 20; x++) {
|
||||
// insert placeholders for unused stages
|
||||
// (before pixels 0, 6, 13)
|
||||
if (x == 0 || x == 6 || x == 13) {
|
||||
sample >>= 1;
|
||||
}
|
||||
uint8_t px = buffer[bufferXOffset + x];
|
||||
bool bit = px & bitPosition;
|
||||
sample = (sample >> 1) | (bit ? 0x80000000 : 0);
|
||||
}
|
||||
// insert placeholder for unused last stage (after pixel 19)
|
||||
sample >>=1;
|
||||
// shift to LSB position
|
||||
sample >>=8;
|
||||
// MSB=1 indicates end of row
|
||||
if (xModule == COL_MODULES - 1) {
|
||||
sample |= 0x80000000;
|
||||
}
|
||||
|
||||
ledBuffer[bi][(ROW_COUNT - 1 - y) * COL_MODULES + xModule] = sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
ledBufferReady = true;
|
||||
|
||||
// copy to framebuffer
|
||||
// TODO: mutex? double buffer? or something...
|
||||
memcpy(framebuffer, buffer, ROW_COUNT * COL_COUNT);
|
||||
free(buffer);
|
||||
return frameSize;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
#include "leds.h"
|
||||
#include "leds.pio.h"
|
||||
|
||||
PIO pusher_pio = pio0;
|
||||
PIO leds_pio = pio0;
|
||||
uint pusher_sm = 255; // invalid
|
||||
uint delay_sm = 255; // invalid
|
||||
uint row_sm = 255; // invalid
|
||||
|
||||
// NOTE: RCLK, SRCLK capture on *rising* edge
|
||||
inline void pulsePin(uint8_t pin) {
|
||||
|
@ -32,12 +34,98 @@ inline void outputEnable(uint8_t pin, bool enable) {
|
|||
// we have COLOR_BITS-bit color depth, so 2^COLOR_BITS levels of brightness
|
||||
// we go from phase 0 to phase (COLOR_BITS-1)
|
||||
uint8_t brightnessPhase = 0;
|
||||
uint8_t brightnessPhaseDelays[COLOR_BITS] = {0, 1, 6, 20, 60};
|
||||
|
||||
// delays in nanoseconds
|
||||
#define NS_TO_DELAY(ns) ns / NS_PER_CYCLE
|
||||
uint32_t brightnessPhaseDelays[COLOR_BITS] = {
|
||||
NS_TO_DELAY(50),
|
||||
NS_TO_DELAY(100),
|
||||
NS_TO_DELAY(200),
|
||||
NS_TO_DELAY(500),
|
||||
NS_TO_DELAY(1500),
|
||||
NS_TO_DELAY(6000),
|
||||
NS_TO_DELAY(20000),
|
||||
NS_TO_DELAY(60000),
|
||||
};
|
||||
|
||||
// NOTE: Alignment required to allow 4-byte reads
|
||||
uint8_t framebuffer[ROW_COUNT * COL_COUNT] __attribute__((aligned(32))) = {0};
|
||||
|
||||
// Framebuffer encoded for fast PIO pixel pushing
|
||||
// There's one buffer for each of pixel's bit indices (aka brightness phases),
|
||||
// Then for each row (laid out bottom to top), we have:
|
||||
// - one 32-bit word per horizontal (column) module:
|
||||
// 20 pixels = 24 shift register stages (4 placeholders), 7 unused bits,
|
||||
// 1 bit to indicate end of row
|
||||
// - one word for selecting (shifting) a row:
|
||||
// 1 bit (LSB) to indicate start of frame (1) or not (0)
|
||||
// remaining bits to indicate a number of shift register pulses
|
||||
// (again, 24 shift register stages per 20 rows, so there are placeholders)
|
||||
uint32_t ledBuffer[8][ROW_COUNT * (COL_MODULES + 1)] = {0};
|
||||
bool ledBufferReady = false;
|
||||
uint32_t ledBuffer[8][ROW_COUNT * COL_MODULES] = {0};
|
||||
|
||||
void leds_set_framebuffer(uint8_t *buffer) {
|
||||
// TODO: Use a separate buffer, then copy to ledsBuffer to avoid tearing
|
||||
for (int bi = 0; bi < 8; bi++) {
|
||||
uint8_t bitPosition = 1 << bi;
|
||||
|
||||
for (int yModule = 0; yModule < ROW_MODULES; yModule++) {
|
||||
for (int moduleY = 0; moduleY < 20; moduleY++) {
|
||||
auto y = yModule * 20 + moduleY;
|
||||
|
||||
auto bufferYOffset = (ROW_COUNT - 1 - y) * COL_COUNT;
|
||||
auto outputYOffset = y * (COL_MODULES + 1);
|
||||
|
||||
// set data for a given row
|
||||
for (int xModule = 0; xModule < COL_MODULES; xModule++) {
|
||||
auto bufferXOffset = bufferYOffset + xModule * 20;
|
||||
uint32_t sample = 0;
|
||||
|
||||
for (int x = 0; x < 20; x++) {
|
||||
// insert placeholders for unused stages
|
||||
// (before pixels 0, 6, 13)
|
||||
if (x == 0 || x == 6 || x == 13) {
|
||||
sample >>= 1;
|
||||
}
|
||||
uint8_t px = buffer[bufferXOffset + x];
|
||||
bool bit = px & bitPosition;
|
||||
sample = (sample >> 1) | (bit ? 0x80000000 : 0);
|
||||
}
|
||||
// insert placeholder for unused last stage (after pixel 19)
|
||||
sample >>=1;
|
||||
// shift to LSB position
|
||||
sample >>=8;
|
||||
// MSB=1 indicates end of row
|
||||
if (xModule == COL_MODULES - 1) {
|
||||
sample |= 0x80000000;
|
||||
}
|
||||
|
||||
ledBuffer[bi][outputYOffset + xModule] = sample;
|
||||
}
|
||||
|
||||
// set row shifting data
|
||||
bool firstRow = y == (ROW_COUNT - 1);
|
||||
uint32_t rowPulses = 1;
|
||||
|
||||
if (moduleY == 0) {
|
||||
rowPulses++;
|
||||
}
|
||||
|
||||
if (moduleY == 7 || moduleY == 14 || (moduleY == 0 && yModule != 0)) {
|
||||
rowPulses++;
|
||||
}
|
||||
|
||||
uint32_t rowData = firstRow | (rowPulses << 1);
|
||||
ledBuffer[bi][outputYOffset + COL_MODULES] = rowData;
|
||||
}
|
||||
}
|
||||
}
|
||||
ledBufferReady = true;
|
||||
|
||||
// copy to framebuffer
|
||||
// TODO: mutex? double buffer? or something...
|
||||
memcpy(framebuffer, buffer, ROW_COUNT * COL_COUNT);
|
||||
}
|
||||
|
||||
void leds_init() {
|
||||
memset(framebuffer, 0, sizeof(framebuffer));
|
||||
|
@ -86,80 +174,42 @@ void main2() {
|
|||
}
|
||||
|
||||
void leds_initPusher();
|
||||
void leds_initRowSelector();
|
||||
void leds_initDelay();
|
||||
|
||||
void leds_initRenderer() {
|
||||
leds_initPusher();
|
||||
leds_initRowSelector();
|
||||
leds_initDelay();
|
||||
multicore_reset_core1();
|
||||
multicore_launch_core1(main2);
|
||||
}
|
||||
|
||||
void leds_render() {
|
||||
if (!ledBufferReady) {
|
||||
outputEnable(ROW_OE, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// brightness phase
|
||||
bool brightPhase = brightnessPhase >= 3;
|
||||
auto buffer = ledBuffer[brightnessPhase + 3];
|
||||
auto buffer = ledBuffer[brightnessPhase];
|
||||
auto delayData = brightnessPhaseDelays[brightnessPhase];
|
||||
|
||||
// hide output
|
||||
outputEnable(ROW_OE, false);
|
||||
|
||||
// clear rows
|
||||
clearShiftReg(ROW_SRCLK, ROW_SRCLR);
|
||||
|
||||
// start selecting rows
|
||||
gpio_put(ROW_SER, HIGH);
|
||||
|
||||
int bufferOffset = 0;
|
||||
for (int yModule = 0; yModule < ROW_MODULES; yModule++) {
|
||||
for (int moduleY = 0; moduleY < 20; moduleY++) {
|
||||
// brigthness - pushing data takes time, so to maximize brightness (at high brightness phases)
|
||||
// we want to keep the matrix on during update (except during latch). At low brightness phases,
|
||||
// we want it off to actually be dim
|
||||
outputEnable(ROW_OE, brightPhase);
|
||||
|
||||
// next row
|
||||
pulsePin(ROW_SRCLK);
|
||||
// only one row
|
||||
gpio_put(ROW_SER, LOW);
|
||||
|
||||
// we use 7/8 stages on shift registers + 1 is unused
|
||||
if (moduleY == 0) {
|
||||
pulsePin(ROW_SRCLK);
|
||||
}
|
||||
|
||||
if (moduleY == 7 || moduleY == 14 || (moduleY == 0 && yModule != 0)) {
|
||||
pulsePin(ROW_SRCLK);
|
||||
}
|
||||
|
||||
// set row data using PIO
|
||||
// latch signal is also sent here
|
||||
// TODO: Some ideas for future optimization:
|
||||
// - see if we can disable px pusher delays on improved electric interface
|
||||
// - improve outer loop which adds 2us of processing on each loop
|
||||
// - change busy wait into some kind of interrupt-based thing so that processing can continue
|
||||
// - DMA?
|
||||
for (int xModule = 0; xModule < COL_MODULES; xModule++) {
|
||||
uint32_t pxValues = buffer[bufferOffset + xModule];
|
||||
pio_sm_put_blocking(pusher_pio, pusher_sm, pxValues);
|
||||
}
|
||||
|
||||
// wait until pushing and RCLK latch are done
|
||||
while (!pio_interrupt_get(pusher_pio, 0)) {
|
||||
tight_loop_contents();
|
||||
}
|
||||
pio_interrupt_clear(pusher_pio, pusher_sm);
|
||||
|
||||
// show for a certain period
|
||||
outputEnable(ROW_OE, true);
|
||||
busy_wait_us_32(brightnessPhaseDelays[brightnessPhase]);
|
||||
outputEnable(ROW_OE, false);
|
||||
|
||||
// next row
|
||||
bufferOffset += COL_MODULES;
|
||||
// The correct data to push onto PIO has been precomputed by leds_set_framebuffer
|
||||
// So we only need to move the buffer onto PIO TX FIFOs to keep them full
|
||||
// TODO: Rewrite this to be interrupt-based. Should be relatively easy to always keep PIO
|
||||
// full via interrupts and free up most of the core's time to other tasks
|
||||
for (uint8_t y = 0; y < ROW_COUNT; y++) {
|
||||
// set row data
|
||||
for (uint8_t x = 0; x < COL_MODULES; x++) {
|
||||
auto pxValues = *buffer++;
|
||||
pio_sm_put_blocking(leds_pio, pusher_sm, pxValues);
|
||||
}
|
||||
|
||||
// set row selection data
|
||||
auto rowSelData = *buffer++;
|
||||
pio_sm_put_blocking(leds_pio, row_sm, rowSelData);
|
||||
|
||||
// set delay data
|
||||
pio_sm_put_blocking(leds_pio, delay_sm, delayData);
|
||||
}
|
||||
|
||||
// next brightness phase
|
||||
|
@ -167,33 +217,34 @@ void leds_render() {
|
|||
}
|
||||
|
||||
void leds_initPusher() {
|
||||
PIO pio = pusher_pio;
|
||||
PIO pio = leds_pio;
|
||||
uint sm = pio_claim_unused_sm(pio, true);
|
||||
pusher_sm = sm;
|
||||
|
||||
uint offset = pio_add_program(pio, &leds_px_pusher_program);
|
||||
|
||||
uint dataPin = COL_SER;
|
||||
uint latchPin = COL_SRCLK;
|
||||
|
||||
pio_sm_config config = leds_px_pusher_program_get_default_config(offset);
|
||||
sm_config_set_clkdiv_int_frac(&config, 1, 0);
|
||||
|
||||
// Shift OSR to the right, autopull
|
||||
sm_config_set_out_shift(&config, true, true, 32);
|
||||
|
||||
// Use FIFO join to create a longer TX FIFO
|
||||
// NOTE: This is not needed for the other SMs, as the px pusher will always be the bottleneck
|
||||
sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_TX);
|
||||
|
||||
// Set OUT (data) pin, connect to pad, set as output
|
||||
sm_config_set_out_pins(&config, dataPin, 1);
|
||||
pio_gpio_init(pio, dataPin);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, dataPin, 1, true);
|
||||
sm_config_set_out_pins(&config, COL_SER, 1);
|
||||
pio_gpio_init(pio, COL_SER);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, COL_SER, 1, true);
|
||||
|
||||
// data is inverted
|
||||
gpio_set_outover(dataPin, GPIO_OVERRIDE_INVERT);
|
||||
gpio_set_outover(COL_SER, GPIO_OVERRIDE_INVERT);
|
||||
|
||||
// Set sideset (SRCLK) pin, connect to pad, set as output
|
||||
sm_config_set_sideset_pins(&config, latchPin);
|
||||
pio_gpio_init(pio, latchPin);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, latchPin, 1, true);
|
||||
sm_config_set_sideset_pins(&config, COL_SRCLK);
|
||||
pio_gpio_init(pio, COL_SRCLK);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, COL_SRCLK, 1, true);
|
||||
|
||||
// Set SET (RCLK) pin, connect to pad, set as output
|
||||
sm_config_set_set_pins(&config, RCLK, 1);
|
||||
|
@ -204,3 +255,55 @@ void leds_initPusher() {
|
|||
pio_sm_init(pio, sm, offset, &config);
|
||||
pio_sm_set_enabled(pio, sm, true);
|
||||
}
|
||||
|
||||
void leds_initRowSelector() {
|
||||
PIO pio = leds_pio;
|
||||
uint sm = pio_claim_unused_sm(pio, true);
|
||||
row_sm = sm;
|
||||
|
||||
uint offset = pio_add_program(pio, &leds_row_selector_program);
|
||||
|
||||
pio_sm_config config = leds_row_selector_program_get_default_config(offset);
|
||||
sm_config_set_clkdiv_int_frac(&config, 1, 0);
|
||||
|
||||
// Shift OSR to the right, autopull
|
||||
sm_config_set_out_shift(&config, true, true, 32);
|
||||
|
||||
// Set OUT and SET (data) pin, connect to pad, set as output
|
||||
sm_config_set_out_pins(&config, ROW_SER, 1);
|
||||
sm_config_set_set_pins(&config, ROW_SER, 1);
|
||||
pio_gpio_init(pio, ROW_SER);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, ROW_SER, 1, true);
|
||||
|
||||
// Set sideset (SRCLK) pin, connect to pad, set as output
|
||||
sm_config_set_sideset_pins(&config, ROW_SRCLK);
|
||||
pio_gpio_init(pio, ROW_SRCLK);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, ROW_SRCLK, 1, true);
|
||||
|
||||
// Load our configuration, and jump to the start of the program
|
||||
pio_sm_init(pio, sm, offset, &config);
|
||||
pio_sm_set_enabled(pio, sm, true);
|
||||
}
|
||||
|
||||
void leds_initDelay() {
|
||||
PIO pio = leds_pio;
|
||||
uint sm = pio_claim_unused_sm(pio, true);
|
||||
delay_sm = sm;
|
||||
|
||||
uint offset = pio_add_program(pio, &leds_delay_program);
|
||||
|
||||
pio_sm_config config = leds_delay_program_get_default_config(offset);
|
||||
sm_config_set_clkdiv_int_frac(&config, 1, 0);
|
||||
|
||||
// Shift OSR to the right, autopull
|
||||
sm_config_set_out_shift(&config, true, true, 32);
|
||||
|
||||
// Set sideset (OE) pin, connect to pad, set as output
|
||||
sm_config_set_sideset_pins(&config, ROW_OE);
|
||||
pio_gpio_init(pio, ROW_OE);
|
||||
pio_sm_set_consecutive_pindirs(pio, sm, ROW_OE, 1, true);
|
||||
|
||||
// Load our configuration, and jump to the start of the program
|
||||
pio_sm_init(pio, sm, offset, &config);
|
||||
pio_sm_set_enabled(pio, sm, true);
|
||||
}
|
||||
|
|
|
@ -22,18 +22,19 @@
|
|||
#define COL_MODULES 2
|
||||
#define COL_COUNT COL_MODULES * 20
|
||||
|
||||
#define COLOR_BITS 5
|
||||
#define COLOR_BITS 8
|
||||
#define FPS 30
|
||||
#define MS_PER_FRAME 1000 / FPS
|
||||
|
||||
#define CPU_MHZ 125
|
||||
#define NS_PER_CYCLE 1000 / CPU_MHZ
|
||||
|
||||
void leds_init();
|
||||
void leds_initRenderer();
|
||||
void leds_disable();
|
||||
void leds_loop();
|
||||
void leds_render();
|
||||
|
||||
extern uint8_t framebuffer[ROW_COUNT * COL_COUNT];
|
||||
extern bool ledBufferReady;
|
||||
extern uint32_t ledBuffer[8][ROW_COUNT * COL_MODULES];
|
||||
void leds_set_framebuffer(uint8_t *buffer);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,31 +1,85 @@
|
|||
.define public irq_did_latch 0
|
||||
.define public irq_delaying 1
|
||||
.define public irq_row_selected 1
|
||||
|
||||
; TODO: check if delays can be lowered with a PCB
|
||||
.define public srclk_0_delay 1
|
||||
.define public srclk_1_delay 2
|
||||
.define public rclk_1_delay 3
|
||||
|
||||
.program leds_px_pusher
|
||||
.side_set 1 opt
|
||||
public entry_point:
|
||||
entry_point:
|
||||
.wrap_target
|
||||
; get 32 bits from fifo (not required with autopull, useful for debug)
|
||||
; pull
|
||||
; push 24 bits to the shift registers
|
||||
; also, return latch bit to 0
|
||||
set x, 23 side 0
|
||||
set x, 23 side 0
|
||||
loop:
|
||||
; TODO: check if delays can be lowered with a PCB
|
||||
; set bit; lower clock edge
|
||||
out pins, 1 side 0 [1]
|
||||
out pins, 1 side 0 [srclk_0_delay]
|
||||
; loop; latch bit (rising edge)
|
||||
jmp x-- loop side 1 [2]
|
||||
jmp x-- loop side 1 [srclk_1_delay]
|
||||
end:
|
||||
; ignore unused bits
|
||||
; load MSBs into x
|
||||
; lower clock edge
|
||||
out x, 8 side 0
|
||||
out x, 8 side 0
|
||||
; MSB=1 indicates end of row
|
||||
jmp x-- end_of_row
|
||||
.wrap
|
||||
jmp !x entry_point
|
||||
end_of_row:
|
||||
; indicate to main processor that row was processed
|
||||
irq set 0
|
||||
; wait until previous row's delay is complete
|
||||
; (irq will be 0 on first row by default)
|
||||
wait 0 irq irq_delaying
|
||||
; wait until row is selected, and clear flag
|
||||
wait 1 irq irq_row_selected
|
||||
; signal that we're about to latch the row onto display
|
||||
irq set irq_did_latch
|
||||
; clock RCLK (latch onto register output stage)
|
||||
set pins, 1 [3]
|
||||
set pins, 1 [rclk_1_delay]
|
||||
set pins, 0
|
||||
; wait for next row
|
||||
jmp entry_point
|
||||
.wrap
|
||||
|
||||
.program leds_row_selector
|
||||
.side_set 1 opt
|
||||
entry_point:
|
||||
.wrap_target
|
||||
; LSB=1 indicates first (bottom) row, ergo, high SER for the first pulse
|
||||
out pins, 1 side 0 [srclk_0_delay]
|
||||
; The rest of the word indicates number of SRCLK pulses
|
||||
out x, 31
|
||||
loop:
|
||||
; pulse SRCLK x times
|
||||
; set SER=0 after first pulse
|
||||
nop side 1 [srclk_1_delay]
|
||||
set pins, 0 side 0 [srclk_0_delay]
|
||||
jmp x-- loop
|
||||
end:
|
||||
; signal that row is selected and wait until acknowledged
|
||||
; some delay so that in the worst case scenario we don't start changing rows before RCLK latch
|
||||
irq wait irq_row_selected [4]
|
||||
.wrap
|
||||
|
||||
.program leds_delay
|
||||
.side_set 1 opt
|
||||
.define public output_on 0
|
||||
.define public output_off 1
|
||||
entry_point:
|
||||
.wrap_target
|
||||
; begin only after data is latched
|
||||
wait 1 irq irq_did_latch
|
||||
; signal that delay is ongoing
|
||||
irq set irq_delaying
|
||||
; x = number of cycles to delay
|
||||
out x, 32
|
||||
loop:
|
||||
; busy loop for x cycles
|
||||
; enable output
|
||||
jmp x-- loop side output_on
|
||||
end:
|
||||
; signal that delay is complete
|
||||
; disable output
|
||||
irq clear irq_delaying side output_off
|
||||
.wrap
|
||||
|
|
|
@ -8,14 +8,19 @@
|
|||
#include "hardware/pio.h"
|
||||
#endif
|
||||
|
||||
#define irq_did_latch 0
|
||||
#define irq_delaying 1
|
||||
#define irq_row_selected 1
|
||||
#define srclk_0_delay 1
|
||||
#define srclk_1_delay 2
|
||||
#define rclk_1_delay 3
|
||||
|
||||
// -------------- //
|
||||
// leds_px_pusher //
|
||||
// -------------- //
|
||||
|
||||
#define leds_px_pusher_wrap_target 0
|
||||
#define leds_px_pusher_wrap 4
|
||||
|
||||
#define leds_px_pusher_offset_entry_point 0u
|
||||
#define leds_px_pusher_wrap 9
|
||||
|
||||
static const uint16_t leds_px_pusher_program_instructions[] = {
|
||||
// .wrap_target
|
||||
|
@ -23,18 +28,19 @@ static const uint16_t leds_px_pusher_program_instructions[] = {
|
|||
0x7101, // 1: out pins, 1 side 0 [1]
|
||||
0x1a41, // 2: jmp x--, 1 side 1 [2]
|
||||
0x7028, // 3: out x, 8 side 0
|
||||
0x0045, // 4: jmp x--, 5
|
||||
0x0020, // 4: jmp !x, 0
|
||||
0x2041, // 5: wait 0 irq, 1
|
||||
0x20c1, // 6: wait 1 irq, 1
|
||||
0xc000, // 7: irq nowait 0
|
||||
0xe301, // 8: set pins, 1 [3]
|
||||
0xe000, // 9: set pins, 0
|
||||
// .wrap
|
||||
0xc000, // 5: irq nowait 0
|
||||
0xe301, // 6: set pins, 1 [3]
|
||||
0xe000, // 7: set pins, 0
|
||||
0x0000, // 8: jmp 0
|
||||
};
|
||||
|
||||
#if !PICO_NO_HARDWARE
|
||||
static const struct pio_program leds_px_pusher_program = {
|
||||
.instructions = leds_px_pusher_program_instructions,
|
||||
.length = 9,
|
||||
.length = 10,
|
||||
.origin = -1,
|
||||
};
|
||||
|
||||
|
@ -46,3 +52,71 @@ static inline pio_sm_config leds_px_pusher_program_get_default_config(uint offse
|
|||
}
|
||||
#endif
|
||||
|
||||
// ----------------- //
|
||||
// leds_row_selector //
|
||||
// ----------------- //
|
||||
|
||||
#define leds_row_selector_wrap_target 0
|
||||
#define leds_row_selector_wrap 5
|
||||
|
||||
static const uint16_t leds_row_selector_program_instructions[] = {
|
||||
// .wrap_target
|
||||
0x7101, // 0: out pins, 1 side 0 [1]
|
||||
0x603f, // 1: out x, 31
|
||||
0xba42, // 2: nop side 1 [2]
|
||||
0xf100, // 3: set pins, 0 side 0 [1]
|
||||
0x0042, // 4: jmp x--, 2
|
||||
0xc421, // 5: irq wait 1 [4]
|
||||
// .wrap
|
||||
};
|
||||
|
||||
#if !PICO_NO_HARDWARE
|
||||
static const struct pio_program leds_row_selector_program = {
|
||||
.instructions = leds_row_selector_program_instructions,
|
||||
.length = 6,
|
||||
.origin = -1,
|
||||
};
|
||||
|
||||
static inline pio_sm_config leds_row_selector_program_get_default_config(uint offset) {
|
||||
pio_sm_config c = pio_get_default_sm_config();
|
||||
sm_config_set_wrap(&c, offset + leds_row_selector_wrap_target, offset + leds_row_selector_wrap);
|
||||
sm_config_set_sideset(&c, 2, true, false);
|
||||
return c;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------- //
|
||||
// leds_delay //
|
||||
// ---------- //
|
||||
|
||||
#define leds_delay_wrap_target 0
|
||||
#define leds_delay_wrap 4
|
||||
|
||||
#define leds_delay_output_on 0
|
||||
#define leds_delay_output_off 1
|
||||
|
||||
static const uint16_t leds_delay_program_instructions[] = {
|
||||
// .wrap_target
|
||||
0x20c0, // 0: wait 1 irq, 0
|
||||
0xc001, // 1: irq nowait 1
|
||||
0x6020, // 2: out x, 32
|
||||
0x1043, // 3: jmp x--, 3 side 0
|
||||
0xd841, // 4: irq clear 1 side 1
|
||||
// .wrap
|
||||
};
|
||||
|
||||
#if !PICO_NO_HARDWARE
|
||||
static const struct pio_program leds_delay_program = {
|
||||
.instructions = leds_delay_program_instructions,
|
||||
.length = 5,
|
||||
.origin = -1,
|
||||
};
|
||||
|
||||
static inline pio_sm_config leds_delay_program_get_default_config(uint offset) {
|
||||
pio_sm_config c = pio_get_default_sm_config();
|
||||
sm_config_set_wrap(&c, offset + leds_delay_wrap_target, offset + leds_delay_wrap);
|
||||
sm_config_set_sideset(&c, 2, true, false);
|
||||
return c;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
Loading…
Reference in New Issue