一、WS2812B资料
很多博客都有中文的DATASHEET,自行搜索,本文为英文DATASHEET截图。
1.硬件参数
绝对最大额定值与电气特性
根据参数,使用5V电压供电即可;输入的电流1uA,IO口可以直接驱动,但若想驱动多个灯珠,最好还是独立供电。高电平最小值2.7V,低电平最大值0.7V,使用IO口直接做控制输入可以满足。
2.硬件连接
下图为DataSheet中的应用电路
请将“DIN”信号连接到单片机上有定时器通道的引脚。前一个WS2812B的“DOUT”连接到下一个WS2812B的“DIN”。
二、驱动方式选择
1.数据传输与组成
首先看一眼数据传输方式,D1、D2、D3、D4代表按顺序排列的WS2812B灯珠。
可以看到,第三个WS2812B灯珠接收到了第三个24bit的数据,
故推断第一个WS2812B灯珠先收到第一个24bit的数据后,做出响应(发光);
再收到第二个24bit的数据后,直接转发给第二个WS2812B灯珠,由第二个WS2812B灯珠做出响应;
依次类推。
做不同颜色的灯光显示时,注意数据传输的先后。
那么这24bit的数据内容是什么呢?下图为该24bit数据的组成。
根据描述,数据发送的顺序是GRB,高位先发。
每8bit代表了WS2812B灯珠对此种颜色光发光的强度,故耀眼的绿、红、蓝纯色光对应十六进制的编码分别为:0xff0000(绿)、0x00ff00(红)、0x0000ff(蓝)。
2.数据编码
有了编码信息,再看看WS2812B灯珠对0/1码的接收方式是什么。下图为WS2812B的编码时序图。
其中的数据传输时间如下图所示。
可以看到,WS2812B编码所需的时间精度为ns级别,定时器中断不合适了;stm32的时钟周期在72mhz的系统时钟下为13.89ns,使用systemtick做delay也可以实现,但时间不会很精确,且需要考虑不同的指令周期对编码的影响,粗糙又繁琐。
WS2812B的0/1编码很像一个周期内不同占空比的波形,可以考虑PWM控制,但此时又出现一个问题:
如何在不改变频率的情况下在几个周期内连续改变占空比?
通常我们是用库函数中TIM_SetCompare Y(TIM_TypeDef* TIM X, uint16_t Compare Z)来改变,但由于该函数执行时间很长,影响了编码时序,故不能采用此方法。
另一种方式是直接改变定时器CCRx寄存器的值,此时又出现一个问题:
如何在不到1us的时间内改变CCRx寄存器值?
知识点来了:STM32中,有影子寄存器的预装载寄存器有三个,分别是:分频寄存器PSC,自动重装载ARR,自动捕获CCRx,我们读写访问的是这几个寄存器的值。CCRx寄存器的值会放到其影子寄存器中,然后定时器按照此比较值运行。
当我们使能了预装载寄存器后(TIMx_CCMRy寄存器的OCzPE位为0 ),当PWM执行完一个周期后,产生更新事件(更新事件UEV:在定时器的计数值溢出时,会产生一次事),将CCRx预装载寄存器中下一半字(uint16_t)的值赋给影子寄存器。参考:STM32xx参考手册的"捕获/比较模式寄存器 1(TIMx_CCMR1)"章节。
知道了这点后,我们可以在确定24bit的颜色编码后,得到对应位的0/1编码比较值,用数组传输给CCRx。然后启动PWM,即可得到占空比不断变化的PWM波。
注意到STM32在定时器部分有DMA功能后,便可以尝试将数据传输部分交给DMA了。
三、软件驱动编写
我测试使用的MCU为STM32f103c8t6,定时器使用是TIM2_CH2,移植时需要注意函数及实参使用相对应的。附图通道对应的DMA,作为移植参考。
其他废话不多说,直接上代码,注释我写的很清楚。
头文件:HDL_WS2812B.h
/*******************************************************************************
* @file HDL_WS2812B.h
* @author huiyang
* @version V1.0
* @date 01/21/2022
******************************************************************************/
#ifndef _HDL_WS2812B_H
#define _HDL_WS2812B_H
#include "stm32f10x.h"
#define WS2812B_PIN GPIO_Pin_1
#define WS2812B_PORT GPIOA
#define WS2812B_HIGH GPIO_SetBits(WS2812B_PORT,WS2812B_PIN)
#define WS2812B_LOW GPIO_ResetBits(WS2812B_PORT,WS2812B_PIN)
/* 根据DataSheet定义0/1编码对应的高电平比较值,因为高电平在前,定义高电平的即可
** 高低电平的比例约为2:1
*/
#define WS2812B_ARR 90 //TIM2的自动重装载值
#define T0H 30 //0编码高电平时间占1/3
#define T1H 60 //1编码高电平时间占2/3
/* 灯珠亮的个数 */
#define LED_NUM 100 //底层驱动未用,为应用方便,先加上
#define DATA_SIZE 24 //WS2812B一个编码的bit数,3*8
void PWM_WS2812B_Init(uint16_t arr);
void WS2812B_Reset(void);
void PWM_WS2812B_Write_24Bits(uint16_t num,uint32_t GRB_Data);
void PWM_WS2812B_Show(uint16_t num);
void PWM_WS2812B_Red(uint16_t num);
void PWM_WS2812B_Green(uint16_t num);
void PWM_WS2812B_Blue(uint16_t num);
#endif
c文件:HDL_WS2812B.c
/*******************************************************************************
* @file HDL_WS2812B.c
* @author huiyang
* @version V1.0
* @date 01/21/2022
******************************************************************************/
#include "HDL_WS2812B.h"
#include "delay.h" //延时函数加自己对应的头文件,在复位处使用
/* 单个灯珠的编码对应的比较值数组 */
uint16_t Single_LED_Buffer[DATA_SIZE*LED_NUM];
/*
*作用:通用定时器2 TIM2_CH2 PWM输出+DMA初始化
*参数:arr:自动重装值
*说明:定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
* Ft=定时器工作频率,单位:Mhz
*/
void PWM_WS2812B_Init(uint16_t arr)
{
//结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
DMA_InitTypeDef DMA_InitStructure;
//使能RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能PORTA时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //使能TIM2时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); //使能DMA1时钟
//初始化GPIO口
GPIO_InitStructure.GPIO_Pin = WS2812B_PIN; //GPIO口
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(WS2812B_PORT,&GPIO_InitStructure); //根据指定的参数初始化GPIO口
//初始化TIM2
TIM_TimeBaseStructure.TIM_Prescaler=0; //定时器分频:(0+1)=1,不分频
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseStructure.TIM_Period=arr; //自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分割
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); //根据指定的参数初始化TIM2
//初始化TIM2 Channel2 PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
//TIM_OCInitStructure.TIM_Pulse = 0; //待装入捕获比较寄存器的脉冲值(此程序不用加初值)
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC2Init(TIM2, &TIM_OCInitStructure); //根据指定的参数初始化外设TIM2 Channel2
/* 此处为知识点所描述处的对应代码,一定要有。移植时注意0C2代表定时器通道2*/
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); //使能TIM2在CCR2上的预装载寄存器
TIM_Cmd(TIM2, DISABLE); //失能TIM2,防止第一个脉冲异常
TIM_DMACmd(TIM2, TIM_DMA_CC2, ENABLE); //使能TIM2_CH2的DMA功能(CC2对应通道2)
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(TIM2->CCR2); //设置DMA目的地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Single_LED_Buffer; //设置DMA源地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //方向:从内存SendBuff到内存ReceiveBuff
DMA_InitStructure.DMA_BufferSize = DATA_SIZE; //一次传输大小DMA_BufferSize=SendBuffSize
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //ReceiveBuff地址不增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //SendBuff地址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //ReceiveBuff数据单位,16bit
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //SENDBUFF_SIZE数据单位,16bit
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //DMA模式:正常模式(传输一次)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级:中
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //内存到内存的传输
DMA_Init(DMA1_Channel7, &DMA_InitStructure); //配置DMA1的7通道(不同定时器的通道不一样)
DMA_Cmd(DMA1_Channel7, DISABLE); //失能DMA1的7通道,因为一旦使能就开始传输
}
//复位灯带
void WS2812B_Reset(void)
{
TIM_Cmd(TIM2, DISABLE);
WS2812B_LOW;
delay_ms(1); //此处延时时间最小值大于50us即可
}
//写数据编码
void PWM_WS2812B_Write_24Bits(uint16_t num,uint32_t GRB_Data)
{
uint8_t i,j;
for(j = 0; j < num; j++)
{
for(i = 0; i < DATA_SIZE; i++)
{
/*因为数据发送的顺序是GRB,高位先发,所以从高位开始判断,判断后比较值先放入缓存数组*/
Single_LED_Buffer[i] = ((GRB_Data << i) & 0x800000) ? T1H : T0H;
}
}
}
//点亮灯珠
void PWM_WS2812B_Show(uint16_t num)
{
/* 移植时此处对应的通道和中断标志都需要更改 */
DMA_SetCurrDataCounter(DMA1_Channel7, num*DATA_SIZE);
DMA_Cmd(DMA1_Channel7, ENABLE);
TIM_Cmd(TIM2, ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC7) != SET);
DMA_Cmd(DMA1_Channel7, DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC7);
TIM_Cmd(TIM2, DISABLE);
}
//N个灯珠发红光
void PWM_WS2812B_Red(uint16_t num)
{
PWM_WS2812B_Write_24Bits(num,0x00ff00);
PWM_WS2812B_Show(num);
}
//N个灯珠发绿光
void PWM_WS2812B_Green(uint16_t num)
{
PWM_WS2812B_Write_24Bits(num,0xff0000);
PWM_WS2812B_Show(num);
}
//N个灯珠发蓝光
void PWM_WS2812B_Blue(uint16_t num)
{
PWM_WS2812B_Write_24Bits(num,0x0000ff);
PWM_WS2812B_Show(num);
}
测试主函数
#include "stm32f10x.h"
#include "delay.h"
#include "HDL_WS2812B.h"
int main(void)
{
SystemInit();
delay_init(72);
PWM_WS2812B_Init(WS2812B_ARR);
while(1)
{
WS2812B_Reset();
PWM_WS2812B_Red(1); //灯珠个数可修改
PWM_WS2812B_Green(1);
PWM_WS2812B_Blue(1);
}
}
四、测试效果
总结
至此,WS2812B灯珠的简单驱动就编写完成了,其他颜色以及亮灯效果可以在此基础上任意发挥。