/* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ /** library to drive a WS2812B LED chain * @file * @author King Kévin * @date 2016-2020 * @note peripherals used: SPI @ref led_ws2812b_spi, timer @ref led_ws2812b_timer, DMA (for SPI MISO) */ /* 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 // SPI library #include // timer library #include // DMA library #include // interrupt handler #include "led_ws2812b.h" // LED WS2812B library API #include "global.h" // common methods /** peripheral configuration */ /** @defgroup led_ws2812b_spi SPI peripheral used to control the WS2812B LEDs * @{ */ #define LED_WS2812B_SPI 1 /**< SPI peripheral */ /** @} */ /** @defgroup led_ws2812b_timer timer peripheral used to generate SPI clock * @{ */ #define LED_WS2812B_TIMER 3 /**< timer peripheral */ #define LED_WS2812B_CLK_CH 3 /**< timer channel to output PWM (PB0), connect to SPI clock input */ #define LED_WS2812B_TIMER_OC TIM_OC3 /**< timer output compare used to set PWM frequency */ /** @} */ /** bit template to encode one byte to be shifted out by SPI to the WS2812B LEDs * @details For each WS2812B bit which needs to be transfered we require to transfer 3 SPI bits. * The first SPI bit is the high start of the WS2812B bit frame. * The second SPI bit determines if the WS2812B bit is a 0 or 1. * The third SPI bit is the last part of the WS2812B bit frame, which is always low. * The binary pattern is 0b100100100100100100100100 */ #define LED_WS2812B_SPI_TEMPLATE 0x924924 uint8_t led_ws2812b_data[LED_WS2812B_LEDS * 3 * 3 + 40 * 3 / 8 + 1] = {0}; /**< data encoded to be shifted out by SPI for the WS2812B, plus the 50us reset (~40 data bits) */ static volatile bool transmit_flag = false; /**< flag set in software when transmission started, clear by interrupt when transmission completed */ void led_ws2812b_set_rgb(uint16_t led, uint8_t red, uint8_t green, uint8_t blue) { // verify the led exists if (led >= LED_WS2812B_LEDS) { return; } // wait for transmission to complete before changing the color while (transmit_flag) { __WFI(); } const uint8_t colors[] = {green, red, blue}; // color order for the WS2812B const uint8_t pattern_bit[] = {0x02, 0x10, 0x80, 0x04, 0x20, 0x01, 0x08, 0x40}; // which bit to change in the pattern const uint8_t pattern_byte[] = {2, 2, 2, 1, 1, 0, 0, 0}; // in which byte in the pattern to write the pattern bit for (uint8_t color = 0; color < LENGTH(colors); color++) { // colors are encoded similarly // fill the middle bit (fixed is faster than calculating it) for (uint8_t bit = 0; bit < 8; bit++) { // bit from the color to set/clear if (colors[color] & (1 << bit)) { // setting bit led_ws2812b_data[led * 3 * 3 + color * 3 + pattern_byte[bit]] |= pattern_bit[bit]; // setting bit is pattern } else { // clear bit led_ws2812b_data[led * 3 * 3 + color * 3 + pattern_byte[bit]] &= ~pattern_bit[bit]; // clearing bit is pattern } } } } bool led_ws2812b_transmit(void) { if (transmit_flag) { // a transmission is already ongoing return false; } transmit_flag = true; // remember transmission started dma_set_memory_address(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), (uint32_t)led_ws2812b_data); dma_set_number_of_data(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), LENGTH(led_ws2812b_data)); // set the size of the data to transmit dma_enable_transfer_complete_interrupt(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // warm when transfer is complete to stop transmission dma_enable_channel(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // enable DMA channel spi_enable_tx_dma(SPI(LED_WS2812B_SPI)); // use DMA to provide data stream to be transfered timer_set_counter(TIM(LED_WS2812B_TIMER), 0); // reset timer counter fro clean clock timer_enable_counter(TIM(LED_WS2812B_TIMER)); // start timer to generate clock return true; } void led_ws2812b_setup(void) { // setup timer to generate clock of (using PWM): 800kHz*3 rcc_periph_clock_enable(RCC_TIM_CH(LED_WS2812B_TIMER, LED_WS2812B_CLK_CH)); // enable clock for GPIO peripheral gpio_set_mode(TIM_CH_PORT(LED_WS2812B_TIMER, LED_WS2812B_CLK_CH), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, TIM_CH_PIN(LED_WS2812B_TIMER, LED_WS2812B_CLK_CH)); // set pin as output rcc_periph_clock_enable(RCC_AFIO); // enable clock for alternate function (PWM) rcc_periph_clock_enable(RCC_TIM(LED_WS2812B_TIMER)); // enable clock for timer peripheral rcc_periph_reset_pulse(RST_TIM(LED_WS2812B_TIMER)); // reset timer state timer_set_mode(TIM(LED_WS2812B_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(LED_WS2812B_TIMER), 0); // no prescaler to keep most precise timer (72MHz/2^16=1099<800kHz) timer_set_period(TIM(LED_WS2812B_TIMER), rcc_ahb_frequency/800000/3-1); // set the clock frequency to 800kHz*3bit since we need to send 3 bits to output a 800kbps stream timer_set_oc_value(TIM(LED_WS2812B_TIMER), LED_WS2812B_TIMER_OC, rcc_ahb_frequency/800000/3/2); // duty cycle to 50% timer_set_oc_mode(TIM(LED_WS2812B_TIMER), LED_WS2812B_TIMER_OC, TIM_OCM_PWM1); // set timer to generate PWM (used as clock) timer_enable_oc_output(TIM(LED_WS2812B_TIMER), LED_WS2812B_TIMER_OC); // enable output to generate the clock // setup SPI to transmit data (we are slave and the clock comes from the above PWM): 3 SPI bits for 1 WS2812B bit rcc_periph_clock_enable(RCC_SPI_SCK_PORT(LED_WS2812B_SPI)); // enable clock for SPI IO peripheral gpio_set_mode(SPI_SCK_PORT(LED_WS2812B_SPI), GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, SPI_SCK_PIN(LED_WS2812B_SPI)); // set clock as input rcc_periph_clock_enable(RCC_SPI_MISO_PORT(LED_WS2812B_SPI)); // enable clock for SPI IO peripheral gpio_set_mode(SPI_MISO_PORT(LED_WS2812B_SPI), GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, SPI_MISO_PIN(LED_WS2812B_SPI)); // set MISO as output rcc_periph_clock_enable(RCC_AFIO); // enable clock for SPI alternate function rcc_periph_clock_enable(RCC_SPI(LED_WS2812B_SPI)); // enable clock for SPI peripheral spi_reset(SPI(LED_WS2812B_SPI)); // clear SPI values to default spi_set_slave_mode(SPI(LED_WS2812B_SPI)); // set SPI as slave (since we use the clock as input) spi_set_bidirectional_transmit_only_mode(SPI(LED_WS2812B_SPI)); // we won't receive data spi_set_unidirectional_mode(SPI(LED_WS2812B_SPI)); // we only need to transmit data spi_set_dff_8bit(SPI(LED_WS2812B_SPI)); // use 8 bits for simpler encoding (but there will be more interrupts) spi_set_clock_polarity_1(SPI(LED_WS2812B_SPI)); // clock is high when idle spi_set_clock_phase_1(SPI(LED_WS2812B_SPI)); // output data on second edge (rising) spi_send_msb_first(SPI(LED_WS2812B_SPI)); // send least significant bit first spi_enable_software_slave_management(SPI(LED_WS2812B_SPI)); // control the slave select in software (since there is no master) spi_set_nss_low(SPI(LED_WS2812B_SPI)); // set NSS low so we can output spi_enable(SPI(LED_WS2812B_SPI)); // enable SPI // do not disable SPI or set NSS high since it will put MISO high, breaking the beginning of the next transmission // configure DMA to provide the pattern to be shifted out from SPI to the WS2812B LEDs rcc_periph_clock_enable(RCC_DMA_SPI(LED_WS2812B_SPI)); // enable clock for DMA peripheral dma_channel_reset(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // start with fresh channel configuration dma_set_memory_address(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), (uint32_t)led_ws2812b_data); // set bit pattern as source address dma_set_peripheral_address(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), (uint32_t)&SPI_DR(SPI(LED_WS2812B_SPI))); // set SPI as peripheral destination address dma_set_read_from_memory(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // set direction from memory to peripheral dma_enable_memory_increment_mode(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // go through bit pattern dma_set_memory_size(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), DMA_CCR_MSIZE_8BIT); // read 8 bits from memory dma_set_peripheral_size(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), DMA_CCR_PSIZE_8BIT); // write 8 bits to peripheral dma_set_priority(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), DMA_CCR_PL_HIGH); // set priority to high since time is crucial for the peripheral nvic_enable_irq(DMA_IRQ_SPI_TX(LED_WS2812B_SPI)); // enable interrupts for this DMA channel // fill buffer with bit pattern for (uint16_t i = 0; i < LED_WS2812B_LEDS * 3; i++) { led_ws2812b_data[i * 3 + 0] = (uint8_t)(LED_WS2812B_SPI_TEMPLATE >> 16); led_ws2812b_data[i * 3 + 1] = (uint8_t)(LED_WS2812B_SPI_TEMPLATE >> 8); led_ws2812b_data[i * 3 + 2] = (uint8_t)(LED_WS2812B_SPI_TEMPLATE >> 0); } // fill remaining with with 0 to encode the reset code for (uint16_t i = LED_WS2812B_LEDS * 3 * 3; i < LENGTH(led_ws2812b_data); i++) { led_ws2812b_data[i] = 0; } led_ws2812b_transmit(); // set LEDs } /** DMA interrupt service routine to stop transmission after it finished */ void DMA_ISR_SPI_TX(LED_WS2812B_SPI)(void) { if (dma_get_interrupt_flag(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), DMA_TCIF)) { // transfer completed dma_clear_interrupt_flags(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI), DMA_TCIF); // clear flag dma_disable_transfer_complete_interrupt(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // stop warning transfer completed spi_disable_tx_dma(SPI(LED_WS2812B_SPI)); // stop SPI asking for more data while (SPI_SR(SPI(LED_WS2812B_SPI)) & SPI_SR_BSY); // wait for data to be shifted out timer_disable_counter(TIM(LED_WS2812B_TIMER)); // stop clock dma_disable_channel(DMA_SPI(LED_WS2812B_SPI), DMA_CHANNEL_SPI_TX(LED_WS2812B_SPI)); // stop using DMA transmit_flag = false; // transmission completed } }