本文开发环境:
- MCU型号:STM32F103C8T6
- IDE环境: MDK 5.27
- 代码生成工具:STM32CubeMx 5.6.1
- HAL库版本:STM32Cube_FW_F1_V1.8.0
本文内容:
- STM32 使用 DMA+PWM 方式驱动 ws2812(x4)
附件:
- MDK5 示例工程
STM32 系列的 MCU DMA 可以搬运2个源地址的数据,DMA 自动在2个地址A和B中来回切换,可以运用于搬运大数据:当DMA在搬运A数据时,MCU把新数据准备到B中,当DMA在搬运B数据时,MCU把新的数据准备到A中,通过不断的来回错开操作,可以搬运大的数据。
本文使用另一种方式:在DMA传输过程中,不只传输完成时,会产生一个传输完成(TC)中断,在完成一半的时候,也会产生一个半完成传输(HC)中断。
这种方式虽然只有一个源地址,但其效果是一至的。
上文(ws2812 程序设计与应用(1))的原理是把所有 ws2812 数据 填充到数组中:
每增加一颗 ws2812 ,[DATA] 的体积就会增加(24x2)字节,在灯珠较多的情况下,会有比较大的内存损耗。
利用DMA的半完成传输机制,无论灯组多少,[DATA] 的数据永远是2个 ws2812 的大小,即(24x2x2)96个字节,同时,它需要建立一个数组,用来保存灯珠的颜色信息:
在 DMA 双缓存模式中,程序通过set2812_RGB_set()
函数来对灯的颜色进行设置,这些值首先存放在pixel
结构体中,由于有多个灯,所以需要一个结构体数组,接着需要一个数据填充函数ws2812_pixel_data_fill()
,它可以把原始的数据转换成PWM占空比的值,存在DAM缓存中,最后通过HAL库函数,HAL_TIM_PWM_Start()
函数,启动 DMA 传输,就完成了ws2812的驱动。
DMA 被设置为循环传输模式,所以会不断的传输[pixel data]这48个字节:
MCU 是通过 ws2812_pixel_data_fill()
来写入的,该函数的数据是 struct pixel
提供的,而pixel的数据是 ws2812_RGBset()
设置的。
对于 ws2812 ,在一帧数据传输之前,需要一个不低于50us的低电平,作为Reset信号,接收到Reset信号以后,前24个PWM就成了第一个灯组的颜色数据,为了程序的统一,本文改进了流程:
在上图中,首先将 [pixel data]的值设置为零,当DMA传输的时候,首先会传输48个0,即产生了为时长约为60us(1.25us x 48) 的低电平,这个信号可以作为Reset信号
对于 MCU 来说,它响应DMA的中断有一定的滞后性,比如,当DMA发送半传输中断时,内核首先响应这个中断,然后通过HAL库处理一些标志位,最后才调用到回调函数去执行用户程序,这些都需要时间,当用户程序被执行时候,DMA已经传输一些波形了,这就导致一个现象,ws8212 接收到的 PWM 是多出几个波形的,这会导致一个非常奇怪的bug:最后一个灯的最后一个数据,不能是 0x03、0x07、0x1F 等这些bit1为1的值。处理这个问题是简单的,只需要让它多传输的PWM,占空比为0即可,所以需要进一步改进流程(以4灯为例):
虽然滞后性无法避免的,但是通过这种方法,可以让多出来的波形占空比为0,其实就是低电平,所以不影响后续数据的传输。
CubeMx 的配置与上文(ws2812 程序设计与应用(1))基本一致,只需要将占空比默认值59改为0,避免首次运行颜色错乱的现象:
这是因为如果不改为0,DMA首次运行的时候,会多产生一个1码(DMA需要定时器溢出作为触发条件,第一次溢出会产生一个PWM,然后触发第一次DMA搬运),接着是一个复位信号,才是灯的颜色数据,根据测试效果,虽然是这个1码在复位信号之前,却依然会影响到灯的显示效果。
新建一个 ws2812.c
文件:
#include "main.h"
#include "string.h"
#include "stdio.h"
extern TIM_HandleTypeDef htim1; //声明 htim1 handle
extern DMA_HandleTypeDef hdma_tim1_ch1; //声明 hdma_tim1_ch1 handle
#define PULSE_1_CODE (59) //1 码计数个数
#define PULSE_0_CODE (29) //0 码计数个数
#define PIXEL_NUM (4) //led 个数
#define PIXEL_DATA_LEN (24)
// ws2812 结构体数组
typedef struct{
uint8_t r;
uint8_t g;
uint8_t b;
}piexel_t;
piexel_t pixel[PIXEL_NUM];
// DMA 传输的数据
uint16_t ws2812_DMA_data[PIXEL_DATA_LEN * 2] = {0};
/************************ 函数声明 ******************************/
void ws2812_RGB_set(uint8_t R,uint8_t G,uint8_t B,uint8_t pixel_id); //设置颜色
uint8_t ws2812_pixel_data_fill(uint8_t pixel_next,uint16_t *addr); //传输数据
/************************ 函数定义 ******************************/
ws2812_RGB_set()
函数需要对参数进行检查,如果传入的值不对,不做处理,避免数组越界:
void ws2812_RGB_set(uint8_t R,uint8_t G,uint8_t B,uint8_t led_id)
{
if(led_id >= (PIXEL_NUM))
return;
pixel[led_id].r = R;
pixel[led_id].g = G;
pixel[led_id].b = B;
return;
}
ws2812_pixel_data_fill()
同样需要对参数,避免访问越界数组
uint8_t ws2812_pixel_data_fill(uint8_t pixel_next,uint16_t *addr)
{
if(pixel_next >= PIXEL_NUM)
{
return 0;
}
for (uint8_t i = 0;i < 8;i++)
{
//填充数组
addr[i] = (pixel[pixel_next].g << i) & (0x80)?PULSE_1_CODE:PULSE_0_CODE;
addr[i + 8] = (pixel[pixel_next].r << i) & (0x80)?PULSE_1_CODE:PULSE_0_CODE;
addr[i + 16] = (pixel[pixel_next].b << i) & (0x80)?PULSE_1_CODE:PULSE_0_CODE;
}
}
回调函数本文实现的比较简陋,请根据实际程序改进。它是根据DMA中断的次数,推算需要将第几个ws2812的数值,写入到[pixel data],传输完成灯光颜色数据后,使用memset()
,清零数组,让其传输低电平:
volatile int cnt = 0;
//TC 回调函数
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
cnt++;
if(cnt == 2)
{
ws2812_pixel_data_fill(1,ws2812_DMA_data+24);
}
if(cnt == 4)
{
ws2812_pixel_data_fill(3,ws2812_DMA_data+24);
}
if(cnt == 6)
{
memset(ws2812_DMA_data+24,0x00,48);
}
if(cnt == 8)
{
cnt = 0;
HAL_TIM_PWM_Stop_DMA(&htim1,TIM_CHANNEL_1);
//__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,0);
}
}
//HC 回调函数
void HAL_TIM_PWM_PulseFinishedHalfCpltCallback(TIM_HandleTypeDef *htim)
{
cnt++;
if(cnt == 1)
{
ws2812_pixel_data_fill(0,ws2812_DMA_data);
}
if(cnt == 3)
{
ws2812_pixel_data_fill(2,ws2812_DMA_data);
}
if(cnt == 5)
{
memset(ws2812_DMA_data,0x00,48);
}
}
__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,0)
是非必须的,因为最后清零数组,占空比肯定为 0。
最后一个API测试效果:
void ws2812_example_2(void)
{
uint8_t static bri1 = 0x0F;
//memset(ws2812_DMA_data,0x00,96);
ws2812_RGB_set(bri1, bri1, bri1, 0);
ws2812_RGB_set(bri1, 0x00, 0x00, 1);
ws2812_RGB_set(0x00, bri1, 0x00, 2);
ws2812_RGB_set(0x00, 0x00, bri1, 3);
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)ws2812_DMA_data,(48));
HAL_Delay(100);
}
memset(ws2812_DMA_data,0x00,96);
是非必须的,其目的清零数组,使其可产生复位信号,但是这个数组在最后数据传输阶段,为了避免bug,已经被清零了。
直接在 main.c 调用示例函数即可,注意用 extern 声明外部函数:
//file main.c:
... ...
extern void ws2812_example_2(void);
void main(void)
{
... ...
while(1)
{
ws2812_example_2();
}
}
链接:ws2812 DMA 双缓存
提取码:1234