led_clock-old/lib/led_ws2812b.c

189 lines
10 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/>.
*
*/
/** @brief 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" // global definitions
/** peripheral configuration */
/** @defgroup led_ws2812b_spi SPI peripheral used to control the WS2812b LEDs
* @{
*/
#define WS2812B_SPI SPI1 /**< SPI peripheral */
#define WS2812B_SPI_DR SPI1_DR /**< SPI data register for the DMA */
#define WS2812B_SPI_RCC RCC_SPI1 /**< SPI peripheral clock */
#define WS2812B_SPI_PORT GPIOA /**< SPI port */
#define WS2812B_SPI_CLK GPIO_SPI1_SCK /**< SPI clock pin (PA5), connect to PWM output */
#define WS2812B_SPI_DOUT GPIO_SPI1_MISO /**< SPI data pin (PA6), connect to WS2812b DIN */
/** @} */
/** @defgroup led_ws2812b_timer timer peripheral used to generate SPI clock
* @{
*/
#define WS2812B_TIMER TIM3 /**< timer peripheral */
#define WS2812B_TIMER_RCC RCC_TIM3 /**< timer peripheral clock */
#define WS2812B_TIMER_OC TIM_OC3 /**< timer output compare used to set PWM frequency */
#define WS2812B_CLK_RCC RCC_GPIOB /**< timer port peripheral clock */
#define WS2812B_CLK_PORT GPIOB /**< timer port */
#define WS2812B_CLK_PIN GPIO_TIM3_CH3 /**< timer pin to output PWM (PB0), connect to SPI clock input */
/** @} */
/** @defgroup led_ws2812b_dma DMA peripheral used to send the data
* @{
*/
#define WS2812B_DMA DMA1 /**< DMA peripheral to put data for WS2812b LED in SPI queue (only DMA1 supports SPI1_TX interrupt) */
#define WS2812B_DMA_RCC RCC_DMA1 /**< DMA peripheral clock */
#define WS2812B_DMA_CH DMA_CHANNEL3 /**< DMA channel (only DMA1 channel 3 supports SPI1_TX interrupt) */
#define WS2812B_DMA_IRQ NVIC_DMA1_CHANNEL3_IRQ /**< DMA channel interrupt signal */
#define WS2812B_DMA_ISR dma1_channel3_isr /**< DMA channel interrupt service routine */
/** @} */
/** @brief 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.
* Only the first 24 bits (3*8) are used.
* The binary pattern is 0b10010010010010010010010000000000
*/
#define WS2812B_SPI_TEMPLATE 0x92492400
uint8_t ws2812b_data[WS2812B_LEDS*3*3+40*3/8] = {0}; /**< data encoded to be shifted out by SPI for the WS2812b, pulse the 50us reset */
static volatile bool transmit_flag = false; // is transmission ongoing
void ws2812b_set_rgb(uint16_t led, uint8_t red, uint8_t green, uint8_t blue)
{
// verify the led exists
if (led>=WS2812B_LEDS) {
return;
}
// wait for transmission to complete before changing the color
while (transmit_flag) {
__WFI();
}
uint8_t colors[] = {green, red, blue};
// generate the pattern
for (uint8_t color=0; color<LENGTH(colors); color++) {
uint32_t bits_color = WS2812B_SPI_TEMPLATE; // template to encode the bits in
for (uint8_t bit=0; bit<8; bit++) {
bits_color |= ((colors[color]>>bit)&0x1)<<(bit*3+9); // encode the data bits in the pattern
}
// store pattern
ws2812b_data[led*3*3+color*3+0] = (bits_color>>24);
ws2812b_data[led*3*3+color*3+1] = (bits_color>>16);
ws2812b_data[led*3*3+color*3+2] = (bits_color>>8);
}
}
void ws2812b_transmit(void)
{
while (transmit_flag) { // wait for previous transmission to complete
__WFI();
}
transmit_flag = true; // remember transmission started
dma_set_memory_address(WS2812B_DMA, WS2812B_DMA_CH, (uint32_t)ws2812b_data);
dma_set_number_of_data(WS2812B_DMA, WS2812B_DMA_CH, LENGTH(ws2812b_data)); // set the size of the data to transmit
dma_enable_transfer_complete_interrupt(WS2812B_DMA, WS2812B_DMA_CH); // warm when transfer is complete to stop transmission
dma_enable_channel(WS2812B_DMA, WS2812B_DMA_CH); // enable DMA channel
spi_enable_tx_dma(WS2812B_SPI); // use DMA to provide data stream to be transfered
timer_set_counter(WS2812B_TIMER, 0); // reset timer counter fro clean clock
timer_enable_counter(WS2812B_TIMER); // start timer to generate clock
}
void ws2812b_setup(void)
{
/* setup timer to generate clock of (using PWM): 800kHz*3 */
rcc_periph_clock_enable(WS2812B_CLK_RCC); // enable clock for GPIO peripheral
gpio_set_mode(WS2812B_CLK_PORT, GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, WS2812B_CLK_PIN); // set pin a output
rcc_periph_clock_enable(RCC_AFIO); // enable clock for alternate function (PWM)
rcc_periph_clock_enable(WS2812B_TIMER_RCC); // enable clock for timer peripheral
timer_reset(WS2812B_TIMER); // reset timer state
timer_set_mode(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(WS2812B_TIMER, 0); // no prescaler to keep most precise timer (72MHz/2^16=1099<800kHz)
timer_set_period(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(WS2812B_TIMER, WS2812B_TIMER_OC, rcc_ahb_frequency/800000/3/2); // duty cycle to 50%
timer_set_oc_mode(WS2812B_TIMER, WS2812B_TIMER_OC, TIM_OCM_PWM1); // set timer to generate PWM (used as clock)
timer_enable_oc_output(WS2812B_TIMER, 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(WS2812B_SPI_RCC); // enable clock for SPI peripheral
gpio_set_mode(WS2812B_SPI_PORT, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, WS2812B_SPI_CLK); // set clock as input
gpio_set_mode(WS2812B_SPI_PORT, GPIO_MODE_OUTPUT_10_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, WS2812B_SPI_DOUT); // set MISO as output
spi_reset(WS2812B_SPI); // clear SPI values to default
spi_set_slave_mode(WS2812B_SPI); // set SPI as slave (since we use the clock as input)
spi_set_bidirectional_transmit_only_mode(WS2812B_SPI); // we won't receive data
spi_set_unidirectional_mode(WS2812B_SPI); // we only need to transmit data
spi_set_dff_8bit(WS2812B_SPI); // use 8 bits for simpler encoding (but there will be more interrupts)
spi_set_clock_polarity_1(WS2812B_SPI); // clock is high when idle
spi_set_clock_phase_1(WS2812B_SPI); // output data on second edge (rising)
spi_send_msb_first(WS2812B_SPI); // send least significant bit first
spi_enable_software_slave_management(WS2812B_SPI); // control the slave select in software (since there is no master)
spi_set_nss_low(WS2812B_SPI); // set NSS low so we can output
spi_enable(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(WS2812B_DMA_RCC); // enable clock for DMA peripheral
dma_channel_reset(WS2812B_DMA, WS2812B_DMA_CH); // start with fresh channel configuration
dma_set_memory_address(WS2812B_DMA, WS2812B_DMA_CH, (uint32_t)ws2812b_data); // set bit pattern as source address
dma_set_peripheral_address(WS2812B_DMA, WS2812B_DMA_CH, (uint32_t)&WS2812B_SPI_DR); // set SPI as peripheral destination address
dma_set_read_from_memory(WS2812B_DMA, WS2812B_DMA_CH); // set direction from memory to peripheral
dma_enable_memory_increment_mode(WS2812B_DMA, WS2812B_DMA_CH); // go through bit pattern
dma_set_memory_size(WS2812B_DMA, WS2812B_DMA_CH, DMA_CCR_MSIZE_8BIT); // read 8 bits from memory
dma_set_peripheral_size(WS2812B_DMA, WS2812B_DMA_CH, DMA_CCR_PSIZE_8BIT); // write 8 bits to peripheral
dma_set_priority(WS2812B_DMA, WS2812B_DMA_CH, DMA_CCR_PL_HIGH); // set priority to high since time is crucial for the peripheral
nvic_enable_irq(WS2812B_DMA_IRQ); // enable interrupts for this DMA channel
// reset color
for (uint16_t led=0; led<WS2812B_LEDS; led++) {
ws2812b_set_rgb(led, 0x00, 0x00, 0x00); // switch off (set to black)
}
ws2812b_transmit(); // set LEDs
}
/** @brief DMA interrupt service routine to stop transmission after it finished */
void WS2812B_DMA_ISR(void)
{
if (dma_get_interrupt_flag(WS2812B_DMA, WS2812B_DMA_CH, DMA_TCIF)) { // transfer completed
dma_clear_interrupt_flags(WS2812B_DMA, WS2812B_DMA_CH, DMA_TCIF); // clear flag
dma_disable_transfer_complete_interrupt(WS2812B_DMA, WS2812B_DMA_CH); // stop warning transfer completed
spi_disable_tx_dma(WS2812B_SPI); // stop SPI asking for more data
while (SPI_SR(WS2812B_SPI) & SPI_SR_BSY); // wait for data to be shifted out
timer_disable_counter(WS2812B_TIMER); // stop clock
dma_disable_channel(WS2812B_DMA, WS2812B_DMA_CH); // stop using DMA
transmit_flag = false; // transmission completed
}
}