Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples_ch5xx/ws2812b_pwm/Makefile
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
46 changes: 46 additions & 0 deletions examples_ch5xx/ws2812b_pwm/README.md
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
20 changes: 20 additions & 0 deletions examples_ch5xx/ws2812b_pwm/funconfig.h
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
231 changes: 231 additions & 0 deletions examples_ch5xx/ws2812b_pwm/ws2812b_pwm.c
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)

Copy link
Copy Markdown
Owner

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

{
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};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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
}
}
Loading