Linux 驱动开发基础(2)-设备树LED驱动前情回顾 Linux 驱动开发基础1—— 设备树详解。在上一节中我们了解了设备树Device Tree的基本语法和内核操作函数。本节将正式实战编写一个基于设备树的 LED 字符设备驱动。在前几节的 LED 驱动中我们在驱动代码里硬编码了寄存器的物理地址并使用ioremap映射。而本节我们将采用Linux 内核推荐的标准做法设备树DTS描述硬件信息寄存器地址、属性。驱动代码Driver通过内核提供的OF 函数Open Firmware读取设备树信息实现硬件控制。学习重点DTS 编写在.dts文件中创建设备节点。属性获取在驱动中读取compatible、status、reg等属性。GPIO 初始化使用获取到的属性初始化 LED 并实现闪烁。01 修改设备树文件 (.dts)我们需要在板子对应的.dts文件如imx6ull-14x14-evk.dts的根节点/下添加一个名为alphaled的子节点。添加设备节点/* 在根节点 / 下添加 alphaled 节点 */alphaled{#address-cells1;#size-cells1;compatibleatkalpha-led;/* 驱动匹配关键词 */statusokay;/* 设备状态 *//* * reg 属性定义了 LED 驱动需要的寄存器物理地址和长度 * 格式起始地址 长度 * 对应关系 * Index 0: CCM_CCGR1 (时钟) * Index 1: SW_MUX_GPIO1_IO03 (复用) * Index 2: SW_PAD_GPIO1_IO03 (电气属性) * Index 3: GPIO1_DR (数据) * Index 4: GPIO1_GDIR (方向) */reg0X020C406C0X04/* CCM_CCGR1_BASE */0X020E00680X04/* SW_MUX_GPIO1_IO03_BASE */0X020E02F40X04/* SW_PAD_GPIO1_IO03_BASE */0X0209C0000X04/* GPIO1_DR_BASE */0X0209C0040X04;/* GPIO1_GDIR_BASE */};⚠️ 注意修改完 dts 文件后必须重新编译设备树make dtbs并使用新的.dtb文件启动开发板。02 LED 驱动程序编写新建dtsled.c本驱动采用了新字符设备注册方式alloc_chrdev_regioncdev并集成了class_create自动创建设备节点的功能。核心代码详解#includelinux/types.h#includelinux/kernel.h#includelinux/delay.h#includelinux/ide.h#includelinux/init.h#includelinux/module.h#includelinux/errno.h#includelinux/gpio.h#includelinux/cdev.h#includelinux/device.h#includelinux/of.h/* 设备树核心头文件 */#includelinux/of_address.h/* OF 地址转换相关 */#includeasm/mach/map.h#includeasm/uaccess.h#includeasm/io.h#defineDTSLED_CNT1/* 设备号个数 */#defineDTSLED_NAMEdtsled/* 设备名 /dev/dtsled */#defineLEDOFF0/* 关灯 */#defineLEDON1/* 开灯 *//* 映射后的虚拟地址指针 */staticvoid__iomem*IMX6U_CCM_CCGR1;staticvoid__iomem*SW_MUX_GPIO1_IO03;staticvoid__iomem*SW_PAD_GPIO1_IO03;staticvoid__iomem*GPIO1_DR;staticvoid__iomem*GPIO1_GDIR;/* dtsled 设备结构体 */structdtsled_dev{dev_tdevid;/* 设备号 */structcdevcdev;/* cdev 字符设备结构 */structclass*class;/* 类用于自动创建设备节点 */structdevice*device;/* 设备 */intmajor;/* 主设备号 */intminor;/* 次设备号 */structdevice_node*nd;/* 存放对应的设备树节点 */};structdtsled_devdtsled;/* 声明设备实例 *//** * brief LED 硬件控制函数 * param sta : LEDON(0) 打开, LEDOFF(1) 关闭 * note I.MX6ULL 的 LED 为低电平点亮 */voidled_switch(u8 sta){u32 val0;if(staLEDON){valreadl(GPIO1_DR);val~(13);/* bit3 清零输出低电平 */writel(val,GPIO1_DR);}elseif(staLEDOFF){valreadl(GPIO1_DR);val|(13);/* bit3 置一输出高电平 */writel(val,GPIO1_DR);}}/* * brief 打开设备 * note 一般在 open 中将 private_data 指向设备结构体方便后续函数使用 */staticintled_open(structinode*inode,structfile*filp){filp-private_datadtsled;return0;}/* * brief 从设备读取数据 (本例未实现) */staticssize_tled_read(structfile*filp,char__user*buf,size_tcnt,loff_t*offt){return0;}/* * brief 向设备写入数据 (控制 LED) * param buf : 用户空间传递的数据0为开灯1为关灯 */staticssize_tled_write(structfile*filp,constchar__user*buf,size_tcnt,loff_t*offt){intretvalue;unsignedchardatabuf[1];unsignedcharledstat;/* 获取用户空间数据 */retvaluecopy_from_user(databuf,buf,cnt);if(retvalue0){printk(kernel write failed!\r\n);return-EFAULT;}ledstatdatabuf[0];/* 根据指令控制 LED */if(ledstatLEDON){led_switch(LEDON);}elseif(ledstatLEDOFF){led_switch(LEDOFF);}return0;}/* * brief 关闭/释放设备 */staticintled_release(structinode*inode,structfile*filp){return0;}/* file_operations 结构体 */staticstructfile_operationsdtsled_fops{.ownerTHIS_MODULE,.openled_open,.readled_read,.writeled_write,.releaseled_release,};/* * brief 驱动入口函数 * note 完成设备树节点获取、内存映射、硬件初始化、字符设备注册 */staticint__initled_init(void){u32 val0;intret;u32 regdata[14];constchar*str;structproperty*proper;/* 1. 获取设备树属性 *//* 1.1 获取设备节点通过路径查找 /alphaled */dtsled.ndof_find_node_by_path(/alphaled);if(dtsled.ndNULL){printk(alphaled node can not found!\r\n);return-EINVAL;}else{printk(alphaled node has been found!\r\n);}/* 1.2 获取 compatible 属性 (仅做演示非必须) */properof_find_property(dtsled.nd,compatible,NULL);if(properNULL){printk(compatible property find failed\r\n);}else{printk(compatible %s\r\n,(char*)proper-value);}/* 1.3 获取 status 属性 */retof_property_read_string(dtsled.nd,status,str);if(ret0){printk(status read failed!\r\n);}else{printk(status %s\r\n,str);}/* 1.4 获取 reg 属性 (原始数组方式读取) */retof_property_read_u32_array(dtsled.nd,reg,regdata,10);if(ret0){printk(reg property read failed!\r\n);}else{u8 i0;printk(reg data:\r\n);for(i0;i10;i)printk(%#X ,regdata[i]);printk(\r\n);}/* 2. 初始化 LED 硬件 *//* * 内存映射推荐使用 of_iomap * of_iomap(node, index) 会自动从 reg 属性中提取第 index 组的地址和长度进行映射 */IMX6U_CCM_CCGR1of_iomap(dtsled.nd,0);SW_MUX_GPIO1_IO03of_iomap(dtsled.nd,1);SW_PAD_GPIO1_IO03of_iomap(dtsled.nd,2);GPIO1_DRof_iomap(dtsled.nd,3);GPIO1_GDIRof_iomap(dtsled.nd,4);/* 2.1 使能 GPIO1 时钟 */valreadl(IMX6U_CCM_CCGR1);val~(326);/* 清除以前的设置 */val|(326);/* 设置新值 (11b) */writel(val,IMX6U_CCM_CCGR1);/* 2.2 设置复用功能 (MUX) */writel(5,SW_MUX_GPIO1_IO03);/* 2.3 设置电气属性 (PAD) */writel(0x10B0,SW_PAD_GPIO1_IO03);/* 2.4 设置 GPIO 方向为输出 */valreadl(GPIO1_GDIR);val~(13);val|(13);writel(val,GPIO1_GDIR);/* 2.5 默认关闭 LED (高电平关闭) */valreadl(GPIO1_DR);val|(13);writel(val,GPIO1_DR);/* 3. 注册字符设备驱动 *//* 3.1 申请设备号 */if(dtsled.major){dtsled.devidMKDEV(dtsled.major,0);register_chrdev_region(dtsled.devid,DTSLED_CNT,DTSLED_NAME);}else{/* 动态申请设备号 */alloc_chrdev_region(dtsled.devid,0,DTSLED_CNT,DTSLED_NAME);dtsled.majorMAJOR(dtsled.devid);dtsled.minorMINOR(dtsled.devid);}printk(dtsled major%d, minor%d\r\n,dtsled.major,dtsled.minor);/* 3.2 初始化 cdev 并添加到内核 */dtsled.cdev.ownerTHIS_MODULE;cdev_init(dtsled.cdev,dtsled_fops);cdev_add(dtsled.cdev,dtsled.devid,DTSLED_CNT);/* 3.3 创建类 (在 /sys/class/ 下创建目录) */dtsled.classclass_create(THIS_MODULE,DTSLED_NAME);if(IS_ERR(dtsled.class)){returnPTR_ERR(dtsled.class);}/* 3.4 创建设备 (在 /dev/ 下创建节点文件) */dtsled.devicedevice_create(dtsled.class,NULL,dtsled.devid,NULL,DTSLED_NAME);if(IS_ERR(dtsled.device)){returnPTR_ERR(dtsled.device);}return0;}/* * brief 驱动出口函数 */staticvoid__exitled_exit(void){/* 1. 取消地址映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 2. 注销字符设备 */cdev_del(dtsled.cdev);unregister_chrdev_region(dtsled.devid,DTSLED_CNT);/* 3. 销毁设备与类 */device_destroy(dtsled.class,dtsled.devid);class_destroy(dtsled.class);}module_init(led_init);module_exit(led_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(xxxx);03 小结本例程通过以下三个关键步骤实现了设备树驱动节点查找使用of_find_node_by_path找到设备树中的节点。属性读取使用of_property_read_u32_array等函数获取reg属性。地址映射使用of_iomap替代传统的ioremap它自动解析reg属性中的物理地址和长度更加简便。这种方式将“硬件描述”DTS与“驱动逻辑”.c分离开来当硬件修改时只需修改 DTS 即可无需重新编译驱动代码。
Linux 驱动开发基础(2)——设备树LED驱动
Linux 驱动开发基础(2)-设备树LED驱动前情回顾 Linux 驱动开发基础1—— 设备树详解。在上一节中我们了解了设备树Device Tree的基本语法和内核操作函数。本节将正式实战编写一个基于设备树的 LED 字符设备驱动。在前几节的 LED 驱动中我们在驱动代码里硬编码了寄存器的物理地址并使用ioremap映射。而本节我们将采用Linux 内核推荐的标准做法设备树DTS描述硬件信息寄存器地址、属性。驱动代码Driver通过内核提供的OF 函数Open Firmware读取设备树信息实现硬件控制。学习重点DTS 编写在.dts文件中创建设备节点。属性获取在驱动中读取compatible、status、reg等属性。GPIO 初始化使用获取到的属性初始化 LED 并实现闪烁。01 修改设备树文件 (.dts)我们需要在板子对应的.dts文件如imx6ull-14x14-evk.dts的根节点/下添加一个名为alphaled的子节点。添加设备节点/* 在根节点 / 下添加 alphaled 节点 */alphaled{#address-cells1;#size-cells1;compatibleatkalpha-led;/* 驱动匹配关键词 */statusokay;/* 设备状态 *//* * reg 属性定义了 LED 驱动需要的寄存器物理地址和长度 * 格式起始地址 长度 * 对应关系 * Index 0: CCM_CCGR1 (时钟) * Index 1: SW_MUX_GPIO1_IO03 (复用) * Index 2: SW_PAD_GPIO1_IO03 (电气属性) * Index 3: GPIO1_DR (数据) * Index 4: GPIO1_GDIR (方向) */reg0X020C406C0X04/* CCM_CCGR1_BASE */0X020E00680X04/* SW_MUX_GPIO1_IO03_BASE */0X020E02F40X04/* SW_PAD_GPIO1_IO03_BASE */0X0209C0000X04/* GPIO1_DR_BASE */0X0209C0040X04;/* GPIO1_GDIR_BASE */};⚠️ 注意修改完 dts 文件后必须重新编译设备树make dtbs并使用新的.dtb文件启动开发板。02 LED 驱动程序编写新建dtsled.c本驱动采用了新字符设备注册方式alloc_chrdev_regioncdev并集成了class_create自动创建设备节点的功能。核心代码详解#includelinux/types.h#includelinux/kernel.h#includelinux/delay.h#includelinux/ide.h#includelinux/init.h#includelinux/module.h#includelinux/errno.h#includelinux/gpio.h#includelinux/cdev.h#includelinux/device.h#includelinux/of.h/* 设备树核心头文件 */#includelinux/of_address.h/* OF 地址转换相关 */#includeasm/mach/map.h#includeasm/uaccess.h#includeasm/io.h#defineDTSLED_CNT1/* 设备号个数 */#defineDTSLED_NAMEdtsled/* 设备名 /dev/dtsled */#defineLEDOFF0/* 关灯 */#defineLEDON1/* 开灯 *//* 映射后的虚拟地址指针 */staticvoid__iomem*IMX6U_CCM_CCGR1;staticvoid__iomem*SW_MUX_GPIO1_IO03;staticvoid__iomem*SW_PAD_GPIO1_IO03;staticvoid__iomem*GPIO1_DR;staticvoid__iomem*GPIO1_GDIR;/* dtsled 设备结构体 */structdtsled_dev{dev_tdevid;/* 设备号 */structcdevcdev;/* cdev 字符设备结构 */structclass*class;/* 类用于自动创建设备节点 */structdevice*device;/* 设备 */intmajor;/* 主设备号 */intminor;/* 次设备号 */structdevice_node*nd;/* 存放对应的设备树节点 */};structdtsled_devdtsled;/* 声明设备实例 *//** * brief LED 硬件控制函数 * param sta : LEDON(0) 打开, LEDOFF(1) 关闭 * note I.MX6ULL 的 LED 为低电平点亮 */voidled_switch(u8 sta){u32 val0;if(staLEDON){valreadl(GPIO1_DR);val~(13);/* bit3 清零输出低电平 */writel(val,GPIO1_DR);}elseif(staLEDOFF){valreadl(GPIO1_DR);val|(13);/* bit3 置一输出高电平 */writel(val,GPIO1_DR);}}/* * brief 打开设备 * note 一般在 open 中将 private_data 指向设备结构体方便后续函数使用 */staticintled_open(structinode*inode,structfile*filp){filp-private_datadtsled;return0;}/* * brief 从设备读取数据 (本例未实现) */staticssize_tled_read(structfile*filp,char__user*buf,size_tcnt,loff_t*offt){return0;}/* * brief 向设备写入数据 (控制 LED) * param buf : 用户空间传递的数据0为开灯1为关灯 */staticssize_tled_write(structfile*filp,constchar__user*buf,size_tcnt,loff_t*offt){intretvalue;unsignedchardatabuf[1];unsignedcharledstat;/* 获取用户空间数据 */retvaluecopy_from_user(databuf,buf,cnt);if(retvalue0){printk(kernel write failed!\r\n);return-EFAULT;}ledstatdatabuf[0];/* 根据指令控制 LED */if(ledstatLEDON){led_switch(LEDON);}elseif(ledstatLEDOFF){led_switch(LEDOFF);}return0;}/* * brief 关闭/释放设备 */staticintled_release(structinode*inode,structfile*filp){return0;}/* file_operations 结构体 */staticstructfile_operationsdtsled_fops{.ownerTHIS_MODULE,.openled_open,.readled_read,.writeled_write,.releaseled_release,};/* * brief 驱动入口函数 * note 完成设备树节点获取、内存映射、硬件初始化、字符设备注册 */staticint__initled_init(void){u32 val0;intret;u32 regdata[14];constchar*str;structproperty*proper;/* 1. 获取设备树属性 *//* 1.1 获取设备节点通过路径查找 /alphaled */dtsled.ndof_find_node_by_path(/alphaled);if(dtsled.ndNULL){printk(alphaled node can not found!\r\n);return-EINVAL;}else{printk(alphaled node has been found!\r\n);}/* 1.2 获取 compatible 属性 (仅做演示非必须) */properof_find_property(dtsled.nd,compatible,NULL);if(properNULL){printk(compatible property find failed\r\n);}else{printk(compatible %s\r\n,(char*)proper-value);}/* 1.3 获取 status 属性 */retof_property_read_string(dtsled.nd,status,str);if(ret0){printk(status read failed!\r\n);}else{printk(status %s\r\n,str);}/* 1.4 获取 reg 属性 (原始数组方式读取) */retof_property_read_u32_array(dtsled.nd,reg,regdata,10);if(ret0){printk(reg property read failed!\r\n);}else{u8 i0;printk(reg data:\r\n);for(i0;i10;i)printk(%#X ,regdata[i]);printk(\r\n);}/* 2. 初始化 LED 硬件 *//* * 内存映射推荐使用 of_iomap * of_iomap(node, index) 会自动从 reg 属性中提取第 index 组的地址和长度进行映射 */IMX6U_CCM_CCGR1of_iomap(dtsled.nd,0);SW_MUX_GPIO1_IO03of_iomap(dtsled.nd,1);SW_PAD_GPIO1_IO03of_iomap(dtsled.nd,2);GPIO1_DRof_iomap(dtsled.nd,3);GPIO1_GDIRof_iomap(dtsled.nd,4);/* 2.1 使能 GPIO1 时钟 */valreadl(IMX6U_CCM_CCGR1);val~(326);/* 清除以前的设置 */val|(326);/* 设置新值 (11b) */writel(val,IMX6U_CCM_CCGR1);/* 2.2 设置复用功能 (MUX) */writel(5,SW_MUX_GPIO1_IO03);/* 2.3 设置电气属性 (PAD) */writel(0x10B0,SW_PAD_GPIO1_IO03);/* 2.4 设置 GPIO 方向为输出 */valreadl(GPIO1_GDIR);val~(13);val|(13);writel(val,GPIO1_GDIR);/* 2.5 默认关闭 LED (高电平关闭) */valreadl(GPIO1_DR);val|(13);writel(val,GPIO1_DR);/* 3. 注册字符设备驱动 *//* 3.1 申请设备号 */if(dtsled.major){dtsled.devidMKDEV(dtsled.major,0);register_chrdev_region(dtsled.devid,DTSLED_CNT,DTSLED_NAME);}else{/* 动态申请设备号 */alloc_chrdev_region(dtsled.devid,0,DTSLED_CNT,DTSLED_NAME);dtsled.majorMAJOR(dtsled.devid);dtsled.minorMINOR(dtsled.devid);}printk(dtsled major%d, minor%d\r\n,dtsled.major,dtsled.minor);/* 3.2 初始化 cdev 并添加到内核 */dtsled.cdev.ownerTHIS_MODULE;cdev_init(dtsled.cdev,dtsled_fops);cdev_add(dtsled.cdev,dtsled.devid,DTSLED_CNT);/* 3.3 创建类 (在 /sys/class/ 下创建目录) */dtsled.classclass_create(THIS_MODULE,DTSLED_NAME);if(IS_ERR(dtsled.class)){returnPTR_ERR(dtsled.class);}/* 3.4 创建设备 (在 /dev/ 下创建节点文件) */dtsled.devicedevice_create(dtsled.class,NULL,dtsled.devid,NULL,DTSLED_NAME);if(IS_ERR(dtsled.device)){returnPTR_ERR(dtsled.device);}return0;}/* * brief 驱动出口函数 */staticvoid__exitled_exit(void){/* 1. 取消地址映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 2. 注销字符设备 */cdev_del(dtsled.cdev);unregister_chrdev_region(dtsled.devid,DTSLED_CNT);/* 3. 销毁设备与类 */device_destroy(dtsled.class,dtsled.devid);class_destroy(dtsled.class);}module_init(led_init);module_exit(led_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(xxxx);03 小结本例程通过以下三个关键步骤实现了设备树驱动节点查找使用of_find_node_by_path找到设备树中的节点。属性读取使用of_property_read_u32_array等函数获取reg属性。地址映射使用of_iomap替代传统的ioremap它自动解析reg属性中的物理地址和长度更加简便。这种方式将“硬件描述”DTS与“驱动逻辑”.c分离开来当硬件修改时只需修改 DTS 即可无需重新编译驱动代码。