STM32MP157 Linux驱动学习笔记(一):驱动基础与设备模型入门(同步互斥/LCD/I2C/Input)

这篇文章整理自课程第 02-05 章,目标不是逐页搬运讲义,而是把初学者最先该建立的驱动脑图串起来:并发基础、显示设备、总线模型、输入子系统。
源文件地址: git clone https://e.coding.net/weidongshan/linux/doc_and_source_for_drivers.git


02_同步与互斥

本章表面上先讲内联汇编,再讲原子操作和各种锁;真正的主线其实只有一条:驱动里的共享状态会被多个执行上下文同时访问时,怎样保证结果正确。后面的 03_LCD04_I2C05_Input 都会遇到进程上下文、中断、Softirq、Timer、多核并发这些问题,所以课程把这一章放在驱动细节前面,不是绕路,而是在先补"并发判断力"。

章节定位

如果只站在单线程 C 程序的视角看驱动,很容易以为"读一个变量、改一个变量、写一个寄存器"都是顺序完成的。但 Linux 内核里的真实环境不是这样:有抢占,有中断,有下半部,有 SMP 多核。也就是说,驱动代码写到一半,完全可能被别的执行流打断,回来时共享数据已经变了。

本章先从 01_inline_assembly 这组小实验切入,不是为了教你手写"更快的加法",而是为了让你真正看到:一行 C 代码落到机器层面后,往往会变成多条指令。只要是"读出、修改、写回"这样的序列,就可能暴露竞态条件。理解了这个前提,后面再看 atomic_t、位操作、自旋锁、信号量、互斥量,才不会把它们当成一组需要死记硬背的 API。

所以,这一章给后续驱动学习打的基础主要有 3 个:

  • 给"并发"建立最小心智模型:谁会同时访问一份数据,谁会打断谁。
  • 给"原子操作"建立硬件基础:为什么 ARM 上要用 ldrex/strex 这类指令。
  • 给"锁选择"建立判断框架:当前上下文能不能睡眠,要不要关中断,要不要防 Softirq,要不要考虑多核。

核心概念

  • 同步:条件还不满足时先等待,等条件满足了再继续。
  • 互斥:同一时刻只允许一个执行流进入临界区,独占临界资源。
  • 临界资源:驱动里的共享状态,比如 valid 标志、引用计数、状态位、缓冲区、队列,甚至某些需要"读改写"的寄存器访问。
  • 抢占和并发来源:进程上下文会被更高优先级任务抢占,中断会打断当前执行,Softirq、Tasklet、Timer 也可能参与共享数据访问;到了 SMP 上,另一个 CPU 还能并行执行同一段逻辑。
  • 普通 C 语句不等于原子操作。无论是 if (!valid) valid = 0;,还是 --valid,在机器层面通常都不是一个不可分割的动作,而是"读出、修改、写回"的组合。
  • 关当前 CPU 的中断不是万能解法。文档里的"失败例子 3"已经点明:对单核 UP 系统可能够用,但在 SMP 上,你只能挡住当前 CPU,挡不住别的 CPU。
  • 原子操作的关键不在"绝对不被打断",而在"即使被打断,最终效果仍然等价于不可分割"。ARMv6 及以上用 ldrex/strex 做的就是这件事:中途如果被别人改过,就放弃写回并重试。
  • 锁大致分两类:不能睡眠的上下文用自旋锁;允许睡眠的进程上下文才考虑 mutexsemaphore。真正选择哪一种,取决于竞争双方分别是谁。

先把课程主线看懂:失败例子 1 -> 2 -> 3 -> atomic_t

这一章最重要的,不是把 API 名字背下来,而是看懂课程为什么要连续给你 3 个"失败例子"。它们不是重复,而是在一步步逼你接受一个结论:只要共享状态会被多个执行流同时碰到,就不能靠"感觉上更短"的代码自保。

阶段 代码思路 为什么失败或为什么成立 你应该得出的判断
失败例子 1 if (!valid) return -EBUSY; valid = 0; 检查 valid把 valid 改成 0 之间隔着多条语句,程序 A 可能在写回前被更高优先级程序 B 抢占,结果 A、B 都看到 valid == 1 "先判断、后修改"不是原子操作,只要中间能被抢占,就会出竞态。
失败例子 2 if (--valid) { valid++; return -EBUSY; } --valid 看起来只有一行,但机器层面仍然是"读出 -> 减 1 -> 写回"三步;A、B 完全可能都先读到旧值 1,再各自算出 0。 "代码更短"不等于"不可打断",关键要看底层是不是读改写序列。
失败例子 3 --valid 前后加 raw_local_irq_save/restore 关本地中断只能防住当前 CPU 上的中断和部分抢占,挡不住另一个 CPU 同时访问 valid;所以它不是跨 CPU 的互斥。 "本地禁止中断"只解决本地 CPU 的问题,不等于系统级互斥。
走向 atomic_t atomic_t 和原子 API 重写 valid 对单个共享变量的更新交给 ldrex/strex 这类原子指令或重试机制处理,中间若被别人改过就重来。 单变量状态更新优先考虑原子变量;复杂临界区再上锁。

读完这一段,你至少要能用一句话分别说清楚:

  • 失败例子 1 失败在"检查"和"写回"之间可能被抢占。
  • 失败例子 2 失败在"自减"本身仍然是读改写,不是天然原子。
  • 失败例子 3 失败在"只关本地中断",没有解决 SMP 上其他 CPU 的并发访问。
  • atomic_t 成立的边界是"单个共享状态的原子更新",不是"整个临界区都不用锁"。

术语小字典

  • preempt:内核抢占。进程在内核态执行时,也可能被调度器切走,让另一个更高优先级任务先跑。
  • Softirq:软件中断,是硬中断之后处理延后工作的机制;它依然属于不能睡眠的上下文。
  • Tasklet:建立在 Softirq 之上的一种更易用封装,可以把它理解为"受 Softirq 调度的下半部"。
  • Timer:内核定时器回调;从并发语义上看,它也跑在 Softirq 体系里。
  • ATPCS:ARM Procedure Call Standard,ARM 平台的函数调用约定;例如参数常放在 r0-r3,返回值常放在 r0
  • Clobbers:内联汇编的"额外副作用声明";告诉编译器这段汇编还会改哪些寄存器、标志位或内存,别做错误优化。
  • _bh:Bottom Halves 的缩写,历史上指"中断下半部";在这里你可以直接把它理解成"顺带禁止本地 Softirq"。
  • _irqsave:加锁前先保存当前中断状态,再关中断;解锁时恢复到原来的状态,而不是无脑开中断。

源码和实验怎么对应着看

先接受一个事实:仓库里和本章直接配套的源码,主要集中在 source/A7/02_同步与互斥/01_inline_assembly,它对应文档的 1.1 节。文档 1.2 之后关于失败案例、原子操作、锁类型和锁实现,主要靠 同步与互斥.docx 里的伪代码、时序图和内核源码路径来讲,并没有在仓库里再拆成独立实验目录。

建议按下面的"看什么、验证什么、为什么"来读,而不是把目录机械刷一遍:

目录 重点文件 你要看什么 你要验证什么
01_c_code main.ctest.dis 不要通读整个反汇编,只定位 <add> 这个符号附近。 真正完成加法的核心只有一条 add 指令,其余大多是在做建栈、取参数、回传结果;这就是"C 里一行,底层多步"的证据。
02_assembly main.cadd.S 对照 add.Sadd r0, r0, r1bx lr ATPCS 是怎么落地的:参数为什么进 r0/r1,返回值为什么回到 r0
03_inline_assembly main.cmain2.c 先读 main.c,再读 main2.c main.c 里把 :"=r"(sum) 当成 OutputOperands,把 :"r"(a), "r"(b) 当成 InputOperands,把 :"cc" 当成 Clobbers;再用 main2.c 看具名操作数只是写法更直观,不是原理变化。
04_earlyclobber main.c 重点盯住 =&r 仓库里保留的是修正版:输出寄存器会被提前写坏时,必须用 & 告诉编译器不要和输入复用同一寄存器;否则 ab 可能在还没用完时就被覆盖。
同步与互斥.docx 的 1.2 到 1.7 失败例子、atomic_t、锁章节 这是本章正文。 把"为什么失败""为什么 atomic_t 行""为什么不同场景要换锁"这三条线真正串起来。

推荐实验顺序可以直接照着目录走:

bash 复制代码
cd doc_and_source_for_drivers/STM32MP157/source/A7/02_同步与互斥/01_inline_assembly/01_c_code && make
cd ../02_assembly && make
cd ../03_inline_assembly && make
cd ../04_earlyclobber && make

如果本机没有 arm-linux-gnueabihf- 交叉编译环境,不必卡住。每个目录里已经有现成的反汇编或源码,直接对着源码和反汇编一起看,照样可以完成这一章最重要的学习目标。

这里再把 3 个最容易看偏的点钉死:

  • test.dis 不要从第一行开始硬看,只看 <add> 附近那十几行就够了;你不是在学 ELF 格式,而是在找"真正做加法的是哪一条指令"。
  • 03_inline_assembly/main.c 的核心不是会不会写 asm,而是能不能把 OutputOperandsInputOperandsClobbers 三段一眼拆开。
  • 04_earlyclobber/main.c 不是在教一个冷门语法点,而是在告诉你:如果不把副作用告诉编译器,编译器就可能"帮倒忙"。

另外要注意,文档里明确写了"失败例子"这一节在 GIT 上没有源码。也就是说,1.2 那些 valid 的例子、后面很多内核实现代码,重点是帮助你建立判断方法,而不是让你在仓库里把每一段都找到对应目录。

你需要真正掌握什么

锁选择最小场景表

初学者最容易卡住的不是"锁有哪些",而是"我现在到底该选哪一个"。先把课程里最常用的 3 个场景记牢:

场景 竞争双方 能不能睡眠 首选 为什么
用户态 vs 用户态 两个进程上下文都可能进入同一临界区 可以 mutexsemaphore 两边都在进程上下文,拿不到锁时可以休眠,不需要自旋。默认先用 mutex;只有需要计数资源或"一个执行流等待、另一个执行流释放"的同步语义时再考虑 semaphore
用户态 vs Softirq / Tasklet / Timer 进程上下文会和下半部共享同一资源 不可以让下半部睡眠 spin_lock_bh 你不仅要互斥,还要在进入临界区前先挡住本地 Softirq;TaskletTimer 都属于这类。
Softirq / 用户态 vs IRQ 共享资源既可能被下半部访问,也可能被硬中断访问 不可以 spin_lock_irqsave 你要同时防住"当前 CPU 被 IRQ 打断"和"其他 CPU 并发抢锁";_irqsave 还能保证解锁时恢复到进入前的中断状态。

这张表不是全量锁手册,但已经够你应付后续大多数驱动入门场景。真正落到代码前,先问 3 个问题:

  • 当前执行流能不能睡眠?
  • 竞争者是不是 Softirq / Tasklet / Timer / IRQ?
  • 我需要保护的是"单个共享变量",还是"一段包含多步逻辑的临界区"?

mutexsemaphore 怎么快速区分

课程里专门把这两个放在一起比较,原因是它们都属于"休眠型锁",很容易被初学者混成一个东西。最小差异先记这张表:

维度 semaphore mutex
count 能否大于 1 可以,本质上是计数资源 不可以,只表示"锁着/没锁"
谁加锁谁解锁 不强制要求同一执行流,适合同步语义 学习和写驱动时应按"谁 mutex_lockmutex_unlock"来用
能否用于中断上下文进入临界区 不适合,down() 可能睡眠 不适合,mutex_lock() 可能睡眠

这里有一个很重要的初学者提醒:文档里说 semaphore 的释放方可以是"别的程序、中断等",这描述的是它更适合做"等待者 / 唤醒者"同步,不等于你应该在 IRQ 里用 down() 去拿一个休眠锁。只要"获取锁"这一步可能睡眠,就不该放进中断上下文。

这一章你至少要做到的事

  • 能用自己的话说清楚:同步是"等条件",互斥是"抢临界区",两者经常一起出现,但不是同一个概念。
  • 能把"失败例子 1 -> 失败例子 2 -> 失败例子 3 -> atomic_t"这条递进链完整复述出来。
  • 能理解为什么"只用一个全局变量判断一下"在驱动里不可靠,特别是在可抢占内核和 SMP 上。
  • 能说出 raw_local_irq_save() 这类做法为什么只能解决本地 CPU 的问题,不能天然推广成跨核互斥。
  • 能理解 atomic_t 解决的是"单个共享状态的原子更新",不是"任意复杂临界区都不需要锁"。
  • 能把 01_c_code -> 02_assembly -> 03_inline_assembly -> 04_earlyclobber 这条线串起来:先看懂普通 C 会被拆成什么指令,再理解汇编函数和内联汇编,最后理解为什么内核里的原子操作和位操作必须精确描述寄存器约束。
  • 能根据上面的最小场景表做基本选型:进程上下文且允许睡眠,优先考虑 mutex / semaphore;牵涉 Softirq、Tasklet、Timer、IRQ 时,优先回到自旋锁变体。
  • 能接受一个非常重要的事实:后面驱动里所谓"并发正确",不是看代码看起来像顺序执行,而是要看它在抢占、中断、多核条件下是否仍然成立。

初学者常见误区

  • 误区 1:源码只有一行,所以操作就是原子的。实际要看机器指令,不看源码行数。
  • 误区 2:把 --valid 这种写法当成天然安全。它仍然是读、改、写三步,竞态条件一点没少。
  • 误区 3:只要关中断就万事大吉。对单核局部场景可能够用,但在 SMP 上,别的 CPU 依然能同时访问共享资源。
  • 误区 4:用了 atomic_t 就不再需要锁。原子变量适合很小的状态更新;只要临界区里有多步逻辑、多个共享对象,还是要靠锁保护整体一致性。
  • 误区 5:spin_lock_bh 只是"给 Tasklet 用的锁"。不对,_bh 真正防的是本地 Softirq;Tasklet、Timer 都只是 Softirq 家族里的具体成员。
  • 误区 6:spin_lock_irq()spin_lock_irqsave() 只是名字不同。前者是"直接关中断",后者是"先记住原状态再关中断";当代码可能在不同上下文复用时,_irqsave 更稳。
  • 误区 7:mutexsemaphore 都能睡眠,所以随便用哪个都行。实际上默认应优先用 mutex 解决互斥;semaphore 更适合计数资源和同步语义。
  • 误区 8:本章只有内联汇编,和后面的驱动关系不大。实际上内联汇编只是入口,它是在给后面的原子操作、寄存器级并发和锁实现铺路。
  • 误区 9:把"内核内存里的原子更新"和"设备寄存器的原子访问"混为一谈。后面做驱动时,atomic_t 保护的是共享内存状态;设备寄存器的 readl / writel 往往还要结合锁和访问时序来考虑。

学完后如何迁移到驱动开发

到了后面的 LCD、I2C、Input 章节,你可以把本章直接迁移成一个固定问法:谁在共享这份数据,谁可能打断谁,当前上下文能不能睡眠。只要这 3 个问题问清楚,很多驱动里的同步选择就不会凭感觉。

  • 如果目标是"设备一次只允许一个进程打开",你应该先判断这是"单个状态位"问题还是"打开到关闭之间整段生命周期"问题:前者常见于 atomic_t,后者常见于 mutex
  • 如果进程上下文和中断 / Softirq 要共享缓冲区、队列、状态位,你应该首先想到自旋锁及其变体,而不是在不能睡眠的上下文里硬上 mutex
  • 如果后面需要维护标志位、引用计数、状态计数,本章的 atomic_t 和位操作心智模型会直接复用。
  • 如果后面要做寄存器读改写,这一章给你的最大提醒是:不要把"读寄存器、改某一位、再写回"想成天然安全。只要这个寄存器可能被另一个执行上下文同时访问,就必须回到本章的判断方法,重新选择保护手段。
  • 如果以后读内核源码时看到 atomic.hbitops.hspinlock.hsemaphore.hmutex.h,你也不会只把它们看成 API 头文件,而会知道它们背后分别解决的是哪类并发问题。

一句话概括:这章真正要你掌握的,不是锁函数表,而是"先判断并发来源,再选择正确同步原语"的工作方式。后面的驱动章节换的是硬件对象和子系统,复用的却正是这套方法。


03_LCD

一句话摘要:这一章是整套课程里第一个大型真实驱动专题。它不再只讲某个局部 API,而是第一次把"硬件原理、内核框架、设备树、平台资源、应用验证、显示体验优化"连成一条完整学习链路。

章节定位

如果把前两章看成"进场"和"打地基",这一章才是第一次真正面对一个完整外设。

  • 第 1 章解决的是课程入口和资料入口。
  • 第 2 章解决的是驱动开发的工程基础:共享资源、临界区、并发意识。
  • 第 3 章开始,你要把这些基础落到一个真实设备上,第一次同时处理硬件、内核、设备树和实验现象。

所以,LCD 之所以是课程里的第一个大型真实驱动专题,不是因为"显示最重要",而是因为它足够完整,能把驱动开发最核心的方法论一次性讲透:

硬件原理 -> framebuffer 框架 -> QEMU 最小驱动 -> 结合 APP 分析 -> STM32MP157 实机移植 -> 多 framebuffer

这条主线的价值在于,它让你第一次建立稳定的心智模型:

  • 用户程序看到的是 /dev/fb0
  • 内核里承上启下的是 fbmem.c
  • 具体驱动要准备的是 fb_infofb_ops、显存和硬件初始化
  • 到实机上,驱动还必须从设备树里拿到引脚、时钟、显示时序、寄存器资源
  • 真正把屏点亮后,还要继续面对显示撕裂、多 buffer 这些"能用"和"好用"之间的问题

换句话说,这一章不是单独学 LCD,而是在学"如何把一个硬件外设变成 Linux 里的可用设备"。

核心概念

1. 先把显示链路看成数据流,不要一上来就背代码

文档 01_单片机_Linux下不同接口的LCD硬件操作原理07_硬件_8080接口LCD时序分析08_硬件_TFT-RGB接口LCD时序分析09_硬件_STM32MP157的LCD控制器 解决的是同一个问题:像素数据到底怎么到屏上去。

这一层要先想清楚 5 个对象:

  • 像素:一个点的颜色
  • bpp:每个像素用多少位表示
  • framebuffer:一块连续内存,保存一整帧或多帧像素数据
  • LCD 控制器:从 framebuffer 取数据,按时序送给屏
  • LCD 接口和时序:决定什么时候送数据、送多少数据、信号极性是什么

只有先知道这些概念,后面代码里的 xresyresbits_per_pixelline_lengthpixel clockhsync/vsync 才不是"魔法数字"。

2. framebuffer 是本章的软件主线

本章的软件主线可以先记成:

APP -> /dev/fb0 -> fbmem.c -> fb_info/fb_ops -> screen_base -> LCD 控制器 -> LCD

这里最关键的是 fb_info。它不是一个普通结构体,而是 framebuffer 驱动和上层应用之间的契约。

  • var 表示可变显示参数:分辨率、颜色格式、虚拟分辨率等
  • fix 表示相对固定的信息:显存物理地址、显存总大小、每行字节数等
  • screen_base 是内核可访问的显存虚拟地址
  • fbops 描述驱动愿意提供哪些操作

初学者最容易混淆的是:var 更像"这一帧该怎么解释",fix 更像"这块显存和硬件资源长什么样"。

3. QEMU 阶段学的是框架,不是偷懒

04_最简单的LCD驱动_基于QEMU05_上机实验_基于QEMU 的设计非常关键。它不是为了绕开硬件,而是为了先把问题拆开。

真实 SoC 上,点亮 LCD 往往要同时处理:

  • 引脚复用
  • 时钟树
  • 控制器寄存器
  • 显存分配
  • 屏参和时序

这些因素一次性压上来,初学者很容易分不清到底是框架错了,还是硬件没配对。QEMU 的作用,就是把硬件复杂度压到最低,让你先看清 framebuffer 驱动的骨架。

4. 设备树不是"顺手写一下",而是实机驱动的输入

06_lcd_drv_framework_use_devicetree 开始,章节重点发生变化:驱动不再只靠硬编码参数运行,而是通过设备树拿平台资源。

这一层要理解的是:

  • compatible 决定驱动和设备节点怎么匹配
  • pinctrl-* 决定 LCD 引脚复用
  • backlight-gpios 决定背光 GPIO
  • clocksclock-names 决定像素时钟来源
  • displaydisplay-timings 决定分辨率、时序、极性等显示参数
  • reg 决定控制器寄存器的物理地址范围

也就是说,设备树不是"配置文件",而是平台资源到驱动入口之间的桥。

5. LTDC 是 STM32MP157 这一章真正的硬件难点

09_硬件_STM32MP157的LCD控制器10_分析内核自带的LCD驱动程序_基于STM32MP15715_编程_配置LCD控制器之寄存器操作_基于STM32MP157 共同回答一个问题:拿到屏参之后,怎么把它们写进 LTDC。

这一层要抓住两件事:

  • 时序参数最后都要落到 LTDC 寄存器里,比如同步宽度、前后肩、有效显示区域、总周期
  • framebuffer 的地址和像素格式也要写到 LTDC 图层寄存器里,比如显存基址、每行长度、像素格式、图层窗口范围

所以,实机阶段的本质,不是"再写一个 framebuffer 驱动",而是把通用的 framebuffer 框架,接到 STM32MP157 这颗芯片的 LTDC 控制器上。

6. 多 framebuffer 说明"点亮"不是终点

17_单Buffer的缺点与改进方法18_STM32MP157内核自带的LCD驱动不支持多buffer 把章节又往前推进了一步:显示不是只要有 /dev/fb0 就结束了。

单 buffer 的问题,本质上是"LCD 控制器正在读这一帧,APP 也正在改这一帧"。结果就是:

  • 慢速绘制时,用户能看到逐步画出来的过程
  • 高速整屏刷新时,容易出现撕裂和闪烁

多 buffer 要解决的就是这个问题:显示一帧、准备下一帧、再切换。这里既涉及驱动是否提供足够显存和 pan display 能力,也涉及应用如何正确使用 yres_virtualyoffsetFBIOPAN_DISPLAY

源码和实验怎么对应着看

不要把本章按目录平均用力。最有效的方式,是顺着主线看"每一步到底新增了什么"。

建议顺序

  1. 先看硬件文档,建立像素、时序、控制器的物理直觉。
  2. 再看 framebuffer 框架和最小骨架,搞懂 Linux 这边的抽象。
  3. 在 QEMU 上把最小驱动跑通,再结合 APP 看 /dev/fb0 的使用链路。
  4. 然后进入 STM32MP157,按"框架 -> 引脚 -> 时钟 -> 屏参 -> LTDC 寄存器 -> 实机跑通"的顺序读源码。
  5. 最后再看多 framebuffer,这时你才能分清它是"体验优化"和"驱动能力边界"。

第一段:用文档建立硬件直觉

先看什么 为什么先看 看完后你应该知道什么
01_单片机_Linux下不同接口的LCD硬件操作原理 先搞清像素、颜色格式、framebuffer 是什么 为什么显存大小和 xres * yres * bpp 有关
07_硬件_8080接口LCD时序分析 了解 MCU 风格 LCD 的命令/数据时序 不是所有 LCD 都是"直接吐像素流"
08_硬件_TFT-RGB接口LCD时序分析 这是本章实机路线最直接相关的接口 为什么要关心 hsync/vsync/de/pclk
09_硬件_STM32MP157的LCD控制器 先认识 LTDC 再看寄存器代码 LTDC 到底帮 CPU 做了什么

这一段不用停太久,但不能跳过。跳过之后,后面所有显示参数都会变成死记硬背。

第二段:先看 framebuffer 最小骨架

01_fb_info 是整章最应该"抄一遍、改一遍、背后的逻辑讲一遍"的目录。

源码目录 这一步新增了什么 真正的学习点
01_fb_info 只做 3 件事:framebuffer_alloc、设置 fb_inforegister_framebuffer 一个 framebuffer 驱动最小需要哪些软件对象

01_fb_info/lcd_drv.c 时,重点不是每一行都记住,而是抓住这几个动作:

  • 分配 fb_info
  • varfix
  • 分配显存,得到 screen_basesmem_start
  • 设置 fbops
  • 注册 framebuffer

如果这一层没看懂,后面你会把所有"硬件相关代码"误以为是驱动的主体。

第三段:QEMU 阶段只解决"最小可运行"

源码目录 这一步新增了什么 你要观察什么
02_lcd_drv_qemu 在最小骨架之上,增加 QEMU 虚拟 LCD 寄存器映射和写寄存器操作 framebuffer 物理地址、分辨率、bpp 是怎么告诉硬件的
03_lcd_drv_qemu_ok 补齐 fix.idline_lengthxres_virtual/yres_virtualpseudo_palettefb_setcolreg 为什么"能点亮"还不等于"能被通用 fb 应用正确使用"

这两个目录一定要连着读。02_lcd_drv_qemu 教你"最小硬件初始化",03_lcd_drv_qemu_ok 教你"面向 APP 使用的完整度"。

这里强烈建议结合 06_结合APP分析LCD驱动程序 一起看。因为它把 openioctl(FBIOGET_VSCREENINFO)ioctl(FBIOGET_FSCREENINFO)mmap 这些调用一路追到了 fbmem.c,这一步会帮你彻底搞明白:

  • 为什么 APP 不是直接操作硬件寄存器
  • 为什么 fb_info->varfb_info->fix 要提前填好
  • 为什么 mmap 能直接映射 framebuffer

如果只看驱动、不看 APP,这一章的软件主线会少一半。

第四段:STM32MP157 的关键不是重写,而是逐步移植

进入实机阶段后,最好的办法不是直接盯着 11_lcd_drv_stm32mp157_ok,而是按增量阅读 0611

源码目录 这一步比上一版多了什么 你真正该抓住的点
06_lcd_drv_framework_use_devicetree 从简单模块入口改成 platform_driver + of_match + probe/remove 实机驱动的入口不再是"主动加载即运行",而是"匹配设备节点后再初始化"
07_lcd_drv_pin_config_use_devicetree 从设备树获取 backlight-gpios,用 GPIO 子系统打开背光 引脚复用和 GPIO 控制开始进入驱动主线
08_lcd_drv_clk_config_use_devicetree devm_clk_getclk_set_rateclk_prepare_enable 像素时钟必须匹配屏的要求,时钟是 LCD 成败的核心因素之一
09_lcd_drv_lcdcontroller_config_use_devicetree 解析 display 节点、bits-per-pixelbus-widthdisplay-timings 显示参数不该硬编码在 C 里,而应该由设备树输入
10_lcd_drv_lcdcontroller_reg_config_use_devicetre 引入 LTDC 寄存器结构体和 lcd_controller_init,把时序和图层信息真正写入控制器 这一版才是"从 framebuffer 框架走到 STM32MP157 硬件"的关键跃迁
11_lcd_drv_stm32mp157_ok 做成最终可运行版本,修正显存分配设备归属,按设备树时序设置分辨率和时钟,并配合 DTS 禁用原有面板链路冲突 实机移植从来不是只改 C 文件,设备树冲突、内核自带驱动冲突也要一起处理

这里有两个非常重要的阅读策略。

第一,不要把 0611 当成 6 个平级示例,它们本质上是一次连续移植过程。

第二,不要只读 lcd_drv.c,要把对应 DTS 一起看:

  • 07_lcd_drv_pin_config_use_devicetree/stm32mp157c-100ask-512d-lcd-v1.dts
  • 08_lcd_drv_clk_config_use_devicetree/stm32mp157c-100ask-512d-lcd-v1.dts
  • 09_lcd_drv_lcdcontroller_config_use_devicetree/stm32mp157c-100ask-512d-lcd-v1.dts
  • 10_lcd_drv_lcdcontroller_reg_config_use_devicetre/stm32mp157c-100ask-512d-lcd-v1.dts
  • 11_lcd_drv_stm32mp157_ok/stm32mp157c-100ask-512d-lcd-v1.dts

把 DTS 和驱动对照着读,你才能真正看懂一条完整链路:

  • compatible 怎么触发 probe
  • display 怎么把时序交给驱动
  • clocks 怎么交给时钟子系统
  • reg 怎么变成 LTDC 寄存器映射
  • backlight-gpios 怎么变成背光控制

其中 11_lcd_drv_stm32mp157_ok 的 DTS 还特意把原有 panelpanel_backlight 置为 disabled。这件事非常重要,因为它说明实机移植时,你经常不是"从零开始接管硬件",而是在和板子原有显示链路做资源隔离。

第五段:多 framebuffer 要和应用一起看

多 framebuffer 这一段,如果只看理论,不容易真的吃透。建议把文档和两个应用目录绑在一起看。

资料/目录 作用 你要看什么
17_单Buffer的缺点与改进方法 解释为什么单 buffer 会撕裂 明白问题根源是"显示和写入在抢同一帧"
13_multi_framebuffer_example 一个更完整的应用侧多 buffer 示例 看它怎么根据 smem_len 算可用帧数,怎么用 FBIOPAN_DISPLAY 切换
14_use_multi_framebuffer 一个精简测试程序 yres_virtualyoffsetFBIOPUT_VSCREENINFOFBIOPAN_DISPLAY 之间的最小关系
18_STM32MP157内核自带的LCD驱动不支持多buffer 给出平台边界 明白"理论支持"和"当前驱动支持"不是一回事

14_use_multi_framebuffer/multi_framebuffer_test.c 非常值得反复看,因为它把多 buffer 的应用侧逻辑压缩到了最小:

  • 先通过 FBIOGET_FSCREENINFOFBIOGET_VSCREENINFO 得到显存总大小和单帧大小
  • 再根据 fix.smem_len / screen_size 判断有几个 buffer
  • 再设置 var.yres_virtual
  • 最后通过修改 var.yoffset 并调用 FBIOPAN_DISPLAY 来切换显示帧

这段代码会逼着你重新理解 fixvar 的边界,也会让你明白为什么驱动如果不支持 pan display,APP 再努力也做不出真正的多 buffer。

你需要真正掌握什么

学完这一章,不是会背目录名,而是要真正掌握下面这些能力。

  • 能从头讲清楚这条链路:像素数据先写入 framebuffer,LCD 控制器按时序从显存取数据,再通过接口送到面板,Linux 用 /dev/fb0 把这条链路暴露给应用。
  • 能分清 fb_info 里哪些信息属于显示参数,哪些信息属于显存和硬件资源,尤其是 varfixscreen_basesmem_startline_length
  • 能解释为什么课程先用 QEMU,再上 STM32MP157:前者是拆开学习框架,后者是补齐平台资源和真实硬件。
  • 能看懂设备树里哪些字段会进入驱动:compatibleregclockspinctrl-*backlight-gpiosdisplay/display-timings
  • 能把 display_timing 里的分辨率、同步宽度、前后肩、极性,和 LTDC 的时序寄存器一一对应起来。
  • 能知道一个 LCD 驱动的排错顺序:先确认驱动是否匹配、/dev/fb0 是否存在、APP 能否取到 var/fix,再查背光、引脚、时钟、时序、显存地址、控制器寄存器。
  • 能明白多 framebuffer 不是"高级技巧",而是显示体验优化;它依赖驱动、显存布局和应用三者配合。

如果你现在还只是"会抄一个能亮屏的例子",还不算真正掌握。真正掌握的标志,是你能说清每一步为什么要这样做,以及换一块屏、换一颗 SoC 之后哪些地方必须重新确认。

初学者常见误区

  • 误区一:一上来就盯寄存器。

    正确做法是先看硬件链路,再看 framebuffer 框架,再看具体控制器寄存器。否则你很容易把"平台细节"误以为"驱动主线"。

  • 误区二:觉得 fb_info 只是模板代码。

    实际上它就是上层 APP 和底层驱动之间的契约。fix.line_lengthfix.smem_lenvar.xres_virtual 这些字段一旦理解不准,APP 侧就会出问题。

  • 误区三:跳过 03_lcd_drv_qemu_ok,直接看 STM32MP157 最终版。

    这样会把平台差异和驱动通用逻辑混在一起,最后什么都不稳。最该吃透的是"相邻版本之间新增了什么"。

  • 误区四:把设备树当成抄模板。
    display-timings、时钟、GPIO、寄存器资源都必须和板子、屏幕、SoC 对得上。设备树写错,不是"配置不优雅",而是硬件根本不会按预期工作。

  • 误区五:看到屏亮了就认为驱动完成了。

    亮屏只说明最小链路可能通了,不代表分辨率、颜色格式、行长度、用户态访问、撕裂控制都正确。

  • 误区六:以为多 buffer 只是应用层小技巧。

    没有足够显存、没有 yres_virtual、没有 FBIOPAN_DISPLAY 支持,应用层根本做不成真正的双 buffer。

  • 误区七:混淆课程自写的 framebuffer 驱动和内核自带的 DRM/LTDC 驱动。

    课程刻意用 framebuffer 这条线讲原理,是为了降低学习门槛;但在真实产品里,显示系统常常走 DRM/KMS,这一点要在脑子里分开。

学完后如何迁移到驱动开发

这一章最值得迁移的,不是 LCD 本身,而是它提供的驱动分析框架。

以后你看任何一个外设驱动,都可以先套这 6 个问题:

  1. 这个硬件的数据是怎么流动的,真正和 CPU 交互的是什么寄存器或总线?
  2. Linux 为这类设备提供了什么框架或子系统?
  3. 驱动和 APP 之间的接口是什么?
  4. 平台资源从哪里来,哪些放在设备树里?
  5. probe 里必须拿到哪些资源,初始化顺序是什么?
  6. 除了"能工作",还有哪些体验或性能问题要继续处理?

把这个方法迁到后面几章会非常自然。

  • 04_I2C 时,你会把 framebuffer 主线换成 adapter/client/transfer 主线,但"框架 + 设备树 + 实验 + 实机验证"的套路是一样的。
  • 05_Input 时,你会把 /dev/fb0 换成输入事件设备,但"应用如何感知、内核如何抽象、驱动如何注册"的思路仍然一样。
  • 如果以后继续走显示方向,这一章就是你进入 DRM/KMS、panel、backlight、regulator、vsync、page flip 的前置基础。

从第一性原理看,这一章真正教会你的,是把一个"板子上的硬件功能"拆成四层去理解:

  • 硬件层:屏和控制器到底怎么工作
  • 框架层:Linux 用什么抽象来管理它
  • 平台层:设备树、引脚、时钟、寄存器资源怎么交给驱动
  • 验证层:应用怎么证明驱动真的可用

如果你能把这四层拆开,再把它们重新串起来,你就已经开始具备迁移到其他驱动开发的能力了。


04_I2C 学习总结

TL;DR:这一章真正要学会的,不是"会调一个 EEPROM",而是建立一条稳定主线:协议认知 -> 用户态直接访问 -> i2c-dev/通用模型 -> i2c_driver/i2c_client -> adapter 驱动 -> GPIO 模拟 I2C -> STM32MP157 具体平台。后面的 Input、传感器、触摸屏驱动,很多都只是把"如何通过 I2C 和芯片通信"嫁接到别的子系统上。

章节定位

04_I2C 是前 5 章里第一次把"总线子系统"讲完整的章节。

03_LCD 里,你更多是在理解某个具体外设怎么挂到设备树、怎么被驱动;到了这一章,课程开始把视角抬高,回答一个更关键的问题:Linux 为什么能用一套统一框架去管理很多不同的 I2C 设备。

这一章的课程位置可以这样理解:

  • 往前承接 03_LCD:你已经接触过设备树、字符设备、平台驱动,I2C 章把这些零散知识串成"总线 + 设备 + 驱动"的整体模型。
  • 往后支撑 05_Input:很多输入设备,尤其是触摸屏、距离/光照传感器,底层通信就是 I2C。后面再学 Input,你会发现那是在 I2C 通信之上再加一层"事件上报"。
  • 对后续传感器驱动同样重要:AP3216C 这种传感器,当前章节只是先用字符设备把通信链路讲清楚;实际项目里,它往往还会继续接入 Input、IIO、hwmon 等子系统。

所以,这一章的核心不是"某颗芯片怎么读",而是:

  • I2C 协议在硬件上怎么工作
  • Linux 用哪些对象描述 I2C 总线和设备
  • 用户态、通用驱动、具体芯片驱动、adapter 驱动之间是怎么接起来的

核心概念

先抓住整章学习主线

建议你严格按下面这条线学,不要一上来就扎进某个具体芯片驱动:

  1. 协议认知
    先看 02_I2C协议.md03_SMBus协议.md,弄清楚 SCL/SDAStart/StopACK/NACKRepeated Start。否则后面看到 i2c_msgi2c_smbus_read_word_data 时,只会背 API,不知道总线上究竟发了什么。
  2. 用户态直接访问
    先用 i2c-tools01_at24c02_test 证明总线是通的、地址是对的、读写格式没问题。先把"硬件通不通"和"驱动写没写对"拆开,是初学者最划算的做法。
  3. i2c-dev/通用模型
    再看 07_通用驱动i2c-dev分析.md,理解 /dev/i2c-X 为什么存在,为什么用户态 ioctl(I2C_SMBUS) 最后还是会落到 I2C Core。
  4. i2c_driver/i2c_client
    这一步开始真正进入 Linux 驱动模型。你要明白驱动模板怎么注册,设备实例怎么出现,probe 为什么会触发。
  5. adapter 驱动
    再回头看总线底层。adapter 不是某个从设备驱动,而是"谁来把 I2C 时序真正发出去"。
  6. GPIO 模拟 I2C
    这是把 adapter 本质讲透的一步。只要你能按协议拉出波形、实现 master_xfer,Linux 就承认这是一条 I2C 总线。
  7. 具体芯片平台
    最后再回头看 STM32MP157 的真实 I2C 控制器驱动。此时你会发现,SoC 专用驱动和虚拟 adapter、GPIO adapter 的区别,不在框架,而在 master_xfer 背后换成了寄存器、中断和 DMA。

这几个关键词在学习中的位置

关键词 它是什么 在这一章里的位置
adapter 一条 I2C 总线的控制者,通常对应 SoC 的 I2C 控制器或软件模拟总线 它回答"这条总线怎么发时序、怎么完成传输"
client 总线上的一个设备实例,至少要知道地址、挂在哪条总线上 它回答"总线上现在到底有谁"
driver 某类 I2C 设备的驱动模板 它回答"如果出现这种设备,内核该怎么初始化和访问它"
message struct i2c_msg,一次传输描述单元,包含地址、方向、长度、缓冲区 它是"协议动作"在 Linux 里的数据表示;读 EEPROM 往往要用两个 message
i2c-tools 用户态工具集,也是很好的示例代码来源 它是最适合初学者的"观察窗口",先验证总线和设备,再写驱动

如果只记一句话,请记住这句:

无论你是用 i2c-tools、自己写 APP,还是写一个具体的 i2c_driver,最后都会落到 i2c_transferi2c_smbus_xfer,再由某个 i2c_adaptermaster_xfer 去真正完成传输。

adapterclientdrivermessage 是怎么串起来的

可以先把链路记成:

用户程序/i2c-tools -> i2c-dev 或具体芯片驱动 -> I2C Core -> i2c_adapter -> i2c_msg[] -> i2c_client 对应的设备

这条链里最容易混淆的是下面 3 点:

  • adapter 关心的是"怎么发",不关心 AP3216C、AT24C02 这些设备的寄存器语义。
  • client 是设备实例,不是驱动;一个 driver 可以匹配多个 client
  • message 不是"一个寄存器",而是"一次传输描述"。例如 AT24C02 读数据,往往先发一个写 message 送内部地址,再发一个读 message 取数据,中间依赖 Repeated Start

源码和实验怎么对应着看

这一章最好的学习方式,不是"把所有代码从头到尾看完",而是让每个源码目录只承担一个明确任务。

目录 它在主线里的角色 重点看什么 你要验证什么
01_at24c02_test 用户态直接访问 I2C 设备 at24c02_test.copen_i2c_devset_slave_addri2c_smbus_write_byte_datai2c_smbus_read_i2c_block_data;配套的 smbus.ci2cbusses.c 来自 i2c-tools 用户态也能通过 i2c-dev 直接访问 EEPROM;写 EEPROM 要考虑写周期,所以代码里有 nanosleep(20ms)
02_i2c_driver_example 最小 i2c_driver 骨架 i2c_driver_example.c 里的 of_match_tableid_tableprobe_newi2c_add_driver 先理解"驱动模板长什么样",不要一开始就被具体芯片细节分散注意力
03_ap3216c 把骨架替换成真实传感器访问 a3216c_drv.cap3216c_openap3216c_read,以及对 i2c_smbus_read_word_data 的调用 设备驱动真正关心的是芯片寄存器语义;同时也能看到:只写 driver 壳子还不够,匹配表和 client 生成还没补完整
04_ap3216c_ok i2c_driver + i2c_client + 测试 这条链补齐 ap3216c_drv.cap3216c_client.cap3216c_drv_test.cstm32mp157c-100ask-512d-lcd-v1.dts client 既可以通过代码 i2c_get_adapter + i2c_new_device 创建,也可以由设备树 ap3216c@1e 节点生成;匹配成功后才会进入 probe,再导出 /dev/ap3216c
05_i2c_adapter_framework adapter 驱动的最小框架 i2c_adapter_drv.c 里的 i2c_bus_virtual_master_xferi2c_bus_virtual_algoi2c_add_adapter,以及 i2c_adapter.dts adapter 驱动本质上是在注册一条新的 I2C bus,核心不是设备寄存器,而是 master_xfer
06_i2c_adapter_virtual_ok 用虚拟 adapter 模拟 EEPROM,把上层链路跑通 i2c_adapter_drv.c 里的 eeprom_emulate_xferi2c_bus_virtual_master_xfer 只要 adapter 正确处理 i2c_msg[],上层 i2c-tools、APP、设备驱动都不用改;这最能说明 Linux I2C 框架的分层价值
07_i2c_gpio_dts_stm32mp157 GPIO 模拟 I2C 和 STM32MP157 板级落地 stm32mp157c-100ask-512d-lcd-v1.dts 里的 i2c_gpio_100ask 节点,以及 &i2c1 下的 ap3216c@1e 软件 bit-bang 也能成为正规 adapter;板级 DTS 既要描述设备,也要描述这条总线怎么被创建

建议这样配对资料和实验

  1. 先读 02_I2C协议.md03_SMBus协议.md04_I2C系统的重要结构体.md
    目标:先把协议和 adapter/client/message 的抽象对上。
  2. 再读 05_无需编写驱动直接访问设备_I2C-Tools介绍.md,并在板子上先执行:
    • i2cdetect -l
    • i2cdetect -F 0
    • i2cdetect -y -a 0
      目标:先确认系统里有哪些 bus、这些 bus 支持哪些功能、设备地址是否真的存在。
  3. 再看 01_at24c02_test
    目标:把 i2c-tools 的调用方式变成你自己的程序,感受"APP 直接访问 I2C 设备"是什么体验。
  4. 再读 07_通用驱动i2c-dev分析.md
    目标:把刚才用户态看到的现象,和 i2c-devopen/ioctl 路径串起来。
  5. 再看 08_I2C系统驱动程序模型.md09_编写设备驱动之i2c_driver.md,配合 02_i2c_driver_example03_ap3216c
    目标:明白 i2c_driver 是模板,probe 成功的前提是系统里先出现了匹配的 client
  6. 再看 10_编写设备驱动之i2c_client.md,配合 04_ap3216c_ok
    目标:把"设备怎么出现"这件事彻底弄清楚,包括设备树、代码和用户态 new_device 三种思路。
  7. 最后看 1115 这几份资料,配合 05_i2c_adapter_framework06_i2c_adapter_virtual_ok07_i2c_gpio_dts_stm32mp157
    目标:把"设备驱动"和"总线驱动"彻底区分开,再回头读 STM32MP157 的 i2c-stm32f7.c 时就不会是黑盒。

这一章最值得你亲手观察的现象

  • 01_at24c02_test 里,为什么写每个字节后都要延时。
  • 04_ap3216c_ok/ap3216c_client.c 里,为什么 i2c_get_adapter(0) 能创建一个地址为 0x1eclient
  • 04_ap3216c_ok/stm32mp157c-100ask-512d-lcd-v1.dts 里,为什么把 ap3216c@1e 挂到 &i2c1 下就能触发 probe
  • 05_i2c_adapter_framework 里,为什么只要注册 i2c_adapter,系统里就会多出一条 bus。
  • 06_i2c_adapter_virtual_ok 里,为什么上层工具完全不知道"下面其实是个假 EEPROM"。
  • 07_i2c_gpio_dts_stm32mp157 里,为什么加载 i2c-gpio 后,i2cdetect -l 会多出一条 i2c-3

你需要真正掌握什么

这一章真正的验收标准,不是"我记住了几个 API 名字",而是你是否具备下面这些能力:

  • 能独立讲清楚一条完整链路:用户程序 为什么能通过 i2c-dev 访问设备,i2c_driver 为什么会被匹配,底层又为什么一定要靠 adapter->master_xfer
  • 能区分 4 类问题:
    • 协议层问题:地址、读写方向、Repeated Start、ACK 是否正确。
    • bus 层问题:这条 I2C bus 有没有被注册出来,i2cdetect -l 能不能看到。
    • client 层问题:设备有没有被实例化出来,设备树/new_device/代码创建是否正确。
    • driver 层问题:compatibleid_tableprobe、寄存器读写流程是否正确。
  • 能看懂 i2c_msg[] 在表达什么,知道"读 EEPROM 常常需要两个 message"并不是技巧,而是协议要求。
  • 能把 i2c-tools 放在正确位置上:它是最好的验证工具和示例代码来源,但它不是最终产品驱动。
  • 能理解 04_ap3216c_ok 的教学用意:这里故意先用字符设备把焦点放在 I2C 通信本身,而不是一上来就引入更复杂的传感器子系统。
  • 能把 STM32MP157 的板级事实记牢:设备树里的 i2c1 在本课程资料里对应的是 I2C BUS0 ,不要把节点名和 /dev/i2c-X 机械地一一对应。

如果你能独立回答下面这个问题,就说明这章真的学进去了:

某个 I2C 设备为什么会出现在某条总线上,它为什么会匹配到某个驱动,而你从用户态发起的一次读写为什么最终一定会落到某个 adapter 的传输函数。

初学者常见误区

  • 误区 1:把 adapter 当成某颗芯片的设备驱动。
    纠正:adapter 是总线控制者,负责"怎么发时序";AP3216C、AT24C02 这些才是总线上的设备。
  • 误区 2:把 clientdriver 当成一回事。
    纠正:client 是设备实例,driver 是驱动模板;一个 driver 可以匹配多个 client
  • 误区 3:觉得 message 就等于"读一个寄存器"。
    纠正:message 是传输描述单元,一次完整读操作可能要用多个 message 才能完成。
  • 误区 4:一上来就写驱动,不先用 i2c-tools 或用户态程序验证。
    纠正:先排除总线、地址、接线、功能位问题,再写驱动,调试效率会高很多。
  • 误区 5:以为写了 i2c_driver 就一定会进 probe
    纠正:系统里还必须先出现匹配的 i2c_client,它可能来自设备树、代码,也可能来自 /sys/bus/i2c/devices/.../new_device
  • 误区 6:把设备树节点名 i2c1 直接等同于 /dev/i2c-1
    纠正:在这套 STM32MP157 资料里,i2c1 对应的是 I2C BUS0,总线编号是内核注册后的逻辑编号。
  • 误区 7:看到 i2cdetect 里的 UU 就以为硬件坏了。
    纠正:UU 很多时候表示这个地址已经被某个内核驱动占用了,不是设备损坏。
  • 误区 8:觉得 GPIO 模拟 I2C 是"旁门左道"。
    纠正:它仍然是标准的 adapter 驱动,只是把专用控制器换成了软件 bit-bang。
  • 误区 9:忽略 EEPROM 写周期。
    纠正:01_at24c02_test 里每次写完都 nanosleep(20ms),不是多余,而是在等待芯片完成内部写入。

学完后如何迁移到驱动开发

这一章和后续 Input、传感器、触摸屏驱动之间的关系,可以用一句话概括:

I2C 章解决"怎么跟芯片通信",后续子系统章节解决"通信得到的数据该怎样以 Linux 认可的方式暴露出去"。

和 Input、传感器、触摸屏的关系

  • AP3216C 本身就是传感器例子。
    课程里先把它做成字符设备,是为了让你先专注在 i2c_clienti2c_driver、寄存器读写这条链,而不是被更复杂的上层框架分散注意力。
  • 05_Input 时,你会看到另一层抽象。
    触摸屏驱动通常是:i2c_driver.probe 里拿到 client,申请中断,分配 input_dev,中断里通过 I2C 读坐标,再上报 ABS_X/ABS_Y/BTN_TOUCH 这类事件。
  • 这意味着:
    • I2C 负责把数据从芯片读出来。
    • Input 负责把这些数据变成应用层可消费的输入事件。
  • 很多真实项目驱动其实都是"总线子系统 + 功能子系统"的组合:
    • 触摸屏:I2C + Input
    • 环境传感器:I2C + IIO/hwmon
    • 简单控制芯片:I2C + misc/char device

真正进入驱动开发时,建议按这个顺序落地

  1. 先用 i2cdetecti2cgeti2cset 或自己的测试程序确认硬件链路真的通。
  2. 再确认设备实例化方式。
    先决定是放到设备树里,还是调试期用 new_device,还是像 ap3216c_client.c 一样在代码里创建。
  3. 再写 i2c_driver 骨架。
    先把 compatibleid_tableprobe/remove 跑通,再填芯片寄存器逻辑。
  4. 把寄存器访问封成少量 helper。
    例如 read_regwrite_regread_block,不要把 i2c_smbus_* 调用散落在整个驱动里。
  5. 最后再接入上层子系统。
    如果设备最终是给应用读事件,就接 Input;如果是周期采样传感器,就考虑 IIO 或 hwmon;如果只是课程验证,字符设备也可以。
  6. 只有在 bus 本身有问题时,才向下钻 adapter 层。
    这时就回来看 05_i2c_adapter_framework06_i2c_adapter_virtual_ok07_i2c_gpio_dts_stm32mp157,以及真实平台驱动 i2c-stm32f7.c

对初学者来说,这一章最有价值的迁移能力其实是:以后你再遇到任何 I2C 传感器、I2C 触摸屏、I2C 电源管理芯片,都不会再把问题混成一团,而是能自然地分成"协议格式""bus 是否存在""client 是否生成""driver 是否匹配""上层子系统如何接入"这几步去排查和实现。


05_Input 学习总结

TL;DR:这一章真正要学会的,不是"怎么直接读某块按键或触摸屏硬件",而是把 GPIO 按键、QEMU 触摸屏、I2C 电容屏都看成"输入事件源",再通过 Input 子系统统一地暴露给 /dev/input/eventXtslibuinput04_I2C 解决的是"怎么把数据从芯片里读出来",05_Input 解决的是"读出来的数据怎样变成标准输入事件"。

章节定位

05_Input 是前 5 章里第一次把"功能子系统抽象"讲完整的章节。

04_I2C 里,你已经学过怎样通过总线把寄存器数据读出来;到了这一章,课程把视角再抬高一层,回答的是另一个更关键的问题:Linux 为什么不让应用直接面对某个具体硬件,而是把它们统一抽象成输入事件。

这章和前后章节的关系可以这样看:

  • 往前承接 02_同步与互斥:Input 驱动里同样有中断、定时器、等待队列、异步通知,前一章的并发判断会直接复用。
  • 往前承接 04_I2C:真实电容屏控制器往往挂在 I2C 上,I2C 章负责"怎么通信",Input 章负责"怎样把通信结果上报成标准事件"。
  • 同时复用 GPIO 和中断知识:GPIO 按键、QEMU 触摸屏、真实触摸芯片,底层触发方式不同,但最终都要上报 EV_KEYEV_ABS 这一类标准事件。
  • 往后给触摸屏、按键、传感器类驱动打基础:你以后再看 gpio_keys.cgoodix.c,就不会只看到"又一个平台驱动",而会知道它们是在往 Input Core 里注册一个 input_dev

为什么 Input 子系统要抽象"输入事件",而不是让应用直接操作硬件

如果没有 Input 子系统,每种输入设备都可能有一套私有 API:

  • GPIO 按键可能自己定义"按下/松开"的字符设备格式
  • 电阻屏可能直接返回 ADC 值
  • 电容屏可能直接暴露 I2C 寄存器布局
  • USB 鼠标、键盘又会是另一套协议

这样做的结果是:驱动无法复用,应用也必须理解每种硬件细节。

Input 子系统做的事情正好相反:它把"硬件差异"压在驱动内部,把"用户真正关心的事情"抽出来,统一成 type + code + value 形式的输入事件。比如:

  • 按键最终都是 EV_KEY
  • 鼠标移动最终都是 EV_REL
  • 触摸坐标最终都是 EV_ABS
  • 一批事件上报完成后用 SYN_REPORT 收尾

这样一来:

  • 驱动开发者只要负责"从硬件读数据 -> 翻译成标准事件"
  • 应用开发者只要面向 /dev/input/eventXtsliblibinput 这类统一接口
  • evdev 这类 handler 还能把同一份事件同时分发给多个应用

所以,这一章的核心不是"某块硬件怎么操作",而是建立一条稳定的数据链路:

硬件中断/轮询 -> 设备驱动读取数据 -> input_event/input_sync -> Input Core -> evdev 等 handler -> /dev/input/eventX -> APP/tslib

学习顺序建议

不要一上来就看 goodix.c 或 QEMU 触摸屏驱动。对初学者来说,最省力的路径就是按"先看事件消费者,再看事件生产者"来学。

  1. 先学应用层事件怎么看
    先看 02_先学习输入系统应用编程.md03_输入系统应用编程.docx,再对照 source/A7/05_Input/01_input_app
    这一阶段只解决 3 件事:/dev/input/eventX 是什么、struct input_event 长什么样、应用到底怎么读取事件。
  2. 再学 Input Core 的对象关系
    接着看 01_Input子系统视频介绍.mdDRV_01_Input子系统框架详解.md
    这一阶段要把 input_devinput_handlerinput_handleevdev 的角色彻底分开。
  3. 再学最小 input_dev 驱动骨架
    然后看 DRV_02_编写input_dev驱动框架.md,配合 source/A7/05_Input/02_input_dev_framework
    重点不是立刻跑通,而是知道一个 Input 驱动最少要做哪几步:分配 input_dev、声明支持哪些事件、注册设备、处理中断并上报事件。
  4. 再看具体案例,把抽象落地
    先看 DRV_03_编写最简单的触摸屏驱动程序_基于QEMU.md03_touchscreen_qemu,再看 DRV_05_GPIO按键驱动分析与使用.mdDRV_06_I2C接口触摸屏驱动分析.md
    这一步的重点是:虽然底层一个是 GPIO,一个是 QEMU 虚拟硬件,一个是 I2C 触摸芯片,但上报给 Input Core 的仍然是标准事件。
  5. 最后再学 uinput
    最后看 DRV_07_UInput分析_用户态创建input_dev.md04_uinput
    只有你先理解"内核驱动怎样创建输入设备",再回头看"用户态怎样伪造一个输入设备",uinput 才不会像魔法。

如果只给自己保留一条强制主线,请记住:

应用层事件 -> Input 子系统抽象 -> input_dev 驱动框架 -> 触摸屏/按键案例 -> uinput

Input 子系统的核心对象

先把最小数据流记成一句话:

input_dev 负责描述"我是什么输入设备";

驱动通过 input_event/input_sync 上报数据;
input_handler 负责接住这些数据并决定怎么往上层分发;
evdev 是最常见的通用 handler;

应用通过 /dev/input/eventXtslib 读事件;
uinput 则反过来让用户态伪造一个新的 input_dev

这几个关键词必须分清

关键词 它是什么 这章里要抓住什么
input_dev 一个输入设备对象 驱动在 probe 里分配、设置能力位、注册它;它代表"这个设备能产生哪些事件"
input_event 一条标准输入事件 本质是 time + type + code + value;应用最终读到的就是它
input_handler 事件处理者 它不直接操作硬件,而是决定"这些事件要怎么被消费"
input_handle input_devinput_handler 的绑定关系 匹配成功后由 Input Core 建立,用来把设备和 handler 连起来
evdev 最常用的通用 handler 它把事件排进缓冲区、唤醒等待者,并提供 /dev/input/eventX 给应用读取
tslib 用户态触摸屏库 它不是驱动,而是在 evdev 之上帮你做校准、过滤、触摸数据处理
uinput 用户态创建虚拟输入设备的接口 它不是绕过 Input Core,而是让用户态也能走同一套 Input 事件分发链路

input_devinput_eventhandler 为什么经常混淆

  • input_dev 是"设备描述",不是"一次事件"。
    比如驱动会说"我支持 EV_KEYEV_ABS,我有 BTN_TOUCHABS_XABS_Y",这些都是能力声明。
  • input_event 是"实时上报的数据"。
    比如"现在按下了""X 坐标是 100""Y 坐标是 200"。
  • input_handler/evdev 是"消费和分发这些事件的人"。
    驱动不负责自己实现 poll/select/fasync 的整套接口,evdev 已经把这些统一做好了。

对初学者最关键的一句话是:

设备驱动负责产生事件,evdev 负责把事件变成 /dev/input/eventXtslib 负责在用户态进一步处理触摸屏事件。

应用层要看到什么,驱动层就要上报什么

03_输入系统应用编程.docx01_input_app 里,你至少要建立 3 个映射:

  • 按键类设备常见的是 EV_KEY
  • 单点触摸常见的是 BTN_TOUCH + ABS_X + ABS_Y
  • 多点触摸常见的是 ABS_MT_SLOT + ABS_MT_TRACKING_ID + ABS_MT_POSITION_X + ABS_MT_POSITION_Y

这里的 SYN_REPORT 也很重要。它表示"一批相关事件上报结束了"。没有它,应用层拿到的往往是一串零散字段,不知道哪些值属于同一次触摸或同一次按键动作。

源码与实验地图

这一章最好的学习方式,不是把所有代码从头刷到尾,而是让每个目录只承担一个明确任务:它到底是在教你"怎么读事件"、还是"怎么造事件"、还是"怎么模拟一个输入设备"。

目录或资料 在主线里的角色 重点看什么 你要确认什么
01_input_app/01_app_demo 先从应用层认识事件 01_get_input_info.c 里的 EVIOCGIDEVIOCGBIT02_input_read.c 的阻塞/非阻塞 read03_input_read_poll.c04_input_read_select.c05_input_read_fasync.c 同一个 /dev/input/eventX 既可以直接 read,也可以走 poll/select/fasync;应用关心的是标准事件,不关心底层是 GPIO、I2C 还是 USB
01_input_app/02_tslib 认识 tslib 在用户态的位置 mt_cal_distance.c 里的 ts_setupts_read_mtEVIOCGABS(ABS_MT_SLOT) tslib 是建在 evdev 之上的用户态库;多点触摸应用并不是直接自己解释每个 slot
DRV_01_Input子系统框架详解.md 建立 Input Core 心智模型 input_devinput_handlerinput_handle 三者关系,以及 input_register_deviceinput_register_handler 的匹配流程 以后再看 evdev.cgpio_keys.cgoodix.c 时,不会把"设备驱动"和"事件处理者"混成一层
02_input_dev_framework 最小 input_dev 骨架 input_dev.cdevm_input_allocate_device__set_bitinput_set_abs_paramsinput_register_devicerequest_irq 这个目录是教学骨架,不是开箱即跑的完整驱动:ISR 里还有 XX 占位,input_dev.dtsinterrupts = <...>; 也是未填完整示例
03_touchscreen_qemu/01_irq_ok 先把中断接通 touchscreen_qemu.cof_get_gpiogpio_to_irqrequest_irq QEMU 触摸屏驱动不是一上来就全做完,而是先验证"硬件事件能不能进中断"
03_touchscreen_qemu/02_all_ok 把单点触摸链路补全 touchscreen_qemu.cINPUT_PROP_DIRECTBTN_TOUCHABS_XABS_Y、定时器轮询坐标;touchscreen_qemu.dtsreg = <0x021B4000 16>gpios = <&gpio1 5 1> QEMU 例子是单点触摸 ,主要看 BTN_TOUCH + ABS_X + ABS_Y;它的作用是把 Input 抽象讲透,不是替代真实电容屏驱动
DRV_05_GPIO按键驱动分析与使用.md 连接 GPIO、中断和 Input gpio_keys 设备树属性、去抖思路、input_event(...EV_KEY...) GPIO 按键本质上就是"GPIO/IRQ + Input"的组合;按键去抖往往要靠定时器或延时策略
DRV_06_I2C接口触摸屏驱动分析.md 连接 I2C 和 Input 文档里的 goodix_ts_probegoodix_request_irqgoodix_ts_irq_handlerinput_mt_slotinput_mt_report_slot_state 真实电容屏通常是 I2C + Input 的组合:I2C 负责读触点数据,Input 负责把它组织成多点触摸事件
04_uinput 从用户态反向创建输入设备 uinput_test.cUI_SET_PROPBITUI_SET_EVBITUI_SET_KEYBITUI_SET_ABSBITUI_DEV_CREATE uinput 不是"伪造几条随机事件"这么简单,而是先声明设备能力,再创建虚拟设备,然后按规范发送事件流

建议这样做一轮最小实验

  1. 先执行 cat /proc/bus/input/devices,确认系统里有哪些输入设备。
  2. 再用 01_get_input_info.chexdump /dev/input/eventX 看看某个事件节点到底支持哪些 EV_* 类型。
  3. 然后跑 02_input_read.c03_input_read_poll.c04_input_read_select.c05_input_read_fasync.c,对比应用拿事件的 4 种方式。
  4. 再看 02_input_dev_framework/input_dev.c,把"应用层看到了什么"反推回"驱动应该声明哪些能力位、应该上报哪些事件"。
  5. 再看 03_touchscreen_qemu/02_all_ok/touchscreen_qemu.c,把 BTN_TOUCHABS_XABS_YINPUT_PROP_DIRECT 这些概念落到一个完整驱动上。
  6. 最后再看 DRV_06_I2C接口触摸屏驱动分析.md04_uinput/uinput_test.c,理解真实多点触摸和用户态虚拟输入设备分别怎样接进同一套框架。

uinput 实验前置条件要先说死

04_uinput 这段最容易让初学者误判成"写几个事件就行",实际前置条件至少有这些:

  • 先编出并加载 uinput.ko
  • 确认 /dev/uinput 设备节点已经出现
  • 再运行 uinput_test
  • uinput_test 里先暂停 60 秒,是为了给 ts_calibratets_test 留出发现新设备的时间
  • 如果系统里已经有真实触摸屏,可能要先导出 TSLIB_TSDEVICE=/dev/input/eventX,明确让 tslib 绑定到虚拟设备

也就是说,uinput 本质上是在用户态"先注册一个虚拟 input_dev,再上报事件",它并没有跳过 Input Core。

初学者最容易混淆的点

  • 混淆 1:以为 Input 子系统是在教"怎么直接操作触摸屏硬件"。
    实际上,这章教的是"怎么把各种输入硬件统一翻译成输入事件"。硬件访问本身往往分散在 GPIO、I2C、USB 等子系统里。
  • 混淆 2:把 input_dev 当成"应用读到的那个结构体"。
    错。应用读到的是 struct input_eventinput_dev 是驱动在内核里注册的设备对象。
  • 混淆 3:把 evdev 当成具体设备驱动。
    错。evdev 是通用事件处理者,它负责把事件排队、唤醒等待者,并导出 /dev/input/eventX
  • 混淆 4:把 tslib 当成驱动。
    错。tslib 是用户态库,主要做校准、过滤和触摸数据处理,它依赖下面已经存在的事件节点。
  • 混淆 5:把 02_input_dev_framework/input_dev.c 当成可直接运行的成品。
    错。这个文件明显保留了教学占位:ISR 里有 XX,设备树里 interrupts 也没有填满,它的作用是帮你看清"最小骨架"。
  • 混淆 6:把 QEMU 触摸屏和真实 Goodix 电容屏当成同一种事件模型。
    不完全对。QEMU 例子是单点触摸 ,主看 BTN_TOUCH + ABS_X + ABS_Y;真实 Goodix 电容屏是多点触摸 ,主看 ABS_MT_SLOT + ABS_MT_TRACKING_ID + ABS_MT_POSITION_X/Y
  • 混淆 7:觉得 BTN_TOUCHABS_X/ABS_Y 足以描述所有触摸屏。
    对电阻屏或简单单点触摸大体够用,但对电容多点触摸不够;这就是 Type B 协议存在的原因。
  • 混淆 8:觉得 uinput 是"旁门左道",和真实驱动无关。
    错。uinput 恰好能反向帮助你理解 Input 子系统:真实驱动是在内核态创建 input_devuinput 是在用户态通过 uinput.c 做同样的事情。
  • 混淆 9:以为事件节点编号固定不变。
    错。/dev/input/eventX 的编号会随设备注册顺序变化,实验时不要把 event1event2 写死。

学完后的迁移方向

学完本章后,你至少应该能做到什么

  • 能解释为什么 Input 子系统要抽象"事件",而不是暴露每种硬件的私有读法。
  • 能说清 input_devinput_eventinput_handlerevdevtslibuinput 各自在整条链路里的位置。
  • 能自己写一个最小的 input_dev 骨架:分配设备、声明能力、注册设备、在中断或定时器里上报事件。
  • 能区分单点触摸和多点触摸的上报思路,知道 BTN_TOUCH + ABS_X/YABS_MT_* 不是一回事。
  • 能从应用层反推驱动层:如果应用希望 poll/select/fasync 正常工作,那么驱动最好接到 Input/evdev 这套标准框架里,而不是自己临时发明接口。

之后怎样迁移到真实驱动开发

  • 做 GPIO 按键时,你会自然把它拆成:GPIO/IRQ 负责感知,Input 负责上报 EV_KEY
  • 做 I2C 电容屏时,你会自然把它拆成:I2C 负责读触点数据,Input 负责组织 ABS_MT_* 事件。
  • 做简单触摸测试和自动化回归时,你会想到 uinput,因为它能在没有真实手指、没有真实硬件的前提下给应用喂标准输入事件。
  • 再看 drivers/input/keyboard/gpio_keys.cdrivers/input/touchscreen/goodix.cdrivers/input/evdev.cdrivers/input/misc/uinput.c 时,你不会再把这些文件当成一团黑盒,而能看出它们分别属于"设备驱动""通用 handler""用户态入口"哪一层。

一句话概括这章的迁移价值:

以后你再遇到任何按键、触摸屏、手柄、甚至某些传感器的人机输入场景,都可以先问自己两件事:底层数据是怎么来的?上层应该把它当成什么输入事件。

资料边界说明

本章同时参考了 05_Input 目录下的 Markdown 讲义、03_输入系统应用编程.docx 提取出来的正文片段,以及 source/A7/05_Input 的示例源码。

其中 03_输入系统应用编程.docx 的正文文字可以提取到应用层 API、触摸屏数据格式、Type B 协议等关键信息;但原文里的部分截图、排版和图片细节无法完整还原。因此,这份总结里凡是涉及应用层实验现象、tslib 用法、QEMU 单点触摸实现的地方,都是结合目录结构和示例源码交叉补全的,没有对未明确读到的图示内容做臆测。

相关推荐
Joseph Cooper1 小时前
STM32MP157 Linux驱动学习笔记(二):硬件资源地基(Pinctrl/GPIO/Interrupt)
linux·stm32·学习
сокол1 小时前
【网安-应急响应-基础记录】Linux入侵排查
linux·网络安全·系统安全
Z文的博客1 小时前
FLASHDB实战详解 - 嵌入式KV/TSD数据库开发全攻略
stm32·单片机·嵌入式·flash·flashdb·w25q256
xieliyu.2 小时前
Java手搓数据结构:从零模拟实现单向无头非循环链表
java·数据结构·学习·链表
一个小浪吴啊2 小时前
MacOS/Linux/Windows 跨平台一键安装OpenCode指南
linux·windows·macos·opencode
圆山猫2 小时前
[AI] [Linux] 教我用rust写一个GPIO驱动
linux·rust
y = xⁿ2 小时前
MySQL学习日记:关于MVCC及一些八股总结
数据库·学习·mysql
~无忧花开~2 小时前
CSS全攻略:从基础到实战技巧
开发语言·前端·css·学习·css3
江公望2 小时前
Linux kernel devm_of_platform_populate()函数浅谈
linux