1,WS2812B和WS2811F简介
RGBLED驱动芯片有两个常用的选择:WS2812B和WS2811F,它们都采用单线归零码的通讯方式,数据发送速度可达800Kbps。
芯片在上电复位以后,DIN端接受从控制器传输过来的数据,首先送过来的24bit数据被第一个芯片提取后,送到芯片内部的数据锁存器,剩余的数据经过内部整形处理电路整形放大后通过DO端口开始转发输出给下一个级联的芯片,每经过一个芯片的传输,信号减少24bit。芯片采用自动整形转发技术,使得该芯片的级联个数不受信号传送的限制,仅仅受限信号传输速度要求。
当各个芯片都收到24bit的数据后,我们再发送一个大于280us的低电平复位信号,所有的芯片再同步发送对应的R、G、B信号,来控制RGBLED灯的红色、绿色和蓝色三种发光二极。我们就可以看见RGBLED灯亮不同的颜色了。
特别说明:WS2812B和WS2811F通讯和级联方式等,都基本一样,只是24位数据结构有先后区别。
图1 WS2812B或WS2811F级联示意图
图2 WS2812B或WS2811F数据传输方法
24位的数据结构(WS2812B):按GRB顺序,高位在前
图3 24位的数据结构(WS2812B)
24位的数据结构(WS2811F):按RGB顺序,高位在前
图4 24位的数据结构(WS2811F)
对我们的驱动来说,图3和图4是WS2812B和WS2811F的唯一区别,其他部分的代码编写是一模一样的。
WS2812B和WS2811F逻辑0、逻辑1和复位信号的时序如下:
图5 WS2812B和WS2811F逻辑0、逻辑1和复位信号的时序图
表1 WS2812B和WS2811F数据传输时间表
实际使用中,PWM的占空比为三分之一代表逻辑0,PWM的占空比为三分之二代表逻辑1,这个时间大致即可。
文章开头说了,数据发送速度可达800Kbps,即每一个数据位传输时间就是:1 / 800000s = 1.25us。每个RGB灯数据有24位,所以每一个RGB数据传输时间就是:1.25 * 24us = 30us。
假设用1000颗RGB灯组成一个矩阵屏,那么给屏幕刷一帧数据的时间为:1000*30+280us = 30280us,即矩阵屏的帧率为33帧每秒。这个帧率看个视频是够了,打游戏可能差点意思,哈哈哈。
特别注意:级联的灯珠太多,要考虑供电问题。一个LED电流大概是10~20mA,一个RGB灯有三个LED,即30~60mA。
2,实验硬件简介
2.1,RGB灯方案
WS2811F方案的RGB灯,我们使用的是正点原子的BEE BLOCK系列的RGBLED模块。该模块设计非常美观,灯珠够大,显示效果很好,预留了灯珠级联接口。
WS2812B方案的RGB灯,我自己画了一块由20颗RGB灯级联的灯板。注意WS2812B芯片是集成到RGB灯内部了。
图6 BEE BLOCK系列RGBLED模块(左) 和 20颗RGB灯的灯板(右)
2.2,主控方案
主控制器使用STM32F103C8T6,这里直接使用正点原子的STM32入门套件BEE BLOCK系列,价格虽然贵一些,但是外观和品质是真的很好。
图7 正点原子的STM32入门套件BEE BLOCK系列
2.3,仪表工具
仪表工具使用正点原子的DL16逻辑分析仪,方便观察WS2812B和WS2811F的时序。
图8 正点原子的DL16逻辑分析仪
3,实验代码
3.1,WS2812B驱动代码
TIM_HandleTypeDef g_tim2_handle = {0}; /* 定时器x句柄 */
DMA_HandleTypeDef g_dma_handle = {0}; /* DMA句柄 */
/* 用于存放10个RGBLED灯的颜色值 */
uint32_t g_rgb888_color[10] =
{
GRB888_RED, /* 红色 */
GRB888_GREEN, /* 绿色 */
GRB888_BLUE, /* 蓝色 */
GRB888_VIOLET, /* 紫罗兰 */
GRB888_YELLOW, /* 黄色 */
GRB888_IRED, /* 浅红色 */
GRB888_ORANGE, /* 橙色 */
GRB888_PURPLE, /* 紫色 */
GRB888_PING, /* 粉色 */
GRB888_CYAN, /* 青色 */
};
uint16_t g_pixel_buf[LED_NUM][DATA_SIZE]; /* 用于存放RGBLED灯数据对应的比较值 */
/**
* @brief 发送数据给ws2812b
* @param 无
* @retval 无
*/
void ws2812b_dats_send(void)
{
/* 使用DMA将数据发送到定时器的CCRx */
HAL_TIM_PWM_Start_DMA(&g_tim2_handle, TIM_CHANNEL_3, (uint32_t *)g_pixel_buf, sizeof(g_pixel_buf));
}
/**
* @brief 复位ws2812b
* @note 保持低电平280us以上即可
* @param 无
* @retval 无
*/
void ws2812b_reset(void)
{
/* CCRX设置为0,直接输出低电平 */
__HAL_TIM_SetCompare(&g_tim2_handle, TIM_CHANNEL_3, 0);
delay_us(280);
}
/**
* @brief 将GRB888的颜色值转换成逻辑1和逻辑0对应的CCRx值并存放到
* g_pixel_buf里,并发送给CCRx,生成PWM
* @note 可指定第几个灯
* @param led_num : 需控制灯的数量
* @param color : GRB888颜色值指针
* @retval 无
*/
void ws2812b_display(uint8_t led_num, uint32_t *color_buf)
{
uint8_t i, j;
if(led_num > LED_NUM)return; /* 防止写入LED数量大于LED总数 */
/* 这里是对 g_pixel_buf[i][j]写入一个周期内高电平的持续时间
(是要存放在比较值寄存器CCR的值)*/
for(i = 0; i < led_num; i++)
{
for(j = 0; j < DATA_SIZE; j++)
{
g_pixel_buf[i][j] =
(((color_buf[i] << j) & 0x800000) ? HIGH_LEVEL : LOW_LEVEL);
}
}
ws2812b_dats_send(); /* 将g_pixel_buf发送到定时器的CCRx中,生成相应波形 */
}
/**
* @brief ws2812b不显示颜色,复位状态
* @param 无
* @retval 无
*/
void ws2812b_show_black(void)
{
uint32_t rgb_buf[LED_NUM];
for(int j = 0; j < LED_NUM; j++)
{
rgb_buf[j] = GRB888_BLACK;
}
ws2812b_display(LED_NUM, rgb_buf); /* RGBLED灯光显示 */
}
/**
* @brief 初始化ws2812b相关IO口,以及配置定时器和DMA功能 并使能时钟
* @param 无
* @retval 无
*/
void ws2812b_init(void)
{
TIM_OC_InitTypeDef timx_oc_pwm_chy = {0}; /* 定时器输出句柄 */
GPIO_InitTypeDef gpio_init_struct = {0};
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启通道y的GPIO时钟 */
__HAL_RCC_TIM2_CLK_ENABLE(); /* 使能定时器时钟 */
gpio_init_struct.Pin = GPIO_PIN_2; /* 通道的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
g_tim2_handle.Instance = TIM2; /* 定时器x */
g_tim2_handle.Init.Prescaler = (1 - 1); /* 定时器分频 */
g_tim2_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_tim2_handle.Init.Period = (90 - 1); /* 自动重装载值 */
g_tim2_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
g_tim2_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_PWM_Init(&g_tim2_handle); /* 初始化PWM */
timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM1 */
timx_oc_pwm_chy.Pulse = 0; /* 设置比较值,此值用来确定占空比 */
timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH; /* 输出比较极性为高 */
timx_oc_pwm_chy.OCIdleState = TIM_OCIDLESTATE_RESET; /* 空闲时输出低电平 */
HAL_TIM_PWM_ConfigChannel(&g_tim2_handle, &timx_oc_pwm_chy,TIM_CHANNEL_3);
/* 使能CCRX的影子寄存器,当修改CCRX的值时,等产生更新事件在生效 */
__HAL_TIM_ENABLE_OCxPRELOAD(&g_tim2_handle, TIM_CHANNEL_3);
g_dma_handle.Instance = DMA1_Channel1; /* DMA1通道1 */
g_dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; /* 从内存到外设模式 */
g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; /* 16位 */
g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 16位 */
g_dma_handle.Init.Mode = DMA_NORMAL; /* 普通模式 */
g_dma_handle.Init.Priority = DMA_PRIORITY_HIGH; /* 高优先级 */
HAL_DMA_Init(&g_dma_handle);
/* 将DMA与timer联系起来 */
__HAL_LINKDMA(&g_tim2_handle, hdma[TIM_DMA_ID_CC3], g_dma_handle);
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
ws2812b_show_black();
}
/**
* @brief DMA1通道1中断服务函数
* @param 无
* @retval 无
*/
void DMA1_Channel1_IRQHandler(void)
{
if(__HAL_DMA_GET_FLAG(&g_dma_handle, __HAL_DMA_GET_TC_FLAG_INDEX(&g_dma_handle)) != RESET)
{
__HAL_DMA_CLEAR_FLAG(&g_dma_handle,
__HAL_DMA_GET_TC_FLAG_INDEX(&g_dma_handle));
ws2812b_reset();
/* 传输完成后手动停止PWM */
HAL_TIM_PWM_Stop_DMA(&g_tim2_handle, TIM_CHANNEL_3);
}
}
ws2812b.h代码如下:
/**************************************************************************/
/* RGBLED的数据高低电平逻辑定义 */
#define HIGH_LEVEL (uint16_t)60 /* CCR3为占空比为三分之二代表逻辑1 */
#define LOW_LEVEL (uint16_t)30 /* CCR3为占空比为三分之一代表逻辑0 */
#define LED_NUM 20 /* 灯珠的个数 */
#define DATA_SIZE 24 /* 一个RGB数据的位数,3*8 */
/**************************************************************************/
/* RGB888颜色值 */
#define GRB888_RED 0x00FF00 /* 红色 */
#define GRB888_GREEN 0xFF0000 /* 绿色 */
#define GRB888_BLUE 0x0000FF /* 蓝色 */
#define GRB888_BLACK 0x000000 /* 黑色 */
#define GRB888_WHITE 0xFFFFFF /* 白色 */
#define GRB888_YELLOW 0xFFFF00 /* 黄色 */
#define GRB888_IRED 0x5CCD5C /* 浅红色 */
#define GRB888_ORANGE 0xA5FF00 /* 橙色 */
#define GRB888_PURPLE 0x008080 /* 紫色 */
#define GRB888_PING 0xB6FFC1 /* 浅粉色 */
#define GRB888_CYAN 0xFF00FF /* 青色 */
#define GRB888_PBLUE 0x80008C /* 孔雀蓝 */
#define GRB888_VIOLET 0x008BFF /* 紫罗兰 */
/**************************************************************************/
extern TIM_HandleTypeDef g_tim2_handle; /* 定时器x句柄 */
extern uint32_t g_rgb888_color[10];
/* 函数声明 */
void ws2812b_init(void); /* 初始化W2812B */
void ws2812b_reset(void); /* 复位W2812B */
/* 写入多个颜色值到W2812B(控制多个灯) */
void ws2812b_display(uint8_t led_num, uint32_t *color);
uint32_t color_change_brigh(uint32_t rgb, float k); /* 改变颜色的亮度值 */
void ws2812b_show_black(void); /* 不显示颜色,关灯 */
3.2,WS2811F驱动代码
WS2811F的代码和ws2812b几乎一样,只有RGB数据结构顺序上有区别。下面把有差异的代码贴出来。
WS2811F.c有差异部分代码如下:
/* 用于存放10个RGBLED灯的颜色值 */
uint32_t g_rgb888_color[10] =
{
RGB888_RED, /* 红色 */
RGB888_GREEN, /* 绿色 */
RGB888_BLUE, /* 蓝色 */
RGB888_VIOLET, /* 紫罗兰 */
RGB888_YELLOW, /* 黄色 */
RGB888_VRED, /* 朱红色 */
RGB888_ORANGE, /* 橙色 */
RGB888_PURPLE, /* 紫色 */
RGB888_PGREEN, /* 孔雀绿 */
RGB888_CYAN, /* 青色 */
};
WS2811F.h有差异部分代码如下:
/**************************************************************************/
/* RGB888颜色值 */
#define RGB888_RED 0xFF0000 /* 红色 */
#define RGB888_GREEN 0x00FF00 /* 绿色 */
#define RGB888_BLUE 0x0000FF /* 蓝色 */
#define RGB888_BLACK 0x000000 /* 黑色 */
#define RGB888_WHITE 0xFFFFFF /* 白色 */
#define RGB888_YELLOW 0xFFFF00 /* 黄色 */
#define RGB888_VRED 0xFF4D00 /* 朱红色 */
#define RGB888_ORANGE 0xFFA500 /* 橙色 */
#define RGB888_PURPLE 0x800080 /* 紫色 */
#define RGB888_PING 0xFFC0CB /* 粉色 */
#define RGB888_CYAN 0x00FFFF /* 青色 */
#define RGB888_PGREEN 0x00A15C /* 孔雀绿 */
#define RGB888_VIOLET 0x8B00FF /* 紫罗兰 */
/**************************************************************************/
3.3,main.c代码
int main(void)
{
uint8_t i = 0, j = 0;
uint32_t rgb_buf[LED_NUM]; /* 各灯珠RGB颜色缓冲区 */
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
ws2812b_init(); /* 初始化RGBLED */
while (1)
{
/* 多个灯珠亮相同颜色,10种颜色 */
for(i = 0; i < 10; i++)
{
for(j = 0; j < LED_NUM; j++)
{
rgb_buf[j] = g_rgb888_color[i];
}
ws2812b_display(LED_NUM, rgb_buf); /* RGBLED灯光显示 */
delay_ms(500);
}
}
}
在本实验中,我们根据连接的灯珠数量设置好LED_NUM宏定义。
mian函数中,实现多个灯珠亮相同颜色,共10种颜色。
详细的代码可以下载正点原子BEE BLOCK系列RGBLED模块的资料查看。网址如下:http://www.openedv.com/docs/boards/stm32/beeblock.html
4,实验现象
4.1,WS2812B实验现象
图9 WS2812B实验现象
4.2,WS2811F实验现象
图10 WS2811F实验现象
4.3,用DL16逻辑分析仪观察PWM
WS2812B数据结构是GRB888,所以红色数据是0x00FF00。
图11 WS2812B时序
WS2811F数据结构是RGB888,所以红色数据是0xFF0000。
图12 WS2811F时序
PWM详细参数如下图:
图13 PWM时序信息
PWM周期为1.25us,逻辑1高电平时间为850ns,低电平时间为400ns。所以可以知道逻辑0高电平时间为400ns,低电平时间为850ns。