SPI驅動WS2812全彩LED
接口通信格式
WS2812/WS2812B LED 使用 24 bit 數據調節 RGB 色彩, 每個 bit 都是通過(一個高電平 + 一個低電平)表示的.



根據手冊
? 0 表示為一個短的(0.35 μs)高電平加一個長的(0.90 μs)低電平
? 1 表示為一個長的(0.90 μs)高電平加一個短的(0.35 μs)低電平
? 單個 bit 信號周期, 高低電平時長合計為 1.25 μs
? 發送超過 24 bit 信號后, 之前輸入的信號會依次傳遞給串行的下一個 WS2812 LED
? 控制器發送數據前需要保持低電平超過 50 μs(又稱為 RESET), 用于通知 WS2812 開始接收數據.
根據上面的信息, 對單顆LED發送數據, 需要的時間為
24 × 1.25 μ s + 50 μ s = 80 μ s 24 × 1.25 μs + 50 μs = 80 μs24×1.25μs+50μs=80μs
對于8顆LED, 需要的時間為
8 × 24 × 1.25 μ s + 50 μ s = 290 μ s 8 × 24 × 1.25 μs + 50 μs = 290 μs8×24×1.25μs+50μs=290μs
實際的通信時間間隔要求
當傳輸信號時, 高低電平時間間隔如果不符合手冊要求, 差距較大時LED會不工作(不亮), 在間隔接近但是不完全滿足時, LED會出現顯示錯亂, 色彩亂跳等.
Tim “cpldcpu” 做過一系列實驗 https://cpldcpu.wordpress.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/ 驗證過時間間隔的邊界, 發現這些要求實際上相當寬松:
? 觸發RESET只需要 9 μs (比手冊要求的 50 μs 小很多)
? 一個 bit 的周期至少需要 1.25 μs, 但是不能超過 9 μs, 因為這樣容易觸發RESET
? 0 的高電平時間, 手冊要求是 0.35 μs, 實際上可以短至 62.5 ns , 但是不能長于 0.50 μs
? 1 的高電平時間, 手冊要求是 0.65 μs, 實際上長度可以幾乎跨越整個 bit 周期, 但是不能短于 0.625 μs
SPI驅動時的bit數選擇
對于輸出固定長度的電平組合, SPI是最簡單的方式. 可以使用 SPI, 通過控制其中的數據值與 WS2812 通信, 而時間間隔控制則需要通過控制 SPI 的時鐘以及每次發送的 bit 數量實現, 根據Controlling WS2812(B) leds using STM32 HAL SPI 的計算, 通過對比多種 bit 數的時間要求, 發現使用 bit 數越多, 兼容性越好, MCU越容易實現. 因此可以使用默認的 8bit SPI 通信.
對于 PY32F002A/PY32F003/PY32F030, 因為最高頻率是48MHz, 所以當SPI分頻為8, 16時, 分別對應 6MHz, 3MHz, 在工作范圍內; 對于 PY32F040/PY32F071/PY32F072, 最高頻率是72MHz, 當SPI分頻為8, 16, 32時, 分別對應 9MHz, 4.5MHz, 2.25MHz, 都在工作范圍內.
設計思路
由于控制高低電平占比(ws2812 bit)的時間需要多個SPI bit,故SPI發送的長度肯定大于通信的內容,即用多個SPI bit去模擬一個ws2812 bit。常見的SPI傳輸是8bit,但也可配置為12、16等比特。但為了DMA搬運數據時方便對齊,故配置為8bit。
在SPI 8bit的數據長度下,我們采用2,4,8,12,16的SPI bit去模擬一個ws2812 bit。比如可以得出一下組合(實際上的ws2812時序并沒有數據手冊上那么嚴格,且不同廠家的配置不同):
用4個SPI bit去模擬,則用SPI bit表示ws2812 bit0為:1000;bit1可表示為1110。且一個SPI bit在250~420ns均可。
用8個SPI bit去模擬,則用SPI bit表示ws2812 bit0為:1100 0000;bit1可表示為1111 1100。且一個SPI bit在120~210ns均可。
當然不同比例的SPI bit可以調整出不同的SPI時間,具體根據SPI的配置時間。8個SPI bit能將時間調整得更精細,而4個SPI bit更省RAM。具體怎么配置還要和SPI的速度匹配。故我采用4個SPI bit模式。那么SPI的通信速率為2.4Mbps~4Mbps。
實現
用4個SPI bit去模擬,則用SPI bit表示ws2812bit0為:1000;bit1可表示為1110。即
LOW=0x08
HIGH=0x0E
首先將SPI調整發送模式、數據長度、大小端、分頻系數、CPHA:
#define WSLEDNUM 8 // 定義燈的數量
uint8_t wsFillMap[4] = {0x88, 0x8E, 0xE8, 0xEE}; // 這是一個哈希表 00=0x88 01=0x8E 10=0xE8 11=0xEE
uint8_t ws2812Buffer[WSLEDNUM*12+2] = {0}; // 數組緩沖區
void setOnePixRGB(uint8_t R, uint8_t G, uint8_t B, uint16_t index)
{
uint8_t i;
uint8_t *bufHead = ws2812Buffer + (12 * index); // 通過bufHead確定該像素的首地址,減少后面計算
for (i = 0; i < 4; ++i) // 每次位移兩位,通過哈希表來填充緩存區(當然你可以寫一個4位的哈希表,就不用位移了)。并同時為GRB賦值,減少循環次數。
{
bufHead[0+i] = wsFillMap[(G >> (6 - 2 * i)) & 0x03];
bufHead[4+i] = wsFillMap[(R >> (6- 2 * i)) & 0x03];
bufHead[8+i] = wsFillMap[(B >> (6 - 2 * i)) & 0x03];
}
}
void flushWs2812(void) // 刷新函數,多發兩個低電平穩定電平(心里安慰,其實沒用)
{
HAL_SPI_Transmit_DMA(&hspi1, ws2812Buffer, WSLEDNUM*12+2);
}
這段代碼實現了刷新一個燈的緩沖區,并通過哈希表減少運算量。最后通過DMA發送緩沖區數據即更新所有燈的狀態。
注意,同時因為下拉的存在,所以不需要發送很多的0來發送RESET,但必須需保證兩幀間隔大于文檔中的RESET碼時間。

浙公網安備 33010602011771號