Linux驱动开发实战(四):设备树点RGB灯
文章目录
前言
在嵌入式Linux开发中,如何将硬件与软件紧密结合是一项基础却重要的技能。本文将详细讲解如何通过驱动程序控制i.MX6平台上的RGB LED,并深入分析从驱动代码、设备树配置到硬件原理图之间的关系
提示:以下是本篇文章正文内容,下面案例可供参考
一、驱动实现
1.1 驱动设计思路
我们使用平台驱动模型结合设备树进行开发,通过配置引脚复用和GPIO控制来实现RGB LED的控制。整体思路是:
- 定义数据结构保存LED控制需要的寄存器地址
- 从设备树获取资源并初始化GPIO
- 实现用户空间接口用于控制LED
1.2 关键数据结构
#define DEV_NAME "rgb_led"
#define DEV_CNT (1)
- DEV_NAME: 定义字符设备的名称为"rgb_led"
- DEV_CNT: 定义要创建的设备数量为1
struct rgb_led_dev {
struct device_node *device_node; // 设备节点
void __iomem *virtual_CCM_CCGR; // 时钟控制寄存器
void __iomem *virtual_IOMUXC_SW_MUX_CTL_PAD; // 引脚复用寄存器
void __iomem *virtual_IOMUXC_SW_PAD_CTL_PAD; // 引脚电气特性寄存器
void __iomem *virtual_DR; // GPIO数据寄存器
void __iomem *virtual_GDIR; // GPIO方向寄存器
unsigned int pin_num; // GPIO引脚号
};
1.3 字符设备操作函数
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
printk("\n open form driver \n");
return 0;
}
这是字符设备的打开函数,仅打印一条日志信息并返回成功(0)。
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int ret,error;
unsigned int register_data = 0;
unsigned char receive_data[10];
unsigned int write_data;
if(cnt>10)
cnt =10;
error = copy_from_user(receive_data, buf, cnt);
if (error < 0)
{
return -1;
}
ret = kstrtoint(receive_data, 16, &write_data);
if (ret) {
return -1;
}
/*设置 GPIO1_04 输出电平*/
if (write_data & 0x04)
{
register_data = ioread32(led_red.virtual_DR);
register_data &= ~(0x01 << 4);
iowrite32(register_data, led_red.virtual_DR); // GPIO1_04引脚输出低电平,红灯亮
}
else
{
register_data = ioread32(led_red.virtual_DR);
register_data |= (0x01 << 4);
iowrite32(register_data, led_red.virtual_DR); // GPIO1_04引脚输出高电平,红灯灭
}
/*设置 GPIO4_20 输出电平*/
if (write_data & 0x02)
{
register_data = ioread32(led_green.virtual_DR);
register_data &= ~(0x01 << 20);
iowrite32(register_data, led_green.virtual_DR); // GPIO4_20引脚输出低电平,绿灯亮
}
else
{
register_data = ioread32(led_green.virtual_DR);
register_data |= (0x01 << 20);
iowrite32(register_data, led_green.virtual_DR); // GPIO4_20引脚输出高电平,绿灯灭
}
/*设置 GPIO4_19 输出电平*/
if (write_data & 0x01)
{
register_data = ioread32(led_blue.virtual_DR);
register_data &= ~(0x01 << 19);
iowrite32(register_data, led_blue.virtual_DR); //GPIO4_19引脚输出低电平,蓝灯亮
}
else
{
register_data = ioread32(led_blue.virtual_DR);
register_data |= (0x01 << 19);
iowrite32(register_data, led_blue.virtual_DR); //GPIO4_19引脚输出高电平,蓝灯灭
}
return cnt;
}
这是字符设备的写函数,主要完成:
- 限制接收数据量最大为10字节
- 使用copy_from_user将用户空间数据复制到内核空间
- 使用kstrtoint将接收到的字符串转换为整数(16进制)
- 根据接收到的值控制三个LED的状态:
- 位0控制蓝灯(值为1时点亮)
- 位1控制绿灯(值为2时点亮)
- 位2控制红灯(值为4时点亮)
- 通过读取当前寄存器值,修改相应位,然后写回寄存器的方式控制GPIO输出
- 注意这些LED是低电平点亮的
开发板终端输出:
static struct file_operations led_chr_dev_fops =
{
.owner = THIS_MODULE,
.open = led_chr_dev_open,
.write = led_chr_dev_write,
};
定义了字符设备的操作函数集,包含了所有者、打开函数和写函数。
1.4 平台驱动探测函数
static int led_probe(struct platform_device *pdv)
{
int ret = -1;
unsigned int register_data = 0;
printk(KERN_ALERT "\t match successed \n");
/*获取rgb_led的设备树节点*/
rgb_led_device_node = of_find_node_by_path("/rgb_led");
if (rgb_led_device_node == NULL)
{
printk(KERN_ERR "\t get rgb_led failed! \n");
return -1;
}
平台驱动的探测函数开始部分:
- 声明变量用于保存返回值和寄存器数据
- 打印匹配成功信息
- 通过路径"/rgb_led"获取设备树节点,失败则返回错误
接下来分别初始化红、绿、蓝三个LED,以红色LED为例:
/*获取rgb_led节点的红灯子节点*/
led_red.device_node = of_find_node_by_name(rgb_led_device_node,"rgb_led_red");
if (led_red.device_node == NULL)
{
printk(KERN_ERR "\n get rgb_led_red_device_node failed ! \n");
return -1;
}
/*获取 reg 属性并转化为虚拟地址*/
led_red.virtual_CCM_CCGR = of_iomap(led_red.device_node, 0);
led_red.virtual_IOMUXC_SW_MUX_CTL_PAD = of_iomap(led_red.device_node, 1);
led_red.virtual_IOMUXC_SW_PAD_CTL_PAD = of_iomap(led_red.device_node, 2);
led_red.virtual_DR = of_iomap(led_red.device_node, 3);
led_red.virtual_GDIR = of_iomap(led_red.device_node, 4);
/*初始化红灯*/
register_data = ioread32(led_red.virtual_CCM_CCGR);
register_data |= (0x03 << 26);
iowrite32(register_data, led_red.virtual_CCM_CCGR); //开启时钟
register_data = ioread32(led_red.virtual_IOMUXC_SW_MUX_CTL_PAD);
register_data &= ~(0xf << 0);
register_data |= (0x05 << 0);
iowrite32(register_data, led_red.virtual_IOMUXC_SW_MUX_CTL_PAD); //设置复用功能
register_data = ioread32(led_red.virtual_IOMUXC_SW_PAD_CTL_PAD);
register_data = (0x10B0);
iowrite32(register_data, led_red.virtual_IOMUXC_SW_PAD_CTL_PAD); //设置PAD 属性
register_data = ioread32(led_red.virtual_GDIR);
register_data |= (0x01 << 4);
iowrite32(register_data, led_red.virtual_GDIR); //设置GPIO1_04 为输出模式
register_data = ioread32(led_red.virtual_DR);
register_data |= (0x01 << 4);
iowrite32(register_data, led_red.virtual_DR); //设置 GPIO1_04 默认输出高电平
红色LED初始化过程:
1.获取红色LED的设备树节点
2.将设备树中的reg属性映射为虚拟地址(共5个寄存器)
3.初始化GPIO:
- 使能相关时钟
- 设置管脚复用功能(设为GPIO模式)
- 设置管脚物理属性
- 设置GPIO为输出模式
- 设置默认输出高电平(LED熄灭)
绿色和蓝色LED的初始化过程类似,但操作的是不同的GPIO引脚。
最后是注册字符设备的部分:
/*---------------------注册 字符设备部分-----------------*/
//第一步
//采用动态分配的方式,获取设备编号,次设备号为0,
//设备名称为rgb-leds,可通过命令cat /proc/devices查看
//DEV_CNT为1,当前只申请一个设备编号
ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0)
{
printk("fail to alloc led_devno\n");
goto alloc_err;
}
//第二步
//关联字符设备结构体cdev与文件操作结构体file_operations
led_chr_dev.owner = THIS_MODULE;
cdev_init(&led_chr_dev, &led_chr_dev_fops);
//第三步
//添加设备至cdev_map散列表中
ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);
if (ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
//第四步
/*创建类 */
class_led = class_create(THIS_MODULE, DEV_NAME);
/*创建设备*/
device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME);
return 0;
add_err:
//添加设备失败时,需要注销设备号
unregister_chrdev_region(led_devno, DEV_CNT);
printk("\n error! \n");
alloc_err:
return -1;
}
字符设备注册过程:
- 使用alloc_chrdev_region动态分配设备号
- 初始化字符设备结构体并关联操作函数
- 添加字符设备到系统中
- 创建设备类和设备节点
- 设置错误处理,如果添加失败则释放设备号.
1.5 匹配表和平台驱动结构体
static const struct of_device_id rgb_led[] = {
{.compatible = "fire,rgb_led"},
{/* sentinel */}};
/*定义平台设备结构体*/
struct platform_driver led_platform_driver = {
.probe = led_probe,
.driver = {
.name = "rgb-leds-platform",
.owner = THIS_MODULE,
.of_match_table = rgb_led,
}};
这部分定义了:
- 设备树匹配表,用于匹配compatible属性为"fire,rgb_led"的设备树节点
- 平台驱动结构体,指定了探测函数、驱动名称、所有者和匹配表
1.6 模块初始化和注销函数
static int __init led_platform_driver_init(void)
{
int DriverState;
DriverState = platform_driver_register(&led_platform_driver);
printk(KERN_ALERT "\tDriverState is %d\n", DriverState);
return 0;
}
static void __exit led_platform_driver_exit(void)
{
/*取消物理地址映射到虚拟地址*/
iounmap(led_green.virtual_CCM_CCGR);
iounmap(led_green.virtual_IOMUXC_SW_MUX_CTL_PAD);
iounmap(led_green.virtual_IOMUXC_SW_PAD_CTL_PAD);
iounmap(led_green.virtual_DR);
iounmap(led_green.virtual_GDIR);
iounmap(led_red.virtual_CCM_CCGR);
iounmap(led_red.virtual_IOMUXC_SW_MUX_CTL_PAD);
iounmap(led_red.virtual_IOMUXC_SW_PAD_CTL_PAD);
iounmap(led_red.virtual_DR);
iounmap(led_red.virtual_GDIR);
iounmap(led_blue.virtual_CCM_CCGR);
iounmap(led_blue.virtual_IOMUXC_SW_MUX_CTL_PAD);
iounmap(led_blue.virtual_IOMUXC_SW_PAD_CTL_PAD);
iounmap(led_blue.virtual_DR);
iounmap(led_blue.virtual_GDIR);
/*删除设备*/
device_destroy(class_led, led_devno); //清除设备
class_destroy(class_led); //清除类
cdev_del(&led_chr_dev); //清除设备号
unregister_chrdev_region(led_devno, DEV_CNT); //取消注册字符设备
/*注销字符设备*/
platform_driver_unregister(&led_platform_driver);
printk(KERN_ALERT "led_platform_driver exit!\n");
}
module_init(led_platform_driver_init);
module_exit(led_platform_driver_exit);
MODULE_LICENSE("GPL");
模块初始化和注销函数:
- led_platform_driver_init:注册平台驱动并打印状态
- led_platform_driver_exit:
- 释放所有虚拟地址映射
- 销毁设备和设备类
- 删除字符设备
- 注销平台驱动
- 打印退出信息
- 使用module_init和module_exit宏注册这些函数
- 声明模块许可证为GPL
二、设备树配置
设备树是连接硬件与驱动的桥梁,这部分定义了RGB LED所需的硬件资源:
/*
*CCM_CCGR1 0x020C406C
*IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 0x020E006C
*IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 0x020E02F8
*GPIO1_GD 0x0209C000
*GPIO1_GDIR 0x0209C004
*/
/*
*CCM_CCGR3 0x020C4074
*IOMUXC_SW_MUX_CTL_PAD_CSI_HSYNC 0x020E01E0
*IOMUXC_SW_PAD_CTL_PAD_CSI_HSYNC 0x020E046C
*GPIO4_GD 0x020A8000
*GPIO4_GDIR 0x020A8004
*/
/*
*CCM_CCGR3 0x020C4074
*IOMUXC_SW_MUX_CTL_PAD_CSI_VSYNC 0x020E01DC
*IOMUXC_SW_PAD_CTL_PAD_CSI_VSYNC 0x020E0468
*GPIO4_GD 0x020A8000
*GPIO4_GDIR 0x020A8004
*/
/*添加led节点*/
rgb_led{
#address-cells = <1>;
#size-cells = <1>;
compatible = "fire,rgb_led";
/*红灯节点*/
ranges;
rgb_led_red@0x020C406C{
reg = <0x020C406C 0x00000004
0x020E006C 0x00000004
0x020E02F8 0x00000004
0x0209C000 0x00000004
0x0209C004 0x00000004>;
status = "okay";
};
/*绿灯节点*/
rgb_led_green@0x020C4074{
reg = <0x020C4074 0x00000004
0x020E01E0 0x00000004
0x020E046C 0x00000004
0x020A8000 0x00000004
0x020A8004 0x00000004>;
status = "okay";
};
/*蓝灯节点*/
rgb_led_blue@0x020C4074{
reg = <0x020C4074 0x00000004
0x020E01DC 0x00000004
0x020E0468 0x00000004
0x020A8000 0x00000004
0x020A8004 0x00000004>;
status = "okay";
};
};
其中 0x020E01E0 是 IOMUXC_SW_MUX_CTL_PAD_CSI_HSYNC 的寄存器地址。这个寄存器就是用来配置 CSI_HSYNC 引脚功能的复用控制寄存器。同样,0x020E046C 是 IOMUXC_SW_PAD_CTL_PAD_CSI_HSYNC 的寄存器地址,用于设置 CSI_HSYNC 引脚的电气特性。
蓝色 LED 节点也类似,使用 0x020E01DC(IOMUXC_SW_MUX_CTL_PAD_CSI_VSYNC) 和 0x020E0468(IOMUXC_SW_PAD_CTL_PAD_CSI_VSYNC) 来配置 CSI_VSYNC 引脚。
在 i.MX 处理器的设计中,每个引脚都有专用的复用控制寄存器和 PAD 控制寄存器,寄存器的地址是固定的,与引脚一一对应。因此在设备树中,我们只需要提供寄存器地址,而不需要显式地声明引脚名称,驱动程序通过访问对应地址的寄存器就能控制特定的引脚。
这就是为什么在设备树中看不到 “CSI_HSYNC” 或 “CSI_VSYNC” 这些名称的原因 - 它们是通过对应的寄存器地址来表示的。而在电路图中,则直接使用引脚的功能名称来标识连接方式。
这种做法在嵌入式系统中很常见,因为它直接反映了硬件设计,而不需要额外的名称映射层。
三、从原理图分析设备树配置
3.1 引脚与GPIO的对应关系
查看i.MX6UL原理图,我们可以发现RGB LED连接到以下引脚:
红色LED: CSI_VSYNC引脚
绿色LED: CSI_HSYNC引脚
蓝色LED: CSI_DATA00引脚
根据i.MX6UL数据手册中的引脚复用表:CSI_VSYNC的ALT5功能对应GPIO1_04
CSI_HSYNC的ALT5功能对应GPIO4_20
CSI_DATA00的ALT5功能对应GPIO4_19
这是为什么设备树中我们配置了:
led-red {
gpio-pin = <4>; /* GPIO1_04 */
}
3.2 寄存器地址解析
设备树中的reg属性定义了控制每个LED所需的寄存器地址:
- 时钟控制寄存器(CCM_CCGR1) : 0x020C4074
- 在i.MX6UL中,GPIO模块需要使能时钟才能工作
- CCM_CCGR1寄存器控制GPIO1的时钟门控
- 引脚复用控制寄存器 (IOMUXC_SW_MUX_CTL_PAD_CSI_VSYNC) : 0x020E01B0
- 控制CSI_VSYNC引脚的功能选择
- 写入0x05选择ALT5功能,即GPIO1_04
- 引脚电气特性寄存器(IOMUXC_SW_PAD_CTL_PAD_CSI_VSYNC) : 0x020E0478
- 控制引脚的驱动能力、上拉/下拉电阻等
- 值0x10B0配置适当的驱动强度和上拉电阻
- GPIO数据寄存器(GPIO1_DR) : 0x0209C000
- 控制GPIO1组引脚的输出电平
- 第4位控制GPIO1_04的输出电平
- GPIO方向寄存器(GPIO1_GDIR) : 0x0209C004
- 控制GPIO1组引脚的方向(输入/输出)
- 设置第4位为1,将GPIO1_04配置为输出
3.3 电路连接分析
从原理图可以看出,RGB LED采用的是共阳极连接方式:
- LED的阳极连接到电源(VCC)
- LED的阴极通过限流电阻连接到MCU的GPIO引脚
- 当GPIO输出低电平时,LED点亮
- 当GPIO输出高电平时,LED熄灭
这就是为什么在驱动代码中:
if (write_data & 0x02)
{
register_data = ioread32(led_green.virtual_DR);
register_data &= ~(0x01 << 20);
iowrite32(register_data, led_green.virtual_DR); // GPIO4_20引脚输出低电平,绿灯亮
}
else
{
register_data = ioread32(led_green.virtual_DR);
register_data |= (0x01 << 20);
iowrite32(register_data, led_green.virtual_DR); // GPIO4_20引脚输出高电平,绿灯灭
}
四、引脚复用机制详解
i.MX6处理器的引脚复用是通过IOMUXC模块实现的。每个引脚可以配置为多种功能(ALT0~ALT7)。
4.1 引脚功能选择过程
register_data = ioread32(led_red.virtual_IOMUXC_SW_MUX_CTL_PAD);
register_data &= ~(0xf << 0);// 清除低4位
register_data |= (0x05 << 0);// 设置为ALT5模式
iowrite32(register_data, led_red.virtual_IOMUXC_SW_MUX_CTL_PAD); //设置复用功能
值0x05表示选择ALT5功能,将CSI_VSYNC配置为GPIO1_04。
4.2 为什么要配置PAD属性
PAD属性配置影响引脚的电气特性:
register_data = (0x10B0);
iowrite32(register_data, led_red.virtual_IOMUXC_SW_PAD_CTL_PAD); //设置PAD 属性
值0x10B0包含以下配置:
- 上拉/下拉选择
- 上拉/下拉强度
- 开漏输出使能/禁用
- 驱动强度
- 速率控制
- 迟滞比较器使能/禁用
这些配置确保GPIO引脚有正确的驱动能力和电气特性,使LED能够正常工作。
五、实验
5.1 添加设备树
5.2 编译设备树
5.3 替换设备树
5.4 编译驱动文件
5.5 加载驱动文件
5.6 往设备文件写入值(点灯!!!)
sudo sh -c "echo '3'>/dev/rgb_led"
总结
通过本文,我们详细解析了在i.MX6ULL平台上如何:
- 编写驱动程序控制RGB LED
- 配置设备树定义硬件资源
- 根据原理图理解硬件连接与设备树配置的关系
这种基于设备树的驱动开发方式具有良好的可移植性和可维护性,是现代嵌入式Linux开发的标准实践。通过理解从原理图到设备树再到驱动代码的全链路,可以更加深入地掌握嵌入式系统的软硬件协同工作原理