
这篇文章整理自课程第 13-15 章。它们分别对应视频采集、传感器统一接口,以及临时验证与正式驱动之间的边界,是整套课后半段最能拉开工程认知的一组内容。
源文件地址:git clone https://e.coding.net/weidongshan/linux/doc_and_source_for_drivers.git
13_V4L2 学习总结
TL;DR:先别急着背术语,先把这件事想成"怎么从
/dev/videoX拿到一帧图像"。用户程序先打开/dev/videoX,问它支持什么格式和分辨率,再申请几块给摄像头写数据的内存,把这些内存排队交给驱动。驱动从硬件拿到一帧图像后,填进其中一块内存,用户程序再把这块内存取出来,于是就能把它保存成 jpg,或者直接显示到 LCD。这一章真正要学的,就是把这条"打开设备 -> 选格式 -> 申请 buffer -> 开始采集 -> 取出一帧 -> 再放回去继续采集"的链路看通。等这条白话链路稳定后,再把它和
VIDIOC_*、vb2、虚拟摄像头、USB 摄像头驱动这些名字对上。
章节定位
13_V4L2 是课程里第一次系统进入 Linux 多媒体子系统。
它在整套课里的位置,建议这样理解:
| 章节关系 | 这一章承接了什么 | 这一章新增了什么 |
|---|---|---|
往前承接 03_LCD |
前面已经见过图像最后怎么显示到屏幕 | 这章开始解释图像怎么从摄像头进入系统 |
往前承接 08_Interrupt |
设备准备好数据后,最终还是要靠中断/唤醒推进流程 | 这章把连续帧数据接到用户态 |
往前承接 12_USB |
已经知道 USB 设备怎么枚举、怎么匹配驱动 | 这章拿 USB 摄像头作为具体设备类来分析 |
| 往后延伸到更完整的视频链路 | 后续如果继续学 sensor、subdev、media controller、ISP | 这一章是用户态采集和基础驱动框架的地基 |
所以,这一章的边界要先划清:
- 它重点不是讲图像算法,也不是讲 ISP 细节。
- 它重点是讲 Linux 怎样把摄像头统一抽象成
/dev/videoX,并把一帧一帧的数据安全、连续地交给应用。
一句话概括:这章是在建立"摄像头采集链路"的心智模型。
先把"一帧图像怎么到用户程序"讲白
把这章的主线先压成最朴素的 8 步:
- 用户程序打开
/dev/videoX - 先问设备:你会什么格式、什么分辨率
- 选一种双方都能接受的格式
- 申请几块内存,当作"装图像的盒子"
- 把这些空盒子交给驱动
- 驱动从摄像头拿到一帧图像后,往某个盒子里写数据
- 用户程序把这个已经装满图像的盒子取回来
- 用户程序把图像保存成 jpg、显示到 LCD,或者继续交回去抓下一帧
如果把白话和课程源码对起来,大致就是:
| 白话动作 | 对应接口/代码 |
|---|---|
| 打开设备 | open("/dev/videoX", O_RDWR) |
| 问设备会什么 | VIDIOC_QUERYCAP、VIDIOC_ENUM_FMT、VIDIOC_ENUM_FRAMESIZES |
| 选一种格式 | VIDIOC_S_FMT |
| 申请盒子 | VIDIOC_REQBUFS |
| 找到盒子并映射到用户空间 | VIDIOC_QUERYBUF + mmap |
| 把空盒子交给驱动 | VIDIOC_QBUF |
| 开始连续采集 | VIDIOC_STREAMON |
| 等一帧、取一帧、再还回去 | poll -> VIDIOC_DQBUF -> 处理数据 -> VIDIOC_QBUF |
| 收工时停止采集 | VIDIOC_STREAMOFF |
这一章里,最终拿到的一帧图像可以有三种很直观的去向:
04_video_get_data/video_test.c:把它保存成video_raw_data_0000.jpg02_video2lcd:把它直接显示到 LCD01_mjpg-streamer:把它继续做成网络视频流
所以不要把这章只看成"摄像头驱动框架"。它本质上是在解释:一帧图像怎样从设备进到用户程序,再被用户程序消费。
标准概念闭环 和 课程示例代码路径,要分开看
这一点是初学者最容易混淆的。
1. 标准 streaming 概念闭环
标准的 V4L2 streaming 流程是:
open -> QUERYCAP -> ENUM_FMT/ENUM_FRAMESIZES -> S_FMT -> REQBUFS -> QUERYBUF/mmap -> QBUF(全部) -> STREAMON -> {poll -> DQBUF -> 处理 -> QBUF}循环 -> STREAMOFF -> close
也就是说,STREAMOFF 是标准 streaming 流程的一部分。它负责告诉驱动"这次采集结束了,可以停设备、清理 streaming 状态了"。
2. 课程 04_video_get_data/video_test.c 的实际代码路径
课程示例为了持续抓帧,核心逻辑写成了无限循环:
poll -> DQBUF -> 把这一帧写成 jpg -> QBUF -> 再次 poll
它的源码末尾虽然也写了 VIDIOC_STREAMOFF,但是位置在 while (1) 之后。也就是说:
- 从"标准概念"上,
STREAMOFF必须在闭环里占一个位置 - 从"课程示例正常运行"上,你看到的主路径会停在
poll -> DQBUF -> QBUF循环 - 只有你主动改代码跳出循环,或者专门加退出条件,程序才会真正跑到
VIDIOC_STREAMOFF
这两个视角都要留着,不能混成一句"示例程序没有 STREAMOFF",也不能混成一句"示例程序正常抓帧时一定会执行到 STREAMOFF"。
高频术语,只记第一遍需要的
第一次学这一章,先把这些词记到"够用"。
| 术语 | 直白解释 | 第一遍先记到什么程度 |
|---|---|---|
buffer |
一块用于装一帧图像数据的内存 | 把它当成"装图像的盒子" |
queue |
驱动管理这些 buffer 的队列 | 先理解"空闲的"和"已完成的"会流转 |
mmap |
把驱动里的 buffer 映射到用户空间 | 先理解"映射的是 buffer,不是寄存器" |
streamon |
告诉驱动开始连续采集 | 先理解"真正开始跑数据流了" |
streamoff |
告诉驱动停止连续采集 | 先理解"收工并结束 streaming 状态" |
control |
摄像头参数控制接口,比如亮度、对比度、曝光 | 它和取图像数据不是一条线 |
descriptor |
设备上报"我支持什么"的描述信息 | USB 摄像头的格式、分辨率、控制能力都能从这里追根 |
vb2 |
videobuf2,V4L2 的通用 buffer 管理框架 |
第一次只要知道它负责 buffer 生命周期 |
vb2_ops |
驱动最值得先看的回调组 | 第一次重点看 queue_setup、buf_queue、start_streaming、stop_streaming |
vb2_mem_ops |
负责内存分配、映射等辅助动作 | 第一次知道"管内存"就够了,不必逐个成员深挖 |
vb2_buf_ops |
负责用户态 v4l2_buffer 和内核 vb2_buffer 之间的参数搬运 |
第一次知道"管参数搬运"就够了,不必逐个成员深挖 |
一句话记忆:
第一次学习时,vb2 先当成"V4L2 帮你管理 buffer 的公共底座";vb2_mem_ops 和 vb2_buf_ops 先知道名字和职责,不要求第一次就吃透。
把白话流程和框架名词对上
等你能用白话讲清"一帧图像怎么进来"后,再把它和内核里的结构对上。
| 你从用户态看到的 | 内核里主要是谁在接 |
|---|---|
/dev/videoX 这个设备节点 |
video_device |
open/release/poll/mmap 这些文件操作 |
v4l2_file_operations |
用户发出的 VIDIOC_* |
先到 video_ioctl2,再分发给 v4l2_ioctl_ops |
QBUF/DQBUF/STREAMON/OFF 这些 buffer 流程 |
很多驱动最终都交给 vb2 处理 |
| 驱动真正"我需要几个 buffer""开始采集""停止采集" | vb2_ops |
这时再去记层次关系,会比较稳:
video_device:代表一个/dev/videoXv4l2_file_operations:处理文件级入口v4l2_ioctl_ops:处理 V4L2 语义vb2:处理 buffer 生命周期
第一次读框架时,建议把注意力放在这三层:
vidioc_querycap、vidioc_enum_fmt_vid_cap、vidioc_s_fmt_vid_capvb2_ops里的queue_setup、buf_queue、start_streaming、stop_streaming- 驱动里"什么时候调用
vb2_buffer_done把一帧还给用户"
至于 vb2_mem_ops、vb2_buf_ops 的细枝末节,放到第二遍再看更合适。
可执行 Runbook
这一节不是"建议",而是尽量写成你可以照着跑的最小闭环。
Runbook 1:先确认真实摄像头有没有正确暴露能力
目标:先别急着抓图,先确认 /dev/videoX 存在,格式和分辨率能枚举出来。
最小命令集:
sh
# 如果板子上有 gcc,就直接用 gcc;如果没有,就把 gcc 换成你的交叉编译器
cd /Volumes/ExFAT/study/100ask/drivers/doc_and_source_for_drivers/STM32MP157/source/A7/13_V4L2/03_video_params
gcc -o video_params video_test.c
ls /dev/video*
./video_params /dev/video0
预期设备节点:
- 常见是
/dev/video0 - 如果系统里已经有别的摄像头或虚拟摄像头,也可能是
/dev/video1、/dev/video2
成功信号:
ls /dev/video*能看到至少一个/dev/videoX- 程序能打印出格式列表和分辨率,例如某个格式对应多个
width x height
失败时先看哪里:
ls /dev/video*什么都没有:先看摄像头有没有枚举出来,再看dmesg | tail -n 50- 程序一开始就
can not open /dev/videoX:多半是设备节点选错了 VIDIOC_ENUM_FMT没结果:先确认这是不是 capture 节点,而不是别的媒体节点
Runbook 2:把一帧一帧的数据抓成 jpg
目标:跑通这一章最核心的用户态数据链路。
最小命令集:
sh
cd /Volumes/ExFAT/study/100ask/drivers/doc_and_source_for_drivers/STM32MP157/source/A7/13_V4L2/04_video_get_data
gcc -o video_get_data video_test.c
./video_get_data /dev/video0
预期设备节点:
- 一般还是
/dev/video0 - 如果
Runbook 1里实际可用的是别的节点,这里也要改成那个节点
成功信号:
- 终端持续打印
capture to video_raw_data_0000.jpg、video_raw_data_0001.jpg... - 当前目录不断出现新的 jpg 文件
观察点:
- 这一步最值得看的是
REQBUFS -> QUERYBUF/mmap -> QBUF -> STREAMON -> poll -> DQBUF -> 写文件 -> QBUF - 这已经是整章最核心的"标准概念闭环"的主体部分
失败时先看哪里:
can not get capability:多半不是正确的 capture 节点can not set format:示例代码里写死了1024x768 + MJPEG,先回到Runbook 1看这个摄像头到底支不支持can not request buffers或Unable to map buffer:先看驱动是否支持 streaming /mmap- 一直没有 jpg:先看是不是卡在
poll,再看dmesg
Runbook 3:抓帧的同时调亮度
目标:把"数据流"和"控制流"分开看。
最小命令集:
sh
cd /Volumes/ExFAT/study/100ask/drivers/doc_and_source_for_drivers/STM32MP157/source/A7/13_V4L2/05_video_brightness
gcc -o video_brightness video_test.c -lpthread
./video_brightness /dev/video0
操作方式:
- 程序运行后,按
u/U提高亮度 - 按
d/D降低亮度
成功信号:
- 程序先打印
brightness min = ... max = ... - 图像仍在持续抓取
- 摄像头亮度变化,或者至少
VIDIOC_S_CTRL能成功返回
失败时先看哪里:
can not query brightness:不是所有摄像头都支持亮度 control,先不要急着判驱动坏了- 先对照
tmp_摄像头控制接口.md看这个控制项理论上属于哪一类 entity - 再对照
USB摄像头描述符.txt里的Processing UnitbmControls看 Brightness 是否真的被设备声明支持
Runbook 4:加载虚拟摄像头驱动,验证 /dev/videoX 是怎么被造出来的
目标:把"真实硬件采图"换成"课程自己造一台虚拟摄像头",从而看懂驱动最小闭环。
前提:
- 你使用的内核源码目录已经配置并编译过
04_virtual_driver_ok/Makefile里的KERN_DIR要对应你正在运行的内核源码目录
最小命令集:
sh
cd /Volumes/ExFAT/study/100ask/drivers/doc_and_source_for_drivers/STM32MP157/source/A7/13_V4L2/06_virtual_driver/04_virtual_driver_ok
make KERN_DIR=/path/to/Linux-5.4
在 STM32MP157 开发板上加载:
sh
systemctl stop myir
modprobe videobuf2-common
modprobe videobuf2-v4l2
modprobe videobuf2-memops
modprobe videobuf2-vmalloc
insmod my_video_drv.ko
ls /dev/video*
然后用前面 Runbook 2 里编好的 video_get_data 去验证:
sh
./video_get_data /dev/videoX
如果你已经把 02_video2lcd/video2lcd.7z 解压并编好,也可以:
sh
./video2lcd /dev/videoX
预期设备节点:
- 如果系统里之前没有别的摄像头,常见是新出现
/dev/video0 - 如果之前已经有真实摄像头,它也可能是
/dev/video1或更后面的节点
成功信号:
insmod后出现新的/dev/videoX- 用
video_get_data跑时能持续生成 jpg - 用
video2lcd跑时,LCD 上能看到课程虚拟驱动塞进去的红/绿/蓝画面切换
失败时先看哪里:
insmod失败:先看模块和当前内核版本是否匹配,再看dmesg | tail -n 50- 没有新的
/dev/videoX:先看lsmod | grep videobuf2,再看dmesg - LCD 没画面:先确认
systemctl stop myir是否已经执行,避免 GUI 抢占屏幕
video2lcd 和 mjpg-streamer 在学习路径里的角色
这两个应用都很有用,但建议放在核心闭环之后:
| 应用 | 它更适合放在什么时候看 | 作用 |
|---|---|---|
video2lcd |
在你已经能抓到 jpg 之后 | 让你看到同一帧数据也可以直接显示到 LCD |
mjpg-streamer |
在你已经理解 poll -> DQBUF -> QBUF 之后 |
让你看到同一帧数据还能被转成网络视频流 |
先看什么、再做什么实验、再读哪些源码
建议强制按这个顺序,不要跳。
| 顺序 | 做什么 | 为什么这么排 |
|---|---|---|
| 1 | 先看 01_V4L2应用程序开发.md |
先把用户态采图闭环立住 |
| 2 | 跑 Runbook 1 |
先确认设备节点、格式、分辨率 |
| 3 | 跑 Runbook 2 |
真正把一帧图像抓出来 |
| 4 | 跑 Runbook 3 |
把数据流和控制流分开理解 |
| 5 | 再看 02_V4L2驱动程序框架.md |
这时再看 video_device、fops、ioctl_ops、vb2 不会太抽象 |
| 6 | 读 06_virtual_driver/01_virtual_driver_framework |
先看骨架怎么搭起来 |
| 7 | 读 06_virtual_driver/02_virtual_driver_hardware |
看最小格式协商和 queue 是怎么补上的 |
| 8 | 读 06_virtual_driver/03_virtual_driver_hardware_datatransfer |
看"硬件产生一帧数据"怎么变成 vb2_buffer_done |
| 9 | 读 06_virtual_driver/04_virtual_driver_ok |
看完整闭环、锁和停止 streaming 时的回收 |
| 10 | 最后看 04_USB摄像头驱动程序分析.md |
这时再看 UVC 才能把真实设备对到前面的统一框架上 |
Control 和 Descriptor 这条侧线,怎么挂到具体材料上
这一章的另一条重要侧线是:为什么某些摄像头有亮度控制,为什么某些格式和分辨率能枚举出来。
这里不要空谈,要直接挂到课程给的两份材料上。
1. 对照 tmp_摄像头控制接口.md
这份材料的重点不是整段英文,而是两件事:
- 摄像头内部有
VideoControl接口 Processing Unit、Extension Unit、Input Terminal这些 entity 会声明自己支持哪些 control
建议你对照下面这张表看:
| 材料里先看什么 | 再对照哪个 ioctl | 你要观察哪些字段 |
|---|---|---|
Processing Unit 支持 Brightness、Contrast、Hue 等 |
VIDIOC_QUERYCTRL |
minimum、maximum、step、default_value |
| control 有 current / min / max / resolution / default 这些属性 | VIDIOC_G_CTRL、VIDIOC_S_CTRL |
当前值能不能读出来、改过去后会不会生效 |
Extension Unit 代表扩展控制能力 |
仍然先从 VIDIOC_QUERYCTRL 出发 |
某些厂商私有控制项为什么不属于最常见的标准 control |
最值得建立的印象是:
V4L2 的 control 不是凭空出现的,它通常可以追溯到摄像头控制接口里的某个 entity 声明。
2. 对照 USB摄像头描述符.txt
这份材料最值得先抓的不是整份 lsusb -v 输出,而是这几类字段:
| 描述符字段 | 再对照哪个 ioctl | 你要建立什么对应关系 |
|---|---|---|
bNumFormats、FORMAT_UNCOMPRESSED、FORMAT_MJPEG 之类的格式描述 |
VIDIOC_ENUM_FMT |
用户态能枚举到哪些像素格式 |
FRAME_UNCOMPRESSED / FRAME_MJPEG 里的 wWidth、wHeight |
VIDIOC_ENUM_FRAMESIZES |
用户态能枚举到哪些分辨率 |
dwFrameInterval |
这份示例程序没打印,但值得知道它存在 | 同一分辨率为什么还能有不同帧率 |
Processing Unit 里的 bmControls |
VIDIOC_QUERYCTRL |
某个 control 为什么能查到,或者为什么查不到 |
建议你按这个顺序做一次对照:
- 先跑
03_video_params/video_test.c - 记下它打印出来的
VIDIOC_ENUM_FMT、VIDIOC_ENUM_FRAMESIZES结果 - 再去看
USB摄像头描述符.txt里的FORMAT_*、FRAME_*、wWidth、wHeight - 再跑
05_video_brightness/video_test.c - 最后去看
tmp_摄像头控制接口.md和描述符里的Processing UnitbmControls
这样你会更容易形成一个稳定认识:
VIDIOC_ENUM_FMT不是凭空编出来的,它背后能追到设备报告的格式能力VIDIOC_ENUM_FRAMESIZES不是凭空编出来的,它背后能追到设备报告的帧大小VIDIOC_QUERYCTRL能查到什么,也通常能追到控制接口声明了什么
用虚拟摄像头驱动把抽象跑通
第 13 章最有价值的地方,是它没有停在框架图,而是给了一个从 0 补起来的虚拟摄像头驱动。
建议按目录顺序看,不要直接跳最终版:
| 目录 | 这一阶段在补什么 | 你要重点看什么 |
|---|---|---|
06_virtual_driver/01_virtual_driver_framework |
先搭骨架 | video_device、v4l2_file_operations、v4l2_ioctl_ops、vb2_queue 怎么挂起来 |
06_virtual_driver/02_virtual_driver_hardware |
开始补"像个摄像头"的最小能力 | querycap、enum_fmt、s_fmt、enum_framesizes、queue_setup、buf_queue |
06_virtual_driver/03_virtual_driver_hardware_datatransfer |
开始补数据传输 | 用 timer 模拟硬件不断产出帧,并把数据填进 buffer |
06_virtual_driver/04_virtual_driver_ok |
把能跑通的闭环补完整 | g_fmt、锁、device_caps、停止 streaming 时的队列回收 |
最终版 virtual_video_drv.c 最值得抓的,不是每一行,而是这几个动作:
1. 驱动注册
- 先初始化
vb2_queue - 再让
g_vdev.queue = &g_vb_queue - 再
v4l2_device_register - 最后
video_register_device(&g_vdev, VFL_TYPE_GRABBER, -1)
这里最关键的认识是:
用户看到的是 /dev/videoX,内核里要先有一个 video_device 被注册出来。
2. 格式协商
在 virtual_s_fmt_cap 里,用户虽然可以传入期望的格式,但驱动会把它收敛到自己真正支持的配置,比如最终固定为 800x600 + MJPEG。
这对初学者非常关键:
VIDIOC_S_FMT 更接近"协商"而不是"强制设置"。
3. buffer 排队与完成
virtual_buf_queue 里,驱动把用户交来的 vb2_buffer 放进 g_queued_bufs 链表。
后面 timer 到时,相当于"硬件产生了一帧数据":
- 从
g_queued_bufs里拿一个空闲 buffer - 通过
vb2_plane_vaddr()得到它的内核地址 - 把
red/green/blue这些模拟图片数据拷进去 - 用
vb2_set_plane_payload()告诉上层这一帧实际用了多少字节 - 最后
vb2_buffer_done(..., VB2_BUF_STATE_DONE),把这个 buffer 标成"已完成"
这一段最值得反复看,因为它把"硬件填数据 -> 用户态拿到一帧"这件事彻底具体化了。
4. STREAMON / STREAMOFF 的真实含义
在虚拟驱动里:
virtual_start_streaming里启动 timer,模拟硬件开始持续输出virtual_stop_streaming里停止 timer,并把还在队列里的 buffer 以VB2_BUF_STATE_ERROR结束掉
这说明:
STREAMON不是只改一个标志位,它通常意味着"硬件采集真的开始了"STREAMOFF也不是只停开关,它还要处理队列里尚未完成的 buffer
USB 摄像头分析应该怎么看
文档 04_USB摄像头驱动程序分析.md 的价值,不在于把 UVC 规范每个字段都背下来,而在于建立两个稳定认识。
1. 先分清控制面和数据面
USB 摄像头内部至少有两类接口:
| 接口 | 作用 | 对应到用户态更像什么 |
|---|---|---|
VideoControl |
控制摄像头参数 | VIDIOC_QUERYCTRL、VIDIOC_G_CTRL、VIDIOC_S_CTRL |
VideoStreaming |
传输视频帧 | REQBUFS/QBUF/DQBUF/STREAMON 这条数据流 |
所以 UVC 驱动至少要做两件事:
- 一条线处理 control
- 另一条线处理视频数据流
2. 再把真实设备对到统一框架上
学完前面的虚拟驱动后,再回头看 USB 摄像头分析,会更容易抓住真实驱动的职责:
| UVC 驱动要做的事 | 你可以怎么理解 |
|---|---|
| 解析 USB 描述符 | 弄清控制接口、流接口、格式、分辨率、控制项 |
| 注册成 V4L2 设备 | 最终仍然要提供 /dev/videoX |
| 初始化队列 | 最终仍然要接 vb2 |
| 开始数据传输 | 底层可能是 USB 包、URB,但上层看到的仍是 buffer 完成 |
| 把 control 暴露给应用 | 最终仍然还是 VIDIOC_G_CTRL / VIDIOC_S_CTRL 这类语义 |
换句话说:
UVC 是底层设备类别,V4L2 是 Linux 暴露给应用的统一视频接口。
Linux 初学者最容易混淆的点
下面这些点,如果不提前指出,读源码时很容易一直绕圈。
| 易混淆点 | 正确认知 |
|---|---|
VIDIOC_S_FMT 成功了,就说明格式完全按应用要求设置成功 |
不一定。驱动可以返回"最接近硬件能力"的格式 |
REQBUFS 申请 32 个 buffer,就一定能得到 32 个 |
不一定。驱动最终分配出来的个数可能更少 |
mmap 之后,应用就已经拿到一帧图像了 |
不对。mmap 只是映射内存,真正拿到有效帧还要 QBUF + STREAMON + DQBUF |
QBUF / DQBUF 是复制图像数据 |
不对。它们主要是 buffer 所有权和状态的切换 |
STREAMON 只是一个普通开关 |
不完整。它通常意味着底层硬件/传输真的开始工作 |
STREAMOFF 在标准流程里不重要 |
不对。标准 streaming 闭环一定有它,只是课程 04_video_get_data 正常运行时停在无限抓帧循环里 |
control 和 format 是一回事 |
不对。format 决定图像怎么组织,control 决定摄像头参数 |
VIDIOC_QUERYCTRL 失败就说明驱动坏了 |
不一定。也可能是这个设备根本没声明支持该 control |
read() 和 mmap streaming 是同一套思路 |
不完全一样。课程重点是更常见的 mmap + streaming 路线 |
vb2_mem_ops、vb2_buf_ops 必须第一次就吃透 |
不需要。第一次先知道它们分别负责"内存"和"参数搬运"就够了 |
/dev/video0 一定是我要用的那个节点 |
不一定。真实摄像头、虚拟摄像头并存时,节点号会变化 |
| 示例程序一跑不通就是驱动框架没懂 | 很多时候只是节点选错、格式不支持、模块版本不匹配,先排最短路径问题 |
其中最值得反复提醒自己的一句是:
V4L2 的难点不在于单个 ioctl,而在于"格式协商 + buffer 生命周期 + streaming 状态机"这三件事是连在一起的。
学完这一章后,应该能做到什么
学完这章后,至少应该具备下面这些能力:
- 能用白话讲清"一帧图像怎样从
/dev/videoX进到用户程序" - 能把
QUERYCAP、ENUM_FMT、ENUM_FRAMESIZES、S_FMT、REQBUFS、QBUF、DQBUF、STREAMON/OFF串成一条完整链路 - 能区分"标准 streaming 概念闭环"和"课程
04_video_get_data实际运行时停在哪" - 能照着 runbook 跑出
/dev/videoX、jpg 输出、亮度控制、虚拟摄像头最小闭环 - 能分清
v4l2_file_operations、v4l2_ioctl_ops、vb2、虚拟摄像头、UVC 之间的大致分工 - 能把
tmp_摄像头控制接口.md、USB摄像头描述符.txt和用户态VIDIOC_QUERYCTRL、VIDIOC_ENUM_FMT、VIDIOC_ENUM_FRAMESIZES对上
如果这章学完后你还能稳定回答下面 3 个问题,就说明主线已经建立起来了:
- 为什么
mmap之后还要QBUF、STREAMON、DQBUF? - 为什么课程示例正常抓帧时主要停在
poll -> DQBUF -> QBUF,但标准流程里仍然必须知道STREAMOFF的位置? - 为什么真实 USB 摄像头和课程里的虚拟摄像头,最后都能被同样的
/dev/videoX用户程序打开?
能把这 3 个问题答顺,这章就不是"看过",而是真的入门了。
14_IIO 学习总结
TL;DR:先别急着背
buffer、trigger、event,先把 IIO 想成"Linux 给各类传感器和 ADC 提供的统一数据接口"。如果应用只想读一次数据,就走sysfs直读;如果要连续拿一串采样,就把多个 channel 按 scan 格式塞进buffer;如果要决定"什么时候采",就挂trigger;如果要在越阈时主动通知用户,就再加event。课程里的 DHT11 示例,正是沿着这条线一步步增强出来的。
章节定位
14_IIO 是这套驱动课里第二次比较系统地进入一个 Linux 子系统。
它在整套课里的位置,建议这样理解:
| 章节关系 | 这一章承接了什么 | 这一章新增了什么 |
|---|---|---|
往前承接 06_Pinctrl、07_GPIO、08_Interrupt |
你已经知道引脚怎么配、GPIO 怎么读写、中断怎么收边沿 | 这一章开始把"底层引脚/中断细节"包装成标准传感器接口 |
可类比 13_V4L2 |
上一章学的是"视频帧数据怎么统一暴露给应用" | 这一章学的是"标量传感器数据怎么统一暴露给应用" |
往后对照 15_devmem和UIO |
你会看到另一种更底层、更临时的访问方式 | 这一章先建立"优先走现成子系统"的正路思维 |
所以这章的边界要先划清:
- 它重点不是讲 DHT11 通信时序本身,也不是讲 ADC 电路细节。
- 它重点是讲 Linux 为什么要把温度、湿度、电压、加速度这类"传感器数据"统一抽象成 IIO。
一句话概括:这章是在建立"传感器数据应该怎样被标准化暴露给用户态"的心智模型。
学习主线
这一章建议强制按下面这条主线走,不要一上来就钻进细节:
- 先回答:为什么 Linux 不希望每个传感器都各写一套私有字符设备接口
- 再看最小 IIO 模型:
iio_dev、channel、sysfs直读 - 接着看
buffer:为什么单次读取不够,为什么需要批量采样 - 再看
trigger:为什么"什么时候采"也要从驱动里抽出来复用 - 最后看
event:为什么"越阈通知"不能继续靠用户态死循环轮询 - 学完 DHT11 的完整演进后,再回头看 STM32 ADC 现成驱动
如果按这条线学,IIO 不会像一堆零散术语;它会变成一条很稳定的能力增强链路:
sysfs 直读 -> buffer 批量采样 -> trigger 决定时机 -> event 主动上报 -> 套回真实 ADC 驱动
为什么 Linux 里会有 IIO 子系统
先用第一性原理看这个问题。
Linux 里有很多"会产生测量值"的设备:
- ADC:给你电压采样值
- DHT11:给你温度、湿度
- 光照、加速度、陀螺仪、磁力计:给你一个或多个物理量
这些设备长得不一样,但对软件来说,经常在解决同一类问题:
| 共同问题 | 如果没有 IIO,会发生什么 | IIO 想统一什么 |
|---|---|---|
| 一个设备往往不止一个测量量 | 每个驱动自己定义"温度怎么读、湿度怎么读、电压怎么读" | 用 channel 统一表示每个测量通道 |
| 有时只想读一次,有时想连续采样 | 每个驱动各写一套私有 read/ioctl/poll 接口 |
用 sysfs、buffer、trigger 提供统一能力 |
| 应用得知道数据单位和格式 | 每个驱动自己发明命名和换算规则 | 用 _raw、_scale、_input、scan 格式统一表达 |
| 某些场景要阈值报警 | 每个驱动各写一套中断和告警接口 | 用 event 统一事件上报 |
所以 IIO 的价值,不是"帮你少写几行代码",而是:
- 让传感器类驱动不要重复发明用户接口
- 让应用看到统一的命名、统一的数据路径、统一的采样模型
- 让"单次读取""批量采样""触发采样""阈值事件"这些常见需求都能复用框架
如果把这章和 13_V4L2 并排看,会更容易理解:
V4L2统一的是"视频帧"IIO统一的是"传感器/ADC 这种标量采样数据"
先把 IIO 想成什么
第一次学 IIO,先把它压缩成下面这张图景:
| 对象 | 直白解释 | 这一章怎么对应 |
|---|---|---|
iio_dev |
一个 IIO 设备实例 | 例如一个 DHT11 设备、一个 ADC 控制器实例 |
channel |
这个设备里某个可测量的量 | DHT11 里的温度、湿度;ADC 里的 voltage0、voltage1 |
sysfs 直读 |
"现在给我一个值" | cat in_temp_input、cat in_voltage0_raw |
buffer |
把多次采样按固定布局存起来,连续读 | hexdump /dev/iio:deviceX |
trigger |
决定"什么时候进行一次采样" | 软件循环、手工触发、定时器、外部中断 |
event |
不是样本本身,而是"发生了越阈/异常事件" | 温度超过上限时通知应用 |
最值得先记住的一句是:
IIO 的核心不是"一个设备节点",而是"一个设备下有多个 channel,每个 channel 可以按统一方式被读取、采样和告警"。
iio_dev、channel 和 sysfs:先把最小模型看稳
第一次读源码时,建议先只盯住最小 IIO 模型,不要一上来就钻进 buffer/trigger/event。
1. iio_dev 是设备容器
一个最小 IIO 驱动,通常先做这几件事:
devm_iio_device_alloc- 填
iio->info - 填
iio->channels和iio->num_channels devm_iio_device_register
对初学者来说,先把它理解成:
iio_dev代表"这个设备整体"info代表"这个设备有哪些回调能力"channels代表"这个设备能提供哪些测量量"
2. channel 是逻辑测量通道,不是另一个设备
这点非常关键。
在 DHT11 里:
- 温度是一个 channel
- 湿度是一个 channel
- 但它们共享同一个
iio_dev
在 STM32 ADC 里:
voltage0、voltage1、voltage2... 都是 channel- 但它们也共享同一个 ADC 的
iio_dev
所以不要把 channel 理解成"又注册了一个新设备"。
channel 更像"这个设备里的一列数据"。
3. sysfs 直读是最小可用路径
最小 IIO 驱动先解决的问题通常是:
应用现在就要一个值,驱动怎么给?
课程里最早让你体验的是这个路径:
sh
cd /sys/bus/iio/devices/iio:deviceX
cat in_humidityrelative_input
cat in_temp_input
这背后对应的是:
- DHT11 驱动实现
read_raw - channel 声明自己支持
IIO_CHAN_INFO_PROCESSED - 框架因此生成
in_temp_input、in_humidityrelative_input
如果对照 STM32 ADC,会看到另一种典型模式:
sh
cd /sys/bus/iio/devices/iio:deviceX
cat in_voltage0_raw
cat in_voltage_scale
这说明:
- 有些设备直接给"处理后的物理量",所以用户读
_input - 有些设备更适合先给原始采样值,所以用户读
_raw再配合scale/offset
第一次学习时,只要把这个区别记成一句话就够了:
DHT11 更像"直接给你可用值",ADC 更像"先给原始码值,再告诉你怎么换算"。
sysfs 直读、buffer 批量采样、trigger 触发采样、event 阈值/事件上报 到底是什么关系
这四个词不是彼此割裂的四套系统,它们更像逐层叠加的能力。
| 能力 | 用户最直接看到什么 | 它回答的问题 | 它依赖什么 |
|---|---|---|---|
sysfs 直读 |
in_*_raw、in_*_input |
"现在给我一个值" | iio_dev + channel + read_raw |
buffer |
scan_elements/*、buffer/*、/dev/iio:deviceX |
"把多次采样结果连续交给我" | 还要知道每个 channel 的 scan 布局 |
trigger |
trigger/current_trigger |
"什么时候触发一次采样" | 通常和 buffer 一起用,也可以驱动 event |
event |
events/*、事件 fd |
"当值越线了,主动通知我" | 事件描述、事件队列、触发来源 |
把它们串起来,关系更清楚:
channel定义"这台设备有什么可测量的量"sysfs直读是最短路径,应用主动来问一次,驱动给一次buffer负责保存"一批采样结果",让应用连续读取trigger负责定义"何时进行一次采样"event负责告诉应用"发生了某个条件变化",但它本身不是采样数据
最容易混淆的两个点,要单独拎出来:
1. buffer 决定"怎么存",trigger 决定"何时采"
很多初学者会把这两个词混成一句"buffer 采样"。
更准确的理解是:
buffer负责把采样结果排成一帧一帧的 scan,存给用户态trigger负责决定什么时候执行这次采样
所以它们是配合关系,不是替代关系。
2. event 不是另一份样本数据
event 上报的是"事件 ID + 时间戳",不是温度值本身。
也就是说:
- 你可以用
buffer持续收样本 - 同时再用
event收"越上限了/越下限了"的通知
这两条线能并存,但语义不一样。
DHT11 示例是怎样一步步增强的
这一章最有价值的地方,不是分别讲了 4 个概念,而是用同一个 DHT11 驱动把它们按顺序叠加起来。
建议按下面这个演进表来理解:
| 阶段 | 资料/源码 | 新增了什么能力 | 你从用户态会看到什么 |
|---|---|---|---|
| 第 0 步:最小 IIO 驱动 | 01_IIO子系统简化框架及第1个驱动分析.md,内核树 drivers/iio/humidity/dht11.c |
只做 iio_dev + channel + read_raw |
in_temp_input、in_humidityrelative_input |
第 1 步:加 buffer |
02_iio_buffer的使用.md,source/A7/14_iio/01_dht11_iio_buffer_software、02_dht11_iio_buffer_software_课堂现场编写 |
增加 scan_index、scan_type、available_scan_masks、kfifo buffer、iio_push_to_buffers |
新增 scan_elements/*、buffer/*,并能读 /dev/iio:deviceX |
第 2 步:加 trigger |
03_iio_trigger的使用.md,source/A7/14_iio/03_dht11_iio_trigger、04_dht11_iio_trigger_课堂现场编写 |
采样时机从"驱动自己 schedule work"升级为"由 IIO trigger 框架提供" | 新增 trigger/current_trigger,可以切换 loop/hrtimer 等触发源 |
第 3 步:加 event |
04_iio_event的使用.md,source/A7/14_iio/05_dht11_iio_event、06_dht11_iio_event_课堂现场编写 |
增加阈值事件描述、事件队列、iio_push_event、事件 fd |
新增 events/*,应用可以阻塞等待越阈事件 |
第 1 步:为什么先加 buffer
因为单次 sysfs 读取只能回答"现在是多少"。
一旦你想要:
- 连续观察温湿度变化
- 一次读一批数据
- 同时拿到温度和湿度的成组样本
就需要 buffer。
这一步的关键不是"有了一个新文件",而是心智模型发生了变化:
- 直读时,应用问一次,驱动采一次
buffer时,驱动会持续把样本推入缓冲区,应用从/dev/iio:deviceX连续取 scan
而且从这一步开始,channel 不只是"能读什么",还决定了"buffer 里每一帧怎么排":
scan_index决定顺序scan_type决定每个通道占多少 bit
第 2 步:为什么再引入 trigger
01_dht11_iio_buffer_software 已经能靠 delayed_work 周期性采样了。
那为什么还要多学一层 trigger?
因为"什么时候采"本身也应该被标准化,而不该写死在每个驱动里。
这一步引入 trigger 后,DHT11 驱动不再执着于"我自己开 worker 循环读硬件",而是开始复用 IIO 统一触发框架:
iio-trig-loop:软件循环触发iio-trig-sysfs:手工写 sysfs 触发一次iio-trig-hrtimer:高精度定时器周期触发iio-trig-interrupt:借助别的中断触发
所以 trigger 这一层的本质,是把:
- 采样动作的实现者:DHT11 驱动
- 采样时机的提供者:trigger 驱动
分离开来。
第 3 步:为什么最后再加 event
到 buffer + trigger 这一步,应用已经能持续拿到样本了。
但如果应用真正关心的是:
- 温度是不是超过 30 度了
- 湿度是不是掉到某个阈值以下了
它并不想一直自己轮询所有样本。
于是就有了 event:
- 驱动检测到条件成立
- 用
iio_push_event把事件写入 IIO 事件队列 - 用户态通过
IIO_GET_EVENT_FD_IOCTL拿到事件 fd - 再
read(event_fd, ...)阻塞等待事件
这里有一个很值得注意的课程细节:
05_dht11_iio_event更偏"概念演示",源码里是硬编码条件后直接iio_push_event06_dht11_iio_event_课堂现场编写才把read_event_value/write_event_value/read_event_config/write_event_config补齐,让events/in_temp_thresh_*这些 sysfs 真正和驱动里的阈值变量对上
所以如果你读 05 觉得"怎么阈值配置接口还没完全落地",这不是你看错了,而是课程本来就是一步步补全的。
可执行 Runbook
这一节尽量写成你能照着跑的最小路径。
先做一次固定定位动作
在正式跑各个 Runbook 之前,先把"设备编号"和"trigger 名称"定位出来,不要一上来就硬写 iio:deviceX 或 trigger1。
最小命令集:
sh
for d in /sys/bus/iio/devices/iio:device*; do
echo "$d : $(cat $d/name 2>/dev/null)"
done
for t in /sys/bus/iio/devices/trigger*; do
echo "$t : $(cat $t/name 2>/dev/null)"
done
观察点:
- 先确认哪个
iio:deviceX真的是 DHT11,或者你当前要看的 ADC - 再确认
trigger*目录里到底有哪些 trigger,以及它们的真实名字 - 后面所有命令里的
iio:deviceX、loop0、timer_abc,都应该替换成你这一步查出来的实际名字
Runbook 1:先只体验 sysfs 直读
目标:先确认自己真的理解了"一个 IIO 设备下有多个 channel"。
最小命令集:
sh
cd /sys/bus/iio/devices/iio:deviceX
cat in_humidityrelative_input
cat in_temp_input
观察点:
iio:deviceX的编号不固定,不要死记device1或device2- 你读的是同一个设备下的两个 channel
- 这一步还没有
buffer、trigger、event的负担
Runbook 2:开启 buffer,看 scan 是怎么出现的
目标:把"单次值"升级成"连续样本流"。
最小命令集:
sh
cd /sys/bus/iio/devices/iio:deviceX
echo 1 > scan_elements/in_humidityrelative_en
echo 1 > scan_elements/in_temp_en
echo 16 > buffer/length
echo 1 > buffer/enable
hexdump /dev/iio\:deviceX
观察点:
scan_elements/*_en只是选择哪些 channel 进入 scanbuffer/enable之后,/dev/iio:deviceX才有连续数据可读hexdump看到的是 scan 布局后的二进制,不是漂亮文本
Runbook 3:给 buffer 换上标准 trigger
目标:理解"采样时机"和"缓冲存储"是两回事。
以 loop trigger 为例:
sh
insmod /root/iio-trig-loop.ko
mkdir /sys/kernel/config/iio/triggers/loop/loop0
cd /sys/bus/iio/devices/iio:deviceX
echo loop0 > trigger/current_trigger
echo 1 > scan_elements/in_humidityrelative_en
echo 1 > scan_elements/in_temp_en
echo 1 > buffer/enable
hexdump /dev/iio\:deviceX
再试一次 hrtimer trigger:
sh
mkdir /sys/kernel/config/iio/triggers/hrtimer/timer_abc
echo timer_abc > /sys/bus/iio/devices/iio:deviceX/trigger/current_trigger
观察点:
- 你换的是"谁来触发采样"
- 不是"换了一个 buffer"
- DHT11 采样和用户态读取路径都没变,变的是采样时机来源
Runbook 4:最后体验 event
目标:把"持续采样"和"越阈通知"分开理解。
前置条件:
- 这一步默认延续
Runbook 3,也就是trigger/current_trigger、scan_elements/*_en、buffer/enable已经配置好 - 如果你不是连续从
Runbook 3往下做,就先把下面这些命令补上
sh
cd /sys/bus/iio/devices/iio:deviceX
echo loop0 > trigger/current_trigger
echo 1 > scan_elements/in_humidityrelative_en
echo 1 > scan_elements/in_temp_en
echo 1 > buffer/enable
最小命令集:
sh
cd /sys/bus/iio/devices/iio:deviceX
echo 30 > events/in_temp_thresh_rising_value
echo 10 > events/in_temp_thresh_falling_value
echo 1 > events/in_temp_thresh_either_en
/root/dht11_test /dev/iio\:deviceX
观察点:
- 读事件不是
hexdump /dev/iio:deviceX - 应用会先用
IIO_GET_EVENT_FD_IOCTL拿事件 fd event只告诉你"发生了什么",不是把温度值本身塞进事件里
先看什么、再做什么实验、再读哪些源码
建议强制按这个顺序,不要跳。
| 顺序 | 做什么 | 为什么这么排 |
|---|---|---|
| 1 | 先看 01_IIO子系统简化框架及第1个驱动分析.md |
先把"为什么需要 IIO"与最小 sysfs 路径看懂 |
| 2 | 对照内核树 stm32mp15xc-kernel/drivers/iio/humidity/dht11.c |
先看一个只有 INDIO_DIRECT_MODE 的最小 IIO 驱动 |
| 3 | 跑 Runbook 1 |
先把 iio_dev、channel、sysfs 直读建立起来 |
| 4 | 再看 02_iio_buffer的使用.md |
这时再学 scan、buffer、/dev/iio:deviceX 不会太抽象 |
| 5 | 读 source/A7/14_iio/01_dht11_iio_buffer_software,再对照 02_dht11_iio_buffer_software_课堂现场编写 |
先看成型版,再看课堂版是怎么一步步补出来的 |
| 6 | 跑 Runbook 2 |
亲眼看到 scan_elements 和 buffer 生效 |
| 7 | 再看 03_iio_trigger的使用.md |
这时再学 trigger,能明确它是在解决"何时采样" |
| 8 | 读 source/A7/14_iio/03_dht11_iio_trigger,再对照 04_dht11_iio_trigger_课堂现场编写 和内核树 drivers/iio/trigger/iio-trig-loop.c、iio-trig-hrtimer.c、industrialio-trigger.c |
看 DHT11 如何挂到通用 trigger 框架上,也看课堂版是怎么补齐这层抽象的 |
| 9 | 跑 Runbook 3 |
把 loop trigger、hrtimer trigger 的差别跑出来 |
| 10 | 再看 04_iio_event的使用.md |
这时再引入 event,层次最清楚 |
| 11 | 先读 source/A7/14_iio/05_dht11_iio_event,再读 06_dht11_iio_event_课堂现场编写 |
先看概念演示,再看完整阈值配置版 |
| 12 | 跑 Runbook 4 |
把"样本流"和"事件流"真正分开 |
| 13 | 最后看 05_iio驱动示例.md,并对照 stm32mp15xc-kernel/drivers/iio/adc/stm32-adc.c |
把 DHT11 学到的模型迁移到真实 ADC 驱动上 |
每一阶段从哪个函数入口读起
只知道"读哪个文件"还不够。对初学者来说,更实用的是先抓函数入口。
| 阶段 | 先看哪些函数 | 带着什么问题去看 |
|---|---|---|
最小 sysfs 模型 |
dht11_probe、dht11_read_raw |
iio_dev 怎么注册出来,in_temp_input/in_humidityrelative_input 为什么会出现 |
buffer 阶段 |
iio_push_to_buffers,以及 DHT11 里和 buffer setup 相关的初始化路径 |
样本什么时候被推进 buffer,/dev/iio:deviceX 为什么开始有数据 |
trigger 阶段 |
iio_triggered_buffer_setup、iio_trigger_poll,再对照 iio-trig-loop.c、iio-trig-hrtimer.c |
trigger 怎么决定"什么时候采",以及它怎么和 buffer 连接起来 |
event 阶段 |
read_event_value、write_event_value、iio_push_event |
阈值配置怎么进驱动,事件又是怎么被推给用户态的 |
| STM32 ADC 迁移 | stm32_adc_probe、stm32_adc_read_raw、stm32_adc_chan_of_init |
DHT11 学到的 iio_dev/channel/read_raw 思路,如何迁移到真实 ADC 驱动 |
GPIO/ADC 普通驱动 和 IIO 通道驱动,不要混
这是 Linux 初学者最容易绕晕的一点。
1. DHT11 的底层可以用 GPIO,但对外不该只暴露成 GPIO
DHT11 的通信过程,底层确实会用到:
- GPIO 方向切换
- GPIO 边沿中断
- 时间戳采集
但这只是驱动内部实现手段。
对应用来说,它真正关心的不是:
- GPIO 现在是高还是低
而是:
- 温度是多少
- 湿度是多少
- 能不能连续采样
- 能不能越阈告警
所以 DHT11 这种设备,底层可以借助 GPIO/IRQ 实现,对外更适合暴露成 IIO channel 驱动。
2. ADC 普通驱动思路 和 IIO ADC 驱动思路,不是一回事
如果你自己写一个"普通 ADC 字符驱动",常见思路是:
- 自己注册
/dev/adcX - 自己定义
read格式或ioctl - 自己决定通道切换、单位换算、轮询、缓存
而 IIO ADC 驱动的思路是:
- ADC 作为一个
iio_dev - 每个输入脚是一个
channel - 单次读取走
in_voltageX_raw - 单位换算走
scale/offset - 连续采样复用
buffer + trigger
这两类思路的区别,可以直接对照下面这张表:
| 维度 | GPIO/私有字符设备思路 | IIO 通道驱动思路 |
|---|---|---|
| 对外暴露的核心对象 | 引脚状态、私有设备节点、私有命令 | 标准化的物理量 channel |
| 用户接口 | ioctl/read/poll 全靠驱动自己定义 |
sysfs、/dev/iio:deviceX、事件 fd |
| 多通道表达 | 常常靠自定义命令切换通道 | 天然就是多个 channel |
| 连续采样 | 通常自己造环形缓冲区 | 复用 IIO buffer |
| 定时/外部触发采样 | 驱动自己造定时器/线程 | 复用 IIO trigger |
| 阈值告警 | 驱动自己造事件机制 | 复用 IIO event |
最值得反复提醒自己的一句是:
GPIO/IRQ 讲的是"怎么摸到硬件",IIO 讲的是"把测量数据怎样标准化暴露出去"。
Linux 初学者最容易混淆的点
下面这些点,如果不提前指出,读源码时很容易一直打转。
| 易混淆点 | 正确认知 |
|---|---|
iio:deviceX 就代表一个测量值 |
不对。iio_dev 是整个设备,温度、湿度、电压 0 等都只是其中的 channel |
| channel 就是另一个设备节点 | 不对。channel 更像"设备里的一列测量数据" |
_raw 和 _input 只是名字不同 |
不完整。_raw 常表示原始码值,_input 或 processed 更接近已经可直接使用的物理量 |
使能了 scan_elements/*_en 就已经开始采样 |
不对。那只是选择哪些 channel 进入 scan,真正开始缓冲通常还要 buffer/enable |
buffer 自己决定什么时候采样 |
不对。buffer 管存储布局,trigger 管采样时机 |
trigger 就等于硬件中断 |
不对。它也可以是软件 loop、sysfs 手工触发、hrtimer 定时触发 |
event 是另一份样本数据 |
不对。event 主要是事件 ID + 时间戳,不是完整样本 |
读 /dev/iio:deviceX 就能直接拿到 event |
不对。事件通常要先通过 IIO_GET_EVENT_FD_IOCTL 拿事件 fd 再读 |
scan_type 是给 sysfs 直读用的 |
不对。它主要描述 buffer 中每个 channel 的存储格式和排列方式 |
iio:device1、trigger1 这些编号是固定的 |
不对。设备号和 trigger 号都可能变化,实验时要现场确认 |
| 直读和 buffer 总能随便同时用 | 不一定。像 ADC 这类驱动,read_raw 往往要 claim direct mode,buffer 开着时可能就不能直接读 |
其中最重要的一条,是很多人第一次都会混:
/sys/bus/iio/devices/iio:deviceX/ 主要面向"配置和单次读取",/dev/iio:deviceX 主要面向"buffer 数据流",而 event 还会再分出一个事件 fd。
学完这一章后,应该能做到什么
学完这章后,至少应该具备下面这些能力:
- 能回答"为什么 Linux 里要有 IIO,而不是每个传感器都各写一套私有字符驱动"
- 能分清
iio_dev、channel、sysfs、buffer、trigger、event分别在干什么 - 能把
sysfs 直读 -> buffer 批量采样 -> trigger 决定时机 -> event 上报告警串成一条完整链路 - 能说清 DHT11 示例是怎样从最小 IIO 驱动一步步增强到支持 buffer、trigger、event 的
- 能照着建议顺序去跑实验,并知道每一步应该观察什么
- 能把 DHT11 学到的模型迁移到 STM32 ADC 这种真实 IIO 驱动上
- 能明确区分"GPIO/ADC 普通驱动的底层实现手段"和"IIO 通道驱动的对外抽象方式"
如果这章学完后你还能稳定回答下面 3 个问题,就说明主线已经建立起来了:
- 为什么单次
sysfs读取解决不了连续采样问题? - 为什么
buffer和trigger必须分开理解? - 为什么 DHT11 底层明明靠 GPIO/IRQ 实现,对外却更适合做成 IIO 驱动?
能把这 3 个问题答顺,这章就不是"看过",而是真的入门了。
15_devmem和UIO 学习总结
TL;DR:这一章不是在教你"少写驱动的捷径",而是在教你区分 3 种完全不同的工程层次。
devmem/devmem2是用户态工具,它们借助/dev/mem和mmap直接碰物理地址,适合寄存器级验证;UIO是"内核保留最小控制权,用户态承担主要设备逻辑"的折中方案,内核暴露/dev/uioX、内存窗口和可选中断,用户程序再用mmap、read/poll去工作;真正要量产、要接入 Linux 设备模型和子系统时,还是要回到正式内核驱动。这章最重要的收获,不是点亮一个 LED,而是知道:什么时候可以临时在用户态做实验,什么时候必须回到内核里把驱动写完整。
章节定位
15_devmem和UIO 是课程后半段的一章"边界校准课"。
前面的很多章节都在教你怎么按 Linux 的正规方式写驱动:比如 06_Pinctrl、07_GPIO、08_Interrupt 让你理解硬件资源和中断是怎么进入内核的,09_UART、11_SPI、12_USB、13_V4L2、14_IIO 又让你看到设备最终通常要挂到某个子系统下面,给用户态提供稳定接口。
到了这一章,课程故意换一个角度:如果你现在只是想快速验证寄存器、想把一块很简单的设备先跑起来,是否一定要马上写完整内核驱动?
这章给出的答案不是"是"或"否",而是分层:
devmem/devmem2:最野、最快,但边界最差,只适合临时验证。UIO:内核先把设备的地址空间和可选中断整理出来,再交给用户态处理主要逻辑。- 正式内核驱动:当设备已经超出"简单寄存器 + 简单中断 + 单一应用独占"的范围时,就必须回到这条路。
一句话概括:这章是在帮你建立"用户态直接控硬件"和"正式内核驱动"之间的分界线。
学习主线总览
这章最稳的学习主线,不是背概念,而是沿着下面这条路径走:
| 阶段 | 你要解决的问题 | 代表对象 |
|---|---|---|
| 第 1 层:直接摸寄存器 | 我能不能先不写驱动,直接改一个寄存器验证硬件? | devmem/devmem2、/dev/mem、mmap |
| 第 2 层:用户态承担主要逻辑 | 我能不能只写很薄的一层内核代码,把寄存器和中断交给用户态? | UIO、/dev/uioX、read/poll |
| 第 3 层:回到正式驱动 | 设备一旦变复杂,哪些事不能继续停在用户态? | 子系统驱动、设备树、时钟/复位/电源管理、DMA、标准 ABI |
如果只记一条最短主线,请记住:
devmem/devmem2 -> /dev/mem -> mmap -> 直接访问物理寄存器
和
uio_info/uio_register_device -> /dev/uioX -> mmap/read/poll -> 用户态主逻辑
这两条线看起来都叫"用户态访问硬件",但它们的安全边界、内核参与程度、可维护性完全不同。
先把对象关系讲清楚
1. devmem/devmem2、/dev/mem、mmap 是什么关系
先把最容易混淆的一点钉住:
devmem、devmem2只是用户态工具,不是驱动。- 课程里更常出现
devmem2,因为它更方便指定位宽;但devmem和devmem2依赖的底层机制是同一类东西。 - 真正提供"访问物理地址"能力的是内核里的字符设备
/dev/mem。 mmap是进程把某段物理地址映射到自己虚拟地址空间的系统调用方式。
把它们串起来,就是:
- 用户程序执行
devmem或devmem2 - 工具去打开
/dev/mem - 通过
mmap把目标物理页映射进当前进程 - 用户程序在映射后的虚拟地址上做读写
- 最终效果看起来像"我在用户态改了寄存器"
对应到内核源码:
/dev/mem的核心实现可以看stm32mp15xc-kernel/drivers/char/mem.c- 其中
mmap_mem()负责把物理页映射给用户进程 - 访问是否被允许,会受
CONFIG_STRICT_DEVMEM和平台限制影响
所以,devmem2 0x50002018 w 0x400 这类命令,本质上不是"系统自带了 GPIO 驱动",而是"你绕过了设备模型,直接对某个物理地址动手"。
2. UIO、/dev/uioX、mmap、read/poll 是什么关系
UIO 比 /dev/mem 多做了一层整理。
它的核心思路是:
- 内核里仍然要有一个很薄的 UIO 驱动
- 这个驱动告诉 UIO 核心:"这台设备有哪些内存窗口、有没有中断、怎么做最小中断处理"
- UIO 核心据此创建设备节点
/dev/uioX - 用户程序打开
/dev/uioX以后:- 用
mmap映射驱动声明的内存窗口 - 用
read或poll等待中断事件 - 必要时用
write告诉内核重新使能中断
- 用
对应到课程和内核代码:
| 对象 | 在这章里扮演什么角色 |
|---|---|
struct uio_info |
UIO 驱动给内核的"设备说明书",里面描述 mem[]、irq、handler、irqcontrol 等信息 |
uio_register_device() |
把这份说明书注册给 UIO 核心,创建 /dev/uioX 和 sysfs 信息 |
/dev/uioX |
用户态与这台 UIO 设备交互的设备节点 |
mmap |
把 uio_info.mem[n] 描述的寄存器或内存窗口映射给用户态 |
read/poll |
等待 UIO 事件计数增加,通常对应一次中断到来 |
write |
调用驱动提供的 irqcontrol,常用于重新使能中断 |
3. 课程 uio_led 示例,具体把什么交给了用户态
课程示例 source/A7/15_devmem和UIO/02_uio_led/uio_led.c 做的事情非常克制:
- 分配并填写
struct uio_info - 填了两段物理内存:
mem[0] = 0x50000000mem[1] = 0x50002000
memtype都设成UIO_MEM_PHYS- 调用
uio_register_device()
这意味着示例驱动只演示了 UIO 的"内存映射"半边,没有演示"中断"半边。
用户程序 uio_led_test.c 随后:
- 打开
/dev/uio0 mmap(..., offset = 0 * 4096)映射mem[0]mmap(..., offset = 1 * 4096)映射mem[1]- 再自己去改 RCC/GPIO 寄存器完成 LED 开关
这里最值得初学者记住的是:
/dev/uio0不是"自动帮你操作 LED"的设备文件- 它只是一个受 UIO 驱动约束后的入口
- 真正的寄存器写法,仍然在用户程序里
4. UIO 的中断链路,到底是谁在做什么
这一段是初学者最容易讲混的地方。
正确的顺序应该是:
- 硬件中断先进入内核
- UIO 驱动提供的中断回调先在内核里执行
- 如果回调返回
IRQ_HANDLED,UIO 核心增加事件计数并唤醒等待队列 - 用户程序里阻塞在
/dev/uioX的read或poll被唤醒 - 用户程序再去读寄存器、清中断源、做自己的业务逻辑
- 如果驱动设计成"先关中断、用户态处理完再开",用户程序还要
write一个整数回/dev/uioX,让irqcontrol重新使能中断
也就是说:
- "中断回调"首先是内核回调,不是用户态回调
- 用户态拿到的不是一个神奇的 ISR,而是"事件到了,你该醒了"的通知
read/poll读到的是事件计数变化,不是硬件数据本身
对照内核实现来看:
drivers/uio/uio.c里的uio_interrupt()会先调用驱动的handler- 如果处理成功,再通过
uio_event_notify()增加事件计数并唤醒阻塞中的进程 uio_read()返回的是一个 32 位事件计数uio_poll()判断事件计数是否变化uio_write()会调用irqcontrol
而在通用平台驱动 drivers/uio/uio_pdrv_genirq.c 里,可以看到更典型的 UIO 中断模型:
- 内核里的通用 handler 先把 IRQ 关掉,防止中断风暴
- 用户态处理完设备状态后,再通过
write(/dev/uioX)触发irqcontrol把 IRQ 打开
这也是为什么说:UIO 不是"零代码驱动",而是"最少但仍然必要的内核代码 + 更多的用户态代码"。
如果把这条链路翻译成最小用户态伪代码,大致像这样:
c
int fd = open("/dev/uio0", O_RDWR);
void *regs = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
for (;;) {
uint32_t cnt;
read(fd, &cnt, sizeof(cnt)); // 读到的是"事件计数",不是硬件数据
handle_device_status(regs); // 读状态寄存器、清中断源、处理业务
uint32_t on = 1;
write(fd, &on, sizeof(on)); // 如果驱动走 genirq/irqcontrol 模式,再重新使能 IRQ
}
注意:课程里的 uio_led 示例只覆盖 mmap 这半边,没有真正演示 IRQ 路径,所以你在 uio_led_test.c 里看不到这段 read/poll/write 流程。
把 3 种方案的边界放在一张表里
| 方案 | 内核侧参与度 | 用户态能做什么 | 适合什么 | 明显不适合什么 |
|---|---|---|---|---|
devmem/devmem2 + /dev/mem |
几乎没有设备级抽象 | 直接读写物理地址 | bring-up、临时验证、确认寄存器位定义 | 量产、复杂设备、中断协作、资源仲裁 |
UIO |
需要一个薄内核驱动描述内存和可选中断 | 主体逻辑在用户态,能 mmap,能等中断 |
简单寄存器设备、FPGA、实验性质控制程序、单应用独占设备 | 复杂时序、复杂 DMA、强子系统依赖、标准 ABI 暴露 |
| 正式内核驱动 | 内核完整接管设备生命周期 | 用户态通过标准节点/子系统接口访问 | 量产设备、需要稳定接口和系统集成的设备 | 初期寄存器验证时成本较高 |
为什么 devmem 不能被写成"推荐量产方案"
这章必须把 devmem 的局限讲重一点,否则初学者很容易得出错误结论。
1. 它不是驱动,只是绕过驱动
devmem 成功,不代表你的 Linux 驱动架构已经成立。
它只能说明:
- 这个物理地址大致是对的
- 某些寄存器位写进去以后,硬件可能有反应
它不能说明:
- 时钟、复位、电源、pinctrl 都被正确管理了
- 设备资源没有和别的驱动冲突
- 系统休眠恢复、热插拔、多进程访问等问题都成立
2. 它直接碰物理地址,风险天然很高
主要风险包括:
- 需要高权限,安全边界差
- 容易误写到别的寄存器,直接把系统状态搞乱
- 可能和内核里已经加载的正式驱动"抢同一块硬件"
- 某些寄存器有副作用,比如写 1 清中断、读后清状态,用
devmem很容易误操作 CONFIG_STRICT_DEVMEM可能限制访问,导致"有些地址能碰,有些地址碰不了"
3. 它没有设备级资源管理
/dev/mem 不理解"这是一颗 GPIO 控制器"还是"这是一段时钟寄存器"。
它只知道"某个物理地址区间"。所以它不会自动替你做这些事:
- 申请和独占寄存器资源
- 管理时钟/复位/电源
- 接入中断框架
- 接入某个 Linux 子系统
这也是课程里 STM32MP157 上直接用 devmem2 控 LED 没有成功的关键提醒:寄存器地址对,不等于系统级条件都满足。
如果你在 STM32 上用 devmem/devmem2 失败,建议按这个顺序排查:
- 先确认权限和
/dev/mem是否可用。
先看ls -l /dev/mem,再确认是不是 root,以及内核是否启用了会限制访问的STRICT_DEVMEM策略。 - 再确认地址是不是当前 SoC 和当前板级设计真的在用的那一段。
不要只抄资料里的地址,先对照芯片手册、设备树和板级原理图。 - 再查这块硬件是不是已经被正式驱动接管。
可以先看dmesg、ls /sys/bus/platform/devices、相关模块是否已加载;如果已有正式驱动占用,同一组寄存器就可能不能按你预期工作。 - 再查系统级前置条件是否已经满足。
尤其是pinctrl、时钟、复位、电源域;很多寄存器"能写进去"不代表外设已经真的在工作。 - 最后再查"写这个寄存器是否足以得到现象"。
例如点 LED 往往不只是改一个 GPIO 输出寄存器,还可能要先开时钟、切引脚复用、关闭冲突功能。
什么时候可以停在 UIO,什么时候必须继续写正式驱动
这个判断标准,比"会不会用 UIO"更重要。
适合先停在 UIO 的场景
- 设备本质上就是几段寄存器 + 一两个简单中断
- 单一用户程序独占这台设备
- 目标是 bring-up(上电初期验证)、验证、实验、工具化控制,而不是系统通用能力
- 不需要接入现成子系统,也不需要对外提供稳定标准接口
- 用户态算法或控制逻辑经常改,用 UIO 迭代更快
必须继续写正式内核驱动的信号
- 设备要接到
input、iio、v4l2、tty、netdev等标准子系统 - 需要稳定 ABI(也就是用户态长期依赖、不能随便改的接口),不能要求应用都直接 mmap 寄存器
- 需要完整处理时钟、复位、regulator、runtime PM(运行时电源管理)、休眠恢复
- 中断处理复杂,或者有共享 IRQ、线程化 IRQ、复杂屏蔽/确认时序
- 涉及 DMA、cache 一致性、IOMMU(设备地址映射与隔离)、scatter-gather(把多段离散内存拼成一次 DMA 传输)、零拷贝等问题
- 需要多个进程协作访问,或者权限边界需要被严格控制
- 设备会量产、要长期维护、要交给别人复用
最短判断句可以记成:
UIO 适合"设备简单、应用独占、逻辑多变";正式驱动适合"设备复杂、系统集成、接口要稳定"。
如果你只想记一个新手版判断法,可以直接用这句:
只要设备开始碰到标准子系统、多进程共享、复杂中断、DMA 或功耗管理,就不要停在 UIO。
推荐学习顺序
这一章建议严格按"先建立边界,再做实验,再回源码"来学,不要一上来就钻内核代码。
| 顺序 | 先看什么 | 再做什么 | 最后读什么源码 | 这一轮的目标 |
|---|---|---|---|---|
| 1 | doc_pic/15_devmem和UIO/01_devmem和UIO.md |
不急着跑实验,先把 /dev/mem 和 UIO 的角色分开 |
暂时不读源码 | 先知道这章不是两种"等价方案" |
| 2 | doc_pic/15_devmem和UIO/02_devmem应用与驱动分析.md |
用 devmem/devmem2 做一次只针对寄存器的验证,重点观察"它只是直接改地址" |
stm32mp15xc-kernel/drivers/char/mem.c 里的 /dev/mem 和 mmap_mem() |
建立 工具 -> /dev/mem -> mmap -> 物理地址 这条线 |
| 3 | doc_pic/15_devmem和UIO/03_uio驱动分析与编写.md |
先跑 uio_led 示例:insmod uio.ko、insmod uio_led.ko、ls /dev/uio*、执行 uio_led_test |
source/A7/15_devmem和UIO/02_uio_led/uio_led.c、uio_led_test.c |
看懂 UIO 至少需要"薄驱动 + 用户程序"两部分 |
| 4 | 补看内核树 drivers/uio |
如果条件允许,查看 /sys/class/uio/uio0/maps/map0/、map1/ 的 addr/size/name |
stm32mp15xc-kernel/drivers/uio/uio.c |
看懂 /dev/uioX、mmap、read/poll/write 的内核实现 |
| 5 | 再看 drivers/uio/uio_pdrv_genirq.c |
不一定要马上实操中断,但要把中断链路画出来 | uio_pdrv_genirq.c |
补齐 UIO 的中断半边,而不是只停在 LED 映射示例 |
推荐实验顺序
如果你是 Linux 驱动初学者,建议实验顺序用最保守的版本:
- 先看
01_devmem和UIO.md,脑子里先有边界。 - 再看
02_devmem应用与驱动分析.md,只把devmem2当"寄存器验证工具"。 - 如果板子环境复杂,不要把"STM32 上一定点亮 LED"当作唯一成败标准;这一节更重要的是理解为什么
devmem容易受系统状态影响。 - 接着跑
02_uio_led,因为它更接近"可维护的实验方案"。 - 跑通
uio_led_test之后,再倒回来读uio_led.c和drivers/uio/uio.c,理解每一层到底帮了你什么。 - 最后补
uio_pdrv_genirq.c,把read/poll和中断重使能这条线补齐。
Linux 初学者最容易混淆的点
1. devmem 不是驱动
它只是一个用户态工具,底层借助 /dev/mem 去访问物理地址。你用它点亮 LED,不等于你已经"有了一个 LED 驱动"。
2. UIO 不是零代码驱动
你仍然需要内核侧代码去:
- 声明哪些地址可以映射
- 决定有没有中断
- 决定中断来了以后最小要做什么
- 决定用户态能不能重新使能中断
没有这层内核代码,就不会有 /dev/uioX。
3. mmap 不是 UIO 独有的
/dev/mem 和 /dev/uioX 都会用到 mmap。差别不在于"有没有 mmap",而在于:
/dev/mem映射的是你自己指定的物理地址/dev/uioX映射的是驱动提前注册好的地址窗口
4. /dev/uioX 不是"业务数据流"
很多初学者看到 read(/dev/uio0),会下意识以为它像串口、I2C、网络 socket 那样返回设备数据。
不是。
UIO 里常见的 read() 返回的是事件计数,主要用于"有中断来了,把我叫醒"。
5. poll 被唤醒,不等于设备已经全处理完
poll/read 只说明事件到了。真正的寄存器确认、中断源清除、状态机推进,通常还要用户程序自己做。
6. 课程 LED 示例只展示了 UIO 的 mmap 半边
uio_led 没有设置 IRQ,所以它不能代表 UIO 的完整能力。你必须再读 uio.c 和 uio_pdrv_genirq.c,才能把 read/poll 那半边补齐。
学完这一章后,应该能做到什么
学完这章后,你至少应该能独立做到下面几件事:
- 能向别人解释
devmem/devmem2、/dev/mem、mmap之间的关系 - 能解释为什么
devmem适合验证,不适合当量产驱动方案 - 能看懂一个最小 UIO 驱动里
uio_info、mem[]、uio_register_device()在干什么 - 能看懂用户态程序为什么要
open("/dev/uioX")、mmap()、以及在有中断时为什么要read/poll - 能说清楚 UIO 的中断链路里"内核回调"和"用户态处理"分别发生在哪里
- 面对一个新设备时,能判断它应该先用
devmem验证、用 UIO 过渡,还是直接进入正式内核驱动开发
结语
如果把前面很多章节理解成"怎么把设备正确接进 Linux",那这一章就是在提醒你:工程上确实存在更快的临时路径,但这些路径各自只能解决一部分问题。
所以,这章最成熟的学习结论不是"以后我都用 UIO 就行",而是:
- 用
devmem做最小寄存器验证 - 用 UIO 做受约束的用户态原型
- 一旦设备进入系统集成和长期维护阶段,就回到正式内核驱动
这样学,这一章才真正接上了整套驱动课程的主线。