@[TOC](目錄)
## 前言
串行燈帶的應(yīng)用十分廣泛,其中以WS2812最為經(jīng)典,這種燈帶一般都是通過單總線的方式來驅(qū)動,也就是由一根數(shù)據(jù)線按照特定的時序輸出,繼而驅(qū)動燈帶。這種方式在硬件和軟件上都非常簡單,但是如果軟件用GPIO模擬時序的話比較占用主線程的資源,因此,如果能用硬件外設(shè)(比如PWM、SPI、串口)來模擬出這個時序,就能節(jié)省MCU的資源。
本文以PWM+DMA為例介紹如何驅(qū)動WS2812。
?
## 1 硬件介紹
### 1.1 WS2812介紹
#### 1.1.1 芯片簡介
?WS2812是一款智能控制LED光源,其外觀采用最新的MOLDING封裝技術(shù)、控制電路和RGB芯片集成在2020組件的封裝中。其內(nèi)部包括智能數(shù)字端口數(shù)據(jù)鎖存和信號整形放大驅(qū)動電路。還包括精密內(nèi)部振蕩器和電壓可編程恒流控制部分,有效保證像素點(diǎn)光源的顏色。
?
#### 1.1.2 引腳描述
| 引腳 | 名稱 | 描述 |
|:-------|:------|:------|
| DO | 數(shù)據(jù)輸出 | 控制數(shù)據(jù)輸出到下一個芯片 |
| GND | 地 | 電源負(fù)極 |
| DI | 數(shù)據(jù)輸入 | 控制數(shù)據(jù)輸入 |
| VDD | 電源 | 電源正極 |
?
#### 1.1.3 工作原理
通過級聯(lián)法把每個燈的DI和DO引腳首尾相連,數(shù)據(jù)可以從第一個IC開始,不斷的傳輸?shù)胶竺婷恳粋€IC,從而實(shí)現(xiàn)整條燈帶的控制。

#### 1.1.4 時序
WS2812通過不同的時序來表示`0碼`、`1碼`和`復(fù)位碼`,如下圖所示:

其中各信號的電平如下圖所示:

<font color=#ff9966>注:不同型號的芯片在時序上會有點(diǎn)差異,具體以芯片數(shù)據(jù)手冊為準(zhǔn)。</font>
#### 1.1.5 傳輸協(xié)議
傳輸過程如下圖所示:

?
每一個燈珠的RGB數(shù)據(jù)排列如下:

### 1.2 電路設(shè)計(jì)
WS2812的控制方法很簡單,每個燈珠首尾相接進(jìn)行級聯(lián)即可,如下圖所示:

其中,第一個燈珠的DI引腳接入到MCU的一個GPIO上面。
?
我這里使用STM32F103來作為主控MCU,引腳接線如下:
| MCU引腳 | 燈帶引腳 | 描述 |
|:-------|:------|:------|
| PA0 | DI | 由MCU發(fā)送控制信號輸入到燈帶 |
?
## 2 軟件編程
### 2.1 軟件原理
舉個例子:按照上面的手冊的時序要求,每一個邏輯電平周期在1.25us左右,也就是800kHz,那么PWM輸出的頻率就可以設(shè)置為800kHz。此時改變PWM的占空比,就可以區(qū)分編碼“0”和編碼“1”,比如編碼“0”的高電平脈寬和低電平脈寬分別為0.4us和0.85us,那么對應(yīng)的PWM占空比就是32%和68%,然后通過DMNA連續(xù)傳輸RGB數(shù)據(jù)就可以實(shí)現(xiàn)燈帶的顏色和亮度控制。
?
測試電平時序如下:
| 邏輯電平 | 脈寬 | PWM占空比 |
|:------------|:------------|:------------|
| 邏輯0高電平 | 0.40±0.15us | 32% |
| 邏輯0低電平 | 0.85±0.15us | 68% |?
| 邏輯1高電平 | 0.85±0.15us | 68% |?
| 邏輯1低電平 | 0.40±0.15us | 32% |?
| 復(fù)位低電平 | 1.25±0.60us | 0% |?
### 2.2 測試代碼
根據(jù)上述原理,編寫測試代碼。
#### 2.2.1 底層驅(qū)動
<font color=#0033ff>ws2812_driver.h :</font>
```c
#ifndef __WS2812_DRIVER_H
#define __WS2812_DRIVER_H
?
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
?
#define TIM2_CCR1_Address 0x40000034? // stm32 tim2 base address offset 0x34
?
#define LED_NUM? ? ?8? ? // LED的數(shù)量
#define RGB_BIT? ? ?24? ?// 每個燈有24bit的RGB數(shù)據(jù),依次按G R B排列
?
#define RESET_WORD? 5? ? // 在傳輸RGB數(shù)據(jù)前保持一段低電平
#define DUMMY_WORD? 5? ? // 在傳輸RGB數(shù)據(jù)后保持一段低電平
?
#define TIMING_0? ? 29? ?// T0H(32%) = 1.25us * (29 / 90) = 0.40us, T0L(68%) = 1.25 - 0.40 = 0.85us?
#define TIMING_1? ? 61? ?// T1H(68%) = 1.25us * (61 / 90) = 0.85us, T1L(32%) = 1.25 - 0.85 = 0.40us?
?
void led_display(uint8_t (*led_buf)[3], uint8_t led_num);
void ws2812_init(void);
?
#endif
```
?
<font color=#0033ff>ws2812_driver.c :</font>
```c
#include "ws2812_driver.h"
#include "string.h"
?
uint16_t pwm_dma_buf[RESET_WORD + RGB_BIT * LED_NUM + DUMMY_WORD];
?
void pwm_init(void)
{
? ? GPIO_InitTypeDef GPIO_InitStructure;
? ? TIM_TimeBaseInitTypeDef? TIM_TimeBaseStructure;
? ? TIM_OCInitTypeDef? TIM_OCInitStructure;
?
? ? RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
? ? GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
? ? GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
? ? GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
? ? GPIO_Init(GPIOA, &GPIO_InitStructure);
? ? GPIO_ResetBits(GPIOA, GPIO_Pin_0);
?
? ? RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
? ? TIM_TimeBaseStructure.TIM_Period = 90 - 1;? ? ?// 72MHz / 90 = 800kHz
? ? TIM_TimeBaseStructure.TIM_Prescaler = 0;
? ? TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
? ? TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
? ? TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
?
? ? /* PWM2 Mode configuration: Channel1 */
? ? TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
? ? TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
? ? TIM_OCInitStructure.TIM_Pulse = 50;
? ? TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
? ? TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
? ? TIM_OC1Init(TIM2, &TIM_OCInitStructure);
?
? ? TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
?
// TIM_ARRPreloadConfig(TIM2, ENABLE);
?
? ? /* TIM2 enable counter */
? ? TIM_Cmd(TIM2, ENABLE);
}
?
void pwm_dma_init(void)
{
? ? /* configure DMA */
? ? DMA_InitTypeDef DMA_InitStructure;//定義DMA初始化結(jié)構(gòu)體
?
? ? /* DMA clock enable */
? ? RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA時鐘(用于SPI的數(shù)據(jù)傳輸)
?
? ? memset((uint8_t*)&pwm_dma_buf, 0, sizeof(pwm_dma_buf));
?
? ? /* DMA1 Channel5 Config for PWM2 by TIM2_CH1*/
? ? DMA_DeInit(DMA1_Channel5);
? ? DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)TIM2_CCR1_Address; // physical address of Timer 3 CCR1
? ? DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&pwm_dma_buf; // this is the buffer memory?
? ? DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // data shifted from memory to peripheral
? ? DMA_InitStructure.DMA_BufferSize = sizeof(pwm_dma_buf)/2;
? ? DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
? ? DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // automatically increase buffer index
? ? DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
? ? DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
? ? DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // stop DMA feed after buffer size is reached
? ? DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
? ? DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
? ? DMA_Init(DMA1_Channel5, &DMA_InitStructure);
?
? ? /* TIM2 DMA Request enable */
? ? TIM_DMACmd(TIM2, TIM_DMA_CC1, ENABLE);
? ? TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
}
?
void pwm_dma_send(void)
{
? ? DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(pwm_dma_buf)/2); // load number of bytes to be transferred
? ? DMA_Cmd(DMA1_Channel5, ENABLE); // enable DMA channel 5
? ? TIM_Cmd(TIM2, ENABLE); // enable Timer 2
? ? while(!DMA_GetFlagStatus(DMA1_FLAG_TC5)) ; // wait until transfer complete
? ? DMA_Cmd(DMA1_Channel5, DISABLE); // disable DMA channel 5
? ? DMA_ClearFlag(DMA1_FLAG_TC5); // clear DMA1 Channel 5 transfer complete flag
? ? TIM_Cmd(TIM2, DISABLE); // disable Timer 2
}
?
void led_display(uint8_t (*led_buf)[3], uint8_t led_num)
{
uint8_t i, j;
?
// led_buf -> pwm_dma_buf
for(i = 0; i < led_num; i++)
{// N led
for(j = 0; j < 8; j++)
{// 1 color -> 8bit
// g
pwm_dma_buf[RESET_WORD+RGB_BIT*i+j] = ((led_buf[i][1] << j) & 0x80) ? TIMING_1 : TIMING_0;
// r
pwm_dma_buf[RESET_WORD+RGB_BIT*i+j+8] = ((led_buf[i][0] << j) & 0x80) ? TIMING_1 : TIMING_0;
// b
pwm_dma_buf[RESET_WORD+RGB_BIT*i+j+16] = ((led_buf[i][2] << j) & 0x80) ? TIMING_1 : TIMING_0;
}
}
// pwm start
pwm_dma_send();
}
?
void ws2812_init(void)
{
? ? pwm_init();
? ? pwm_dma_init();
}
```
#### 2.2.2 燈效應(yīng)用
<font color=#0033ff>ws2812_app.h :</font>
```c
#ifndef __WS2812_APP_H
#define __WS2812_APP_H
?
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "ws2812_driver.h"
?
typedef enum?
{
LED_MODE_OFF,
LED_MODE_ALL_ON,
LED_MODE_BREATHE,
LED_MODE_GRADIENT,
LED_MODE_FLOW,
}led_mode_t;
?
typedef struct
{
? ? led_mode_t mode;??
? ? uint8_t g;? ? ? ? ? ? ? ??
uint8_t r;? ? ? ? ? ? ??
uint8_t b;? ? ? ? ? ? ??
uint8_t brightness;??
}led_t;
?
void led_init(void);
void led_handle(void);
?
#endif
```
?
<font color=#0033ff>ws2812_app.c :</font>
```c
#include "ws2812_app.h"
?
led_t led;
uint8_t rgb_buf[LED_NUM][3];
?
void led_init(void)
{
? ? ws2812_init();
?
led.mode = LED_MODE_ALL_ON;
led.r = 0x00;
led.g = 0xE0;
led.b = 0x80;
}
?
void led_handle(void)
{
uint8_t i;
? ? switch (led.mode)
{
case LED_MODE_OFF:
for (i = 0; i < LED_NUM; i++)
{
rgb_buf[i][0] = 0;? // r
rgb_buf[i][1] = 0;? // g
rgb_buf[i][2] = 0;? // b
}
break;
case LED_MODE_ALL_ON:
for (i = 0; i < LED_NUM; i++)
{
rgb_buf[i][0] = led.r;? // r
rgb_buf[i][1] = led.g;? // g
rgb_buf[i][2] = led.b;? // b
}
break;
// ......可以自己加入更多的燈效
default:
break;
}
?
led_display(rgb_buf, LED_NUM);
}
```
?
<font color=#0033ff>main.c :</font>
```c
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "ws2812_app.h"
?
int main(void)
{
? ? SystemInit();
? ? delay_init();
? ? led_init();
? ? while(1)
? ? {
? ? ? ? led_handle();
? ? ? ? delay_ms(5);
? ? }
}
?
```
?
### 2.3 運(yùn)行測試
#### 2.3.1 時序測試
使用邏輯分析儀抓取信號,得到的結(jié)果如下:
?
1. 8個LED連續(xù)寫入RGB值:

?
2. 編碼1高電平(T1H)850ns:

?
3. 編碼1低電平(T1L)400ns:

?
4. 編碼1周期1.25us:

?
5. 編碼0高電平(T0H)400ns:

?
6. 編碼0高電平(T0H)850ns:

?
7. 編碼0周期1.25us:

?
<font color=ff033ff>結(jié)論:實(shí)際輸出的波形和理論一致。</font>
?
#### 2.3.2 實(shí)際效果
?
用在線顏色選取器把代碼設(shè)置的顏色值輸入進(jìn)去,得到該顏色,然后和實(shí)際燈帶點(diǎn)亮的顏色比對。
1. 顏色拾取器顯示如下:

2. 實(shí)際燈帶顏色如下:

?
<font color=ff033ff>結(jié)論:燈帶實(shí)際顯示的顏色準(zhǔn)確無誤。</font>
?
## 結(jié)束語
關(guān)于stm32如何使用PWM+DMA驅(qū)動WS2812的講解就到這里,如果還有什么問題,歡迎在評論區(qū)留言。
?
[源碼下載鏈接](https://download.csdn.net/download/ShenZhen_zixian/89073358)
?
如果這篇文章能夠幫到你,就.....你懂的。

閱讀全文