项目中使用到了ws2812灯带,作为产品的外观显示灯,经过开发之后,整理一下,仅供大家参考。
WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源。 其外型与一个5050 LED灯珠相同, 每个元件即为一个像素点。 像素点内部包含了智能数字接口数据锁存信号整形放大驱动电路, 还包含有高精度的内部振荡器和可编程定电流控制部分, 有效保证了像素点光的颜色高度一致。数据协议采用单线归零码的通讯方式, 像素点在上电复位以后, DIN端接受从控制器传输过来的数据, 首先送过来的24bit数据被第一个像素点提取后, 送到像素点内部的数据锁存器, 剩余的数据经过内部整形处理电路整形放大后通过DO端口开始转发输出给下一个级联的像素点, 每经过一个像素点的传输, 信号减少24bit。 像素点采用自动整形转发技术, 使得该像素点的级联个数不受信号传送的限制, 仅受限信号传输速度要求。高达2KHz的端口扫描频率, 在高清摄像头的捕捉下都不会出现闪烁现象,非常适合高速移动产品的使用。280μs以上的RESET时间, 出现中断也不会引起误复位, 可以支持更低频率、 价格便宜的MCU。LED具有低电压驱动、 环保节能、 亮度高、 散射角度大、 一致性好超、 低功率及超长寿命等优点。 将控制电路集成于LED上面, 电路变得更加简单, 体积小, 安装更加简便。
WS2812B主要特点:
● IC控制电路与LED点光源共用一个电源。
● 控制电路与RGB芯片集成在一个5050封装的元器件中, 构成一个完整的外控像素点。
● 内置信号整形电路, 任何一个像素点收到信号后经过波形整形再输出, 保证线路波形畸变不会累加。
● 内置上电复位和掉电复位电路。
● 每个像素点的三基色颜色可实现256级亮度显示, 完成16777216种颜色的全真色彩显示。
● 端口扫描频率2KHz/s。
● 串行级联接口, 能通过一根信号线完成数据的接收与解码。
● 当刷新速率30帧/秒时, 级联数不小于1024点。
● 数据发送速度可达800Kbps。
● 光的颜色高度一致, 性价比高。
● 电源反接不会损坏。
● 外围不需要包含电容在内的所有任何电子元器件。
首先看一下其实现原理吧,对于应用者来说,硬件电路设计只需要三根线即可(5V电源线,地线,IO数据线)。其电路模型也很简单,如下:
对于嵌入式程序开发者来说也比较简单,仅仅控制IO数据线就可以了,但是里要控制这个神奇的灯的颜色,这就需要对其数据传输的要求有所了解了。
如上图所示,当数据线传入灯带后,第一个灯珠截取第一个24位数据留做己用,而后会将其余的数据进行整形后发送给第二颗灯珠,第二颗灯珠会依次截取24位数据并对剩余数据进行整形发送,以此类推。直到最后一组数据被显示为止。每一组24位数根据数据的不同显示不同的颜色和亮度(每个像素点的三基色颜色可实现256级亮度显示, 完成16777216种颜色的全真色彩显示)。
再来看一看每个灯的24位数据,其每一位数据只能是0或1,但是其电位时序室友要个要求的,如下:
根据其电位控制要求,我们可以通过软件实现IO控制模拟0码和1码的实现。并进行数据传输。
说完了灯珠控制原理,再来谈一谈控制平台,众所周知,stm32系列的内核速率有限,考虑到指令周期的严格性,采用IO电平反转的方式来实现灯珠码型是不可靠,不现实的。
考虑到项目中实际应用的灯珠数量是比较大的(我的项目是使用270个灯珠,约4.5米的灯带),每个灯珠24bit数据,数据量每次传输量即为 270 * 24 / 8 = 810Bytes。传送800Bytes数据对于stm32来说并不苦难,但是频繁的传送可能会导致其不可靠行增大。
所以在项目中采用了TIM1+PWM+DMA的控制方式。设计思路是这样的,根据其特性可知数据发送速率最大达800kbps,根据此速率计算其周期为T= 1.25us。通过定时器实现其周期,再利用PWM实现其0码和1码,在通过DMA将数据传送出去。
废话不多说,看代码。
首先看一下定时器的初始化:
void time1andPwmInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
/* GPIO remap to TIM1*/
GPIO_PinAFConfig(GPIOE, GPIO_PinSource9, GPIO_AF_TIM1);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
/* timer period : T =(arr + 1) * (PSC + 1) / Tck. arr: period value PSC:prescaler value Tck: system clock */
TIM_TimeBaseStructure.TIM_Period = 210 - 1; /* T = (TIM_Period + 1)*(0+1)/168M = 800kHz*/
TIM_TimeBaseStructure.TIM_Prescaler = 0; /* 0 */
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; /* 0 */
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
/* PWM1 Mode configuration: Channel1 */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; /* 1 ~ TIM_TimeBaseStructure.TIM_Period */
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Disable; //TIM_OutputNState_Enable;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCNIdleState_Set;
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_Cmd(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1,ENABLE);
}
在我的工程中,我采用的是stm32f407VGT6,其内核时钟为168M,可根据时钟更改210这个值。采用的IO数据端口为E9作为TIM1_CH1输出,将其配置为PWM输出。
再来看一下DMA模块初始化:
#define TIM1_CCR1_Address (TIM1_BASE + 0x34) //0x40001034
void dmaInit(void)
{
DMA_InitTypeDef DMA_InitStructure;
/* DMA clock enable */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_DeInit(DMA2_Stream6);
/* DMA2 Stream6 Config for PWM1 by TIM1_CH1*/
DMA_InitStructure.DMA_Channel = DMA_Channel_0;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)TIM1_CCR1_Address;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)g_ledDataBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_BufferSize = 42;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
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(DMA2_Stream6, &DMA_InitStructure);
/* TIM1 DMA Request enable */
TIM_DMACmd(TIM1, TIM_DMA_CC1, ENABLE);
}
参考数据手册有关DMA模块可知。
TIM1_CH1外设只能使用DMA2_Stream6_Ch0和DMA2_Stream1_Ch6,本项目采用前者。对于此配置,最重要的是就是以下三点:
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)TIM1_CCR1_Address; //外设基地址
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)g_ledDataBuffer; //内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //传输方式:内存到映射
根据TIM1的PWM输出原理,可知我们此时要传输的外设地址为TIM1_CCR1寄存器地址。DMA内存地址即是我们传输数据的首地址。
最后看一看数据发送模块的代码:
#define TIMING_ONE (143)
#define TIMING_ZERO (67)
uint8_t rgbRed[][3] = {{0xff, 0x00, 0x00}};
void ledSingleShow(uint8_t (*color)[3], uint16_t len)
{
uint8_t i = 0;
uint16_t memaddr = 0;
uint16_t buffersize = 0;
buffersize = (len * 24) + 1; // number of bytes needed is #LEDs * 24 bytes + 42 trailing bytes
memaddr = 0; // reset buffer memory index
while (len)
{
/* green data */
for(i = 0; i < 8; i++)
{
g_ledDataBuffer[memaddr] = ((color[0][1] << i) & 0x0080) ? TIMING_ONE : TIMING_ZERO;
memaddr++;
}
/* red data */
for(i = 0; i < 8; i++)
{
g_ledDataBuffer[memaddr] = ((color[0][0] << i) & 0x0080) ? TIMING_ONE : TIMING_ZERO;
memaddr++;
}
/* blue data */
for(i = 0; i < 8; i++)
{
g_ledDataBuffer[memaddr] = ((color[0][2] << i) & 0x0080) ? TIMING_ONE : TIMING_ZERO;
memaddr++;
}
len--;
}
DMA_SetCurrDataCounter(DMA2_Stream6, buffersize);
TIM_Cmd(TIM1, ENABLE);
TIM_DMACmd(TIM1, TIM_DMA_CC1, ENABLE);
DMA_Cmd(DMA2_Stream6, ENABLE);
while(!DMA_GetFlagStatus(DMA2_Stream6, DMA_FLAG_TCIF6));
DMA_Cmd(DMA2_Stream6, DISABLE);
DMA_ClearFlag(DMA2_Stream6, DMA_FLAG_TCIF6);
TIM_Cmd(TIM1, DISABLE);
}
上述代码实际包含两部分功能,数据组成和DMA传送。
根据传入参数格式可知,参数为灯珠颜色的数组,根据其数组各元素定义,重新定义DMA要发送的内存数据。而后将DMA发送功能打开。 其实此时我们只是将不同的占空比系数发送给了TIM1->CCR1寄存器,TIM1的周期为1.25us,再此周期中,通过占空比,会实现led灯珠的0码和1码。从而实现led显示。
#define TIMING_ONE (143)
#define TIMING_ZERO (67)
至于这两个宏定义,大家根据led灯的时序波形图计算记得得出。
经过过这个小东西,发现是stm32的强大之处还是很多的,需要再接再厉。