166 lines
9.5 KiB
C
166 lines
9.5 KiB
C
/* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
/** library to drive a WS2812B LED chain (code)
|
|
* @file led_ws2812b.c
|
|
* @author King Kévin <kingkevin@cuvoodoo.info>
|
|
* @date 2016
|
|
* @note peripherals used: SPI @ref led_ws2812b_spi, timer @ref led_ws2812b_timer, DMA @ref led_ws2812b_dma
|
|
*/
|
|
|
|
/* standard libraries */
|
|
#include <stdint.h> // standard integer types
|
|
#include <stdlib.h> // general utilities
|
|
|
|
/* STM32 (including CM3) libraries */
|
|
#include <libopencm3/stm32/rcc.h> // real-time control clock library
|
|
#include <libopencm3/stm32/gpio.h> // general purpose input output library
|
|
#include <libopencm3/stm32/spi.h> // SPI library
|
|
#include <libopencm3/stm32/timer.h> // timer library
|
|
#include <libopencm3/stm32/dma.h> // DMA library
|
|
#include <libopencm3/cm3/nvic.h> // interrupt handler
|
|
#include <libopencmsis/core_cm3.h> // Cortex M3 utilities
|
|
|
|
#include "led_ws2812b.h" // LED WS2812B library API
|
|
#include "global.h" // common methods
|
|
|
|
/** 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(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, (uint32_t)led_ws2812b_data);
|
|
dma_set_number_of_data(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, LENGTH(led_ws2812b_data)); // set the size of the data to transmit
|
|
dma_enable_transfer_complete_interrupt(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // warm when transfer is complete to stop transmission
|
|
dma_enable_channel(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // enable DMA channel
|
|
|
|
spi_enable_tx_dma(LED_WS2812B_SPI); // use DMA to provide data stream to be transfered
|
|
|
|
timer_set_counter(LED_WS2812B_TIMER, 0); // reset timer counter fro clean clock
|
|
timer_enable_counter(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(LED_WS2812B_CLK_RCC); // enable clock for GPIO peripheral
|
|
gpio_set_mode(LED_WS2812B_CLK_PORT, GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, LED_WS2812B_CLK_PIN); // set pin as output
|
|
rcc_periph_clock_enable(RCC_AFIO); // enable clock for alternate function (PWM)
|
|
rcc_periph_clock_enable(LED_WS2812B_TIMER_RCC); // enable clock for timer peripheral
|
|
timer_reset(LED_WS2812B_TIMER); // reset timer state
|
|
timer_set_mode(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(LED_WS2812B_TIMER, 0); // no prescaler to keep most precise timer (72MHz/2^16=1099<800kHz)
|
|
timer_set_period(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(LED_WS2812B_TIMER, LED_WS2812B_TIMER_OC, rcc_ahb_frequency/800000/3/2); // duty cycle to 50%
|
|
timer_set_oc_mode(LED_WS2812B_TIMER, LED_WS2812B_TIMER_OC, TIM_OCM_PWM1); // set timer to generate PWM (used as clock)
|
|
timer_enable_oc_output(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(LED_WS2812B_SPI_PORT_RCC); // enable clock for SPI IO peripheral
|
|
gpio_set_mode(LED_WS2812B_SPI_PORT, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, LED_WS2812B_SPI_CLK); // set clock as input
|
|
gpio_set_mode(LED_WS2812B_SPI_PORT, GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, LED_WS2812B_SPI_DOUT); // set MISO as output
|
|
rcc_periph_clock_enable(RCC_AFIO); // enable clock for SPI alternate function
|
|
rcc_periph_clock_enable(LED_WS2812B_SPI_RCC); // enable clock for SPI peripheral
|
|
spi_reset(LED_WS2812B_SPI); // clear SPI values to default
|
|
spi_set_slave_mode(LED_WS2812B_SPI); // set SPI as slave (since we use the clock as input)
|
|
spi_set_bidirectional_transmit_only_mode(LED_WS2812B_SPI); // we won't receive data
|
|
spi_set_unidirectional_mode(LED_WS2812B_SPI); // we only need to transmit data
|
|
spi_set_dff_8bit(LED_WS2812B_SPI); // use 8 bits for simpler encoding (but there will be more interrupts)
|
|
spi_set_clock_polarity_1(LED_WS2812B_SPI); // clock is high when idle
|
|
spi_set_clock_phase_1(LED_WS2812B_SPI); // output data on second edge (rising)
|
|
spi_send_msb_first(LED_WS2812B_SPI); // send least significant bit first
|
|
spi_enable_software_slave_management(LED_WS2812B_SPI); // control the slave select in software (since there is no master)
|
|
spi_set_nss_low(LED_WS2812B_SPI); // set NSS low so we can output
|
|
spi_enable(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(LED_WS2812B_DMA_RCC); // enable clock for DMA peripheral
|
|
dma_channel_reset(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // start with fresh channel configuration
|
|
dma_set_memory_address(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, (uint32_t)led_ws2812b_data); // set bit pattern as source address
|
|
dma_set_peripheral_address(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, (uint32_t)&LED_WS2812B_SPI_DR); // set SPI as peripheral destination address
|
|
dma_set_read_from_memory(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // set direction from memory to peripheral
|
|
dma_enable_memory_increment_mode(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // go through bit pattern
|
|
dma_set_memory_size(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, DMA_CCR_MSIZE_8BIT); // read 8 bits from memory
|
|
dma_set_peripheral_size(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, DMA_CCR_PSIZE_8BIT); // write 8 bits to peripheral
|
|
dma_set_priority(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, DMA_CCR_PL_HIGH); // set priority to high since time is crucial for the peripheral
|
|
nvic_enable_irq(LED_WS2812B_DMA_IRQ); // 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 LED_WS2812B_DMA_ISR(void)
|
|
{
|
|
if (dma_get_interrupt_flag(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, DMA_TCIF)) { // transfer completed
|
|
dma_clear_interrupt_flags(LED_WS2812B_DMA, LED_WS2812B_DMA_CH, DMA_TCIF); // clear flag
|
|
dma_disable_transfer_complete_interrupt(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // stop warning transfer completed
|
|
spi_disable_tx_dma(LED_WS2812B_SPI); // stop SPI asking for more data
|
|
while (SPI_SR(LED_WS2812B_SPI) & SPI_SR_BSY); // wait for data to be shifted out
|
|
timer_disable_counter(LED_WS2812B_TIMER); // stop clock
|
|
dma_disable_channel(LED_WS2812B_DMA, LED_WS2812B_DMA_CH); // stop using DMA
|
|
transmit_flag = false; // transmission completed
|
|
}
|
|
}
|