Contributed by jkernet, curated by Ai-Thinker
Preface
I saw a video by a big shot on Bilibili about a monitor ambience light and thought it was beautiful. Taking advantage of this activity, I decided to make one too!
The WS2812B LED
Searching WS2812B on JLC (LCSC), you can see various models. 
For example, this "XL-5050RGBC-WS2812B" LED: XL is the manufacturer, 5050 is the LED size in millimeters, and RGBC is the color and color temperature it can emit. You can search for details yourself.
Ordinary RGB LEDs have 6 pins; the more LEDs, the more pins are needed, even with a matrix, and control is tedious. WS2812B adds a control chip to an ordinary RGB LED, using a simple serial control protocol, so only 3 pins (power, ground, signal) can control many LEDs.
For example, LED strips and panels are chained head-to-tail by connecting DO (output) to DIN (input). Especially with LED strips, look carefully - it is easy to get confused. Just remember that the board signal output pin must connect to the strip DIN (input).
So how do we make an LED emit a certain color? When it comes to color, RGB comes to mind first - the most common data format for representing colors. Red, Green and Blue are each represented by 1 byte in sequence, ranging from 0-255; the larger the value, the brighter (deeper) the color, and mixing them produces different colors. 1 byte is 8 bits, 3 bytes are 24 bits, also called 24-bit color. For example, (255,0,0) is green; expanded into 24-bit binary data it is "11111111 00000000 00000000". However, this can only represent solid colors, with no transparency. Since transparency is used in many places these days, an extra byte A (Alpha) was added to RGB to represent transparency, making it RGBA. From the figure above, the WS2812B needs 24-bit color data, but not in RGB order - it uniquely uses GRB order. I was puzzled at first, so I asked Tongyi Qianwen, and this is how it answered:
It did not directly answer my question, but I felt what it said made some sense. Let us continue. Note that the positions of green and red are swapped: RGB green (255,0,0) becomes GRB (0,255,0), which expands to the 24-bit binary data "00000000 11111111 00000000".
Now that we know the data format the LED needs, how does the controller (the M61 dev board) send data to the WS2812B? Set the GPIO pin connected to the WS2812B DIO (input) pin as output mode and output high/low levels. As shown above, a 0 bit corresponds to binary 0 and a 1 bit corresponds to binary 1. Looking at the waveform on the right, one high-low pair represents one data bit; the 0 bit has a short high-level duration and a longer low-level duration; the 1 bit has a longer high-level duration and a shorter low-level duration. The table below the figure gives the corresponding durations. Actually, you don't need to care too much about the table data; the notes under the table are the real takeaway. There are 5 points we need to note:
Operation Steps
The duration of the high level determines whether it is a 0 bit or a 1 bit
Keep one full cycle of high+low level around 1.25us
The low level must not exceed 30us, otherwise it may be treated as a reset
It is best to leave margin in the low-level reset time, e.g. 90us
Reduce the duration as much as possible while keeping margin, which improves the rate
System Architecture
After understanding the WS2812B, a rough architecture formed in my mind. For the WS2812B to change according to the computer desktop color, a host program must continuously sample the desktop color and send it to the dev board, which then controls the WS2812 LEDs to emit the corresponding color, as shown below: 
Hardware Connection
WS2812B LED strip, 1 meter, 18 LEDs
Ai-M61-32SU dev board
Be careful to identify the head and tail of the strip; the marking here is a bit confusing. Several times the LEDs did not light because I connected them reversed.
Hand-drawn wiring diagram 
Driving the WS2812B (with a logic analyzer)
Key point!!! Driving the WS2812B is the prerequisite for all features and the goal of this article! There are many driving methods, such as SPI, PWM, GPIO... I chose the most primitive method here: GPIO toggling (switching high/low levels), so as not to occupy limited peripheral resources. The driver code is actually not complex; the key is timing control. From the manual above, the minimum delay the WS2812B needs is around 0.2us. Tested with a logic analyzer, the M61's GPIO toggle speed can reach at least 42ns (the limit of the 24M logic analyzer), but the SDK only provides ms- and us-level delay functions without decimal support, so we need to implement ns-level delays ourselves using "nop" instructions. Clock cycle: The clock cycle, also called the oscillation cycle, is defined as the reciprocal of the clock frequency. It is the most basic and smallest unit of time in a computer. Within one clock cycle, the CPU completes only one "micro-operation". The clock cycle is a measure of time and represents the highest frequency at which SDRAM can run. A smaller clock cycle means a higher operating frequency. Machine cycle: The machine cycle, also called the CPU cycle. In computers, for easier management, the execution of an instruction is often divided into several stages (such as fetch, decode, execute, etc.); each stage completes one basic operation. The time needed to complete one basic operation is called a machine cycle. Generally, one machine cycle consists of several clock cycles. Instruction cycle: The time the CPU takes to fetch and execute an instruction is called an instruction cycle. Since different instructions perform different operations, instruction cycles also differ. The M61 dev board runs at 320MHz, so the clock cycle = 1s/320MHz = 1s/320000000Hz = 0.000000003125s = 0.000003125ms = 0.003125us = 3.125ns. Normally a "nop" is a single-cycle (machine cycle) instruction, but how many clock cycles does one machine cycle take? I went to check the BL616 chip manual but didn't find it (too many documents, and a newbie doesn't know the right keywords). The actual IO toggle speed and the time a "nop" takes can only be measured with a logic analyzer (best connected directly to the tested IO). Let's first test what it looks like without any delay.
#include "board.h"
#include "bflb_gpio.h"
#define TEST_PIN 18
int main(void)
{
board_init();
struct bflb_device_s *gpio;
gpio = bflb_device_get_by_name("gpio");
bflb_gpio_init(gpio, TEST_PIN, GPIO_OUTPUT | GPIO_PULLDOWN);
bflb_gpio_reset(gpio, TEST_PIN);
while (1) {
bflb_gpio_set(gpio, TEST_PIN);
bflb_gpio_reset(gpio, TEST_PIN);
}
}
This is the captured waveform. The code switches from high to low; focus on the high-level duration, which has some jitter but is mostly 42ns. The smaller this value, the faster the IO toggles, but this 24M logic analyzer has a minimum sampling interval of 42ns, so we tried adding "nop" for delay. Writing assembly in C code requires a special modifier. The complete "nop" instruction code: `__ASM __volatile("nop");` ```c #include "board.h" #include "bflb_gpio.h" #define TEST_PIN 18 int main(void) { board_init(); struct bflb_device_s *gpio; gpio = bflb_device_get_by_name("gpio"); bflb_gpio_init(gpio, TEST_PIN, GPIO_OUTPUT | GPIO_PULLDOWN); bflb_gpio_reset(gpio, TEST_PIN); while (1) { bflb_gpio_set(gpio, TEST_PIN); // execute the "nop" instruction; __ASM means assembly, __volatile prevents compiler optimization __ASM __volatile("nop"); bflb_gpio_reset(gpio, TEST_PIN); } } ```
After adding one "nop" instruction for delay, the high-level duration was still 42ns with no change. This shows the measured 42ns is indeed the logic analyzer limit; the actual IO toggle speed is even faster! That is certain, since at 320MHz the clock period is 3.125ns. For now, assume a "nop" takes only one clock cycle; to see a change under the 24M logic analyzer, double the duration: 42ns*2=84ns, 84ns/3.125=26.88, rounded to 27 "nop" instructions. ```c #include "board.h" #include "bflb_gpio.h" #define TEST_PIN 18 int main(void) { board_init(); struct bflb_device_s *gpio; gpio = bflb_device_get_by_name("gpio"); bflb_gpio_init(gpio, TEST_PIN, GPIO_OUTPUT | GPIO_PULLDOWN); bflb_gpio_reset(gpio, TEST_PIN); while (1) { bflb_gpio_set(gpio, TEST_PIN); // execute the "NOP" instruction; __ASM means assembly, __volatile prevents compiler optimization __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); __ASM __volatile("nop"); bflb_gpio_reset(gpio, TEST_PIN); } } ``` .........I was really speechless here. I had written content for a whole afternoon, then carelessly clicked a hyperlink, and after the page jumped, everything was gone!!! I had to revert the code, and the waveform capture project was too big, so I had to omit it! Sorry. The general content is: Operation Steps (continued)
Add more delay. (second high-level duration - first high-level duration) / (second delay count - first delay count) gives the time of one “nop” as about 3.074ns, close to the previously calculated clock period of 3.125ns. Considering the error, we can safely regard one “nop” as 3.125ns, i.e., one clock cycle.
Write a dynamic delay method in units of 100ns to improve the WS2812B driver. I found abnormal waveforms - the first two lasted too long, about 3us. After troubleshooting, it may be caused by the time spent loading code from FLASH for execution; you need to add the ATTR_TCM_SECTION modifier to put the timing-critical method code into the high-speed cache,
The complete library code.
mini_ws2812b.h
#ifndef MINI_WS2812B_H
#include "bflb_core.h"
void ws2812b_delay100ns(uint32_t time);
void ws2812b_flush();
bool ws2812b_isReady();
void ws2812b_deinit();
void ws2812b_init(uint8_t pin, uint32_t num, uint8_t type);
void ws2812b_setColor(uint32_t index, uint8_t *color);
void ws2812b_setColors(uint32_t start, uint32_t end, uint8_t *color);
void ws2812b_setOff(uint32_t index);
void ws2812b_setOffAll();
void ws2812b_setColorAll(uint8_t *color);
void ws2812b_reset();
#endif
mini_ws2812b.c
#include "board.h"
#include "bflb_gpio.h"
#include "hardware/gpio_reg.h"
#include "bflb_clock.h"
#include "mini_ws2812b.h"
// LED type
// 3-color
#define WS2812B_TYPE_GRB 3
// 4-color
#define WS2812B_TYPE_GRBW 4
// instruction cycles needed for 100ns
uint16_t ws2812b_delay_ns = 0;
// LED color data
static uint8_t *ws2812b_addr = NULL;
// LED color data size
static uint32_t ws2812b_size = 0;
// LED count
static uint32_t ws2812b_num = 0;
// LED type
static uint8_t ws2812b_type = WS2812B_TYPE_GRB;
// data pin
static uint8_t ws2812b_pin = 0;
// GPIO control
static struct bflb_device_s *ws2812b_gpio;
static uint32_t ws2812b_pin_addr = 0;
static uint32_t ws2812b_pin_high = 0;
static uint32_t ws2812b_pin_low = 0;
// nanosecond delay, unit: 100ns
// ATTR_TCM_SECTION puts this method into the high-speed cache to improve efficiency and ensure delay accuracy.
void __WEAK ATTR_TCM_SECTION ws2812b_delay100ns(uint32_t time)
{
// time *= ws2812b_delay_ns;
// // subtract the extra 4 instructions
// time -= 4;
// while (time)
// {
// __ASM __volatile("nop");
// time--;
// }
__ASM __volatile(
"lhu a5,ws2812b_delay_ns\n\t"
"mul a0,a5,a0\n\t"
"addi a0,a0,-4\n\t"
"beqz a0,wsdlend\n\t"
"wsdlloop:"
"nop\n\t"
"addi a0,a0,-1\n\t"
"bnez a0,wsdlloop\n\t"
"wsdlend:"
"ret\n\t");
}
// send data
// ATTR_TCM_SECTION puts this method into the high-speed cache to improve efficiency and ensure timing accuracy.
void ATTR_TCM_SECTION ws2812b_flush()
{
uint8_t *addr;
uint8_t value;
// iterate over all LEDs
for (uint32_t i = 0; i < ws2812b_num; i++)
{
// calculate the color data address of LED i
addr = ws2812b_addr + i * ws2812b_type;
// iterate over each color value
for (uint8_t j = 0; j < ws2812b_type; j++)
{
// get the color value, 1 byte
value = *(addr + j);
// iterate over each bit
for (uint8_t k = 0; k < 8; k++)
{
// 0x80 in binary is 1000 0000; ANDing value with it checks whether the highest bit is 1
if (value & 0x80)
{
// 1 bit
// output 0.6us low level
putreg32(ws2812b_pin_addr, ws2812b_pin_high);
ws2812b_delay100ns(6);
// output 0.3us low level
putreg32(ws2812b_pin_addr, ws2812b_pin_low);
ws2812b_delay100ns(3);
}
else
{
// 0 bit
// output 0.2us high level
putreg32(ws2812b_pin_addr, ws2812b_pin_high);
ws2812b_delay100ns(2);
// output 0.7us low level
putreg32(ws2812b_pin_addr, ws2812b_pin_low);
ws2812b_delay100ns(7);
}
// little-endian, high->low
// shift value left by 1 bit; e.g., 1111 1111 shifted left becomes 1111 1110, moving everything left, dropping the high bit and adding a 0 at the low bit
value <<= 1;
}
}
}
}
// #pragma GCC pop_options
// initialize
void ws2812b_init(uint8_t pin, uint32_t num, uint8_t type)
{
ws2812b_deinit();
// calculate the instruction cycles needed for 100ns based on the main frequency
ws2812b_delay_ns = 100.0 / (1000000000.0 / bflb_clk_get_system_clock(BFLB_SYSTEM_CPU_CLK)) / 3.0;
ws2812b_pin = pin;
ws2812b_num = num;
ws2812b_type = type;
ws2812b_size = num * type;
// do we need to consider alignment?
ws2812b_addr = malloc(ws2812b_size);
memset(ws2812b_addr, 0, ws2812b_size);
// initialize the control IO
ws2812b_gpio = bflb_device_get_by_name("gpio");
bflb_gpio_init(ws2812b_gpio, ws2812b_pin, GPIO_OUTPUT | GPIO_PULLDOWN | GPIO_SMT_DIS | GPIO_DRV_0);
ws2812b_pin_addr = 1 << (ws2812b_pin & 0x1f);
// bflb_gpio_set
ws2812b_pin_high = ws2812b_gpio->reg_base + GLB_GPIO_CFG138_OFFSET + ((ws2812b_pin >> 5) << 2);
// bflb_gpio_reset
ws2812b_pin_low = ws2812b_gpio->reg_base + GLB_GPIO_CFG140_OFFSET + ((ws2812b_pin >> 5) << 2);
// reset
ws2812b_reset();
}
// de-initialize
void ws2812b_deinit()
{
if (!ws2812b_isReady())
{
return;
}
bflb_gpio_deinit(ws2812b_gpio, ws2812b_pin);
free(ws2812b_addr);
ws2812b_addr = NULL;
}
// whether initialized
bool ws2812b_isReady()
{
return ws2812b_addr != NULL;
}
// set a single LED color
// index: LED index, starting from 0
// color: LED color
void ws2812b_setColor(uint32_t index, uint8_t *color)
{
memcpy(ws2812b_addr + index * ws2812b_type, color, ws2812b_type);
}
// set colors for multiple consecutive LEDs
// start: starting LED index
// end: ending LED index
// color: LED color
void ws2812b_setColors(uint32_t start, uint32_t end, uint8_t *color)
{
for (uint32_t i = start; i <= end; i++)
{
ws2812b_setColor(i, color);
}
}
// set all LED colors
void ws2812b_setColorAll(uint8_t *color)
{
ws2812b_setColors(0, ws2812b_num - 1, color);
}
// turn off a single LED
// index: LED index, starting from 0
void ws2812b_setOff(uint32_t index)
{
memset(ws2812b_addr + index * ws2812b_type, 0, ws2812b_type);
}
// turn off all LEDs
void ws2812b_setOffAll()
{
memset(ws2812b_addr, 0, ws2812b_size);
}
// reset (only resets the send state; does not clear the current LED states)
void ws2812b_reset()
{
putreg32(ws2812b_pin_addr, ws2812b_pin_low);
ws2812b_delay100ns(1000);
}
// example
void ws2812b_test()
{
// initialize ws2812b: data pin 18, 1 LED, type GRB
ws2812b_init(18, 1, WS2812B_TYPE_GRB);
// color variable, blue
uint8_t color[3] = {0, 0, 255};
// set the first LED to blue
ws2812b_setColor(0, color);
// send data to light up the LEDs
ws2812b_flush();
}
The measured data is within the error range.
Successfully lit up a little blue star.Communication Method
The M61-32S is a dev board focused on WiFi/Bluetooth. The first idea was to use sockets, but that requires a provisioning process to establish communication, which is a bit tedious. Thinking carefully about the usage scenario - the device is used with a computer and mostly powered by the host USB - why not just communicate over USB directly? After researching USB device communication protocols, I found a pitfall: the M61 dev board has only one USB port, connected to a CH340 USB-to-serial chip, so it cannot be switched to USB device mode... (external wiring is too troublesome, so skip it). We'll have to make do with serial communication. Note that this conflicts with the SDK's own console feature, so you need to disable the "console_init" initialization function in the "board_init" function first.
Communication Protocol
Data uses uppercase hexadecimal text; the character range is "0123456789ABCDEF", i.e., "48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70" Control markers use decimal; 0-47 and 71-255 can be used, which is completely sufficient. Configuration command: configuration start marker + LED count + control pin + LED type + configuration end marker 1 00000003 12 03 2 Data command: data start marker + [index + green + red + blue ...] + data end marker 10 00000000 00 00 FF 11
Host Software
Written in E-language (a Chinese programming language); usage: 



Effect Demo
Notes
If you use more than 8 LEDs, consider a separate power supply, otherwise anomalies may occur!
Have questions?
For other questions, please visit the unified discussion area: Ai-Thinker Discussions

