aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Challis <git@zvecr.com>2020-03-01 02:05:56 +0000
committerGitHub <noreply@github.com>2020-03-01 13:05:56 +1100
commitf74c769a19662147ead15dcd14777afe5e53c167 (patch)
treeb18226acb011831820f0f6ce9a0c2cbed287bf9a
parentbb472364909aa8ad0ff8efb76a428df2960e1a7c (diff)
downloadqmk_firmware-f74c769a19662147ead15dcd14777afe5e53c167.tar.gz
qmk_firmware-f74c769a19662147ead15dcd14777afe5e53c167.zip
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 <nick@tzarc.org> Co-authored-by: Nick Brassel <nick@tzarc.org>
-rw-r--r--docs/ws2812_driver.md36
-rw-r--r--drivers/arm/ws2812_pwm.c208
2 files changed, 241 insertions, 3 deletions
diff --git a/docs/ws2812_driver.md b/docs/ws2812_driver.md
index 80b694858..51b053b9b 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
15| bit bang | :heavy_check_mark: | :heavy_check_mark: | 15| bit bang | :heavy_check_mark: | :heavy_check_mark: |
16| I2C | :heavy_check_mark: | | 16| I2C | :heavy_check_mark: | |
17| SPI | | :heavy_check_mark: | 17| SPI | | :heavy_check_mark: |
18| PWM | | Soon™ | 18| PWM | | :heavy_check_mark: |
19 19
20## Driver configuration 20## Driver configuration
21 21
@@ -66,4 +66,36 @@ While not an exhaustive list, the following table provides the scenarios that ha
66| f103 | A7 :heavy_check_mark: | B15 :heavy_check_mark: | N/A | 66| f103 | A7 :heavy_check_mark: | B15 :heavy_check_mark: | N/A |
67| f303 | A7 :heavy_check_mark: B5 :heavy_check_mark: | B15 :heavy_check_mark: | B5 :heavy_check_mark: | 67| f303 | A7 :heavy_check_mark: B5 :heavy_check_mark: | B15 :heavy_check_mark: | B5 :heavy_check_mark: |
68 68
69*Other supported ChibiOS boards and/or pins may function, it will be highly chip and configuration dependent.* \ No newline at end of file 69*Other supported ChibiOS boards and/or pins may function, it will be highly chip and configuration dependent.*
70
71### PWM
72
73Targeting 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:
74
75```make
76WS2812_DRIVER = pwm
77```
78
79Configure the hardware via your config.h:
80```c
81#define WS2812_PWM_DRIVER PWMD2 // default: PWMD2
82#define WS2812_PWM_CHANNEL 2 // default: 2
83#define WS2812_PWM_PAL_MODE 2 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 2
84#define WS2812_DMA_STREAM STM32_DMA1_STREAM2 // DMA Stream for TIMx_UP, see the respective reference manual for the appropriate values for your MCU.
85#define WS2812_DMA_CHANNEL 2 // DMA Channel for TIMx_UP, see the respective reference manual for the appropriate values for your MCU.
86```
87
88You must also turn on the PWM feature in your halconf.h and mcuconf.h
89
90#### Testing Notes
91
92While not an exhaustive list, the following table provides the scenarios that have been partially validated:
93
94| | Status |
95|-|-|
96| f072 | ? |
97| f103 | :heavy_check_mark: |
98| f303 | :heavy_check_mark: |
99| f401/f411 | :heavy_check_mark: |
100
101*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 2094e5009..1a1721029 100644
--- a/drivers/arm/ws2812_pwm.c
+++ b/drivers/arm/ws2812_pwm.c
@@ -1 +1,207 @@
1#error("NOT SUPPORTED") \ No newline at end of file 1#include "ws2812.h"
2#include "quantum.h"
3#include "hal.h"
4
5/* Adapted from https://github.com/joewa/WS2812-LED-Driver_ChibiOS/ */
6
7#ifdef RGBW
8# error "RGBW not supported"
9#endif
10
11#ifndef WS2812_PWM_DRIVER
12# define WS2812_PWM_DRIVER PWMD2 // TIMx
13#endif
14#ifndef WS2812_PWM_CHANNEL
15# define WS2812_PWM_CHANNEL 2 // Channel
16#endif
17#ifndef WS2812_PWM_PAL_MODE
18# define WS2812_PWM_PAL_MODE 2 // DI Pin's alternate function value
19#endif
20#ifndef WS2812_DMA_STREAM
21# define WS2812_DMA_STREAM STM32_DMA1_STREAM2 // DMA Stream for TIMx_UP
22#endif
23#ifndef WS2812_DMA_CHANNEL
24# define WS2812_DMA_CHANNEL 2 // DMA Channel for TIMx_UP
25#endif
26
27#ifndef WS2812_PWM_TARGET_PERIOD
28//# define WS2812_PWM_TARGET_PERIOD 800000 // Original code is 800k...?
29# define WS2812_PWM_TARGET_PERIOD 80000 // TODO: work out why 10x less on f303/f4x1
30#endif
31
32/* --- PRIVATE CONSTANTS ---------------------------------------------------- */
33
34#define WS2812_PWM_FREQUENCY (STM32_SYSCLK / 2) /**< Clock frequency of PWM, must be valid with respect to system clock! */
35#define WS2812_PWM_PERIOD (WS2812_PWM_FREQUENCY / WS2812_PWM_TARGET_PERIOD) /**< Clock period in ticks. 1 / 800kHz = 1.25 uS (as per datasheet) */
36
37/**
38 * @brief Number of bit-periods to hold the data line low at the end of a frame
39 *
40 * The reset period for each frame must be at least 50 uS; so we add in 50 bit-times
41 * of zeroes at the end. (50 bits)*(1.25 uS/bit) = 62.5 uS, which gives us some
42 * slack in the timing requirements
43 */
44#define WS2812_RESET_BIT_N (50)
45#define WS2812_COLOR_BIT_N (RGBLED_NUM * 24) /**< Number of data bits */
46#define WS2812_BIT_N (WS2812_COLOR_BIT_N + WS2812_RESET_BIT_N) /**< Total number of bits in a frame */
47
48/**
49 * @brief High period for a zero, in ticks
50 *
51 * Per the datasheet:
52 * WS2812:
53 * - T0H: 200 nS to 500 nS, inclusive
54 * - T0L: 650 nS to 950 nS, inclusive
55 * WS2812B:
56 * - T0H: 200 nS to 500 nS, inclusive
57 * - T0L: 750 nS to 1050 nS, inclusive
58 *
59 * The duty cycle is calculated for a high period of 350 nS.
60 */
61#define WS2812_DUTYCYCLE_0 (WS2812_PWM_FREQUENCY / (1000000000 / 350))
62
63/**
64 * @brief High period for a one, in ticks
65 *
66 * Per the datasheet:
67 * WS2812:
68 * - T1H: 550 nS to 850 nS, inclusive
69 * - T1L: 450 nS to 750 nS, inclusive
70 * WS2812B:
71 * - T1H: 750 nS to 1050 nS, inclusive
72 * - T1L: 200 nS to 500 nS, inclusive
73 *
74 * The duty cycle is calculated for a high period of 800 nS.
75 * This is in the middle of the specifications of the WS2812 and WS2812B.
76 */
77#define WS2812_DUTYCYCLE_1 (WS2812_PWM_FREQUENCY / (1000000000 / 800))
78
79/* --- PRIVATE MACROS ------------------------------------------------------- */
80
81/**
82 * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given bit
83 *
84 * @param[in] led: The led index [0, @ref RGBLED_NUM)
85 * @param[in] byte: The byte number [0, 2]
86 * @param[in] bit: The bit number [0, 7]
87 *
88 * @return The bit index
89 */
90#define WS2812_BIT(led, byte, bit) (24 * (led) + 8 * (byte) + (7 - (bit)))
91
92/**
93 * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given red bit
94 *
95 * @note The red byte is the middle byte in the color packet
96 *
97 * @param[in] led: The led index [0, @ref RGBLED_NUM)
98 * @param[in] bit: The bit number [0, 7]
99 *
100 * @return The bit index
101 */
102#define WS2812_RED_BIT(led, bit) WS2812_BIT((led), 1, (bit))
103
104/**
105 * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given green bit
106 *
107 * @note The red byte is the first byte in the color packet
108 *
109 * @param[in] led: The led index [0, @ref RGBLED_NUM)
110 * @param[in] bit: The bit number [0, 7]
111 *
112 * @return The bit index
113 */
114#define WS2812_GREEN_BIT(led, bit) WS2812_BIT((led), 0, (bit))
115
116/**
117 * @brief Determine the index in @ref ws2812_frame_buffer "the frame buffer" of a given blue bit
118 *
119 * @note The red byte is the last byte in the color packet
120 *
121 * @param[in] led: The led index [0, @ref RGBLED_NUM)
122 * @param[in] bit: The bit index [0, 7]
123 *
124 * @return The bit index
125 */
126#define WS2812_BLUE_BIT(led, bit) WS2812_BIT((led), 2, (bit))
127
128/* --- PRIVATE VARIABLES ---------------------------------------------------- */
129
130static uint32_t ws2812_frame_buffer[WS2812_BIT_N + 1]; /**< Buffer for a frame */
131
132/* --- PUBLIC FUNCTIONS ----------------------------------------------------- */
133/*
134 * Gedanke: Double-buffer type transactions: double buffer transfers using two memory pointers for
135the memory (while the DMA is reading/writing from/to a buffer, the application can
136write/read to/from the other buffer).
137 */
138
139void ws2812_init(void) {
140 // Initialize led frame buffer
141 uint32_t i;
142 for (i = 0; i < WS2812_COLOR_BIT_N; i++) ws2812_frame_buffer[i] = WS2812_DUTYCYCLE_0; // All color bits are zero duty cycle
143 for (i = 0; i < WS2812_RESET_BIT_N; i++) ws2812_frame_buffer[i + WS2812_COLOR_BIT_N] = 0; // All reset bits are zero
144
145#if defined(USE_GPIOV1)
146 palSetLineMode(RGB_DI_PIN, PAL_MODE_STM32_ALTERNATE_PUSHPULL);
147#else
148 palSetLineMode(RGB_DI_PIN, PAL_MODE_ALTERNATE(WS2812_PWM_PAL_MODE) | PAL_STM32_OSPEED_HIGHEST | PAL_STM32_PUPDR_FLOATING);
149#endif
150
151 // PWM Configuration
152 //#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
153 static const PWMConfig ws2812_pwm_config = {
154 .frequency = WS2812_PWM_FREQUENCY,
155 .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
156 .callback = NULL,
157 .channels =
158 {
159 [0 ... 3] = {.mode = PWM_OUTPUT_DISABLED, .callback = NULL}, // Channels default to disabled
160 [WS2812_PWM_CHANNEL - 1] = {.mode = PWM_OUTPUT_ACTIVE_HIGH, .callback = NULL}, // Turn on the channel we care about
161 },
162 .cr2 = 0,
163 .dier = TIM_DIER_UDE, // DMA on update event for next period
164 };
165 //#pragma GCC diagnostic pop // Restore command-line warning options
166
167 // Configure DMA
168 // dmaInit(); // Joe added this
169 dmaStreamAlloc(WS2812_DMA_STREAM - STM32_DMA1_STREAM1, 10, NULL, NULL);
170 dmaStreamSetPeripheral(WS2812_DMA_STREAM, &(WS2812_PWM_DRIVER.tim->CCR[WS2812_PWM_CHANNEL - 1])); // Ziel ist der An-Zeit im Cap-Comp-Register
171 dmaStreamSetMemory0(WS2812_DMA_STREAM, ws2812_frame_buffer);
172 dmaStreamSetTransactionSize(WS2812_DMA_STREAM, WS2812_BIT_N);
173 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));
174 // M2P: Memory 2 Periph; PL: Priority Level
175
176 // Start DMA
177 dmaStreamEnable(WS2812_DMA_STREAM);
178
179 // Configure PWM
180 // NOTE: It's required that preload be enabled on the timer channel CCR register. This is currently enabled in the
181 // 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,
182 // disable counting, enable the channel, and then make whatever configuration changes we need.
183 pwmStart(&WS2812_PWM_DRIVER, &ws2812_pwm_config);
184 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
185}
186
187void ws2812_write_led(uint16_t led_number, uint8_t r, uint8_t g, uint8_t b) {
188 // Write color to frame buffer
189 for (uint8_t bit = 0; bit < 8; bit++) {
190 ws2812_frame_buffer[WS2812_RED_BIT(led_number, bit)] = ((r >> bit) & 0x01) ? WS2812_DUTYCYCLE_1 : WS2812_DUTYCYCLE_0;
191 ws2812_frame_buffer[WS2812_GREEN_BIT(led_number, bit)] = ((g >> bit) & 0x01) ? WS2812_DUTYCYCLE_1 : WS2812_DUTYCYCLE_0;
192 ws2812_frame_buffer[WS2812_BLUE_BIT(led_number, bit)] = ((b >> bit) & 0x01) ? WS2812_DUTYCYCLE_1 : WS2812_DUTYCYCLE_0;
193 }
194}
195
196// Setleds for standard RGB
197void ws2812_setleds(LED_TYPE* ledarray, uint16_t leds) {
198 static bool s_init = false;
199 if (!s_init) {
200 ws2812_init();
201 s_init = true;
202 }
203
204 for (uint16_t i = 0; i < leds; i++) {
205 ws2812_write_led(i, ledarray[i].r, ledarray[i].g, ledarray[i].b);
206 }
207}