STC15F104W驱动WS2812
提示:学习目标:了解WS2812的驱动原理,写出驱动代码。
最近开了个专栏,专门用来学习ESP32S3的,当然,代码也适用于ESP32板子,改下引脚号就可以的。感兴趣的可以看看。
https://blog.csdn.net/weixin_38476200/article/details/129828874
内容:
提示:这里可以添加要学的内容
例如:
1、 连接WS2812时序;
2、 基于STC15F104W写出驱动程序。
3、 软件调试计算程序的机器周期。
4、 测试程序,并根据示波器得到的实际机器周期时间重新调整驱动程序
正文:
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_FREQUENCYif(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+2RGB_PIN_L();delay3NOP();//4+3+5(跳出这个else需要3个机器周期,进入下一个else则需要2个机器周期)}if(byte & 0x40){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}if(byte & 0x20){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}if(byte & 0x10){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}if(byte & 0x8){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}if(byte & 0x4){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}if(byte & 0x2){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}if(byte & 0x1){RGB_PIN_H();//delay7NOP();//4+7RGB_PIN_L();//4+8}else{RGB_PIN_H();delay2NOP();//4+2RGB_PIN_L();delay3NOP();//4+3+5}
#elseif(byte & 0x80){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x40){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x20){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x10){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x8){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x4){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x2){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_PIN_L();delay2NOP();//4+2+5}if(byte & 0x1){RGB_PIN_H();//delay5NOP();//4+5RGB_PIN_L();//4+8}else{RGB_PIN_H();delay1NOP();//4+1RGB_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;//4delay6NOP();//6P33=0;//4delay2NOP();//2+4
}
但是实际用示波器测试时,发现高电平时间和低电平时间均为3.8格*200ns/格=760ns。所以实际上每个机器周期的时间我这里是76ns左右。距离理论值83.3ns相差不多,而且ws2812每个高电平信号都可以有150ns左右的误差,因此这里按照理论计算和按照实际计算均可。我按理论值计算。
代码链接
上面的代码可以直接复制使用的。最近有点穷,设置5个积分,如果各位路过的大佬觉得对您有帮助,跪求各位大佬赏口饭吃。
https://download.csdn.net/download/weixin_38476200/16163812?spm=1001.2014.3001.5501