STM32定时器驱动WS2812

STM32定时器驱动WS2812

最近在学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。

  • 编码方式

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

程序构建思路

  • PWM的参数

    从上面的通信协议可以看出来无论是bit0码还是bit1码,周期都在1us左右,区别仅仅是占空比的不同。于是就可以通过周期不变的PWM波来表示,PWM波的频率设置为1MHz,周期刚好是1us。当要表示bit0码时可将占空比设置在30%左右,这样一个PWM波就是高电平0.3us+低电平0.7us,对应于数据bit0码的范围之内;要表示bit1码时可将占空比设置在60%左右,这样一个PWM波就是高电平0.6us+低电平0.4us,对应于数据bit1码的范围之内。
    image

  • 缓冲区的设置

    因为是采用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)时灯珠却不显示任何颜色,于是我拿出示波器开始观察波形。

    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);
    用于使能CCR寄存器的影子寄存器,最后重新编译下载后问题立刻得到解决。

  • 配置DMA

    配置DMA的代码比较常规,我就不贴出来了,有需要可以去看我上传的源代码。

最终效果

image

流水灯又愉快地转起来咯~

源代码结构

  • 函数原型

    /*初始化相关外设*/
    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);
    
  • 使用方法

    1. 初始化时调用一次WS2812_Init()对外设进行配置,这个函数会自动将缓冲区所有数据设置为RGB(0,0,0)并刷新到灯带上。
    2. 准备发送一帧数据时先调用WS2812_SetQuantity()函数设置这一帧要控制的灯的数量;
    3. 再根据需要调用WS2812_FullRGB()WS2812_FullHSV()WS2812_AreaFullRGB()WS2812_AreaFullHSV()对这一帧的缓存区进行数据填充;
    4. 最后调用WS2812_Refresh()函数将这一帧数据发送到WS2812中。
  • 说明

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

源代码获取

WS2812库函数

RGB与HSV互相转换库函数

转载说明

  • 本文为本人原创,转载时请注明出处及原作者:UENG。
posted @   UENG  阅读(4027)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示