ARM Linux 驱动开发篇--- 设备树下的 LED 驱动实验-- Ubuntu20.04

ARM Linux 驱动开发篇--- 设备树下的 LED 驱动实验-- Ubuntu20.04
🎬 渡水无言个人主页渡水无言

专栏传送门: 《linux专栏》   《嵌入式linux驱动开发》
⭐️流水不争先,争的是滔滔不绝

 📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、设备树 LED 驱动原理

二、硬件原理图分析(看过之前的博客可以忽略了)

三、实验程序编写

四、LED 灯驱动程序编写

五、编写测试 APP

六、运行测试

6.1、编译驱动程序和测试 APP

总结


前言

前几期博客我们详细的讲解了设备树语法以及在驱动开发中常用的 OF 函数,本期博客就正式开始

第一个基于设备树的 Linux 驱动实验。


一、设备树 LED 驱动原理

本期博客使用设备树来向 Linux 内核传递相关的寄存器物理地址,Linux 驱动文件使用上一章讲解的OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的 IO。本章实验还是比较简单的,本章实验重点内容如下:

①、在 imx6ull-alientek-emmc.dts 文件中创建相应的设备节点。

②、编写驱动程序(新版led驱动实验程序编写),获取设备树中的相关属性值。

③、使用获取到的有关属性值来初始化LED所使用的GPIO。

二、硬件原理图分析(看过之前的博客可以忽略了)

从图中可以看出,LED0 接到了 GPIO_3 上,GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03 输出低电平 (0) 的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平 (1) 的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03 的输出电平,输出 0 就亮,输出 1 就灭。
 

三、实验程序编写

在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-alientek-emmc.dts 文件, 在根节点“/”最后面输入如下所示内容:

alphaled { #address-cells = <1>; #size-cells = <1>; compatible = "atkalpha-led"; status = "okay"; reg = < 0x020C406C 0x04 /* CCM_CCGR1_BASE */ 0x020E0068 0x04 /* SW_MUX_GPIO1_IO03_BASE */ 0x020E02F4 0x04 /* SW_PAD_GPIO1_IO03_BASE */ 0x0209C000 0x04 /* GPIO1_DR_BASE */ 0x0209C004 0x04 >; /* GPIO1_GDIR_BASE */ };

属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长。
(cell),地址长度也占用一个字长(cell)。

属性 compatbile 设置 alphaled 节点兼容性为“atkalpha-led”。
属性 status 设置状态为“okay”。
reg 属性设置了驱动里面所要使用的寄存器物理地址。

比如0X020C406C 0X04表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地址为 0X020C406C,长度为 4 个字节。
设备树修改完成以后输入如下命令重新编译一下 imx6ull-alientek-emmc.dts。

make dtbs

编译完成以后得到 imx6ull-alientek-emmc.dtb,使用新的 imx6ull-alientek-emmc.dtb 启动Linux 内核。Linux 启动成功以后进入到/proc/device-tree/目录中查看是否有“alphaled”这个节点。

四、LED 灯驱动程序编写

#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 版本 : V1.0 描述 : LED驱动文件。 其他 : 无 ***************************************************************/ #define DTSLED_CNT 1 /* 设备号个数 */ #define DTSLED_NAME "dtsled" /* 名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* 映射后的寄存器虚拟地址指针 */ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO03; static void __iomem *SW_PAD_GPIO1_IO03; static void __iomem *GPIO1_DR; static void __iomem *GPIO1_GDIR; /* dtsled设备结构体 */ struct dtsled_dev{ dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ struct device_node *nd; /* 设备节点 */ }; struct dtsled_dev dtsled; /* led设备 */ /* * @description : LED打开/关闭 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED * @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) { val = readl(GPIO1_DR); val &= ~(1 << 3); writel(val, GPIO1_DR); }else if(sta == LEDOFF) { val = readl(GPIO1_DR); val|= (1 << 3); writel(val, GPIO1_DR); } } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { filp->private_data = &dtsled; /* 设置私有数据 */ return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat; retvalue = copy_from_user(databuf, buf, cnt); if(retvalue < 0) { printk("kernel write failed!\r\n"); return -EFAULT; } ledstat = databuf[0]; /* 获取状态值 */ if(ledstat == LEDON) { led_switch(LEDON); /* 打开LED灯 */ } else if(ledstat == LEDOFF) { led_switch(LEDOFF); /* 关闭LED灯 */ } return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 设备操作函数 */ static struct file_operations dtsled_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static int __init led_init(void) { u32 val = 0; int ret; u32 regdata[14]; const char *str; struct property *proper; /* 获取设备树中的属性数据 */ /* 1、获取设备节点:alphaled */ dtsled.nd = of_find_node_by_path("/alphaled"); if(dtsled.nd == NULL) { printk("alphaled node nost find!\r\n"); return -EINVAL; } else { printk("alphaled node find!\r\n"); } /* 2、获取compatible属性内容 */ proper = of_find_property(dtsled.nd, "compatible", NULL); if(proper == NULL) { printk("compatible property find failed\r\n"); } else { printk("compatible = %s\r\n", (char*)proper->value); } /* 3、获取status属性内容 */ ret = of_property_read_string(dtsled.nd, "status", &str); if(ret < 0){ printk("status read failed!\r\n"); } else { printk("status = %s\r\n",str); } /* 4、获取reg属性内容 */ ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10); if(ret < 0) { printk("reg property read failed!\r\n"); } else { u8 i = 0; printk("reg data:\r\n"); for(i = 0; i < 10; i++) printk("%#X ", regdata[i]); printk("\r\n"); } /* 初始化LED */ #if 0 /* 1、寄存器地址映射 */ IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]); SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]); SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]); GPIO1_DR = ioremap(regdata[6], regdata[7]); GPIO1_GDIR = ioremap(regdata[8], regdata[9]); #else IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1); SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2); GPIO1_DR = of_iomap(dtsled.nd, 3); GPIO1_GDIR = of_iomap(dtsled.nd, 4); #endif /* 2、使能GPIO1时钟 */ val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); /* 清楚以前的设置 */ val |= (3 << 26); /* 设置新值 */ writel(val, IMX6U_CCM_CCGR1); /* 3、设置GPIO1_IO03的复用功能,将其复用为 * GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03); /*寄存器SW_PAD_GPIO1_IO03设置IO属性 *bit 16:0 HYS关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper功能 *bit [12]: 1 pull/keeper使能 *bit [11]: 0 关闭开路输出 *bit [7:6]: 10 速度100Mhz *bit [5:3]: 110 R0/6驱动能力 *bit [0]: 0 低转换率 */ writel(0x10B0, SW_PAD_GPIO1_IO03); /* 4、设置GPIO1_IO03为输出功能 */ val = readl(GPIO1_GDIR); val &= ~(1 << 3); /* 清除以前的设置 */ val |= (1 << 3); /* 设置为输出 */ writel(val, GPIO1_GDIR); /* 5、默认关闭LED */ val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); /* 注册字符设备驱动 */ /* 1、创建设备号 */ if (dtsled.major) { /* 定义了设备号 */ dtsled.devid = MKDEV(dtsled.major, 0); register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME); } else { /* 没有定义设备号 */ alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */ dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */ dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */ } printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor); /* 2、初始化cdev */ dtsled.cdev.owner = THIS_MODULE; cdev_init(&dtsled.cdev, &dtsled_fops); /* 3、添加一个cdev */ cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT); /* 4、创建类 */ dtsled.class = class_create(THIS_MODULE, DTSLED_NAME); if (IS_ERR(dtsled.class)) { return PTR_ERR(dtsled.class); } /* 5、创建设备 */ dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME); if (IS_ERR(dtsled.device)) { return PTR_ERR(dtsled.device); } return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); /* 注销字符设备驱动 */ cdev_del(&dtsled.cdev);/* 删除cdev */ unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */ device_destroy(dtsled.class, dtsled.devid); class_destroy(dtsled.class); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("duan"); 

dtsled.c 文件中的内容其实和之前的led实验的内容基本一样,只是 dtsled.c 中包含了处理设备树的代码,我们重点来看一下这部分代码。

在设备结构体 dtsled_dev 中添加了成员变量 nd,nd 是 device_node 结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加 device_node 指针变量来存放这个节点。

通过 of_find_node_by_path 函数得到 alphaled 节点,后续其他的 OF 函数要 使用 device_node。

通过 of_find_property 函数获取 alphaled 节点的 compatible 属性,返回值为 property 结构体类型指针变量,property 的成员变量 value 表示属性值。

通过 of_property_read_string 函数获取 alphaled 节点的 status 属性值。

通过 of_property_read_u32_array 函数获取 alphaled 节点的 reg 属性所有值, 并且将获取到的值都存放到 regdata 数组中。

使用“古老”的 ioremap 函数完成内存映射,将获取到的 regdata 数组中的寄存器物理地址转换为虚拟地址。

使用 of_iomap 函数一次性完成读取 reg 属性以及内存映射,of_iomap 函数是设备树推荐使用的 OF 函数。

五、编写测试 APP

本次博客直接使用的测试 APP和之前的led实验一样,将之前实验的 ledApp.c 文件复制到本章实验工程下即可。

#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : ledApp.c 版本 : V1.0 描述 :led驱测试APP。 其他 : 无 使用方法 :./ledtest /dev/led 0 关闭LED ./ledtest /dev/led 1 打开LED ***************************************************************/ #define LEDOFF 0 #define LEDON 1 /* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功;其他 失败 */ int main(int argc, char *argv[]) { int fd, retvalue; char *filename; unsigned char databuf[1]; if(argc != 3){ printf("Error Usage!\r\n"); return -1; } filename = argv[1]; /* 打开led驱动 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("file %s open failed!\r\n", argv[1]); return -1; } databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */ /* 向/dev/led文件写入数据 */ retvalue = write(fd, databuf, sizeof(databuf)); if(retvalue < 0){ printf("LED Control Failed!\r\n"); close(fd); return -1; } retvalue = close(fd); /* 关闭文件 */ if(retvalue < 0){ printf("file %s close failed!\r\n", argv[1]); return -1; } return 0; }

六、运行测试

6.1、编译驱动程序和测试 APP

编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为 dtsled.o,Makefile 内容如下所示:

KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2 CURRENT_PATH := $(shell pwd) obj-m := dtsled.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译测试 APP输入如下命令编译测试 ledApp.c 这个测试程序:

arm-linux-gnueabihf-gcc ledApp.c -o ledApp

6.2、运行测试

将上一小节编译出来的 dtsled.ko 和 ledApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

sudo cp dtsled.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
sudo cp ledApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

进入到目录 lib/modules/4.1.15 中,输入如下命令加载 dtsled.ko 驱动模块:

depmod //第一次加载驱动的时候需要运行此命令 modprobe dtsled.ko //加载驱动

驱动加载成功以后会在终端中输出一些信息,如下图所示:

从上图中可以看出,alpahled 这个节点找到了,并且 compatible 属性值为“atkalpha-led”,status 属性值为“okay”。reg 属性的值为“0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4”,这些都和我们设置的设备树一致。驱动加载成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打开 LED 灯:

./ledApp /dev/dtsled 1 //打开 LED 灯

输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭 LED 灯:

./ledApp /dev/dtsled 0//关闭 LED 灯

输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否熄灭。如果要卸载驱动的话输入如下命令即可:

rmmod dtsled.ko


总结

本期博客就正式完成了第一个基于设备树的 Linux 驱动实验。

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk