STM32+PWM+DMA驱动WS2812 —— 2024年9月24日
一、项目简介
采用STM32f103C8t6单片机,使用HAL库编写。项目中针对初学者驱动WS2812时会遇到的一些问题,给出了解决方案。
二、ws2812驱动原理
WS2812采用单线归零码的通讯方式,即利用高低电平的持续时间来确定0和1。这种通信方式优点是只需要一根通信线,缺点是对通信的时序要求较高。
以官方数据手册的时序图为例,通信速率为800kbit/s,也就是PWM波的速率为800Kbit/s,每个PWM的周期为1.25微妙。PWM的一个周期即为一个数据帧,每个数据帧由一段高电平和一段低电平组成。下图为官方规定的数据传输时间:
T0H | 0码,高电平时间 | 0.22 us~0.35 us |
T0L | 0码,低电平时间 | 0.58 us~1.0 us |
T1H | 1码,高电平时间 | 0.58 us~1.0 us |
T1L | 1码,低电平时间 | 0.22 us~0.42 us |
RES | 帧间隔,低电平时间 | 50us以上 |
也就是说:一个1码,由2/3左右的高电平 和 1/3左右的低电平组成。
一个0码,由1/3左右的高电平 和 2/3左右的低电平组成。
若定时器的时钟频率为72MHz,那么预分频值设置为0,比较值设置为89,这样产生的PWM波的频率就为800KHz,周期为1.25us。要发送1码时,设置占空比为60。要发送0码时,设置占空比为29。
每个WS2812需要用24bit的数据来控制,当n个ws2812进行级联的时候,第一个灯会将第一个24bit的数据拦截,将后面的数据进行转发。第二个灯又会拦截第二个24bit的数据,将后面的数据进行转发。后面的逻辑也是一样,数据每经过一个灯,数据的前24bit就会被拦截下来,作为这个灯的显示内容。数据传输方法如下图所示:
代码编写逻辑:初始化的时候,需生成一个显存数组,由DMA将数组中的内容实时搬运到定时器的比较寄存器中,DMA要开启循环模式。之后我们只需要更新显存数组中的数据,WS2812的显示内容就会被实时更新。
三、Cube MX 生成底层代码
1、配置Debug的模式
2、配置外部晶振
3、配置时钟
4、配置定时器
5、配置定时器的DMA
6、生成代码
四、代码编写
1、下面ws2812.c的代码
#include "ws2812.h"
//显存数组,长度为 灯的数量*24+复位周期
uint16_t WS2812_RGB_Buff[LED_NUM*DATA_LEN+WS2812_RST_NUM] = {0};
/**
* 函数:WS2812单灯设置函数
* 参数:num:灯的位置,R、G、B分别为三个颜色通道的亮度,最大值为255
* 作用:单独设置每一个WS2812的颜色
***/
void WS2812_Set(uint16_t num,uint8_t R,uint8_t G,uint8_t B)
{
uint32_t indexx=(num*(3*8));
for (uint8_t i = 0;i < 8;i++)
{
//填充数组
WS2812_RGB_Buff[indexx+i] = (G << i) & (0x80)?WS_H:WS_L;
WS2812_RGB_Buff[indexx+i + 8] = (R << i) & (0x80)?WS_H:WS_L;
WS2812_RGB_Buff[indexx+i + 16] = (B << i) & (0x80)?WS_H:WS_L;
}
}
//WS2812初始化函数
void WS2812_Init()
{
//设置关闭所有灯
for(int i=0;i<8;i++)
{
WS2812_Set(i,0,20,0);
}
//作用:调用DMA将显存中的内容实时搬运至定时器的比较寄存器
HAL_TIM_PWM_Start_DMA(&htim2,TIM_CHANNEL_1,(uint32_t *)WS2812_RGB_Buff,sizeof(WS2812_RGB_Buff)/sizeof(uint16_t));
}
2、下面为ws2812.h的代码
#include "main.h"
#include "tim.h"
#define WS_H 60 // 1 码相对计数值
#define WS_L 29 // 0 码相对计数值
#define WS_REST 40 // 复位信号脉冲数量
#define LED_NUM 8 // WS2812灯个数
#define DATA_LEN 24 // WS2812数据长度,单个需要24个字节
#define WS2812_RST_NUM 50 // 官方复位时间为50us(40个周期),保险起见使用50个周期
void WS2812_Init(void);
void WS2812_Set(uint16_t num,uint8_t R,uint8_t G,uint8_t B);
3.下面为main.c中调用的代码,效果为流水灯
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
WS2812_Init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
//效果一,流水灯
for(int i=0;i<8;i++)
{
HAL_Delay(100);
WS2812_Set(i,2*(i+1),4*(i+1),10*(i+1));
}
HAL_Delay(300);
for(int i=0;i<8;i++)
{
WS2812_Set(i,0,0,0);
}
HAL_Delay(100);
//效果二,跑马灯
// for(int i=0;i<8;i++)
// {
// HAL_Delay(100);
// WS2812_Set(i,0,20,0);
// if(i==0) WS2812_Set(7,0,0,0);
// else WS2812_Set(i-1,0,0,0);
// }
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
五、烧录效果
六、注意事项
1、DMA的搬运方向为 内存(Memory) 到 外设(Peripheral)
2、DMA的模式为循环模式
3、DMA设置内存地址自增