请添加图片描述

这篇文章整理自课程第 13-15 章。它们分别对应视频采集、传感器统一接口,以及临时验证与正式驱动之间的边界,是整套课后半段最能拉开工程认知的一组内容。
源文件地址:git clone https://e.coding.net/weidongshan/linux/doc_and_source_for_drivers.git

STM32MP157 Linux驱动学习笔记(一):驱动基础与设备模型入门(同步互斥/LCD/I2C/Input)
STM32MP157 Linux驱动学习笔记(二):硬件资源地基(Pinctrl/GPIO/Interrupt)
STM32MP157 Linux驱动学习笔记(三):系统级驱动框架(UART/PCIe)
STM32MP157 Linux驱动学习笔记(四):典型总线与设备模型(SPI/USB)
STM32MP157 Linux驱动学习笔记(五):子系统与工程边界(V4L2/IIO/devmem/UIO)


文章目录


13_V4L2 学习总结

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 步:

  1. 用户程序打开 /dev/videoX
  2. 先问设备:你会什么格式、什么分辨率
  3. 选一种双方都能接受的格式
  4. 申请几块内存,当作“装图像的盒子”
  5. 把这些空盒子交给驱动
  6. 驱动从摄像头拿到一帧图像后,往某个盒子里写数据
  7. 用户程序把这个已经装满图像的盒子取回来
  8. 用户程序把图像保存成 jpg、显示到 LCD,或者继续交回去抓下一帧

如果把白话和课程源码对起来,大致就是:

白话动作对应接口/代码
打开设备open("/dev/videoX", O_RDWR)
问设备会什么VIDIOC_QUERYCAPVIDIOC_ENUM_FMTVIDIOC_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.jpg
  • 02_video2lcd:把它直接显示到 LCD
  • 01_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 摄像头的格式、分辨率、控制能力都能从这里追根
vb2videobuf2,V4L2 的通用 buffer 管理框架第一次只要知道它负责 buffer 生命周期
vb2_ops驱动最值得先看的回调组第一次重点看 queue_setupbuf_queuestart_streamingstop_streaming
vb2_mem_ops负责内存分配、映射等辅助动作第一次知道“管内存”就够了,不必逐个成员深挖
vb2_buf_ops负责用户态 v4l2_buffer 和内核 vb2_buffer 之间的参数搬运第一次知道“管参数搬运”就够了,不必逐个成员深挖

一句话记忆:

第一次学习时,vb2 先当成“V4L2 帮你管理 buffer 的公共底座”;vb2_mem_opsvb2_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/videoX
  • v4l2_file_operations:处理文件级入口
  • v4l2_ioctl_ops:处理 V4L2 语义
  • vb2:处理 buffer 生命周期

第一次读框架时,建议把注意力放在这三层:

  1. vidioc_querycapvidioc_enum_fmt_vid_capvidioc_s_fmt_vid_cap
  2. vb2_ops 里的 queue_setupbuf_queuestart_streamingstop_streaming
  3. 驱动里“什么时候调用 vb2_buffer_done 把一帧还给用户”

至于 vb2_mem_opsvb2_buf_ops 的细枝末节,放到第二遍再看更合适。

可执行 Runbook

这一节不是“建议”,而是尽量写成你可以照着跑的最小闭环。

Runbook 1:先确认真实摄像头有没有正确暴露能力

目标:先别急着抓图,先确认 /dev/videoX 存在,格式和分辨率能枚举出来。

最小命令集:

# 如果板子上有 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

目标:跑通这一章最核心的用户态数据链路。

最小命令集:

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.jpgvideo_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 buffersUnable to map buffer:先看驱动是否支持 streaming / mmap
  • 一直没有 jpg:先看是不是卡在 poll,再看 dmesg

Runbook 3:抓帧的同时调亮度

目标:把“数据流”和“控制流”分开看。

最小命令集:

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 Unit bmControls 看 Brightness 是否真的被设备声明支持

Runbook 4:加载虚拟摄像头驱动,验证 /dev/videoX 是怎么被造出来的

目标:把“真实硬件采图”换成“课程自己造一台虚拟摄像头”,从而看懂驱动最小闭环。

前提:

  • 你使用的内核源码目录已经配置并编译过
  • 04_virtual_driver_ok/Makefile 里的 KERN_DIR 要对应你正在运行的内核源码目录

最小命令集:

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 开发板上加载:

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 去验证:

./video_get_data /dev/videoX

如果你已经把 02_video2lcd/video2lcd.7z 解压并编好,也可以:

./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 抢占屏幕

video2lcdmjpg-streamer 在学习路径里的角色

这两个应用都很有用,但建议放在核心闭环之后:

应用它更适合放在什么时候看作用
video2lcd在你已经能抓到 jpg 之后让你看到同一帧数据也可以直接显示到 LCD
mjpg-streamer在你已经理解 poll -> DQBUF -> QBUF 之后让你看到同一帧数据还能被转成网络视频流

先看什么、再做什么实验、再读哪些源码

建议强制按这个顺序,不要跳。

顺序做什么为什么这么排
1先看 01_V4L2应用程序开发.md先把用户态采图闭环立住
2Runbook 1先确认设备节点、格式、分辨率
3Runbook 2真正把一帧图像抓出来
4Runbook 3把数据流和控制流分开理解
5再看 02_V4L2驱动程序框架.md这时再看 video_devicefopsioctl_opsvb2 不会太抽象
606_virtual_driver/01_virtual_driver_framework先看骨架怎么搭起来
706_virtual_driver/02_virtual_driver_hardware看最小格式协商和 queue 是怎么补上的
806_virtual_driver/03_virtual_driver_hardware_datatransfer看“硬件产生一帧数据”怎么变成 vb2_buffer_done
906_virtual_driver/04_virtual_driver_ok看完整闭环、锁和停止 streaming 时的回收
10最后看 04_USB摄像头驱动程序分析.md这时再看 UVC 才能把真实设备对到前面的统一框架上

Control 和 Descriptor 这条侧线,怎么挂到具体材料上

这一章的另一条重要侧线是:为什么某些摄像头有亮度控制,为什么某些格式和分辨率能枚举出来。

这里不要空谈,要直接挂到课程给的两份材料上。

1. 对照 tmp_摄像头控制接口.md

这份材料的重点不是整段英文,而是两件事:

  • 摄像头内部有 VideoControl 接口
  • Processing UnitExtension UnitInput Terminal 这些 entity 会声明自己支持哪些 control

建议你对照下面这张表看:

材料里先看什么再对照哪个 ioctl你要观察哪些字段
Processing Unit 支持 Brightness、Contrast、Hue 等VIDIOC_QUERYCTRLminimummaximumstepdefault_value
control 有 current / min / max / resolution / default 这些属性VIDIOC_G_CTRLVIDIOC_S_CTRL当前值能不能读出来、改过去后会不会生效
Extension Unit 代表扩展控制能力仍然先从 VIDIOC_QUERYCTRL 出发某些厂商私有控制项为什么不属于最常见的标准 control

最值得建立的印象是:

V4L2 的 control 不是凭空出现的,它通常可以追溯到摄像头控制接口里的某个 entity 声明。

2. 对照 USB摄像头描述符.txt

这份材料最值得先抓的不是整份 lsusb -v 输出,而是这几类字段:

描述符字段再对照哪个 ioctl你要建立什么对应关系
bNumFormatsFORMAT_UNCOMPRESSEDFORMAT_MJPEG 之类的格式描述VIDIOC_ENUM_FMT用户态能枚举到哪些像素格式
FRAME_UNCOMPRESSED / FRAME_MJPEG 里的 wWidthwHeightVIDIOC_ENUM_FRAMESIZES用户态能枚举到哪些分辨率
dwFrameInterval这份示例程序没打印,但值得知道它存在同一分辨率为什么还能有不同帧率
Processing Unit 里的 bmControlsVIDIOC_QUERYCTRL某个 control 为什么能查到,或者为什么查不到

建议你按这个顺序做一次对照:

  1. 先跑 03_video_params/video_test.c
  2. 记下它打印出来的 VIDIOC_ENUM_FMTVIDIOC_ENUM_FRAMESIZES 结果
  3. 再去看 USB摄像头描述符.txt 里的 FORMAT_*FRAME_*wWidthwHeight
  4. 再跑 05_video_brightness/video_test.c
  5. 最后去看 tmp_摄像头控制接口.md 和描述符里的 Processing Unit bmControls

这样你会更容易形成一个稳定认识:

  • VIDIOC_ENUM_FMT 不是凭空编出来的,它背后能追到设备报告的格式能力
  • VIDIOC_ENUM_FRAMESIZES 不是凭空编出来的,它背后能追到设备报告的帧大小
  • VIDIOC_QUERYCTRL 能查到什么,也通常能追到控制接口声明了什么

用虚拟摄像头驱动把抽象跑通

第 13 章最有价值的地方,是它没有停在框架图,而是给了一个从 0 补起来的虚拟摄像头驱动。

建议按目录顺序看,不要直接跳最终版:

目录这一阶段在补什么你要重点看什么
06_virtual_driver/01_virtual_driver_framework先搭骨架video_devicev4l2_file_operationsv4l2_ioctl_opsvb2_queue 怎么挂起来
06_virtual_driver/02_virtual_driver_hardware开始补“像个摄像头”的最小能力querycapenum_fmts_fmtenum_framesizesqueue_setupbuf_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 到时,相当于“硬件产生了一帧数据”:

  1. g_queued_bufs 里拿一个空闲 buffer
  2. 通过 vb2_plane_vaddr() 得到它的内核地址
  3. red/green/blue 这些模拟图片数据拷进去
  4. vb2_set_plane_payload() 告诉上层这一帧实际用了多少字节
  5. 最后 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_QUERYCTRLVIDIOC_G_CTRLVIDIOC_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 正常运行时停在无限抓帧循环里
controlformat 是一回事不对。format 决定图像怎么组织,control 决定摄像头参数
VIDIOC_QUERYCTRL 失败就说明驱动坏了不一定。也可能是这个设备根本没声明支持该 control
read()mmap streaming 是同一套思路不完全一样。课程重点是更常见的 mmap + streaming 路线
vb2_mem_opsvb2_buf_ops 必须第一次就吃透不需要。第一次先知道它们分别负责“内存”和“参数搬运”就够了
/dev/video0 一定是我要用的那个节点不一定。真实摄像头、虚拟摄像头并存时,节点号会变化
示例程序一跑不通就是驱动框架没懂很多时候只是节点选错、格式不支持、模块版本不匹配,先排最短路径问题

其中最值得反复提醒自己的一句是:

V4L2 的难点不在于单个 ioctl,而在于“格式协商 + buffer 生命周期 + streaming 状态机”这三件事是连在一起的。

学完这一章后,应该能做到什么

学完这章后,至少应该具备下面这些能力:

  • 能用白话讲清“一帧图像怎样从 /dev/videoX 进到用户程序”
  • 能把 QUERYCAPENUM_FMTENUM_FRAMESIZESS_FMTREQBUFSQBUFDQBUFSTREAMON/OFF 串成一条完整链路
  • 能区分“标准 streaming 概念闭环”和“课程 04_video_get_data 实际运行时停在哪”
  • 能照着 runbook 跑出 /dev/videoX、jpg 输出、亮度控制、虚拟摄像头最小闭环
  • 能分清 v4l2_file_operationsv4l2_ioctl_opsvb2、虚拟摄像头、UVC 之间的大致分工
  • 能把 tmp_摄像头控制接口.mdUSB摄像头描述符.txt 和用户态 VIDIOC_QUERYCTRLVIDIOC_ENUM_FMTVIDIOC_ENUM_FRAMESIZES 对上

如果这章学完后你还能稳定回答下面 3 个问题,就说明主线已经建立起来了:

  1. 为什么 mmap 之后还要 QBUFSTREAMONDQBUF
  2. 为什么课程示例正常抓帧时主要停在 poll -> DQBUF -> QBUF,但标准流程里仍然必须知道 STREAMOFF 的位置?
  3. 为什么真实 USB 摄像头和课程里的虚拟摄像头,最后都能被同样的 /dev/videoX 用户程序打开?

能把这 3 个问题答顺,这章就不是“看过”,而是真的入门了。


14_IIO 学习总结

14_IIO 全景图

TL;DR:先别急着背 buffertriggerevent,先把 IIO 想成“Linux 给各类传感器和 ADC 提供的统一数据接口”。如果应用只想读一次数据,就走 sysfs 直读;如果要连续拿一串采样,就把多个 channel 按 scan 格式塞进 buffer;如果要决定“什么时候采”,就挂 trigger;如果要在越阈时主动通知用户,就再加 event。课程里的 DHT11 示例,正是沿着这条线一步步增强出来的。

章节定位

14_IIO 是这套驱动课里第二次比较系统地进入一个 Linux 子系统。

它在整套课里的位置,建议这样理解:

章节关系这一章承接了什么这一章新增了什么
往前承接 06_Pinctrl07_GPIO08_Interrupt你已经知道引脚怎么配、GPIO 怎么读写、中断怎么收边沿这一章开始把“底层引脚/中断细节”包装成标准传感器接口
可类比 13_V4L2上一章学的是“视频帧数据怎么统一暴露给应用”这一章学的是“标量传感器数据怎么统一暴露给应用”
往后对照 15_devmem和UIO你会看到另一种更底层、更临时的访问方式这一章先建立“优先走现成子系统”的正路思维

所以这章的边界要先划清:

  • 它重点不是讲 DHT11 通信时序本身,也不是讲 ADC 电路细节。
  • 它重点是讲 Linux 为什么要把温度、湿度、电压、加速度这类“传感器数据”统一抽象成 IIO。

一句话概括:这章是在建立“传感器数据应该怎样被标准化暴露给用户态”的心智模型。

学习主线

这一章建议强制按下面这条主线走,不要一上来就钻进细节:

  1. 先回答:为什么 Linux 不希望每个传感器都各写一套私有字符设备接口
  2. 再看最小 IIO 模型:iio_dev、channel、sysfs 直读
  3. 接着看 buffer:为什么单次读取不够,为什么需要批量采样
  4. 再看 trigger:为什么“什么时候采”也要从驱动里抽出来复用
  5. 最后看 event:为什么“越阈通知”不能继续靠用户态死循环轮询
  6. 学完 DHT11 的完整演进后,再回头看 STM32 ADC 现成驱动

如果按这条线学,IIO 不会像一堆零散术语;它会变成一条很稳定的能力增强链路:

sysfs 直读 -> buffer 批量采样 -> trigger 决定时机 -> event 主动上报 -> 套回真实 ADC 驱动

为什么 Linux 里会有 IIO 子系统

先用第一性原理看这个问题。

Linux 里有很多“会产生测量值”的设备:

  • ADC:给你电压采样值
  • DHT11:给你温度、湿度
  • 光照、加速度、陀螺仪、磁力计:给你一个或多个物理量

这些设备长得不一样,但对软件来说,经常在解决同一类问题:

共同问题如果没有 IIO,会发生什么IIO 想统一什么
一个设备往往不止一个测量量每个驱动自己定义“温度怎么读、湿度怎么读、电压怎么读”channel 统一表示每个测量通道
有时只想读一次,有时想连续采样每个驱动各写一套私有 read/ioctl/poll 接口sysfsbuffertrigger 提供统一能力
应用得知道数据单位和格式每个驱动自己发明命名和换算规则_raw_scale_input、scan 格式统一表达
某些场景要阈值报警每个驱动各写一套中断和告警接口event 统一事件上报

所以 IIO 的价值,不是“帮你少写几行代码”,而是:

  • 让传感器类驱动不要重复发明用户接口
  • 让应用看到统一的命名、统一的数据路径、统一的采样模型
  • 让“单次读取”“批量采样”“触发采样”“阈值事件”这些常见需求都能复用框架

如果把这章和 13_V4L2 并排看,会更容易理解:

  • V4L2 统一的是“视频帧”
  • IIO 统一的是“传感器/ADC 这种标量采样数据”

先把 IIO 想成什么

第一次学 IIO,先把它压缩成下面这张图景:

对象直白解释这一章怎么对应
iio_dev一个 IIO 设备实例例如一个 DHT11 设备、一个 ADC 控制器实例
channel这个设备里某个可测量的量DHT11 里的温度、湿度;ADC 里的 voltage0voltage1
sysfs 直读“现在给我一个值”cat in_temp_inputcat 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 驱动,通常先做这几件事:

  1. devm_iio_device_alloc
  2. iio->info
  3. iio->channelsiio->num_channels
  4. devm_iio_device_register

对初学者来说,先把它理解成:

  • iio_dev 代表“这个设备整体”
  • info 代表“这个设备有哪些回调能力”
  • channels 代表“这个设备能提供哪些测量量”

2. channel 是逻辑测量通道,不是另一个设备

这点非常关键。

在 DHT11 里:

  • 温度是一个 channel
  • 湿度是一个 channel
  • 但它们共享同一个 iio_dev

在 STM32 ADC 里:

  • voltage0voltage1voltage2… 都是 channel
  • 但它们也共享同一个 ADC 的 iio_dev

所以不要把 channel 理解成“又注册了一个新设备”。
channel 更像“这个设备里的一列数据”。

3. sysfs 直读是最小可用路径

最小 IIO 驱动先解决的问题通常是:

应用现在就要一个值,驱动怎么给?

课程里最早让你体验的是这个路径:

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_inputin_humidityrelative_input

如果对照 STM32 ADC,会看到另一种典型模式:

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_*_rawin_*_input“现在给我一个值”iio_dev + channel + read_raw
bufferscan_elements/*buffer/*/dev/iio:deviceX“把多次采样结果连续交给我”还要知道每个 channel 的 scan 布局
triggertrigger/current_trigger“什么时候触发一次采样”通常和 buffer 一起用,也可以驱动 event
eventevents/*、事件 fd“当值越线了,主动通知我”事件描述、事件队列、触发来源

把它们串起来,关系更清楚:

  1. channel 定义“这台设备有什么可测量的量”
  2. sysfs 直读是最短路径,应用主动来问一次,驱动给一次
  3. buffer 负责保存“一批采样结果”,让应用连续读取
  4. trigger 负责定义“何时进行一次采样”
  5. 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_rawin_temp_inputin_humidityrelative_input
第 1 步:加 buffer02_iio_buffer的使用.mdsource/A7/14_iio/01_dht11_iio_buffer_software02_dht11_iio_buffer_software_课堂现场编写增加 scan_indexscan_typeavailable_scan_masks、kfifo buffer、iio_push_to_buffers新增 scan_elements/*buffer/*,并能读 /dev/iio:deviceX
第 2 步:加 trigger03_iio_trigger的使用.mdsource/A7/14_iio/03_dht11_iio_trigger04_dht11_iio_trigger_课堂现场编写采样时机从“驱动自己 schedule work”升级为“由 IIO trigger 框架提供”新增 trigger/current_trigger,可以切换 loop/hrtimer 等触发源
第 3 步:加 event04_iio_event的使用.mdsource/A7/14_iio/05_dht11_iio_event06_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_event
  • 06_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:deviceXtrigger1

最小命令集:

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:deviceXloop0timer_abc,都应该替换成你这一步查出来的实际名字

Runbook 1:先只体验 sysfs 直读

目标:先确认自己真的理解了“一个 IIO 设备下有多个 channel”。

最小命令集:

cd /sys/bus/iio/devices/iio:deviceX
cat in_humidityrelative_input
cat in_temp_input

观察点:

  • iio:deviceX 的编号不固定,不要死记 device1device2
  • 你读的是同一个设备下的两个 channel
  • 这一步还没有 buffertriggerevent 的负担

Runbook 2:开启 buffer,看 scan 是怎么出现的

目标:把“单次值”升级成“连续样本流”。

最小命令集:

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 进入 scan
  • buffer/enable 之后,/dev/iio:deviceX 才有连续数据可读
  • hexdump 看到的是 scan 布局后的二进制,不是漂亮文本

Runbook 3:给 buffer 换上标准 trigger

目标:理解“采样时机”和“缓冲存储”是两回事。

以 loop trigger 为例:

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:

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_triggerscan_elements/*_enbuffer/enable 已经配置好
  • 如果你不是连续从 Runbook 3 往下做,就先把下面这些命令补上
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

最小命令集:

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 驱动
3Runbook 1先把 iio_dev、channel、sysfs 直读建立起来
4再看 02_iio_buffer的使用.md这时再学 scan、buffer、/dev/iio:deviceX 不会太抽象
5source/A7/14_iio/01_dht11_iio_buffer_software,再对照 02_dht11_iio_buffer_software_课堂现场编写先看成型版,再看课堂版是怎么一步步补出来的
6Runbook 2亲眼看到 scan_elementsbuffer 生效
7再看 03_iio_trigger的使用.md这时再学 trigger,能明确它是在解决“何时采样”
8source/A7/14_iio/03_dht11_iio_trigger,再对照 04_dht11_iio_trigger_课堂现场编写 和内核树 drivers/iio/trigger/iio-trig-loop.ciio-trig-hrtimer.cindustrialio-trigger.c看 DHT11 如何挂到通用 trigger 框架上,也看课堂版是怎么补齐这层抽象的
9Runbook 3把 loop trigger、hrtimer trigger 的差别跑出来
10再看 04_iio_event的使用.md这时再引入 event,层次最清楚
11先读 source/A7/14_iio/05_dht11_iio_event,再读 06_dht11_iio_event_课堂现场编写先看概念演示,再看完整阈值配置版
12Runbook 4把“样本流”和“事件流”真正分开
13最后看 05_iio驱动示例.md,并对照 stm32mp15xc-kernel/drivers/iio/adc/stm32-adc.c把 DHT11 学到的模型迁移到真实 ADC 驱动上

每一阶段从哪个函数入口读起

只知道“读哪个文件”还不够。对初学者来说,更实用的是先抓函数入口。

阶段先看哪些函数带着什么问题去看
最小 sysfs 模型dht11_probedht11_read_rawiio_dev 怎么注册出来,in_temp_input/in_humidityrelative_input 为什么会出现
buffer 阶段iio_push_to_buffers,以及 DHT11 里和 buffer setup 相关的初始化路径样本什么时候被推进 buffer/dev/iio:deviceX 为什么开始有数据
trigger 阶段iio_triggered_buffer_setupiio_trigger_poll,再对照 iio-trig-loop.ciio-trig-hrtimer.ctrigger 怎么决定“什么时候采”,以及它怎么和 buffer 连接起来
event 阶段read_event_valuewrite_event_valueiio_push_event阈值配置怎么进驱动,事件又是怎么被推给用户态的
STM32 ADC 迁移stm32_adc_probestm32_adc_read_rawstm32_adc_chan_of_initDHT11 学到的 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 常表示原始码值,_inputprocessed 更接近已经可直接使用的物理量
使能了 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:device1trigger1 这些编号是固定的不对。设备号和 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、sysfsbuffertriggerevent 分别在干什么
  • 能把 sysfs 直读 -> buffer 批量采样 -> trigger 决定时机 -> event 上报告警 串成一条完整链路
  • 能说清 DHT11 示例是怎样从最小 IIO 驱动一步步增强到支持 buffer、trigger、event 的
  • 能照着建议顺序去跑实验,并知道每一步应该观察什么
  • 能把 DHT11 学到的模型迁移到 STM32 ADC 这种真实 IIO 驱动上
  • 能明确区分“GPIO/ADC 普通驱动的底层实现手段”和“IIO 通道驱动的对外抽象方式”

如果这章学完后你还能稳定回答下面 3 个问题,就说明主线已经建立起来了:

  1. 为什么单次 sysfs 读取解决不了连续采样问题?
  2. 为什么 buffertrigger 必须分开理解?
  3. 为什么 DHT11 底层明明靠 GPIO/IRQ 实现,对外却更适合做成 IIO 驱动?

能把这 3 个问题答顺,这章就不是“看过”,而是真的入门了。


15_devmem和UIO 学习总结

15_devmem和UIO 全景图

TL;DR:这一章不是在教你“少写驱动的捷径”,而是在教你区分 3 种完全不同的工程层次。devmem/devmem2 是用户态工具,它们借助 /dev/memmmap 直接碰物理地址,适合寄存器级验证;UIO 是“内核保留最小控制权,用户态承担主要设备逻辑”的折中方案,内核暴露 /dev/uioX、内存窗口和可选中断,用户程序再用 mmapread/poll 去工作;真正要量产、要接入 Linux 设备模型和子系统时,还是要回到正式内核驱动。
这章最重要的收获,不是点亮一个 LED,而是知道:什么时候可以临时在用户态做实验,什么时候必须回到内核里把驱动写完整。

章节定位

15_devmem和UIO 是课程后半段的一章“边界校准课”。

前面的很多章节都在教你怎么按 Linux 的正规方式写驱动:比如 06_Pinctrl07_GPIO08_Interrupt 让你理解硬件资源和中断是怎么进入内核的,09_UART11_SPI12_USB13_V4L214_IIO 又让你看到设备最终通常要挂到某个子系统下面,给用户态提供稳定接口。

到了这一章,课程故意换一个角度:如果你现在只是想快速验证寄存器、想把一块很简单的设备先跑起来,是否一定要马上写完整内核驱动?

这章给出的答案不是“是”或“否”,而是分层:

  • devmem/devmem2:最野、最快,但边界最差,只适合临时验证。
  • UIO:内核先把设备的地址空间和可选中断整理出来,再交给用户态处理主要逻辑。
  • 正式内核驱动:当设备已经超出“简单寄存器 + 简单中断 + 单一应用独占”的范围时,就必须回到这条路。

一句话概括:这章是在帮你建立“用户态直接控硬件”和“正式内核驱动”之间的分界线。

学习主线总览

这章最稳的学习主线,不是背概念,而是沿着下面这条路径走:

阶段你要解决的问题代表对象
第 1 层:直接摸寄存器我能不能先不写驱动,直接改一个寄存器验证硬件?devmem/devmem2/dev/memmmap
第 2 层:用户态承担主要逻辑我能不能只写很薄的一层内核代码,把寄存器和中断交给用户态?UIO/dev/uioXread/poll
第 3 层:回到正式驱动设备一旦变复杂,哪些事不能继续停在用户态?子系统驱动、设备树、时钟/复位/电源管理、DMA、标准 ABI

如果只记一条最短主线,请记住:

devmem/devmem2 -> /dev/mem -> mmap -> 直接访问物理寄存器

uio_info/uio_register_device -> /dev/uioX -> mmap/read/poll -> 用户态主逻辑

这两条线看起来都叫“用户态访问硬件”,但它们的安全边界、内核参与程度、可维护性完全不同。

先把对象关系讲清楚

1. devmem/devmem2/dev/memmmap 是什么关系

先把最容易混淆的一点钉住:

  • devmemdevmem2 只是用户态工具,不是驱动。
  • 课程里更常出现 devmem2,因为它更方便指定位宽;但 devmemdevmem2 依赖的底层机制是同一类东西。
  • 真正提供“访问物理地址”能力的是内核里的字符设备 /dev/mem
  • mmap 是进程把某段物理地址映射到自己虚拟地址空间的系统调用方式。

把它们串起来,就是:

  1. 用户程序执行 devmemdevmem2
  2. 工具去打开 /dev/mem
  3. 通过 mmap 把目标物理页映射进当前进程
  4. 用户程序在映射后的虚拟地址上做读写
  5. 最终效果看起来像“我在用户态改了寄存器”

对应到内核源码:

  • /dev/mem 的核心实现可以看 stm32mp15xc-kernel/drivers/char/mem.c
  • 其中 mmap_mem() 负责把物理页映射给用户进程
  • 访问是否被允许,会受 CONFIG_STRICT_DEVMEM 和平台限制影响

所以,devmem2 0x50002018 w 0x400 这类命令,本质上不是“系统自带了 GPIO 驱动”,而是“你绕过了设备模型,直接对某个物理地址动手”。

2. UIO/dev/uioXmmapread/poll 是什么关系

UIO/dev/mem 多做了一层整理。

它的核心思路是:

  • 内核里仍然要有一个很薄的 UIO 驱动
  • 这个驱动告诉 UIO 核心:“这台设备有哪些内存窗口、有没有中断、怎么做最小中断处理”
  • UIO 核心据此创建设备节点 /dev/uioX
  • 用户程序打开 /dev/uioX 以后:
    • mmap 映射驱动声明的内存窗口
    • readpoll 等待中断事件
    • 必要时用 write 告诉内核重新使能中断

对应到课程和内核代码:

对象在这章里扮演什么角色
struct uio_infoUIO 驱动给内核的“设备说明书”,里面描述 mem[]irqhandlerirqcontrol 等信息
uio_register_device()把这份说明书注册给 UIO 核心,创建 /dev/uioX 和 sysfs 信息
/dev/uioX用户态与这台 UIO 设备交互的设备节点
mmapuio_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] = 0x50000000
    • mem[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 的中断链路,到底是谁在做什么

这一段是初学者最容易讲混的地方。

正确的顺序应该是:

  1. 硬件中断先进入内核
  2. UIO 驱动提供的中断回调先在内核里执行
  3. 如果回调返回 IRQ_HANDLED,UIO 核心增加事件计数并唤醒等待队列
  4. 用户程序里阻塞在 /dev/uioXreadpoll 被唤醒
  5. 用户程序再去读寄存器、清中断源、做自己的业务逻辑
  6. 如果驱动设计成“先关中断、用户态处理完再开”,用户程序还要 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 不是“零代码驱动”,而是“最少但仍然必要的内核代码 + 更多的用户态代码”。

如果把这条链路翻译成最小用户态伪代码,大致像这样:

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 失败,建议按这个顺序排查:

  1. 先确认权限和 /dev/mem 是否可用。
    先看 ls -l /dev/mem,再确认是不是 root,以及内核是否启用了会限制访问的 STRICT_DEVMEM 策略。
  2. 再确认地址是不是当前 SoC 和当前板级设计真的在用的那一段。
    不要只抄资料里的地址,先对照芯片手册、设备树和板级原理图。
  3. 再查这块硬件是不是已经被正式驱动接管。
    可以先看 dmesgls /sys/bus/platform/devices、相关模块是否已加载;如果已有正式驱动占用,同一组寄存器就可能不能按你预期工作。
  4. 再查系统级前置条件是否已经满足。
    尤其是 pinctrl、时钟、复位、电源域;很多寄存器“能写进去”不代表外设已经真的在工作。
  5. 最后再查“写这个寄存器是否足以得到现象”。
    例如点 LED 往往不只是改一个 GPIO 输出寄存器,还可能要先开时钟、切引脚复用、关闭冲突功能。

什么时候可以停在 UIO,什么时候必须继续写正式驱动

这个判断标准,比“会不会用 UIO”更重要。

适合先停在 UIO 的场景

  • 设备本质上就是几段寄存器 + 一两个简单中断
  • 单一用户程序独占这台设备
  • 目标是 bring-up(上电初期验证)、验证、实验、工具化控制,而不是系统通用能力
  • 不需要接入现成子系统,也不需要对外提供稳定标准接口
  • 用户态算法或控制逻辑经常改,用 UIO 迭代更快

必须继续写正式内核驱动的信号

  • 设备要接到 inputiiov4l2ttynetdev 等标准子系统
  • 需要稳定 ABI(也就是用户态长期依赖、不能随便改的接口),不能要求应用都直接 mmap 寄存器
  • 需要完整处理时钟、复位、regulator、runtime PM(运行时电源管理)、休眠恢复
  • 中断处理复杂,或者有共享 IRQ、线程化 IRQ、复杂屏蔽/确认时序
  • 涉及 DMA、cache 一致性、IOMMU(设备地址映射与隔离)、scatter-gather(把多段离散内存拼成一次 DMA 传输)、零拷贝等问题
  • 需要多个进程协作访问,或者权限边界需要被严格控制
  • 设备会量产、要长期维护、要交给别人复用

最短判断句可以记成:

UIO 适合“设备简单、应用独占、逻辑多变”;正式驱动适合“设备复杂、系统集成、接口要稳定”。

如果你只想记一个新手版判断法,可以直接用这句:

只要设备开始碰到标准子系统、多进程共享、复杂中断、DMA 或功耗管理,就不要停在 UIO。

推荐学习顺序

这一章建议严格按“先建立边界,再做实验,再回源码”来学,不要一上来就钻内核代码。

顺序先看什么再做什么最后读什么源码这一轮的目标
1doc_pic/15_devmem和UIO/01_devmem和UIO.md不急着跑实验,先把 /dev/memUIO 的角色分开暂时不读源码先知道这章不是两种“等价方案”
2doc_pic/15_devmem和UIO/02_devmem应用与驱动分析.mddevmem/devmem2 做一次只针对寄存器的验证,重点观察“它只是直接改地址”stm32mp15xc-kernel/drivers/char/mem.c 里的 /dev/memmmap_mem()建立 工具 -> /dev/mem -> mmap -> 物理地址 这条线
3doc_pic/15_devmem和UIO/03_uio驱动分析与编写.md先跑 uio_led 示例:insmod uio.koinsmod uio_led.kols /dev/uio*、执行 uio_led_testsource/A7/15_devmem和UIO/02_uio_led/uio_led.cuio_led_test.c看懂 UIO 至少需要“薄驱动 + 用户程序”两部分
4补看内核树 drivers/uio如果条件允许,查看 /sys/class/uio/uio0/maps/map0/map1/addr/size/namestm32mp15xc-kernel/drivers/uio/uio.c看懂 /dev/uioXmmapread/poll/write 的内核实现
5再看 drivers/uio/uio_pdrv_genirq.c不一定要马上实操中断,但要把中断链路画出来uio_pdrv_genirq.c补齐 UIO 的中断半边,而不是只停在 LED 映射示例

推荐实验顺序

如果你是 Linux 驱动初学者,建议实验顺序用最保守的版本:

  1. 先看 01_devmem和UIO.md,脑子里先有边界。
  2. 再看 02_devmem应用与驱动分析.md,只把 devmem2 当“寄存器验证工具”。
  3. 如果板子环境复杂,不要把“STM32 上一定点亮 LED”当作唯一成败标准;这一节更重要的是理解为什么 devmem 容易受系统状态影响。
  4. 接着跑 02_uio_led,因为它更接近“可维护的实验方案”。
  5. 跑通 uio_led_test 之后,再倒回来读 uio_led.cdrivers/uio/uio.c,理解每一层到底帮了你什么。
  6. 最后补 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.cuio_pdrv_genirq.c,才能把 read/poll 那半边补齐。

学完这一章后,应该能做到什么

学完这章后,你至少应该能独立做到下面几件事:

  • 能向别人解释 devmem/devmem2/dev/memmmap 之间的关系
  • 能解释为什么 devmem 适合验证,不适合当量产驱动方案
  • 能看懂一个最小 UIO 驱动里 uio_infomem[]uio_register_device() 在干什么
  • 能看懂用户态程序为什么要 open("/dev/uioX")mmap()、以及在有中断时为什么要 read/poll
  • 能说清楚 UIO 的中断链路里“内核回调”和“用户态处理”分别发生在哪里
  • 面对一个新设备时,能判断它应该先用 devmem 验证、用 UIO 过渡,还是直接进入正式内核驱动开发

结语

如果把前面很多章节理解成“怎么把设备正确接进 Linux”,那这一章就是在提醒你:工程上确实存在更快的临时路径,但这些路径各自只能解决一部分问题。

所以,这章最成熟的学习结论不是“以后我都用 UIO 就行”,而是:

  • devmem 做最小寄存器验证
  • 用 UIO 做受约束的用户态原型
  • 一旦设备进入系统集成和长期维护阶段,就回到正式内核驱动

这样学,这一章才真正接上了整套驱动课程的主线。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐