/** control RGB LED matrix panels through shift registers * @file * @author King Kévin * @copyright SPDX-License-Identifier: GPL-3.0-or-later * @date 2022 */ /* standard libraries */ #include // standard integer types #include // general utilities /* STM32 (including CM3) libraries */ #include // Cortex M3 utilities #include // real-time control clock library #include // general purpose input output library #include // DMA library #include // timer library #include // interrupt handler #include "led_rgbpanel.h" // library API #include "global.h" // common methods // RGB matrix pins (warning: hard coded) #define RGBPANEL_OE_PIN PB10 /**< pin to enable output (active low) */ #define RGBPANEL_A_PIN PB0 /**< pin to select line */ #define RGBPANEL_B_PIN PB1 /**< pin to select line */ #define RGBPANEL_C_PIN PB2 /**< pin to select line */ #define RGBPANEL_D_PIN PB3 /**< pin to select line */ #define RGBPANEL_CLK_PIN PA0 /**< pin to generate clock for serial data */ #define RGBPANEL_LAT_PIN PA1 /**< pin to latch data on rising edge */ #define RGBPANEL_R1_PIN PA2 /**< pin to enable red color on top half */ #define RGBPANEL_G1_PIN PA3 /**< pin to enable green color on top half */ #define RGBPANEL_B1_PIN PA4 /**< pin to enable blue color on top half */ #define RGBPANEL_R2_PIN PA5 /**< pin to enable red color on bottom half */ #define RGBPANEL_G2_PIN PA6 /**< pin to enable green color on bottom half */ #define RGBPANEL_B2_PIN PA7 /**< pin to enable blue color on bottom half */ #define RGBPANEL_DMA DMA2 /**< DMA used to send data to the RGB matrix (only DMA2 can be used for memory-to-memory transfer) */ #define RGBPANEL_RCC_DMA RCC_DMA2 /**< RCC for DMA used for the RGB matrix */ #define RGBPANEL_STREAM DMA_STREAM1 /**< stream used to send data to the RGB matrix (any stream can be used for memory-to-memory transfer) */ #define RGBPANEL_CHANNEL DMA_SxCR_CHSEL_0 /**< channel used to send data to the RGB matrix (any channel can be used for memory-to-memory transfer) */ #define RGBPANEL_IRQ NVIC_DMA2_STREAM1_IRQ /**< IRQ for when a line transfer is complete */ #define RGBPANEL_ISR dma2_stream1_isr /**< ISR for when a line transfer is complete */ #define RGBPANEL_TIMER 3 /**< timer to update lines */ static uint8_t rgbpanel_data[RGBPANEL_HEIGHT / 2][RGBPANEL_WIDTH * 2]; /**< data to be sent to RGB matrix (one byte includes upper and lower half values, each byte has 2 clock edges) */ static volatile uint8_t rgbpanel_line = 0; // line being transferred void rgbpanel_setup(void) { // configure pin for output enable rcc_periph_clock_enable(GPIO_RCC(RGBPANEL_OE_PIN)); // enable clock for GPIO port peripheral gpio_set(GPIO_PORT(RGBPANEL_OE_PIN), GPIO_PIN(RGBPANEL_OE_PIN)); // disable output gpio_set_output_options(GPIO_PORT(RGBPANEL_OE_PIN), GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN(RGBPANEL_OE_PIN)); // set fast edge gpio_mode_setup(GPIO_PORT(RGBPANEL_OE_PIN), GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_OE_PIN)); // set pin as output // configure pins for data and clock lines const uint32_t rgbpanel_serial_port = GPIO_PORT(RGBPANEL_LAT_PIN); // common port for pins controlling the serial data const uint16_t rgbpanel_serial_pins = GPIO_PIN(RGBPANEL_LAT_PIN) | GPIO_PIN(RGBPANEL_CLK_PIN) | GPIO_PIN(RGBPANEL_R1_PIN) | GPIO_PIN(RGBPANEL_G1_PIN) | GPIO_PIN(RGBPANEL_B1_PIN) | GPIO_PIN(RGBPANEL_R2_PIN) | GPIO_PIN(RGBPANEL_G2_PIN) | GPIO_PIN(RGBPANEL_B2_PIN); // pins controlling the serial data rcc_periph_clock_enable(GPIO_RCC(RGBPANEL_LAT_PIN)); // enable clock for GPIO port peripheral gpio_clear(rgbpanel_serial_port, rgbpanel_serial_pins); // disable LEDs gpio_set_output_options(rgbpanel_serial_port, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, rgbpanel_serial_pins); // set fast edge gpio_mode_setup(rgbpanel_serial_port, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, rgbpanel_serial_pins); // set pin as output // configure pins for address lines rcc_periph_clock_enable(RCC_GPIOB); // enable clock for GPIO port peripheral gpio_clear(RCC_GPIOB, GPIO0 | GPIO1 | GPIO2 | GPIO3); // unselect line gpio_set_output_options(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO0 | GPIO1 | GPIO2 | GPIO3); // set fast edge gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO0 | GPIO1 | GPIO2 | GPIO3); // set pin as output // configure DMA to sent line data // because there is no peripheral request for data, this is a memory to memory transfer rcc_periph_clock_enable(RGBPANEL_RCC_DMA); // enable clock for DMA peripheral (any DMA and channel can be used) dma_disable_stream(RGBPANEL_DMA, RGBPANEL_STREAM); // disable stream before re-configuring while (DMA_SCR(RGBPANEL_DMA, RGBPANEL_STREAM) & DMA_SxCR_EN); // wait until transfer is finished before we can reconfigure dma_stream_reset(RGBPANEL_DMA, RGBPANEL_STREAM); // use default values dma_set_peripheral_size(RGBPANEL_DMA, RGBPANEL_STREAM, DMA_SxCR_PSIZE_8BIT); // we only write the 8 first bit dma_enable_peripheral_increment_mode(RGBPANEL_DMA, RGBPANEL_STREAM); // increment address of memory to read dma_set_memory_address(RGBPANEL_DMA, RGBPANEL_STREAM, (uint32_t) &GPIOA_ODR); // set GPIOA as destination (for memory-to-memory transfer, the destination is the memory) dma_set_memory_size(RGBPANEL_DMA, RGBPANEL_STREAM, DMA_SxCR_MSIZE_8BIT); // read 8 bits for transfer dma_disable_memory_increment_mode(RGBPANEL_DMA, RGBPANEL_STREAM); // don't increment GPIO address dma_set_number_of_data(RGBPANEL_DMA, RGBPANEL_STREAM, LENGTH(rgbpanel_data[0])); // set transfer size (one line) dma_channel_select(RGBPANEL_DMA, RGBPANEL_STREAM, RGBPANEL_CHANNEL); // set the channel for this stream dma_set_transfer_mode(RGBPANEL_DMA, RGBPANEL_STREAM, DMA_SxCR_DIR_MEM_TO_MEM); // set transfer from memory to memory dma_set_priority(RGBPANEL_DMA, RGBPANEL_STREAM, DMA_SxCR_PL_LOW); // there is no need to rush dma_enable_transfer_complete_interrupt(RGBPANEL_DMA, RGBPANEL_STREAM); // interrupt when line transfer is complete nvic_enable_irq(RGBPANEL_IRQ); // enable interrupt rgbpanel_clear(); // clear matrix gpio_clear(GPIO_PORT(RGBPANEL_OE_PIN), GPIO_PIN(RGBPANEL_OE_PIN)); // enable output // configure timer to go through rows/lines rcc_periph_clock_enable(RCC_TIM(RGBPANEL_TIMER)); // enable clock for timer domain rcc_periph_reset_pulse(RST_TIM(RGBPANEL_TIMER)); // reset timer state timer_set_mode(TIM(RGBPANEL_TIMER), TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); // set timer mode, use undivided timer clock, edge alignment (simple count), and count up timer_set_prescaler(TIM(RGBPANEL_TIMER), 2 - 1); // hand tuned prescale to minimize inter-line ghosting timer_set_period(TIM(RGBPANEL_TIMER), 0x9fff - 1); // hand tuned period to minimize inter-line ghosting timer_clear_flag(TIM(RGBPANEL_TIMER), TIM_SR_UIF); // clear update (overflow) flag timer_update_on_overflow(TIM(RGBPANEL_TIMER)); // only use counter overflow as UEV source (use overflow as next line update indication) timer_enable_irq(TIM(RGBPANEL_TIMER), TIM_DIER_UIE); // enable update interrupt for timer nvic_enable_irq(NVIC_TIM_IRQ(RGBPANEL_TIMER)); // catch interrupt in service routine timer_enable_counter(TIM(RGBPANEL_TIMER)); // start timer to update RGB matrix } void rgbpanel_release(void) { // release timer timer_disable_counter(TIM(RGBPANEL_TIMER)); // stop timer nvic_disable_irq(NVIC_TIM_IRQ(RGBPANEL_TIMER)); // disable interrupt rcc_periph_reset_pulse(RST_TIM(RGBPANEL_TIMER)); // reset timer state // release DMA (it will stop onve completed while (DMA_SCR(RGBPANEL_DMA, RGBPANEL_STREAM) & DMA_SxCR_EN); // wait until DMA is complete dma_disable_stream(RGBPANEL_DMA, RGBPANEL_STREAM); // disable stream dma_stream_reset(RGBPANEL_DMA, RGBPANEL_STREAM); // use default values nvic_disable_irq(RGBPANEL_IRQ); // disable interrupt // release GPIO gpio_mode_setup(GPIO_PORT(RGBPANEL_OE_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_OE_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_A_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_A_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_B_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_B_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_C_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_C_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_D_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_D_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_CLK_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_CLK_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_LAT_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_LAT_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_R1_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_R1_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_G1_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_G1_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_B1_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_B1_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_R2_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_R2_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_G2_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_G2_PIN)); gpio_mode_setup(GPIO_PORT(RGBPANEL_B2_PIN), GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN(RGBPANEL_B2_PIN)); } void rgbpanel_clear(void) { for (uint8_t i = 0; i < LENGTH(rgbpanel_data); i++) { // for each line for (uint8_t j = 0; j < LENGTH(rgbpanel_data[0]); j += 2) { // for each clock cycle rgbpanel_data[i][j + 0] = 0; // create clock falling edge rgbpanel_data[i][j + 1] = 1; // create clock rising edge } rgbpanel_data[i][LENGTH(rgbpanel_data[0]) - 1] |= (1 << 1); // latch data (next line will remove the latch) } } void rgbpanel_set(int16_t x, int16_t y, bool r, bool g, bool b) { if (x < 0 || x >= RGBPANEL_WIDTH) { return; } if (y < 0 || y >= RGBPANEL_HEIGHT) { return; } const uint8_t row = y % (RGBPANEL_HEIGHT / 2); // get the actual line/row const uint8_t col = x * 2; // get the actual column uint8_t data = 0; // there we will set the color bits if (y < (RGBPANEL_HEIGHT / 2)) { data = rgbpanel_data[row][col] & 0xe0; // keep lower line colors if (r) { data |= (1 << 2); } if (g) { data |= (1 << 3); } if (b) { data |= (1 << 4); } } else { data = rgbpanel_data[row][col] & 0x1c; // keep upper line colors if (r) { data |= (1 << 5); } if (g) { data |= (1 << 6); } if (b) { data |= (1 << 7); } } // set data on low edge rgbpanel_data[row][col + 0] &= 0x3; // clear color data (don't touch clock and latch data) rgbpanel_data[row][col + 0] |= data; // set the LED data // set data on high edge rgbpanel_data[row][col + 1] &= 0x3; // clear color data (don't touch clock and latch data) rgbpanel_data[row][col + 1] |= data; // set the LED data on clock high edge } /** ISR triggered when the data for the line of the RGB matrix has been transferred * we switch line just after the data is latched, to reduce ghosting */ void RGBPANEL_ISR(void) { if (dma_get_interrupt_flag(RGBPANEL_DMA, RGBPANEL_STREAM, DMA_TCIF)) { dma_clear_interrupt_flags(RGBPANEL_DMA, RGBPANEL_STREAM, DMA_TCIF); GPIOB_ODR = (GPIOB_ODR & 0xfff0) | (rgbpanel_line & 0xf); // select line (line on lower and upper half are updated at once) rgbpanel_line = (rgbpanel_line + 1) % (RGBPANEL_HEIGHT / 2); // go to next line (two lines are updated at once) } } /** interrupt service routine called to update next line of RGB matrix * @note ideally the next line should be updated when the current one is complete, but the DMA is too fast. * @note switching lines too fast causes inter-line ghosting of the LEDs (on the same column), due to capacitance and driver switching limitations */ void TIM_ISR(RGBPANEL_TIMER)(void) { if (timer_get_flag(TIM(RGBPANEL_TIMER), TIM_SR_UIF)) { // update event happened timer_clear_flag(TIM(RGBPANEL_TIMER), TIM_SR_UIF); // clear flag if (DMA_SCR(RGBPANEL_DMA, RGBPANEL_STREAM) & DMA_SxCR_EN) { // DMA is not complete return; } dma_set_peripheral_address(RGBPANEL_DMA, RGBPANEL_STREAM, (uint32_t)&rgbpanel_data[rgbpanel_line]); // set memory containing line data to be transferred (for memory-to-memory transfer, the source is the peripheral) dma_enable_stream(RGBPANEL_DMA, RGBPANEL_STREAM); // start sending next line } }