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的通信时序如下:

image-20210531221246754

可知,0code1code 由一段时间内的高电平时间来区分。因此每个bitcode需要用多个SPIbit来表示,我设计了两种表示格式,如下图,第一种为使用5个SPIbit表示一个bitcode,第二种使用8个SPIbit表示一个bitcode。

image-20210531223040274

为了保证最终通许速率为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);
    }
}
posted @ 2021-05-31 23:06  Gentleaves  阅读(588)  评论(0编辑  收藏  举报