基于CH549单片机用旋转电位器对WS2812B灯带的控制学习
前言
新手起步,以CH549单片机进行学习,也正是看了论坛上的《以 CH549 为例的 51 教程》,想要进一步对自己的掌握程度了解,开始对WS2812B灯带的控制进行学习。
WS2812B基础介绍
WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源。其外型与5050LED灯珠相同,每个元件即为一个像素点。像素点内部包含了智能数字接口数据锁存信号整形放大驱动电路,还包含有高精度的内部振荡器和12V高压可编程定电流控制部分,有效保证了像素点光的颜色高度一致。
机械尺寸与引脚
特点总结
这款彩色LED灯特点鲜明,简而言之可以总结为以下三点:
- 采用单总线通信方式,节约IO口,而且可以多级串联。
- 对时序要求高,要在一个周期内,根据高电平和低电平的时间往灯带中写入1或者0码。
- 一个灯由24位数据控制,高位先发,按照GRB的顺序发送数据,GRB分别对应8位。
接线图和灯带实物
对于想要复现的小伙伴。这里也提供了接线图进行参考;
此时我们选用的是p2.2口作为WS2812B的输入口,p1.7口让电位器可以调节灯带的亮度。
具体程序 main.c
/**
******************************************************************
* @file main.c
* @author merlin
* @version V1.0
* @date 2022-5-10
* @brief ADC,WS2812B
******************************************************************
* @attention
* verimake 用于
* 用旋转电位器调节WS2812B灯带亮度
******************************************************************
*/
#include <CH549_sdcc.h> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#include <CH549_DEBUG.h> //CH549官方提供库的头文件,定义了一些关于主频,延时,串口设置,看门口,赋值设置等基础函数
#include <CH549_ADC.h> //CH549官方提供库的头文件,定义了一些关于ADC初始化,采集数据等函数
/********************************************************************
* TIPS:
* adc各通道对应引脚关系
* P1.1 AIN1
* P1.2 AIN2
* P1.3 AIN3
* P1.4 AIN4
* P1.5 AIN5
* P1.6 AIN6
* P1.7 AIN7
* P0.0 AIN8
* P0.1 AIN9
* P0.2 AIN10
* P0.3 AIN11
* P0.4 AIN12
* P0.5 AIN13
* P0.6 AIN14
* P0.7 AIN15
*********************************************************************/
#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();\
_nop();_nop();_nop();_nop();_nop();}while(0)
#define numLEDs 14 //灯的个数
//发送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 );
}
//直接将颜色数据赋予灯带
void Set_Light(unsigned char x,unsigned char y,unsigned char z)
{
unsigned char i;
for ( i = 0; i < numLEDs; i++)
{
Send_2811_24bits( x , y , z );//发送显示
}
}
/********************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void main(void)
{
UINT8 ch;
UINT16 Voltage;
CfgFsys(); //CH549时钟选择配置
mDelaymS(20);
ADC_ExInit(3); //ADC初始化,选择采样时钟
/* 主循环 */
while (1)
{
ch = 7; //选择P1.7口作为ADC的采样口
ADC_ChSelect(ch); //选择通道
ADC_StartSample(); //启动采样
while ((ADC_CTRL & bADC_IF) == 0)
{
; //查询等待标志置位
}
ADC_CTRL = bADC_IF; //清标志2
Voltage = ADC_DAT * 255.0 / 4095.0; //由于voltage数据类型的原因,先乘后除可以避免出现除法之后只有0和1再乘会出现灯带闪烁的情况
/*
adc采集到的值为ADC_DAT,由于ch549是12位ADC模数转换器因此ADC_DAT为0-4095之间的数,它表达的含义是电压值的相对高低
因此 实际电压值的算法为ADC_DAT乘255除以4095以ch549所使用的亮度
*/
if (Voltage > 32 ) //voltage值大于32时打开灯
{
Set_Light(Voltage,Voltage,Voltage ); //根据voltage值赋予灯亮度
mDelaymS(30); //WS2812B需要一小段延迟将数据写入
}
else
{
Set_Light(0x00,0x00,0x00 );
mDelaymS(30);
}
}
}
整篇代码分为三个部分
第一部分:头文件及宏定义
#include <CH549_sdcc.h> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#include <CH549_DEBUG.h> //CH549官方提供库的头文件,定义了一些关于主频,延时,串口设置,看门口,赋值设置等基础函数
#include <CH549_ADC.h> //CH549官方提供库的头文件,定义了一些关于ADC初始化,采集数据等函数
#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();\
_nop();_nop();_nop();_nop();_nop();}while(0)
#define numLEDs 14 //灯的个数
其中<CH549_sdcc.h>、<CH549_DEBUG.h>、<CH549_ADC.h>均为CH549官方提供库的头文件;定义了p2.2口作为WS2812B的输入口;
#define _nop() __asm NOP __endasm
由于VSCode与Keil的语句语法差异,无法直接调用nop空运行指令,所以定义为宏,方便下面使用;
#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循环可以将宏定义的部分可以被识别为语句,方便纠错
#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();\
_nop();_nop();_nop();_nop();_nop();}while(0)
之前我们有提到WS2812B对时序要求是极高的,只有满足一个周期内的高电平和低电平时间才能准确的向其中输入1和0码。
时序波形图
数据传输时间(TH+TL=1.25μs±150ns)
本次程序已同时使用示波器和逻辑分析仪将高电平时间和低电平时间控制于要求的区间中。
(PS:这一部分可以使用函数来定义1码和0码,但是由于本身运算时间的原因低电平时间过长无法满足1码的低电平时间要求,所以采用宏代替函数从而降低运算时间,从而降低低电平时间使其满足要求)
第二部分:输入颜色函数
//发送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 );
}
//直接将颜色数据赋予灯带
void Set_Light(unsigned char x,unsigned char y,unsigned char z)
{
unsigned char i;
for ( i = 0; i < numLEDs; i++)
{
Send_2811_24bits( x , y , z );//发送显示
}
该部分对于输入的颜色值每一位进行判断,如果是1就运行之前1码的宏,如果是0就运行之前设定的0码的宏;同时将之后要输入的24位颜色值直接赋予灯带。一串24位二进制数,每种颜色8位二进制数。先是绿色,然后是红色,最后是蓝色。
第三部分:main函数
void main(void)
{
UINT8 ch;
UINT16 Voltage;
CfgFsys(); //CH549时钟选择配置
mDelaymS(20);
ADC_ExInit(3); //ADC初始化,选择采样时钟
/* 主循环 */
while (1)
{
ch = 7; //选择P1.7口作为ADC的采样口
ADC_ChSelect(ch); //选择通道
ADC_StartSample(); //启动采样
while ((ADC_CTRL & bADC_IF) == 0)
{
; //查询等待标志置位
}
ADC_CTRL = bADC_IF; //清标志2
Voltage = ADC_DAT * 255.0 / 4095.0; //由于voltage数据类型的原因,先乘后除可以避免出现除法之后只有0和1再乘会出现灯带闪烁的情况
/*
adc采集到的值为ADC_DAT,由于ch549是12位ADC模数转换器因此ADC_DAT为0-4095之间的数,它表达的含义是电压值的相对高低
因此 实际电压值的算法为ADC_DAT乘255除以4095以ch549所使用的亮度
*/
if (Voltage > 32 ) //voltage值大于32时打开灯
{
Set_Light(Voltage,Voltage,Voltage ); //根据voltage值赋予灯亮度
mDelaymS(30); //WS2812B需要一小段延迟将数据写入
}
else
{
Set_Light(0x00,0x00,0x00 );
mDelaymS(30);
}
}
}
主函数主要是旋转电位器的设定以及对之前颜色函数的调用。某种特定的颜色(RGB之一)的值越大,则这种颜色的亮度越大。如果每种颜色(RGB)都被设置为0,则LED被关闭。如果每种颜色都被设置为最大,即255,那么LED会显示为它能显示的最亮的白色。
首先使用ADC进行采集旋钮给定的值:
使用ADC_ExInit()函数进行ADC初始化。
使用ADC_ChSelect()函数选择ad通道。
使用ADC_StartSample()函数启动采样。
等待采样完成。
获得 采样后数据 ADC_DAT。
由于ch549是12位ADC模数转换器因此ADC_DAT为0-4095之间的数,它表达的含义是电压值的相对高低,因此 实际电压值的算法为ADC_DAT乘255除以4095以ch549所使用的亮度。
再设立一个判断语句灯带开关,之后就是调节灯带的亮度。
问题与反思
本文到这基本上宣告这次基于CH549单片机用旋转电位器对WS2812B灯带的控制学习结果是好的,接下来要讲的我遇到的一些问题(也许你们也可能会遇到)。
1.14盏灯出现颜色各异的现象
不知道你们在看接线图的时候有没有疑问,为什么在灯带的din口要串联一个5Ω的电阻;
如图所示,先说结论在WS2812B的输入口和CH549的输出口间串联一个5Ω左右的电阻,可以有效保护输出口。可以在示波器中明显看到,未串联电阻的时候时序的高低电平会出现很陡峭的尖峰,串联上之后尖峰趋向平缓;也正是这个原因使得14盏灯出现颜色混乱的现象。
补充:WS2812B连接电源前,可以在电源和地之间并联一个大电容,容值在100uF1000uF间最好。这个电容可以让电源输出更加平滑。WS2812的负载电流的变化范围很广,当电流上下变化时,它可以补偿电源的电流变化,保证电源的稳定输出。这个电容的作用相当于一个电源储备库,在电源充足的时候把能量储存起来。(本次学习实践中的负载电流变化相对较小,没有产生实际影响,所以没有加)
2.用逻辑分析仪的时候图像与设计逻辑相反
由于示波器的图像显示范围较小的局限性,改用逻辑分析仪对时序进行分析,更直观的了解时序,但是在测量时,前两个周期的图像正常,到第二个周期之后电平会被突然拉高,图像显示与所设计的逻辑相反的情况。
这个问题困扰我很久,与各个前辈讨论亦无果,挨个测量灯的显示,由于灯的数量选的越多越暗,考虑到是不是灯带地线断了,经检测属实。
至此灯带的控制完成。
3.使用旋转电位器控制灯带亮度时,灯带不停闪烁
对于旋转电位器的控制操作我参考的是论坛上的《模拟信号与数字信号的互相转换》,按本身的逻辑来说没有问题,但是由于选定的颜色用的是char(字符型),而原本测电压用的是float(浮点型),原本电压显示本身就带小数,所以使用起来无问题,但是颜色值的赋予中无小数,且先÷的4095导致得到的运算数据要么是1要么是0,所以会出现闪烁,因此要先×255再÷4095才能得到准确的颜色值,且255和4095也必须要写为255.0和4095.0才能真正满足要求。
至此旋转电位器的控制也完成了。
本次的项目工程已经置于Gitee仓库上,文件名是:SDCC-LIGHT
欢迎参考!