STM32使用SPI驱动WS2812灯带
由来
最近有使用ws2812实现大规模灯带的需求,所以研究了一下如何驱动一排排的灯带。
目前网上有开源的WS2812驱动,它是用Arduino实现的,这些实现都使用arduino的io口模拟ws2812的通信时序,因此具有固有的耗时的缺点。WS2812的数据手册描述如下。
When the refresh rate is 30fps, low speed model cascade number are not less than 512 points, high speed mode not less than1024 points.
即在高速模式下,30Fps的帧率可以最多连接1024个LED。
arduino的驱动一方面依靠模拟通信时序,另一方面arduino的单片机性能本来就比较低,所以较难应对高帧率的刷新要求。所以这里决定使用STM32的SPI进行驱动开发。
驱动原理
WS2812灯的驱动时序以800K的速度为例,其采用单线通信的设计,通信协议为非归零编码,每个LED需要24个bit的数据,数据依次经过串联的LED时,第一个LED截取数据开头的24bit,并将剩下的数据流传给下一个LED,以此类推。 那么从这种形式上看,是非常类似SPI的通信时序的,也就是说可以直接使用STM32的SPI外设,只使用MOSI引脚,只要将合适的数据内容丢给SPI,那么SPI就可以输出合适的WS2812通信时序。
结合STM32的DMA功能,就可以将驱动灯带的功能与CPU隔开,可以达到非常高的效率,即:CPU计算一帧数据到缓存 --> 使用DMA将缓存内容发给SPI --> 驱动灯带。
因此这里的重点就在于如何让SPI模拟WS2812通信时序,WS2812的通信时序如下:
可知,0code
和 1code
由一段时间内的高电平时间来区分。因此每个bitcode需要用多个SPIbit来表示,我设计了两种表示格式,如下图,第一种为使用5个SPIbit表示一个bitcode,第二种使用8个SPIbit表示一个bitcode。
为了保证最终通许速率为800k,每一个bitcode持续时间为1.25us,因此,5SPIbit表示法需要4M的SPI速率,8SPIbit表示法需要6.4M的SPI速率。但最后经过实际测试,800k的速度通信时,灯带会存在随机漂移,导致乱码。最后使用8SPIbit表示法,SPI速率在8M,即WS2812通信速度为1M比较合适,不会造成乱码,通信很稳定。
同样地,最后经过实际测试,发现SPI驱动的灯带存在一个bit的偏移,使用逻辑分析仪测量信号发现是SPI默认电平为1导致的,因为WS2812的通信协议中,默认不发信号的电平应当为0。找了半天也没有发现让SPI MOSI信号默认电平为0的配置,所以可以考虑在发送的缓存中填充长度大于50us的0数据,表示复位信号。
有了表示法,即可编写相应的程序进行驱动
程序编写
相关宏定义和结构体,注意下面的两个结构体都是屏幕的原始数据,最终转换出的WS2812码流需要单独申请一块内存,不需要结构体。
struct frame_buf {
struct led_pixel color; // 整个屏幕使用统一颜色
uint8_t pixel_brightness[LED_NUM]; // 每个像素亮度
};
union ws2812_pixel{ // 单个像素的格式
struct {
uint8_t g;
uint8_t r;
uint8_t b;
}color;
uint8_t data[3];
};
#define FIVEBIT_0CODE 0x18
#define FIVEBIT_1CODE 0x1c
#define EIGHTBIT_0CODE 0xc0
#define EIGHTBIT_1CODE 0xf8
转换源码:
/**
* 转换成ws2812缓存
* 有两种转换模式,
* 一种是5个SPI bit 表示一个ws2812bit,要求SPI发送速率为4Mhz,ws2812信号频率为800k
* 一种是8个SPI bit 表示一个ws2812bit,要求SPI发送速率为8Mhz,ws2812信号频率为1M
* 经实测,还是8bit/1M 的模式比较准确,灯带不会误识别造成乱码,
* 因此函数的第四个参数 推荐使用 EIGHTBIT
*/
int convert2ws2812(struct frame_buf* fbuf, uint8_t *ws_buf, uint16_t buf_len, enum spi_format format){
union ws2812_pixel pcolor;
uint8_t *subpixel = NULL;
if (format == FIVEBIT){
ws_buf[0] = 0;
for (uint16_t pos = 0; pos < LED_NUM; pos++) {
// 处理当前像素点颜色
pcolor.color.r = ((uint16_t)fbuf->color.r * fbuf->pixel_brightness[pos]) >> 8;
pcolor.color.g = ((uint16_t)fbuf->color.g * fbuf->pixel_brightness[pos]) >> 8;
pcolor.color.b = ((uint16_t)fbuf->color.b * fbuf->pixel_brightness[pos]) >> 8;
// 转换每个颜色通道
memset(ws_buf + pos * 15, 0, 15);
for(uint16_t i = 0; i < 3; i++) {
subpixel = ws_buf + pos * 15 + i * 5 + 0;
subpixel[0] |= ((pcolor.data[i] & 0x80) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 3;
subpixel[0] |= ((pcolor.data[i] & 0x40) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 2;
subpixel[1] |= ((pcolor.data[i] & 0x40) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 6;
subpixel[1] |= ((pcolor.data[i] & 0x20) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 1;
subpixel[1] |= ((pcolor.data[i] & 0x10) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 4;
subpixel[2] |= ((pcolor.data[i] & 0x10) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 4;
subpixel[2] |= ((pcolor.data[i] & 0x08) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 1;
subpixel[3] |= ((pcolor.data[i] & 0x08) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 7;
subpixel[3] |= ((pcolor.data[i] & 0x04) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 2;
subpixel[3] |= ((pcolor.data[i] & 0x02) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 3;
subpixel[4] |= ((pcolor.data[i] & 0x02) ? FIVEBIT_1CODE : FIVEBIT_0CODE) << 5;
subpixel[4] |= ((pcolor.data[i] & 0x01) ? FIVEBIT_1CODE : FIVEBIT_0CODE) >> 0;
}
}
} else if (format == EIGHTBIT){
ws_buf[0] = 0;
for (uint16_t pos = 0; pos < LED_NUM; pos++) {
// 处理当前像素点颜色
pcolor.color.r = fbuf->color.r * fbuf->pixel_brightness[pos] / UINT8_MAX;
pcolor.color.g = fbuf->color.g * fbuf->pixel_brightness[pos] / UINT8_MAX;
pcolor.color.b = fbuf->color.b * fbuf->pixel_brightness[pos] / UINT8_MAX;
// 转换每个颜色通道
memset(ws_buf + pos * 24, 0, 24);
for(uint16_t i = 0; i < 3; i++) {
subpixel = ws_buf + pos * 24 + i * 8 + 0;
subpixel[0] |= ((pcolor.data[i] & 0x80) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[1] |= ((pcolor.data[i] & 0x40) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[2] |= ((pcolor.data[i] & 0x20) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[3] |= ((pcolor.data[i] & 0x10) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[4] |= ((pcolor.data[i] & 0x08) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[5] |= ((pcolor.data[i] & 0x04) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[6] |= ((pcolor.data[i] & 0x02) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
subpixel[7] |= ((pcolor.data[i] & 0x01) ? EIGHTBIT_1CODE : EIGHTBIT_0CODE);
}
}
} else return -1;
return 0;
}
程序使用
#define WS2812_RESET_HEAD 100 // 100us
main() {
uint8_t *lsp, *ws_buf; // 这里申请两个指针
struct frame_buf fbuf; // 屏幕数据
uint16_t wsbuflen = 24*LED_NUM + 0; // 采用8SPIbit表示法,每一个LED用24*8bit也就是24byte表示
lsp = malloc(LED_SCREEN_PAYLOAD_LEN); // 申请屏幕数据缓存
ws_buf = malloc(wsbuflen + WS2812_RESET_HEAD); // WS2812码流缓存,其中有100us长度的0数据
memset(ws_buf, 0, WS2812_RESET_HEAD); // 把前面的一段填充为0
while(1){
/* 首先通过一个函数根据传入的参数填充好fbuf,该函数对于本文内容不重要,就不展示源码了 */
if(HAL_OK == frame_create(lsp, lsp_recv_count, slave_id, &fbuf, LED_BAR_POLAR_UP))
/* 根据fbuf的内容,以及8SPIbit表示法,填充ws_buf,当然要偏移掉前面的0数据段 */
convert2ws2812(&fbuf, ws_buf + WS2812_RESET_HEAD, wsbuflen, EIGHTBIT);
/* 最后用DMA把ws_buf中的码流发送出去,完成一帧的显示 */
HAL_SPI_Transmit_DMA(&hspi1, ws_buf, wsbuflen);
// 下面这一行的延时可以换成别的内容,因为使用DMA+SPI时,数据发送时不占用CPU时间的。
HAL_Delay(20);
}
}