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

发布于:2025-03-14 ⋅ 阅读:(16) ⋅ 点赞:(0)

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



前言

在嵌入式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_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 匹配表和平台驱动结构体

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 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
    这是为什么设备树中我们配置了:

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开发的标准实践。通过理解从原理图到设备树再到驱动代码的全链路,可以更加深入地掌握嵌入式系统的软硬件协同工作原理