【嵌入式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*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_init或probe),我们必须做一次映射:
// 将物理地址映射为内核可操作的虚拟地址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_rdev在cdev_map中找到对应的struct cdev, 并将其中的file_operations绑定到当前文件对象, 随后执行驱动实现的open函数。
实际的硬件操作通常在驱动的probe阶段完成寄存器映射,并在read/write/ioctl等文件操作中通过访问寄存器来控制硬件
如果把这个过程比作一次“寄快递”:
- APP (open):你在快递单上填了一个地址
"/dev/led0"(路径)。 - VFS (内核):快递公司根据地址查库,发现这是寄给“老王公司”(驱动程序)下的“0号员工”(次设备号)的。
- Driver (驱动):老王公司收到件,派单给 0 号员工。0 号员工查阅手册,按下了他办公桌上的开关(ioremap 后的虚拟地址)。
- Hardware (硬件):开关连接的电缆通电,终点的灯亮起。
