
这篇文章整理自课程第 09-10 章。它们共同把"单个设备驱动"提升到"系统框架":UART 连接到 TTY/console,PCIe 连接到枚举、BAR、中断和 Host 框架。
源文件地址:git clone https://e.coding.net/weidongshan/linux/doc_and_source_for_drivers.git
09_UART 学习总结
TL;DR:这一章真正要学会的,不只是"串口怎么收发数据",而是把整条链路看通:
UART 硬件 -> termios 应用编程 -> TTY/line discipline -> serial_core -> 硬件 uart_ops -> console/printk/earlycon -> RS485。前面08_Interrupt讲的是"事件怎么进内核",这一章讲的是"字符流怎么穿过 TTY 体系进出串口硬件"。
章节定位
09_UART 是课程里跨度很大、但又非常值得精学的一章。
表面上它在讲串口,实际上它一次性把这几层都串起来了:
- 硬件层:UART 的移位寄存器、FIFO、波特率、收发中断
- 用户态层:
open/read/write/ioctl,以及termios - 框架层:TTY、line discipline、serial_core、
uart_driver、uart_port - 调试层:
/dev/console、printk、early_printk、earlycon - 扩展层:RS485
这章和前后知识点的关系可以这样看:
- 往前承接
06_Pinctrl:串口能不能工作,先取决于引脚有没有切到uart功能。 - 往前承接
08_Interrupt:串口接收、发送完成、错误处理最终都靠中断框架推动。 - 往后服务几乎所有真实调试工作:板子 bring-up、内核启动日志、外设模块联调、console 输出,都会反复用到这一章的知识。
一句话概括:这一章是在解释 Linux 怎样把"一个会收发字节的硬件",接成"一个既能给应用读写、又能给内核打印、还能切换行规程的终端系统"。
学习主线建议
这一章如果一上来就读 drivers/tty/serial/serial_core.c,初学者通常会被三层 ops 和一堆历史包袱绊住。建议强制按下面顺序学:
- 先把 UART 硬件想清楚
先看01_UART子系统视频介绍.md、02_硬件知识_UART硬件介绍.md。
只回答三个问题:- UART 和 RS232/TTL/USB 转串口是什么关系
- 一帧数据怎么由起始位、数据位、校验位、停止位组成
- FIFO、发送完成中断、接收中断为什么存在
- 再学用户态串口编程
看05_Linux串口应用编程.md、05_a_在STM32MP157上做串口实验的准备工作.md,先跑通serial_send_recv.c。
这一步先建立直觉:对应用来说,串口首先就是一个设备节点 +termios。 - 再把 TTY 体系中的设备节点和角色分开
看03_TTY体系中设备节点的差别.md、04_TTY驱动程序框架.md。
先分清:/dev/ttySTM3是具体串口/dev/tty是当前进程自己的终端/dev/tty0是前台虚拟终端/dev/console是内核选中的控制台
- 再用"普通字符设备"做一次对照
看07_字符设备驱动程序的另一种注册方法.md,再对照03_char_dev_driver。
这一步的目标是先确认:UART 不是简单重复register_chrdev/cdev那套,而是要借serial_core + tty走一条更厚的框架路径。 - 再顺着注册、open、read、write 这条链读源码
看08_UART驱动情景分析_注册.md、09_UART驱动情景分析_open.md、10_UART驱动情景分析_read.md、11_UART驱动情景分析_write.md。
这一段的目标不是记调用栈,而是看明白三层ops:tty_operationstty_port_operationsuart_ops
- 再用虚拟 UART 驱动把抽象跑通
按13 -> 14 -> 15 -> 16的顺序,看04_virtual_uart_driver到08_virtual_uart_driver_console。
这一步的价值是:你终于能看到一个"从uart_register_driver到uart_add_one_port到 RX 中断到 flip buffer 到 console"的最小闭环。 - 最后再看 printk / console / earlycon / RS485
看17、18、19、20、21。
这一段不是"附加知识",而是把"串口是应用设备"和"串口是内核输出通道"这两个身份合在一起。
如果只保留一条最短主线,请记住:
UART 硬件 -> termios 应用编程 -> TTY 设备节点和 N_TTY -> serial_core 注册与 open/read/write -> 虚拟 UART 驱动 -> console/printk/earlycon -> RS485
UART/TTY 关键层次与对象
先记一句全章最关键的话:
UART 在 Linux 里不是一个"普通字符设备特例",而是被包在 TTY 体系里:应用面对的是 tty 设备和 line discipline,串口核心层负责把 tty 操作转成 uart_port/uart_ops,硬件驱动只实现最底下那一层。
1. UART 硬件:先别急着想 TTY
硬件视角下,UART 做的事情很朴素:
- 发送:CPU 把数据写进发送 FIFO / 寄存器,硬件按波特率移位发出
- 接收:硬件把 Rx 引脚上的位流重组为字节,放进接收 FIFO
- 触发:发送完成、接收完成、错误状态通常通过中断告诉 CPU
所以如果你后面看到 start_tx、stop_tx、startup、set_termios,不要忘了它们背后始终是在配置这套硬件行为。
2. 设备节点:/dev/ttySTM3、/dev/tty、/dev/console 不是一回事
这是本章第一个大坑。
| 名称 | 它是什么 | 你要抓住什么 |
|---|---|---|
/dev/ttySTM3 |
某个具体 STM32 串口设备 | 它对应一个具体 uart_port |
/dev/tty |
当前进程自己的终端 | 是"当前终端"的别名,不一定是串口 |
/dev/tty0 |
当前前台虚拟终端 | 和串口不是一层概念 |
/dev/console |
内核选中的控制台 | 由 cmdline 中 console= 以及 console 注册顺序决定 |
在 STM32MP157 的课程实验里,之所以能看到 /dev/ttySTM3,关键不是"驱动突然多支持了一个串口",而是设备树里:
- 使能了
uart8 - 通过 pinctrl 选对了引脚
- 通过
aliases让它拿到serial3
最后才对应到 /dev/ttySTM3。
3. line discipline:TTY 里真正负责"字符流语义"的那层
很多人第一次学串口时,只盯着 read/write,忽略了 line discipline。
这会导致两个误区:
- 以为应用
read()直接从硬件 FIFO 取字节 - 以为"回显、规范模式、raw 模式"是串口驱动自己做的
其实默认情况下,TTY 通常挂的是 N_TTY 这条 line discipline。它负责:
- 行编辑
- 回显
- 规范模式 / 非规范模式
- 某些换行和控制字符语义
所以这一章一定要把 TTY 驱动 和 line discipline 分开:
- TTY 驱动负责把字符运进运出
- line discipline 决定这些字符在上交应用前怎么组织
4. tty_driver、tty_struct、tty_port
可以先用很粗但有效的方式记:
| 对象 | 它是什么 | 你要抓住什么 |
|---|---|---|
tty_driver |
一类 tty 设备的"总描述" | 比如一批串口设备的入口 |
tty_struct |
某次打开后的 tty 实例 | 跟一次 open/use 关联最紧 |
tty_port |
介于 tty 层和底层端口之间的通用端口对象 | 帮你处理 activate/shutdown 等通用流程 |
tty_open() 并不是直接跳进硬件驱动。它会先:
- 找
tty_driver - 分配 / 初始化
tty_struct - 建立 line discipline
- 再调用
tty_driver->ops->open
5. uart_driver、uart_port、uart_ops
这是 UART 章节第二个大关。
| 对象 | 它是什么 | 你要抓住什么 |
|---|---|---|
uart_driver |
serial_core 管理的一类 UART 设备 | 它会被 serial_core 注册到 TTY 层 |
uart_port |
某个具体串口端口 | 保存寄存器基址、IRQ、锁、状态等 |
uart_ops |
最底层硬件操作集合 | startup、start_tx、set_termios 等最终落这里 |
最关键的调用链是:
uart_register_driver -> tty_register_driver
也就是说,UART 不是绕过 TTY 自己另起炉灶,而是借 serial_core 接进了 TTY 体系。
6. open / read / write 三条主线
先抓骨架,不要背所有细节:
| 动作 | 主线 | 你要抓住什么 |
|---|---|---|
| open | tty_open -> uart_open -> uart_startup -> uart_ops.startup |
第一次真正把硬件端口启动起来 |
| read | IRQ 收到字符 -> flip buffer -> line discipline -> n_tty_read |
应用读到的通常不是"硬件字节原样直达" |
| write | uart_write 把数据塞进 circ_buf -> uart_ops.start_tx 触发硬件发送 |
写入先入缓冲,再由底层发送 |
所以"为什么串口驱动这么绕"的答案是:
因为 Linux 试图把"终端语义"和"硬件发送/接收语义"分层解耦。
7. console、printk、early_printk、earlycon
这是很多初学者第一次会觉得"这章怎么突然又讲内核打印"的地方。
其实这正说明串口有双重身份:
- 对应用:它是 tty 设备
- 对内核:它可能还是 console 输出设备
要先分清:
printk:把日志写进内核 log buffer,并在合适时机刷到已注册 consoleconsole:真正负责把字符吐到设备的输出对象early_printk:非常早期、较老的打印方式earlycon:更推荐的早期 console 方式,通常依赖设备树或 cmdline 提供寄存器信息
8. RS485:不是另一个总线框架,而是 UART 的发送控制扩展
RS485 不是把 UART 全推翻重来。
从软件视角看,RS485 只是:
- 数据通道仍然是 UART
- 但发送前后要控制 DE/RE 之类的使能信号
- Linux 通常通过
serial_rs485和TIOCSRS485/TIOCGRS485把它抽象出来
所以学 RS485 时,不要把它看成"又一个全新的驱动框架",而要看成"在 UART/TTY 体系上多了一层方向控制语义"。
源码与资料地图
这一章最有效的学习方式,不是直接通读 serial_core.c,而是让每份资料只回答一个问题。
至少先盯住这 9 个关键入口
source/A7/09_UART/00_stm32mp157_devicetree_for_uart8
角色:板级准备。
重点看:uart8使能、pinctrl、aliases。
验证点:/dev/ttySTM3不是凭空来的,而是设备树资源、别名和驱动共同决定的。source/A7/09_UART/03_char_dev_driver
角色:框架对照组。
重点看:最普通字符设备的注册和读写路径。
验证点:先建立"普通字符设备怎么做",再看 UART 为什么必须接入 TTY/serial_core,能明显降低后面理解成本。stm32mp15xc-kernel/drivers/tty/tty_io.c
角色:TTY 入口层。
重点看:tty_open、tty_open_by_driver、tty_register_driver。
验证点:应用 open 串口时,先进入的是 TTY 层,而不是底层串口寄存器代码。stm32mp15xc-kernel/drivers/tty/n_tty.c
角色:默认 line discipline。
重点看:n_tty_init、n_tty_receive_buf_common、n_tty_read。
验证点:应用读到的数据,通常已经经过 line discipline 缓冲和处理。stm32mp15xc-kernel/drivers/tty/serial/serial_core.c
角色:串口核心层。
重点看:uart_register_driver、uart_add_one_port、uart_open、uart_write、uart_console_write。
验证点:serial_core 负责把 UART 硬件驱动接进 TTY,并把 tty 操作转成uart_port生命周期。stm32mp15xc-kernel/drivers/tty/serial/stm32-usart.c
角色:STM32MP157 真实硬件驱动。
重点看:stm32_usart_serial_probe、stm32_usart_startup、stm32_usart_interrupt、stm32_usart_console_write、stm32_usart_config_rs485。
验证点:设备树资源、IRQ、DMA、uart_ops、console、RS485 最终都落在这个文件里。source/A7/09_UART/07_virtual_uart_driver_ok/virtual_uart.c
角色:最小可跑通的虚拟 UART 驱动。
重点看:virtual_uart_probe、virt_start_tx、virt_uart_rxint、irq_set_irqchip_state。
验证点:一个 UART 驱动最少要把注册、端口添加、发送路径、接收中断和 flip buffer 串起来。source/A7/09_UART/08_virtual_uart_driver_console/virtual_uart.c
角色:最小 console 驱动样例。
重点看:virt_uart_console_write、virt_uart_console_device、virt_uart_drv.cons。
验证点:console 不是另一套完全独立的串口驱动,而是uart_driver上挂的 console 角色。stm32mp15xc-kernel/kernel/printk/printk.c和stm32mp15xc-kernel/drivers/tty/serial/earlycon.c
角色:内核打印链路。
重点看:vprintk_emit、register_console、console_unlock、setup_earlycon。
验证点:printk先写日志缓存,再由 console 刷出;earlycon则在更早阶段先把 console 注册起来。
推荐实验顺序
- 先做
01_app_send_recv/serial_send_recv.c
目标:先把termios + read/write用起来,不急着进内核。 - 再看
00_stm32mp157_devicetree_for_uart8
目标:搞清/dev/ttySTM3为何出现,以及 STM32MP157 上串口实验前为什么要先改设备树。 - 再读
03_TTY体系中设备节点的差别.md
目标:别把/dev/tty、/dev/console、/dev/ttySTM3混掉。 - 再对照
03_char_dev_driver和07_字符设备驱动程序的另一种注册方法.md
目标:先建立"普通字符设备"和"TTY 串口设备"的差别。 - 再顺着
08/09/10/11四篇情景分析看 open/read/write
目标:把三层ops和N_TTY的位置理顺。 - 再按
04_virtual_uart_driver -> 05_virtual_uart_driver_uart_ops -> 06_virtual_uart_driver_txrx -> 07_virtual_uart_driver_ok -> 08_virtual_uart_driver_console
目标:看课程如何一步步把最小 UART 驱动补完整。 - 最后看
17、18、19、20、21
目标:把"串口用于应用"和"串口用于内核打印/早期打印/RS485"整合起来。
如果你要验证一个最小闭环,最少观察什么
- 应用层:
看serial_send_recv能否在指定设备节点上收发。 - 设备树层:
看 STM32MP157 上uart8是否 enable,serial3 = &uart8是否存在。 - 驱动层:
看stm32_usart_serial_probe是否成功调用uart_add_one_port。 - 中断/接收层:
看virt_uart_rxint或真实驱动里的接收中断,数据是否进了 flip buffer。 - console 层:
看console=、/dev/console和register_console是否能对上。
初学者最容易卡住的地方
- 卡点 1:把 UART 当成普通字符设备去理解。
不够。它最终要接进 TTY 体系,而不是只靠一个file_operations完事。 - 卡点 2:分不清
/dev/ttySTM3、/dev/tty、/dev/tty0、/dev/console。
这几个节点指向的语义层次完全不同。 - 卡点 3:不知道 line discipline 在哪一层。
N_TTY不属于底层串口硬件驱动,它是 tty 体系中处理字符流语义的一层。 - 卡点 4:看不懂为什么 open 过程会绕三层
ops。
因为 Linux 把"终端语义""端口生命周期""硬件操作"拆开了。 - 卡点 5:以为
read()是直接从串口寄存器或 FIFO 拿字节。
实际上中间往往还经过 flip buffer 和 line discipline。 - 卡点 6:以为 console 就是某个固定串口。
不对。/dev/console由 cmdline 和 console 注册顺序共同决定。 - 卡点 7:把
printk和 console 混成一个东西。
printk负责生成日志并写入 ring buffer,console 负责把它真正输出到设备。 - 卡点 8:把
early_printk和earlycon当成"只是不同名字"。
earlycon是更通用、和设备树/驱动绑定更好的现代方式。 - 卡点 9:以为 RS485 是另一个完全独立的外设框架。
它通常还是基于 UART,只是多了发送方向控制和一些时序配置。
学完后的迁移方向
学完本章后,你至少应该能做到什么
- 能解释 UART 硬件一帧数据的基本构成,以及 FIFO / IRQ 的作用。
- 能用
termios写一个最小串口收发程序。 - 能分清 TTY、Terminal、Console、UART 这几个概念。
- 能说清
uart_register_driver、uart_add_one_port、uart_open各自在哪一层。 - 能理解应用 read/write 为什么会经过
N_TTY和 flip buffer。 - 能知道 console、
printk、earlycon为什么都和串口驱动有关。 - 能知道 RS485 在 Linux 里通常如何通过
serial_rs485暴露出来。
之后怎么迁移到真实驱动开发
- 看别的串口驱动时,你会先找:
uart_ops、probe、startup、set_termios、中断处理函数。 - 排查"串口设备节点不出现"时,你会优先检查:
设备树status、pinctrl、aliases、uart_add_one_port是否成功。 - 排查"应用能打开但收不到数据"时,你会分层检查:
硬件中断是否触发、RX 中断是否进来、flip buffer 是否 push、line discipline 是否在等数据。 - 排查"printk 没输出"时,你会先区分:
是 console 没注册、console=没指定、console_loglevel太低,还是需要earlycon。
一句话概括这章的迁移价值:
以后你再看串口问题,不会只会盯寄存器或设备节点,而会从应用、TTY、serial_core、硬件、console 这几层系统地排查。
资料边界说明
本章同时参考了:
doc_pic/09_UART下的 Markdown 讲义source/A7/09_UART下的应用与虚拟 UART 示例- 本地内核树
stm32mp15xc-kernel里的tty_io.c、n_tty.c、serial_core.c、stm32-usart.c、printk.c、earlycon.c
其中课程讲义负责解释术语、历史和主线;A7 示例负责把最小 UART 驱动、接收中断、console 样例跑通;本地内核实现负责补全真实 STM32MP157 驱动和 TTY/printk 的落点。这份总结里没有对材料不足的部分做编造;涉及真实调用链和对象关系的地方,都以讲义和本地内核代码能互相印证的内容为准。
10_PCI_PCIe 学习总结
TL;DR:这一章真正要学会的,不是"怎么写一个简单的 PCIe 驱动",而是把整条链路看通:
配置空间/Type0-Type1 -> PCIe 分层和路由 -> Host 地址映射与枚举 -> INTx/MSI/MSI-X/LPI -> Linux PCI 驱动框架。课程自己的 A7 示例源码很少,这是正常的,因为 PCIe 的关键逻辑本来就分散在讲义、Host 控制器驱动和内核 PCI Core 里。
章节定位
10_PCI_PCIe 是前 10 章里最"系统架构导向"的一章。
前面的 06_Pinctrl、07_GPIO、08_Interrupt、09_UART,你还能较容易地围绕一个具体外设来理解驱动;到了 PCI/PCIe,单靠"某个设备驱动怎么读写寄存器"已经不够了,因为你先要理解:
- CPU 地址空间和 PCI/PCIe 地址空间不是同一个空间
- 设备不是上来就能访问,必须先经过配置空间读取、BAR 分配、桥配置、总线枚举
- 中断也不只是
request_irq(),还会牵涉 INTx、MSI、MSI-X、GICv3 ITS/LPI - 真正的关键实现不在课程自带一个小 toy driver 里,而在 Linux 内核现成的 PCI Core 和 Host 控制器驱动里
这章和前后知识点的关系可以这样看:
- 往前承接
08_Interrupt:PCIe 中断最后还是要落回 Linux 中断框架,只是前面多了 INTx/MSI/MSI-X 和 MSI domain 这层抽象。 - 往前承接
09_UART:UART 章主要是"设备已经存在,怎么接进 TTY";PCIe 章主要是"设备怎么先被发现、分配资源,再进入驱动匹配"。 - 往后承接真实驱动开发:以后你看 NVMe、网卡、Wi-Fi、采集卡这类 PCIe 设备时,都会同时碰到枚举、BAR、MSI、
pci_driver、DMA。
一句话概括:这一章是在解释 Linux 怎么把一棵 PCIe 拓扑树,变成内核里可匹配、可分配资源、可处理中断的设备模型。
学习主线建议
这一章如果直接扎进 drivers/pci/msi.c 或某个 SoC 的 PCIe Host 驱动,初学者通常会迷路。建议强制按下面顺序学:
- 先用"软件视角"看 PCI/PCIe
先看01_从软件开发角度看待PCI和PCIe.md。
只解决两个问题:为什么 PCI/PCIe 设备也能"像内存一样访问",以及为什么这里一定会出现"地址空间转换"。 - 再把配置空间和 Type0/Type1 搞清楚
看02_PCI设备的访问方法_非桥设备(type0).md、03_PCI设备的访问方法_桥设备(type1).md。
先搞懂:- Type 0 Header 是普通设备
- Type 1 Header 是桥
- 为什么桥后面的设备要靠 bus/device/function 和 Type 1 配置请求继续往下找
- 再看 PCIe 为什么比 PCI 多出一层"分层 + 路由"
看04_从软件角度看PCIe设备的硬件结构.md、05_PCIe设备的配置过程.md、06_PCIe路由方式.md。
这一段的目标不是背 TLP 位定义,而是建立这几个心智模型:- RC / Switch / Endpoint 是点对点拓扑,不是大家挂在一条并行总线上
- 配置请求走 ID 路由
- 内存读写走地址路由
- 返回完成包时又要靠 Requester ID / Transaction ID 回来
- 再看 Host 怎么做地址映射和枚举
看09_RK3399_PCIe_Host驱动分析_地址映射.md、10_RK3399_PCIe_Host驱动分析_设备枚举.md,并对照本地内核的 PCI Core 与 Host 控制器驱动。
这一段的核心问题是:- RC 怎么用 outbound ATU 把 CPU 地址映射成 PCIe 地址
- Host 怎么发 CFG0 / CFG1 去读配置空间
- 枚举完以后,Linux 里是怎么生成
pci_bus、pci_dev的
- 最后再看中断和驱动框架
看11_INTx_MSI_MSIX三种中断机制分析.md、12_INTx中断机制源码分析.md、13_GICv3_LPI机制.md、14/15_MSI_MSI-X中断之源码分析.md、16_怎么编写PCIe设备驱动程序.md。
这一步要把:
INTx -> MSI/MSI-X -> ITS/LPI -> pci_driver
串成一条线。
如果只保留一条最短主线,请记住:
PCI/PCIe 软件视角 -> 配置空间 Type0/Type1 -> PCIe 分层与路由 -> Host 地址映射与枚举 -> INTx/MSI/MSI-X/LPI -> Linux pci_driver
PCI/PCIe 关键层次与对象
先记一句全章最关键的话:
PCI/PCIe 设备不是"天然就存在于 Linux 里"的。内核必须先通过 Host 控制器访问配置空间、枚举总线、分配 BAR 和中断资源,最后才会构造 pci_dev 交给 pci_driver 去匹配。
1. CPU 地址空间、PCI/PCIe 地址空间、配置空间
课程一开始就强调:PCI/PCIe 设备之所以能像内存一样访问,本质上还是"地址 + 读写"。
但这里至少有三种视角:
| 视角 | 它是什么 | 你要抓住什么 |
|---|---|---|
| CPU 地址空间 | CPU 发出的地址 | 驱动里最后 ioremap、readl/writel 用的是它 |
| PCI/PCIe 地址空间 | 设备侧看到的 Memory / I/O 地址 | 由 Host 桥或 ATU 负责转换 |
| 配置空间 | 设备自描述空间 | 先读它,才知道设备是谁、需要多大 BAR、是否支持 MSI/MSI-X |
所以这章最容易犯的第一个错,就是把"CPU 看到的地址"和"PCIe 总线上用来路由的地址"当成同一个概念。
2. Type 0、Type 1 和桥的意义
普通设备和桥设备的配置空间头部不一样:
- Type 0 Header:普通 Endpoint
- Type 1 Header:桥设备
桥为什么重要?
- 没有桥时,只需要看直连设备
- 有桥以后,软件必须先给桥写
Primary / Secondary / Subordinate Bus Number - 之后才能继续向桥下游发配置请求
这也是为什么课程专门拆出 Type0、Type1 两节。你真正要记住的不是表格,而是:
- 直连设备:配置请求相对简单
- 桥后设备:必须先配置桥,再靠 bus/device/function 继续寻址
3. PCIe 分层和路由
PCIe 比 PCI 多出来的关键抽象,不是"速度更快",而是"点对点 + 分层包化"。
软件角度最重要的三层是:
| 层次 | 你要抓住什么 |
|---|---|
| Transaction Layer | 你关心的读写动作最终都会变成 TLP |
| Data Link Layer | 负责可靠传输、确认、重传 |
| Physical Layer | 真正把数据变成 lane 上的电信号 |
对应到驱动理解时,不要去背所有字段,而要先抓住三类路由:
- ID 路由:配置读写最重要,靠 Bus/Device/Function 找设备
- 地址路由:Memory / I/O 访问最重要,靠 BAR 和桥窗口转发
- 隐式路由:消息类事务常见,比如某些中断或广播语义
4. Host 控制器、ATU、总线枚举
对 Linux 驱动开发者来说,PCIe Host 最重要的动作只有几个:
- 建链路
- 配 RC 自己的配置空间
- 配 outbound ATU,把 CPU 地址映射到 PCIe 空间
- 发 CFG0 / CFG1 去读配置空间
- 扫总线,生成
pci_bus/pci_dev - 分配 BAR、桥窗口和中断资源
这也是为什么课程里地址映射和枚举要单独讲两节。你看到的 pci_dev->resource[]、pci_dev->irq,都不是"设备自己冒出来的",而是 Host + PCI Core 扫描后填进去的。
5. INTx、MSI、MSI-X、LPI
这一段最容易学乱,先把差别钉死:
| 机制 | 本质 | 你要抓住什么 |
|---|---|---|
| INTx | 引脚式共享中断 | 老机制,像传统"线" |
| MSI | 向某个地址写一个 message data | 中断变成一次 Memory Write |
| MSI-X | MSI 的增强版,向量表放在 BAR 里 | 向量更多,更灵活 |
| LPI | GICv3 ITS 支持的消息型中断落点 | PCIe 设备写 ITS 的翻译地址,最后变成 Linux IRQ |
对 ARM GICv3 + ITS 平台,课程强调的关键直觉是:
MSI/MSI-X 不是"拉高一根引脚",而是设备发一个 TLP,向 ITS 的翻译地址写入数据。
6. Linux 里的 pci_host_bridge、pci_bus、pci_dev、pci_driver
Linux PCI 框架的对象关系可以先记成:
| 对象 | 它是什么 | 你要抓住什么 |
|---|---|---|
pci_host_bridge |
Host 控制器在 PCI Core 里的入口对象 | 负责把一棵 root bus 接进内核 |
pci_bus |
一条 PCI 总线 | 组织桥和设备 |
pci_dev |
一个已枚举出来的设备 | 保存 VID/DID/class/BAR/IRQ 等资源 |
pci_driver |
你写的驱动 | 用 id_table 和 probe 去匹配 pci_dev |
所以 PCIe 驱动的最小心智模型不是"platform_driver + ioremap",而是:
Host 枚举出 pci_dev -> pci_driver 匹配 -> probe -> pci_enable_device / 取 BAR / 申请中断 / DMA
源码与资料地图
这一章必须先接受一个事实:课程自己的 A7 示例源码很少,这是正常的,不是资料缺失。
原因在于,PCIe 的关键逻辑本来就不适合被压缩成一个独立的小玩具工程:
- 配置空间访问要依赖 Host 控制器
- 枚举要依赖 PCI Core
- MSI/MSI-X 要依赖中断域和 GIC/ITS
- 真正常见的设备驱动写法,通常要看现成内核驱动,比如 NVMe
所以这一章更正确的学习方式是:
讲义建立概念 -> 内核现成实现验证概念 -> 再回头看 PCIe 设备驱动框架
至少先盯住这 8 个关键入口
source/A7/10_PCI_PCIe/源码使用说明.md
角色:课程源码边界说明。
验证点:这一章不是靠本目录下一个 toy driver 学会的,而是靠讲义和内核现成实现建立模型。stm32mp15xc-kernel/drivers/pci/access.c
角色:配置空间访问入口。
重点看:pci_bus_read_config_*、pci_bus_write_config_*。
验证点:Linux 访问配置空间,最终还是统一收敛到pci_ops的读写回调。stm32mp15xc-kernel/drivers/pci/probe.c
角色:PCI Core 的枚举主线。
重点看:pci_host_probe、pci_scan_root_bus_bridge、pci_bus_add_devices。
验证点:root bus 是怎么被扫描、桥资源怎么被分配、pci_dev何时真正加入系统。stm32mp15xc-kernel/drivers/pci/controller/dwc/pcie-designware-host.c
角色:典型 Host 控制器实现。
重点看:dw_pcie_host_init、dw_pcie_rd_conf、dw_pcie_wr_conf、dw_pcie_rd_other_conf、dw_pcie_setup_rc。
验证点:CFG0/CFG1 怎么区分、RC 怎么配 bus number、BAR 和 outbound ATU。stm32mp15xc-kernel/drivers/pci/controller/dwc/pcie-designware.c
角色:地址转换底座。
重点看:dw_pcie_prog_outbound_atu。
验证点:CPU 地址到 PCIe 地址的映射,并不是"天然就通",而是 Host 驱动显式编程出来的。stm32mp15xc-kernel/drivers/pci/msi.c
角色:MSI/MSI-X 公共框架。
重点看:pci_alloc_irq_vectors_affinity、pci_irq_vector、pci_msi_create_irq_domain、pci_msi_domain_write_msg。
验证点:设备驱动拿到中断向量的 API 只是表层,底下已经把 MSI descriptor、irq domain、message address/data 组织好了。stm32mp15xc-kernel/drivers/pci/pci-driver.c
角色:PCI 设备驱动匹配入口。
重点看:pci_match_device、pci_bus_match、pci_device_probe、__pci_register_driver。
验证点:pci_driver不是"自己去找设备",而是 PCI Core 在枚举出pci_dev后按id_table触发匹配与 probe。stm32mp15xc-kernel/drivers/pci/controller/pcie-rockchip-host.c
角色:课程 RK3399 背景板的 Host 示例。
重点看:rockchip_pcie_prog_ob_atu、rockchip_pcie_init_irq_domain、rockchip_pcie_probe。
验证点:讲义里讲的 RK3399 地址映射、INTx 域、Host 初始化,在内核里各自落在哪些函数。
资料阅读顺序
01到06:只建立 PCI/PCIe 软件模型,不急着进源码。07:先知道 Linux 最终是pci_dev对pci_driver。09、10:把 Host 地址映射和枚举跟内核文件对上。11到15:把 INTx / MSI / MSI-X / ITS 串起来。16:最后再回到"怎么写 PCIe 设备驱动"。
如果你有板子,最小实验怎么做
课程自己的实操重点在 RK3399 + NVMe,不在当前目录的 A7 小工程里。最小观察顺序建议是:
lspci -vv:先看设备有没有被枚举出来、BAR 是否分配、MSI/MSI-X capability 是否存在。dmesg | grep -i pci:看 Host 初始化、枚举、资源分配日志。- 对照讲义里的 BAR / MSI-X 向量表示例,理解
Region 0、Vector table、PBA分别是什么意思。 - 再回内核里对应
pci_alloc_irq_vectors_affinity和 Host 驱动,确认 Linux 是如何把设备 capability 变成真实 IRQ 的。
初学者最容易卡住的地方
- 卡点 1:把 PCI/PCIe 设备当成"平台设备的另一种寄存器映射"。
不够。平台设备通常设备树一描述就能probe,PCIe 设备则要先被枚举和分配资源。 - 卡点 2:分不清配置空间、Memory BAR、I/O BAR。
配置空间是"自描述和控制";BAR 指向的是设备真正工作用的 Memory / I/O 空间。 - 卡点 3:看不懂 Type0、Type1 到底为什么要分。
本质上是在区分"普通 Endpoint"和"桥",以及"你是否还要继续往下游找设备"。 - 卡点 4:以为 PCIe 路由就是看地址。
不对。配置请求走 ID 路由,内存访问走地址路由,完成包还要靠 Requester ID 回来。 - 卡点 5:以为 Host 控制器驱动只是做链路训练。
不止。它还负责配置 RC、ATU、配置空间访问窗口、INTx/MSI 域等关键基础设施。 - 卡点 6:把 INTx、MSI、MSI-X 理解成"同一种中断,只是名字不同"。
差别很大。INTx 是引脚语义,MSI/MSI-X 是 message 语义,MSI-X 还把向量表放进 BAR。 - 卡点 7:以为课程示例源码少,说明这一章不重要。
恰好相反。正因为这一章更靠近通用框架和真实内核实现,才不适合用几个玩具函数替代。 - 卡点 8:看规范 PDF 看太多,反而忘了课程目标。
初学者最先要建立的是"Linux 怎么用这些概念",不是去背完整 TLP 位图。
学完后的迁移方向
学完本章后,你至少应该能做到什么
- 能解释 CPU 地址空间、PCIe 地址空间、配置空间三者的区别。
- 能说清 Type 0 / Type 1 配置头为什么和桥枚举相关。
- 能用自己的话解释 RC、Switch、Endpoint、BAR、CFG0/CFG1、ATU。
- 能看懂
pci_host_probe -> pci_scan_root_bus_bridge -> pci_bus_add_devices这条主线。 - 能理解
pci_driver为什么是在枚举之后才有机会匹配设备。 - 能分清 INTx、MSI、MSI-X、ITS/LPI 的基本差异。
之后怎么迁移到真实驱动开发
- 看 NVMe、网卡、Wi-Fi 这类驱动时,你会先找:
pci_register_driver、id_table、probe、pci_enable_device、pci_alloc_irq_vectors_affinity。 - 排查"设备枚举不到"时,你会先分层检查:
链路是否起来、CFG0/CFG1 是否通、桥 bus number 是否对、BAR 是否分配。 - 排查"MSI 不触发"时,你会先看:
capability 是否存在、Host 是否建立 MSI domain、设备是否拿到 message address/data、ITS/LPI 是否连通。
一句话概括这章的迁移价值:
以后你再看到一个 PCIe 设备,不会只会问"驱动怎么写",而会先问"它是怎么被发现、编号、映射、分配中断并接进 Linux 的"。
资料边界说明
本章同时参考了:
doc_pic/10_PCI_PCIe下的 Markdown 讲义source/A7/10_PCI_PCIe/源码使用说明.md- 本地内核树
stm32mp15xc-kernel里的 PCI Core、DesignWare Host、Rockchip Host、MSI 相关实现
其中目录下的 PDF 和规范资料主要用于补充"PCIe 分层、TLP、ITS/LPI"背景,不作为这份总结的正文依据。这份总结里涉及枚举、ATU、MSI domain、驱动模型的部分,都是结合讲义和本地内核现成实现交叉补全的;对讲义里没有展开、而本地材料也不足以证实的细节,没有编造。