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、驱动模型的部分,都是结合讲义和本地内核现成实现交叉补全的;对讲义里没有展开、而本地材料也不足以证实的细节,没有编造。

相关推荐
chudonghao1 小时前
[UE学习笔记][基于源码] 运行时网格 PMC / DMC / RMC
笔记·学习·ue5
kongba0072 小时前
rules经验落盘
学习
一條狗2 小时前
学习日报 20260423|[特殊字符] 深度解析:Vue 3 SPA 部署到 Spring Boot 的 404/500 错误排查与完美解决方案-2
vue.js·spring boot·学习
funnycoffee1232 小时前
centos 上没有安装telnet命令 ,如何测试到1个目标IP的 443端口是否open
linux·tcp/ip·centos
森G2 小时前
STM32F103C8T6工程---标准库版usart2写回显
stm32·单片机
爱莉希雅&&&2 小时前
Ansible+Docker案例(含ansible配置安装docker)
linux·运维·mysql·nginx·docker·容器·ansible
学习论之费曼学习法2 小时前
AI 入门 30 天挑战 - Day 18 费曼学习法版 - 图像分割基础
人工智能·学习
wicb91wJ62 小时前
Linux服务器性能调优常用命令
linux·服务器·网络
圆山猫2 小时前
[AI] [Linux] 教我编一个启用rust的riscv kernel用于qemu启动
linux·ai·rust