WS2812 是一種整合了控制器的全綵LED, 常見單體尺寸為50mm * 50mm, 4個PIN, 分別是 VCC, GND, DIN, DOUT, 工作電壓3.7V-5.3V, 電流16mA. 市面上出售的大都是製作成條狀, 環狀或矩陣的成品. 供電電壓有5V和12V兩種, 前者因為電壓低, 如果長度較長, 每隔兩三百顆需要外接電源補電.
WS2812的特點就是全綵並且是單線序列介面, 只需要一個IO就可以對彩燈實現全部控制.
WS2812/WS2812B LED 使用 24 bit 資料調節 RGB 色彩, 每個 bit 都是通過(一個高電平 + 一個低電平)表示的.
根據手冊
根據上面的資訊, 對單顆LED傳送資料, 需要的時間為
\(24 × 1.25 µs + 50 µs = 80 µs\)
對於8顆LED, 需要的時間為
\(8 × 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/ 驗證過時間間隔的邊界, 發現這些要求實際上相當寬鬆:
對於輸出固定長度的電平組合, 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, 都在工作範圍內.
對應不同的LED數量, 需要調整下面程式碼中WS2812_NUM_LEDS
的值, 這裡使用的是一個8x8的點陣, 因此設為64. 注意這個ws2812_buffer
實際上非常佔記憶體, 對於數量超過64的LED燈帶(矩陣), 需要考慮其它的實現.
標頭檔案 ws2812_spi.h
#include "main.h"
#define WS2812_NUM_LEDS 64
#define WS2812_SPI_HANDLE Spi1Handle
#define WS2812_RESET_PULSE 60
#define WS2812_BUFFER_SIZE (WS2812_NUM_LEDS * 24 + WS2812_RESET_PULSE)
extern SPI_HandleTypeDef WS2812_SPI_HANDLE;
extern uint8_t ws2812_buffer[];
void ws2812_init(void);
void ws2812_send_spi(void);
void ws2812_pixel(uint16_t led_no, uint8_t r, uint8_t g, uint8_t b);
void ws2812_pixel_all(uint8_t r, uint8_t g, uint8_t b);
函數實現 ws2812_spi.c
#include <string.h>
#include "ws2812_spi.h"
#define WS2812_FILL_BUFFER(COLOR) \
for( uint8_t mask = 0x80; mask; mask >>= 1 ) { \
if( COLOR & mask ) { *ptr++ = 0xfc; } \
else { *ptr++ = 0x80; }}
uint8_t ws2812_buffer[WS2812_BUFFER_SIZE];
void ws2812_init(void) {
memset(ws2812_buffer, 0, WS2812_BUFFER_SIZE);
ws2812_send_spi();
}
void ws2812_send_spi(void) {
HAL_SPI_Transmit(&WS2812_SPI_HANDLE, ws2812_buffer, WS2812_BUFFER_SIZE, HAL_MAX_DELAY);
}
void ws2812_pixel(uint16_t led_no, uint8_t r, uint8_t g, uint8_t b) {
uint8_t * ptr = &ws2812_buffer[24 * led_no];
WS2812_FILL_BUFFER(g);
WS2812_FILL_BUFFER(r);
WS2812_FILL_BUFFER(b);
}
void ws2812_pixel_all(uint8_t r, uint8_t g, uint8_t b) {
uint8_t * ptr = ws2812_buffer;
for( uint16_t i = 0; i < WS2812_NUM_LEDS; ++i) {
WS2812_FILL_BUFFER(g);
WS2812_FILL_BUFFER(r);
WS2812_FILL_BUFFER(b);
}
}
具體的SPI初始化可以參考文章結尾的範例程式碼, 根據各自環境的工作頻率不同, 需要控制SPI的時鐘週期在工作範圍之內
對於開啟PLL執行在48MHz的PY32F002A/003/030, 使用8分頻
static void APP_SPIConfig(void)
{
LL_SPI_InitTypeDef SPI_InitStruct = {0};
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
//...
/* The frequency after prescaler should be below 8.25MHz */
SPI_InitStruct.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV8;
//...
}
對於 PY32F040/071/072, 工作在HSI 24MHz, 使用4分頻
static void APP_SPI_Config(void)
{
Spi1Handle.Instance = SPI1;
/* The frequency after prescale should be below 8.25MHz */
Spi1Handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
//...
}
範例程式碼中, 通過迴圈依次修改畫素點的RGB值演示LED全綵效果
ws2812_pixel_all(r, g, b);
ws2812_send_spi();
while (1)
{
i = (i + 1) % WS2812_NUM_LEDS;
ws2812_pixel(i, r++, g++, b++);
ws2812_send_spi();
LL_mDelay(20);
}
完整的範例程式碼通過以下連結檢視
要注意自己使用的 WS2812 的供電電壓是 5V 還是 12V, 不要和 PY32F0 的供電混在一起. WS2812 數量多了之後電流是很大的, 對 5V 8x8 的矩陣實測工作電流在 500mA 以上, 如果是 16x16 的矩陣, 電流會超過 2A. 這麼大的電流最好單獨供電.