STM32MP157 Linux驱动学习笔记(三):系统级驱动框架(UART/PCIe)

这篇文章整理自课程第 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_driveruart_port
  • 调试层:/dev/consoleprintkearly_printkearlycon
  • 扩展层:RS485

这章和前后知识点的关系可以这样看:

  • 往前承接 06_Pinctrl:串口能不能工作,先取决于引脚有没有切到 uart 功能。
  • 往前承接 08_Interrupt:串口接收、发送完成、错误处理最终都靠中断框架推动。
  • 往后服务几乎所有真实调试工作:板子 bring-up、内核启动日志、外设模块联调、console 输出,都会反复用到这一章的知识。

一句话概括:这一章是在解释 Linux 怎样把"一个会收发字节的硬件",接成"一个既能给应用读写、又能给内核打印、还能切换行规程的终端系统"。

学习主线建议

这一章如果一上来就读 drivers/tty/serial/serial_core.c,初学者通常会被三层 ops 和一堆历史包袱绊住。建议强制按下面顺序学:

  1. 先把 UART 硬件想清楚
    先看 01_UART子系统视频介绍.md02_硬件知识_UART硬件介绍.md
    只回答三个问题:
    • UART 和 RS232/TTL/USB 转串口是什么关系
    • 一帧数据怎么由起始位、数据位、校验位、停止位组成
    • FIFO、发送完成中断、接收中断为什么存在
  2. 再学用户态串口编程
    05_Linux串口应用编程.md05_a_在STM32MP157上做串口实验的准备工作.md,先跑通 serial_send_recv.c
    这一步先建立直觉:对应用来说,串口首先就是一个设备节点 + termios
  3. 再把 TTY 体系中的设备节点和角色分开
    03_TTY体系中设备节点的差别.md04_TTY驱动程序框架.md
    先分清:
    • /dev/ttySTM3 是具体串口
    • /dev/tty 是当前进程自己的终端
    • /dev/tty0 是前台虚拟终端
    • /dev/console 是内核选中的控制台
  4. 再用"普通字符设备"做一次对照
    07_字符设备驱动程序的另一种注册方法.md,再对照 03_char_dev_driver
    这一步的目标是先确认:UART 不是简单重复 register_chrdev/cdev 那套,而是要借 serial_core + tty 走一条更厚的框架路径。
  5. 再顺着注册、open、read、write 这条链读源码
    08_UART驱动情景分析_注册.md09_UART驱动情景分析_open.md10_UART驱动情景分析_read.md11_UART驱动情景分析_write.md
    这一段的目标不是记调用栈,而是看明白三层 ops
    • tty_operations
    • tty_port_operations
    • uart_ops
  6. 再用虚拟 UART 驱动把抽象跑通
    13 -> 14 -> 15 -> 16 的顺序,看 04_virtual_uart_driver08_virtual_uart_driver_console
    这一步的价值是:你终于能看到一个"从 uart_register_driveruart_add_one_port 到 RX 中断到 flip buffer 到 console"的最小闭环。
  7. 最后再看 printk / console / earlycon / RS485
    1718192021
    这一段不是"附加知识",而是把"串口是应用设备"和"串口是内核输出通道"这两个身份合在一起。

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

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_txstop_txstartupset_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_drivertty_structtty_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_driveruart_portuart_ops

这是 UART 章节第二个大关。

对象 它是什么 你要抓住什么
uart_driver serial_core 管理的一类 UART 设备 它会被 serial_core 注册到 TTY 层
uart_port 某个具体串口端口 保存寄存器基址、IRQ、锁、状态等
uart_ops 最底层硬件操作集合 startupstart_txset_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,并在合适时机刷到已注册 console
  • console:真正负责把字符吐到设备的输出对象
  • early_printk:非常早期、较老的打印方式
  • earlycon:更推荐的早期 console 方式,通常依赖设备树或 cmdline 提供寄存器信息

8. RS485:不是另一个总线框架,而是 UART 的发送控制扩展

RS485 不是把 UART 全推翻重来。

从软件视角看,RS485 只是:

  • 数据通道仍然是 UART
  • 但发送前后要控制 DE/RE 之类的使能信号
  • Linux 通常通过 serial_rs485TIOCSRS485 / TIOCGRS485 把它抽象出来

所以学 RS485 时,不要把它看成"又一个全新的驱动框架",而要看成"在 UART/TTY 体系上多了一层方向控制语义"。

源码与资料地图

这一章最有效的学习方式,不是直接通读 serial_core.c,而是让每份资料只回答一个问题。

至少先盯住这 9 个关键入口

  1. source/A7/09_UART/00_stm32mp157_devicetree_for_uart8
    角色:板级准备。
    重点看:uart8 使能、pinctrl、aliases
    验证点:/dev/ttySTM3 不是凭空来的,而是设备树资源、别名和驱动共同决定的。
  2. source/A7/09_UART/03_char_dev_driver
    角色:框架对照组。
    重点看:最普通字符设备的注册和读写路径。
    验证点:先建立"普通字符设备怎么做",再看 UART 为什么必须接入 TTY/serial_core,能明显降低后面理解成本。
  3. stm32mp15xc-kernel/drivers/tty/tty_io.c
    角色:TTY 入口层。
    重点看:tty_opentty_open_by_drivertty_register_driver
    验证点:应用 open 串口时,先进入的是 TTY 层,而不是底层串口寄存器代码。
  4. stm32mp15xc-kernel/drivers/tty/n_tty.c
    角色:默认 line discipline。
    重点看:n_tty_initn_tty_receive_buf_commonn_tty_read
    验证点:应用读到的数据,通常已经经过 line discipline 缓冲和处理。
  5. stm32mp15xc-kernel/drivers/tty/serial/serial_core.c
    角色:串口核心层。
    重点看:uart_register_driveruart_add_one_portuart_openuart_writeuart_console_write
    验证点:serial_core 负责把 UART 硬件驱动接进 TTY,并把 tty 操作转成 uart_port 生命周期。
  6. stm32mp15xc-kernel/drivers/tty/serial/stm32-usart.c
    角色:STM32MP157 真实硬件驱动。
    重点看:stm32_usart_serial_probestm32_usart_startupstm32_usart_interruptstm32_usart_console_writestm32_usart_config_rs485
    验证点:设备树资源、IRQ、DMA、uart_ops、console、RS485 最终都落在这个文件里。
  7. source/A7/09_UART/07_virtual_uart_driver_ok/virtual_uart.c
    角色:最小可跑通的虚拟 UART 驱动。
    重点看:virtual_uart_probevirt_start_txvirt_uart_rxintirq_set_irqchip_state
    验证点:一个 UART 驱动最少要把注册、端口添加、发送路径、接收中断和 flip buffer 串起来。
  8. source/A7/09_UART/08_virtual_uart_driver_console/virtual_uart.c
    角色:最小 console 驱动样例。
    重点看:virt_uart_console_writevirt_uart_console_devicevirt_uart_drv.cons
    验证点:console 不是另一套完全独立的串口驱动,而是 uart_driver 上挂的 console 角色。
  9. stm32mp15xc-kernel/kernel/printk/printk.cstm32mp15xc-kernel/drivers/tty/serial/earlycon.c
    角色:内核打印链路。
    重点看:vprintk_emitregister_consoleconsole_unlocksetup_earlycon
    验证点:printk 先写日志缓存,再由 console 刷出;earlycon 则在更早阶段先把 console 注册起来。

推荐实验顺序

  1. 先做 01_app_send_recv/serial_send_recv.c
    目标:先把 termios + read/write 用起来,不急着进内核。
  2. 再看 00_stm32mp157_devicetree_for_uart8
    目标:搞清 /dev/ttySTM3 为何出现,以及 STM32MP157 上串口实验前为什么要先改设备树。
  3. 再读 03_TTY体系中设备节点的差别.md
    目标:别把 /dev/tty/dev/console/dev/ttySTM3 混掉。
  4. 再对照 03_char_dev_driver07_字符设备驱动程序的另一种注册方法.md
    目标:先建立"普通字符设备"和"TTY 串口设备"的差别。
  5. 再顺着 08/09/10/11 四篇情景分析看 open/read/write
    目标:把三层 opsN_TTY 的位置理顺。
  6. 再按 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 驱动补完整。
  7. 最后看 1718192021
    目标:把"串口用于应用"和"串口用于内核打印/早期打印/RS485"整合起来。

如果你要验证一个最小闭环,最少观察什么

  1. 应用层:
    serial_send_recv 能否在指定设备节点上收发。
  2. 设备树层:
    看 STM32MP157 上 uart8 是否 enable,serial3 = &uart8 是否存在。
  3. 驱动层:
    stm32_usart_serial_probe 是否成功调用 uart_add_one_port
  4. 中断/接收层:
    virt_uart_rxint 或真实驱动里的接收中断,数据是否进了 flip buffer。
  5. console 层:
    console=/dev/consoleregister_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_printkearlycon 当成"只是不同名字"。
    earlycon 是更通用、和设备树/驱动绑定更好的现代方式。
  • 卡点 9:以为 RS485 是另一个完全独立的外设框架。
    它通常还是基于 UART,只是多了发送方向控制和一些时序配置。

学完后的迁移方向

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

  • 能解释 UART 硬件一帧数据的基本构成,以及 FIFO / IRQ 的作用。
  • 能用 termios 写一个最小串口收发程序。
  • 能分清 TTY、Terminal、Console、UART 这几个概念。
  • 能说清 uart_register_driveruart_add_one_portuart_open 各自在哪一层。
  • 能理解应用 read/write 为什么会经过 N_TTY 和 flip buffer。
  • 能知道 console、printkearlycon 为什么都和串口驱动有关。
  • 能知道 RS485 在 Linux 里通常如何通过 serial_rs485 暴露出来。

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

  • 看别的串口驱动时,你会先找:
    uart_opsprobestartupset_termios、中断处理函数。
  • 排查"串口设备节点不出现"时,你会优先检查:
    设备树 status、pinctrl、aliasesuart_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.cn_tty.cserial_core.cstm32-usart.cprintk.cearlycon.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_Pinctrl07_GPIO08_Interrupt09_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 驱动,初学者通常会迷路。建议强制按下面顺序学:

  1. 先用"软件视角"看 PCI/PCIe
    先看 01_从软件开发角度看待PCI和PCIe.md
    只解决两个问题:为什么 PCI/PCIe 设备也能"像内存一样访问",以及为什么这里一定会出现"地址空间转换"。
  2. 再把配置空间和 Type0/Type1 搞清楚
    02_PCI设备的访问方法_非桥设备(type0).md03_PCI设备的访问方法_桥设备(type1).md
    先搞懂:
    • Type 0 Header 是普通设备
    • Type 1 Header 是桥
    • 为什么桥后面的设备要靠 bus/device/function 和 Type 1 配置请求继续往下找
  3. 再看 PCIe 为什么比 PCI 多出一层"分层 + 路由"
    04_从软件角度看PCIe设备的硬件结构.md05_PCIe设备的配置过程.md06_PCIe路由方式.md
    这一段的目标不是背 TLP 位定义,而是建立这几个心智模型:
    • RC / Switch / Endpoint 是点对点拓扑,不是大家挂在一条并行总线上
    • 配置请求走 ID 路由
    • 内存读写走地址路由
    • 返回完成包时又要靠 Requester ID / Transaction ID 回来
  4. 再看 Host 怎么做地址映射和枚举
    09_RK3399_PCIe_Host驱动分析_地址映射.md10_RK3399_PCIe_Host驱动分析_设备枚举.md,并对照本地内核的 PCI Core 与 Host 控制器驱动。
    这一段的核心问题是:
    • RC 怎么用 outbound ATU 把 CPU 地址映射成 PCIe 地址
    • Host 怎么发 CFG0 / CFG1 去读配置空间
    • 枚举完以后,Linux 里是怎么生成 pci_buspci_dev
  5. 最后再看中断和驱动框架
    11_INTx_MSI_MSIX三种中断机制分析.md12_INTx中断机制源码分析.md13_GICv3_LPI机制.md14/15_MSI_MSI-X中断之源码分析.md16_怎么编写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 发出的地址 驱动里最后 ioremapreadl/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_bridgepci_buspci_devpci_driver

Linux PCI 框架的对象关系可以先记成:

对象 它是什么 你要抓住什么
pci_host_bridge Host 控制器在 PCI Core 里的入口对象 负责把一棵 root bus 接进内核
pci_bus 一条 PCI 总线 组织桥和设备
pci_dev 一个已枚举出来的设备 保存 VID/DID/class/BAR/IRQ 等资源
pci_driver 你写的驱动 id_tableprobe 去匹配 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 个关键入口

  1. source/A7/10_PCI_PCIe/源码使用说明.md
    角色:课程源码边界说明。
    验证点:这一章不是靠本目录下一个 toy driver 学会的,而是靠讲义和内核现成实现建立模型。
  2. stm32mp15xc-kernel/drivers/pci/access.c
    角色:配置空间访问入口。
    重点看:pci_bus_read_config_*pci_bus_write_config_*
    验证点:Linux 访问配置空间,最终还是统一收敛到 pci_ops 的读写回调。
  3. stm32mp15xc-kernel/drivers/pci/probe.c
    角色:PCI Core 的枚举主线。
    重点看:pci_host_probepci_scan_root_bus_bridgepci_bus_add_devices
    验证点:root bus 是怎么被扫描、桥资源怎么被分配、pci_dev 何时真正加入系统。
  4. stm32mp15xc-kernel/drivers/pci/controller/dwc/pcie-designware-host.c
    角色:典型 Host 控制器实现。
    重点看:dw_pcie_host_initdw_pcie_rd_confdw_pcie_wr_confdw_pcie_rd_other_confdw_pcie_setup_rc
    验证点:CFG0/CFG1 怎么区分、RC 怎么配 bus number、BAR 和 outbound ATU。
  5. stm32mp15xc-kernel/drivers/pci/controller/dwc/pcie-designware.c
    角色:地址转换底座。
    重点看:dw_pcie_prog_outbound_atu
    验证点:CPU 地址到 PCIe 地址的映射,并不是"天然就通",而是 Host 驱动显式编程出来的。
  6. stm32mp15xc-kernel/drivers/pci/msi.c
    角色:MSI/MSI-X 公共框架。
    重点看:pci_alloc_irq_vectors_affinitypci_irq_vectorpci_msi_create_irq_domainpci_msi_domain_write_msg
    验证点:设备驱动拿到中断向量的 API 只是表层,底下已经把 MSI descriptor、irq domain、message address/data 组织好了。
  7. stm32mp15xc-kernel/drivers/pci/pci-driver.c
    角色:PCI 设备驱动匹配入口。
    重点看:pci_match_devicepci_bus_matchpci_device_probe__pci_register_driver
    验证点:pci_driver 不是"自己去找设备",而是 PCI Core 在枚举出 pci_dev 后按 id_table 触发匹配与 probe。
  8. stm32mp15xc-kernel/drivers/pci/controller/pcie-rockchip-host.c
    角色:课程 RK3399 背景板的 Host 示例。
    重点看:rockchip_pcie_prog_ob_aturockchip_pcie_init_irq_domainrockchip_pcie_probe
    验证点:讲义里讲的 RK3399 地址映射、INTx 域、Host 初始化,在内核里各自落在哪些函数。

资料阅读顺序

  1. 0106:只建立 PCI/PCIe 软件模型,不急着进源码。
  2. 07:先知道 Linux 最终是 pci_devpci_driver
  3. 0910:把 Host 地址映射和枚举跟内核文件对上。
  4. 1115:把 INTx / MSI / MSI-X / ITS 串起来。
  5. 16:最后再回到"怎么写 PCIe 设备驱动"。

如果你有板子,最小实验怎么做

课程自己的实操重点在 RK3399 + NVMe,不在当前目录的 A7 小工程里。最小观察顺序建议是:

  1. lspci -vv:先看设备有没有被枚举出来、BAR 是否分配、MSI/MSI-X capability 是否存在。
  2. dmesg | grep -i pci:看 Host 初始化、枚举、资源分配日志。
  3. 对照讲义里的 BAR / MSI-X 向量表示例,理解 Region 0Vector tablePBA 分别是什么意思。
  4. 再回内核里对应 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_driverid_tableprobepci_enable_devicepci_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、驱动模型的部分,都是结合讲义和本地内核现成实现交叉补全的;对讲义里没有展开、而本地材料也不足以证实的细节,没有编造。

相关推荐
ltl4 小时前
大多数「无锁」代码其实不是无锁的
linux
ltl4 小时前
Linux 内核的内存屏障:一个让我调了三天的 bug
linux
paopaokaka_luck5 小时前
基于springboot3+vue3的企业考勤管理系统(部门树递归、Echarts图形化分析)
开发语言·spring boot·学习·echarts·mybatis·需求分析·代码规范
minglie15 小时前
zynq高频小数据量PS闭环 抖动时间的测量
学习
long3165 小时前
MySQL 学习练习(配套 01 入门资料)
数据库·学习·mysql
Lonely 净土6 小时前
Rocky Linux 安装教程
linux·运维·服务器
梦雨生生6 小时前
java开发工具(学习第一天)
java·开发语言·学习
lhbweilai8 小时前
STM32 ADC模拟数字转换(hal库)
stm32·单片机·嵌入式硬件
小僧景贤10 小时前
STM32 工程文件深度解析(Keil‑MDK + STM32CubeIDE)
stm32·单片机·stm32cubeide
李白你好10 小时前
Windows 离线 Linux 提权辅助查询工具
linux