【嵌入式linux学习】01_1应用层open怎么到硬件控制

发布时间:2026/6/23 16:19:58
【嵌入式linux学习】01_1应用层open怎么到硬件控制 【嵌入式linux学习】从应用层的open到led闪烁这个blog讲讲为什么我在应用层写一句简单的open(/dev/led, O_RDWR)电路板上的 LED 灯就能亮起来整个过程可以看作是一次跨越三个“世界”的通信用户空间 (User Space)APP 只认识文件路径字符串如/dev/led。内核空间 (Kernel Space)内核只认识数据结构inode,cdev,file。硬件世界 (Hardware)硬件只认识物理电信号高低电平。我们的目标就是要把“字符串”变成“电信号”。总结一下1️⃣通过open产生系统调用到达内核层_通过路径查找Path Walk机制逐层解析目录,找到相应结构体inode——识别到是一个字符设备——内核根据inode-i_rdev(设备号)在内核的cdev_map散列表中查找对应的struct cdev对象 ——当chrdev_open找到cdev后它做了一个赋值操作filp-f_op cdev-ops;这里的cdev-ops就是你写的my_fops在这一行代码执行之前这个文件只是一个普通的、冷冰冰的设备节点内核只知道它有个设备号。在这一行代码执行之后这个struct file就“活”了。它绑定到了你的驱动上。关键点不仅是open用户层对这个文件描述符 (fd) 调用的read、write、ioctl全都会绕过通用逻辑直接跳转到你写的my_read、my_write函数里去。2️⃣找到设备后通过物理地址和虚拟地址映射控制寄存器从而对LED进行控制文章目录【嵌入式linux学习】从应用层的open到led闪烁第一阶段 (VFS 层)第一步从字符串到 Inode (VFS 的工作)第二步识别“我是个字符设备”第三步默认处理函数 chrdev_open第四步偷天换日 (最关键的一步)具体代码第二阶段驱动层1. 次设备号 (Minor Number) 的作用2. 代码实现逻辑第三阶段硬件层1. 物理地址与虚拟地址2. 寄存器操作总结第一阶段 (VFS 层)当 APP 调用open时系统调用触发中断CPU 陷入内核态执行sys_open。内核的第一件事是根据路径找到“负责这件事的人”。第一步从字符串到 Inode (VFS 的工作)APP 传入的是一个字符串路径如/dev/hello。内核中的sys_open无法直接操作字符串它必须把这个路径解析成内核能理解的对象。内核通过路径查找Path Walk机制逐层解析目录。最终找到/dev/hello对应的目录项结构体dentry。每个dentry都指向一个inode(索引节点)。关键点inode是文件或设备节点在文件系统中的静态表示。无论被打开多少次inode只有一个。它里面存储了最关键的信息设备号Major/Minor。第二步识别“我是个字符设备”找到inode后内核检查inode-i_mode。如果是普通文件按文件系统逻辑处理。如果是字符设备文件 (S_IFCHR)内核会意识到“这不是普通文件我需要找到对应的驱动。”此时inode结构体中的i_rdev字段包含主/次设备号成为了连接驱动的钥匙。第三步默认处理函数chrdev_open在inode被初始化时通常是mknod或device_create时字符设备的i_fop默认被指向了一个通用的内核函数def_chr_fops。当 VFS 尝试打开这个文件时它首先调用的是这个默认的def_chr_fops-open也就是chrdev_open。第四步偷天换日 (最关键的一步)在chrdev_open函数内部发生了一次“移花接木”内核根据inode-i_rdev(设备号)在内核的cdev_map散列表中查找对应的struct cdev对象。找到cdev后内核将cdev中保存的file_operations也就是你写的驱动代码my_fops拿出来。替换将当前文件对象 (struct file) 的f_op指针修改为你写的驱动fops。调用最后手动调用你写的.open函数并将inode和file传给你。具体代码// 伪代码内核层处理 open 系统调用的简化逻辑intsys_open(constchar*filename,intflags){// 1. 路径解析通过 filename 找到 inodestructinode*inodepath_lookup(filename);// 2. 创建 file 结构体代表本次会话structfile*filpalloc_file();filp-f_opinode-i_fop;// 此时还是默认的 def_chr_fops// 3. 调用 open// 如果是字符设备这里调用的是 chrdev_openif(filp-f_op-open){returnfilp-f_op-open(inode,filp);}}// 字符设备通用的 open 函数intchrdev_open(structinode*inode,structfile*filp){// 1. 根据 inode-i_rdev (设备号) 找到 cdevstructcdev*pinode-i_cdev;if(!p){kobjkobj_lookup(cdev_map,inode-i_rdev,...);pcontainer_of(kobj,structcdev,kobj);}// 2. 关键将 file 的操作集替换为驱动定义的 fopsfilp-f_opfops_get(p-ops);// 3. 真正调用你自己写的驱动 open 函数if(filp-f_op-open){returnfilp-f_op-open(inode,filp);}}第二阶段驱动层现在执行流终于进入了你写的驱动代码中的.open函数。intmy_driver_open(structinode*inode,structfile*file);这个时候最关键的问题来了驱动怎么知道你要打开具体的哪一个硬件假设你的驱动管理了 4 个 LED 灯APP 打开/dev/led0驱动怎么保证不去操作 LED11. 次设备号 (Minor Number) 的作用答案就在inode参数里。主设备号决定了用哪个驱动程序找对类。次设备号决定了用该驱动下的哪个具体设备找对人。2. 代码实现逻辑在驱动的open函数中通常有以下标准操作structmy_led_dev{void__iomem*reg_base;// 寄存器基地址intgpio_pin;// 具体引脚号};// 假设我们有多个 LED 设备对象structmy_led_devled_devs[4];intmy_driver_open(structinode*inode,structfile*file){// 1. 获取次设备号intminoriminor(inode);// 2. 根据次设备号找到对应的硬件描述结构体structmy_led_dev*devled_devs[minor];// 3. 【最佳实践】将私有数据挂在 file 结构体上// 这样以后 read/write 函数直接从 file-private_data 取不用再看 inodefile-private_datadev;return0;// 成功}第三阶段硬件层找到了代表 LED0 的结构体led_devs[0]最后一步就是让硬件动起来。1. 物理地址与虚拟地址CPU 不能直接访问物理地址如0x10008000Linux 内核运行在虚拟地址空间。 在驱动初始化阶段module_init或probe我们必须做一次映射// 将物理地址映射为内核可操作的虚拟地址dev-reg_baseioremap(PHYS_ADDR_LED_CTRL,SIZE);2. 寄存器操作回到open函数当我们拿到了dev结构体其实就拿到了那个映射好的reg_base指针。 虽然open通常只做初始化如上电但为了演示假设我们在 open 时点亮它// 读-改-写 操作u32 valioread32(dev-reg_base);// 读取硬件寄存器当前状态val|(1dev-gpio_pin);// 修改位iowrite32(val,dev-reg_base);// 写回硬件当iowrite32执行的那一瞬间CPU 通过总线向物理内存地址发送了电信号硬件控制器接收到信号驱动了 MOS 管电流导通LED 灯亮了总结用户态调用open(/dev/xxx)后触发系统调用内核通过路径查找定位到对应的 inode 发现是字符设备后根据inode-i_rdev在cdev_map中找到对应的struct cdev 并将其中的file_operations绑定到当前文件对象 随后执行驱动实现的open函数。实际的硬件操作通常在驱动的probe阶段完成寄存器映射并在read/write/ioctl等文件操作中通过访问寄存器来控制硬件如果把这个过程比作一次“寄快递”APP (open)你在快递单上填了一个地址/dev/led0路径。VFS (内核)快递公司根据地址查库发现这是寄给“老王公司”驱动程序下的“0号员工”次设备号的。Driver (驱动)老王公司收到件派单给 0 号员工。0 号员工查阅手册按下了他办公桌上的开关ioremap 后的虚拟地址。Hardware (硬件)开关连接的电缆通电终点的灯亮起。