Skip to main content

RT-Thread驱动WS2812-PWM+DMA

演示视频

WS2812简介

以下内容节选自WS2812数据手册

概述

​ WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源。其外型与一个5050LED灯珠相同,每 个元件即为一个像素点。像素点内部包含了智能数字接口数据锁存信号整形放大驱动电路,还包含有高精度的内 部振荡器和12V高压可编程定电流控制部分,有效保证了像素点光的颜色高度一致。 ​ 数据协议采用单线归零码的通讯方式,像素点在上电复位以后,DIN端接受从控制器传输过来的数据,首先 送过来的24bit数据被第一个像素点提取后,送到像素点内部的数据锁存器,剩余的数据经过内部整形处理电路 整形放大后通过DO端口开始转发输出给下一个级联的像素点,每经过一个像素点的传输,信号减少24bit。像素 点采用自动整形转发技术,使得该像素点的级联个数不受信号传送的限制,仅仅受限信号传输速度要求。 ​ LED具有低电压驱动,环保节能,亮度高,散射角度大,一致性好,超低功率,超长寿命等优点。将控制电 路集成于LED上面,电路变得更加简单,体积小,安装更加简便。

机械尺寸

0

引脚功能

1

电气参数

2

时序波形图

3

数据传输时间( TH+TL=1.1µs±300ns)

4

数据传输方法

注:其中 D1 为 MCU 端发送的数据,D2、D3、D4 为级联电路自动整形转发的数据。

5

数据结构及应用电路

6

WS2812驱动方式

目前驱动ws2812的方式可以概括为3中

  1. 直接使用IO口操作:由于WS2812的数据位在ns级别,这对于低速单片机极不友好,占用CPU资源
  2. SPI+DMA:SPI总线的频率设置在某一特定频率的时候,刚好可以模拟出WS2812的数据位,但实际上发现任然有很多问题,例如在RGB值给到10以下会出时序不对的情况,单片机的SPI接口一般较少,占用了稀缺的资源
  3. PWM+DMA:PWM本身由定时器产生,不占用CPU时间,由DMA来负责改变定时器的波形,CPU只需要填充好一条灯带的数据,告诉DMA控制器即可完成一整条灯带的控制;其次定时器资源在现代MCU中是非常丰富的,因此推荐此方式

例如在rtt中,maplerian也为其开发了一款软件包rt_ws2812b,使用 SPI + DMA 方式驱动。因为Rt-thread的设备驱动中对SPI的DMA模式有良好的支持。但其也有不少问题,例如第一个灯的颜色始终不正常,1个ws2812b节点需要 2 x 8 x 3 = 48字节 因此,太多节点谨慎使用。

Rt-thread中实现

目前为止,rtt的设备框架中的pwm设备还没有支持pwm的dma模式,因此可以看到在rtt中并没有一款软件包可以支持PWM+DMA的方式来实现WS2812的驱动

那么,是不是就办不成了呢。

当然不是,我们可以直接调用HAL库,不经过其设备驱动层,直接访问底层资源,当然这样做有几个限制:

  1. 无法适配不同的MCU,不同平台,可移植性收到限制
  2. 无法作为软件包发布

但,只要掌握方法,在任何平台移植就行,该问题需要Rt-thread在以后的版本中将pwm的dma模式加上,届时再改进

使用cubemx生成PWM与DMA配置文件

由于我是用的是Rt-thread Studio,4.0.2版本以后在Studio中创建的工程都会有CubeMx Setting选项,双击打开cube配置

开启定时器4的第一路通道选择pwm生成

10

配置定时器重载值(系统时钟频率为168MHZ)

11

配置DMA

12

拷贝配置文件

生成之后,在文件树中cubemx文件夹下即可看到生成之后的代码,但这不是全部,因为有些部分没有添加到编译中来,因此我们右键,在文件资源管理器中打开。

在工程中新建文件ws2812_port.c

  1. 将cubemx/src/main.c 中的MX_TIM4_Init 拷贝过来
  2. 将cubemx/src/main.c 中的MX_DMA_Init拷贝到MX_TIM4_Init
  3. 将cubemx/src/stm32f4xx_it.c 中的DMA1_Stream0_IRQHandler拷贝过来
  4. 拷贝htim4以及hdma_tim4_ch1的定义

因为main.c和stm32f4xx_it.c在rtt中默认不会被编译,因此需要自己编写,这里使用cubemx生成拷贝,简化流程。 在末尾使用了INIT_BOARD_EXPORT,该函数会自动在启动时被调用,完成底层的初始化。 程序如下(ws2812_port.c)

#include "board.h"

extern void HAL_TIM_MspPostInit(TIM_HandleTypeDef *htim);

TIM_HandleTypeDef htim4;
DMA_HandleTypeDef hdma_tim4_ch1;

void DMA1_Stream0_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_tim4_ch1);
}

int MX_TIM4_Init(void)
{
/* USER CODE BEGIN TIM4_Init 0 */
__HAL_RCC_DMA1_CLK_ENABLE()
;
HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream0_IRQn);
/* USER CODE END TIM4_Init 0 */

TIM_ClockConfigTypeDef sClockSourceConfig = { 0 };
TIM_MasterConfigTypeDef sMasterConfig = { 0 };
TIM_OC_InitTypeDef sConfigOC = { 0 };

/* USER CODE BEGIN TIM4_Init 1 */

/* USER CODE END TIM4_Init 1 */
htim4.Instance = TIM4;
htim4.Init.Prescaler = 0;
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = 104;
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim4) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 59;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM4_Init 2 */

/* USER CODE END TIM4_Init 2 */
HAL_TIM_MspPostInit(&htim4);
return 0;
}

INIT_BOARD_EXPORT(MX_TIM4_Init);

WS2812的驱动

底层完成之后,即可直接调用HAL库驱动WS2812

ws2812.c

#include "rtthread.h"
#include "board.h"
#include "ws2812.h"

extern TIM_HandleTypeDef htim4;

void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim4, TIM_CHANNEL_1);
}

uint16_t RGB_buffur[RESET_PULSE + WS2812_DATA_LEN] = { 0 };
uint16_t *ptr;

void ws2812_set_rgb(uint8_t R, uint8_t G, uint8_t B, uint16_t num)
{
//指针偏移:需要跳过复位信号的N个0
uint16_t* p = (RGB_buffur + RESET_PULSE) + (num * LED_DATA_LEN);

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

void ws2812_set_all_rgb(uint8_t R, uint8_t G, uint8_t B, uint16_t led_nums)
{
uint16_t num_data;
num_data = 80 + led_nums * 24;
for (uint8_t i = 0; i < led_nums; i++)
{
ws2812_set_rgb(R, G, B, i);
}
HAL_TIM_PWM_Start_DMA(&htim4, TIM_CHANNEL_1, (uint32_t *) RGB_buffur, (num_data));
}

void ws2812_init(uint8_t led_nums)
{
ws2812_set_all_rgb(255, 255, 255, led_nums);
}

ws2812.h

#ifndef  WS2812B_H
#define WS2812B_H

#define ONE_PULSE (69) //1 码计数个数
#define ZERO_PULSE (35) //0 码计数个数
#define RESET_PULSE (80) //80 复位电平个数(不能低于40)
#define LED_NUMS (100) //led 个数
#define LED_DATA_LEN (24) //led 长度,单个需要24个字节
#define WS2812_DATA_LEN (LED_NUMS*LED_DATA_LEN) //ws2812灯条需要的数组长度

void ws2812_init(uint8_t led_nums);
void ws2812_set_rgb(uint8_t R, uint8_t G, uint8_t B, uint16_t num);
void ws2812_set_all_rgb(uint8_t R, uint8_t G, uint8_t B, uint16_t led_nums);

#endif

现象

如成功驱动,在开机时调用ws2812_init,所有灯会变为白色。