找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 5988|回复: 0
收起左侧

STC15F104W单片机驱动WS2812程序

[复制链接]
ID:1015912 发表于 2022-5-15 19:49 | 显示全部楼层 |阅读模式
1,WS2812时序
WS2812是一个集控制电路与发光电路于一体的智能外控LED光源,每个ws2812均含有4个引脚,引脚功能如下图:

WS2812可采用级联的方式,将上一个WS2812的DOUT引脚连接到下一个WS2812的DIN引脚,即可实现一个引脚控制多个WS2812。一般自己设计电路时还要在电源输入处添加一个0.1uF的小电容进行滤波。注意,虽然理论上可以连接足够多的ws2812,但是使用时要注意驱动电压和驱动电流是否足够驱动这些彩灯。

由于只有一个引脚控制ws2812,所以我们只能通过控制引脚输出高、低电平的时间,来让WS2812知道我们想让他显示哪一个灯,显示什么颜色。
想要让1个ws2812显示我们想要的颜色,我们需要给它发送颜色数据。每一个ws2812的颜色数据都是24位:8位绿色+8位红色+8位蓝色。

如果我们想控制1个ws2812,我们发送24位颜色数据;如果我们想控制2个ws2812,我们需要连续发送48位颜色数据;如果我们想控制n个ws2812,我们需要连续发送n*24位颜色数据。

那么ws2812如何知道我们发送的每一个位的数据是“1”还是“0”呢?
WS2812手册中对于数据有明确的时间定义:

可以看出,当我们想要发送一个位的数据时,如果这个位是“1”,我们就控制单片机的引脚输出高电平0.85us,然后控制引脚输出低电平0.40us;如果这个位是“0”,则控制引脚输出高电平0.40us,然后控制引脚输出低电平0.85us。

而如果我们想控制1个灯显示颜色,我们需要发送24个位的颜色数据给ws2812。如果我们想控制n个灯显示颜色,我们从第1个灯的颜色数据开始发送,直至发送完n*24个数据。最后我们需要控制单片机引脚输出低电平超过50us,让彩灯显示颜色。

2,写出驱动程序
注意,STC15F104W只有8个引脚,我们一般采用内部晶振电路。下面的代码都是基于12MHz的晶振频率写的。WS2812对于单片机的引脚时序要求比较高。我们一般调用intrins.h这个头文件的机器周期函数,nop()函数执行一次占用一个机器周期(此处时钟周期和机器周期相等),所以每条机器周期时间为1/12MHz=83.3us。(代码中很多处使用宏定义,宏定义在预编译阶段程序就完成代码替代工作,不会影响程序的执行时间,且修改方便)
下面是h文件代码:

#ifndef __WS2812_H
#define __WS2812_H

//头文件区
#include <STC15F2K60S2.H>
#include <intrins.h>

//用户修改参数区
//#define WS2812_FREQUENCY
#define RGB_PIN           P33                       //控制彩灯引脚(需要配置为强推挽输出)
#define WS2812_MAX        25                        //彩灯最大个数
#define WS2812_NUMBERS    8                         //彩灯个数


#define RED               0xff0000                  //红色
#define GREEN             0x00ff00                  //绿色
#define BLUE              0x0000ff                  //蓝色
#define BLACK             0x000000                  //熄灭
#define WHITE             0xffffff                  //白色

#define RGB_PIN_H()  RGB_PIN = 1
#define RGB_PIN_L()  RGB_PIN = 0
#define delay1NOP()  _nop_();
#define delay1NOP()  _nop_();
#define delay2NOP()  delay1NOP(); _nop_();
#define delay3NOP()  delay2NOP();_nop_();
#define delay5NOP()  delay3NOP();delay2NOP();
#define delay7NOP()  delay5NOP();delay2NOP();

void Ws2812b_WriteByte(unsigned char byte);//发送一个字节数据(@12.000MHz,理论每个机器周期83ns,测试约为76ns)                                                      
void setLedCount(unsigned char count);//设置彩灯数目,范围0-25.                                                           
unsigned char getLedCount();//彩灯数目查询函数                                                                     
void rgb_SetColor(unsigned char LedId, unsigned long color);//设置彩灯颜色                                    
void rgb_SetRGB(unsigned char LedId, unsigned long red, unsigned long green, unsigned long blue);//设置彩灯颜色
void rgb_SendArray();//发送彩灯数据                                                                           
#endif                                                                     

接着我们对c代码进行详解。
首先调用h文件,然后定义一个数组,用来存放彩灯的颜色数据。这里数据存放在data空间(空间有限,只能存放25个灯左右的数据),后面有需要再优化到xdata空间。ledsCount 记录实际我们想要控制的彩灯的数目,nbLedsBytes 用来记录彩灯的数据个数(一个灯需要3个字节)。

#include "ws2812.h"

unsigned char LedsArray[WS2812_MAX * 3];      //定义颜色数据存储数组
unsigned int ledsCount = WS2812_NUMBERS;      //定义实际彩灯默认个数
unsigned int nbLedsBytes = WS2812_NUMBERS*3;  //定义实际彩灯颜色数据个数

接着我们开始编写彩灯的设置数目函数,查询数目函数,以及两个设置指定彩灯颜色的函数。

//设置彩灯数目,范围0-25.
void setLedCount(unsigned char count)
{
    ledsCount = WS2812_MAX > count ? count : WS2812_MAX;
    nbLedsBytes = ledsCount*3;
}

//彩灯数目查询函数
unsigned char getLedCount()
{
    return ledsCount;
}

//设置彩灯颜色(在这里我将绿和红色进行颠倒,这样比较符合我们日常生活的红绿蓝的顺序)
void rgb_SetColor(unsigned char LedId, unsigned long color)
{
    if( LedId > ledsCount )
    {
        return;    //to avoid overflow
    }
    LedsArray[LedId * 3]     = (color>>8)&0xff;
    LedsArray[LedId * 3 + 1] = (color>>16)&0xff;
    LedsArray[LedId * 3 + 2] = (color>>0)&0xff;
}

//设置彩灯颜色
void rgb_SetRGB(unsigned char LedId, unsigned long red, unsigned long green, unsigned long blue)
{
    unsigned long Color=red<<16|green<<8|blue;
    rgb_SetColor(LedId,Color);
}


然后我们对彩灯数据通过引脚发送出去。注意,发送彩灯数据的过程中,请将中断关闭,否则有可能会导致数据发送到一半被中断打断,导致显示异常。Ws2812b_WriteByte()函数是这个驱动代码的核心。

//发送彩灯数据
void rgb_SendArray()
{
    unsigned int i;
    bit a=EA;
    //发送数据
    EA=0;
    for(i=0; i<nbLedsBytes; i++)
        Ws2812b_WriteByte(LedsArray[ i]);
    EA=a;
}

下面的Ws2812b_WriteByte函数是我基于stc15f104w调试的。我写过不同单片机的驱动。不同单片机的每条指令花费的时间都不一样。如果您想要采用其他系列的单片机,我们只需要修改Ws2812b_WriteByte()函数里面的内容即可。其他的函数可以不做修改。方便代码的移植。
主要修改的地方是高电平总的拉高时间。这里之所以不对“1”和“0”的内容进行函数封装,是为了我自己调试的方便。如果您觉得代码不够简短好看,您可以将判断条件里面的内容用宏定义进行一下封装。

/*
//使用12.000MHz频率,理论每个机器周期83ns,实际测试约为76ns。
//下面都是基于76ns纳秒一个机器周期计算的。
//测试时发现可以支持12-20Mhz频率。
//如果想要使用11.0592Mhz频率,在h文件取消“#define WS2812_FREQUENCY”的注释
//在实际测试中发现,ws2812对高电平时间较为敏感,对低电平时间不敏感
//也就是说,低电平时间可以稍微长一些也不会影响程序,但是高电平时间需要控制的比较准确才行。
*/
void Ws2812b_WriteByte(unsigned char byte)
{
#ifndef WS2812_FREQUENCY
    if(byte & 0x80)
    {
        RGB_PIN_H();//4个机器周期(STC15F104W的拉高拉低都需要4个机器周期,其他系列暂时不知道,但是很大可能不是4个机器周期)
        delay7NOP();//7个机器周期
        RGB_PIN_L();//4+8(跳出这个if判断需要5个机器周期,再进入下一个if判断需要3个机器周期)
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5(跳出这个else需要3个机器周期,进入下一个else则需要2个机器周期)
    }
    if(byte & 0x40)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
    if(byte & 0x20)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
    if(byte & 0x10)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
    if(byte & 0x8)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
    if(byte & 0x4)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
    if(byte & 0x2)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
    if(byte & 0x1)
    {
        RGB_PIN_H();//
        delay7NOP();//4+7
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay2NOP();//4+2
        RGB_PIN_L();
        delay3NOP();//4+3+5
    }
#else
    if(byte & 0x80)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x40)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x20)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x10)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x8)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x4)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x2)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
    if(byte & 0x1)
    {
        RGB_PIN_H();//
        delay5NOP();//4+5
        RGB_PIN_L();//4+8
    }
    else
    {
        RGB_PIN_H();
        delay1NOP();//4+1
        RGB_PIN_L();
        delay2NOP();//4+2+5
    }
#endif
}


main文件里面,我是用stc生成了一个300ms的延时函数。让程序每隔300ms更换一下颜色并输出。红绿蓝三色交替闪烁。

#include <STC15F2K60S2.H>
#include "ws2812.h"
void Delay300ms();//@12.000MHz
void main()
{
    int i=0;
    setLedCount(12);//可以设置0-25之间的任何数目,根据实际情况而定
    rgb_SetRGB(0,0,0,0);//设置第一个灯关闭(这里作为使用示例,下面的rgb_SetColor覆盖了这个函数的作用)
    while(1)
    {
        for(i=0; i<getLedCount(); i++)//设置所有的灯为红色
        {
            rgb_SetColor(i,RED);
        }
        rgb_SendArray();//发送给ws2812,显示颜色
        Delay300ms();
        for(i=0; i<getLedCount(); i++)//设置所有的灯为绿色
        {
            rgb_SetColor(i,GREEN);
        }
        rgb_SendArray();//发送给ws2812,显示颜色
        Delay300ms();
        for(i=0; i<getLedCount(); i++)//设置所有的灯为蓝色
        {
            rgb_SetColor(i,BLUE);
        }
        rgb_SendArray();//发送给ws2812,显示颜色
        Delay300ms();
    }
}
void Delay300ms()//@12.000MHz
{
    unsigned char i, j, k;
    _nop_();
    _nop_();
    i = 14;
    j = 174;
    k = 224;
    do
    {
        do
        {
            while (--k);
        } while (--j);
    } while (--i);
}

3,软件调试
确保您的芯片型号选择正确。

填写软件调试的晶振频率,我这里使用16MHz(只是用来软件计算机器周期,和实际晶振无关)。1/16MHz=62.5ns。如果采用12MHz,我计算机器周期不太方便。

点击调试。没有配置时默认是软件调试模式。
点击引脚信号的逻辑分析。

在新弹出的窗口中点击Setup。然后输入您的控制引脚。

使用调试控件,一边观察波形图。

在程序执行RGB_PIN_H();这一句之前,我用红色的线定位了此时的时间位置,执行这一句后,我用蓝色的线查看它们之间的差值,差值为0.25us。这里我的软件仿真使用的是16MHz晶振,每个机器周期为62.5ns。0.25us相当于4个机器周期。正如我上面的发送函数里面的机器周期备注一样。同理可以计算出每行代码需要花费的机器周期时间,从而准确写出驱动。

比如,我们需要0.85us的高电平时间,也就是850ns,然后实际上我们使用的晶振是12MHz。每个机器周期的时间是理论是1/12Mhz=83.3ns。那么我们就需要850/83.3=10.2个机器周期。这里取11个机器周期。根据前面我们知道电平拉高需要用去4个机器周期,因此我们还需要让高电平维持7个机器周期的时间,然后将电平拉低。以此原理,写出我们的Ws2812b_WriteByte()函数的驱动。

4,硬件调试
实际硬件调试时,我写了一个测试程序。我用软件调试时,波形为引脚电平拉高10个机器周期,然后拉低10个机器周期。循环动作。

while(1)
{
        P33=1;//4
        delay6NOP();//6
        P33=0;//4
        delay2NOP();//2+4
}
但是实际用示波器测试时,发现高电平时间和低电平时间均为3.8格*200ns/格=760ns。所以实际上每个机器周期的时间我这里是76ns左右。距离理论值83.3ns相差不多,而且ws2812每个高电平信号都可以有150ns左右的误差,因此这里按照理论计算和按照实际计算均可。我按理论值计算。
————————————————
版权声明:本文为CSDN博主「DIY爱好玩家」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_38476200/article/details/115519393

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表