平台设备
那些无法自动探测(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 传输和硬件流控,可以进一步提高性能。