rpine lab Tech Blog

Tech blog covering a wide range of topics, including my hobby of programming (web and backend), setting up a home lab, and electronics projects using microcontrollers.

🚦Driving a WS2812B RGB LED (NeoPixel) with the CH32V003 Microcontroller

Driving a WS2812B RGB LED (NeoPixel) with the CH32V003 Microcontroller
Table of contents
🤖
This article was AI-translated. Some nuances may differ from the original Japanese version.

Introduction

In this article I drive a WS2812B RGB LED (commonly known as NeoPixel) from a CH32V003 microcontroller.

I use MounRiver Studio Ⅱ (the official IDE) with the official SDK, and for LED control I use the WS2812B driver library (SPI + DMA) from the community library ch32fun.

Hardware Used

  • Microcontroller: CH32V003F4P6 (SSOP-20) (Akizuki Denshi, AliExpress)
  • Debugger and programmer: WCH-LinkE (Akizuki Denshi, AliExpress)
  • RGB LED: WeAct Studio WS2812B breakout board x1
    I used the single-chip breakout board type because it is easy to handle
🌏
Akizuki Denshi is a Japanese electronic parts shop. Outside Japan, the CH32V003F4P6 and WCH-LinkE are also available from LCSC, AliExpress, and similar distributors.

Development Environment

  • IDE: MounRiver Studio Ⅱ (the official VSCode-based development environment)
  • SDK: official SDK (bundled with MRS2)

Wiring

Since the LED is driven by the microcontroller's built-in SPI peripheral, the control signal is output on the SPI MOSI pin (PC6).

Connections:

  • Control signal input (DIN) → MOSI PC6 (pin 16)
  • Power → 5 V supply (from the WCH-LinkE)
  • GND → GND
CH32V003F4P6 pinout (from the CH32V003 datasheet)
CH32V003F4P6 pinout (from the CH32V003 datasheet)

Assembled on a mini breadboard, it looks like this. The RGB LED module is at the front right.

The actual wiring
The actual wiring

About the NeoPixel Control Signal

NeoPixel-type LEDs receive color data through their own timing-based signal. A data 0 or 1 is represented by the lengths of the High and Low periods.

0/1/Reset signal timing diagram (from the WS2812B datasheet)
0/1/Reset signal timing diagram (from the WS2812B datasheet)
Timing table corresponding to the timing diagram (from the WS2812B datasheet)
Timing table corresponding to the timing diagram (from the WS2812B datasheet)

Since a High pulse as short as 400 ns (2.5 MHz) has to be sent, the timing constraints are tight, and generating this signal from a microcontroller requires a special implementation.

There are broadly two ways to drive NeoPixel-type LEDs from a microcontroller.

  1. Direct GPIO control (bit-banging): adjust the timing with NOP instructions and toggle the GPIO on and off in software
    • Any GPIO pin can be used
    • Since the timing is on the order of 0.1 μs, delay functions are basically unusable, and fine timing adjustment by the number of NOP instructions is required (time for the oscilloscope or logic analyzer)
    • Because it is done in software, the CPU is fully occupied while the signal is being sent. Interrupts cannot be used during that time because they would throw off the timing
  2. Using the microcontroller's built-in peripherals (a timer or SPI)

    In the SPI case, you prepare special output data with the bits arranged so that the required signal is produced, and use DMA (Direct Memory Access) to transfer it sequentially to the peripheral's data register, which generates the signal.

    • Signal generation is offloaded to the peripheral, so the CPU is not occupied
    • Only the specific pins tied to the peripheral output can be used
    • DMA is almost mandatory, which makes the implementation more complex. Without DMA, the software has to feed the data one piece at a time and the advantage fades.

This time I take the latter approach and generate the control signal with the CH32V003's SPI peripheral and DMA.

Fortunately, ch32fun, an open-source SDK for CH32V, has a WS2812B driver library, so I reuse it.

Preparing the NeoPixel LED Driver Library

Place ws2812b_dma_spi_led_driver.h from this link in your project's User folder. This library consists of a single header file, which you include from main.c or wherever you use it.

Some register definitions seem to differ between the official SDK and ch32fun, so the following parts of the code need to be changed.

Line 233
< 	GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP_AF)<<(4*6);
---
> 	GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_Mode_AF_PP)<<(4*6);
Line 244
< 	SPI1->CTLR1 |= CTLR1_SPE_Set;
---
> 	SPI1->CTLR1 |= SPI_CTLR1_SPE;

Program

I write a test program while referring to the following sample program.

ch32fun/examples/ws2812bdemo at master · cnlohr/ch32fun

Including the Library

Near the top of the file where you use the library (main.c if you call it from the main function), include the library header as follows.

#include "debug.h" // Write the lines below under this original include

#define WS2812DMA_IMPLEMENTATION
#define WSGRB // For WS2812B (order in which RGB data is sent)

#include "ws2812b_dma_spi_led_driver.h"

Callback Function for Setting the LED Color

Add the callback function WS2812BLEDCallback, which is called from the interrupt handler inside the library. This function receives the LED position ledno and must return a 24-bit value containing the color, 8 bits each in R, G, B order starting from the low bits.

To keep things simple, it just returns a single global variable ledVal for every LED. This global variable is updated from the main function written next.

static volatile uint32_t ledVal = 0; // Color value (global variable)

// Callback function for setting the color
uint32_t WS2812BLEDCallback(int ledno)
{
    return ledVal; // Return a 24-bit RGB value
}

Main Routine

Write the main routine in the main function. Initialize with WS2812BDMAInit, then call WS2812BDMAStart to light the LED in the color set in ledVal.

This assumes the debug feature is in use, so remove the printf lines if you do not need them.

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
    SystemCoreClockUpdate();
    Delay_Init();
#if (SDI_PRINT == SDI_PR_OPEN)
    SDI_Printf_Enable();
#else
    USART_Printf_Init(115200);
#endif
    printf("SystemClk:%d\r\n",SystemCoreClock);
    printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );

    // Initialization function
    WS2812BDMAInit();

    // Lighting patterns
    static const uint32_t ledPatterns[] = {
        0x00000000, // Off
        0x000000FF, // Red
        0x0000FF00, // Green
        0x00FF0000, // Blue
        0x0000FFFF, // R + G = Yellow
        0x00FFFF00, // G + B = Cyan
        0x00FF00FF, // R + B = Magenta
    };

    // Show each color in turn
    for (unsigned int i = 0; i < sizeof(ledPatterns) / sizeof(ledPatterns[0]); i++) {
        // Assign the color value to the global variable
        ledVal = ledPatterns[i];
        
        // Debug output
        printf("LED val: %08X\r\n", ledVal);
        
        // Start the DMA transfer (1 = number of LEDs)
        WS2812BDMAStart(1);

        // Wait
        Delay_Ms(500);
    }
}

Checking the Result

This is what it looks like when the program runs. (Everything is set to 0xFF for maximum brightness, so it is quite bright.)

As the program runs, you can see the LED light up in the order red → green → blue → yellow → cyan → magenta.

Running the test program
Running the test program

Summary

I was able to drive a WS2812B LED with the CH32V003 microcontroller. Only one LED is connected this time, but this library can also drive multiple LEDs, such as an LED strip.

I have not yet decided what to build with this microcontroller, but next I would like to try I2C.

References

If you found this article helpful, please consider supporting me!