以前,一直很疑惑,那些挂在树上的LED灯条,是如何实现流水的效果的。灯条是如此的窄,不可能放下很多的信号线,除非是串行的连接。受限于知识面,一直不太清楚为什么可以做到,也算是懒,一直没去查这方面的资料。
直到有一天,突然发现了某个村产的带锁存器的5050 LED灯珠,才恍然大悟,原来如此。
这段时间,由于要做开关面板,并且面板上需要有背光灯,于是就想起了这个神奇的5050灯珠。。于是就将其画在了原理图上。
原理图很简单,针对我们使用的WS2812C目前使用5V供电,每颗的VCC脚附近,需要放一颗104电容,因为灯珠内部还有数字电路。另外,就是DI,DOUT的串行数据线。连接在一起即可。
至于前面的电阻电容,只是用来防静电的一些常用措施。
不同于网络上常用的WS2812B,我们购买的是WS2812C,也得感谢立创,其实为我们这些小公司提供了很大的便利。特别是研发阶段的物料购买上。
在软件上,整体时序如下:
1. 系统上电,WS2812操作口,保持低电平。
2. 开始发送数据,空闲时,IO口为低电平
3. 数据发送完毕后,维持低电平,超过规格书上定义的RESET时间。特别解释下,只要低电平时间超过规格书上的时间(280uS),即可认为是reset,而不需要先拉高,再拉低。
如下是官方提供的整体时序图。我在网络上搜了很多的BBS和教程,其实大家都很专注在每个bit的实现上,而多数都忽略了整体的时序。也有可能是我的思维方式的问题,我一般喜欢从整体到细节的去掌握一个东西。了解了整体时序后,在去看字节组成,再去看位如何实现。
另外,右图是流程图。先发数据,后发RESET波形。
如下是每24bit的组成。注意,顺序不是RGB888,而是GRB888。一般我们取颜色的数值,都是RGB顺序,所以这里在代码里实现的时候,会需要做一下移位。另外,需要注意的是,需要高位先发(MSB)。
可能是以前的移位寄存器用多了,当发现最近的WS2812锁存的是第一帧的24bit数据,后面的灯珠依次类推,才发现,这颗灯珠做的其实挺人性化的。这点比以前的移位寄存器好多了。
另外,内部自带整型,所以挂长条灯带,也不怕波形畸变了。只能说,虽然是村里出厂的产品,其实考虑了挺多的因素在里面了。然后,就是确定用什么样的波形,来表示bit。波形以及时间参数如下:
用什么样的方法去表示bit的波形,网络上的方法有很多。例如PWM,也有用SPI。。由于是第一次驱动2812,就先别折腾,我暂时就先用IO口模拟来实现吧。
目前选择的MCU是ST新出的STM32G070,主频设置为64MHz。WS2812的IO口连接在PA7上。另外需要注意的是,STM32是3V的电平,WS2812是5V电平,因此这里需要有一颗常用的电平转换芯片,我这里用的是SN74LVC1T45DBVR。电路如下所示:
1. 在GPIO口初始化时,将PA7设置为输出,并且输出为0.
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;//GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF0_SPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
2.定义存储颜色的缓冲区,我这里定义了两个数组。一个是存储原始RGB颜色的数组,另外一个是将RGB拆分成GRB的数组。另外,我的板子上总共有8颗2812,因此定义如下:
#define LED_COUNT 8
unsigned long RGBBuffer[LED_COUNT];
unsigned char GRBBuffer[LED_COUNT*3];
void WS2812_Initial_IO(void) //一个初始化函数:
{
for(unsigned int i = 0; i < LED_COUNT; i++)
{
RGBBuffer[i] = 0;
}
WS2812_Set_Led_Color(0, 0);
}
//送入的颜色是RGB,所以内部需要转换一下,Index为第几个灯,RGB是颜色参数
void WS2812_Set_Led_Color(unsigned char Index, unsigned long RGB)
{
unsigned long GRB = 0;
GRB = (RGB << 8) & 0x00ff0000;
GRB |= ((RGB >> 8) & 0x0000ff00);
GRB |= (RGB & 0x000000ff);
RGBBuffer[Index] = GRB;
for(unsigned char i = 0; i < LED_COUNT; i++)
{
GRBBuffer[i*3] = RGBBuffer[i] >> 16;
GRBBuffer[i*3 +1] = RGBBuffer[i] >> 8;
GRBBuffer[i*3 + 2] = RGBBuffer[i] >> 0;
}
GPIOA->MODER &= 0xffff3fff; //normal gpio
GPIOA->MODER |= 0x00004000;
unsigned char i;
unsigned char j = 0;
unsigned char Dat = 0;
for(j = 0; j < 3*LED_COUNT; j++)
{
Dat = GRBBuffer[j];
for(unsigned char i = 0; i < 8; i++)
{
if(Dat & 0x80)
{
GPIOA->BSRR = (uint32_t)GPIO_PIN_7;
__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();
GPIOA->BRR = (uint32_t)GPIO_PIN_7;
__NOP(); __NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();
}
else
{
GPIOA->BSRR = (uint32_t)GPIO_PIN_7;
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();
GPIOA->BRR = (uint32_t)GPIO_PIN_7;
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();
}
Dat = Dat << 1;
}
}
HAL_Delay(1); //等待复位结束
}
主函数调用方法如下:
WS2812_Initial_IO();//熄灭所有灯
WS2812_Set_Led_Color(0, 0x0f0000); //第一盏为红灯,较暗
WS2812_Set_Led_Color(1, 0xf00000); //第二盏为红灯,较亮
WS2812_Set_Led_Color(2, 0x00ff00); //第三盏为绿灯,全亮
WS2812_Set_Led_Color(3, 0x0000ff); //第四盏为蓝灯,全亮
WS2812_Set_Led_Color(4, 0x00ffff); //第五盏为蓝白灯,全亮
从以上的代码里,也可以看出来,灯珠的亮度,是可调的,通过调整送入的数值来调整。
第一步就先做到IO口模拟来控制,后面可以尝试用SPI+DMA来模拟试试看。只要整体时序对了,也有参考了,那么下一步就会比较容易一些了。