From f74c769a19662147ead15dcd14777afe5e53c167 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Sun, 1 Mar 2020 02:05:56 +0000 Subject: [PATCH] PWM DMA based RGB Underglow for STM32 (#7928) * Add pwm ws2812 driver * Add docs for pwm ws2812 driver * Update ws2812_pwm for ChibiOS 19 Co-Authored-By: Nick Brassel Co-authored-by: Nick Brassel --- docs/ws2812_driver.md | 36 ++++++- drivers/arm/ws2812_pwm.c | 208 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 241 insertions(+), 3 deletions(-) diff --git a/docs/ws2812_driver.md b/docs/ws2812_driver.md index 80b6948586..51b053b9b4 100644 --- a/docs/ws2812_driver.md +++ b/docs/ws2812_driver.md @@ -15,7 +15,7 @@ These LEDs are called "addressable" because instead of using a wire per color, e | bit bang | :heavy_check_mark: | :heavy_check_mark: | | I2C | :heavy_check_mark: | | | SPI | | :heavy_check_mark: | -| PWM | | Soon™ | +| PWM | | :heavy_check_mark: | ## Driver configuration @@ -66,4 +66,36 @@ While not an exhaustive list, the following table provides the scenarios that ha | f103 | A7 :heavy_check_mark: | B15 :heavy_check_mark: | N/A | | f303 | A7 :heavy_check_mark: B5 :heavy_check_mark: | B15 :heavy_check_mark: | B5 :heavy_check_mark: | -*Other supported ChibiOS boards and/or pins may function, it will be highly chip and configuration dependent.* \ No newline at end of file +*Other supported ChibiOS boards and/or pins may function, it will be highly chip and configuration dependent.* + +### PWM + +Targeting STM32 boards where WS2812 support is offloaded to an PWM timer and DMA stream. The advantage is that the use of DMA offloads processing of the WS2812 protocol from the MCU. To configure it, add this to your rules.mk: + +```make +WS2812_DRIVER = pwm +``` + +Configure the hardware via your config.h: +```c +#define WS2812_PWM_DRIVER PWMD2 // default: PWMD2 +#define WS2812_PWM_CHANNEL 2 // default: 2 +#define WS2812_PWM_PAL_MODE 2 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 2 +#define WS2812_DMA_STREAM STM32_DMA1_STREAM2 // DMA Stream for TIMx_UP, see the respective reference manual for the appropriate values for your MCU. +#define WS2812_DMA_CHANNEL 2 // DMA Channel for TIMx_UP, see the respective reference manual for the appropriate values for your MCU. +``` + +You must also turn on the PWM feature in your halconf.h and mcuconf.h + +#### Testing Notes + +While not an exhaustive list, the following table provides the scenarios that have been partially validated: + +| | Status | +|-|-| +| f072 | ? | +| f103 | :heavy_check_mark: | +| f303 | :heavy_check_mark: | +| f401/f411 | :heavy_check_mark: | + +*Other supported ChibiOS boards and/or pins may function, it will be highly chip and configuration dependent.* diff --git a/drivers/arm/ws2812_pwm.c b/drivers/arm/ws2812_pwm.c index 2094e50098..1a17210298 100644 --- a/drivers/arm/ws2812_pwm.c +++ b/drivers/arm/ws2812_pwm.c @@ -1 +1,207 @@ -#error("NOT SUPPORTED") \ No newline at end of file +#include "ws2812.h" +#include "quantum.h" +#include "hal.h" + +/* Adapted from https://github.com/joewa/WS2812-LED-Driver_ChibiOS/ */ + +#ifdef RGBW +# error "RGBW not supported" +#endif + +#ifndef WS2812_PWM_DRIVER +# define WS2812_PWM_DRIVER PWMD2 // TIMx +#endif +#ifndef WS2812_PWM_CHANNEL +# define WS2812_PWM_CHANNEL 2 // Channel +#endif +#ifndef WS2812_PWM_PAL_MODE +# define WS2812_PWM_PAL_MODE 2 // DI Pin's alternate function value +#endif +#ifndef WS2812_DMA_STREAM +# define WS2812_DMA_STREAM STM32_DMA1_STREAM2 // DMA Stream for TIMx_UP +#endif +#ifndef WS2812_DMA_CHANNEL +# define WS2812_DMA_CHANNEL 2 // DMA Channel for TIMx_UP +#endif + +#ifndef WS2812_PWM_TARGET_PERIOD +//# define WS2812_PWM_TARGET_PERIOD 800000 // Original code is 800k...? +# define WS2812_PWM_TARGET_PERIOD 80000 // TODO: work out why 10x less on f303/f4x1 +#endif + +/* --- PRIVATE CONSTANTS ---------------------------------------------------- */ + +#define WS2812_PWM_FREQUENCY (STM32_SYSCLK / 2) /**< Clock frequency of PWM, must be valid with respect to system clock! */ +#define WS2812_PWM_PERIOD (WS2812_PWM_FREQUENCY / WS2812_PWM_TARGET_PERIOD) /**< Clock period in ticks. 1 / 800kHz = 1.25 uS (as per datasheet) */ + +/** + * @brief Number of bit-periods to hold the data line low at the end of a frame + * + * The reset period for each frame must be at least 50 uS; so we add in 50 bit-times + * of zeroes at the end. (50 bits)*(1.25 uS/bit) = 62.5 uS, which gives us some + * slack in the timing requirements + */ +#define WS2812_RESET_BIT_N (50) +#define WS2812_COLOR_BIT_N (RGBLED_NUM * 24) /**< Number of data bits */ +#define WS2812_BIT_N (WS2812_COLOR_BIT_N + WS2812_RESET_BIT_N) /**< Total number of bits in a frame */ + +/** + * @brief High period for a zero, in ticks + * + * Per the datasheet: + * WS2812: + * - T0H: 200 nS to 500 nS, inclusive + * - T0L: 650 nS to 950 nS, inclusive + * WS2812B: + * - T0H: 200 nS to 500 nS, inclusive + * - T0L: 750 nS to 1050 nS, inclusive + * + * The duty cycle is calculated for a high period of 350 nS. + */ +#define WS2812_DUTYCYCLE_0 (WS2812_PWM_FREQUENCY / (1000000000 / 350)) + +/** + * @brief High period for a one, in ticks + * + * Per the datasheet: + * WS2812: + * - T1H: 550 nS to 850 nS, inclusive + * - T1L: 450 nS to 750 nS, inclusive + * WS2812B: + * - T1H: 750 nS to 1050 nS, inclusive + * - T1L: 200 nS to 500 nS, inclusive + * + * The duty cycle is calculated for a high period of 800 nS. + * This is in the middle of the specifications of the WS2812 and WS2812B. + */ +#define WS2812_DUTYCYCLE_1 (WS2812_PWM_FREQUENCY / (1000000000 / 800)) + +/* --- PRIVATE MACROS ------------------------------------------------------- */ + +/** + * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given bit + * + * @param[in] led: The led index [0, @ref RGBLED_NUM) + * @param[in] byte: The byte number [0, 2] + * @param[in] bit: The bit number [0, 7] + * + * @return The bit index + */ +#define WS2812_BIT(led, byte, bit) (24 * (led) + 8 * (byte) + (7 - (bit))) + +/** + * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given red bit + * + * @note The red byte is the middle byte in the color packet + * + * @param[in] led: The led index [0, @ref RGBLED_NUM) + * @param[in] bit: The bit number [0, 7] + * + * @return The bit index + */ +#define WS2812_RED_BIT(led, bit) WS2812_BIT((led), 1, (bit)) + +/** + * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given green bit + * + * @note The red byte is the first byte in the color packet + * + * @param[in] led: The led index [0, @ref RGBLED_NUM) + * @param[in] bit: The bit number [0, 7] + * + * @return The bit index + */ +#define WS2812_GREEN_BIT(led, bit) WS2812_BIT((led), 0, (bit)) + +/** + * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given blue bit + * + * @note The red byte is the last byte in the color packet + * + * @param[in] led: The led index [0, @ref RGBLED_NUM) + * @param[in] bit: The bit index [0, 7] + * + * @return The bit index + */ +#define WS2812_BLUE_BIT(led, bit) WS2812_BIT((led), 2, (bit)) + +/* --- PRIVATE VARIABLES ---------------------------------------------------- */ + +static uint32_t ws2812_frame_buffer[WS2812_BIT_N + 1]; /**< Buffer for a frame */ + +/* --- PUBLIC FUNCTIONS ----------------------------------------------------- */ +/* + * Gedanke: Double-buffer type transactions: double buffer transfers using two memory pointers for +the memory (while the DMA is reading/writing from/to a buffer, the application can +write/read to/from the other buffer). + */ + +void ws2812_init(void) { + // Initialize led frame buffer + uint32_t i; + for (i = 0; i < WS2812_COLOR_BIT_N; i++) ws2812_frame_buffer[i] = WS2812_DUTYCYCLE_0; // All color bits are zero duty cycle + for (i = 0; i < WS2812_RESET_BIT_N; i++) ws2812_frame_buffer[i + WS2812_COLOR_BIT_N] = 0; // All reset bits are zero + +#if defined(USE_GPIOV1) + palSetLineMode(RGB_DI_PIN, PAL_MODE_STM32_ALTERNATE_PUSHPULL); +#else + palSetLineMode(RGB_DI_PIN, PAL_MODE_ALTERNATE(WS2812_PWM_PAL_MODE) | PAL_STM32_OSPEED_HIGHEST | PAL_STM32_PUPDR_FLOATING); +#endif + + // PWM Configuration + //#pragma GCC diagnostic ignored "-Woverride-init" // Turn off override-init warning for this struct. We use the overriding ability to set a "default" channel config + static const PWMConfig ws2812_pwm_config = { + .frequency = WS2812_PWM_FREQUENCY, + .period = WS2812_PWM_PERIOD, // Mit dieser Periode wird UDE-Event erzeugt und ein neuer Wert (Länge WS2812_BIT_N) vom DMA ins CCR geschrieben + .callback = NULL, + .channels = + { + [0 ... 3] = {.mode = PWM_OUTPUT_DISABLED, .callback = NULL}, // Channels default to disabled + [WS2812_PWM_CHANNEL - 1] = {.mode = PWM_OUTPUT_ACTIVE_HIGH, .callback = NULL}, // Turn on the channel we care about + }, + .cr2 = 0, + .dier = TIM_DIER_UDE, // DMA on update event for next period + }; + //#pragma GCC diagnostic pop // Restore command-line warning options + + // Configure DMA + // dmaInit(); // Joe added this + dmaStreamAlloc(WS2812_DMA_STREAM - STM32_DMA1_STREAM1, 10, NULL, NULL); + dmaStreamSetPeripheral(WS2812_DMA_STREAM, &(WS2812_PWM_DRIVER.tim->CCR[WS2812_PWM_CHANNEL - 1])); // Ziel ist der An-Zeit im Cap-Comp-Register + dmaStreamSetMemory0(WS2812_DMA_STREAM, ws2812_frame_buffer); + dmaStreamSetTransactionSize(WS2812_DMA_STREAM, WS2812_BIT_N); + dmaStreamSetMode(WS2812_DMA_STREAM, STM32_DMA_CR_CHSEL(WS2812_DMA_CHANNEL) | STM32_DMA_CR_DIR_M2P | STM32_DMA_CR_PSIZE_WORD | STM32_DMA_CR_MSIZE_WORD | STM32_DMA_CR_MINC | STM32_DMA_CR_CIRC | STM32_DMA_CR_PL(3)); + // M2P: Memory 2 Periph; PL: Priority Level + + // Start DMA + dmaStreamEnable(WS2812_DMA_STREAM); + + // Configure PWM + // NOTE: It's required that preload be enabled on the timer channel CCR register. This is currently enabled in the + // ChibiOS driver code, so we don't have to do anything special to the timer. If we did, we'd have to start the timer, + // disable counting, enable the channel, and then make whatever configuration changes we need. + pwmStart(&WS2812_PWM_DRIVER, &ws2812_pwm_config); + pwmEnableChannel(&WS2812_PWM_DRIVER, WS2812_PWM_CHANNEL - 1, 0); // Initial period is 0; output will be low until first duty cycle is DMA'd in +} + +void ws2812_write_led(uint16_t led_number, uint8_t r, uint8_t g, uint8_t b) { + // Write color to frame buffer + for (uint8_t bit = 0; bit < 8; bit++) { + ws2812_frame_buffer[WS2812_RED_BIT(led_number, bit)] = ((r >> bit) & 0x01) ? WS2812_DUTYCYCLE_1 : WS2812_DUTYCYCLE_0; + ws2812_frame_buffer[WS2812_GREEN_BIT(led_number, bit)] = ((g >> bit) & 0x01) ? WS2812_DUTYCYCLE_1 : WS2812_DUTYCYCLE_0; + ws2812_frame_buffer[WS2812_BLUE_BIT(led_number, bit)] = ((b >> bit) & 0x01) ? WS2812_DUTYCYCLE_1 : WS2812_DUTYCYCLE_0; + } +} + +// Setleds for standard RGB +void ws2812_setleds(LED_TYPE* ledarray, uint16_t leds) { + static bool s_init = false; + if (!s_init) { + ws2812_init(); + s_init = true; + } + + for (uint16_t i = 0; i < leds; i++) { + ws2812_write_led(i, ledarray[i].r, ledarray[i].g, ledarray[i].b); + } +}