STM32定时器驱动WS2812

发布时间 2023-12-30 20:15:18作者: UENG

最近在学STM32F103的定时器的标准库驱动,在学到定时器的比较输出功能时发现它可以和DMA配合一起使用产生一连串占空比各不同的PWM波,于是我立刻想到用这个东西来驱动WS2812,手边正好有一串30颗灯珠的WS2812灯带。

WS2812的通信协议

  • 数据格式

WS2812是一种采用单线通信方式的全彩灯珠,它只需要一根线就可以与控制器进行通信。它内置R、G、B三种颜色的光源,每种颜色通过一个字节的数据进行控制,所以每颗灯珠都需要3个字节的数据来控制颜色。数据是通过它的DIN引脚输入,下图是它的数据传输格式:

image

可以看到,它的数据格式是高位在前。并且值得注意的是,虽然我们习惯叫它“RGB灯”,但它的通信格式其实是“GRB”,也就是“G”的数据在第一字节,后面才是“R”和“B”的数据,这对于我们写驱动非常重要。

  • 级联方式

WS2812还支持级联,就是将前面一颗灯珠的DOUT接到后一颗的DIN引脚,这样就可以通过第一颗灯珠的DIN引脚控制这一整串灯条。

image

例如:你要控制的灯带上有3颗灯珠,那么你就要通过第一颗灯珠的DIN引脚发送9字节的数据(每个灯珠需要3字节的数据),当第1颗灯珠接收到这一连串的数据后它会保存首3个字节的数据用作控制它的颜色,然后将剩下6个字节的数据通过它的DOUT引脚转发出去,同样地,第2颗灯珠会将这6字节数据的前3字节保存用作控制自己的颜色,然后将剩下3字节的数据通过DOUT转发给下一颗灯珠...

事实上,当你通过第一颗灯珠的DIN引脚将9字节的数据全部发送出去之后,灯珠并不会立刻将颜色刷新到当前的显示上,因为它还需要一个RESET信号,这个RESET信号是通过将信号线置于低电平保持至少80us的时间表示的。也就是说当你把数据全部发送出去之后需要立刻将信号线拉低至少80us之后灯串才会“响应”你的这些数据。如果你是通过PWM进行控制的话就要格外注意这一点。例如如果你通过PWM将数据发送出去后发现灯的颜色并没有任何变化,可能并不是你的周期和占空比有问题,有可能仅仅是你的PWM波在数据发送结束后还在不停变化着,解决方法就是在数据发送完成后再将PWM的占空比调整为0。

  • 编码方式

下图是它的数据编码方式,可以看到bit1码是通过高电平0.195us+低电平0.595us的方式进行表示,而bit0码刚好相反,实际操作中为了便于计算,我们可以取0.3us和0.6us。最后就是RESET码是通过至少80us的低电平表示的。
image

定时器的配置

  • 配置时基单元

我使用通用定时器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);//时钟滤波器值

经过这样设置,当要输出bit1时可以设置捕获比较寄存器CCR1的值为4,这样一个PWM波就是高电平0.64us+低电平0.36us;而要输出bit0时可以设置捕获比较寄存器CCR1的值为2,这样一个PWM波就是高电平0.32us+低电平0.68us。当然,我们采用的是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)时灯珠却不显示任何颜色,于是我拿出示波器开始观察波形。

image

这是在发送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寄存器时预分频寄存器的值也会被改变(毕竟提到周期有问题肯定第一时间想到预分频寄存器嘛),但经过我的一系列排查最终打消了我这个想法。于是我又仔细捋了一下我所掌握的信息后怀疑是影子寄存器的问题,于是我立刻打开数据手册查看:
image
image

我发现CCR寄存器的影子寄存器默认是不使能的,这也就意味写入CCR寄存器的数据会被立刻刷新进去,而不是等待此周期结束后的更新事件到来时再刷新。于是我在使能输出通道的语句
TIM_CCxCmd(TIM4, TIM_Channel_1, TIM_CCx_Enable);
前面加了一句
TIM_OC1PreloadConfig(TIM4,TIM_OCPreload_Enable);
重新编译下载后问题立刻得到解决。

  • 配置DMA

写配置DMA的代码的过程没有这么坎坷,我就不贴出来了,有需要可以去看我上传的源代码。

源代码结构

  • 函数原型

void WS2812_Init(void); //初始化相关外设
void WS2812_SetQuantity(uint16_t quantity); //设置控制的灯数
uint8_t WS2812_FullRGB(uint16_t ln, uint8_t r, uint8_t g, uint8_t b); //往第ln颗灯填充rgb数据
uint8_t WS2812_AreaFullRGB(uint16_t lnBegin, uint16_t lnEnd, uint8_t r, uint8_t g, uint8_t b); //对第lnBegin到第lnEnd颗灯填充rgb数据
uint8_t WS2812_FullHSV(uint16_t ln, double h, double s, double v); //往第ln颗灯填充hsv数据
uint8_t WS2812_AreaFullHSV(uint16_t lnBegin, uint16_t lnEnd, double h, double s, double v); //对第lnBegin到第lnEnd颗灯填充hsv数据
uint8_t WS2812_Refresh(void); //将填充完的数据刷新到灯带中去

  • 使用方法

  1. 先调用WS2812_SetQuantity()函数设置要控制的灯的数量。
  2. 再根据需要调用WS2812_FullRGB()WS2812_FullHSV()WS2812_AreaFullRGB()WS2812_AreaFullHSV()对缓存区进行数据填充。
  3. 最后调用WS2812_Refresh()函数将数据发送到WS2812中。
  • 说明

  1. WS2812.h文件中有一个宏“MAX_QUANTITY_GRAIN”是用来定义级联的WS2812数量的。
  2. 本库函数中是用TIM4的通道1配合DMA来实现控制的,如果你要换成别的通道或者是定时器的话也要记得根据下表把DMA相关的函数也重新更改一下。
    image

源代码获取

WS2812库函数

https://files.cnblogs.com/files/blogs/814108/ws2812.zip?t=1703925215&download=true

RGB与HSV互相转换库函数

https://files.cnblogs.com/files/blogs/814108/RGBHSV.zip?t=1703925215&download=true