STM32定时器驱动WS2812
STM32定时器驱动WS2812
最近在学STM32F103的定时器的标准库驱动,在学到定时器的比较输出功能时发现它可以和DMA配合一起使用产生一连串占空比各不同的PWM波,于是我立刻想到用这个东西来驱动WS2812,手边正好有一串30颗灯珠的WS2812灯带。
WS2812的通信协议
-
数据格式
WS2812是一种采用单线通信方式的全彩灯珠,它只需要一根线就可以与控制器进行通信。它内置R、G、B三种颜色的光源,每种颜色通过一个字节的数据进行控制,所以每颗灯珠都需要3个字节的数据来控制颜色。数据是通过它的DIN引脚输入,下图是它的数据传输格式:
可以看到,它的数据格式是高位在前。并且值得注意的是,虽然我们习惯叫它“RGB灯”,但它的通信格式其实是“GRB”,也就是“G”的数据在第一字节,后面才是“R”和“B”的数据,这对于我们写驱动非常重要。
-
级联方式
WS2812还支持级联,就是将前面一颗灯珠的DOUT接到后一颗的DIN引脚,这样就可以通过第一颗灯珠的DIN引脚控制这一整串灯条。
例如:你要控制的灯带上有3颗灯珠,那么你就要通过第一颗灯珠的DIN引脚发送9字节的数据(每个灯珠需要3字节的数据),当第1颗灯珠接收到这一连串的数据后它会保存首3个字节的数据用作控制它的颜色,然后将剩下6个字节的数据通过它的DOUT引脚转发出去,同样地,第2颗灯珠会将这6字节数据的前3字节保存用作控制自己的颜色,然后将剩下3字节的数据通过DOUT转发给下一颗灯珠...
事实上,当你通过第一颗灯珠的DIN引脚将9字节的数据全部发送出去之后,灯珠并不会立刻将颜色刷新到当前的显示上,因为它还需要一个RESET信号,这个RESET信号是通过将信号线置于低电平保持至少80us的时间表示的。也就是说当你把数据全部发送出去之后需要立刻将信号线拉低至少80us之后灯串才会“响应”你的这些数据。如果你是通过PWM进行控制的话就要格外注意这一点。例如如果你通过PWM将数据发送出去后发现灯的颜色并没有任何变化,可能并不是你的周期和占空比有问题,有可能仅仅是你的PWM波在数据发送结束后还在不停变化着,解决方法就是在数据发送完成后再将PWM的占空比调整为0。
-
编码方式
下图是它的数据编码方式,可以看到
bit0码
是通过高电平0.295us
+低电平0.595us
的方式进行表示,而bit1码
刚好相反,实际操作中为了便于计算,我们可以取0.3us和0.6us。最后就是RESET码
是通过至少80us的低电平表示的。
程序构建思路
-
PWM的参数
从上面的通信协议可以看出来无论是
bit0码
还是bit1码
,周期都在1us左右,区别仅仅是占空比的不同。于是就可以通过周期不变的PWM波来表示,PWM波的频率设置为1MHz,周期刚好是1us。当要表示bit0码
时可将占空比设置在30%左右,这样一个PWM波就是高电平0.3us
+低电平0.7us
,对应于数据bit0码
的范围之内;要表示bit1码
时可将占空比设置在60%左右,这样一个PWM波就是高电平0.6us
+低电平0.4us
,对应于数据bit1码
的范围之内。
-
缓冲区的设置
因为是采用DMA自动发送的方案,所以需要在程序中使用一个缓存区用来保存每个位对应的PWM占空比:
#define MAX_QUANTITY_GRAIN 30 //灯的最大数量 static uint8_t WSData_Buf[MAX_QUANTITY_GRAIN*24+1];
这里每个位的占空比用一个字节来表示。
MAX_QUANTITY_GRAIN
是一个宏,定义的是灯的最大数量,由于我使用的灯带有30颗灯珠,于是我这里定义的值就是30。所以这个缓存数组的长度就是30*24+1
=721字节的长度:30
对应于30颗灯珠,24
对应于一颗灯珠需要24位进行控制,那后面的+1
是什么呢?别忘记了,当你把一帧数据发送出去之后,后面还需要跟上一个RESET
信号,而这个信号是通过将信号线拉低至少80us来表示的,也就是说缓冲区“多余”的那最后一个字节是用来放RESET
信号的,对应就是0%的占空比。
定时器的配置
-
配置时基单元
我使用通用定时器TIM4的通道1配合DMA来输出PWM波。首先要做的就是配置定时器的频率。芯片的工作频率是72MHz,经过计算我选择将PSC预分频器的分频系数设置为(12-1),计数器的自动重装载寄存器的值设置为(6-1),经过这样的分频最终PWM的输出频率就是72MHz/12/6 = 1MHz,周期刚好是1us,精度值为1/6 us约为0.16us。
TIM_PrescalerConfig(TIM4, 12-1, TIM_PSCReloadMode_Immediate);//设置预分频器 TIM_CounterModeConfig(TIM4, TIM_CounterMode_Up);//计数模式 TIM_SetAutoreload(TIM4, 6-1);//自动重装载值 TIM_SetClockDivision(TIM4, TIM_CKD_DIV1);//时钟滤波器值
经过这样配置,当把捕获比较寄存器CCR1的值设置为4时,一个PWM波就是
高电平0.64us
+低电平0.36us
,正好对应于数据bit1
;当把捕获比较寄存器CCR1的值设置为2时,一个PWM波就是高电平0.32us
+低电平0.68us
,正好对应于数据bit0
。当然,我们采用的是DMA的方式,所以后面不需要自己去调用TIM_SetCompare1()
手动填写捕获比较寄存器CCR,而是通过DMA自动填写它的值。时基单元配置好后接下来就是要配置输出通道和DMA了。 -
配置输出通道
TIM_SetCompare1(TIM4, 0);//设置比较值 TIM_OC1PolarityConfig(TIM4, TIM_OCPolarity_High);//设置输出极性 TIM_SelectOCxM(TIM4, TIM_Channel_1, TIM_OCMode_PWM1);//设置输出通道模式 TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);//使能CCR寄存器的影子寄存器 TIM_CCxCmd(TIM4, TIM_Channel_1, TIM_CCx_Enable);//使能输出通道
一开始我并没有特地去调用函数
TIM_OC1PreloadConfig()
使能CCR寄存器的影子寄存器,但后面测试的时候发现一个很大的问题,当我往灯珠发送数据RGB(255,0,0)
时,灯珠能够显示红色,但发送数据RGB(128,0,0)
时灯珠却不显示任何颜色,于是我拿出示波器开始观察波形。这是在发送
RGB(255,0,0)
时从示波器观察到的数据,可以看到G数据的高7位数据无论是周期还是占空比都很正常,周期都是1us,高电平的时间都在0.3us左右。但看到后面就有点奇怪了,G数据的bit0和R数据的bit7似乎有些“纠缠不清”了,正常情况下G数据的bit0的PWM波周期应该和前面的一样都占满一整个格子的宽度(1us),但现在却是和R数据的bit7一起”挤”在同一个格子里面,这就让我有些诧异了。这也正和前面灯珠颜色显示异常对上了,当写入RGB(255,0,0)时对应发送的二进制数据是“00000000’11111111’00000000”(别忘记灯的实际通信格式是GRB),从上面示波器的数据可以看到R数据的bit7周期是异常的,这会让WS2812无法正常识别出bit7的数据,但后面低7位的数据的周期是正常的,会被正常识别出来,这就会导致原本R=255但WS2812识别成了R=127,于是尽管数据识别出错但好歹还是能让红色的灯亮起来,顶多亮度不对罢了。但如果写入的数据是RGB(128,0,0)时,情况就有些麻烦了。RGB(128,0,0)对应的二进制数据是“00000000’10000000’00000000”,可以看出来,同样是把bit7丢掉,对于前面的数据顶多是亮度不对,但对于这串数据而言将是致命的破坏,此时WS2812将得到一连串的“0”,于是将没有任何一种颜色的灯会被点亮。
从上面的分析我得到一个信息,那就是现在的PWM波在改变占空比的时候周期会短暂变短。一开始我怀疑是不是在写CCR寄存器时预分频寄存器的值也会被改变(毕竟提到周期有问题肯定第一时间想到预分频寄存器),但经过我的一系列排查最终打消了我这个想法。于是我又仔细捋了一下我所掌握的信息,最后怀疑是影子寄存器的问题,于是我立刻打开数据手册查看:
我发现CCR寄存器的影子寄存器默认是不使能的,这也就意味写入CCR寄存器的数据会被立刻刷新进去,而不是等待此周期结束后的更新事件到来时再刷新。于是我在使能输出通道的语句
TIM_CCxCmd(TIM4, TIM_Channel_1, TIM_CCx_Enable);
前面加了一句
TIM_OC1PreloadConfig(TIM4,TIM_OCPreload_Enable);
用于使能CCR寄存器的影子寄存器,最后重新编译下载后问题立刻得到解决。 -
配置DMA
配置DMA的代码比较常规,我就不贴出来了,有需要可以去看我上传的源代码。
最终效果
流水灯又愉快地转起来咯~
源代码结构
-
函数原型
/*初始化相关外设*/ void WS2812_Init(void);
/*设置此次要控制的灯数(不能大于宏MAX_QUANTITY_GRAIN所定义的数量)*/ void WS2812_SetQuantity(uint16_t quantity);
/*往第ln颗灯填充rgb数据(从0开始编号,不能大于quantity-1)*/ uint8_t WS2812_FullRGB(uint16_t ln, uint8_t r, uint8_t g, uint8_t b);
/*对第lnBegin到第lnEnd颗灯填充rgb数据(从0开始编号,lnBegin不能大于lnEnd,lnBegin和lnEnd皆不能大于quantity-1)*/ uint8_t WS2812_AreaFullRGB(uint16_t lnBegin, uint16_t lnEnd, uint8_t r, uint8_t g, uint8_t b);
/*往第ln颗灯填充hsv数据(从0开始编号,不能大于quantity-1)*/ uint8_t WS2812_FullHSV(uint16_t ln, double h, double s, double v);
/*对第lnBegin到第lnEnd颗灯填充hsv数据(从0开始编号,lnBegin不能大于lnEnd,lnBegin和lnEnd皆不能大于quantity-1)*/ uint8_t WS2812_AreaFullHSV(uint16_t lnBegin, uint16_t lnEnd, double h, double s, double v);
/*将填充完的数据刷新到灯带中去*/ uint8_t WS2812_Refresh(void);
-
使用方法
- 初始化时调用一次
WS2812_Init()
对外设进行配置,这个函数会自动将缓冲区所有数据设置为RGB(0,0,0)并刷新到灯带上。 - 准备发送一帧数据时先调用
WS2812_SetQuantity()
函数设置这一帧要控制的灯的数量; - 再根据需要调用
WS2812_FullRGB()
、WS2812_FullHSV()
、WS2812_AreaFullRGB()
或WS2812_AreaFullHSV()
对这一帧的缓存区进行数据填充; - 最后调用
WS2812_Refresh()
函数将这一帧数据发送到WS2812中。
- 初始化时调用一次
-
说明
- 上面有返回值的函数当返回值为非0时表示执行失败。
- WS2812.h文件中有一个宏“MAX_QUANTITY_GRAIN”是用来定义级联的WS2812数量的。
- 本库函数中是用TIM4的通道1配合DMA来实现控制的,如果你要换成别的通道或者是定时器的话也要记得根据下表把DMA相关的函数也重新更改一下。
源代码获取
WS2812库函数
RGB与HSV互相转换库函数
转载说明
- 本文为本人原创,转载时请注明出处及原作者:UENG。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?