2、嵌入式系统基础:处理器架构、内存映射、外设寄存器、中断与DMA的基本概念

嵌入式设备驱动开发的本质,是让软件能够正确、高效地控制硬件资源。在编写任何一行驱动代码之前,开发者必须深刻理解处理器如何“看待”这个世界:CPU核心通过何种路径访问外设?内存地址空间如何布局?外设寄存器与普通内存有何区别?中断与DMA如何协同工作以提升系统实时性?本章将围绕这四个核心主题展开,为后续的驱动框架设计、设备树编写和底层操作函数实现奠定坚实的理论基础。

嵌入式系统硬件抽象全景图 CPU 核心 ARM Cortex-A/R/M RISC-V / X86 总线互联矩阵 AXI / AHB / APB OCP / NoC 内存子系统 DDR4/LPDDR4 SRAM / Flash映射区 外设寄存器组 UART / SPI / I2C GPIO / Timer / DMA 取指/数据 读/写 MMIO 访问 中断控制器 GIC / NVIC / PLIC DMA 控制器 多通道 / 描述符链 外设功能块 FIFO / 状态机 / 引擎 IRQ/FIQ 中断请求 DMA 请求/应答 数据搬运 内存访问 典型 32 位地址空间布局(以 ARM Cortex-A9 为例) 0x0000_0000 - 0x0FFF_FFFF DDR 内存(256MB) 0x1000_0000 - 0x1FFF_FFFF 外设 I/O 映射区(MMIO) 0x2000_0000 - 0x2FFF_FFFF SRAM / 片上存储 0xE000_0000 - 0xFFFF_FFFF 系统控制 / 调试接口

2.1 处理器架构与驱动开发的关联

核心原理:指令集与编程模型

嵌入式处理器架构决定了驱动代码的编译方式、内存访问规则和中断响应机制。目前主流架构包括 ARM Cortex-A(应用处理器)、Cortex-R(实时处理器)、Cortex-M(微控制器)以及 RISC-V。从驱动开发视角看,最关键的差异在于:内存管理单元(MMU)的有无、中断延迟的确定性、以及寄存器访问的指令开销。Cortex-A 系列通常运行 Linux,依赖 MMU 实现虚拟地址映射;Cortex-M 系列则直接访问物理地址,中断延迟可低至 12 个时钟周期。

驱动开发者的核心关注点:

  • 字节序(Endianness):ARM 支持大小端切换,但外设寄存器通常固定为小端模式,驱动中需使用 readl/writel 等访问宏保证一致性。
  • 内存屏障(Memory Barrier):CPU 可能重排内存访问指令,驱动中操作 DMA 描述符或外设寄存器后必须插入 dmb() 或 dsb() 指令。
  • 缓存一致性:DMA 访问的内存区域需要标记为 DMA_ATTR_NON_CONSISTENT 或使用 dma_alloc_coherent 分配一致性内存。

判断方法:选择正确的访问模型

在编写驱动前,需要判断目标外设属于哪种访问类型:

  1. PIO(Programmed I/O)模式:CPU 直接读写寄存器,适用于低速外设(如 GPIO、UART 配置寄存器)。
  2. MMIO(Memory-Mapped I/O)模式:外设寄存器被映射到内存地址空间,使用普通内存访问指令操作,这是绝大多数 SoC 采用的方式。
  3. Port I/O 模式:x86 架构特有,使用 in/out 指令,在嵌入式 Linux 中已较少使用。

案例拆解:Cortex-A9 与 Cortex-M7 的驱动差异

假设需要操作同一个 UART IP 核,在 Cortex-A9(Linux 环境)下,驱动通过 ioremap 将物理地址映射为虚拟地址,再使用 writel 写入寄存器;而在 Cortex-M7(裸机或 RTOS 环境)下,直接使用 volatile 指针访问物理地址。两者的核心区别在于:A9 需要处理页表映射和缓存一致性,而 M7 则无需考虑 MMU,但需要手动管理编译器优化带来的乱序访问问题。

常见误区:初学者容易在 Cortex-M 上使用 volatile 关键字后便认为万事大吉,但 volatile 只能防止编译器优化,无法阻止 CPU 硬件乱序执行。在涉及 DMA 或双核通信时,必须使用内存屏障指令。

2.2 内存映射与地址空间布局

核心原理:物理地址与虚拟地址

现代嵌入式 SoC 内部集成了多种存储器和外设,它们被统一编址到一张物理地址映射表中。这张表由芯片厂商在参考手册中给出,是驱动开发最基础的参考资料。以典型的 32 位处理器为例,地址空间通常划分为:DDR 区间、片上 SRAM、外设寄存器区间、系统控制区间。驱动开发者必须清楚每个外设的基地址和寄存器偏移,才能正确构造访问地址。

地址区间 大小 用途 访问属性
0x0000_0000 - 0x0FFF_FFFF 256MB DDR 内存 可缓存、可写回
0x1000_0000 - 0x1FFF_FFFF 256MB 外设 MMIO 不可缓存、设备内存
0x2000_0000 - 0x2FFF_FFFF 256MB 片上 SRAM 可缓存、可写通
0xE000_0000 - 0xFFFF_FFFF 512MB 系统控制/调试 不可缓存、强序

操作流程:从物理地址到虚拟地址的映射

在 Linux 驱动中,访问外设寄存器必须遵循以下流程:

// 1. 获取外设物理基地址(来自设备树或平台数据)
#define UART_BASE 0x10000000
#define UART_DATA 0x00
#define UART_STATUS 0x04

// 2. 建立虚拟地址映射
void __iomem *base = ioremap(UART_BASE, SZ_4K);
if (!base) {
    pr_err("ioremap failed\n");
    return -ENOMEM;
}

// 3. 通过访问宏操作寄存器
u32 status = readl(base + UART_STATUS);
writel(0x01, base + UART_DATA);

// 4. 驱动卸载时释放映射
iounmap(base);

实战场景:设备树中的内存映射描述

设备树(Device Tree)是描述硬件资源的标准方式。以下是一个典型 UART 节点的内存映射描述:

uart0: serial@10000000 {
    compatible = "vendor,uart-ip";
    reg = <0x10000000 0x1000>;  // 基地址 + 地址长度
    interrupts = <0 33 4>;      // 中断控制器类型、中断号、触发方式
    clocks = <&uart_clk>;
    status = "okay";
};

驱动通过 platform_get_resource 获取 reg 属性中的地址信息,再调用 devm_ioremap_resource 完成映射。这种方式避免了硬编码地址,提高了代码的可移植性。

经验提醒:在裸机开发中,建议将外设基地址定义为宏,并统一放在 memory_map.h 头文件中。同时,为每个外设建立独立的 struct 描述寄存器布局,使用 offsetof 宏计算偏移,避免硬编码偏移值。

2.3 外设寄存器访问机制

核心原理:寄存器位域与读写属性

外设寄存器是硬件状态机的“控制面板”,每个寄存器由若干位域(Bit Field)组成,分别控制不同的功能。寄存器按读写属性可分为:只读(RO)、只写(WO)、读写(RW)、写1清0(W1C)、写1置1(W1S)。其中 W1C 类型最易出错——向该位写 1 会清除中断标志,写 0 无效。驱动开发者必须严格遵循数据手册中的位定义,否则可能导致硬件状态异常。

判断方法:如何安全地修改寄存器位域

修改寄存器位域时,必须采用“读-改-写”序列,避免影响其他位:

// 错误示例:直接赋值会覆盖其他位
writel(0x01, base + UART_CTRL);

// 正确示例:读-改-写
u32 ctrl = readl(base + UART_CTRL);
ctrl |= (1 << 3);      // 设置 bit3
ctrl &= ~(0x3 << 4);  // 清除 bit4-5
writel(ctrl, base + UART_CTRL);

案例拆解:UART 状态寄存器轮询

以 16550 UART 的 LSR(Line Status Register)为例,bit5 为发送保持寄存器空标志(THRE),bit0 为接收数据就绪标志(DR)。驱动发送数据时,需要轮询 THRE 位;接收数据时,需要检查 DR 位。若使用中断方式,则需在 ISR 中读取 IIR(中断标识寄存器)判断中断源,并读取 LSR 清除错误状态。

风险提示:某些寄存器(如中断状态寄存器)在读取后会自动清除标志位。如果驱动在读取后未及时处理数据,可能导致中断丢失。务必参考数据手册中的“读副作用”说明。

2.4 中断与DMA:高效数据通路

核心原理:中断优先级与嵌套

中断机制允许外设在事件发生时主动通知 CPU,避免轮询浪费 CPU 周期。嵌入式系统中,中断控制器(如 GIC、NVIC)负责管理多个中断源的优先级、使能状态和挂起状态。驱动开发者需要理解:中断上下文与进程上下文的区别——中断处理函数不能睡眠,不能调用可能阻塞的函数(如 mutex_lock、kmalloc(GFP_KERNEL))。

操作流程:中断驱动的标准注册流程

// 1. 申请中断号(来自设备树)
int irq = platform_get_irq(pdev, 0);
if (irq < 0) return irq;

// 2. 注册中断处理函数
request_irq(irq, uart_isr,
            IRQF_TRIGGER_RISING,
            "my_uart", dev);

// 3. 中断处理函数(上半部)
static irqreturn_t uart_isr(int irq, void *data) {
    struct uart_dev *dev = data;
    u32 status = readl(dev->base + UART_STATUS);
    if (status & RX_READY) {
        // 读取数据到缓冲区
        dev->rx_buf[dev->rx_cnt++] = readl(dev->base + UART_DATA);
    }
    // 清除中断标志(W1C)
    writel(status & RX_READY, dev->base + UART_STATUS);
    return IRQ_HANDLED;
}

DMA 工作原理与驱动集成

DMA(Direct Memory Access)控制器可以在无需 CPU 干预的情况下,在外设 FIFO 与内存之间搬运数据。驱动开发中,DMA 的使用分为三步:分配 DMA 缓冲区、配置 DMA 描述符、启动传输并等待完成中断。以 Linux 内核的 dmaengine 框架为例:

// 1. 获取 DMA 通道
struct dma_chan *chan = dma_request_chan(dev, "rx");

// 2. 准备传输描述符
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_single(chan, buf_phys,
                                   len, DMA_DEV_TO_MEM,
                                   DMA_PREP_INTERRUPT);

// 3. 注册回调并提交
desc->callback = dma_rx_callback;
dmaengine_submit(desc);
dma_async_issue_pending(chan);

案例拆解:高速 ADC 数据采集系统

某工业数据采集系统使用 ADC 以 1MSPS 采样率连续采集模拟信号,每次采样产生 16 位数据。若采用中断方式,CPU 每秒需处理 100 万次中断,几乎耗尽全部性能。改用 DMA 后,ADC 每次采样完成将数据直接写入内存环形缓冲区,每满 1024 个样本触发一次 DMA 完成中断,CPU 只需每秒处理约 1000 次中断,负载降低 99.9%。

指标 中断模式 DMA 模式
CPU 中断频率 1 MHz ~1 kHz
CPU 占用率 85% 5%
数据丢失风险 高(中断延迟抖动) 低(硬件搬运)
实现复杂度 低 中

复盘经验:DMA 描述符链(Descriptor Chain)支持连续传输多个不连续的内存块,适用于网络包或音频帧的分散-聚集 I/O。但描述符本身必须存放在一致性内存中,且更新描述符后需要执行内存屏障,确保 DMA 控制器看到最新数据。

2.5 常见误区与设计复盘

误区一:忽视缓存一致性

当 DMA 写入内存后,CPU 读取该内存时可能命中 Cache 中的旧数据。解决方法是使用 dma_alloc_coherent 分配一致性内存,或在每次 DMA 传输前后调用 dma_map_single 配合 dma_sync_single_for_cpu 进行同步。

误区二:中断处理函数中执行耗时操作

中断处理函数应遵循“快进快出”原则,仅完成数据读取和标志清除,耗时操作(如协议解析、数据拷贝)应放到工作队列或 tasklet 中执行。否则会阻塞其他中断,导致系统实时性下降。

误区三:寄存器地址硬编码

在多平台复用或 SoC 版本升级时,硬编码地址会导致驱动无法移植。应始终通过设备树或平台数据获取地址信息,并使用 devm_ioremap_resource 进行资源管理。

设计复盘清单:

  • 是否明确了外设寄存器的读写属性和位域含义?
  • 是否考虑了 MMU 和 Cache 对 DMA 传输的影响?
  • 中断处理函数是否足够短小?是否需要下半部机制?
  • DMA 描述符是否存放在一致性内存中?是否执行了内存屏障?
  • 地址信息是否来自设备树而非硬编码?

掌握处理器架构、内存映射、寄存器访问和中断/DMA 机制,是驱动开发的基本功。本章内容为后续的设备树解析、字符设备驱动编写、中断子系统集成和 DMA 引擎使用打下了坚实基础。在实际项目中,开发者应始终以芯片参考手册为最终依据,结合内核框架的抽象接口,编写出健壮、可移植的驱动程序。