基于51单片机和WS2812B彩色灯带的流水灯
目录
- 系列文章目录
- 前言
- 一、效果展示
- 二、原理分析
- 三、各模块代码
- 四、主函数
- 总结
系列文章目录
前言
用彩色灯带按自己想法DIY一条流水灯,谁不喜欢呢?
所用单片机:STC15W204S
(也可以用其他1T单片机,例如,STC8G系列、STC32G系列)
用到的外设:WS2812B彩色灯带(60个灯珠)
效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。
一、效果展示
二、原理分析
1、用什么MCU
如果只是驱动一条灯带,并且灯珠的数量不多(少于或等于77个)的话,用STC15W204S是比较合适的。因为只需用到一个引脚,用引脚多的单片机会比较浪费,STC15W204S总共有8个引脚,除了供电的VCC和GND,就只有P30~P33和P54~P55共6个IO口引脚。并且STC15W204S是1T单片机,适合驱动灯带。
我买的板子PCB板上印错了,P54、P55印成了P34、P35。
2、显示缓存
每个灯珠需要写入24Bit(3个字节)控制显示的颜色,共有Quantity个灯珠的话,要用3*Quantity个字节作为WS2812B彩色灯带的显示缓存。
必须要用到显示缓存,不能只对其中一个灯珠的WS2812B芯片写入数据,因为灯珠内的WS2812B芯片接收的数据是靠上一个灯珠内的WS2812B芯片传递过来的。
更显灯带显示的时候要通过数据线写入3*Quantity个字节。
3、WS2812B芯片
WS2812B芯片要求我们要按G(绿)、R(红)、B(蓝)的顺序发送数据,并且每个字节要高位先发。具体的时序要求可以看一下其他博主的介绍。我们可以用空操作 nop(); 来延时,分频系数为1,频率为24.000MHz的话,一个 nop(); 执行的时长就是1/24us。
为了方便向灯珠中的WS2812B芯片写入数据,缓存数组中的数据每3个字节为一组,每组的3个字节按G、R、B的顺序存放,这样的话,需要更新显示时,就按照索引的顺序,将3*Quantity个字节发送给灯带即可。修改缓存的时候每一组也是需要按照G、R、B的顺序赋值。
第一个灯珠接收了3个字节的数据后,会将后面的数据直接传给第二个灯珠,以此类推。
4、不同灯珠数量的灯带
已经考虑到了这种情况,所以代码中作了相应的处理,修改宏定义中Quantity对应的数值即可,经过测试,如果用STC15W204S的话,1~77个灯都是可以驱动的。
5、亮度的调整
G、R、B的值越大,就表示该种颜色越亮,所以想要调整亮度的话,修改G、R、B的值就行了。
6、函数的设计
可以根据自己的需求设计对应的函数。我觉得以下函数是必须的,其他的可以自由发挥。
①写一个字节的函数
②显示缓存数据清零的函数
③更改一个灯珠对应的三个字节的缓存的函数
④更新显示的函数(将缓存数组的数据全部发送给灯带)
三、各模块代码
无
四、主函数
main.c
/*
by甘腾胜@20250131
效果查看/操作演示:可以在B站搜索“甘腾胜”或“gantengsheng”查看
开发环境:Keil C51
单片机:STC15W204S
分频系数及频率:1T@24.000MHz
外设:WS2812B彩色灯带(60个灯珠)
注意:
(1)驱动WS2812B彩色点阵屏最好用1T的单片机,想用传统的12T单片机,则需要用较高频率的晶振,并且要使能6T(双倍速)模式
(2)如果使用STC15W204S这款单片机,并且灯珠数量小于等于77的话,根据实际灯珠数量修改宏定义中Quantity对应的数值即可
*/
#include <STC15F2K60S2.H> //包含寄存器定义的头文件
#include <INTRINS.H> //需要用空操作 _nop_(); 来延时
//Quantity:灯珠的数量,如果用STC15W204S,则范围是:1~77(data不能超过256个字节,实测超过77就不能正常显示了)
//想控制更加多的灯珠,需要更换为有较多片外RAM的单片机,一个灯珠需要3个字节的RAM缓存
//并且把缓存数组的idata改成xdata
#define Quantity 60
#define Duration1 15 //呼吸灯延时的时长,更改数值可以改变呼吸灯的快慢,数值越大,呼吸灯越慢
#define Duration2 25 //渐变灯延时的时长,更改数值可以改变渐变灯的快慢,数值越大,渐变灯越慢
#define Duration3 30 //流星流水灯延时的时长,更改数值可以改变移动的速度,数值越大,速度越慢
//引脚定义
//有6个IO口,P30~P33和P54、P55
//我买的板子PCB板上印错了,P54、P55印成了P34、P35
sbit WS2812B_Din=P5^4;
//用3*Quantity个字节作为WS2812B彩色灯带的显示缓存,共有Quantity个灯珠,每个灯珠需要写入24Bit(3个字节)控制显示的颜色
//WS2812B芯片要求按G、R、B的顺序发送数据,并且每个字节要高位先发
//缓存数组中每三个字节为一组,每一组分别对应一个灯珠的G(绿)、R(红)、B(蓝)三原色
//第一组对应数据传输方向的第一个灯珠,第二组对应第二个灯珠,以此类推
//STC15W204S只有256K的片内RAM空间,没有片外RAM空间,只能用idata修饰,不能用xdata修饰,否则灯带不能正常显示
//STC15W204S最多只能定义七十多个灯珠的缓存,想要控制更加多的灯,需要用有较多片外RAM空间的单片机,并且要把idata改成xdata
//想更改灯带的显示,先更改此显示缓存中的数据,再通过函数WS2812B_UpdateDisplay将显示缓存的数据写入每个灯珠的WS2812B芯片内
unsigned char idata WS2812B_Buffer[3*Quantity];
//用来实现流水灯退出的效果
unsigned char code Table0[]={
0,0,0, //无显示
};
//通过查表的方法显示流水灯,三个为一组,一组对应一个灯珠的R、G、B的值
unsigned char code Table1[]={ //流星流水灯
255,0,0,191,0,0,127,0,0,95,0,0,63,0,0,47,0,0,31,0,0,23,0,0, //红
15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,2,0,0,1,0,0,0,0,0,
0,255,0,0,191,0,0,127,0,0,95,0,0,63,0,0,47,0,0,31,0,0,23,0, //绿
0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,2,0,0,1,0,0,0,0,
0,0,255,0,0,191,0,0,127,0,0,95,0,0,63,0,0,47,0,0,31,0,0,23, //蓝
0,0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,2,0,0,1,0,0,0,
255,255,0,191,191,0,127,127,0,95,95,0,63,63,0,47,47,0,31,31,0,23,23,0, //黄
15,15,0,11,11,0,7,7,0,5,5,0,3,3,0,2,2,0,1,1,0,0,0,0,
255,0,255,191,0,191,127,0,127,95,0,95,63,0,63,47,0,47,31,0,31,23,0,23, //紫
15,0,15,11,0,11,7,0,7,5,0,5,3,0,3,2,0,2,1,0,1,0,0,0,
0,255,255,0,191,191,0,127,127,0,95,95,0,63,63,0,47,47,0,31,31,0,23,23, //青
0,15,15,0,11,11,0,7,7,0,5,5,0,3,3,0,2,2,0,1,1,0,0,0,
255,255,255,191,191,191,127,127,127,95,95,95,63,63,63,47,47,47,31,31,31,23,23,23, //白
15,15,15,11,11,11,7,7,7,5,5,5,3,3,3,2,2,2,1,1,1,0,0,0,
};
unsigned char code Table2[]={ //顺向逆向流星流水灯叠加
255,191,127,95,63,47,31,23,15,11,7,5,3,2,1,0,
};
/**
* @brief 延时函数,1T@24.000MHz调用可延时约xms毫秒
* @param xms 要延时的时间,范围:0~65535
* @retval 无
*/
void Delay(unsigned int xms)
{
unsigned char i,j;
while(xms--)
{
i=24;
j=85;
do
{
while(--j);
}while(--i);
}
}
/**
* @brief WS2812B彩带私有延时函数,1T@24.000MHz调用可延时约100us
* @param 无
* @retval 无
*/
void WS2812B_Delay100us(void)
{
unsigned char i,j;
i=3;
j=82;
do
{
while(--j);
}while(--i);
}
/**
* @brief WS2812B彩色灯带清空显示缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param 无
* @retval 无
*/
void WS2812B_Clear(void)
{
unsigned int i; //如果用unsigned char而不用unsigned int的话,Quantity大于85之后,此函数就会陷入死循环
//用unsigned int定义是为了方便拓展显示更多的灯珠
for(i=0;i<3*Quantity;i++){WS2812B_Buffer[i]=0;}
}
/**
* @brief WS2812B彩色灯带按灯带数据传输方向移动显示数组Array中的数据(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param *Array Array为传递过来的指针(即内存地址),数组名就是数组的首地址
* @param Offset 偏移量,范围:0~Array数组数据总数/3-1
Array中的数据三个为一组,移动后第一个灯珠显示第Offset组的数据(第0组为数组Array中的前三个数据)
* @retval 无
*/
void WS2812B_MoveInOrder(unsigned char *Array,unsigned int Offset)
{ //Offset用unsigned int定义是为了方便拓展显示更长的流水灯
unsigned int i;
//缓存数组中的数据按数组索引增大的方向移动3个字节
for(i=0;i<Quantity-1;i++)
{
WS2812B_Buffer[3*(Quantity-i)-3]=WS2812B_Buffer[3*(Quantity-i)-6];
WS2812B_Buffer[3*(Quantity-i)-2]=WS2812B_Buffer[3*(Quantity-i)-5];
WS2812B_Buffer[3*(Quantity-i)-1]=WS2812B_Buffer[3*(Quantity-i)-4];
}
//移动后,对第一个灯珠对应的三个字节进行赋值,从而实现流水灯的效果
Array+=Offset*3; //指针方面的知识不清楚的先去了解一下
WS2812B_Buffer[1]=*Array; //WS2812B_Buffer中按G、R、B存放,
WS2812B_Buffer[0]=*(Array+1); //Array数组中按R、G、B存放,
WS2812B_Buffer[2]=*(Array+2); //赋值的时候需要调一下顺序
}
/**
* @brief WS2812B彩色灯带按灯带数据传输方向的反方向移动显示数组Array中的数据(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param *Array Array为传递过来的指针(即内存地址),数组名就是数组的首地址
* @param Offset 偏移量,范围:0~Array数组数据总数/3-1
Array中的数据三个为一组,移动后最后一个灯珠显示第Offset组的数据(第0组为数组Array中的前三个数据)
* @retval 无
*/
void WS2812B_MoveInReverseOrder(unsigned char *Array,unsigned int Offset)
{
unsigned int i;
//缓存数组中的数据按数组索引减小的方向移动3个字节
for(i=0;i<Quantity-1;i++)
{
WS2812B_Buffer[3*i ]=WS2812B_Buffer[3*i +3];
WS2812B_Buffer[3*i+1]=WS2812B_Buffer[3*i+1+3];
WS2812B_Buffer[3*i+2]=WS2812B_Buffer[3*i+2+3];
}
//移动后,对最后一个灯珠对应的三个字节进行赋值,从而实现流水灯的效果
Array+=Offset*3;
WS2812B_Buffer[3*Quantity-2]=*Array; //WS2812B_Buffer中按G、R、B存放,
WS2812B_Buffer[3*Quantity-3]=*(Array+1); //Array数组中按R、G、B存放,
WS2812B_Buffer[3*Quantity-1]=*(Array+2); //赋值的时候需要调一下顺序
}
/**
* @brief WS2812B彩色灯带设置一个灯珠的缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param Position 要设置的位置,范围:0~Quantity-1,对应第1个到第Quantity个灯珠
* @param R 红(Red),范围:0~255
* @param G 绿(Green),范围:0~255
* @param B 蓝(Blue),范围:0~255
* @retval 无
*/
void WS2812B_SetBuffer(unsigned int Position,unsigned char R,unsigned char G,unsigned char B)
{
//缓存数组中,每三个为一组,每一组按G、R、B的顺序存放,
//这样方便数据的写入,通过数据线按数组索引顺序直接写入缓存数组的3*Quantity个字节的数据就行了
WS2812B_Buffer[3*Position ]=G;
WS2812B_Buffer[3*Position+1]=R;
WS2812B_Buffer[3*Position+2]=B;
}
/**
* @brief WS2812B彩色灯带写入一个字节
* @brief 频率要求:1T@24.000MHz,如果要换其他频率,则需要调整“_nop_();”的数量
* @param Byte 要写入的字节
* @retval 无
*/
void WS2812B_WriteByte(unsigned char Byte)
{
unsigned char i;
// EA=0; //关闭总中断(如果用到中断的话)(时序要求严格,不能被打断),并要求中断函数执行的时间不能太长
//时间太长,相当于发送了重置信号
for(i=0;i<8;i++)
{
if(Byte&(0x80>>i)) //写1(高位先发)
{
WS2812B_Din=1; //根据高电平的时长确定发送的是1还是0,跟DS18B20类似
//用空操作进行延时,单片机使用不同的频率,就需要不一样“_nop_();”的数量
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
WS2812B_Din=0; //经测试,数据线拉低后不用加延时
}
else //写0
{
WS2812B_Din=1;
_nop_();_nop_();_nop_();_nop_();_nop_();
WS2812B_Din=0; //经测试,数据线拉低后不用加延时
}
}
// EA=1; //开启总中断
}
/**
* @brief WS2812B彩色灯带更新显示,将显示缓存数组WS2812B_Buffer的数据写入到灯珠的WS2812B芯片内
* @param 无
* @retval 无
*/
void WS2812B_UpdateDisplay(void)
{
unsigned int i;
for(i=0;i<3*Quantity;i++){WS2812B_WriteByte(WS2812B_Buffer[i]);} //连续写入显示缓存的3*Quantity个字节
WS2812B_Delay100us(); //Reset(重置)信号
}
void main()
{
unsigned int i,j,k;
unsigned char Sum; //用来控制顺向和逆向流星流水灯的叠加显示
//Sum表示顺向或逆向流星流水灯出现的个数总和-1
P3M1=0;P3M0=0; //将P3口设置为上拉模式
P5M1=0;P5M0=0; //将P5口设置为上拉模式
while(1)
{
//呼吸灯(红、绿、蓝、黄、紫、青、白)
for(k=0;k<7;k++) //总共要显示七种颜色的呼吸灯
{
for(i=0;i<256;i=i+3) //变亮
{
for(j=0;j<Quantity;j++)
{
switch(k)
{
case 0:WS2812B_SetBuffer(j,i,0,0);break; //红
case 1:WS2812B_SetBuffer(j,0,i,0);break; //绿
case 2:WS2812B_SetBuffer(j,0,0,i);break; //蓝
case 3:WS2812B_SetBuffer(j,i,i,0);break; //黄
case 4:WS2812B_SetBuffer(j,i,0,i);break; //紫
case 5:WS2812B_SetBuffer(j,0,i,i);break; //青
case 6:WS2812B_SetBuffer(j,i,i,i);break; //白
default:break;
}
}
WS2812B_UpdateDisplay(); //更新显示
Delay(Duration1); //延时“Duration1”ms
}
for(i=0;i<256;i=i+3) //变暗
{
for(j=0;j<Quantity;j++)
{
switch(k)
{
case 0:WS2812B_SetBuffer(j,255-i,0,0);break; //红
case 1:WS2812B_SetBuffer(j,0,255-i,0);break; //绿
case 2:WS2812B_SetBuffer(j,0,0,255-i);break; //蓝
case 3:WS2812B_SetBuffer(j,255-i,255-i,0);break; //黄
case 4:WS2812B_SetBuffer(j,255-i,0,255-i);break; //紫
case 5:WS2812B_SetBuffer(j,0,255-i,255-i);break; //青
case 6:WS2812B_SetBuffer(j,255-i,255-i,255-i);break; //白
default:break;
}
}
WS2812B_UpdateDisplay();
Delay(Duration1); //更改延时时间可以改变呼吸灯的快慢
}
}
//红绿蓝三原色渐变
for(i=0;i<256;i=i+3) //红变亮,至全亮
{
for(j=0;j<Quantity;j++)
{
WS2812B_SetBuffer(j,i,0,0);
}
WS2812B_UpdateDisplay(); //更新显示
Delay(Duration1);
}
for(k=0;k<2;k++) //循环2次
{
for(i=0;i<256;i=i+3) //红->绿渐变
{
for(j=0;j<Quantity;j++)
{
WS2812B_SetBuffer(j,255-i,i,0); //红变暗,至全灭,绿变亮,至全亮
}
WS2812B_UpdateDisplay(); //更新显示
Delay(Duration2);
}
for(i=0;i<256;i=i+3) //绿->蓝渐变
{
for(j=0;j<Quantity;j++)
{
WS2812B_SetBuffer(j,0,255-i,i); //绿变暗,至全灭,蓝变亮,至全亮
}
WS2812B_UpdateDisplay(); //更新显示
Delay(Duration2);
}
for(i=0;i<256;i=i+3) //蓝->红渐变
{
for(j=0;j<Quantity;j++)
{
WS2812B_SetBuffer(j,i,0,255-i); //蓝变暗,至全灭,红变亮,至全亮
}
WS2812B_UpdateDisplay(); //更新显示
Delay(Duration2);
}
}
for(i=0;i<256;i=i+3) //红变暗,至全灭
{
for(j=0;j<Quantity;j++)
{
WS2812B_SetBuffer(j,255-i,0,0);
}
WS2812B_UpdateDisplay(); //更新显示
Delay(Duration1);
}
//流星流水灯(红、绿、蓝、黄、紫、青、白)
for(j=0;j<2;j++) //正向,循环2次
{
for(i=0;i<16*7;i++) //每种颜色流星灯的长度是16个灯珠,总共有7种颜色
{
WS2812B_MoveInOrder(Table1,i);
WS2812B_UpdateDisplay();
Delay(Duration3); //更改延时时间可以改变流星灯的速度
}
}
for(i=0;i<Quantity;i++) //退出
{
WS2812B_MoveInOrder(Table0,0);
WS2812B_UpdateDisplay();
Delay(Duration3);
}
for(j=0;j<2;j++) //逆向,循环2次
{
for(i=0;i<16*7;i++)
{
WS2812B_MoveInReverseOrder(Table1,i);
WS2812B_UpdateDisplay();
Delay(Duration3);
}
}
for(i=0;i<Quantity;i++) //退出
{
WS2812B_MoveInReverseOrder(Table0,0);
WS2812B_UpdateDisplay();
Delay(Duration3);
}
//三原色流星流水灯顺向逆向叠加
//相遇的时候会有颜色叠加,看不清楚可以增长延时观察
for(k=0;k<2;k++) //循环2次
{
Sum=0;
for(j=0;j<Quantity+16;j++) //顺红逆绿
{
WS2812B_Clear(); //清空缓存
for(i=0;i<=Sum;i++)
{
if(j<16) //流星灯进入过程
{
WS2812B_Buffer[3*(Sum-i)+1]=Table2[i]; //红
WS2812B_Buffer[3*(Quantity-(Sum-i))-3]=Table2[i]; //绿
}
else if(j-i<Quantity) //完全进入及退出
{
WS2812B_Buffer[3*(j-i)+1]=Table2[i]; //红
WS2812B_Buffer[3*(Quantity-(j-i))-3]=Table2[i]; //绿
}
}
WS2812B_UpdateDisplay();
Sum++; //每经过一个Delay延时,流星灯进入的数量+1
if(Sum>15){Sum=15;} //流星灯长度为1~16,对应sum的0~15,完全进入后保持15不变
//Table2数组中16个数据255,191,127,95,63,47,31,23,15,11,7,5,3,2,1,0,对应的灯珠的亮度
Delay(Duration3);
}
Sum=0;
for(j=0;j<Quantity+16;j++) //顺蓝逆红
{
WS2812B_Clear(); //清空缓存
for(i=0;i<=Sum;i++)
{
if(j<16) //流星灯进入过程
{
WS2812B_Buffer[3*(Sum-i)+2]=Table2[i]; //蓝
WS2812B_Buffer[3*(Quantity-(Sum-i))-2]=Table2[i]; //红
}
else if(j-i<Quantity) //完全进入及退出
{
WS2812B_Buffer[3*(j-i)+2]=Table2[i]; //蓝
WS2812B_Buffer[3*(Quantity-(j-i))-2]=Table2[i]; //红
}
}
WS2812B_UpdateDisplay();
Sum++;
if(Sum>15){Sum=15;}
Delay(Duration3);
}
Sum=0;
for(j=0;j<Quantity+16;j++) //顺绿逆蓝
{
WS2812B_Clear();
for(i=0;i<=Sum;i++)
{
if(j<16)
{
WS2812B_Buffer[3*(Sum-i)]=Table2[i]; //绿
WS2812B_Buffer[3*(Quantity-(Sum-i))-1]=Table2[i]; //蓝
}
else if(j-i<Quantity)
{
WS2812B_Buffer[3*(j-i)]=Table2[i]; //绿
WS2812B_Buffer[3*(Quantity-(j-i))-1]=Table2[i]; //蓝
}
}
WS2812B_UpdateDisplay();
Sum++;
if(Sum>15){Sum=15;}
Delay(Duration3);
}
}
}
}
总结
感觉WS2812B芯片对时序的要求并没有像手册说的那么严格。