点灯,是玩板子的精髓所在。
灯亮的那一刻,只感觉人生的乐趣不过如此。
待到点完灯,板子就可以放墙角嘎啦的箱子里珍藏,直到多年以后一个失眠的夜晚,再翻出来细细品味。
我特别喜欢点灯,玩过的板子,都要好好点一遍灯。
我尤其喜欢用我那个宝贝WS2812B灯环点灯,当五彩的灯光在灯环上跳跃闪动的时候,我会用眼睛盯着灯环上的灯光,眼球随着光亮的流动而转动,慢慢的陷入沉思:灯环的背后,会不会有另一个世界,会不会也有一位玩家,正在点灯!
如果你也想要这样沉思,想要深邃的思考人生,那么,来吧,跟着我一起来点灯吧!
一、硬件材料
- D1s开发板:DongshanPI-D1s开发板
- WS2812B2B:24颗炫彩灯环,DFRobot版本的
- 连接线:杜邦线若干,需要2.0转2.54线;手头没有转接线,直接用测试钩
二、WS2812B了解:
WS2812B炫彩灯环实物如下:
WS2812B是个好东西,1根信号线,就能点上千颗炫彩灯珠,日常生活中非常的常见。
每颗灯珠的颜色,为RGB,每个颜色的取值范围是0~255,所以总共可以设置的颜色为256256256多达16万种颜色,是不是很炫彩。
在上图中,有IN接口和OUT接口,IN接口用于接开发板,OUT接口用于接下一个灯环。就这样,可以一个一个的串联起来,生生不息。
实际上,在这个灯环上的24颗灯珠,每一颗也是有一个IN、一个OUT,然后一个接一个的串联起来的。1颗,2颗,。。。,一直到1000颗,都是这么串联起来的。
单颗WS2812B灯珠:
多颗WS2812B灯珠:
不过需要注意的是,WS2812B工作电压为5V,而每个LED分别点亮红色、绿色和蓝色的时候,需要大约20mA,或者在全亮度下每个LED总共60mA,如果需要点同时点亮多颗LED的话,电源需要提供足够大的电流。如果直接使用开发板5V供电,那么最好不要点较多,以免发生意外。
而如果需要点亮多颗,或者全部点亮,IN上面的电源接口,是可以使用独立电源供电的,和开发板接好供地就成。我之前实际使用中,就使用了明纬的电源供电。
WS2812B的1根信号线传输数据的方式,非常有意思,你通过外设接口发过去的数据,它不是直接接收的,它需要通过特定的方式放松数据,并进行解码,转换为实际需要的。
它要解码,就要接受数据,需要如下图所示的规则:
上图中的H、L,表示高、低电平。当按照规定的要求,持续指定时间的高电平,再持续指定时间的低电平,这样依次一高一低,WS2812B的控制IC,就认为传送过来了1 bit的数据。根据高低电平持续时间的长段,认定为发送的是0(T0),还是1(T1)。
连续8次这样的高低电平组合,就被接收到8 bits,会被组装成1Byte,这1 Byte,就是一种颜色的控制数据,其值就是二进制的00000000 ~ 11111111(无符号),也就是十进制的0~255。
3组这样的连续8次高低电平组合,也就是一共24次高低电平组合,最终被解码为3个Bytes,正好对应RGB三种颜色。
实际的颜色顺序,是GRB,如下图所示:
如果要控制多颗,那么久连续发送符合上面规则的数据即可,如下图所示:
如上图,要控制多少颗,WS2812B就需要收到多少个24bit的数据。收到数据后,第1颗灯珠把第1组数据给取走,然后把剩下的,在通过OUT信号接口,传给第2颗,第2颗灯珠,又把第1组数据给取走。。。依次方式,直到所有的数据被取完。
要是有人看过 人体蜈蚣 这部恶心至极的电影的话,就会发觉,嗨,还真有点那个味儿
一组灯珠的单次控制,像上面那样发送数据即可。那么多次控制的话,就需要在两组数据之间,间隔至少50us,再发送下一批数据即可。
了解其解码的规则了,我们再回到原点,怎么给他发送数据呢?
我们通过UART、I2C、SPI等传输数据,本质上,在信号线上,也是高低电平的变化。
如果你手头有逻辑分析仪,那么可以分析例如UART的通信:
如果你持续发送0xff,那么就会一直是高电平;如果持续发送0x00,那么就会一直是低电平。
如果你发送0x55,那么对应的二进制就是01010101,高低电平就会交替。
如果你改变发送的波特率,那么你就会发现,高低电平保持的时间,会发生变化。
正是因为如此,我们可以以特定的波特率,通过串口发送特定的数据,就可以形成符合WS2812B所需要的高低电平规则了。
在控制板上,发送数据,波特率或者速度,一般都是一次发送1 Byte来计算的。
WS2812B的1 bit,对应控制板的1 Byte,需要在1.25us内发送完成,据此可以计算出来发送频率:
1/1.25us = 6,400,000 HZ = 6.4MHz
很显然,串口不符合要求。通常串口传输数据,波特率高的,也才1500000,不足1.5M/s,达不到要求。
串口不行,那I2C呢?
I2C 常见的速度模式:速度快速模式(400 kbit/s)、快速+模式(1 Mbit/s)高速模式(3.4 Mbit/s)超高速模式(5 Mbit/s)。显然,就算是超高速模式的I2C也差那么一丢丢。
那串口不行,I2C不行,那用程序,控制GPIO口,一会高电平,一会低电平,不就行了吗?
控制GPIO口一会高电平,一会低电平,确实可以。不过,这需要能高速翻转的GPIO口才行。如果有逻辑分析仪,可以试试翻转速度能搞快到多少。
经过一番排查,SPI非常适合,据说高速SPI速度能够达到 200Mbps,那是相当的快。普通的,跑个10Mbps,也不成问题。而咱们D1s开发板的SPI,完全没有问题。
不过,这里要注意的是,我们使用这个SPI接口,并不是要用SPI数据通信的方式,来和WS2812B通信,我们只需要使用SPI接口的MOSI,来从开发板,向WS2812B发送数据即可,其他的,可以一概不管了。
而要形成符合WS2812B解码规则的高低电平持续时间,可以通过计算得到:
备注:上述WS2812B1 bit对控制板1字节(8 bits)的方案,是一个较为基础且方便理解的方案,实际上,很多大佬能够在控制板上使用更少的bits来发送数据,达到WS2812B的规则要求。
三、原理图
通过查看DongshanPI-D1s开发板的原理图,了解到SPI接口使用,并设计线路连接:
在上图核心板的右下位置,可以看到:
PD12:MOSI
三、实物接线
如果要准备点亮所有的灯,那么最好采用外接电源供电,和开发板共地即可。
如果只点亮几颗的话,那么可以直接接到开发板上的5V输出即可。
四、编译固件
参考 DongshanPI-D1s开发板使用教程之SHT30温湿度传感器(I2C)数据读取 进行。
默认编译的固件中,并没有可供使用的SPI设备,我们需要对设备树进行一点小的修改,开启SPI设备。
经过世玉轩大佬以及社区多位大佬的指点,顺利搞定SPI设备树的修改,具体如下:
文件:/sdk/tina-d1-h/lichee/linux-5.4/arch/riscv/boot/dts/sunxi/sun20iw1p1.dtsi
/sdk/tina-d1-h/device/config/chips/d1s/configs/nezha_nor/board.dts
经过上面的配置,然后再重新编译烧录即可。
五、SPI设备检测
DongshanPI-D1s开发板启动后,参考 DongshanPI-D1s开发板使用基础文档【编译、烧录、adb、gpio-led、c】 使用OTG接口通过adb连接,再进行下面的操作。
执行ls -l /dev/spi*
查看当前的spi设备:
可以看到,当前可用的SPI设备为:/dev/spidev1.0
六、编写代码
参考上述WS2812B数据解码的规则,以及网上众多大佬们的文章,编写了如下的程序:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <time.h>
#include <math.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#define TRST 0x00
#define TOL 0x80
#define TOH 0xf8
#define NUM 24
unsigned char default_tx_reset[] = {
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST,
TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST, TRST};
unsigned char default_tx0[] = {
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL};
unsigned char default_tx1[7][24] = {
{
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL
},
{
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
},
{
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH
},
{
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
},
{
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH
},
{
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH
},
{
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH}
};
unsigned char default_rx[ARRAY_SIZE(default_tx0)] = {
0,
};
int main(int argc, char **argv)
{
int fd;
char dev_node[16];
int mode = 1, bits =8, speed = 6400 * 1000;//6400000;//ceil(8 / 1.25e-6);
int ret = 0;
unsigned char tx_buf[1000], rx_buf[1000];
struct spi_ioc_transfer tr;
int i, t1, t2;
if(argc<2) {
perror("[Info] Please input spidev id.");
return -1;
}
sprintf(dev_node, "/dev/spidev%d.0", atoi(argv[1]));
fd = open(dev_node, O_RDWR);
if (fd < 0)
{
perror("[Error] Open dev_node error.");
return -1;
}
/* Mode */
ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
if (ret < 0)
{
perror("[Error] SPI_IOC_WR_MODE error.");
return -1;
}
ret = ioctl(fd, SPI_IOC_RD_MODE, &mode);
if (ret < 0)
{
perror("[Error] SPI_IOC_RD_MODE error.");
return -1;
}
/* bpw */
ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret < 0)
{
perror("[Error] SPI_IOC_WR_BITS_PER_WORD error.");
return -1;
}
ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
if (ret < 0)
{
perror("[Error] SPI_IOC_RD_BITS_PER_WORD error.");
return -1;
}
/* speed */
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret < 0)
{
perror("[Error] SPI_IOC_WR_MAX_SPEED_HZ error.");
return -1;
}
ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
if (ret < 0)
{
perror("[Error] SPI_IOC_RD_MAX_SPEED_HZ error");
return -1;
}
printf("spi = %s\n", dev_node);
printf("Mode = %d\n", mode);
printf("Bpw = %d\n", bits);
printf("Baudrate = %d\n", speed);
tr.tx_buf = (unsigned long)default_tx0;
tr.rx_buf = (unsigned long)default_rx;
tr.len = sizeof(default_tx0);
tr.delay_usecs = 0;
tr.speed_hz = speed;
tr.bits_per_word = bits;
printf("[Info] len=%d speed_hz=%d\ndata: 0=[", tr.len, tr.speed_hz);
for (int i = 0; i < tr.len; i++)
{
printf("%02X ", default_tx0[i]);
}
printf("]\ndata: 1=[");
for (int i = 0; i < tr.len; i++)
{
printf("%02X ", default_tx1[0][i]);
}
printf("]\n");
int index = 0;
int delay = 100;
while (1)
{
if(NUM==1){
if(index%8==0) {
ret = write(fd,default_tx0,ARRAY_SIZE(default_tx0));
if (ret < 1)
printf("[Error] can't send tx0 message ret=%d\n", ret);
usleep(1000*1000);
} else {
ret = write(fd,default_tx1[index%8-1],ARRAY_SIZE(default_tx1[0]));
// ret = write(fd,default_tx0,ARRAY_SIZE(default_tx0));
if (ret < 1)
printf("[Error] can't send tx1 message ret=%d\n", ret);
usleep(1000*1000);
}
index++;
} else {
for(int i=0;i<NUM;i++){
if(i==(index%NUM)){
ret = write(fd,default_tx1[index%8-1],ARRAY_SIZE(default_tx1[0]));
if (ret < 1)
printf("[Error] can't send tx1 message ret=%d\n", ret);
} else {
ret = write(fd,default_tx0,ARRAY_SIZE(default_tx0));
if (ret < 1)
printf("[Error] can't send tx0 message ret=%d\n", ret);
}
}
index++;
delay -= 0.5;
if(delay<=20) {
delay = 20;
}
usleep(delay*1000);
}
};
return 0;
}
上述代码的逻辑如下:
- 定义T0L、T0H、TRST对应的上位机特定数值,以及灯珠数量
- 定义发送数据的列表,default_tx_reset-重置,default_tx0-熄灭,default_tx1-八种颜色
- 根据输入,初始化spidev设备,设置模式、位宽、速率
- 死循环,依次点亮每一颗灯珠;循环部分,根据index自增值,确定当前应该显示24颗中的哪一颗,同时对8取余,来设置颜色。delay用于演示的变化。
将上述代码保存为ws2812b_test,以便后续编译。
在Linux中,很方便的一点是,不管是操作I2C设备,还是SPI设备,都可以当作文件来对待,并使用标准调用来进行处理:
- open():打开设备通信
- ioctl():设置通信参数
- write():发送数据
- read():读取数据
- close():关闭设备通信
七、编译代码并执行
这里需要注意的是,编写、编译、上传、执行,是在不同的环境:
- 编写:可以在编译固件的环境,也可以在主机环境编写好以后在编译环境编译
- 编译:在编译固件的环境中编译
- 上传:在主机环境,通过adb上传到开发板
- 执行:在开发板上执行
那就先编译代码:
# 设置编译工具路径
export PATH=/sdk/tina-d1-h/prebuilt/gcc/linux-x86/riscv/toolchain-thead-glibc/riscv64-glibc-gcc-thead_20200702/bin/:$PATH
# 编译
riscv64-unknown-linux-gnu-gcc -o ws2812b_test ws2812b_test.c
# 查看结果
ls -lh ws2812b_test
-rwxr-xr-x 1 root 17K Nov 10 04:06 ws2812b_test
编译完成后,在主机上,使用adb上传:
adb push ws2812b_test /root/
最后,在开发板上执行:
/root/ws2812b_test 1
实际的执行结果如下:
实际效果如下:
视频效果更好: