STM32使用PWM+DMA方式驱动WS2812灯珠

一. 关于WS2812

WS2812 内部集成了处理芯片和3颗不同颜色的led灯(红,绿,蓝),通过单总线协议分别控制三个灯的亮度强弱,达到全彩的效果。

WS2812B Datasheet

二. WS2812灯珠的几种驱动方式

  1. 使用延时函数
    直接翻转IO口产生时序,这种方式最为简单易用,只需要控制延时的时间,就可以从产生0和1码,它需要占用系统资源。
    使用 SPI 数据传输产生时序
  2. 通过SPI控制
    只需要控制在合适的波特率,在传输不同数据的时候,可以产生符合要求的0和1码,这种方式需要等同于使用了一个SPI设备
  3. 使用 DMA+Timer 产生时序
    这种方式需要使用一个定时器,其中一个通道固定产生一个周期1.25us的PWM,占空比2/3,接着需要另一个通道,在周期的1/3处搬运数据到IO口,若为1,PWM不变,若为0,PWM则为0码,这种方式有更大的局限性,由于DMA只能搬运至少一个字节,所以每次会同时改变8个IO口的高低电平,或许使用位带操作可以解决这问题
  4. 使用 Timer+PWM+DMA 产生时序
    本文讨论的实现方案,这种方案有2种驱动的方式,一种是直接建立一个大的数组,存放所有灯珠的数据,然后启动DMA传输,第二种是建立2个灯组数据大小的数组,当DMA传输一个灯珠数据时,改变另一个灯组数据,通过不断改变数组的方式,节约内存,相比较而言,第一种方式较为直观,第二种方式则可以解决灯珠较多的情况,本文讨论第一种的原理和程序的实现。

三. STM32CubeMX 相关配置

基于 STM32F405RGT6

由于项目选择的TIM8定时器, 查询Datasheet得知, TIM8挂载于APB2总线

APB2的时钟基准为168MHz

计算自动重装载数值:
我们要产生一个周期为1.25us的PWM,
则 自动重装载值 = 0.00000125 * 168000000 = 210
减一不多说


四. 代码实现部分

/**
* @file 在 tim.c 文件增加以下内容
*/

void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
    HAL_TIM_PWM_Stop_DMA(&htim8, TIM_CHANNEL_3);
}
/**
* @file ws2812.c
* @brief WS2812 LED driver, use (PC8) TIM8-CH3 PWM mode.
* @author William
* @date 2022.5.20
*/

#include "ws2812.h"
#include "tim.h"

/* WS2812B f=800k, T=1.25us */
#define ONE_PULSE    (143) //1 码 (2/3*T)
#define ZERO_PULSE   (67)  //0 码 (1/3*T)
#define RESET_PULSE  (9000)  //低电平复位信号50us
#define LED_DATA_LEN (24)  //led 颜色数据长度, 一个灯珠需要24bits
#define WS2812_DATA_LEN (RESET_PULSE + LED_NUMS * LED_DATA_LEN) //ws2812灯条需要的总数组长度

/* !! 若嵌套循环使用, 须注意变量(i) !! */
#define LOOP_ALL for(size_t i = 0; i < LED_NUMS; i++) /* 所有灯珠 */

#define DEFAULT_BRIGHTNESS (100) //灯带默认亮度: 0~100

static uint16_t RGB_buffer[WS2812_DATA_LEN] = {0};

static void ws2812_refresh(void);
static void color_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b);


/* ************************************ Public Functions ************************************ */

/**
* @brief WS2812初始化, 全黑
*/
void ws2812_init(void)
{
    ws2812_set_dark(MAX);
}

/**
* @brief 设置某个灯珠颜色RGB
*
* @param uint8_t R,G,B: RGB色彩格式 红,绿,蓝通道数据
*        uint16_t num, 指定设置颜色的灯珠位号
*/
void ws2812_set_RGB(uint8_t R, uint8_t G, uint8_t B, uint16_t num)
{
    if (num > LED_NUMS)
        return;

    uint16_t *p = (RGB_buffer + RESET_PULSE) + (num * LED_DATA_LEN);

    for (uint16_t i = 0; i < 8; i++)
    {
        p[i]      = (G << i) & (0x80) ? ONE_PULSE : ZERO_PULSE;
        p[i + 8]  = (R << i) & (0x80) ? ONE_PULSE : ZERO_PULSE;
        p[i + 16] = (B << i) & (0x80) ? ONE_PULSE : ZERO_PULSE;
    }
}

/**
* @brief 设置某个灯珠颜色HSV
*
* @param uint8_t H,S,V: HSV色彩格式
*        uint16_t num, 指定设置颜色的灯珠位号
*/
void ws2812_set_HSV(uint16_t H, uint16_t S, uint16_t V, uint16_t num)
{
    uint32_t R = 0, G = 0, B = 0;

    if (num > LED_NUMS)
        return;

    uint16_t *p = (RGB_buffer + RESET_PULSE) + (num * LED_DATA_LEN);

    color_hsv2rgb(H, S, V, &R, &G, &B);

    for (uint16_t i = 0; i < 8; i++)
    {
        p[i]      = (G << i) & (0x80) ? ONE_PULSE : ZERO_PULSE;
        p[i + 8]  = (R << i) & (0x80) ? ONE_PULSE : ZERO_PULSE;
        p[i + 16] = (B << i) & (0x80) ? ONE_PULSE : ZERO_PULSE;
    }
}

/**
* @brief 灭灯
*/
void ws2812_set_dark(uint8_t type)
{
    switch (type)
    {
    default:
        LOOP_ALL
        {
            ws2812_set_RGB(0x00, 0x00, 0x00, i);
        }
        break;
    }

    ws2812_refresh();
}

/**
* @brief WS2812全彩渐变
* @note 需放置循环体内
*/
void ws2812_colorful_shadow(void)
{
    for (uint16_t color = 0; color < 360; color++)
    {
        for (uint8_t i = 0; i < LED_NUMS; i++)
        {
            ws2812_set_HSV(color, 100, DEFAULT_BRIGHTNESS, i);
        }
        ws2812_refresh();
        osDelay(50);
    }
}

/* ************************************ Static Functions ************************************ */

/**
* @brief WS2812颜色数据刷新, 修改颜色值后调用
*/
static void ws2812_refresh(void)
{
    HAL_TIM_PWM_Start_DMA(&htim8, TIM_CHANNEL_3, (uint32_t *)RGB_buffer, WS2812_DATA_LEN);
}

/**
 * @brief 将HSV颜色空间转换为RGB颜色空间
 *
 * @param  h HSV颜色空间的H:色调, 范围0~360
 * @param  s HSV颜色空间的S:饱和度, 范围0~100
 * @param  v HSV颜色空间的V:明度, 范围0~100
 * @param  r 转换后RGB-R值的指针
 * @param  g 转换后RGB-G值的指针
 * @param  b 转换后RGB-B值的指针
 *
 */
static void color_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b)
{
    h %= 360; // h -> [0,360]
    uint32_t rgb_max = v * 2.55f;
    uint32_t rgb_min = rgb_max * (100 - s) / 100.0f;

    uint32_t i = h / 60;
    uint32_t diff = h % 60;

    uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60;

    switch (i)
    {
    case 0:
        *r = rgb_max;
        *g = rgb_min + rgb_adj;
        *b = rgb_min;
        break;
    case 1:
        *r = rgb_max - rgb_adj;
        *g = rgb_max;
        *b = rgb_min;
        break;
    case 2:
        *r = rgb_min;
        *g = rgb_max;
        *b = rgb_min + rgb_adj;
        break;
    case 3:
        *r = rgb_min;
        *g = rgb_max - rgb_adj;
        *b = rgb_max;
        break;
    case 4:
        *r = rgb_min + rgb_adj;
        *g = rgb_min;
        *b = rgb_max;
        break;
    default:
        *r = rgb_max;
        *g = rgb_min;
        *b = rgb_max - rgb_adj;
        break;
    }
}

/**
* @file ws2812.h
* @brief WS2812 LED driver, use TIM8-CH3 PWM mode.
* @author William
* @date 2022.5.20
*/

#ifndef  __WS2812_H
#define __WS2812_H

#include "stdint.h"

#define LED_NUMS (9) //灯珠数量

/* HSV格式常用色值预设 */
#define RED 0
#define YELLOW 60
#define GREEN 120
#define CYAN 180
#define BLUE 240

void ws2812_init(void);
void ws2812_set_RGB(uint8_t R, uint8_t G, uint8_t B, uint16_t num);
void ws2812_set_HSV(uint16_t H, uint16_t S, uint16_t V, uint16_t num);
void ws2812_set_dark(uint8_t type);
void ws2812_colorful_shadow(void);

#endif  //__WS2812_H


声明:
本文有参考其他博客, 时间久远, 已忘记原文链接
另外, 本文整合并精简了部分代码, 也增加了常用的颜色格式转换等函数, 更容易使用些
互相交流学习, 不喜勿喷

物联沃分享整理
物联沃-IOTWORD物联网 » STM32使用PWM+DMA方式驱动WS2812灯珠

发表评论