STM32F412通过TIM PWM DMA方式驱动WS2812B
一、基本思路
原理说明在《STM32F 驱动WS2812B (1) IO口》已经说明
STEP1:
实现逻辑1跟逻辑0的脉冲,这里用PWM修改占空比方式实现。
STEP2:
将数据通过DMA发送到TIM外设,发送前面80us的低电平RESET信号以及每个灯珠的24个0 code或者1 code的脉冲。
二、实现
1.配置CUBEMX
使用的芯片是STM32F412,选用TIM2的第2个通道输出PWM信号,因为是100M的时钟,所以分频系数为0即为不分频,然后125的counter period就是125个时钟周期为一个脉冲周期,所以一个脉冲周期为(1/100M)*125=0.00000125s=0.00125ms=1.25us。
PWM的Pulse先设置成0,这个值用来控制占空比,刚才计算出来125个时钟周期就是1250ns,那么一个时钟周期就是1个时钟周期就是10ns,所以根据ws2812b的手册说明的0code的高电平时400ns,低电平850ns,对应这个PULSE应该设置成40,表示40个时钟周期即为400ns,同理1code的高电平为800ns所以这个值应该设置成80即可。
2.配置DMA
单击Add添加一个DMA通道,这里因为时TIM2的2通道,所以把这个通道打开,然后在Direction的地方选择memory to peripheral表示从内存到外设,这里的内存就是定义的数据数组,外设就是TIM产生PWM的比较寄存器。其他的如下图所示,这边推荐Data Width都选择Word即可。
3.配置GPIO口
把GPIO的上下拉改成下拉,这样在不输出PWM的时候时低电平
4.修改工程
点击生成代码,生成MDK的代码。每个灯珠需要24个脉冲,每个脉冲的占空比根据0,1变化,在这之前需要一个reset信号,是大于50us的低电平。我们使用80us的低电平,之前通过cubemx配置的输出PWM的周期是1.25us,所以这个80us的低电平需要使用64个占空比为0的脉冲形成,所以先定义一个数组用来存放灯的数据,这边以1个灯为例:
uint32_t pwm_data[RESET_WORD+24*LED_NUM+DUMMY_WORD]={0};
这里的RESET_WORD定义成64,LED_NUM定义成1,DUMMY_WORD定义是1,这个DUMMY_WORD如果不加波形会出现问题,这个稍后再说,即1个灯需要64+24+1个脉冲。初始化为0,这样我们修改64个数据之后的数据不会影响前面的reset的数据。
假设需要亮蓝色,蓝色的RGB为0x0000FF,WS2812B的传输顺序是GRB,并且是先传输高bit,所以要先把G的颜色值放入数组,需要一个for循环,循环7次从高到低将bit对应的占空比填入数组,0对应的应该是40,1对应的是80:
for(j=15;j>=8;j--)//G
{
(((color>>j)&0x01)==1)?(pwm_data[RESET_WORD+(i*24+(15-j))]=80):(pwm_data[RESET_WORD+(i*24+(15-j))]=40);
}
因为第一个传输的是G数据,所以从RBG_COLOR的第15位bit15开始判断是否为1如果是1就用占空比数值80表示,一直判断到bit8。这里从RGB_COLOR的bit15到bit8应该存放到pwm_data的对应灯的bit0-bit7。
之后是R传输,同理:
for(j=23;j>=16;j--)//R
(((color>>j)&0x01)==1)?(pwm_data[RESET_WORD+(i*24+(31-j))]=80):(pwm_data[RESET_WORD+(i*24+(31-j))]=40);
最后是B传输,同理:
for(j=7;j>=0;j--)//B (((color>>j)&0x01)==1)?(pwm_data[RESET_WORD+(i*24+(23-j))]=80):(pwm_data[RESET_WORD+(i*24+(23-j))]=40);
把这个数组填好之后就可以开启DMA传输了:
HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_2,&pwm_data[0], RESET_WORD+24*LED_NUM+DUMMY_WORD);
这里的传输数据的个数就是整个数组的个数。
传输已经开始了,在传输结束之后会进入到回调函数,我们引用这个回调函数来处理结束的时候停止DMA传输,否则会一直传输。
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim2, TIM_CHANNEL_2);
}
这样一轮发送点亮等LED灯的DMA就完成了。
三、验证
1.抓取0 code波形
将LED_NUM设置成1,并且将全部的占空比的参数设置成40 也就是0 code的脉冲
2.抓取1 code波形
将LED_NUM设置成1,并且将全部的占空比的参数设置成80 也就是1 code的脉冲
3.验证发送脉冲个数以及时间
正常情况下如果将LED_NUM设置成1,并且将全部的占空比的参数设置成80 也就是1 code的脉冲,应该会发送24个脉冲,如下是抓到的波形:
如果在发送的数据中不加入dummy word就会出现如下的波形,通过计算脉冲个数可以看出脉冲个数不对,而且波形变得很奇怪:
这个原因还没搞清楚,如果有知道原因的可以在下面评论下,互相探讨下
一些想法
1:如果不设置GPIO的上下拉方式,在抓取的波形观察,会发现在没有传输的时候一直为高电平。
2:在设置DMA传输的时候一定需要注意传输的方向是Memory To Peripheral
3:DMA的Data Width要跟程序中定义的数组的类型一致。
部分关键代码
Main关键源码如下:
#define DUMMY_WORD 1
#define RESET_WORD 64
#define LED_NUM 2
uint32_t pwm_data[RESET_WORD+24*LED_NUM+DUMMY_WORD]={0};
void ws2812b_show_dot(uint8_t dot_index,uint32_t color)
{
int i=0,j=0;
for(i=0;i<LED_NUM;i++)
{
if(i==dot_index)
{
for(j=15;j>=8;j--)//G
(((color>>j)&0x01)==1)?(pwm_data[RESET_WORD+(i*24+(15-j))]=80):(pwm_data[RESET_WORD+(i*24+(15-j))]=40);
for(j=23;j>=16;j--)//R
(((color>>j)&0x01)==1)?(pwm_data[RESET_WORD+(i*24+(31-j))]=80):(pwm_data[RESET_WORD+(i*24+(31-j))]=40);
for(j=7;j>=0;j--)//B
(((color>>j)&0x01)==1)?(pwm_data[RESET_WORD+(i*24+(23-j))]=80):(pwm_data[RESET_WORD+(i*24+(23-j))]=40);
}
else
{
for(j=0;j<24;j++)
{
pwm_data[RESET_WORD+i*24+j]=40;
}
}
}
HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_2,&pwm_data[0],RESET_WORD+24*LED_NUM+DUMMY_WORD);
}
int main(void)
{
HAL_Init();
uint32_t RGB_COLOR=0x0000ff;
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM2_Init();
while (1)
{
ws2812b_show_dot(0,RGB_COLOR);
HAL_Delay(500);
ws2812b_show_dot(1,RGB_COLOR);
HAL_Delay(500);
}
}
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim2, TIM_CHANNEL_2);
}