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);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM