STM32应用开发——使用PWM+DMA驱动WS2812
目录
- STM32应用开发——使用PWM+DMA驱动WS2812
- 前言
- 1 硬件介绍
- 1.1 WS2812介绍
- 1.1.1 芯片简介
- 1.1.2 引脚描述
- 1.1.3 工作原理
- 1.1.4 时序
- 1.1.5 传输协议
- 1.2 电路设计
- 2 软件编程
- 2.1 软件原理
- 2.2 测试代码
- 2.2.1 底层驱动
- 2.2.2 灯效应用
- 2.3 运行测试
- 2.3.1 时序测试
- 2.3.2 实际效果
- 结束语
前言
串行灯带的应用十分广泛,其中以WS2812最为经典,这种灯带一般都是通过单总线的方式来驱动,也就是由一根数据线按照特定的时序输出,继而驱动灯带。这种方式在硬件和软件上都非常简单,但是如果软件用GPIO模拟时序的话比较占用主线程的资源,因此,如果能用硬件外设(比如PWM、SPI、串口)来模拟出这个时序,就能节省MCU的资源。
本文以PWM+DMA为例介绍如何驱动WS2812。
1 硬件介绍
1.1 WS2812介绍
1.1.1 芯片简介
WS2812是一款智能控制LED光源,其外观采用最新的MOLDING封装技术、控制电路和RGB芯片集成在2020组件的封装中。其内部包括智能数字端口数据锁存和信号整形放大驱动电路。还包括精密内部振荡器和电压可编程恒流控制部分,有效保证像素点光源的颜色。
1.1.2 引脚描述
引脚 | 名称 | 描述 |
---|---|---|
DO | 数据输出 | 控制数据输出到下一个芯片 |
GND | 地 | 电源负极 |
DI | 数据输入 | 控制数据输入 |
VDD | 电源 | 电源正极 |
1.1.3 工作原理
通过级联法把每个灯的DI和DO引脚首尾相连,数据可以从第一个IC开始,不断的传输到后面每一个IC,从而实现整条灯带的控制。
1.1.4 时序
WS2812通过不同的时序来表示0码
、1码
和复位码
,如下图所示:
其中各信号的电平如下图所示:
注:不同型号的芯片在时序上会有点差异,具体以芯片数据手册为准。
1.1.5 传输协议
传输过程如下图所示:
每一个灯珠的RGB数据排列如下:
1.2 电路设计
WS2812的控制方法很简单,每个灯珠首尾相接进行级联即可,如下图所示:
其中,第一个灯珠的DI引脚接入到MCU的一个GPIO上面。
我这里使用STM32F103来作为主控MCU,引脚接线如下:
MCU引脚 | 灯带引脚 | 描述 |
---|---|---|
PA0 | DI | 由MCU发送控制信号输入到灯带 |
2 软件编程
2.1 软件原理
通过DMA可以精确控制PWM输出的每一个方波,然后通过调整占空比,就可以输出0码
、1码
和复位码
,从而实现灯珠的驱动。
举个例子:按照上面的手册的时序要求,每一个逻辑电平周期在1.25us左右,也就是800kHz,那么PWM输出的频率就可以设置为800kHz。此时改变PWM的占空比,就可以区分编码“0”和编码“1”,比如编码“0”的高电平脉宽和低电平脉宽分别为0.4us和0.85us,那么对应的PWM占空比就是32%和68%,然后通过DMNA连续传输RGB数据就可以实现灯带的颜色和亮度控制。
测试电平时序如下:
逻辑电平 | 脉宽 | 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% |
复位低电平 | 1.25±0.60us | 0% |
2.2 测试代码
根据上述原理,编写测试代码。
2.2.1 底层驱动
ws2812_driver.h :
#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的数量
#define RGB_BIT 24 // 每个灯有24bit的RGB数据,依次按G R B排列
#define RESET_WORD 5 // 在传输RGB数据前保持一段低电平
#define DUMMY_WORD 5 // 在传输RGB数据后保持一段低电平
#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
ws2812_driver.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初始化结构体
/* DMA clock enable */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA时钟(用于SPI的数据传输)
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 灯效应用
ws2812_app.h :
#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
ws2812_app.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);
}
main.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 运行测试
2.3.1 时序测试
使用逻辑分析仪抓取信号,得到的结果如下:
-
8个LED连续写入RGB值:
-
编码1高电平(T1H)850ns:
-
编码1低电平(T1L)400ns:
-
编码1周期1.25us:
-
编码0高电平(T0H)400ns:
-
编码0高电平(T0H)850ns:
-
编码0周期1.25us:
结论:实际输出的波形和理论一致。
2.3.2 实际效果
用在线颜色选取器把代码设置的颜色值输入进去,得到该颜色,然后和实际灯带点亮的颜色比对。
- 颜色拾取器显示如下:
- 实际灯带颜色如下:
结论:灯带实际显示的颜色准确无误。
结束语
关于stm32如何使用PWM+DMA驱动WS2812的讲解就到这里,如果还有什么问题,欢迎在评论区留言。
源码下载链接
如果这篇文章能够帮到你,就…你懂的。