ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)

本文开发环境:

  • MCU型号:STM32F103C8T6
  • IDE环境: MDK 5.27
  • 代码生成工具:STM32CubeMx 5.6.1
  • HAL库版本:STM32Cube_FW_F1_V1.8.0

本文内容:

  • STM32 使用 DMA+PWM 方式驱动 ws2812(x4)

附件:

  • MDK5 示例工程

ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第1张图片

一、DMA 的双缓存模式

STM32 系列的 MCU DMA 可以搬运2个源地址的数据,DMA 自动在2个地址A和B中来回切换,可以运用于搬运大数据:当DMA在搬运A数据时,MCU把新数据准备到B中,当DMA在搬运B数据时,MCU把新的数据准备到A中,通过不断的来回错开操作,可以搬运大的数据。
本文使用另一种方式:在DMA传输过程中,不只传输完成时,会产生一个传输完成(TC)中断,在完成一半的时候,也会产生一个半完成传输(HC)中断。
ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第2张图片
这种方式虽然只有一个源地址,但其效果是一至的。

二、DMA 双缓存在 ws2812 中的应用

1. 框图:

上文(ws2812 程序设计与应用(1))的原理是把所有 ws2812 数据 填充到数组中:
ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第3张图片
每增加一颗 ws2812 ,[DATA] 的体积就会增加(24x2)字节,在灯珠较多的情况下,会有比较大的内存损耗。
利用DMA的半完成传输机制,无论灯组多少,[DATA] 的数据永远是2个 ws2812 的大小,即(24x2x2)96个字节,同时,它需要建立一个数组,用来保存灯珠的颜色信息:
ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第4张图片

2. 基础流程

ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第5张图片
在 DMA 双缓存模式中,程序通过set2812_RGB_set()函数来对灯的颜色进行设置,这些值首先存放在pixel结构体中,由于有多个灯,所以需要一个结构体数组,接着需要一个数据填充函数ws2812_pixel_data_fill(),它可以把原始的数据转换成PWM占空比的值,存在DAM缓存中,最后通过HAL库函数,HAL_TIM_PWM_Start()函数,启动 DMA 传输,就完成了ws2812的驱动。

3. WS2812 时序与 DMA传输

1. ws2812 数据传输

ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第6张图片
DMA 被设置为循环传输模式,所以会不断的传输[pixel data]这48个字节:

  • MCU 准备好 pixel1 和 pixel2 的数据,启动DMA传输
  • DMA 发送半传输中断,此时DMA开始传输[pixel data]后半部分数据,MCU 将 pixel3 的数据写入到[pixel data + 0]([pixel data + 24]前半部分)中。
  • DMA 发生传输完成中断,此时DMA开始传输[pixel data]前半部分数据,MCU 将 pixel4 的数据写入到[pixel data + 24]([pixel data + 24]后半部分)中。

MCU 是通过 ws2812_pixel_data_fill() 来写入的,该函数的数据是 struct pixel 提供的,而pixel的数据是 ws2812_RGBset() 设置的。

2. 复位信号

对于 ws2812 ,在一帧数据传输之前,需要一个不低于50us的低电平,作为Reset信号,接收到Reset信号以后,前24个PWM就成了第一个灯组的颜色数据,为了程序的统一,本文改进了流程:
ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第7张图片
在上图中,首先将 [pixel data]的值设置为零,当DMA传输的时候,首先会传输48个0,即产生了为时长约为60us(1.25us x 48) 的低电平,这个信号可以作为Reset信号

3. 更规范的时序

对于 MCU 来说,它响应DMA的中断有一定的滞后性,比如,当DMA发送半传输中断时,内核首先响应这个中断,然后通过HAL库处理一些标志位,最后才调用到回调函数去执行用户程序,这些都需要时间,当用户程序被执行时候,DMA已经传输一些波形了,这就导致一个现象,ws8212 接收到的 PWM 是多出几个波形的,这会导致一个非常奇怪的bug:最后一个灯的最后一个数据,不能是 0x03、0x07、0x1F 等这些bit1为1的值。处理这个问题是简单的,只需要让它多传输的PWM,占空比为0即可,所以需要进一步改进流程(以4灯为例):
ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第8张图片
虽然滞后性无法避免的,但是通过这种方法,可以让多出来的波形占空比为0,其实就是低电平,所以不影响后续数据的传输。

三、DMA 程序设计与实现

STM32CubeMx 配置

CubeMx 的配置与上文(ws2812 程序设计与应用(1))基本一致,只需要将占空比默认值59改为0,避免首次运行颜色错乱的现象:
ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第9张图片
这是因为如果不改为0,DMA首次运行的时候,会多产生一个1码(DMA需要定时器溢出作为触发条件,第一次溢出会产生一个PWM,然后触发第一次DMA搬运),接着是一个复位信号,才是灯的颜色数据,根据测试效果,虽然是这个1码在复位信号之前,却依然会影响到灯的显示效果。

程序设计(4灯)

新建一个 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 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)_第10张图片

四、附件

链接:ws2812 DMA 双缓存
提取码:1234

你可能感兴趣的:(#,传感器与功能模块)