本文开发环境:
- MCU型号:STM32F1038C8T6
- IDE环境: MDK 5.27
- 代码生成工具:STM32CubeMx 5.6.1
- HAL库版本:STM32Cube_FW_F1_V1.8.0
本文内容:
- STM32 使用 DMA+PWM 方式驱动 ws2812(x4)
附件:
- MDK5 示例工程
- WS2812 中英文数据手册
WS2812 内部集成了处理芯片和3颗不同颜色的led灯(红,绿,蓝),通过单总线协议分别控制三个灯的亮度强弱,达到全彩的效果,每一个灯需要 8 bits(1 byte) 的数据,所以一颗 ws2812 共需要24 bits(3 bytes) 的数据。
ws2812 采用 PWM方式来编码,即每个PWM的周期固定为1.25us(800k),占空比为 1/3 时为 0 码,占空比为 2/3 为 1 码。另外,ws2812 复位信号为一个 不低于50us的低电平:
ws2812 的特点是可以多个灯珠串联起来,这样就可以通过一个总线控制多个灯珠:
ws2812 可以将第一个24字节的时序留下,余下的往下一位传递:
可以结合波逻辑分析仪捕获的波形来理解:
上图是4个led的驱动时序,首先是一个100us的低电平复位来ws2812,接着是第一个led灯的数据,它有24个周期,分为3个部分,每一个部分是8个周期分别对应不同颜色的灯,可以看到,第一个部分都是宽占空比,所以全是1,第二个部分都是窄占空比,所以全是0,第三个部分和第一个部分同。所以第一颗led灯,是绿色和蓝色一起亮,视觉效果为青色,其它同理。
时序的数据结构是,高位到低位:
程序实现之前,需要了解清楚时序,更多请参考附件中相关手册。
本文只讨论 DMA+PWM+TIM 的方式来实现,其他的开发方式还有以下几种:
定时器 TIM 用以产生一个固定周期的PWM,DMA用以改变PWM 的占空比:
如图,DMA通过不断的搬运数据到定时器调节占空比的CCR寄存器,实现ws2812时序的产生,在STM32中,通过配置外设可实现:定时器每产生一次溢出事件(即计数完成),就请求一次DMA搬运一个数据(长度:字节/半字/字可选),所以用户只需要将数据排列在数组里,就可以产生所需要的时序。
基础配置是每一个工程都必须的,包括系统时钟,指定延时函数定时器,打开调试口等:
由 CubeMx 可知,所配置的定时器1通道1在PA8上,所以将ws2812的信号线连接到PA8上,注意ws2812的供电充足,并与STM32共地。
程序的初始化一般CuMx自动为程序员添加代码,所以驱动ws2812只需要以下三步:
... ...
// DMA 传输完成回调函数
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim1,TIM_CHANNEL_1);
}
//DMA 需要传输的数据
uint16_t pulse[176] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\
59,59,59,59,59,59,59,59,\
29,29,29,29,29,29,29,29,\
59,59,59,59,59,59,59,59,\
\
29,29,29,29,29,29,29,29,\
59,59,59,59,59,59,59,59,\
29,29,29,29,29,29,29,29,\
\
29,29,29,29,29,29,29,29,\
29,29,29,29,29,29,29,29,\
59,59,59,59,59,59,59,59,\
\
29,29,29,29,29,29,29,29,\
29,29,29,29,29,29,29,29,\
29,29,29,29,29,29,29,29,};
//测试函数
void ws2182_test(void)
{
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)pulse,(176));
}
第3行:HAL_TIM_PWM_PulseFinishedCallback()
是一个回调函数,当DMA传输完成以后,就会调用这个函数,由于本文DMA传输模式选择为Circular
,所以DMA需要手动关闭,否则DMA会不断的搬运数据。
第9行:这是一个176字节的数组,注意到这不是一个小的数组,所以尽量避免在函数内被申请,请在全局变量区域申请。数组的开始是80个0
,DMA 没一个定时器周期就搬运一个0
到定时器CCR中,定时器将产生一个1.25us的全低电平,80个为100us,这个100us的低电平作为ws2812的复位信号。
接着,数据在逻辑上被分为4个部分,对应的是4个灯珠的颜色数据,其中,每一个部分又分为3行,每一行,代表该灯珠某一颜色的数据。
在定时器中,由于配置为计90个数一个周期,所以计数到30拉低电平(占空比 1/3)就产生了一个0码,若计数到60个拉低电平(占空比 2/3)就产生了1码,定时器计数个数为其值+1,所以这里数值29
,59
将会对应产生0码
和1码
第28行:这是一个示例函数,它使用HAL_TIM_PWM_Start_DMA()
,开启本次的DMA传输,运行该函数以后,定时器将被打开,DMA也开始搬运数据。
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_TIM1_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
ws2182_test(); //调用测试函数
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
验证程序可以在mian()
初始化完所有硬件之后,调用ws8212_test()
函数,之后程序进入while(1)
循环等待。
程序运行正常,将可以看到一个红灯,一个蓝灯,一个青灯,最后一个关闭:
上述程序实现了灯效的控制,但是其数组不够灵活,所以需要封装一个函数,这个函数可以通过用户传入的RGB参数,来自动填充数组:
#define ONE_PULSE (59) //1 码计数个数
#define ZERO_PULSE (29) //0 码计数个数
#define RESET_PULSE (80) //80 复位电平个数(不能低于40)
#define LED_NUMS (4) //led 个数
#define LED_DATA_LEN (24) //led 长度,单个需要24个字节
#define WS2812_DATA_LEN (LED_NUMS*LED_DATA_LEN) //ws2812灯条需要的数组长度
... ...
uint16_t static RGB_buffur[RESET_PULSE + WS2812_DATA_LEN] = { 0 };
... ...
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;
p[i + 8] = (R << i) & (0x80)?ONE_PULSE:ZERO_PULSE;
p[i + 16] = (B << i) & (0x80)?ONE_PULSE:ZERO_PULSE;
/*
}
}
其中 p[i] = (G << i) & (0x80)?ONE_PULSE:ZERO_PULSE;
是一个三元操作符,其等效以下程序:
if ((R << i) & (0x80))
{
p[i] = ONE_PULSE;
}
else
{
p[i] = ZERO_PULSE;
}
void ws2812_example(void)
{
//#1.填充数组
ws2812_set_RGB(0x22, 0x00, 0x00, 0);
ws2812_set_RGB(0x00, 0x22, 0x00, 1);
ws2812_set_RGB(0x00, 0x00, 0x22, 2);
ws2812_set_RGB(0x22, 0x22, 0x22, 3);
//#2.传输数据
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)RGB_buffur,(176));
//#3.延时:使效果可以被观察
HAL_Delay(500);
ws2812_set_RGB(0x22, 0x00, 0x00, 1);
ws2812_set_RGB(0x00, 0x22, 0x00, 2);
ws2812_set_RGB(0x00, 0x00, 0x22, 3);
ws2812_set_RGB(0x22, 0x22, 0x22, 0);
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)RGB_buffur,(176));
HAL_Delay(500);
}
int main(void)
{
... ...
HAL_Init();
... ...
while (1)
{
... ...
ws2812_example();
.... ...
}
/* USER CODE END 3 */
}
ws2812_example()
函数设置了灯的颜色情况,并通过延时来控制灯的闪烁:
MDK5工程及中英文手册
提取码:c4r8