一.WS2812驱动原理
WS2812灯有+5v,GND,DO三个引脚,采用的驱动方式为高低电平驱动,具体高低电平的时序要求如下图所示,采用RGB三元色的搭配来组合颜色,为24bit的数据,对应顺序为GRB。对应颜色的GRB的网上查阅。
由于时序的范围区间比较小,且为纳秒级别,所以一般采用PWM+DMA方式控制,也可采用延时函数的方法,但是操作比较困难。
本人将实现两个WS2812灯珠的点亮。下面是具体的实现步骤。
二.实现步骤
2.1 TIM1初始化
定时器的初始化包括定时器基地址,预分频值,计数模式,自动重装值,分频系数,和重复次数,以及预装载功能的设置。
然后需要先清理PWM更新事件标注位。
接下来配置PWM,PWM模式一般为PWM1,设置自动重装值为0(由于还没有开传输,所以给0即可),低电平有效,关闭快速模式。最后选择通道,使能PWM。DMA使能传输可在传输时再开启。具体代码如下。
void TIM1_Init(uint16_t arr, uint16_t psc, uint8_t rep)
{
//初始化定时器
TIM1_Handle.Instance = TIM1;
TIM1_Handle.Init.Prescaler = psc; //分频值
TIM1_Handle.Init.CounterMode = TIM_COUNTERMODE_UP; //计数模式
TIM1_Handle.Init.Period = arr; //自动重装值
TIM1_Handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; //分频系数
TIM1_Handle.Init.RepetitionCounter = rep; //重复次数
TIM1_Handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; //设置自动重装值 预装载使能位
HAL_TIM_PWM_Init(&TIM1_Handle);
__HAL_TIM_CLEAR_FLAG(&TIM1_Handle, TIM_FLAG_UPDATE); //人为清理 HAL_TIM_PWM_Init 产生的 更新事件的标志位
TIM_OC_Handle.OCMode = TIM_OCMODE_PWM1; //PWM模式1
TIM_OC_Handle.Pulse = 0; //比较值
TIM_OC_Handle.OCPolarity = TIM_OCPOLARITY_HIGH; //低电平有效
TIM_OC_Handle.OCFastMode = TIM_OCFAST_DISABLE; //关闭快速模式
HAL_TIM_PWM_ConfigChannel(&TIM1_Handle, &TIM_OC_Handle, TIM_CHANNEL_1); //设置PWM通道
// HAL_TIM_PWM_Start_DMA(&TIM1_Handle, TIM_CHANNEL_1, (uint32_t *)DMA_BUFF, 4); //DMA方式打开通道1 4次传输
}
2.2 TIM_PWM回调函数
由于使用的是HAL方式,所以要在HAL_TIM_PWM_MspInit函数中进行IO口和DMA的相关初始化工作。
回调函数中首先是打开IO口、TIM1、DMA时钟,然后需要使用HAL_SYSCFG_DMA_Req(0x0b); /* DMA1_MAP 选择TIM1_CH1 */,这也是PY系列和ST系列DMA使用的区别,需重点注意,具体的参数需要在芯片参考手册中查找,这里使用的是TIM1-CH1,所以是写0x0b。然后使能需要使用的IO口。
接着配置DMA,DMA的配置一般包括基地址,使用的是DMA1_Channel1;传输方向,方向为存储器到外设;起始地址是否自增,起始地址为数组地址,自增;终点地址是否自增,终点地址为TIM1,不自增;起始地址和终点地址的数据宽度都为半字,也就是16bit;由于采用轮询的方式,所以工作模式为单次模式。优先级给中等即可。然后链接DMA,这是非常重要的步骤,__HAL_LINKDMA(htim, hdma[TIM_DMA_ID_CC1], DMA_Handle); /* DMA1关联TIM_UP事件 */。然后初始化DMA。
最后设置DMA的中断优先级和使能DMA中断。
具体代码如下图所示。
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(htim -> Instance == TIM1){
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_TIM1_CLK_ENABLE();
__HAL_RCC_DMA_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
HAL_SYSCFG_DMA_Req(0x0b); /* DMA1_MAP 选择TIM1_CH1 */
//
GPIO_InitStruct.Pin = GPIO_PIN_8; //配置pa8
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; //复用功能 推挽输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; //输出速度
GPIO_InitStruct.Alternate = GPIO_AF2_TIM1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// HAL_NVIC_SetPriority(TIM1_CC_IRQn, 1, 1);
// HAL_NVIC_EnableIRQ(TIM1_CC_IRQn);
//DMA配置
DMA_Handle.Instance = DMA1_Channel1;
DMA_Handle.Init.Direction = DMA_MEMORY_TO_PERIPH; //传输方向存储器到外设
DMA_Handle.Init.PeriphInc = DMA_PINC_DISABLE; //终点地址不递增
DMA_Handle.Init.MemInc = DMA_MINC_ENABLE; //起点地址递增
DMA_Handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; //外设数据宽度
DMA_Handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; //存储区数据宽度
DMA_Handle.Init.Mode = DMA_NORMAL; //工作模式 单次模式
DMA_Handle.Init.Priority = DMA_PRIORITY_MEDIUM; //优先级 中等
__HAL_LINKDMA(htim, hdma[TIM_DMA_ID_CC1], DMA_Handle); /* DMA1关联TIM_UP事件 */
HAL_DMA_Init(&DMA_Handle);
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
}
2.3 转化数据函数
这也是驱动WS2812最关键的部分,最重要的是要给定合适的预分配值,自动重装值和比较值,这样才能成功驱动WS2812。使用的时钟为24M,TIM1时钟不分频,所以TIM1时钟也是24M 。
对于发1的协议来说,给定arr为30,那么一个定时器周期为1/(24M/30)=1250ns。给定比较值为15。那么高低电平的时间各为625ns。满足发1的时序要求。
对于发0的协议来说,给定arr为30,那么一个定时器周期为1/(24M/30)=1250ns。给定比较值为8。那么高电平的时间为333.33ns,低电平的时间为916.66。满足发0的时序要求。
需要控制两个灯珠,每一个为24位,所以采用两个for循环依次取出对应的值,然后将其保存在数组中,最后通过DMA搬运到TIM,即可完成对WS2812的控制。由于WS2812的默认电平为低电平,所以最后要给有效值为0,使其一直输出低电平,以方便下次的控制。所以对于两个灯珠来说,数组大小为24+24+1=49。
最后通过HAL_TIM_PWM_Start_DMA开始传输。
具体实现代码如下。
void WS2818_Data(uint32_t led1, uint32_t led2)
{
uint8_t i;
for(i=0; i<24; i++)
{
if(0x00800000 & (led1<<i))
{
tim1_GRB[i] = 15;
}
else
{
tim1_GRB[i] = 8;
}
}
for(i=24; i<48; i++)
{
if(0x00800000 & (led2<< (i - 24)))
{
tim1_GRB[i] = 15;
}
else
{
tim1_GRB[i] = 8;
}
}
tim1_GRB[48] = 0;
HAL_TIM_PWM_Start_DMA(&TIM1_Handle, TIM_CHANNEL_1, (uint32_t *)tim1_GRB, 49);//DMA方式 打开通道1 49次传输
}
2.4 DMA中断函数
在DMA通道1中断处理函数中调用DMA中断处理函数,清除相关标志位,方便下次的传输。
程序中并没有对TIM的关闭操作,再传输完成后,有效值给0,IO口一直保持低电平,所以不必要关闭定时器,下次传输时直接开启DMA即可。
//DMA通道1中断函数
void DMA1_Channel1_IRQHandler(void)
{
HAL_DMA_IRQHandler(TIM1_Handle.hdma[TIM_DMA_ID_CC1]);
}
2.5 main.c操作
给定事先计算好的定时器参数初始化定时器。
TIM1_Init(30 - 1, 1 - 1, 0);
然后在while循环中循环调用转化数据函数。
while(1)
{
WS2818_Data(0x0000FF, 0x0000FF);
HAL_Delay(1000);
WS2818_Data(0x00FF00, 0x00FF00);
HAL_Delay(1000);
WS2818_Data(0xFF0000, 0xFF0000);
HAL_Delay(1000);
}
结语:该文件为自己没事瞎写,记录自己的一点点收获,有不对的地方欢迎各位大神指正,实在看不顺眼也请大哥轻点喷,谢谢谢谢大家!