塞尔达流水灯带
前言
新的进阶练习项目又来啦!这次带来的是塞尔达石板的流水灯显示,简而言之就是一块塞尔达石板,上面的文字像黑客帝国里面的数据条一样,慢慢地掉落下来。和往常一样,此次依然作为51的进阶练习,基础薄弱的可以看verimake的51相关教程《以 CH549 为例的 51 教程》,其中WS2812B相关的内容详细介绍可以看我之前的笔记《基于CH549单片机用旋转电位器对WS2812B灯带的控制学习》。
接线
不难看出这个和之前灯带控制的帖子里的接线基本一样,还是把旋转电位器去了的版本。仅仅串联330Ω的电阻有效保护输出口,毕竟此次学习的重点在程序里。
具体程序 main.c
#include <CH549_sdcc.H> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#include <CH549_DEBUG.h> //CH549官方提供库的头文件,定义了一些关于主频,延时,串口设置,看门口,赋值设置等基础函数
#include <stdlib.h> //调用随机数函数
#define COLUMN_LED 13 //灯的列数
#define COW_LED 21 //灯的行数
#define PIXEL 4 //像素长度
#define COW (COW_LED+PIXEL*4-2) //行数据长度
unsigned char buf_R[COLUMN_LED][COW] = {{0}};//假定灯带长度数据条的输出颜色缓存
unsigned char buf_G[COLUMN_LED][COW] = {{0}};
unsigned char buf_B[COLUMN_LED][COW] = {{0}};
unsigned char Column[COLUMN_LED]; //列定位灯带标志
unsigned char RGB_R[COLUMN_LED];//输入颜色缓存
unsigned char RGB_G[COLUMN_LED];
unsigned char RGB_B[COLUMN_LED];
/***********************************************************************
名 称 :WS2812B时序部分
功 能 :定义WS2812B的1码0码及复位码,以及根据RGB值为WS2812B赋予数据
************************************************************************/
#define WS2812 P2_2 //设定p2.2口作为灯带的输入口
#define _nop() __asm NOP __endasm //将nop指令定义为宏
//1码,高电平850ns 低电平400ns 误差正负150ns
#define RGB_1() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;}while(0)
//此处加do while循环可以将宏定义的部分可以被识别为语句,方便纠错,下同
//0码,高电平400ns 低电平850ns 误差正负150ns
#define RGB_0() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
}while(0)
//发送24位数据
void Send_2811_24bits(unsigned char G8,unsigned char R8,unsigned char B8)
{
char n = 0;
//发送G8位
for(n=0;n<8;n++)
{
if(G8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
G8<<=1;
}
//发送R8位
for(n=0;n<8;n++)
{
if(R8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
R8<<=1;
}
//发送B8位
for(n=0;n<8;n++)
{
if(B8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
B8<<=1;
}
}
//复位码
void RGB_Rst()
{
WS2812 = 0;
mDelayuS( 50 );
}
//缓存清0
void Buf_Put0()
{
unsigned char i,j;
for (j = 0; j < COLUMN_LED; j++)
{
for(i=0;i<COW;i++)
{
buf_R[j][i] = 0;
buf_G[j][i] = 0;
buf_B[j][i] = 0;
}
}
}
//颜色缓存
void genColor()
{
unsigned int j;
float h , s = 1.0 , v = 1.0; //色相等分,明度和饱和度均设定为满
for (j = 0; j < COLUMN_LED; j++)
{
h = j*1.0/COLUMN_LED;
h *= 360.0f; //HSV转RGB
if (s == 0)
{RGB_R[j] = RGB_G[j] = RGB_B[j] = v;}
else
{
h = h / 60.0;
int i = (int)h;
float C = h - i;
v *=255.0;
float X = v * (1 - s) * 255.0;
float Y = v * (1 - s * C) * 255.0;
float Z = v * (1 - s * (1 - C)) * 255.0;
switch (i)
{
case 0: RGB_R[j] = v; RGB_G[j] = Z; RGB_B[j] = X; break;
case 1: RGB_R[j] = Y; RGB_G[j] = v; RGB_B[j] = X; break;
case 2: RGB_R[j] = X; RGB_G[j] = v; RGB_B[j] = Z; break;
case 3: RGB_R[j] = X; RGB_G[j] = Y; RGB_B[j] = v; break;
case 4: RGB_R[j] = Z; RGB_G[j] = X; RGB_B[j] = v; break;
case 5: RGB_R[j] = v; RGB_G[j] = X; RGB_B[j] = Y; break;
}
}
}
}
//设定像素段
void Set_Color(unsigned char column,unsigned char cow)
{
unsigned char i;
for(i=0;i<PIXEL;i++)
{
buf_R[column][cow+PIXEL-i] = RGB_R[column] / (PIXEL * i + 1);
buf_G[column][cow+PIXEL-i] = RGB_G[column] / (PIXEL * i + 1);
buf_B[column][cow+PIXEL-i] = RGB_B[column] / (PIXEL * i + 1);
}
for(i=1;i<PIXEL;i++)
{
buf_R[column][cow+PIXEL+i] = RGB_R[column] / (PIXEL * i + 1);
buf_G[column][cow+PIXEL+i] = RGB_G[column] / (PIXEL * i + 1);
buf_B[column][cow+PIXEL+i] = RGB_B[column] / (PIXEL * i + 1);
}
}
//将显示区的数据发送显示
void Send_ALL()
{
unsigned char i,j;
for (j = 0; j < COLUMN_LED; j++)
{
for(i=PIXEL*2-1;i<COW-PIXEL*2+1;i++)
{
Send_2811_24bits(buf_G[j][i],buf_R[j][i],buf_B[j][i]);
}
}
mDelaymS(50);
}
/********************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void main(void)
{
unsigned int m,i,j;
CfgFsys(); //CH549时钟选择配置
mDelaymS(20);
genColor(); //颜色预设
for ( j = 0; j < COLUMN_LED; j++) //所有列的流水灯进入就绪状态
{
Column[j] = COW_LED+PIXEL*2-1;
}
/* 主循环 */
while (1)
{
Buf_Put0(); //数据条清0
m = rand() % COLUMN_LED; //取随机数
if (Column[m] == COW_LED+PIXEL*2-1) {Column[m] = 0;} //选中的列若以就绪开启流水灯
for ( i = 0; i < COLUMN_LED; i++) //检测每一列的状态
{
if (Column[i] < COW_LED+PIXEL*2-1) //若列数据未走完给该列赋值
{
if (i % 2) //判断单双列
{
Set_Color(i,COW_LED+PIXEL*2-1-Column[i]); //双列
}
else
{
Set_Color(i,Column[i]); //单列
}
Column[i]++; //下次循环走下一个瞬间的数据
}
}
Send_ALL(); //将该时间数据发送显示
}
}
整个代码分为三个部分
其中<CH549_sdcc.h>、<CH549_DEBUG.h>、<stdlib.h>均为CH549官方提供库的头文件;
第一部分:WS2812B控制部分
#define WS2812 P2_2 //设定p2.2口作为灯带的输入口
#define _nop() __asm NOP __endasm //将nop指令定义为宏
//1码,高电平850ns 低电平400ns 误差正负150ns
#define RGB_1() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;}while(0)
//此处加do while循环可以将宏定义的部分可以被识别为语句,方便纠错,下同
//0码,高电平400ns 低电平850ns 误差正负150ns
#define RGB_0() do{WS2812 = 1;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
WS2812 = 0;\
_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();_nop();\
}while(0)
//发送24位数据
void Send_2811_24bits(unsigned char G8,unsigned char R8,unsigned char B8)
{
char n = 0;
//发送G8位
for(n=0;n<8;n++)
{
if(G8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
G8<<=1;
}
//发送R8位
for(n=0;n<8;n++)
{
if(R8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
R8<<=1;
}
//发送B8位
for(n=0;n<8;n++)
{
if(B8&0x80)
{
RGB_1();
}
else
{
RGB_0();
}
B8<<=1;
}
}
//复位码
void RGB_Rst()
{
WS2812 = 0;
mDelayuS( 50 );
}
该部分沿用之前帖子中的设定,不再赘述。
第二部分:数据条的设定与赋予
#define COLUMN_LED 7 //灯的列数
#define COW_LED 21 //灯的行数
#define PIXEL 4 //像素长度
#define COW (COW_LED+PIXEL*4-2) //行数据长度
unsigned char buf_R[COLUMN_LED][COW] = {{0}};//假定灯带长度数据条的输出颜色缓存
unsigned char buf_G[COLUMN_LED][COW] = {{0}};
unsigned char buf_B[COLUMN_LED][COW] = {{0}};
unsigned char Column[COLUMN_LED]; //列定位灯带标志
unsigned char RGB_R[COLUMN_LED];//输入颜色缓存
unsigned char RGB_G[COLUMN_LED];
unsigned char RGB_B[COLUMN_LED];
//缓存清0
void Buf_Put0()
{
unsigned char i,j;
for (j = 0; j < COLUMN_LED; j++)
{
for(i=0;i<COW;i++)
{
buf_R[j][i] = 0;
buf_G[j][i] = 0;
buf_B[j][i] = 0;
}
}
}
//颜色缓存
void genColor()
{
unsigned int j;
float h , s = 1.0 , v = 1.0; //色相等分,明度和饱和度均设定为满
for (j = 0; j < COLUMN_LED; j++)
{
h = j*1.0/COLUMN_LED;
h *= 360.0f; //HSV转RGB
if (s == 0)
{RGB_R[j] = RGB_G[j] = RGB_B[j] = v;}
else
{
h = h / 60.0;
int i = (int)h;
float C = h - i;
v *=255.0;
float X = v * (1 - s) * 255.0;
float Y = v * (1 - s * C) * 255.0;
float Z = v * (1 - s * (1 - C)) * 255.0;
switch (i)
{
case 0: RGB_R[j] = v; RGB_G[j] = Z; RGB_B[j] = X; break;
case 1: RGB_R[j] = Y; RGB_G[j] = v; RGB_B[j] = X; break;
case 2: RGB_R[j] = X; RGB_G[j] = v; RGB_B[j] = Z; break;
case 3: RGB_R[j] = X; RGB_G[j] = Y; RGB_B[j] = v; break;
case 4: RGB_R[j] = Z; RGB_G[j] = X; RGB_B[j] = v; break;
case 5: RGB_R[j] = v; RGB_G[j] = X; RGB_B[j] = Y; break;
}
}
}
}
//设定像素段
void Set_Color(unsigned char column,unsigned char cow)
{
unsigned char i;
for(i=0;i<PIXEL;i++)
{
buf_R[column][cow+PIXEL-i] = RGB_R[column] / (PIXEL * i + 1);
buf_G[column][cow+PIXEL-i] = RGB_G[column] / (PIXEL * i + 1);
buf_B[column][cow+PIXEL-i] = RGB_B[column] / (PIXEL * i + 1);
}
for(i=1;i<PIXEL;i++)
{
buf_R[column][cow+PIXEL+i] = RGB_R[column] / (PIXEL * i + 1);
buf_G[column][cow+PIXEL+i] = RGB_G[column] / (PIXEL * i + 1);
buf_B[column][cow+PIXEL+i] = RGB_B[column] / (PIXEL * i + 1);
}
}
//将显示区的数据发送显示
void Send_ALL()
{
unsigned char i,j;
for (j = 0; j < COLUMN_LED; j++)
{
for(i=PIXEL*2-1;i<COW-PIXEL*2+1;i++)
{
Send_2811_24bits(buf_G[j][i],buf_R[j][i],buf_B[j][i]);
}
}
mDelaymS(50);
}
其中:
#define COLUMN_LED 13 //灯的列数
#define COW_LED 21 //灯的行数
#define PIXEL 4 //像素长度
#define COW (COW_LED+PIXEL*4-2) //行数据长度
unsigned char buf_R[COLUMN_LED][COW] = {{0}};//假定灯带长度数据条的输出颜色缓存
unsigned char buf_G[COLUMN_LED][COW] = {{0}};
unsigned char buf_B[COLUMN_LED][COW] = {{0}};
unsigned char Column[COLUMN_LED]; //列定位灯带标志
unsigned char RGB_R[COLUMN_LED];//输入颜色缓存
unsigned char RGB_G[COLUMN_LED];
unsigned char RGB_B[COLUMN_LED];
由下图可以清晰看出COLUMN_LED为列数,COW_LED为实际列灯带行数,COW为虚拟列灯带长度;像素段的运动将中间最亮的那一部分作为定位点,而PIEXL即为亮点带一边渐暗的像素段,实际的像素段为(PIEXL*2-1);buf_X数组为颜色RGB的缓存数组方便发送显示分为三个数组,分别对应RGB;Column数组为列定位灯带标志,后面会详细说明;RGB_X为循环前就设定好的列输入颜色缓存

void Buf_Put0()
{
unsigned char i,j;
for (j = 0; j < COLUMN_LED; j++)
{
for(i=0;i<COW;i++)
{
buf_R[j][i] = 0;
buf_G[j][i] = 0;
buf_B[j][i] = 0;
}
}
}
Buf_Put0函数即将虚拟灯带长度下的所有颜色缓存数据全部置0.
void genColor()
{
unsigned int j;
float h , s = 1.0 , v = 1.0; //色相等分,明度和饱和度均设定为满
for (j = 0; j < COLUMN_LED; j++)
{
h = j*1.0/COLUMN_LED;
h *= 360.0f; //HSV转RGB
if (s == 0)
{RGB_R[j] = RGB_G[j] = RGB_B[j] = v;}
else
{
h = h / 60.0;
int i = (int)h;
float C = h - i;
v *=255.0;
float X = v * (1 - s) * 255.0;
float Y = v * (1 - s * C) * 255.0;
float Z = v * (1 - s * (1 - C)) * 255.0;
switch (i)
{
case 0: RGB_R[j] = v; RGB_G[j] = Z; RGB_B[j] = X; break;
case 1: RGB_R[j] = Y; RGB_G[j] = v; RGB_B[j] = X; break;
case 2: RGB_R[j] = X; RGB_G[j] = v; RGB_B[j] = Z; break;
case 3: RGB_R[j] = X; RGB_G[j] = Y; RGB_B[j] = v; break;
case 4: RGB_R[j] = Z; RGB_G[j] = X; RGB_B[j] = v; break;
case 5: RGB_R[j] = v; RGB_G[j] = X; RGB_B[j] = Y; break;
}
}
}
}
genColor函数是给列输入颜色缓存设定颜色,其中用到了HSV转RGB的相关算法。
RGB颜色模型
HSV颜色模型
RGB颜色模型是面向硬件的,WS2812B的颜色识别就是RGB的,之前的几次练习都不太在意颜色的准确度,所以直接套用RGB颜色模型就可以满足需求,这次的流水灯要各列灯颜色各异,与其手动输入颜色不如考虑HSV颜色模型。
HSV颜色模型是面向用户的,其中:
- 色调H:用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,紫色为300°;

- 饱和度S:饱和度S表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。饱和度高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,值越大,颜色越饱和。
- 明度V:明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白)。
此处我们将色调H按列分等分分别给输入的颜色缓存赋值,这样便可以做到颜色各异;随着列数增加相邻的两列可能出现颜色相似的情况。
void Set_Color(unsigned char column,unsigned char cow)
{
unsigned char i;
for(i=0;i<PIXEL;i++)
{
buf_R[column][cow+PIXEL-i] = RGB_R[column] / (PIXEL * i + 1);
buf_G[column][cow+PIXEL-i] = RGB_G[column] / (PIXEL * i + 1);
buf_B[column][cow+PIXEL-i] = RGB_B[column] / (PIXEL * i + 1);
}
for(i=1;i<PIXEL;i++)
{
buf_R[column][cow+PIXEL+i] = RGB_R[column] / (PIXEL * i + 1);
buf_G[column][cow+PIXEL+i] = RGB_G[column] / (PIXEL * i + 1);
buf_B[column][cow+PIXEL+i] = RGB_B[column] / (PIXEL * i + 1);
}
}
这段函数是给像素段输入颜色缓存,定位点位于像素段中心且为满亮度,亮度向两侧递减;虽然灯带的串联是S型的,但是设定的流水灯方向是从上向下(主程序部分详细说);如下图所示,像素段实际的起点和终点为橙色椭圆位置,移动距离也由图中标出。
由于WS2812B灯带对亮度的显示变化不算明显(或者可能是人的眼睛对更亮的光分辨不出),仅仅减少一点的RGB数值并不会看出明显的明暗,只有RGB数值变化较大时才能看出变化,所以用的上面的算法。

void Send_ALL()
{
unsigned char i,j;
for (j = 0; j < COLUMN_LED; j++)
{
for(i=PIXEL*2-1;i<COW-PIXEL*2+1;i++)
{
Send_2811_24bits(buf_G[j][i],buf_R[j][i],buf_B[j][i]);
}
}
mDelaymS(50);
}
这段就是将实际灯带长度内的所有颜色缓存数据发送到显示的函数。
第三部分:main函数
void main(void)
{
unsigned int m,i,j;
CfgFsys(); //CH549时钟选择配置
mDelaymS(20);
genColor(); //颜色预设
for ( j = 0; j < COLUMN_LED; j++) //所有列的流水灯进入就绪状态
{
Column[j] = COW_LED+PIXEL*2-1;
}
/* 主循环 */
while (1)
{
Buf_Put0(); //数据条清0
m = rand() % COLUMN_LED; //取随机数
if (Column[m] == COW_LED+PIXEL*2-1) {Column[m] = 0;} //选中的列若以就绪开启流水灯
for ( i = 0; i < COLUMN_LED; i++) //检测每一列的状态
{
if (Column[i] < COW_LED+PIXEL*2-1) //若列数据未走完给该列赋值
{
if (i % 2) //判断单双列
{
Set_Color(i,COW_LED+PIXEL*2-1-Column[i]); //双列
}
else
{
Set_Color(i,Column[i]); //单列
}
Column[i]++; //下次循环走下一个瞬间的数据
}
}
Send_ALL(); //将该时间数据发送显示
}
}

如流程图所示,在main函数中,先配置时钟,然后进行颜色预设,将标志位数值Column置为像素段中心最大移动距离即就绪的位置。进入循环,先将虚拟灯带长度的数据全部置0,再取随机数,就随机数那一列若已就绪就将这一列标志位置0即位于移动起始点,然后进入赋值循环,每一列先判断有无达到就绪,达到就保持该列数据置0的状态,未达到就根据列数是单是双,单数列按正向,双数列按反向(具体如下图),赋予标志位数值的位置的数据(例如标志位为5,就按像素段中心起始点起按移动方向第五点)并将该列的标志位数值加1,保证下一循环进行一次移动,直到每一列都将该瞬间的颜色缓存更新完,发送实际灯带长度的数据给与显示,回到取随机数那一列再继续循环。

成品展示图
可以看出,就显示的来看还是很好看的,虽然中途硬件问题(替换掉灯带上的个别时序有问题的灯珠)让我们头疼了会,但是最后实现出来的效果还是不错的。
老样子具体演示可以看B站verimake上传的视频:传送门
这次的文件我也放到了gitee仓库上,文件名区别于之前的文件的采用:Zelda-light2
欢迎大家参考!!