文章目录
一、实验环境介绍
硬件:野火鲁班猫1(RK3566)、野火鲁班猫2(RK3568)、野火鲁班猫4(RK3588S)、RGB灯模块*2
软件:野火鲁班猫SDK(涉及两个内核版本,一个4.9,一个5.1)
本文档涉及的程序可自行获取:https://github.com/Cohen0415/ws2812_rk356x_rk3588_driver
1.1 WS2812介绍
WS2812是一种集成了控制电路和RGB LED的智能LED光源,它能够实现全彩显示,具有极高的色彩一致性。这种LED灯珠内部包含了智能数字接口数据锁存信号整形放大驱动电路,以及一个精密的内部振荡器和可编程定电流控制部分。WS2812的数据传输协议采用单线归零码的通讯方式,支持串行级联接口,能通过一根信号线完成数据的接收与解码。
1.2 RGB灯模块实物图
1.3 接线图
市面上的RGB灯模块使用方法几乎一样,实验中,我用了两个RGB灯模块来模拟一个灯带:
1.4 WS2812时序
WS2812是单总线协议,发送逻辑0和逻辑1都是靠一根线完成。所以是通过约定各高低电平所占的时间来判断是逻辑0还是逻辑1。
WS2812的逻辑0(0码)和逻辑1(1码)的组成方式如下:
1.5 WS2812传输方法
对于每一个RGB而言,主控需要发送24bit数据来控制RGB的颜色:
高位先发,而且是按照GRB的格式发送!
例如发送0xFF0000,RGB亮绿色;发送0x00FF00,RGB亮红色。
那有多个RGB串联在一起,怎么控制后面的RGB呢?
当第一个RGB接收完24bit数据后,剩下的数据靠级联电路自动整形转发,即第二个RGB会接收第25bit开始的数据,以此类推,每个RGB只接收24bit数据。
二、编写应用程序
这里先介绍应用程序,方便能更好的了解驱动的功能。
主要通过向驱动程序写入一个结构体来控制RGB的颜色:
/* ws2812_app.c */
...
struct ws2812_mes {
unsigned int gpiochip; // data引脚的gpiochip
unsigned int gpionum; // data引脚的gpionum
unsigned int lednum; // 要控制灯带的第几个LED,序号从1开始
unsigned char color[3]; // color[0]:color[1]:color[2] R:G:B
};
...
完整的应用程序如下:
/* ws2812_app.c */
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#define WS2812_DATA_GPIOCHIP 3
#define WS2812_DATA_GPIONUM 19
struct ws2812_mes {
unsigned int gpiochip; // data引脚的gpiochip
unsigned int gpionum; // data引脚的gpionum
unsigned int lednum; // 要控制灯带的第几个LED,序号从1开始
unsigned char color[3]; // color[0]:color[1]:color[2] R:G:B
};
int main(int argc, char **argv)
{
struct ws2812_mes ws2812;
int fd;
int ret;
char hex_str[10];
char *endptr;
ws2812.gpiochip = WS2812_DATA_GPIOCHIP;
ws2812.gpionum = WS2812_DATA_GPIONUM;
ws2812.lednum = 1;
ws2812.color[0] = 0x00;
ws2812.color[1] = 0x00;
ws2812.color[2] = 0x00;
if(argc != 3)
{
printf("Usage: %s <led num> <hex_color>\n", argv[0]);
printf("e.g. : %s 3 FF0000\n", argv[0]);
return -1;
}
/* 参数1检查 */
ws2812.lednum = (int)strtol(argv[1], &endptr, 10);
if (*endptr != '\0' || ws2812.lednum < 1 || ws2812.lednum > 20) {
printf("Error: The first argument must be a number between 1 and 20.\n");
printf("e.g. : %s 3 FF0000\n", argv[0]);
return -1;
}
/* 参数2检查 */
if(strlen(argv[2]) != 6)
{
printf("Error: The second argument has illegal length.\n");
printf("e.g. : %s 3 FF0000\n", argv[0]);
return -1;
}
if (sscanf(argv[2], "%2hhx%2hhx%2hhx", &ws2812.color[0], &ws2812.color[1], &ws2812.color[2]) != 3)
{
printf("Error: Invalid hex color format.\n");
return -1;
}
/* 打开ws2812设备节点 */
fd = open("/dev/ws2812", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/ws2812\n");
return -1;
}
ret = write(fd, &ws2812, sizeof(struct ws2812_mes));
if(ret < 0)
{
printf("ws2812 write err!\n");
}
close(fd);
return 0;
}
三、编写驱动程序
本次驱动程序基于寄存器操作来实现。
对于写逻辑0和逻辑1的时间控制,需要提前用逻辑分析仪测量:
/* ws2812_drv.c */
...
static void ws2812_write_frame_0(void)
{
static unsigned int temp = 0;
temp |= 1 << (16 + bit);
/* T0H:拉高 220ns ~ 380ns */
temp |= 1 << bit;
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 约250ns */
/* T0L: 拉低 580ns ~ 1.6us */
temp &= (~(1 << bit));
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 约1us */
}
static void ws2812_write_frame_1(void)
{
static unsigned int temp = 0;
temp |= 1 << (16 + bit);
/* T1H: 拉高 580ns ~ 1us */
temp |= 1 << bit;
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 约1us */
/* T1L: 拉低 220ns ~ 420ns */
temp &= (~(1 << bit));
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 250ns */
}
...
因为rk356x和rk3588系列的寄存器地址不同,编译前需要通过宏定义来选择目标芯片,也可以在编译命令里加上宏定义参数:
/* ws2812_drv.c */
...
/* 根据目标芯片,打开对应宏定义 */
//#define RK356x
//#define RK3588
...
完整的驱动程序如下:
/* ws2812_drv.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/delay.h>
#define DEV_NAME "ws2812"
#define LED_NUM_MAX (20)
//#define DEBUG
/* 根据目标芯片,打开对应宏定义 */
//#define RK356x
//#define RK3588
#ifdef RK356x
/* RK356x GPIO BASE */
#define GPIO0_BASE_ADDR UL(0xFDD60000)
#define GPIO1_BASE_ADDR UL(0xFE740000)
#define GPIO2_BASE_ADDR UL(0xFE750000)
#define GPIO3_BASE_ADDR UL(0xFE760000)
#define GPIO4_BASE_ADDR UL(0xFE770000)
#endif
#ifdef RK3588
/* RK3588 GPIO BASE */
#define GPIO0_BASE_ADDR UL(0xFD8A0000)
#define GPIO1_BASE_ADDR UL(0xFEC20000)
#define GPIO2_BASE_ADDR UL(0xFEC30000)
#define GPIO3_BASE_ADDR UL(0xFEC40000)
#define GPIO4_BASE_ADDR UL(0xFEC50000)
#endif
/* GPIO Level REG OFFSET */
#define GPIO_SWPORT_DR_L_OFFSET (0x0000)
#define GPIO_SWPORT_DR_H_OFFSET (0x0004)
/* GPIO Direction REG OFFSET */
#define GPIO_SWPORT_DDR_L_OFFSET (0x0008)
#define GPIO_SWPORT_DDR_H_OFFSET (0x000C)
static volatile unsigned int *GPIO_DIR_REG;
static volatile unsigned int *GPIO_LEVEL_REG;
static struct ws2812_mes {
unsigned int gpiochip; // data引脚的gpiochip
unsigned int gpionum; // data引脚的gpionum
unsigned int lednum; // 要控制灯带的第几个LED,序号从1开始
unsigned char color[3]; // color[0]:color[1]:color[2] R:G:B
}ws2812;
static int init_flag = 0;
static int major = 0;
static struct class *ws2812_class;
static int bit;
static int ws2812_drv_open(struct inode *node, struct file *file)
{
return 0;
}
static void ws2812_reset(void)
{
static unsigned int temp = 0;
temp |= 1 << (16 + bit);
/* Reset: 拉低 > 280ns */
temp &= (~(1 << bit));
*GPIO_LEVEL_REG = temp;
udelay(300);
}
static void ws2812_write_frame_0(void)
{
static unsigned int temp = 0;
temp |= 1 << (16 + bit);
/* T0H:拉高 220ns ~ 380ns */
temp |= 1 << bit;
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 约250ns */
/* T0L: 拉低 580ns ~ 1.6us */
temp &= (~(1 << bit));
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 约1us */
}
static void ws2812_write_frame_1(void)
{
static unsigned int temp = 0;
temp |= 1 << (16 + bit);
/* T1H: 拉高 580ns ~ 1us */
temp |= 1 << bit;
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 约1us */
/* T1L: 拉低 220ns ~ 420ns */
temp &= (~(1 << bit));
*GPIO_LEVEL_REG = temp; /* 执行1次,约50ns */
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp;
*GPIO_LEVEL_REG = temp; /* 250ns */
}
static void ws2812_write_byte(unsigned char byte)
{
int i = 0;
for(i = 0; i < 8; i++)
{
if((byte << i) & 0x80)
ws2812_write_frame_1();
else
ws2812_write_frame_0();
}
}
static ssize_t ws2812_drv_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos)
{
int err;
struct ws2812_mes ws2812_usr;
int step;
int i = 1;
unsigned int temp = 0;
err = copy_from_user(&ws2812_usr, buf, sizeof(ws2812_usr));
if(err != 0)
{
printk(KERN_ERR"get ws2812 struct err!\n");
return err;
}
#ifdef DEBUG
printk("ws2812_usr.gpiochip : %d\n", ws2812_usr.gpiochip);
printk("ws2812_usr.gpionum : %d\n", ws2812_usr.gpionum);
printk("ws2812_usr.lednum : %d\n", ws2812_usr.lednum);
printk("ws2812_usr.color[0] : %d\n", ws2812_usr.color[0]);
printk("ws2812_usr.color[1] : %d\n", ws2812_usr.color[1]);
printk("ws2812_usr.color[2] : %d\n", ws2812_usr.color[2]);
#endif
if(ws2812_usr.gpiochip < 0 || ws2812.gpiochip > 4)
{
printk(KERN_ERR"ws2812.gpiochip must >= 0 && <= 4\n");
return -1;
}
if(ws2812_usr.gpionum < 0 || ws2812_usr.gpionum > 31)
{
printk(KERN_ERR"ws2812.gpionum must >= 0 && <= 31\n");
return -1;
}
if(ws2812_usr.lednum < 1 || ws2812_usr.lednum > LED_NUM_MAX)
{
printk(KERN_ERR"ws2812.lednum must >= 1 && <= %d\n", LED_NUM_MAX);
return -1;
}
if(init_flag == 0)
{
/* step : 0-15使用GPIO_SWPORT_DR_L_OFFSET,16-31使用GPIO_SWPORT_DR_H_OFFSET */
step = ws2812_usr.gpionum / 15;
if(ws2812_usr.gpiochip == 0)
{
/* 1、GPIO复用 */
/* 相关复用寄存器中,默认都是配置成GPIO模式,所以这里省略了GPIO的复用配置 */
/* 2、GPIO模式 */
GPIO_DIR_REG = ioremap(GPIO0_BASE_ADDR + GPIO_SWPORT_DDR_L_OFFSET+(step*(0x4)), 4);
/* 3、GPIO电平 */
GPIO_LEVEL_REG = ioremap(GPIO0_BASE_ADDR + GPIO_SWPORT_DR_L_OFFSET+(step*(0x4)), 4);
}
else
{
/* 1、GPIO复用 */
/* 相关复用寄存器中,默认都是配置成GPIO模式,所以这里省略了GPIO的复用配置 */
/* 2、GPIO模式 */
GPIO_DIR_REG = ioremap(GPIO1_BASE_ADDR + (0x10000*(ws2812_usr.gpiochip-1)) + GPIO_SWPORT_DDR_L_OFFSET+(step*(0x4)), 4);
/* 3、GPIO电平 */
GPIO_LEVEL_REG = ioremap(GPIO1_BASE_ADDR + (0x10000*(ws2812_usr.gpiochip-1)) + GPIO_SWPORT_DR_L_OFFSET+(step*(0x4)), 4);
#ifdef DEBUG
printk("GPIO_DIR_REG : 0x%lX\n", GPIO1_BASE_ADDR + (0x10000*(ws2812_usr.gpiochip-1)) + GPIO_SWPORT_DDR_L_OFFSET+(step*(0x4)));
printk("GPIO_LEVEL_REG : 0x%lX\n", GPIO1_BASE_ADDR + (0x10000*(ws2812_usr.gpiochip-1)) + GPIO_SWPORT_DR_L_OFFSET+(step*(0x4)));
#endif
}
if(GPIO_LEVEL_REG == NULL || GPIO_DIR_REG == NULL)
{
printk(KERN_ERR"GPIO_LEVEL_REG or GPIO_DIR_REG is NULL\n");
return -1;
}
bit = ws2812_usr.gpionum % 16;
/* 设置GPIO复用 */
/* 相关复用寄存器中,默认都是配置成GPIO模式,所以这里省略了GPIO的复用配置 */
/* 设置GPIO模式为Output */
temp = *GPIO_DIR_REG;
temp |= 1 << (16 + bit);
temp |= 1 << bit;
*GPIO_DIR_REG = temp;
/* 设置GPIO初始电平为高电平 */
temp = *GPIO_LEVEL_REG;
temp |= 1 << (16 + bit);
temp |= 1 << bit;
*GPIO_LEVEL_REG = temp;
}
ws2812_reset();
for(i = 1; i < ws2812_usr.lednum; i++)
{
ws2812_write_byte(0x00);
ws2812_write_byte(0x00);
ws2812_write_byte(0x00);
}
ws2812_write_byte(ws2812_usr.color[1]); // color G
ws2812_write_byte(ws2812_usr.color[0]); // color R
ws2812_write_byte(ws2812_usr.color[2]); // color B
ws2812_reset();
#ifdef DEBUG
printk("ws2812 write over!\n");
#endif
init_flag = 1;
return 0;
}
static int ws2812_drv_close(struct inode *node, struct file *file)
{
iounmap(GPIO_LEVEL_REG);
iounmap(GPIO_DIR_REG);
GPIO_LEVEL_REG = NULL;
GPIO_DIR_REG = NULL;
init_flag = 0;
return 0;
}
static struct file_operations ws2812_fops = {
.owner = THIS_MODULE,
.open = ws2812_drv_open,
.release = ws2812_drv_close,
.write = ws2812_drv_write,
};
static __init int ws2812_init(void)
{
printk(KERN_INFO"Load the ws2812 module successfully!\n");
major = register_chrdev(0, "rk_ws2812", &ws2812_fops);
ws2812_class = class_create(THIS_MODULE, "rk_ws2812_class");
if (IS_ERR(ws2812_class)) {
printk(KERN_ERR"%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "rk_ws2812");
return PTR_ERR(ws2812_class);
}
device_create(ws2812_class, NULL, MKDEV(major, 0), NULL, "ws2812");
return 0;
}
module_init(ws2812_init);
static __exit void ws2812_exit(void)
{
printk(KERN_INFO"the ws2812 module has been remove!\n");
device_destroy(ws2812_class, MKDEV(major, 0));
class_destroy(ws2812_class);
unregister_chrdev(major, "rk_ws2812");
if(GPIO_LEVEL_REG != NULL)
iounmap(GPIO_LEVEL_REG);
if(GPIO_DIR_REG != NULL)
iounmap(GPIO_DIR_REG);
GPIO_LEVEL_REG = NULL;
GPIO_DIR_REG = NULL;
}
module_exit(ws2812_exit);
MODULE_LICENSE("GPL");
四、测试
测试前可百度RGB颜色对照表:
加载驱动后,使用app测试,app使用方法如下:
# 第一个参数为要控制的RGB序号
# 第二个参数为十六进制的颜色码值
# 控制第1个RGB灯颜色为红色
./ws2812_app 1 FF0000
# 控制第2个RGB灯颜色为绿色
./ws2812_app 2 00FF00
# 关闭第2个RGB灯
./ws2812_app 2 000000
五、调试中遇到的问题
调试过程中发现,往RGB灯发送24位全0数据时,有时候RGB依旧会亮绿灯。后面用逻辑分析仪查看,第一个0码的高电平的持续时间超过了上限380ns,去到了1us多,所以被识别成了1码,导致原本的0x000000数据被识别成0x008000:
后面发现,在引脚初始化时,把引脚的初始电平设置成了高电平,然而发送0码和1码都是先拉高引脚,所以导致第一个高电平的时间不好检测。于是在发送数据前,先发送reset,即先把电平拉低,之后就不会出现乱码的现象了。
若想在后续实现呼吸灯效果,这种驱动方式存在缺陷,具体可以参考另一种基于spi驱动的方式:在ARM Linux应用层下使用SPI驱动WS2812
六、总结
嵌入式Linux学习交流群:424571391
参考文章:一文带你了解WS2812原理及驱动