0 上期回顾
在之前的blog中,我们成功地使用ESP8266WIFI模块连接网络并且通过API请求的方法成功地获取了当前时间,具体实现细节可以点击下方的链接: STM32F4+ESP8266拟辉光钟设计(一)简介及时间获取.
1 灯光控制的总体流程
实现灯光控制的总体流程如下图所示:
循环读取
WS2812初始化
读取RTC时钟的值,获取当前的时,分,秒
将时,分,秒的六位数字分别显示到6块WS2812灯板上
首先进行WS2812的初始化,主要目的是为传输显示数据做准备。之后不断循环读取RTC时钟寄存器里存储的时间值,并通过设计的写入函数进行数据的写入,即可按照当前的时间值进行响应灯光的点亮。在WS2812初始化部分,我们将就WS2812的基本原理、DMA传输以及设计的WS2812初始化函数分别进行介绍;在读取RTC时钟部分及显示部分,我们主要介绍WS2812的显示函数。主要框架如下图:
WS2812初始化
WS2812基本原理介绍
WS2812显示数据的SPI+DMA传输
WS2812初始化函数ws2812_init_function
读取及显示
WS2812的显示函数ws281x_showNum
总体流程
2 WS2812的初始化
2.1 WS2812简介
WS2812是一款RGB彩灯,可以根据传输数据确定其不同的R(红色)、G(绿色)、B(蓝色)值来使其显示多种颜色。在WS2812b的技术手册中的介绍如下:
WS2812B is a intelligent control LED light source that the control circuit and RGB chip are integrated in a package of 5050 components.It internal include intelligent digital port data latch and signal reshaping amplification drive circuit.Also include a precision internal oscillator and a voltage programmable constant current control part, effectively ensuring the pixel point light color height consistent.
可以看出控制一个WS2812灯的关键就是确定如何传输和怎么样传输颜色数据来使其正确的显示颜色。在技术手册中,给到控制一个WS2812灯的数据组成如下图所示: 可以看到,所需传输数据由24位组成,其中R、G、B三种颜色各占8位,取值为0-255,分别对应通用的RGB取值范围。那么写入数据时,怎么判断所传数据的相应位是0还是1呢?在嵌入式系统中,每位数据的0、1值需要由高低电平来决定,WS2812的高低电平界定如下图: 即根据高电平的持续时间来界定当前传输数据位是0还是1,具体的精准实现范围如下表(其中RESET表示清零信号): 由此可见,对于单一的灯,将一定时序的高低电平信号组合起来即可形成WS2812所需要的控制信号,实现灯光的显示,甚至可以通过简单的IO口翻转电平的方法实现,但这种方法对多个WS2812进行控制时会由于时序的不精确性导致控制混乱。
2.2 多个WS2812灯的控制
多个WS2812灯在接受数据时采用的是截取制,即传输方向的第一个灯将传输来的数据中的前24位截取给自己,并将剩余数据继续传递给第二个灯,第二个灯再截取第一个灯传给它的数据中的前24位,之后再把剩余数据向下传递,以此类推,直到最后一个灯(有点像开车时一段一段收取过路费的感觉~),数据传输图示如下: 也可以用下面的更具体的图表示: 可以看出,每个灯数据之间也有间歇280us以上的时序要求,以便区分。 对于多个灯的显示控制不能采用简单的IO口置高低电平的方法,因为时序的不准确性会导致控制的混乱。多个灯的控制有多种解决方案,大概总结为下图:
多个WS2812控制
普通IO口
SPI+DMA
PWM+DMA
...
可参考以下blog: WS2812灯珠(二)-- STM32 SPI+DMA方式驱动 WS2812灯珠(三)-- STM32 PWM+DMA方式驱动 STM32F4 SPI DMA 在本项目中,选取了SPI+DMA的方法。不论何种方法,其根本目的在于产生可靠的、标准的控制时序,并尽可能地实现对主线程无影响的高效传输。
2.3 WS2812的SPI+DMA控制
2.3.1 SPI简介及时序设计
SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,比如MSP430单片机系列处理器。
SPI是双向全双工地同步通信接口,本项目中使用它来模拟时序。根据STM32F401CCU6(主控芯片)的技术手册,可以看到其SPI接口传输的最大速度可以达到42MHz或21MHz(系统时钟的2分频),即1s传输42M或21M个位的数据: 需要注意的是在SPI通信中所说的位并不是控制WS2812的一位数据,可以理解为时长为
1
/
f
S
P
I
1/f_{SPI}
1/fSPI?的高低电平,其中SPI的频率由分频系数决定。在项目中使用SPI1接口并进行16分频来作为通信时序,并由8个SPI位来组成一个WS2812的数据位。SPI所挂时钟为APB2,频率为84MHz,16分频后频率为5.25MHz,则一个SPI位的时间为190us,根据WS2812的时序要求及8位的组成,设计出WS2812的0、1数据表示如下:
即WS2812的1由5个SPI的高电平时段和三个SPI的低电平时段组成,而WS2812的0由3个SPI的高电平时段组成。经过计算会发现这种时序并不是严格满足技术文档里的要求,但经过实验验证是可以的。可以参考下面的这篇文章进行时序的设计: 【STM32】WS2812介绍、使用SPI+DMA发送数据
2.3.2 DMA简介
DMA,全称为:Direct Memory Access,即直接存储器访问。直接存储器存取( DMA )用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须 CPU 干预,数据可以通过 DMA 快速地移动,这就节省了 CPU 的资源来做其他操作。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工作。DMA 传输对于高效能嵌入式系统算法和网络是很重要的。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路, 能使 CPU 的效率大为提高。(摘自https://blog.csdn.net/weixin_44524484/article/details/105671273)
DMA方法为不间断地高效传输灯光控制数据提供了可能。相当于在SPI接口和存储器之间修建了一条快速的专用通道,将存储器中的灯光控制数据绕过CPU的监管直接输出到外设接口上。(这种传输比中断要高效些,因为不需要断点恢复与保护等多余操作。)在STM32F401CCU6的技术手册中,对DMA的描述如下: 在初始化时,需要注意使用的是DMA1还是DMA2,以及SPI接口对应的DMA通道数。
2.4 WS2812初始化函数ws2812_init_function()
2.4.1 SPI1的初始化
函数定义如下:
void ws2812_init_function(DMA_Stream_TypeDef *DMA_Streamx,u32 chx,u32 par,u32 mar,u16 ndtr);
参数名 | 内容 |
---|
DMA_Stream_TypeDef *DMA_Streamx | DMAx,Stream n DMA的选取和Stream的种类 | u32 chx | 通道数 | u32 par | SPI1对应地址 | u32 mar | 所传数据存储地址 | u16 ndtr | 所传数据位数 |
SPI初始化部分,和传统一致:
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
2.4.2 DMA初始化
SPI初始化部分,和传统一致,根据所传参数决定初始化哪个DMA的哪个Stream的哪个通道:
DMA_InitTypeDef DMA_InitStructure;
if((u32)DMA_Streamx>(u32)DMA2)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);
}
else
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);
}
DMA_DeInit(DMA_Streamx);
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}
DMA_InitStructure.DMA_Channel = chx;
DMA_InitStructure.DMA_PeripheralBaseAddr = par;
DMA_InitStructure.DMA_Memory0BaseAddr = mar;
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_BufferSize = ndtr;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA_Streamx, &DMA_InitStructure);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
2.4.3 初始化函数调用
根据技术手册中DMA定义: 可以看到SPI1_TX对应数据流3的通道三,故在main()函数中初始化如下:
ws2812_init_function(DMA2_Stream3,DMA_Channel_3,(u32)&SPI1->DR,(u32)SendBuffTO,WS2812NUM*24);
其中WS2812NUM为灯珠数量:
#define WS2812NUM 120
3 读取RTC时钟部分及显示
3.1 WS2812的显示函数ws281x_showNum()
项目中用到的WS2812灯板原理图如下图所示:
当时没有自己画PCB板,是直接从网上购买的(因为尺寸什么的都一致),购买链接如下: 效哥小店TaoBao 在上一篇文章中提到时钟数字的显示是通过灯光打在亚克力板上数字的划痕处产生折射从而使相应数字突出出来来实现的。根据原理图可以看到灯板由自上至下的十个横排组成,分别对应上方的9-0十个数字,故需要进行设计,使灯板根据当前时间,使相应数字对应的两个WS2812灯亮起来。 实物图片如上图所示。例如,若当前的秒数的最后一位为0,则需要使原理图上标号为LED16和LED16的灯亮起,同时板子上的其他灯均熄灭。在程序中,使用一个二维数组来存放所有灯珠的显示数据:
u8 SendBuffTO[WS2812NUM][24];
数组中的每一行包括24位数据,表示一个灯的颜色与亮灭(若全传入RESET信号则改灯熄灭)。时钟显示的数字为6个,包括小时两个、分钟两个、秒钟两个,可以将数组按照显示的数字分为6个区域,各个区域的构成是一定的,所以设置一个数字的算法完全可以应用到其他数字上,只不过添加一个偏移项即可:
数字 | 数组编号 | offset |
---|
小时十位 | 0-19 | 0 | 小时十位 | 20-39 | 20 | 小时十位 | 40-59 | 40 | 小时十位 | 60-79 | 60 | 小时十位 | 80-99 | 80 | 小时十位 | 100-119 | 100 |
据此,构造显示函数:
void ws281x_showNum(int num,int offset,int flag);
各参数解释如下:
参数名 | 内容 |
---|
int num | 显示的数字 | int offset | 偏移 | int flag | 识别不同的处理方式,即个位需要到9才回0,十位则需要到6才回0,同时小时的十位要到2回0 |
void ws281x_showNum(int num,int offset,int flag)
{
int blue=(int)(rand()/32768)*255;
int red=(int)(rand()/32768)*255;
int green=(int)(rand()/32768)*255;
offset=offset*20;
if(num==0)
{
ws281x_setPixelRGB(5+offset,red,green,blue);
ws281x_setPixelRGB(15+offset,red,green,blue);
if(flag==1)
{ws281x_setPixelRGB(4+offset,0,0,0);
ws281x_setPixelRGB(14+offset,0,0,0);}
if(flag==0)
{ws281x_setPixelRGB(2+offset,0,0,0);
ws281x_setPixelRGB(12+offset,0,0,0);}
}
if(num==1)
{
ws281x_setPixelRGB(0+offset,red,green,blue);
ws281x_setPixelRGB(10+offset,red,green,blue);
ws281x_setPixelRGB(5+offset,0,0,0);
ws281x_setPixelRGB(15+offset,0,0,0);
}
if(num==2)
{
ws281x_setPixelRGB(6+offset,red,green,blue);
ws281x_setPixelRGB(16+offset,red,green,blue);
ws281x_setPixelRGB(0+offset,0,0,0);
ws281x_setPixelRGB(10+offset,0,0,0);
}
if(num==3)
{
ws281x_setPixelRGB(1+offset,red,green,blue);
ws281x_setPixelRGB(11+offset,red,green,blue);
ws281x_setPixelRGB(6+offset,0,0,0);
ws281x_setPixelRGB(16+offset,0,0,0);
}
if(num==4)
{
ws281x_setPixelRGB(7+offset,red,green,blue);
ws281x_setPixelRGB(17+offset,red,green,blue);
ws281x_setPixelRGB(1+offset,0,0,0);
ws281x_setPixelRGB(11+offset,0,0,0);
}
if(num==5)
{
ws281x_setPixelRGB(2+offset,red,green,blue);
ws281x_setPixelRGB(12+offset,red,green,blue);
ws281x_setPixelRGB(7+offset,0,0,0);
ws281x_setPixelRGB(17+offset,0,0,0);
}
if(num==6)
{
ws281x_setPixelRGB(8+offset,red,green,blue);
ws281x_setPixelRGB(18+offset,red,green,blue);
ws281x_setPixelRGB(2+offset,0,0,0);
ws281x_setPixelRGB(12+offset,0,0,0);
}
if(num==7)
{
ws281x_setPixelRGB(3+offset,red,green,blue);
ws281x_setPixelRGB(13+offset,red,green,blue);
ws281x_setPixelRGB(8+offset,0,0,0);
ws281x_setPixelRGB(18+offset,0,0,0);
}
if(num==8)
{
ws281x_setPixelRGB(9+offset,red,green,blue);
ws281x_setPixelRGB(19+offset,red,green,blue);
ws281x_setPixelRGB(3+offset,0,0,0);
ws281x_setPixelRGB(13+offset,0,0,0);
}
if(num==9)
{
ws281x_setPixelRGB(4+offset,red,green,blue);
ws281x_setPixelRGB(14+offset,red,green,blue);
ws281x_setPixelRGB(9+offset,0,0,0);
ws281x_setPixelRGB(19+offset,0,0,0);
}
}
其中ws281x_setPixelRGB()的作用是使第n个WS2812灯显示为指定颜色。
3.2 RTC时钟读取及显示
首先,通过库函数读取RTC时钟的寄存器并将数据分解到暂存变量中:
RTC_TimeTypeDef RTC_TimeTypeInitStructure;
RTC_GetTime(RTC_Format_BIN,&RTC_TimeTypeInitStructure);
flags0=RTC_TimeTypeInitStructure.RTC_Seconds%10;
flags1=RTC_TimeTypeInitStructure.RTC_Seconds/10;
flagm0=RTC_TimeTypeInitStructure.RTC_Minutes%10;
flagm1=RTC_TimeTypeInitStructure.RTC_Minutes/10;
flagh0=RTC_TimeTypeInitStructure.RTC_Hours%10;
flagh1=RTC_TimeTypeInitStructure.RTC_Hours/10;
之后将新得到的数据与之前的数据进行对比,如果变化了,则更新WS2812灯的显示:
if(flags0!=befores0){
ws281x_showNum(RTC_TimeTypeInitStructure.RTC_Seconds%10,5,1);
befores0=flags0;}
if(flags1!=befores1){
ws281x_showNum(RTC_TimeTypeInitStructure.RTC_Seconds/10,4,0);
befores1=flags1;}
if(flagm0!=beforem0){
ws281x_showNum(RTC_TimeTypeInitStructure.RTC_Minutes%10,3,1);
beforem0=flagm0;}
if(flagm1!=beforem1){
ws281x_showNum(RTC_TimeTypeInitStructure.RTC_Minutes/10,2,0);
beforem1=flagm1;}
if(flagh0!=beforeh0){
ws281x_showNum(RTC_TimeTypeInitStructure.RTC_Hours%10,1,1);
beforeh0=flagh0;}
if(flagh1!=beforeh1){
ws281x_showNum(RTC_TimeTypeInitStructure.RTC_Hours/10,0,0);
beforeh1=flagh1;}
最后,通过WS2812的显示函数进行显示:
ws281x_show();
之后发现,采用秒中断的方法会更加精准些!
至此,代码部分就基本结束啦!
4 材料准备
4.1 时钟框架、支撑材料
根据购买的WS2812灯板的尺寸和设计需求进行了3D模型的绘制,以下为底板图和亚克力板两侧固定用的零件图: 底版图 零件图 二者均用SolidWorks进行绘制------感谢一下帮我画的兄弟mrj。
4.2 亚克力板
在淘宝上进行了亚克力板的定做,尺寸是30 X 60 X 2mm,这是定做时的草图:
4.3 PCB板设计
由于在MCU与ESP8266、WS2812之间用杜邦线连接不稳定,且占用空间较大,自行设计了PCB板并进行打样: 原理图如下所示: 添加了滑动电阻模块来调节显示亮度,电压越低则颜色偏暖色。
5 组装调试
组装好后,效果如下图所示: 最终效果(还是不错的hhh):
6 改进
外观设计需要再加把劲儿!各位大佬如果有好的设计想法欢迎与我交流! 之后还可以添加那种随着音乐律动的功能(尝试一下) 还可以再智能化点儿,比如说试着实现通过手机APP控制当前颜色什么的,也请大家多多指点!
总之,谢谢大家的支持,如有错误欢迎大家批评指正!码字不易,也请各位看官点个赞赞~
ps:如果需要源码的可以私聊我
|