使用 STM32 控制 NeoPixel

目前,例如 ArduinoFeather 等高階開發平台已經提供了出色的支援,可以通過易於使用的程式庫和普遍使用的示例程式碼與 NeoPixel LEDs燈條矩陣等相連接。然而,更高階的平台(例如 STM32 開發板)通常缺乏相同水平的支援。因此,希望將 NeoPixel 整合到項目中的開發人員需要全面了解 NeoPixel 通訊協議以及如何克服它所帶來的挑戰。

test_better_300_64

NeoPixels

Adafruit 推出一款十分受歡迎的可尋址全彩 LED 燈「NeoPixel系列」,提供 RGB 和 RGBW 兩個種類。儘管二者都將紅、綠和藍色 LED 與驅動器晶片相集成,但 RGBW 組件還集成了第四個純白色的 LED。可以使用類似的單線序列介面來控制這兩種類型的 NeoPixel,其時序值和數據結構協定僅存在微小的差異。

WS2812

RGB NeoPixel 實際上是 WS2812 智能控制 LED,包括數據訊號輸入引腳(DIN)和數據訊號輸出引腳(DOUT)。這允許多個 LED 串接並且只需一個數據線進行控制。鏈中的第一個 LED 負責處理從 MCU 接收到的前三個字節數據,然後將後續的數據簡單地轉發給 DOUT 引腳,該引腳可以連接到另一個 LED 的 DIN 引腳。 LED 將以此方式繼續向下傳遞數據,直到它們接收到重置訊號為止(即是 DIN 線在一段時間內持續保持低電平狀態)。傳輸的字節按照圖1所示的協定進行組織。第一個字節(G7-G0)表示綠色 LED的 8位 PWM 強度,其中 0x00 是完全關閉,0xFF 是完全開啟。如此類推,第二個字節(R7-R0)用於控制紅色 LED 的強度,第三個字節(B7-B0)用於控制藍色LED的強度。

圖 1 : WS2812 LED 的3字節數據協定結構

每一個這樣的24位數據都是通過改變方波的脈衝寬度來進行編碼,如圖2所示。請注意,無論發送編碼0還是編碼1,方波的周期仍保持在 1.25μs。對於 WS2812,使數據線保持低電平至少 50μs 即可產生重置訊號。另外,圖2中顯示的時序值具有 ±0.15μs 的公差。

image

圖 2 : WS2812 LED 的0和1位的時序圖。

SK6812

一種完全不同的組件,NeoPixel 的 RGBW 種類實際上是 SK6812 智能控制 LED,採用與 WS2812 LED 相同的工作原理。然而,由於它們包含第四個 LED,因此實施了圖3所示的4字節數據協定。與圖1相比,唯一的區別在於數據的序連碼(W7-W0),該字節指定了白色 LED 的8位 PWM 強度。

圖 3 : SK6812 LED 的4字節數據協定的結構

圖4展示了 SK6812 控制訊號的時序值,同樣與 WS2812 略有差別(不過仍在 ±0.15μs 的公差範圍內)。請注意,這兩種編碼的方波週期均保持不變,都為 1.2μs。此外,SK6812 的重置訊號長度為 80μs ,而非 50μs。

image

圖 4 : SK6812 LED 的0位和1位的時序圖。

程序

由於 NeoPixel 的控制訊號對時序要求非常嚴格,因此除非使用組合語言,否則無法通過簡單的位元響應(bit-banging)方法產生此訊號。雖然還有許多其他方法可以利用各種 MCU 周邊裝置、外置硬體或其組合來生成該訊號,但其中最直接的方法是配置 MCU 定時器來產生 PWM 輸出訊號。如上述,這是因為 NeoPixel 控制信號只是一種固定頻率的 PWM 訊號,採用不同的工作週期表示0位和1位。為了以與傳輸協定相同的速率高效地在這兩個佔空比之間進行切換,還必須配置 DMA 流來管理更新。儘管這種方法可能是內存效率最低的方式,但它易於理解、CPU 高效並且易於實施(受惠於 STM32Cube 環境)。

以下應用程式利用 STM32CubeIDE(版本1.8.0)、NUCLEO-F401RE 開發板和 RGBW 5x8 NeoPixel Shield 實現上述的方法。不過,這些步驟可以輕鬆地推廣到任何 STM32 MCU / 板和NeoPixel 產品上。假定我們已經建立了一個 STM32CubeIDE 項目。如需使用其他 IDE,你可以改為使用獨立的 STM32CubeMX 編碼配置器工具,將項目導出到所需的開發平台上。

1. 配置PWM

a. 先開啟 STM32CubeMX 配置 .ioc 文件(如果還未開啟的話)。隨後,STM32CubeIDE 將切換到元件配置工具(Device Configuration Tool ) 透視圖來配置 MCU。

b. 將定時器通道備用功能分配給選定的 GPIO 引腳,以與 NeoPixel(一個或多個) 進行連接。所選定時器通道應該能夠生成 PWM 輸出。圖5顯示了我的項目中的相關部分,我選擇了引腳 PB10,並將它分配給定時器2、通道3(TIM2_CH3)功能。

image

圖 5 : 將連接到 DIN 的 GPIO 引腳配置為定時器通道。

c. 從左邊的組件列表中選擇上一步中確定的定時器周邊裝置,以開啟模式和配置(Mode and Configuration ) 面板。在模式(Mode)面板中,選擇「內部時鐘」作為時鐘源,並從適當的定時器通道的下拉列表中選擇「PWM 產生 CHx」。在圖6中,定時器2、通道3已設為「PWM 產生 CH3」模式,因為我在上一步中選擇了 TIM2_CH3 交替功能。請注意,在完成此步驟後,有關的 GPIO 引腳應在引腳排列視圖中從黃色變為綠色。

d. 在定時器的配置(Configuration)面板中,驗證「預分頻器」和「脈衝」值是否都設置為0。計數器週期,即自動重載寄存器(AutoReload Register ,以下簡稱ARR)需要進行設置以得到所需的PWM 週期(如果使用 RGB WS2812 LED,則為 1.25μs; 如果使用 RGBW SK6812 LED,則為1.2μs)。這將取決於定時器周邊裝置輸入的速率。將所需的 PWM 週期除以時鐘週期,並減去1即可得到此值(減去1是因為定數器從0開始)。就我的元件而言,該公式得出的 ARR 值為 99.8,我將其四捨五入為 100(圖6)。請閱讀下文了解有關計算 ARR 理想值的詳細說明。

image

圖 6 : 將所選定時器通道配置為PWM輸出。


計算 ARR 值

假設定時器「預分頻器」值設為0,可以很容易的計算出 ARR 值

ARR = \frac{f_{timer}}{f_{PWM}} - 1 = \frac{T_{PWM}}{T_{timer}} - 1

具體來說,ARR 值等於 PWM 訊號週期除以定時器周邊裝置的時鐘訊號週期。我們知道,根據使用的NeoPixel 類型不同,TPWM 可以是 1.25μs 或 1.2μs(例如本例中,TPWM=1.2μs)。要確定 Ttimer,你需要查閱 MCU 的規格書,確定定時器周邊裝置連接到哪個總線。規格書可以在 ST 的網站上找到或STM32CubeIDE 會隨附提供:選擇 Help > Target Device Docs and Resources 。然後,在 MCU 選項欄下選擇規格書,如圖7所示。

圖 7 : 閱讀 MCU 規格書

在我使用的MCU(STM32F401RE)規格書中,MCU 方塊圖中顯示我的定時器(TIM2)已連接到 APB1(見圖8)。

image
圖 8 : STM32F401xD/xE的部分方塊圖(源自 DS10086

圖9 解釋了通過切換到 STM32CubeIDE 的 Clock Configuration 選項欄,我們可以發現 TIM2 的時鐘頻率為 84MHz ! (T_{timer} = \frac{1}{f_{timer}} = \frac{1}{84\text{ MHz}})。

image

圖 9 : 確定定時器時鐘頻率

因此,

ARR = \frac{1.2 \times 10^{-6}}{\frac{1}{84 \times 10^6}} - 1 = 99.8

為了使 PWM 週期盡可能接近NeoPixel控制訊號的週期,我們四捨五入至最接近的整數並得到 ARR =100.


2. 配置DMA

a. 從零件列表中,選擇 DMA 周邊裝置。

b. 在 Configuration 面板的 DMA1 選項欄下,按 Add 按鈕。在下拉式選項單中,選擇你的定時器/通道組合。在我的項目中,我選擇了「TIM2_CH3/UP」。

c. 針對新的DMA請求,將方向改為 「Memory To Peripheral」。

d. 同時,將 Priority 優先級改為「Very High」。

e. 驗證預設的 DMA 請求設置是否與圖10中顯示的相同。

f. 儲存 .ioc 文件,以產生項目編碼。

圖 10 : 配置 DMA 資料流,以便有效地更新 PWM 訊號的工作週期

3. 編寫程式

main.c 文件中,按從上到下的順序編寫,本部分展示了一個簡單的示例應用,用於測試 NeoPixel LED 的全色彩顯示能力。此處提供了兩個版本的 main()函數,一個用於 RGB WS2818 LED,一個用於 RGBW SK6812 LED。

a. 在 main.c 文件的私用 typedef 部分,你可以建立一個新的數據類型,以便輕鬆存取單個 LED 顏色值以及整個 NeoPixel 數據結構(如圖1和圖3所示)。清單1提供了 RGB 和 RGBW NeoPixel 零件的typedef。此編碼應貼在 /* USER CODE BEGIN PTD *//* USER CODE END PTD */ 註解之間。

清單 1 : 為 RGB WS2812 和 RGBW SK6812 LED 客製數據類型

typedef union
{
  struct
  {
    uint8_t b;
    uint8_t r;
    uint8_t g;
  } color;
  uint32_t data;
} PixelRGB_t;

typedef union
{
  struct
  {
    uint8_t w;
    uint8_t b;
    uint8_t r;
    uint8_t g;
  } color;
  uint32_t data;
} PixelRGBW_t;

b. 更改「脈衝」寄存器(也稱為 CCRx)的值,這樣可以改變 PWM 波形的工作週期。因此,我們必須計算適當的 CCRx 值,以實現使用的 NeoPixels 所需的編碼0和編碼1方波(無論如圖2還是圖4中所示)。對於 RGB WS2812 LED,這些值計算如下:

ZERO = (ARR + 1)(0.32)
ONE = (ARR + 1)(0.64)

對於 RGBW SK6812 LED,其計算過程稍有不同。

ZERO = (ARR + 1)(0.25)
ONE = (ARR + 1)(0.5)

當然,這些計算出的值應該四捨五入到最接近的整數。在 main.c 文件的私用定義部分,為每個值建立一個 #define 指令(請參見以下圖11中的示例)。

c. 除了 CCRx 值之外,還應在私用定義部分中定義控制的 NeoPixel LED 數量和 DMA 緩衝區大小。如圖11所示,只需將 LED 的數量乘以相應的 NeoPixel 數據結構中的位元數即可(回想圖1和圖3)。還必須分配一個額外的緩衝區元素,因為最後一個 CCRx 值應為零(重置訊號)。

圖 11 : WS2812 和 SK6812 LED 的私用定義

d. 將清單2中提供的 DMA 完成回調函數加在到 /* USER CODE BEGIN 0 *//* USER CODE END 0*/ 之間的私用用戶編碼部分。必須將 TIM_CHANNEL_x 更改為步驟1c中配置的通道。

清單 2HAL_TIM_PWM_PulseFinishedCallback() 函數的應用

void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
  HAL_TIM_PWM_Stop_DMA(htim, TIM_CHANNEL_x);
}

e. 最後,必須將應用編碼加在 main()函數中。列表3提供了一個使用 WS2812 LED 的示例 main()函數,而列表4提供了使用 SK6812 LED 的類似示例 main()函數。請注意,HAL_TIM_PWM_Start_DMA()函數的 TIM_CHANNEL_x 參數必須再次進行修改,以匹配步驟1c中配置的通道。

清單 3 : RGB WS2812 LED 的示例 main()函數

int main(void)
{
  /* USER CODE BEGIN 1 */

  PixelRGB_t pixel[NUM_PIXELS] = {0};
  uint32_t dmaBuffer[DMA_BUFF_SIZE] = {0};
  uint32_t *pBuff;
  int i, j, k;
  uint16_t stepSize;

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_DMA_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  k = 0;
  stepSize = 4;
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    for (i = (NUM_PIXELS - 1); i > 0; i--)
    {
      pixel[i].data = pixel[i-1].data;
    }

    if (k < 255)
    {
      pixel[0].color.g = 254 - k; //[254, 0]
      pixel[0].color.r =  k + 1;  //[1, 255]
      pixel[0].color.b = 0;
    }
    else if (k < 510)
    {
      pixel[0].color.g = 0;
      pixel[0].color.r = 509 - k; //[254, 0]
      pixel[0].color.b = k - 254; //[1, 255]
      j++;
    }
    else if (k < 765)
    {
      pixel[0].color.g = k - 509; //[1, 255];
      pixel[0].color.r = 0;
      pixel[0].color.b = 764 - k; //[254, 0]
    }
    k = (k + stepSize) % 765;

    // not so bright
    pixel[0].color.g >>= 2;
    pixel[0].color.r >>= 2;
    pixel[0].color.b >>= 2;

    pBuff = dmaBuffer;
    for (i = 0; i < NUM_PIXELS; i++)
    {
       for (j = 23; j >= 0; j--)
       {
         if ((pixel[i].data >> j) & 0x01)
         {
           *pBuff = NEOPIXEL_ONE;
         }
         else
         {
           *pBuff = NEOPIXEL_ZERO;
         }
         pBuff++;
     }
    }
    dmaBuffer[DMA_BUFF_SIZE - 1] = 0; // last element must be 0!

    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_x, dmaBuffer, DMA_BUFF_SIZE);

    HAL_Delay(10);
  }
  /* USER CODE END 3 */
}

清單 4 : RGBW SK6812 LED 的示例 main()函數

int main(void)
{
  /* USER CODE BEGIN 1 */

  PixelRGBW_t pixel[NUM_PIXELS] = {0};
  uint32_t dmaBuffer[DMA_BUFF_SIZE] = {0};
  uint32_t *pBuff;
  int i, j, k;
  uint16_t stepSize;

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_DMA_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  k = 0;
  stepSize = 4;
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    for (i = (NUM_PIXELS - 1); i > 0; i--)
    {
      pixel[i].data = pixel[i-1].data;
    }

    if (k < 255)
    {
      pixel[0].color.g = 254 - k; //[254, 0]
      pixel[0].color.r =  k + 1;  //[1, 255]
      pixel[0].color.b = 0;
      pixel[0].color.w = 0;
    }
    else if (k < 510)
    {
      pixel[0].color.g = 0;
      pixel[0].color.r = 509 - k; //[254, 0]
      pixel[0].color.b = k - 254; //[1, 255]
      pixel[0].color.w = 0;
      j++;
    }
    else if (k < 765)
    {
      pixel[0].color.g = 0;
      pixel[0].color.r = 0;
      pixel[0].color.b = 764 - k; //[254, 0]
      pixel[0].color.w = k - 509; //[1, 255]
    }
    else if (k < 1020)
    {
      pixel[0].color.g = k - 764; //[1, 255]
      pixel[0].color.r = 0;
      pixel[0].color.b = 0;
      pixel[0].color.w = 1019 - k; //[254, 0]
    }
    k = (k + stepSize) % 1020;

    // 50% brightness
    pixel[0].color.g >>= 2;
    pixel[0].color.r >>= 2;
    pixel[0].color.b >>= 2;
    pixel[0].color.w >>= 2;

    pBuff = dmaBuffer;
    for (i = 0; i < NUM_PIXELS; i++)
    {
       for (j = 31; j >= 0; j--)
       {
         if ((pixel[i].data >> j) & 0x01)
         {
           *pBuff = NEOPIXEL_ONE;
         }
         else
         {
           *pBuff = NEOPIXEL_ZERO;
         }
         pBuff++;
     }
    }
    dmaBuffer[DMA_BUFF_SIZE - 1] = 0; // last element must be 0!

    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_x, dmaBuffer, DMA_BUFF_SIZE);

    HAL_Delay(10);
  }
  /* USER CODE END 3 */
}

該項目現在應該能夠成功建立,並能夠在您的設備上運作此編碼了。

結論

使用邏輯分析儀捕捉了上面提供的 RGB 和 RGBW 配置產生的控制訊號。分別如圖12和圖13中所示。請注意,它們與圖2和圖4中指定的預期輸出相匹配。

圖 12 : 產生的 WS2812 控制訊號(正在發送0b0011……)

圖 13 : 產生的 SK6812 控制訊號(正在發送0b0010……)

1 Like