-
Notifications
You must be signed in to change notification settings - Fork 267
CH5xx WS2812B by PWM/TMR/DMA/UART example #887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ad2c655
853c4b1
6d8f8da
0e1260b
9e867cd
2406dc8
9843970
d3e8e09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| all : flash | ||
|
|
||
| TARGET:= ws2812b_pwm | ||
|
|
||
| TARGET_MCU?=CH572 | ||
| TARGET_MCU_PACKAGE:=CH572D | ||
| include ../../ch32fun/ch32fun.mk | ||
|
|
||
| flash : cv_flash | ||
| clean : cv_clean |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # WS2812B PWM and TMR DMA example | ||
|
|
||
| This shows how to write WS2812B LED data using PWM, TMR & DMA on the CH5xx. | ||
| The main reasons you might want to do this instead of using SPI is if you don't have or want | ||
| to use the SPI pins or you want to drive more than one set of LEDs. In this example the | ||
| entire buffer is generated and then sent by DMA in one go. With long strips an interrupt | ||
| could be used to refill the buffer like the SPI driver does. | ||
|
|
||
| Also included is an example of using UART0 to write WS2812B data on CH584/5 which is possible | ||
| due to its facility to invert the output. This is not DMA driven though and the 8 byte FIFO | ||
| only gives a buffer of about 30 microseconds. (3 bits per FIFO entry = 24 bits x 1.25 | ||
| microseconds). In theory UART could be used with an external inverter too but SPI or PWM is | ||
| usually a better option. | ||
|
|
||
| Tested on CH572, CH592 and CH584. | ||
|
|
||
| ## Memory usage | ||
|
|
||
| The TMR buffer uses 32 bits per 1 bit of RGB pixel data. | ||
|
|
||
| On CH57x the PWM buffers use 16 bits per 1 bit but you always pay the cost of at least 2 | ||
| channels so it works out the same if you're only driving one set of LEDs. | ||
|
|
||
| The UART method is currently set up to use 32 bits per 24 bits of data because it converts | ||
| as it goes. | ||
|
|
||
| ## CH57x | ||
|
|
||
| Two or three sets of LEDs can be driven simultaneously on the PWM channels. In addition to this | ||
| another strip can be driven by the TMR. | ||
|
|
||
| PWM channels 1-5 are on pins PA7, PA2, PA3, PA4, PA8 | ||
|
|
||
| TMR can be on pin PA7, PA2, PA4 or PA9 | ||
|
|
||
| ## CH584/5 and CH592 | ||
|
|
||
| Two sets of LEDs can be driven simultaneously by the timers. | ||
|
|
||
| TMR1 is PA10 or PB10 | ||
|
|
||
| TMR2 is PA11 or PB11 | ||
|
|
||
| ## CH584/5 | ||
|
|
||
| UART0 is PB7 or PA14 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #ifndef _FUNCONFIG_H | ||
| #define _FUNCONFIG_H | ||
|
|
||
| #if defined(CH58x) && (MCU_PACKAGE == 4 || MCU_PACKAGE == 5) | ||
| #define FUNCONF_USE_HSI 0 // CH585/584 has HSI | ||
| #define FUNCONF_USE_HSE 1 | ||
| #define CLK_SOURCE_CH5XX CLK_SOURCE_HSE_PLL_78MHz | ||
| #define FUNCONF_SYSTEM_CORE_CLOCK (78 * 1000 * 1000) // keep in line with CLK_SOURCE_CH5XX | ||
| #else | ||
| #define FUNCONF_USE_HSI 0 // CH5xx does not have HSI | ||
| #define FUNCONF_USE_HSE 1 | ||
| #define CLK_SOURCE_CH5XX CLK_SOURCE_PLL_60MHz // default so not really needed | ||
| #define FUNCONF_SYSTEM_CORE_CLOCK (60 * 1000 * 1000) // keep in line with CLK_SOURCE_CH5XX | ||
| #endif | ||
|
|
||
| #define FUNCONF_DEBUG_HARDFAULT 0 | ||
| #define FUNCONF_USE_CLK_SEC 0 | ||
| #define FUNCONF_USE_DEBUGPRINTF 1 // saves 16 bytes, enable / remove if you want printf over swio | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| // Examples of using PWM, TMR, DMA & UART0 to drive WS2812B RGB LEDs | ||
|
|
||
| #include "ch32fun.h" | ||
| #include "../ws2812bdemo/color_utilities.h" | ||
|
|
||
| #define NR_LEDS 4 | ||
|
|
||
| #define WS2812B_CLOCK_HZ (800 * 1000) | ||
| #define WS2812B_CYCLES (FUNCONF_SYSTEM_CORE_CLOCK / WS2812B_CLOCK_HZ) | ||
| #define WS2812B_DUTYCYCLE_NS(NANOSECS) ((WS2812B_CYCLES * NANOSECS + 625) / 1250) | ||
| #define WS2812B_0 WS2812B_DUTYCYCLE_NS(350) | ||
| #define WS2812B_1 WS2812B_DUTYCYCLE_NS(900) | ||
|
|
||
| void WS2812B_TMR_SetPixelRGB(uint32_t* buffer, int pixelNumber, int32_t rgb) | ||
| { | ||
| uint32_t* dst = &buffer[pixelNumber * 24]; | ||
| rgb <<= 8; | ||
| for(int bit = 0; bit != 24; ++bit, rgb <<= 1) | ||
| { | ||
| *dst++ = WS2812B_0 + ((rgb >> 31) & (WS2812B_1 - WS2812B_0)); | ||
| } | ||
| } | ||
| #ifdef CH57x | ||
| void WS2812B_PWM_Init(int channel) | ||
| { | ||
| const int pins[] = {PA7, PA2, PA3, PA4, PA8}; | ||
| funPinMode(pins[channel - 1], GPIO_CFGLR_OUT_10Mhz_PP); | ||
| R16_PWM_CLOCK_DIV = 1; | ||
| R8_PWM_POLAR = 0; | ||
| R8_PWM_CONFIG = RB_PWM_CYC_MOD; // 16 bit data mode | ||
| R16_PWM_CYC_VALUE = WS2812B_CYCLES - 1; | ||
| if (channel < 3) | ||
| (&R16_PWM1_DATA)[channel - 1] = 0; | ||
| else | ||
| (&R16_PWM4_DATA)[channel - 4] = 0; | ||
| R8_PWM_OUT_EN |= RB_PWM1_OUT_EN << (channel - 1); | ||
| } | ||
|
|
||
| void WS2812B_PWM123_SetPixelRGB(uint16_t* buffer, int pixelNumber, int pwmChannel, int32_t rgb) | ||
| { | ||
| // each DMA entry contains data for PWM1, 2 & 3, i.e. 6 bytes | ||
| uint16_t* dst = &buffer[(pixelNumber * 24 + 1) * 3 + (pwmChannel - 1)]; | ||
| rgb <<= 8; | ||
| for(int bit = 0; bit != 24; ++bit, dst += 3, rgb <<= 1) | ||
| { | ||
| *dst = WS2812B_0 + ((rgb >> 31) & (WS2812B_1 - WS2812B_0)); | ||
| } | ||
| } | ||
| void WS2812B_PWM123_StartDMA(const uint16_t* buffer, int numLeds) | ||
| { | ||
| R8_PWM_DMA_CTRL = 0; | ||
| R32_PWM_DMA_BEG = (uintptr_t)&buffer[0]; | ||
| R32_PWM_DMA_END = (uintptr_t)&buffer[(numLeds * 24 + 2) * 3]; | ||
| R8_PWM_DMA_CTRL = RB_DMA_SEL|RB_DMA_ENABLE; | ||
| } | ||
| void WS2812B_PWM45_SetPixelRGB(uint16_t* buffer, int pixelNumber, int pwmChannel, int32_t rgb) | ||
| { | ||
| // each DMA entry contains data for PWM4 & 5, i.e. 4 bytes | ||
| uint16_t* dst = &buffer[(pixelNumber * 24 + 1) * 2 + (pwmChannel - 4)]; | ||
| rgb <<= 8; | ||
| for(int bit = 0; bit != 24; ++bit, dst += 2, rgb += rgb) | ||
| { | ||
| *dst = WS2812B_0 + ((rgb >> 31) & (WS2812B_1 - WS2812B_0)); | ||
| } | ||
| } | ||
| void WS2812B_PWM45_StartDMA(const uint16_t* buffer, int numLeds) | ||
| { | ||
| R8_PWM_DMA_CTRL = 0; | ||
| R32_PWM_DMA_BEG = (uintptr_t)&buffer[0]; | ||
| R32_PWM_DMA_END = (uintptr_t)&buffer[(numLeds * 24 + 2) * 2]; | ||
| R8_PWM_DMA_CTRL = RB_DMA_ENABLE; | ||
| } | ||
| void WS2812B_TMR_StartDMA(const uint32_t* buffer, int numLeds) | ||
| { | ||
| // turn off DMA & timer | ||
| R8_TMR_CTRL_DMA = 0; | ||
| R8_TMR_CTRL_MOD = RB_TMR_ALL_CLEAR; | ||
| // set up DMA | ||
| R32_TMR_DMA_BEG = (uintptr_t)&buffer[0]; | ||
| R32_TMR_DMA_END = (uintptr_t)&buffer[numLeds * 24 + 1]; | ||
| R32_TMR_CNT_END = WS2812B_CYCLES; | ||
| // enable timer and then start the DMA | ||
| R8_TMR_CTRL_MOD = RB_TMR_OUT_EN|RB_TMR_COUNT_EN; | ||
| R8_TMR_CTRL_DMA = RB_TMR_DMA_ENABLE; | ||
| } | ||
| #else | ||
| void WS2812B_TMR1_StartDMA(const uint32_t* buffer, int numLeds) | ||
| { | ||
| // turn off DMA & timer | ||
| R8_TMR1_CTRL_DMA = 0; | ||
| R8_TMR1_CTRL_MOD = RB_TMR_ALL_CLEAR; | ||
| // set up DMA | ||
| R32_TMR1_DMA_BEG = (uintptr_t)&buffer[0]; | ||
| R32_TMR1_DMA_END = (uintptr_t)&buffer[numLeds * 24 + 1]; | ||
| R32_TMR1_CNT_END = WS2812B_CYCLES; | ||
| // enable timer and then start the DMA | ||
| R8_TMR1_CTRL_MOD = RB_TMR_OUT_EN|RB_TMR_COUNT_EN; | ||
| R8_TMR1_CTRL_DMA = RB_TMR_DMA_ENABLE; | ||
| } | ||
| void WS2812B_TMR2_StartDMA(const uint32_t* buffer, int numLeds) | ||
| { | ||
| // turn off DMA & timer | ||
| R8_TMR2_CTRL_DMA = 0; | ||
| R8_TMR2_CTRL_MOD = RB_TMR_ALL_CLEAR; | ||
| // set up DMA | ||
| R32_TMR2_DMA_BEG = (uintptr_t)&buffer[0]; | ||
| R32_TMR2_DMA_END = (uintptr_t)&buffer[numLeds * 24 + 1]; | ||
| R32_TMR2_CNT_END = WS2812B_CYCLES; | ||
| // enable timer and then start the DMA | ||
| R8_TMR2_CTRL_MOD = RB_TMR_OUT_EN|RB_TMR_COUNT_EN; | ||
| R8_TMR2_CTRL_DMA = RB_TMR_DMA_ENABLE; | ||
| } | ||
| #endif | ||
|
|
||
| #ifdef CH57x | ||
| #define TMR_PA9 1 | ||
| // the PWM buffers start and end with zero entry (duty cycle of 0 = output off). | ||
| // I'm not sure why the start one is needed but you get flickering without it. | ||
| uint16_t __attribute__((aligned(4))) WS2812B_PWM123_LedBuffer[(NR_LEDS * 24 + 2) * 3] = {0}; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment here explaining why the alignment? (I.e. because these are the raw buffers that are sent out via DMA) |
||
| uint16_t __attribute__((aligned(4))) WS2812B_PWM45_LedBuffer[(NR_LEDS * 24 + 2) * 2] = {0}; | ||
| #endif | ||
| // the TMR buffer just needs one zero at the end to turn off the output | ||
| uint32_t WS2812B_TMR_LedBuffer[NR_LEDS * 24 + 1] = {0}; | ||
|
|
||
| #ifdef RB_PIN_U0_INV // CH584/5 support inverted UART output | ||
| uint32_t WS2812B_UART_LedBuffer[NR_LEDS]; | ||
|
|
||
| static const uint8_t WS2812B_UART_LUT[8] = | ||
| { | ||
| ~0b0100100, // 9 bits are used to encode 3 bits of WS2812B data. | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to change this! But, specifying ~ in a LUT is not normally a natural way to express this, instead, you would normally write out the inverted bitstream in the LUT. This is to make debugging easier. |
||
| ~0b1100100, // Each of these 7 bit payloads is framed by the | ||
| ~0b0101100, // UART high-start and low-stop bits. Entries are | ||
| ~0b1101100, // inverted to compensate for the output inversion. | ||
| ~0b0100101, | ||
| ~0b1100101, | ||
| ~0b0101101, | ||
| ~0b1101101, | ||
| }; | ||
| void WS2812B_UART0_Send(const uint32_t* ledData, int numLeds) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you write a blirb about these functions and what they do, and whether the ledData pointer is used as the DMA pointer, and/or if the data is made available upon completion or after completion. |
||
| { | ||
| UART0->IER = RB_IER_RESET; | ||
| R16_PIN_ALTERNATE |= RB_PIN_U0_INV; | ||
| UART0->FCR = RB_FCR_FIFO_EN | RB_FCR_TX_FIFO_CLR; | ||
| UART0->LCR = 2; // word length: 7 bits | ||
| UART0->DL = (FUNCONF_SYSTEM_CORE_CLOCK / 2400000 + 4) / 8; | ||
| UART0->DIV = 1; | ||
| UART0->IER = RB_IER_TXD_EN; | ||
| for(int i = 0; i < numLeds; ++i) | ||
| { | ||
| uint32_t rgb = ledData[i]; | ||
| for(int j = 0; j < 8; ++j) | ||
| { | ||
| uint8_t nextByte = WS2812B_UART_LUT[rgb >> 29]; | ||
| while (UART0->TFC == 8) | ||
| { | ||
| // wait for free space in TX FIFO | ||
| } | ||
| UART0->THR_RBR = nextByte; | ||
| rgb <<= 3; | ||
| } | ||
| } | ||
| } | ||
| #endif | ||
|
|
||
| int main() | ||
| { | ||
| SystemInit(); | ||
|
|
||
| #ifdef CH57x | ||
| #if TMR_PA9 | ||
| R16_PIN_ALTERNATE_H |= RB_TMR_PIN; // map TMR to PA9 | ||
| funPinMode(PA9, GPIO_CFGLR_OUT_10Mhz_PP); | ||
| #else | ||
| funPinMode(PA7, GPIO_CFGLR_OUT_10Mhz_PP); | ||
| #endif | ||
|
|
||
| WS2812B_PWM_Init(1); | ||
| WS2812B_PWM_Init(2); | ||
| WS2812B_PWM_Init(3); | ||
| WS2812B_PWM_Init(4); | ||
| WS2812B_PWM_Init(5); // TODO: PWM5 doesn't seem to work | ||
| #else | ||
| funPinMode(PA10, GPIO_CFGLR_OUT_10Mhz_PP); // TMR1 | ||
| funPinMode(PA11, GPIO_CFGLR_OUT_10Mhz_PP); // TMR2 | ||
|
|
||
| #ifdef RB_PIN_U0_INV | ||
| funPinMode(PB7, GPIO_CFGLR_OUT_10Mhz_PP); // UART | ||
| funDigitalWrite(PB7, FUN_LOW); | ||
| #endif | ||
| #endif | ||
|
|
||
|
|
||
| uint8_t hue = 0; | ||
| uint8_t sat = 255; | ||
| uint8_t val = 64; // brightness control | ||
| for(;;) | ||
| { | ||
| uint8_t hue1 = hue++; | ||
| for(int i = 0; i < NR_LEDS; ++i) | ||
| { | ||
| #ifdef CH57x | ||
| // up to 5 strips of LEDs can be driven by PWM DMA | ||
| WS2812B_PWM123_SetPixelRGB(WS2812B_PWM123_LedBuffer, i, 1, EHSVtoHEX(hue1, sat, val)); | ||
| WS2812B_PWM123_SetPixelRGB(WS2812B_PWM123_LedBuffer, i, 2, EHSVtoHEX(hue1 + 64, sat, val)); | ||
| WS2812B_PWM123_SetPixelRGB(WS2812B_PWM123_LedBuffer, i, 3, EHSVtoHEX(hue1 + 128, sat, val)); | ||
| WS2812B_PWM45_SetPixelRGB(WS2812B_PWM45_LedBuffer, i, 4, EHSVtoHEX(hue1 + 160, sat, val)); | ||
| WS2812B_PWM45_SetPixelRGB(WS2812B_PWM45_LedBuffer, i, 5, EHSVtoHEX(hue1 + 192, sat, val)); | ||
| #endif | ||
| #ifdef RB_PIN_U0_INV | ||
| WS2812B_UART_LedBuffer[i] = EHSVtoHEX(hue1, sat, val) << 8; | ||
| #endif | ||
| WS2812B_TMR_SetPixelRGB(WS2812B_TMR_LedBuffer, i, EHSVtoHEX(hue1 + 224, sat, val)); | ||
| hue1 += 256 / NR_LEDS; | ||
| } | ||
| #ifdef CH57x | ||
| WS2812B_TMR_StartDMA(WS2812B_TMR_LedBuffer, NR_LEDS); | ||
| WS2812B_PWM123_StartDMA(WS2812B_PWM123_LedBuffer, NR_LEDS); | ||
| Delay_Ms(5); // can't run PWM123 & PWM45 DMAs simultaneously | ||
| WS2812B_PWM45_StartDMA(WS2812B_PWM45_LedBuffer, NR_LEDS); | ||
| Delay_Ms(10); | ||
| #else | ||
| // two strips can be driven simultaneously by TMR PWM DMA | ||
| WS2812B_TMR1_StartDMA(WS2812B_TMR_LedBuffer, NR_LEDS); | ||
| WS2812B_TMR2_StartDMA(WS2812B_TMR_LedBuffer, NR_LEDS); | ||
| #ifdef RB_PIN_U0_INV | ||
| WS2812B_UART0_Send(WS2812B_UART_LedBuffer, NR_LEDS); | ||
| #endif | ||
| Delay_Ms(15); | ||
| #endif | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally, you would make these zero indexed, even though it would disagree with the datasheet. So because you are matching the datasheet, can you change it around so that makes it clear that it's pwm_channel_1_indexed