2. 硬件基础:SoC内存映射与外设寄存器物理地址空间布局

在嵌入式Linux驱动开发中,理解SoC(System on Chip)的内存映射是进行外设寄存器地址映射的前提。本章将深入剖析物理地址空间的布局规则、外设寄存器的编址方式,以及如何从芯片手册中提取关键的地址信息。

2.1 SoC内存映射的基本概念

SoC内部集成了CPU、内存控制器、各种外设控制器(如UART、I2C、SPI、GPIO、DMA等)。为了统一管理这些硬件资源,SoC设计者将物理地址空间划分为多个区域,每个区域映射到特定的硬件模块。CPU通过访问这些物理地址来读写外设的寄存器,从而控制外设的行为。

核心要点: 在ARM/ARM64、RISC-V等架构中,外设寄存器通常被映射到内存映射I/O(MMIO)区域,即CPU使用普通的访存指令(如LDR/STR)来操作外设寄存器,而非使用专用的I/O指令(如x86的IN/OUT)。

2.2 典型SoC物理地址空间布局

以广泛使用的ARM Cortex-A系列SoC为例,其物理地址空间通常按以下方式划分(具体地址范围因厂商而异):

地址范围(起始 - 结束) 区域名称 典型用途 访问属性
0x0000_0000 - 0x3FFF_FFFF DDR内存区域 系统主内存(DRAM),存放代码、堆栈、数据 可缓存、可执行
0x4000_0000 - 0x4FFF_FFFF 片上SRAM / 共享内存 快速数据交换、低延迟存储 可缓存(部分不可缓存)
0x5000_0000 - 0x5FFF_FFFF 外设寄存器区域(Peripheral) UART、I2C、SPI、GPIO、Timer等控制寄存器 不可缓存、不可预测(Device memory)
0x6000_0000 - 0x6FFF_FFFF 显示控制器 / GPU区域 Framebuffer、显示引擎寄存器 不可缓存或写合并
0x7000_0000 - 0x7FFF_FFFF 系统控制 / 调试区域 中断控制器(GIC)、系统定时器、调试接口 不可缓存
关键约束: 外设寄存器区域必须使用Device内存属性进行映射。这意味着CPU对该区域的访问不会被缓存、不会进行指令重排(严格顺序),以保证每次读写都直接作用于硬件寄存器,避免因缓存一致性导致的外设控制失效。

2.3 外设寄存器地址空间结构

每个外设控制器内部包含一组连续的寄存器,这些寄存器在物理地址空间中占据一个基地址(Base Address)加上偏移量(Offset)的区间。驱动开发者需要从芯片手册中获取以下信息:

  • 基地址(Base Address): 外设寄存器区域的起始物理地址,由SoC设计时固定。
  • 寄存器偏移(Register Offset): 每个功能寄存器相对于基地址的偏移量。
  • 寄存器宽度(Register Width): 通常是32位(4字节),也可能是8位或16位。
  • 地址对齐要求: 外设寄存器通常要求按宽度对齐访问(如32位寄存器必须4字节对齐)。

以下是一个典型的UART外设寄存器布局示例:

偏移地址 寄存器名称 宽度 功能描述
0x000 UART_DR 32位 数据寄存器(发送/接收)
0x004 UART_RSR 32位 接收状态寄存器
0x008 UART_FR 32位 标志寄存器(TX/RX FIFO状态)
0x00C UART_IBRD 32位 整数波特率除数
0x010 UART_FBRD 32位 小数波特率除数
0x014 UART_LCR_H 32位 线路控制寄存器(数据位、停止位、校验)
0x018 UART_CR 32位 控制寄存器(使能、中断、DMA)

2.4 外设寄存器地址映射流程(SVG流程图)

从芯片手册到设备树配置,外设寄存器地址映射遵循以下标准化流程:

外设寄存器地址映射流程 外设寄存器地址映射流程 ① 查阅芯片手册 ② 提取基地址 + 寄存器偏移表 ③ 确定内存属性(Device / Strongly Ordered) ④ 编写设备树节点(reg属性) ⑤ 驱动解析reg → ioremap()映射 ⑥ 通过虚拟地址读写寄存器 📘 说明 基地址由SoC设计固定, 寄存器偏移由外设IP核定义。 两者结合构成完整物理地址。 ⚠️ 常见错误 将外设区域映射为 Normal Cacheable属性, 导致寄存器读写被缓存。

2.5 设备树中reg属性的编码规则

在设备树(Device Tree)中,外设的物理地址信息通过reg属性描述。reg属性是一个地址/长度对的数组,每个对描述一个连续的物理地址区域。

// 设备树reg属性语法
uart0: serial@101f1000 {
    compatible = "arm,pl011";
    reg = <0x101f1000 0x1000>;
    // 解释:基地址 = 0x101F1000,区域大小 = 0x1000(4KB)
    interrupts = <0 12 4>;
    clock-frequency = <24000000>;
};
编码规则:
  • 第一个值: 外设寄存器的起始物理地址(基地址)。
  • 第二个值: 该外设占用的地址空间大小(以字节为单位)。通常设置为寄存器覆盖范围,或按页对齐(4KB)。
  • 多区域外设: 如果外设包含多个不连续的地址块(如控制寄存器区和数据缓冲区),可以包含多对地址/长度。

2.6 多区域外设的reg属性示例

某些复杂外设(如DMA控制器、GPU)可能拥有多个物理地址区域,例如控制寄存器区和描述符表区。设备树通过多对reg值来描述:

// 多区域外设示例:DMA控制器
dma0: dma-controller@30000000 {
    compatible = "vendor,dma-controller";
    reg = <0x30000000 0x1000>,  // 控制寄存器区(4KB)
          <0x30001000 0x2000>;  // 描述符表区(8KB)
    interrupts = <0 23 4>;
    #dma-cells = <1>;
};

驱动中通过platform_get_resource()或of_iomap()按索引获取对应的物理地址区域:

// 内核驱动中获取reg区域
struct resource *res;
void __iomem *ctrl_base;
void __iomem *desc_base;

// 获取第一个reg区域(索引0)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
ctrl_base = devm_ioremap_resource(&pdev->dev, res);

// 获取第二个reg区域(索引1)
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
desc_base = devm_ioremap_resource(&pdev->dev, res);

2.7 地址映射中的对齐与页边界

内核的ioremap()函数以页(Page)为单位进行地址映射。这意味着即使外设寄存器只占用4字节,内核也会映射至少一个完整的页(通常是4KB)。因此,设备树中reg的大小应至少覆盖到下一个页边界,以避免访问越界。

外设寄存器范围 推荐reg大小 映射后虚拟地址范围 说明
0x101F1000 - 0x101F100C 0x1000(4KB) 0x101F1000 - 0x101F1FFF 按页对齐,避免后续扩展越界
0x30000000 - 0x30000FFF 0x1000(4KB) 0x30000000 - 0x30000FFF 正好一个页
0x40000000 - 0x400000FF 0x1000(4KB) 0x40000000 - 0x40000FFF 即使只有256字节,也映射4KB
常见陷阱: 如果设备树中reg的大小设置过小(例如只写0x10),而实际外设寄存器访问范围超过该大小,驱动在访问高偏移寄存器时可能触发页错误(Page Fault)或访问到其他外设的地址空间。始终将大小设置为覆盖所有寄存器并向上对齐到页边界。

2.8 从芯片手册到设备树的实战映射

以下是一个从芯片手册提取信息并编写设备树节点的完整示例:

  1. 查阅手册: 找到UART外设的基地址为0x101F1000,寄存器偏移范围0x000 - 0x0FF。
  2. 确定大小: 寄存器覆盖256字节,按4KB页对齐,取0x1000。
  3. 编写设备树:
// 最终设备树节点
uart0: serial@101f1000 {
    compatible = "arm,pl011";
    reg = <0x101f1000 0x1000>;
    interrupts = <0 12 4>;
    clocks = <&uart_clk>;
    status = "okay";
};
  1. 驱动解析: 内核驱动通过of_iomap()将物理地址映射到内核虚拟地址空间,然后通过readl()/writel()访问寄存器。
// 驱动中映射并访问寄存器
static int uart_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    void __iomem *base;

    // 从设备树获取reg并映射
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 通过偏移访问寄存器
    writel(0x01, base + UART_CR);   // 使能UART
    val = readl(base + UART_FR);    // 读取状态标志

    return 0;
}

2.9 本章小结

  • SoC物理地址空间将外设寄存器映射到特定的MMIO区域,CPU通过访存指令操作外设。
  • 外设寄存器区域必须使用Device内存属性映射,禁止缓存和重排。
  • 设备树reg属性由基地址 + 大小组成,大小应覆盖所有寄存器并对齐到页边界。
  • 多区域外设使用多对reg值,驱动按索引分别映射。
  • 从芯片手册到设备树的映射流程是驱动移植的第一步,准确获取基地址和偏移是后续工作的基础。