文章目录

前言

上一篇文章中介绍了整个板子的最基本功能模块——使用GPIO的通用输入输出实现简单的按键输入以及推挽输出控制的功能。本文深入一步,在只使用GPIO的输入输出功能的基础上,通过查看对应模块的芯片手册,模拟其对应的通信时序来驱动对应的模块。

WS2812B

首先来个网红模块——WS2812B的彩灯,它在RGB灯的邻域可以说是一方霸主的存在,内部集成了驱动,可以实现三色(255 _ 255 _ 255=16777216种颜色)的全真色彩,且支持串行控制,单片机可以通过一个GPIO实现对一组灯的控制。详细的特征可以看芯片手册的介绍。




1.模块简介



通过手册的产品概述,可以看出其大致的控制逻辑:
1.单个灯需要一个24位的数据来控制;
2.同时控制多个点时,需要根据串联的灯个数来发送对应个数的24bit控制数据;
3.其复位时间需要280us以上的时间。



上面提到了一个24bit的控制数据,那么这个24bit的数据每一位代表的含义是什么呢?
在数据手册中也有介绍,如下图所示:24bit的数据是由 绿 红 蓝 三个颜色的色度数据拼接而成的,当数据是0xFF 00 00时,对应的灯会亮起绿色,当数据是0x 00 FF 00时小灯是红色,当数据是0x 00 00 FF 时小灯是蓝色。根据数值的不同可以组合出不同的颜色。



这里给个颜色表供大家去查找自己想要的颜色——https://tool.oschina.net/commons?type=3



需要注意的是,写入的24bit数据的顺序是G R B (绿红蓝),而在此查询到的颜色数据是R G B(红绿蓝),在控制时需要调换一下顺序。
那么到此是不是就可以开始写代码了呢?


2.时序介绍

答案是否定的,为了提高数据传输的稳定性,尽可能的消除信号传输过程中的干扰;模块会采取特殊的输出格式来区分电平的0与1。在上一篇的GPIO输入输出中,STM32的“0”就是对应低电平,“1”就是对应高电平,但是对于WS2812B来说,“0”与“1”对应的并不是单纯的高低电平,而是需要根据如下的时序波形图:



直接看这个描述可能会有点抽象,这里笔者用逻辑分析仪抓取了一段波形,我们一起结合上面的描述来分析一下:



这是使用STM32模拟时序来控制一个RGB显示113355的蓝色的数据脚的实际采样波形。根据上面的时序波形图做个对照,



可以依次读出整段波形发送的数据内容是
0011 0011 0001 0001 0101 0101
分别对应三个颜色 G R B ()绿 红 蓝
而波形的最开头和最后结尾的一个很长的低电平就是对应的复位信号。
最终逻辑分析仪解析出来的数据如下图所示:



至此,就已经基本上那个搞清楚了WS2812B单个灯的通信时序了。唯一还没确定的数据是发送0和1分别对应的具体高低电平时间,时序图中给定的是一个范围,实际使用时需要自己进行微调,这个我们放到后面的代码编写中再定。
搞定了单个灯的控制,接下里就是多个灯的串行控制了



根据上图的数据传输逻辑,可以知道,控制多个灯时,只需要连续发送多组24位数据就可以了,比如说要控制八个灯,就一次发送8个24位数据,中间不断,当需要修改时,在最后加一个大于280us的复位信号即可。
下图中,红色部分是实际发送数据的波形,中间的低电平端就是复位信号。



为了方便理解,我们将上图中中间的那一段红色波形给放大一点,此时就可以看出,一次发送了两个24bit的控制数据,此时会有两个串联的小灯被点亮。



好了,基本捋清控制的思路了,接下来再来看看手册中的其他需要注意的参数。


3.硬件介绍

每个灯珠都是如下所示的四个脚位,其中,VDD与VSS是提供电源的,DIN是输入信号,而DOUT则是数据输出脚,需要一次控制多个灯时,只需要将前一个灯的数据输出脚与后一个灯的数据输入脚连接在一起即可实现串行控制。这里我们可以对照引脚介绍图,找到对应的管脚,方便使用逻辑分析仪抓取波形以及其他的调试。




4.传输速率,以及帧数要求

手册里的其他内容大部分都和硬件相关了,这里不做赘述,需要的同学自己去查看,这里还有两个编程需要注意的重要参数,一个就是但刷速录大于30帧/秒时,至少要有1024个灯,而我们这里只有八个灯,所以1s内控制的灯数据包不能超过30个。
另一个就更重要了,数据的发送速度可达800Kbps

插补一点小知识:bps是bit/s ,叫做比特率,也叫码率就是每秒可以传输的数据位数
而baud/s,是波特率,字节每秒的意思,也就是一秒可以传输多少个字节的数据。(一个字节等于八位数据位)。



也就是说,WS2812B最高支持的数据包传输速率是1s接收800 000 位个2进制的数据,换算下来就是每个二进制位最少最少要保持1/800 000=1.25us的时间,还记得前面的时序图吗,单个数据“0”或者单个数据“1”是由不同的高低电平时间段组合成的,也就是说,单个数据“1”或者单个数据“0”的高低电平时间之和不能少于1250ns。
T0H+T0L>1250ns
T1H+T1L>1250ns



搞清楚这些以后,差不多就可以开始写代码了。
在这之前还需要去瞧一眼原理图,找到这个板子中WS2812B的控制引脚,通过原理图可以看出,控制脚是GPIOA_8。



为啥选用PA8呢,这里笔者做了两手准备,WS2812B的常用驱动方式有PWM、SPI来配合DMA的高效率方式,这种驱动方案不占用CPU的资源;另外一种就是使用纯GPIO模拟输出的方案,为了兼顾,所以笔者选了个有复用功能的IO口。只不过,我们这里灯少,8个灯,选则软件模拟的方案也没啥大问题,加上这里的四个定时器都有他用了,为了避免干扰,所以最终选择软件模拟的方案来实现。想看PWM+DMA或者SPI+DMA的方案的可以自己去搜索一下哈,我看CSDN很多大佬都是用的这两种方案,讲的也很细致。



另外就是供电部分,这里笔者选用的是5V供电。


代码
1.初始化

根据上面的总结,这里选用GPIO模拟时序图的方案来进行,首先既要输出高又要输出低,所以GPIOA8需要配置为通用推挽输出模式,这个不再赘述了。

/*********************************
函数名:RGB_Init
函数功能:RGB初始化
形参:void
返回值:void
备注:
RGB-----PA8--------通用推挽输出
**********************************/
void RGB_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;//定义一个结构体的变量
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode =  GPIO_Mode_Out_PP;//通用推挽输出
	GPIO_Init(GPIOA,&GPIO_InitStructure );
}

为了方便操作,这里最好是进行宏定义输出高输出低这里需要注意,结合上一篇的介绍,我们知道,控制GPIO的输出可以使用库函数,也可以使用位带操作,除此之外还有一种执行效率更高,时延更低的方案,直接使用寄存器来驱动。这里笔者把三种宏定义都贴出来,原因是,这个模块它设计到ns的延时,多运行一条代码都会跑过30ns-50ns,这是会严重影响控制效果的。三种宏定义大家自己选择一种即可,最快的是寄存器的,然后位带操作与库函数的差不多。


//位带操作
#define RGB_H   PAout(8)=1
#define RGB_L   PAout(8)=0
//库函数
#define RGB_H   GPIO_SetBits(GPIOA,GPIO_Pin_8)
#define RGB_L   GPIO_ResetBits(GPIOA,GPIO_Pin_8)
//直接操作寄存器(最快)
#define RGB_H   GPIOA->ODR |= (1<<8)
#define RGB_L   GPIOA->ODR &= ~(1<<8)

2.模拟时序

搞定了初始化以后,就该开始最重要的部分了,对照时序图,模拟出时序。
对于这类需要模拟时序的模块,我们一定要自底向上的思想,从最基础的发送“0”,发送“1”开始写,将发0与发1封装成对应的函数;除此之外还需要根据模块的实际需求封装复位、初始化之类的最底层函数,这里WS2812B就还需要一个复位的函数。封装好这些函数后,再来根据时序进行数据包发送的函数封装,进而拼凑出整个模块的时序代码。


1.复位函数

先来最简单的,复位函数,根据数据手册介绍,复位电路的时序就是将信号脚拉低,拉低时间不少于280us,这里我们给个350us。这里的延时是us级的,所以可以使用系统滴答的us延时来实现,系统滴答是所以延时中最准的了,定时器都没他准,所以这里最好是用系统滴答,关于系统滴答的介绍估计得放到和GPIO复用也就是定时器那一部分一起介绍,这里先用。

/*********************************
函数名:RGB_Reset
函数功能:RGB复位信号
形参:void
返回值:void
备注:
**********************************/
void RGB_Reset(void)
{
	RGB_L;
	Systick_Delay_us(350);//调用系统滴答来实现
}

2.发送0、1码

首先需要解决的是ns的延时问题,STM32F103C8T6的主频是72MHZ,也就是1s可以运行72 000 000条机器指令。系统滴答和定时器只能提供us级的延时,这个ns延时只能采用运行固定数量的机器指令来进行了。
计算过程如下:
1s 可以运行72 000 000 条机器指令,那么
1us 可以运行 72条机器指令,换句话说,CPU执行72个机器指令花费的时间是1us。那么1个机器指令花费的时间就是1us/72=13.88889ns;也就是说,CPU每运行一个机器指令,时间过去了13.8889ns。
然后再来看具体的时序图:



这里0码的高电平时间T0H是要在220ns~380ns之间的,而低电平时间T0L要控制在580ns-1600ns之间,而且T0H+T0L>1250ns
这里我们先定T0H,折中,选择300ns,
300/13.889=21.5999,四舍五入,取22个机器指令,
那么问题来了,我们平时都是用的C代码,一条C代码等于一个机器指令吗?
答案是否定的,一条C代码与机器指令之间没有固定的关系,这是因为每条C代码的底层汇编代码都不一样。但是好在留有专门的机器指令nop()
一个
nop()就是一个机器指令。


于是得到了第一个延时:

//220-380 ns折中 300 13.89*22=305.5558
void delay_300ns(void)  //72 000 000MHZ  ==1s  72hz 1us  一个机器指令周期要耗时13.89ns
{
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();
}

然后来解决第二个延时还是采取去中间值的方案,取1090ns,
1090/13.889=78.4,四舍五入取78个


于是得到第二个延时

//折中 1090 78*13.889
void delay_1090ns(void)  //72 000 000MHZ  ==1s  72hz 1us  一个机器指令周期要耗时13.8ns
{
	__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();
	__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();
}

于是0码的发送代码就有了

/*********************************
函数名:RGB_Send0
函数功能:RGB发送0
形参:void
返回值:void
备注:
**********************************/
void RGB_Send0()
{
	RGB_H;
	delay_300ns();
	RGB_L;
	delay_1090ns();
}

依次类推可以退出1码的发送,延时可以就用上面的,不过最好是再弄一个delay_320ns的。实测delay_320的效果会好一点。

/*********************************
函数名:RGB_Send1
函数功能:RGB发送1
形参:void
返回值:void
备注:
**********************************/
void RGB_Send1()
{
	RGB_H;
	delay_1090ns();
	RGB_L;
	delay_320ns();
}

3.封装发送函数

前面介绍过24bit的数据分别对应着GRB的8位,所以这里先整个8位数据的发送函数,高位先发,于是可以得到如下代码:

/*********************************
函数名:RGB_Send_Data
函数功能:RGB发送8位数据
形参:u8 data需要发送的数据
返回值:void
备注:
**********************************/
void RGB_Send_Data(u8 data) 
{ 
   uint8_t i;
   for(i=8;i>0;i--) 
   {
     if(data & 0x80)//按位与,为真发送1,为假发送零
     {
        RGB_Send1();
     }
     else
     {
        RGB_Send0();
     }
     data <<=1;//
   }
}

然后再来封装一个发送24bit的函数

/*********************************
函数名:Send_GRB
函数功能:GRB发送24位数据
形参:u8 G,u8 R,u8 B
返回值:void
备注:
**********************************/
void Send_GRB(uint8_t G,uint8_t R,uint8_t B)
{
	RGB_Send_Data(G);
	RGB_Send_Data(R);
	RGB_Send_Data(B);
	RGB_Reset();
}

有了这个函数就已经可以实现单个灯的控制了,





然后为了控制后面的灯,我们需要封装一个控制多个灯的函数,根据前面分析的时序,
代码如下:

/*********************************
函数名:Continuous_Set_LED
函数功能:设置n个灯为 GRB的颜色
形参:u8 G,u8 R,u8 B
返回值:uint8_t n多少个灯
,uint32_t GRB设置的颜色
备注:
**********************************/
void Continuous_Set_LED(uint8_t n,uint32_t GRB)
{
	while(n--)
	{
		RGB_Send_Data((GRB>>16)&0xFF);
		RGB_Send_Data((GRB>>8)&0xFF);
		RGB_Send_Data((GRB>>0)&0xFF);
	}
	RGB_Reset();
}

调用这个函数就可以实现对指定个数的灯实现控制了,当然进一步还可以做流水,滚动,等等功能,这个有需要的就去查阅一下其他大佬的代码吧。





总结

至此,关于WS2812B的模拟驱动就介绍完了,文如有不足欢迎批评指正,下一篇继续使用模拟时序完成对DHT11的温湿度数据获取,先放个逻辑分析抓取的数据波形解析在这里供大家参考。