RK356x/RK3588 WS2812驱动调试

一、实验环境介绍

硬件:野火鲁班猫1(RK3566)、野火鲁班猫2(RK3568)、野火鲁班猫4(RK3588S)、RGB灯模块*2
软件:野火鲁班猫SDK(涉及两个内核版本,一个4.9,一个5.1)

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,即先把电平拉低,之后就不会出现乱码的现象了。

六、总结

参考文章:一文带你了解WS2812原理及驱动

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值