Contributed by 爱笑, organized by Ai-Thinker
[Ai-WB2 Intermediate] SPI and WS2812B Driver
Update: you can refer to the following two updated posts
SPI and WS2812B Driver The Serial Peripheral Interface Bus (SPI) is a synchronous serial communication interface specification for short-distance communication, in which devices communicate in full-duplex mode in a master-slave configuration with one master and one or more slaves. SPI uses 4 wires for full-duplex communication: CS (chip select), SCLK (clock), MOSI (master output, slave input), and MISO (master input, slave output). This article will detail how to use the Ai-WB2 SPI module to drive the WS2812B.
Click to expand full code
**一:Ai-WB2的SPI介绍**
**BL602的SPI具有如下特性:**
**·** 既可作为SPI主设备,也可作为SPI从设备
**·** 主从设备都支持4种工作模式(CPOL,CPHA)
**·** 主从设备都支持112/3/4字节传输模式
**·** 发送和接收通道各有深度为32个字节的 FIFOQ
**·** 自适应的FIFO深度变化特性,适配高性能的应用场景 · When the frame is 32 bits, the FIFO depth is 8
· When the frame is 24 bits, the FIFO depth is 8
· When the frame is 16 bits, the FIFO depth is 160
· When the frame is 8 bits, the FIFO depth is 32
Click to expand full code
**·** 可调整每个Frame的字节传输顺序
**·** 可配置每个字节内的MSB/LSB优先传输
**·** 灵活的时钟配置,最高可支持80M时钟
**·** 接收忽略功能,可以忽略对每个Frame指定位置数据的接收
**·** 支持从设备模式下的超时机制
**·** 支持DMA传输模式
**SPI的时序控制**Depending on the clock polarity and phase settings, the SPI clock has four modes, configurable via the cr_spi_sck_pol (CPOL) and cr_spi_sclk_ph (CPHA) bits of the spi_config register. CPOL determines the idle level of the SCK clock signal: CPOL=0 (cr_spi_sck_pol=0) means the idle level is low, and CPOL=1 (cr_spi_sck_pol=1) means the idle level is high. CPHA determines the sampling edge: CPHA=0 (cr_spi_sclk_ph=1) samples on the first clock edge of each cycle, and CPHA=1 (cr_spi_sck_ph=0) samples on the second clock edge of each cycle. By setting the spi_prd_0 and spi_prd_1 registers, you can also adjust the duration of the clock start and stop levels, the phase 0/1 time within each cycle, and the interval between frames. The specific settings for the four modes are shown in the figure below:

The meanings of the numbers are as follows:
Click to expand full code
**·** 1是起始条件的长度,通过寄存器spi_prd_0中的cr_spi_prd_s进行配置。
**·** 2是停止条件的长度,通过寄存器spi_prd_0中的cr_spi_prd_p进行配置。
**·** 3是相位0的长度,通过寄存器spi_prd_0中的cr_spi_prd_d_ph_0进行配置。
**·** 4是相位1的长度,通过寄存器spi_prd_0中的cr_spi_prd_d_ph_1进行配置。
**·** 5是每帧数据之间的间隔,通过寄存器spi_prd_1中的cr_spi_prd_i进行配置。
**主设备连续传输模式**After enabling this mode, when there is still data to send in the TX FIFO after the current frame has been sent, the CS signal will not be pulled high. Master/Slave Data Transmission and Reception The frame size for data transmission and reception (8/16/24/32-bit) can be set via the cr_spi_frame_size bit of the spi_config register, and the master and slave devices should keep the same frame size. If the master and slave agree to communicate with a 32-bit frame size, and the master's clock fails to provide 32 cycles in a certain frame due to an anomaly, the following will happen:
Click to expand full code
**·** 主设备当前发送的这帧数据不会进入从设备的 RX FIFO 中,而是被丢弃,从设备当前发送的数据也不会进入主设备的 RX FIFO 中·。
**·** 从设备会认为当前数据帧已经发送结束,等下次主设备 cIk 正常,继续发送下一帧数据。
**接收忽略功能**By setting the ignore start bit and end bit, SPI discards the corresponding data segment in each received frame, as shown in the figure below:

Enable the ignore function via the cr_spi_rxd_ignr_en bit of the spi_config register. Set the start bit of the ignore function via cr_spi_rxd_ignr_s in the spi_rxd_ignr register. Set the end bit of the ignore function via cr_spi_rxd_ignr_p in the spi_rxd_ignr register. As shown in the figure above, if the ignore start bit is set to 0 and the end bit to 7, the Dummy Byte will be received; if the end bit is set to 15, the Dummy Byte will be discarded. SPI Filtering Function By enabling this function and setting a threshold, SPI filters out data whose width is less than or equal to the threshold. Assuming the SPI top clock is 160 MHz and the threshold is set to 4, data with a width below (4/160MHz = 25ns) will be filtered out. This function is enabled by the cr_spi_deg_en bit of the spi_config register, and the threshold can be set via cr_spi_deg_cnt. The filtering process is shown in the figure below, assuming cr_spi_deg_cnt is set to 4, with input as the original data and output as the filtered data. The filtering logic is:
Click to expand full code
**·** tgl为input和output的异或结果。
**·** deg_cnt从0开始计数,计数条件为tgl为高电平,并且reached为低电平。
**·** 若deg_cnt计数值达到cr_urx_deg_cnt设置的值时,reached为高电平。
**·** 当reached为高电平时,将input输出到output。
**·** 注释:deg_cnt自加的条件:tgl为高电平且reached为低电平,其余情况下deg_cnt会被清0。
Adjustable Byte Transmission Order This function only adjusts the priority transmission order of different bytes within each frame. It is configured via the cr_spi_byte_inv bit of the spi_config register: 0 means the low byte is sent first, and 1 means the high byte is sent first. Taking a frame size of 24 bits as an example with the data format Data[23:0] = 0x123456: when sending the low byte first, the transmission order is 0x56 (1st byte: low byte); 0x34 (2nd byte: middle byte); 0x12 (3rd byte: high byte). When sending the high byte first, the transmission order is 0x12 (3rd byte: high byte); 0x34 (2nd byte: middle byte); 0x56 (1st byte: low byte). The byte transmission order adjustment function can be used together with the MSB/LSB transmission configuration function. Configurable MSB/LSB Priority per Byte This function only sets the priority transmission order of the 8 bits within each byte, configured via the cr_spi_bit_inv bit of the spi_config register. 0 means MSB-First and 1 means LSB-First. Again taking a frame size of 24 bits as an example with the data format Data[23:0] = 0x123456: When MSB-First is set, the transmission order is: 01010110 (binary, 1st byte: 0x56); 00110100 (binary, 2nd byte: 0x34); 00010010 (binary, 3rd byte: 0x12). When LSB-First is set, the transmission order is: 01101010 (binary, 1st byte: 0x56); 00101100 (binary, 2nd byte: 0x34); 01001000 (binary, 3rd byte: 0x12). Slave Mode Timeout Mechanism The timeout threshold can be set via the spi_sto_value register. When SPI is in slave mode and detects that CS is pulled low, it starts timing. If no clock signal is received within the time corresponding to the timeout threshold, a timeout interrupt is triggered. I/O Transfer Mode The CPU can respond to interrupts from the FIFO to perform FIFO fill and drain operations. Each FIFO has a programmable trigger threshold for generating interrupts. When rx_fifo_cnt in the spi_fifo_config_1 register is greater than the rx_fifo_th trigger threshold, an RX request interrupt is generated, notifying the CPU to read data from the RX FIFO. When tx_fifo_cnt in the spi_fifo_config_1 register is greater than tx_fifo_th, a TX request interrupt is generated, notifying the CPU to fill the TX FIFO. The sampled values in the FIFO and the FIFO status can be determined by querying the FIFO status register. You need to ensure correct RX FIFO and TX FIFO trigger thresholds to prevent FIFO overflow or underflow. DMA Transfer Mode SPI supports DMA transfer mode. To use this mode, you need to set the thresholds of the TX and RX FIFOs. Set spi_dma_tx_en in the spi_fifo_config_0 register to 1 to enable DMA send mode, and set spi_dma_rx_en in the spi_fifo_config_0 register to 1 to enable DMA receive mode. Once this mode is enabled, SPI checks the TX/RX FIFOs. When tx_fifo_cnt/rx_fifo_cnt in the spi_fifo_config_1 register is greater than tx_fifo_th/rx_fifo_th, a DMA request is issued, and the DMA moves data into the TX FIFO or out of the RX FIFO as configured. SPI Interrupts SPI has rich interrupt control, including the following interrupt modes:
Click to expand full code
**· SPI传输结束中断**
**·** 在主模式下,SPI传输结束中断会在每帧数据传输结束时触发。
**·** 在从模式下,SPI传输结束中断会在 CS 信号被拉高时触发。
**· TX FIFO 请求中断**
**·** TX FIFO 请求中断会在其 FIFO 可用计数值大于设定的阈值时触发,当条件不满足时该中断标志会自动清除。
**· RX FIFO 请求中断**
**·** RX FIFO 请求中断会在其 FIFO 可用计数值大于设定的阈值时触发,当条件不满足时该中断标志会自动清除。
**· 从模式传输超时中断**
**·** 从模式传输超时中断会在从模式下检测到CS拉低之后,超过超时门限值对应的时间后仍未收到时钟信号时触发。
**· 从模式 TX 过载中断**
**·** 从模式 TX 过载中断会在从模式下 TX 没有准备好数据传输而时钟信号却已经到来时触发。
**· TX/RX FIFO 溢出中断**
**·** 如果 TX/RX FIFO 发生了上溢或者下溢,会触发 TX/RX FIFO 溢出中断,当 FIFO 清除寄存器 spi_fifo_config_0 中的tx_fifo_clr/rx_fifo_clr 被置1时,对应的 FIFO 会被清空,同时溢出中断标志会自动清除。You can query the interrupt status via the SPI_INT_STS register and clear the interrupts by writing 1 to the corresponding bits. 2. SPI Driver API Introduction The HOSAL-layer driver API of the BL602 SPI is defined in components/platform/hosal/include/hosal_spi.h. The commonly used APIs are as follows: · int hosal_spi_init(hosal_spi_dev_t *spi): SPI initialization. Parameter description: · spi: SPI device instance. It is defined as follows:
Click to expand full code
typedef struct {
uint8_t port; /**< spi 端口 */
hosal_spi_config_t config; /**< spi 配置 */
hosal_spi_irq_t cb; /**< spi 中断回调函数 */
void *p_arg; /**< 中断回调函数参数 */
void *priv; /**< 用户自定义数据 */
} hosal_spi_dev_t;The callback function is defined as follows:
- typedef void (*hosal_spi_irq_t)(void *parg);
Click to expand full code
**·** 返回值:成功时,返回0;否则返回非零。
**·** int hosal_spi_ send(hosal_spi _dev_t *spi, const uint8_t*data, uint16_t size, uint32_t timeout):SPI发送数据。参数说明如下:
**·** spi: SPl设备
**·** data:需要发送的数据
**·** size:发送数据长度
**·** timeout:通信时长。以毫秒为单位
**·** 返回值:成功时,返回0;否则返回非零。
**·** int hosal_spi _recv(hosal_spi_dev _t *spi, uint8_t*data, uint16_t size, uint32_t timeout):SPI接收数据。参数说明如下:
**·** spi: SPl设备
**·** data:接收数据缓存
**·** size:接收数据长度
**·** timeout:通信时长。以毫秒为单位
**·** 返回值:成功时,返回0;否则返回非零。
**·** int hosal_spi_send_recv(hosal_spi_dev_t*spi, uint8_t *tx_data, uint8_t*rx_data, uint16_t size, uint32_t timeout):发送并接收数据。参数说明如下:
**·** spi:SPl设备
**·** tx_data:发送数据
**·** rx_data:接收数据
**·** size:数据长度
**·** timeout:通信时长。以毫秒为单位
**·** 返回值:成功时,返回0;否则返回非零。· int hosal_ spi_irq_callback_set(hosal_spi_dev_t*spi, hosal_ spi_irg_t pfn, void *p_arg): Set the SPI interrupt callback function. Parameter description:
Click to expand full code
**·** spi:SPl设备
**·** pfn:回调函数
**·** p_arg:回调函数参数
**·** 返回值:成功时,返回0;否则返回非零。
**·** int hosal_spi_set_cs(uint8_t pin,uint8_t value):设置片选引脚电平。参数说明如下:
**·** pin:CS引脚
**·** value:电平值。1表示高电平:0表示低电平
**·** 返回值:成功时,返回0;否则返回非零。
**·** int hosal_spi_finalize(hosal_spi_dev_t*spi):销毁SPI设备实例,并释放相关资源。参数说明如下:
**·** spi:SPl设备实例
**·** 返回值:成功时,返回0;否则返回非零。
**三:WS2812B介绍**The WS2812 uses a single-wire communication design with a non-return-to-zero encoding protocol. Each LED requires 24 bits of data. As the data passes through the daisy-chained LEDs in sequence, the first LED intercepts the first 24 bits of the data stream and passes the remaining data to the next LED, and so on. Each bit on the data line is encoded by a high pulse followed by a low pulse. The timing is as follows:

The WS2812 supports high-speed data transmission, and its data transmission timing is similar to the SPI communication timing, so the BL SPI peripheral can be used to emulate the WS2812 communication timing. 4. WS2812B SPI Driver Implementation
- Define the WS2812B driver. Create a ws2812_spi.h file and add the following content:
Click to expand full code
#ifndef __WS2812B_SPI_H__
#define __WS2812B_SPI_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include "hosal_spi.h"
#define WS2812B_USE_DMA 1
/**
* @brief RGB颜色定义
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} color_t;
typedef struct {
uint16_t id; // 编号
uint16_t led_counts; // LED数量
uint8_t *color_datas; // 24位颜色数据
uint16_t color_data_size; // 颜色数据长度
color_t *led_colors; // LED颜色
bool inited; // 是否初始化,1表示已经初始化,0则表示未初始化
} ws2812b_t;
/**
* @brief 初始化
* @param ws2812b WS2812对象
*/
void ws2812b_init(ws2812b_t *ws2812b);
/***
* @brief 释放WS2812占用内存
*/
void ws2812b_release(ws2812b_t* ws2812b);
/**
* @brief 设置指定位置WS2812B灯珠颜色
* @param ws2812b WS2812对象
* @param index WS2812灯珠位置
* @param color 颜色
*/
void ws2812b_set_color(ws2812b_t *ws2812b, uint16_t index, color_t color);
/**
* @brief 打开WS2812B显示颜色
* @param ws2812b WS2812B对象
*/
void ws2812b_show(ws2812b_t *ws2812b);
#ifdef __cplusplus
}
#endif
#endif //__WS2812B_SPI_H__- Implement the WS2812 driver. Create a ws2812_spi.c file and add the following content:
Click to expand full code
#include "ws2812_spi.h"
#include "blog.h"
#include <stdlib.h>
#include <stdio.h>
// SPI数据为0的时序
#define SPI_NEO0 ((uint8_t) 0b11000000)
// SPI数据为1的时序
#define SPI_NEO1 ((uint8_t) 0b11111100)
#define BITS_PER_LED_COLOR (sizeof(color_t) * 8)
#if (WS2812B_USE_DMA == 1)
ws2812b_t * __g_ws2812b__ = NULL;
#endif
hosal_spi_dev_t spi;
void spi_master_cb(void *arg)
{
blog_info("master send complete\r\n");
}
static bool spi_init(void){
/* spi port set */
spi.port = 0;
/* spi master mode */
spi.config.mode = HOSAL_SPI_MODE_MASTER;
#if (WS2812B_USE_DMA == 1)
/* 1: enable dma, 0: disable dma */
spi.config.dma_enable = 1;
#else
spi.config.dma_enable = 0;
#endif
/* 0: phase 0, polarity low
* 1: phase 1, polarity low
* 2: phase 0, polarity high
* 3: phase 0, polarity high
*/
spi.config.polar_phase= 0;
/* 0 ~ 40M */
spi.config.freq= 8000000;
spi.config.pin_clk = 3;
/* hardware cs now is pin 2 */
spi.config.pin_mosi= 4;
spi.config.pin_miso= 5;
/* init spi device */
hosal_spi_init(&spi);
/* register trans complete callback */
hosal_spi_irq_callback_set(&spi, spi_master_cb, (void*)&spi);
return true;
}
void ws2812b_init(ws2812b_t *ws2812b) {
if(ws2812b == NULL){
return;
}
#if (WS2812B_USE_DMA == 1)
__g_ws2812b__ = ws2812b;
#else
ws2812b->inited = spi_init();
#endif
// 分配24位颜色数据内存
ws2812b->color_datas = (uint8_t*) malloc(
ws2812b->led_counts * BITS_PER_LED_COLOR + 32);
// 分配LED颜色数据内存
ws2812b->led_colors = (color_t*) malloc(
sizeof(color_t) * ws2812b->led_counts);
if (ws2812b->color_datas && ws2812b->led_colors) {
ws2812b->inited = true;
ws2812b->color_data_size = ws2812b->led_counts * BITS_PER_LED_COLOR
+ 32;
} else {
ws2812b->inited = false;
}
}
void ws2812b_release(ws2812b_t* ws2812b){
if(!ws2812b->inited){
return;
}
// 释放内存
free(ws2812b->led_colors);
ws2812b->led_colors = NULL;
free(ws2812b->color_datas);
ws2812b->color_datas = NULL;
ws2812b->led_counts = 0;
ws2812b->inited = false;
}
// 生成24位颜色数据序列
void __build_led_color_data(ws2812b_t *ws2812b) {
for (int i = 0; i < ws2812b->led_counts; i++) {
uint8_t m = 0b10000000;
for (int b = 0; b < 8; b++) {
ws2812b->color_datas[BITS_PER_LED_COLOR * i + b] =
ws2812b->led_colors[i].g & m ? SPI_NEO1 : SPI_NEO0;
m >>= 1u;
}
m = 0b10000000;
for (int b = 0; b < 8; b++) {
ws2812b->color_datas[BITS_PER_LED_COLOR * i + b + 8] =
ws2812b->led_colors[i].r & m ? SPI_NEO1 : SPI_NEO0;
m >>= 1u;
}
m = 0b10000000;
for (int b = 0; b < 8; b++) {
ws2812b->color_datas[BITS_PER_LED_COLOR * i + b + 16] =
ws2812b->led_colors[i].b & m ? SPI_NEO1 : SPI_NEO0;
m >>= 1u;
}
}
}
void ws2812b_set_color(ws2812b_t *ws2812b, uint16_t index, color_t color) {
if (index >= ws2812b->led_counts && ws2812b->inited) {
return;
}
ws2812b->led_colors[index] = color;
}
void ws2812b_show(ws2812b_t *ws2812b) {
if (!ws2812b->inited) {
return;
}
__build_led_color_data(ws2812b);
#if (WS2812B_USE_DMA == 0)
// hosal_spi_set_cs(14, 0);
hosal_spi_send(&spi, ws2812b->color_datas,ws2812b->color_data_size,1000);
//hosal_spi_set_cs(14, 1);
#else
#endif
}- Test the driver. Create a main.c file and add the following content:
Click to expand full code
#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <stdbool.h>
#include <blog.h>
#include "hosal_spi.h"
#include "ws2812_spi.h"
ws2812b_t ws2812b = {
.id = 1,
.inited = false,
.led_counts = 9,
};
color_t RED = { 255, 0, 0 };
color_t GREEN = { 0, 255, 0 };
color_t BLUE = { 0, 0, 255 };
void ws2812b_task(void* params){
printf("ws2812b task start...\r\n");
while(true){
for(size_t i = 0;i < ws2812b.led_counts;i++){
ws2812b_set_color(&ws2812b, i, RED);
}
ws2812b_show(&ws2812b);
vTaskDelay(500);
for(size_t i = 0;i < ws2812b.led_counts;i++){
ws2812b_set_color(&ws2812b, i, GREEN);
}
ws2812b_show(&ws2812b);
vTaskDelay(500);
for(size_t i = 0;i < ws2812b.led_counts;i++){
ws2812b_set_color(&ws2812b, i, BLUE);
}
ws2812b_show(&ws2812b);
vTaskDelay(500);
}
}
void main(void) {
printf("start to init spi master...\r\n");
ws2812b_init(&ws2812b);
if(ws2812b.inited){
xTaskCreate(ws2812b_task, "ws2812b_task", 1024, NULL, 15, NULL);
}
}
