Skip to content

平台设备 ​

那些无法自动探测(Non-discoverable)、通过内存映射 I/O 访问的设备,在 Linux 中被抽象为“平台设备”,挂载在平台设备总线上。由于设备的类型众多,不同的实现五花八门,有的设备并不支持自动探测,需要硬件厂商或者嵌入式开发者自行实现硬件的探测和配置。

在具有 BIOS/ACPI 的硬件平台上,厂商实现了设备探测的能力,设备往往无需操作系统关心,但是在嵌入式平台上,主板厂商往往不会提供 BIOS,这是嵌入式开发的生态决定的。Linux 提供了设备树源加载机制,支持部分平台设备在不走 ACPI 的条件下,通过自定义设备树,提示内核探测自定义硬件,这是 Linux 接管平台设备的指定路径。

设备行为 ​

  • 设备探测:识别平台设备存在及其硬件信息,依赖设备树,无动态扫描;
  • 资源配置:为设备分配硬件资源(如 MMIO 地址、中断号、时钟),基于设备树提供的信息;
  • 电源管理:控制设备电源状态(如开启、挂起);
  • 数据传输:通过 MMIO 或 GPIO 与设备交换数据;
  • 中断处理:通过中断机制,异步监听硬件事件,如:DMA、错误处理;

MMIO 通信 ​

MMIO 是平台设备最主要的通信方式。设备的控制寄存器、状态寄存器和数据寄存器被映射到处理器的物理地址空间,CPU 通过读写这些内存地址来控制设备。这种机制消除了专门的 I/O 指令,统一了内存和外设的访问方式。

MMIO 的实现依赖于处理器的内存管理单元 (MMU)。设备寄存器占据一段物理地址范围,这段范围被标记为不可缓存 (Uncacheable) 或设备内存 (Device Memory),确保每次读写都直接作用于硬件,不会被 CPU 缓存或乱序执行优化。

以 SoC 内部的 UART 控制器为例,其寄存器通常包括发送缓冲寄存器 (THR)、接收缓冲寄存器 (RBR)、线路控制寄存器 (LCR)、中断使能寄存器 (IER) 等。驱动程序通过写入 THR 发送数据,通过读取 RBR 接收数据,通过配置 LCR 设置波特率和数据格式。所有这些操作都是普通的内存读写,但地址对应的是硬件寄存器。

从工程角度看,MMIO 的优势在于简单高效。CPU 使用标准的加载存储指令即可访问设备,无需特殊的总线协议。但 MMIO 也有局限性,设备寄存器的地址空间是固定的,不支持热插拔,每个设备都需要在编译时确定地址映射。

GPIO 通信 ​

GPIO 是最简单的通信接口,每个引脚可以配置为输入或输出模式。在输出模式下,CPU 通过设置寄存器来改变引脚电平;在输入模式下,CPU 通过读取寄存器来获取引脚状态。GPIO 虽然简单,但应用广泛。

GPIO 的典型应用包括 LED 控制、按键检测、芯片选择、中断触发等。对于 SPI 设备,GPIO 可以作为片选信号,CPU 拉低特定 GPIO 引脚来选择要通信的设备。对于 I2C 设备,虽然协议层使用开漏输出,但硬件实现上仍然是通过 GPIO 控制数据线。

GPIO 通信的特点是低延迟、低开销。一次 GPIO 写操作只需要几十纳秒,远快于总线协议的请求响应周期。但 GPIO 的带宽有限,每次只能传输一位信息,不适合高速数据传输。

中断机制 ​

平台设备的中断通常通过 GPIO 中断或专用中断线实现。当设备需要通知 CPU 处理某个事件时,它会触发一个中断信号,CPU 收到信号后跳转到中断处理程序。

GPIO 中断可以配置为边沿触发或电平触发。边沿触发在引脚电平变化时产生中断,适合检测按键按下、传感器状态变化等事件。电平触发在引脚保持特定电平时持续产生中断,适合处理需要持续关注的状态。

从软件角度看,中断处理分为顶半部 (Top Half) 和底半部 (Bottom Half)。顶半部在中断上下文中执行,做最少的工作,如读取状态寄存器、清除中断标志。底半部在进程上下文中执行,处理耗时的操作,如数据拷贝、协议处理。这种分离设计确保了中断处理的实时性和系统的响应性。

GPIO ​

GPIO (General Purpose Input/Output) 是最简单的硬件接口之一,每个 GPIO 引脚可以配置为输入或输出模式。在输入模式下,软件可以读取引脚的电平状态 (高或低);在输出模式下,软件可以设置引脚的电平状态。GPIO 虽然简单,但应用极其广泛,从 LED 控制、按键检测到芯片选择、中断触发,GPIO 是嵌入式开发的基石。

在嵌入式 SoC 上,GPIO 通常以组的形式存在,每组有 32 个或更少的引脚。GPIO 控制器通过寄存器映射到内存地址空间,软件通过读写这些寄存器来控制 GPIO。典型的 GPIO 寄存器包括数据寄存器 (读/写引脚状态)、方向寄存器 (配置输入/输出)、中断配置寄存器 (配置中断触发条件) 等。

从硬件角度看,GPIO 引脚往往支持多种功能。通过引脚复用 (Pin Muxing),同一个物理引脚可以被配置为 GPIO、UART、SPI、I2C 等不同功能。引脚复用配置通常在设备树中描述,由内核的 pinctrl 子系统管理。这种灵活性使得同一个芯片可以适应不同的应用场景,但也增加了配置的复杂性。

Linux 提供了统一的 GPIO 子系统,支持通过设备树或 ACPI 描述 GPIO,提供 sysfs 接口和字符设备接口供用户空间访问。开发者可以通过 /sys/class/gpio 目录控制 GPIO,也可以使用 libgpiod 库进行更高级的操作。在驱动开发中,GPIO 可以通过设备树引用,实现硬件配置的解耦。

UART ​

UART (Universal Asynchronous Receiver/Transmitter) 是一种通用的异步串行通信接口,广泛用于嵌入式系统的控制台、调试输出、低速数据传输等场景。UART 只需要两根信号线 (TX 和 RX) 即可实现全双工通信,硬件实现简单,可靠性高。

UART 的通信参数包括波特率、数据位、停止位、校验位。波特率定义了每秒传输的符号数,常见值有 9600、115200、921600 等。数据位通常是 8 位,停止位通常是 1 位,校验位用于检测传输错误。通信双方必须使用相同的参数配置,否则无法正确解析数据。

UART 是异步通信,不需要时钟信号。接收端通过采样波特率的整数倍来恢复数据,典型做法是采样 16 倍波特率的时钟,在数据位的中间位置采样,以提高抗干扰能力。这种异步特性使得 UART 适合远距离通信,但传输速率受到限制。

在嵌入式系统中,UART 通常用作控制台接口。内核通过串口输出启动日志,开发者可以通过串口登录系统进行调试。UART 也常用于连接 GPS 模块、蓝牙模块、传感器等外设,这些设备通常使用简单的串行协议进行通信。

Linux 的 UART 驱动框架分为 tty 核心层、tty 线路规程层和 UART 驱动层。tty 核心层提供统一的字符设备接口,应用层通过 /dev/ttySx 或 /dev/ttyUSEx 设备文件访问串口。线路规程层负责 termios 参数的处理,如波特率设置、原始模式 vs 规范模式。UART 驱动层负责具体的硬件操作,包括中断处理、DMA 传输等。

从工程实践来看,UART 的可靠性很高,但带宽有限。115200 波特率的理论吞吐量约为 14KB/s,这在低速数据传输场景下足够,但无法满足大数据量传输的需求。现代 SoC 往往提供多个 UART 控制器,有些支持 DMA 传输和硬件流控,可以进一步提高性能。