当前位置: 首页 > news >正文

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

【嵌入式linux学习】:从应用层的open到led闪烁

这个blog讲讲为什么我在应用层写一句简单的open("/dev/led", O_RDWR),电路板上的 LED 灯就能亮起来?

整个过程可以看作是一次跨越三个“世界”的通信:

  1. 用户空间 (User Space):APP 只认识文件路径(字符串,如"/dev/led")。
  2. 内核空间 (Kernel Space):内核只认识数据结构inode,cdev,file)。
  3. 硬件世界 (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) 调用的readwriteioctl,全都会绕过通用逻辑,直接跳转到你写的my_readmy_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被初始化时(通常是mknoddevice_create时),字符设备的i_fop默认被指向了一个通用的内核函数:def_chr_fops

  • 当 VFS 尝试打开这个文件时,它首先调用的是这个默认的def_chr_fops->open,也就是chrdev_open

第四步:偷天换日 (最关键的一步)

chrdev_open函数内部,发生了一次“移花接木”:

  1. 内核根据inode->i_rdev(设备号),在内核的cdev_map散列表中查找对应的struct cdev对象。
  2. 找到cdev后,内核将cdev中保存的file_operations(也就是你写的驱动代码my_fops)拿出来。
  3. 替换:将当前文件对象 (struct file) 的f_op指针,修改为你写的驱动fops
  4. 调用:最后,手动调用你写的.open函数,并将inodefile传给你。

具体代码

// 伪代码:内核层处理 open 系统调用的简化逻辑intsys_open(constchar*filename,intflags){// 1. 路径解析:通过 filename 找到 inodestructinode*inode=path_lookup(filename);// 2. 创建 file 结构体(代表本次会话)structfile*filp=alloc_file();filp->f_op=inode->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*p=inode->i_cdev;if(!p){kobj=kobj_lookup(cdev_map,inode->i_rdev,...);p=container_of(kobj,structcdev,kobj);}// 2. 关键:将 file 的操作集替换为驱动定义的 fopsfilp->f_op=fops_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,驱动怎么保证不去操作 LED1?

1. 次设备号 (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. 获取次设备号intminor=iminor(inode);// 2. 根据次设备号,找到对应的硬件描述结构体structmy_led_dev*dev=&led_devs[minor];// 3. 【最佳实践】将私有数据挂在 file 结构体上// 这样以后 read/write 函数直接从 file->private_data 取,不用再看 inodefile->private_data=dev;return0;// 成功}

第三阶段:硬件层

找到了代表 LED0 的结构体led_devs[0],最后一步就是让硬件动起来。

1. 物理地址与虚拟地址

CPU 不能直接访问物理地址(如0x10008000),Linux 内核运行在虚拟地址空间。 在驱动初始化阶段(module_initprobe),我们必须做一次映射:

// 将物理地址映射为内核可操作的虚拟地址dev->reg_base=ioremap(PHYS_ADDR_LED_CTRL,SIZE);

2. 寄存器操作

回到open函数,当我们拿到了dev结构体,其实就拿到了那个映射好的reg_base指针。 虽然open通常只做初始化(如上电),但为了演示,假设我们在 open 时点亮它:

// 读-改-写 操作u32 val=ioread32(dev->reg_base);// 读取硬件寄存器当前状态val|=(1<<dev->gpio_pin);// 修改位iowrite32(val,dev->reg_base);// 写回硬件

iowrite32执行的那一瞬间,CPU 通过总线向物理内存地址发送了电信号,硬件控制器接收到信号,驱动了 MOS 管,电流导通,LED 灯亮了!

总结

用户态调用open("/dev/xxx")后触发系统调用,内核通过路径查找定位到对应的 inode, 发现是字符设备后,根据inode->i_rdevcdev_map中找到对应的struct cdev, 并将其中的file_operations绑定到当前文件对象, 随后执行驱动实现的open函数。

实际的硬件操作通常在驱动的probe阶段完成寄存器映射,并在read/write/ioctl等文件操作中通过访问寄存器来控制硬件

如果把这个过程比作一次“寄快递”:

  1. APP (open):你在快递单上填了一个地址"/dev/led0"路径)。
  2. VFS (内核):快递公司根据地址查库,发现这是寄给“老王公司”(驱动程序)下的“0号员工”(次设备号)的。
  3. Driver (驱动):老王公司收到件,派单给 0 号员工。0 号员工查阅手册,按下了他办公桌上的开关(ioremap 后的虚拟地址)。
  4. Hardware (硬件):开关连接的电缆通电,终点的灯亮起。

http://www.jsqmd.com/news/1068184/

相关文章:

  • Safety-DB实战:识别和修复10个常见Python包安全漏洞
  • Python安全必备:Safety-DB漏洞数据库完全指南
  • 3步掌握biliTickerBuy:终极B站会员购智能抢票工具完整指南
  • Cortex.js源码深度剖析:理解不可变数据包装器的实现原理
  • 文件系统初探:wyoos操作系统的ATA驱动与存储访问机制
  • ai编程的prompt
  • Clock8与其他PHP时间库对比:选择最适合你的时间管理方案
  • biliTickerBuy:从B站会员购抢票小白到高手的智能助手
  • Kepubify安全特性分析:为什么它是处理不可信电子书的安全选择
  • FRESCO与其他视频翻译工具对比:优势、局限性与适用场景
  • Speedlify终极指南:如何高效构建持续性能监控系统?
  • opsu!游戏模式与Mods指南:如何提升游戏难度与得分
  • Backslide 深度解析:10个高效创建 HTML 演示文稿的实用技巧
  • go2rtc深度架构解析:现代流媒体网关的设计哲学与性能优化
  • 5分钟开启智慧物业新时代:e家宜业开源平台完整部署指南
  • AI Voice Cloning WebUI详解:可视化界面操作与高级功能使用指南
  • vue3-openlayers核心组件解析:地图、图层与控件的终极使用指南
  • 3分钟构建你的离线语音识别系统:Whisper.cpp终极指南
  • 如何用4GB显存流畅运行SDXL模型:Fooocus低配置优化实战指南
  • charset_normalizer:如何高效解决Python字符编码检测问题的完整方案
  • Asciidoctor.js:终极JavaScript文档处理器,快速将AsciiDoc转换为HTML5
  • Scaffold-ETH 2:5分钟高效构建专业级以太坊应用的全栈开发框架
  • 5分钟上手GDevelop:零代码打造你的第一款游戏!
  • 终极指南:如何用Three.js快速构建高还原度的原神风格3D登录界面
  • Steam挂刀行情监控终极指南:5步搭建个人交易数据系统
  • 如何安装ng-inspector?3分钟快速上手Chrome与Safari扩展教程
  • Typedown快捷键自定义教程:打造个性化写作工作流
  • 从信息洪流到永久知识:Claudesidian Firecrawl如何重塑你的研究方式
  • 如何在10分钟内构建完整回合制RPG游戏?Godot Open RPG终极指南
  • 从入门到精通:GoogleNavBar 全功能 API 参考手册 [特殊字符]