Linux驱动开发实战(四):设备树点RGB灯

devtools/2025/3/17 15:39:03/

Linux驱动开发实战(四):设备树点RGB灯


文章目录

  • Linux驱动开发实战(四):设备树点RGB灯
  • 前言
  • 一、驱动实现
    • 1.1 驱动设计思路
    • 1.2 关键数据结构
    • 1.3 字符设备操作函数
    • 1.4 平台驱动探测函数
    • 1.5 匹配表和平台驱动结构体
    • 1.6 模块初始化和注销函数
  • 二、设备树配置
  • 三、从原理图分析设备树配置
    • 3.1 引脚与GPIO的对应关系
    • 3.2 寄存器地址解析
    • 3.3 电路连接分析
  • 四、引脚复用机制详解
    • 4.1 引脚功能选择过程
    • 4.2 为什么要配置PAD属性
  • 五、实验
    • 5.1 添加设备树
    • 5.2 编译设备树
    • 5.3 替换设备树
    • 5.4 编译驱动文件
    • 5.5 加载驱动文件
    • 5.6 往设备文件写入值(点灯!!!)
  • 总结


前言

在嵌入式Linux开发中,如何将硬件与软件紧密结合是一项基础却重要的技能。本文将详细讲解如何通过驱动程序控制i.MX6平台上的RGB LED,并深入分析从驱动代码、设备树配置到硬件原理图之间的关系


提示:以下是本篇文章正文内容,下面案例可供参考

一、驱动实现

1.1 驱动设计思路

我们使用平台驱动模型结合设备树进行开发,通过配置引脚复用和GPIO控制来实现RGB LED的控制。整体思路是:

  1. 定义数据结构保存LED控制需要的寄存器地址
  2. 从设备树获取资源并初始化GPIO
  3. 实现用户空间接口用于控制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;
}

这是字符设备的写函数,主要完成:

  1. 限制接收数据量最大为10字节
  2. 使用copy_from_user将用户空间数据复制到内核空间
  3. 使用kstrtoint将接收到的字符串转换为整数(16进制)
  4. 根据接收到的值控制三个LED的状态:
  • 位0控制蓝灯(值为1时点亮)
  • 位1控制绿灯(值为2时点亮)
  • 位2控制红灯(值为4时点亮)
  1. 通过读取当前寄存器值,修改相应位,然后写回寄存器的方式控制GPIO输出
  2. 注意这些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;}

平台驱动的探测函数开始部分:

  1. 声明变量用于保存返回值和寄存器数据
  2. 打印匹配成功信息
  3. 通过路径"/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_operationsled_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;
}

字符设备注册过程:

  1. 使用alloc_chrdev_region动态分配设备号
  2. 初始化字符设备结构体并关联操作函数
  3. 添加字符设备到系统中
  4. 创建设备类和设备节点
  5. 设置错误处理,如果添加失败则释放设备号.

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,}};

这部分定义了:

  1. 设备树匹配表,用于匹配compatible属性为"fire,rgb_led"的设备树节点
  2. 平台驱动结构体,指定了探测函数、驱动名称、所有者和匹配表

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");

模块初始化和注销函数:

  1. led_platform_driver_init:注册平台驱动并打印状态
  2. led_platform_driver_exit:
  • 释放所有虚拟地址映射
  • 销毁设备和设备类
  • 删除字符设备
  • 注销平台驱动
  • 打印退出信息
  1. 使用module_init和module_exit宏注册这些函数
  2. 声明模块许可证为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 0x000000040x020E006C 0x000000040x020E02F8 0x000000040x0209C000 0x000000040x0209C004 0x00000004>;status = "okay";};/*绿灯节点*/rgb_led_green@0x020C4074{reg = <0x020C4074 0x000000040x020E01E0 0x000000040x020E046C 0x000000040x020A8000 0x000000040x020A8004 0x00000004>;status = "okay";};/*蓝灯节点*/rgb_led_blue@0x020C4074{reg = <0x020C4074 0x000000040x020E01DC 0x000000040x020E0468 0x000000040x020A8000 0x000000040x020A8004 0x00000004>;status = "okay";};};

其中 0x020E01E0IOMUXC_SW_MUX_CTL_PAD_CSI_HSYNC 的寄存器地址。这个寄存器就是用来配置 CSI_HSYNC 引脚功能的复用控制寄存器。同样,0x020E046CIOMUXC_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所需的寄存器地址:

  1. 时钟控制寄存器(CCM_CCGR1) : 0x020C4074
  • 在i.MX6UL中,GPIO模块需要使能时钟才能工作
  • CCM_CCGR1寄存器控制GPIO1的时钟门控
  1. 引脚复用控制寄存器 (IOMUXC_SW_MUX_CTL_PAD_CSI_VSYNC) : 0x020E01B0
  • 控制CSI_VSYNC引脚的功能选择
  • 写入0x05选择ALT5功能,即GPIO1_04
  1. 引脚电气特性寄存器(IOMUXC_SW_PAD_CTL_PAD_CSI_VSYNC) : 0x020E0478
  • 控制引脚的驱动能力、上拉/下拉电阻等
  • 值0x10B0配置适当的驱动强度和上拉电阻
  1. GPIO数据寄存器(GPIO1_DR) : 0x0209C000
  • 控制GPIO1组引脚的输出电平
  • 第4位控制GPIO1_04的输出电平
  1. 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开发的标准实践。通过理解从原理图到设备树再到驱动代码的全链路,可以更加深入地掌握嵌入式系统的软硬件协同工作原理


http://www.ppmy.cn/devtools/167859.html

相关文章

贪心算法--

1.柠檬水找零 link:860. 柠檬水找零 - 力扣&#xff08;LeetCode&#xff09; code class Solution { public:bool lemonadeChange(vector<int>& bills) {// 贪心算法&#xff0c; 优先花出大面额bill&#xff0c; 尽可能保护小面额billint five 0, ten 0;// 不…

路由器和网关支持边缘计算

路由器和网关可以支持边缘计算&#xff0c;但它们的功能和性能可能有所不同&#xff0c;具体取决于设备的设计和用途。以下是路由器和网关在边缘计算中的作用及其支持方式&#xff1a; 路由器在边缘计算中的作用 网络连接与数据传输 路由器主要负责在网络中传输数据包&#xff…

Cisdem Video Converter for Mac v8.4.1 视频格式转换 支持M、Intel芯片

应用介绍 Cisdem Video Converter 将您的视频和音频文件转换为任何格式&#xff0c;以便在一系列设备上即时播放&#xff0c;支持所有编码格式&#xff0c;包括 H.265/HEVC、H.264、Xvid、VP8、VP9 等&#xff0c;并导出视频在最新的 4K UHD 中。它在不牺牲质量的情况下提供了…

4、linux c 进程

【三】进程 1. 进程与程序的区别 程序&#xff1a;存放在磁盘上的指令和数据的有序集合&#xff08;文件&#xff09;&#xff0c;是静态的。 进程&#xff1a;执行一个程序所分配的资源的总称&#xff0c;是动态的。 2. 进程的组成部分 BSS段&#xff08;bss&#xff09;&…

记一次一波三折的众测SRC经历

视频教程和更多福利在我主页简介或专栏里 &#xff08;不懂都可以来问我 专栏找我哦&#xff09; 目录&#xff1a; 前言 波折一&#xff1a;RCE漏洞利用失败 波折二&#xff1a;SQL时间盲注 波折三&#xff1a;寻找管理后台 总结 前言 先谈个人SRC心得体会吧&#xff0c;我虽…

Java集合 - ArrayList

ArrayList 是 Java 集合框架中最常用的动态数组实现类&#xff0c;位于 java.util 包中。它基于数组实现&#xff0c;支持动态扩容和随机访问。 1. 特点 动态数组&#xff1a;ArrayList 的底层是一个数组&#xff0c;可以根据需要动态扩展容量。 有序&#xff1a;元素按照插入…

DAY34 贪心算法Ⅲ

134. 加油站 - 力扣&#xff08;LeetCode&#xff09; 这种环路问题要记一下。 class Solution { public:int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {int curSum0;int totalSum0;int start0;for(int i0;i<gas.size();i){curSumga…

中考英语之08主谓一致

1 主谓一致主要规则 主谓一致是指在英语句子中&#xff0c;主语和谓语在人称和数上要保持一致。初中英语中主谓一致的一些主要规则如下&#xff1a; 1.1 语法一致原则 单数主语用单数谓语动词&#xff0c;复数主语用复数谓语动词。 例如&#xff1a;The book is on …