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

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


文章目录

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

前言

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


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

一、驱动实现

1.1 驱动设计思路

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

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

1.2 关键数据结构

c 复制代码
#define DEV_NAME "rgb_led"
#define DEV_CNT (1)
  • DEV_NAME: 定义字符设备的名称为"rgb_led"
  • DEV_CNT: 定义要创建的设备数量为1
c 复制代码
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 字符设备操作函数

c 复制代码
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\n open form driver \n");
    return 0;
}

这是字符设备的打开函数,仅打印一条日志信息并返回成功(0)。

c 复制代码
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是低电平点亮的

开发板终端输出:

c 复制代码
static struct file_operations led_chr_dev_fops =
{
    .owner = THIS_MODULE,
    .open = led_chr_dev_open,
    .write = led_chr_dev_write,
};

定义了字符设备的操作函数集,包含了所有者、打开函数和写函数。

1.4 平台驱动探测函数

c 复制代码
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为例:

c 复制代码
    /*获取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引脚。

最后是注册字符设备的部分:

c 复制代码
    /*---------------------注册 字符设备部分-----------------*/
    //第一步
    //采用动态分配的方式,获取设备编号,次设备号为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;
}

字符设备注册过程:

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

1.5 匹配表和平台驱动结构体

c 复制代码
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 模块初始化和注销函数

c 复制代码
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所需的硬件资源:

c 复制代码
/*
*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";
		};
	};

其中 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

    这是为什么设备树中我们配置了:

c 复制代码
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熄灭
    这就是为什么在驱动代码中:
c 复制代码
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 引脚功能选择过程

c 复制代码
    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属性配置影响引脚的电气特性:

c 复制代码
    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 往设备文件写入值(点灯!!!)

c 复制代码
sudo sh -c "echo '3'>/dev/rgb_led"




总结

通过本文,我们详细解析了在i.MX6ULL平台上如何:

  • 编写驱动程序控制RGB LED
  • 配置设备树定义硬件资源
  • 根据原理图理解硬件连接与设备树配置的关系

这种基于设备树的驱动开发方式具有良好的可移植性和可维护性,是现代嵌入式Linux开发的标准实践。通过理解从原理图到设备树再到驱动代码的全链路,可以更加深入地掌握嵌入式系统的软硬件协同工作原理

相关推荐
筏.k18 分钟前
grep、wc 与管道符快速上手指南
linux
Johny_Zhao23 分钟前
华为MAAS、阿里云PAI、亚马逊AWS SageMaker、微软Azure ML各大模型深度分析对比
linux·人工智能·ai·信息安全·云计算·系统运维
CodeOfCC31 分钟前
c语言 封装跨平台线程头文件
linux·c语言·windows
广药门徒32 分钟前
定时器时钟来源可以从输入捕获引脚输入
单片机·嵌入式硬件
科文小白狼33 分钟前
Linux下VSCode开发环境配置(LSP)
linux·vscode·里氏替换原则·lsp
jugt2 小时前
CentOS 7.9安装Nginx1.24.0时报 checking for LuaJIT 2.x ... not found
linux·运维·centos
多多*3 小时前
LUA+Reids实现库存秒杀预扣减 记录流水 以及自己的思考
linux·开发语言·redis·python·bootstrap·lua
何双新4 小时前
第21讲、Odoo 18 配置机制详解
linux·python·开源
21号 14 小时前
9.进程间通信
linux·运维·服务器
爱睡觉的王宇昊4 小时前
二、【ESP32开发全栈指南:ESP32 GPIO深度使用】
单片机·嵌入式硬件