我有一个WS2812B炫彩灯环,搭配精选的背景,非常出镜:
在玩过的板子上,我都要把它点亮。
关于WS2812B的介绍资料,网上有很多,这里就不细说了,只说重点:
- WS2812B是单总线控制的:
控制一颗WS2812B与控制多颗WS2812B,方式是一样的,不同的只是每批传送的数据的多少。
我上面的这个灯环,就是24颗串联在一起,第一颗的DIN负责接收控信号,然后每一颗的DOUT,接下一颗的DIN,将控制信号传递过去,直到最后一颗。这种方式,可以连接上千颗一起控制。
需要注意的是,最后一颗的DOUT是留空的。所以上面这个炫彩灯环,首尾是不相连的。
- WS2812B控制一颗灯珠,需要24bits的数据,代表着GRB三种颜色值:
控制多颗,则使用多组连续的24bits数据:
每颗WS2812B截取数据中最开始的24bits,然后把剩下的传递给后来者,直到数据发送完毕。
- 控制设备,不能直接发送这些bit位的信号,而是要按照一定的规则发送信号,才被检测为对应的bit位:
WS2812B规定了三种信号:0码、1码、reset码,这三种码,通过信号线上的高低电平的特定保持时间来做区分,具体如下:
也就是:
- 1个码的信号时间长度位1.25us
- 如果高电平保持0.4us,低电平保持0.85us,则判定为T0码
- 如果高电平保持0.8us,低电平保持0.45us,则判定为T1码
- 如果低电平持续至少50us,则判定位RESET码;例如传递第二批控制信号时,需要使用该码
上述数据,如图表所示,都有一定的容错范围,经过以往的测验经验,最终使用如下的值:
- T0:高0.25us,低1.00us
- T1:高1.00us,低0.25us
- RESET:低50us
要实现0.25us的时间控制精度,那么换算成频率位:1/(0.25/1000000) = 4M。也就是控制设备,需要以4Mbits/s这么快的速度,来控制信号的变化,才能满足WS2812B控制信号的要求。
要满足上述的要求,可以有很多种方法。常用的有GPIO翻转,使用SPI的MOSI发送数据等。
有人可能有疑问,UART、I2C发送数据,也能带来信号线的高低电平变化,哪能使用吗?
UART的传送速度,通常波特率最高位1.5Mbits/s;I2C通信,超高速的能达到5M,但一般都达不到,通常可能为400kbits/s,快的位3.4Mbits/s。
而SPI,通常速度都能达到50Mbits/s。
但使用GPIO翻转来控制信号,并非所有的的设备都能达到,有的翻转的速率没有这么快。
经过实测,咱们的GD32F427开发板完全可以满足。所以,这篇分享,就直接使用GPIO翻转这种简单的方式来做了。
在实际使用中,有很多大佬,也研究了各种各样优化的方法,大家感兴趣的话,可以查找资料了解一下。
通过查看原理图和数据手册,可以使用PC1来进行控制,该引脚一般情况下没有直接复用:
具体接线如下:
因为我只打算一次点亮一颗灯珠,所以供电部分,直接使用板载的3.3V、GND即可。
如果需要点亮多颗灯珠,那么应该使用外部电源供电。点亮一颗灯珠的一种颜色,需要20mA电流,三种颜色都点亮,则需要60mA电流,24颗全部点亮,则需要1440mA电流。一般开发板的GPIO,是供不起的全部点亮的。
在前一篇文章Systick系统定时器的使用 种,探讨了SysTick的基础使用,可以达到us级别的精确控制。不过亚us级别的控制,就会有一些吃力了。
在MCU的精确时间控制中,还有一种使用nop指令来进行控制的。所谓nop,就是一条空指令,一个最小的机器周期。通过一定数量的nop,从而实现亚us级别的控时。
GD32F427开发板的运行频率高低200MHz,经过实测,每200个nop,刚好经过1us,也就是每个nop为0.005us。那么要达到0.25us的控时,就需要50个nop。
在system_gd32f4xx.c中,有系统时钟的定义:
在 core_cmInstr.h 中,有关于nop的符号定义:
那么我们要定义0.25us,可以参考使用如下的方式:
#define 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.25us,1.00us,就分别为1个NOP和4个NOP。
经过以上的准备工作,就可以编写实际的控制代码了,具体代码如下:
/*!
\file main.c
\brief GPIO running WS2812B demo
\version 2022-11-24, V1.0.0, demo for GD32F4xx
*/
#include "gd32f4xx.h"
#include "gd32f427v_start.h"
#include "systick.h"
#include <stdio.h>
#define 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(); \
}
void GPIO_Init(void)
{
rcu_periph_clock_enable(RCU_GPIOC); //使能外部时钟
gpio_mode_set(GPIOC, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_1); //配置端口模式
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1); //输出选项配置
gpio_bit_reset(GPIOC, GPIO_PIN_1); //PC1复位
}
#define LOW 0
#define HIGH 1
#define DIN PC1
#define NUM 24
//拉低DIN保持50us以上
void ws2812_init() {
unsigned char i;
gpio_bit_reset(GPIOC, GPIO_PIN_1);
for (i = 0; i <= 200; i++) {
NOP;
}
}
//高电平0.25us,低电平1us
void ws2812_write_0() {
gpio_bit_set(GPIOC, GPIO_PIN_1);
NOP;
gpio_bit_reset(GPIOC, GPIO_PIN_1);
NOP;
NOP;
NOP;
NOP;
}
//高电平1us,低电0.25us
void ws2812_write_1() {
gpio_bit_set(GPIOC, GPIO_PIN_1);
NOP;
NOP;
NOP;
NOP;
gpio_bit_reset(GPIOC, GPIO_PIN_1);
NOP;
}
// 写入24bits
void ws2812_write_24bits(unsigned long dat) {
unsigned char t[NUM] = {0};
unsigned char i;
for (i = 0; i < NUM; i++) {
if (dat >> i & 1) {
t[i] = HIGH;
} else {
t[i] = LOW;
}
}
for (i = 0; i < NUM; i++) {
if (t[i]) {
ws2812_write_1();
} else {
ws2812_write_0();
}
}
}
void ws2812_test() {
unsigned int i = 0;
unsigned int j = 0;
unsigned int index = 1;
unsigned long colors[8] = {0x000000, 0xff0000, 0x00ff00, 0x0000ff,
0xffff00, 0xff00ff, 0x00ffff, 0xffffff};
ws2812_init();
while (1) {
for (i = 0; i < NUM; i++) {
if (index % NUM == i) {
ws2812_write_24bits(colors[index % 7 + 1]);
} else {
ws2812_write_24bits(colors[0]);
}
}
index++;
if (index >= NUM) {
index = 0;
}
// 延时0.1秒
for (i = 0; i <= 200*50*100; i++) {
NOP;
}
}
}
int main(void)
{
systick_config(); //配置系统时钟
GPIO_Init();
ws2812_test();
}
上述代码中,关键调用说明如下:
- gpio_bit_set(GPIOC, GPIO_PIN_1):设置高电平
- gpio_bit_reset(GPIOC, GPIO_PIN_1):重置为低电平
- ws2812_init():重置WS2812B控制,拉低,保持50us
- ws2812_write_0():输出T0码,高0.25us,低0.25us
- ws2812_write_1():输出T1码,高1.00us,低0.25us
- ws2812_write_24bits():批量生成和输出T0、T1
- ws2812_test():使用index自增,表示当前需要点亮的灯珠,一次发送24组数据,每组24bits,且使用colors预定义8种颜色,colors[0]实际上表示熄灭。
编译以上代码,然后下载到开发板中,就可以看到炫彩的灯光,在灯环上欢快的跳跃了: