C51 MCU驱动WS2812(软件模拟)

1. 简述

以前尝试使用过STC15和STM32来驱动WS2812灯珠,在STM32上能比较好的驱动多个灯珠,但在STC15上确很难正常驱动,在网上看到许多人驱动WS2812时超级注重还原数据手册上所描述的波形参数,当时我也时尽力去还原手册上的波形,这个方法在STM32上是可行的,但在STC51 MCU上却失败了。

最近在网上找资料时,受到一篇文章的启发,用另一种思路来驱动WS2812,就重新拿起来玩玩,于是有了本文。那篇文章链接我已经找不到了,目前将我所用的方法跟大家分享一下,大家也可以测试下这个驱动方法是否能在你的MCU以及灯珠上使用。

2. WS2812驱动信号说明

2.1 手册上的信号说明

如图,这是网上最容易找到的WS2812信号资料说明,这里的说明对每个Bit都有严格的时序要求。

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)

但实际测试时,发现即使使用不符合这个表要求的时序也是可以正常驱动WS2812的,如下图就是一个能正常驱动WS2812信号中断其中一个bit,这个信号发送的是0,高电平宽度是180ns,并且他与下一个bit的低电平时长差不多接近2us了。

C51 MCU驱动WS2812(软件模拟)

请看下图,这是RGB每个字节之间的间断时间,可以发现时间已经超过5us了,但此时WS2812任然能够正确解析信号,信号已经完全超出手册定义的范围,为什么WS2812还能正常工作?

C51 MCU驱动WS2812(软件模拟)

2.2 实际信号工作方式推测

我们推测组成数据信号的关键不是PWM的占空比,而是PWM高电平脉冲的宽度。

这是另一份WS2812灯珠的数据手册,这个手册换了一种描述信号的方法,从图可以看出,只要高电平时间在220~380ns之间就认为是0码580ns~1us之间则认为是1码,这里我们猜测380ns580ns480ns,就是WS2812判断0 1码的标准。

换句话说,只要高电平时间<480ns,WS2812就会读到0码;高电平脉冲>480ns,2812就会读到一个1码,而低电平时长对01码的识别没有太大影响(当然低电平时间不能超过Reset码的时间),这样看来,我们只要把握好每个信号的高电平宽度即可,这个要求对于目前的1T内核的8051MCU还是可以做到的。

C51 MCU驱动WS2812(软件模拟)


3. 程序测试

3.1 测试环境介绍

MCU: CH552T@24MHz
灯珠:淘宝购买的WS2812灯条,上有8颗串联
使用MCU的P1.3引脚输出信号来驱动WS2812,这里将不再赘述如何使用CH552T,这是一颗可以直接使用USB烧录程序的8051内核芯片,请百度查找资料,下图的最小系统板已在立创EDA开源,有兴趣的可以去看看 – 【开源链接】。
注意:如果你的单片机主频比较低,或者指令周期太多是不行的,本次程序中关键是SETB和CLRB两条指令,我这边测试CH552T的这两条指令执行时间大致是80ns,低于480ns所以满足要求,我推测12MHz主频应该也是能跑的。

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)

3.2 编写最基本的代码,点亮第一个灯

这里我们抓住一个重点,严格控制高电平的脉宽就可以正确传输数据,在高电平期间,我们要保证其时间的准确,在低电平期间,我们可以判断即将传输的数据是0码还是1码,或者做一些其他耗时的运算调用函数等操作。

经过测试outPin = 1这行代码,会被编译成SETB指令,而outPin = 0则会被编译成CLRB指令,这两条指令的执行时间都是大致80ns,基于这个我们就可以生成准确高电平脉冲了(注意:过程中不能被中断)

WS2812_WriteByte(unsigned char dat)函数中,我们读取输入数据的每一个bit,并输出对应的高电平脉冲信号,低电平延迟则由数据移位和循环等操作产生,因此我没有给低电平期间增加额外的延迟。

接下来我们在main()函数中,第一复位总线,然后连续发送3个Byte的数据,这样就完成了写入一个灯的数据过程。

#include "ch554.h"
#include "Debug.h"
#include "GPIO.h"


sbit outPin = P1^3;

//功能:产生一个低电平复位信号给WS2812
void WS2812_Reset(void)
{
  int i = 200;
  while(i--)
    outPin = 0;     //这里具体时间没有测试,大于100us应该就可以,请自行调试
}

//功能:给WS2812发送一个字节数据
void WS2812_WriteByte(unsigned char dat)
{
  char i = 8;
  while(i--)
  {
    //产生脉冲时序部分,具体执行的代码需要根据您使用的单片机而定
    //我这里使用的CH552T@24MHz,执行一次SETB或CLRB指令大致耗时80ns,我按照这个特性,写出了下面的时序用于产生0码和1码
    //输出在低电平期间,则进行其他处理,如循环,数据移位等
    if(dat & 0x80)
    {
      //Write Bit 1
      outPin = 1;   //这里多次调用SETB指令来达到延迟的目的,产生一个大致800ns的高电平脉冲
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 0;
    }
    else
    {
      //Write Bit 0
      outPin = 1;   //这里产生一个大致180ns的高电平脉冲
      outPin = 1;
      outPin = 1;
      outPin = 0;
    }
    dat <<= 1;
  }
}

void main(void)
{
  CfgFsys();            //这是官方提供库"Debug.C"中的函数,用于初始化时钟信号,请将"Debug.h"中的"FREQ_SYS = 12000000"改为"FREQ_SYS = 24000000", 使用24M主频
  Port1Cfg(1, 3);       //设置P1.3口为推挽输出

  WS2812_Reset();
  WS2812_WriteByte(0xff);   //Green
  WS2812_WriteByte(0xff);   //Red
  WS2812_WriteByte(0xff);   //Blue
  while(1);
  return;
}

下载程序,复位运行,您就可以看到第一颗WS2812被点亮了,发出耀眼的白光。

C51 MCU驱动WS2812(软件模拟)

3.3 接下来,点亮8个灯

我们改写main()中的代码,让3个颜色的数据重复传输8次,您可以看到,所有灯都被点亮了。

void main(void)
{
  char i;
  CfgFsys();            //这是官方提供库"Debug.C"中的函数,用于初始化时钟信号,请将"Debug.h"中的"FREQ_SYS = 12000000"改为"FREQ_SYS = 24000000", 使用24M主频
  Port1Cfg(1, 3);       //设置P1.3口为推挽输出
  
  WS2812_Reset();
  for(i=0; i<8; i++)
  {
    WS2812_WriteByte(0xff);   //Green
    WS2812_WriteByte(0xff);   //Red
    WS2812_WriteByte(0xff);   //Blue
  }
  while(1);
  return;
}

C51 MCU驱动WS2812(软件模拟)

3.4 接下来,按数组中的定义的颜色点亮灯

我们增加一个叫WS2812_Color_t的结构体,用于表明一盏灯的颜色,方便存储数据,内部有三个变量Red Green Blue,设置数值0则该颜色为最低亮度,255为最高亮度,128则是中等亮度。
然后编写函数WS2812_WriteColors(WS2812_Color_t *pColor, int num),将每颗灯的颜色数据一次性发送出去,下载程序复位后,您可以看到每个灯显示的颜色都不一样,您可以自行修testColorTable[8]数组中的数值,以改变每颗灯珠的颜色以及亮度。

#include "ch554.h"
#include "Debug.h"
#include "GPIO.h"

sbit outPin = P1^3;

//定义一个结构体,用于表明一个灯的颜色数据
typedef struct{
  unsigned char red;
  unsigned char green;
  unsigned char blue;
}WS2812_Color_t;

//功能:产生一个低电平复位信号给WS2812
void WS2812_Reset(void)
{
  int i = 200;
  while(i--)
    outPin = 0;     //这里具体时间没有测试,大于100us应该就可以,请自行调试
}

//功能:给WS2812发送一个字节数据
void WS2812_WriteByte(unsigned char dat)
{
  char i = 8;
  while(i--)
  {
    //产生脉冲时序部分,具体执行的代码需要根据您使用的单片机而定
    //我这里使用的CH552T@24MHz,执行一次SETB或CLRB指令大致耗时80ns,我按照这个特性,写出了下面的时序用于产生0码和1码
    //输出在低电平期间,则进行其他处理,如循环,数据移位等
    if(dat & 0x80)
    {
      //Write Bit 1
      outPin = 1;   //这里多次调用SETB指令来达到延迟的目的,产生一个大致800ns的高电平脉冲
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 0;
    }
    else
    {
      //Write Bit 0
      outPin = 1;   //这里产生一个大致180ns的高电平脉冲
      outPin = 1;
      outPin = 1;
      outPin = 0;
    }
    dat <<= 1;
  }
}

//功能:按照颜色数据,传输一个灯的颜色数据
//参数:*pColor    请输入存放颜色数据的地址(灯颜色数据地址)
void WS2812_WriteColor(WS2812_Color_t *pColor)
{
  WS2812_WriteByte(pColor[0].green);
  WS2812_WriteByte(pColor[0].red);
  WS2812_WriteByte(pColor[0].blue);
}

//功能:按照颜色数据,一次性传输多个灯的颜色数据
//参数:*pColor    请输入存放颜色数据的地址(灯颜色数据地址)
//      num        颜色数据长度,例如要点亮1个灯,输入1
void WS2812_WriteColors(WS2812_Color_t *pColor, int num)
{
  while(num--)
    WS2812_WriteColor(pColor++);
}

void main(void)
{
  //定义每个灯的颜色值,存储在Flash中
  WS2812_Color_t code testColorTable[8] = {
    {255,   0,   0},
    {  0, 255,   0},
    {  0,   0, 255},
    {255, 255,   0},
    {  0, 255, 255},
    {255,   0, 255},
    {128, 255, 255},
    {255, 128, 255},
  };
  
  CfgFsys();            //这是官方提供库"Debug.C"中的函数,用于初始化时钟信号,请将"Debug.h"中的"FREQ_SYS = 12000000"改为"FREQ_SYS = 24000000", 使用24M主频
  Port1Cfg(1, 3);       //设置P1.3口为推挽输出
  
  WS2812_Reset();
  WS2812_WriteColors(testColorTable, 8);    //写出数据,点亮8个灯
  
  while(1);
  return;
}

C51 MCU驱动WS2812(软件模拟)

3.5 最后,我们来做渐变流彩灯效果

在这个版本里,增加了一个函数用于生成连续的色彩变化,当8个灯的颜色相距一段距离,然后依次点亮,行程了流彩灯的效果。

#include "ch554.h"
#include "Debug.h"
#include "GPIO.h"

sbit outPin = P1^3;

//定义一个结构体,用于表明一个灯的颜色数据
typedef struct{
  unsigned char red;
  unsigned char green;
  unsigned char blue;
}WS2812_Color_t;

//功能:产生一个低电平复位信号给WS2812
void WS2812_Reset(void)
{
  int i = 200;
  while(i--)
    outPin = 0;     //这里具体时间没有测试,大于100us应该就可以,请自行调试
}

//功能:给WS2812发送一个字节数据
void WS2812_WriteByte(unsigned char dat)
{
  char i = 8;
  while(i--)
  {
    //产生脉冲时序部分,具体执行的代码需要根据您使用的单片机而定
    //我这里使用的CH552T@24MHz,执行一次SETB或CLRB指令大致耗时80ns,我按照这个特性,写出了下面的时序用于产生0码和1码
    //输出在低电平期间,则进行其他处理,如循环,数据移位等
    if(dat & 0x80)
    {
      //Write Bit 1
      outPin = 1;   //这里多次调用SETB指令来达到延迟的目的,产生一个大致800ns的高电平脉冲
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 1;
      outPin = 0;
    }
    else
    {
      //Write Bit 0
      outPin = 1;   //这里产生一个大致180ns的高电平脉冲
      outPin = 1;
      outPin = 1;
      outPin = 0;
    }
    dat <<= 1;
  }
}

//功能:按照颜色数据,传输一个灯的颜色数据
void WS2812_WriteColor(WS2812_Color_t *pColor)
{
  WS2812_WriteByte(pColor[0].green);
  WS2812_WriteByte(pColor[0].red);
  WS2812_WriteByte(pColor[0].blue);
}

//功能:按照颜色数据,一次性传输多个灯的颜色数据
//参数:*pColor    请输入存放颜色数据的地址(灯颜色数据地址)
//      num        颜色数据长度,例如要点亮1个灯,输入1
void WS2812_WriteColors(WS2812_Color_t *pColor, int num)
{
  while(num--)
    WS2812_WriteColor(pColor++);
}

//根据输入index, 取一个颜色,
//参数:*pColor    请输入存放颜色数据的地址(灯颜色数据地址)
//      cnt        颜色索引,输入不同值会得到不同的颜色输出,输入范围:0~767
void getColors(WS2812_Color_t *pColor, unsigned int index)
{
  //这段代码实则是根据输入数值生成颜色,2个颜色之间此消彼长地变化
  if(index <= 255)
  {
    pColor[0].red = index;
    pColor[0].green = 0;
    pColor[0].blue = 255-index;
  }
  else if(index <= 511)
  {
    pColor[0].red = 511 - index;
    pColor[0].green = index - 256;
    pColor[0].blue = 0;
  }
  else if(index <= 767)
  {
    pColor[0].red = 0;
    pColor[0].green = 767 - index;
    pColor[0].blue = index - 512;
  }
  else
  {
    //不处理
  }
}

//根据输入数值更新8个灯的颜色数据
//参数:*pColor    请输入存放颜色数据的地址(灯颜色数据地址)
//      cnt        颜色索引,输入不同值会得到不同的颜色输出,输入范围:0~767
void ColorsUpdata_8Led(WS2812_Color_t *pColor, unsigned int inIndex)
{
  char i;
  if(inIndex > 767)
    inIndex = 767;
  
  for(i=0; i<8; i++)
  {
    int index;
    
    //这段代码中的数字(60),决定了每个灯之间的颜色差异(颜色渐变的缓和度,可自行测试, 如果设置为0,则失去流水灯效果)
    index  = inIndex + (60) * i;
    if(index >= 768)
      index -= 768;
    
    getColors(pColor + i, index);
  }
}

void main(void)
{
  WS2812_Color_t xdata ColorTable[8]; //用于存储8个灯的颜色数据
  CfgFsys();            //这是官方提供库"Debug.C"中的函数,用于初始化时钟信号,请将"Debug.h"中的"FREQ_SYS = 12000000"改为"FREQ_SYS = 24000000", 使用24M主频
  Port1Cfg(1, 3);       //设置P1.3口为推挽输出
    
  while(1)
  {
    static unsigned int colorIndex = 0;       

    ColorsUpdata_8Led(ColorTable, colorIndex);    //根据Index,获取当前的颜色数据
    WS2812_Reset();
    WS2812_WriteColors(ColorTable, 8);            //写出8个灯的数据
    
    //更新颜色Index
    colorIndex++;
    if(colorIndex >= 768)
      colorIndex = 0;

    mDelaymS(5);  //延迟5ms,更改这个数可以加快(减小数值)或减慢(加大数值)颜色变化的速度
  }
  
  return;
}

C51 MCU驱动WS2812(软件模拟)

3.6 实测波形数据

下面示波器所测的波形都是采集自上述测试环境,运行的是渐变流彩灯程序

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)

C51 MCU驱动WS2812(软件模拟)


4. 总结

4.1 注意事项

在本程序里,WS2812相关函数发送数据中不可被打断,否则可能导致数据传输失败,所以如果您有使用中断,请在发送数据期间暂时关闭中断。

  ...
  WS2812_Reset();
  EA = 0;
  WS2812_WriteColors(ColorTable, 8);            //写出8个灯的数据
  EA = 1;
  ...

4.2 真总结

使用上述方法,成功地在CH552T平台驱动了WS2812灯珠,程序工作稳定表现良好,并且驱动灯珠的数量可以继续扩充,上限就看您的程序结构和存储空间大小。

将驱动的要点总结一下:

  • 低电平大于100us,总线复位(不同灯珠这个时间可能不同)
  • 高电平脉冲时间大于480ns为1码,实际发送750ns
  • 高电平脉冲时间小于480ns为0码,实际发送250ns
  • 低电平时间可以延长,不会导致通讯失败,但不超过总线复位时间(这个是猜测没有验证,但拖长几个微秒是已确定没有问题的)

抓住以上几个要点,就可以轻松驱动WS2812灯珠啦。

这里也分享一下我测试用的程序工程:百度云 (提取码: s66y)

© 版权声明

相关文章

暂无评论

none
暂无评论...