4. ATF核心组件

好,咱们进入第四章。这一章是实战味道最浓的一节。说白了,ATF 能不能在你的板子上跑起来,全看这步做得怎么样。我见过太多人,代码编译过了,一烧进去就黑屏,连个串口打印都没有。嗯,多半是平台适配没做对。

ATF 的设计思路很清晰:它把「芯片通用逻辑」和「板级特定逻辑」分开了。你只需要关心 plat/ 目录下属于你的那一亩三分地。今天我就带你把这三分地犁清楚。

4.1 平台目录结构:先找到你的战场

ATF 的代码仓库里,plat/ 目录长这样:

plat/
├── arm/                # ARM 官方参考平台
│   ├── board/
│   └── common/
├── hisilicon/          # 海思平台
├── mediatek/           # 联发科平台
├── nvidia/             # 英伟达平台
├── rockchip/           # 瑞芯微平台
├── socionext/          # 索喜平台
├── stm32mp/            # ST 平台
└── <your_company>/     # 你的平台放这里

我个人习惯,在 plat/ 下新建一个以公司或芯片命名的文件夹。比如 plat/mychip/。里面再分:

plat/mychip/
├── common/             # 本系列芯片通用代码
│   ├── mychip_bl31_setup.c
│   ├── mychip_gic.c
│   └── mychip_pm.c
├── include/            # 头文件
│   └── platform_def.h  # 核心宏定义
├── mychip_board1/      # 具体板级
│   ├── board.mk
│   ├── plat_mem_map.h  # 内存映射
│   └── plat_uart.c     # UART 驱动
└── mychip_board2/
我的经验: 别把所有东西塞到一个目录里。芯片级和板级分开,以后换板子只需要改 board 目录,芯片公共代码不用动。我在一个项目里吃过亏,把内存映射和芯片配置写在一起,结果换了 DDR 颗粒,改得我头皮发麻。

4.2 平台宏定义:platform_def.h 是命根子

这个文件,是整个平台适配的「总开关」。ATF 编译时,大量条件编译依赖这里的宏。写错了,轻则编译不过,重则运行时莫名其妙崩溃。

核心宏定义清单:

宏名称 含义 典型值
PLAT_PRIMARY_CPU 主 CPU 的 MPIDR 0x0
PLATFORM_CORE_COUNT 总核数 4
PLATFORM_CLUSTER_COUNT 簇数量 1
PLATFORM_MAX_CPUS_PER_CLUSTER 每簇最大核数 4
PLATFORM_STACK_SIZE BL31 阶段栈大小 0x1000
BL31_BASE BL31 加载基地址 0x04000000
BL31_LIMIT BL31 结束地址 0x04020000
PLAT_ARM_TRUSTED_SRAM_SIZE Trusted SRAM 大小 0x00040000
PLAT_ARM_TRUSTED_DRAM_SIZE Trusted DRAM 大小 0x00100000
我曾经踩过的坑: 有一次我把 PLATFORM_CORE_COUNT 写成了 8,但实际芯片只有 4 核。结果 ATF 在初始化 GIC 时,给不存在的 CPU 接口分配了中断向量,系统启动到一半就卡死了。查了两天才定位到。所以,核数一定要和芯片手册对清楚。

另外,platform_def.h 里还要定义一些「开关宏」,用来控制 ATF 特性:

/* 是否使用 GICv3 */
#define MYCHIP_USE_GICV3        1

/* 是否支持 CPU 热插拔 */
#define MYCHIP_SUPPORT_HOTPLUG  0

/* 是否使用安全中断 */
#define MYCHIP_SECURE_INT_SUPPORT 1

这些宏,建议你对照 ARM 参考平台(比如 plat/arm/board/fvp/)的 platform_def.h 来写。先复制一份,再逐行改成你自己的值。

4.3 内存映射配置:地址不对,一切白费

ATF 运行在安全世界,它需要知道自己能访问哪些内存区域。这部分在 plat_mem_map.h 或 plat_mem_map.c 里定义。

核心是构建一个 mmap_region 数组:

/* 内存区域结构体 */
typedef struct mmap_region {
    unsigned long long base;      /* 基地址 */
    unsigned long long size;      /* 大小 */
    unsigned int attrs;           /* 属性:读写、可执行、安全等 */
    unsigned int granularity;     /* 粒度 */
} mmap_region_t;

一个典型的内存映射配置:

const mmap_region_t plat_mmap[] = {
    /* Trusted SRAM:存放 BL31 代码和数据 */
    MAP_REGION(0x04000000, 0x00040000,
               MT_MEMORY | MT_RW | MT_SECURE | MT_EXECUTE),

    /* Trusted DRAM:存放安全世界运行时数据 */
    MAP_REGION(0x06000000, 0x00100000,
               MT_MEMORY | MT_RW | MT_SECURE),

    /* UART 外设:非安全设备,用于调试输出 */
    MAP_REGION(0x10000000, 0x00001000,
               MT_DEVICE | MT_RW | MT_NS),

    /* GIC 控制器:安全外设 */
    MAP_REGION(0x1C000000, 0x00010000,
               MT_DEVICE | MT_RW | MT_SECURE),

    /* 结束标记 */
    {0}
};
关键点: 外设地址空间一定要用 MT_DEVICE 属性,不能用 MT_MEMORY。否则 CPU 会对外设做缓存和乱序访问,UART 发出去的数据顺序都是乱的。我见过有人把 UART 映射成 MT_MEMORY,结果打印出来的字符串是「elpoH」而不是「Hello」。

内存映射的粒度也很重要。ATF 默认使用 4KB 页表粒度。如果你的外设地址范围不是 4KB 对齐,需要手动调整 granularity 字段。嗯,这里要注意,有些芯片的 UART 寄存器基地址是 0x1000 对齐的,但大小只有 0x100 字节。映射时 size 可以写 0x1000,多出来的部分会被页表覆盖,不会造成问题。

4.4 UART 驱动移植:让板子开口说话

没有串口打印,调试 ATF 就像蒙着眼睛走路。所以 UART 驱动是第一个要移植的。

ATF 对 UART 驱动的接口定义在 include/drivers/uart/ 下。你需要实现以下函数:

/* 初始化 UART */
void console_uart_register(uintptr_t base, uint32_t clock, uint32_t baud);

/* 发送一个字符 */
int console_uart_putc(int c);

/* 接收一个字符 */
int console_uart_getc(void);

/* 检查是否有数据可读 */
int console_uart_tstc(void);

以最常见的 16550 UART 为例,移植步骤:

  1. 在 plat/mychip/drivers/uart/ 下新建 mychip_uart.c
  2. 实现初始化函数:配置波特率、数据位、停止位、校验位
  3. 实现 putc:轮询发送保持寄存器为空,然后写入数据
  4. 实现 getc:轮询接收数据寄存器就绪,然后读取
  5. 在 plat_mem_map.h 中映射 UART 地址空间
  6. 在 bl31_plat_setup.c 中调用 console_uart_register()

代码示例:

/* mychip_uart.c */
#include <console.h>
#include <drivers/uart/uart_16550.h>

/* 假设 UART 基地址在 0x10000000,时钟 24MHz,波特率 115200 */
void mychip_uart_init(void)
{
    uintptr_t base = 0x10000000;
    uint32_t clock = 24000000;
    uint32_t baud = 115200;

    /* 调用 16550 驱动库的初始化函数 */
    uart_16550_init(base, clock, baud);

    /* 注册到 ATF 控制台框架 */
    console_uart_register(base, clock, baud);
}
我的建议: 如果你的芯片 UART 和 16550 不完全兼容,别硬套。自己从头写一个驱动,只要实现那四个接口就行。ATF 的 console 框架是插件式的,不挑驱动实现方式。我在一个项目里用过 Synopsys DW_apb_uart,寄存器偏移和 16550 不一样,直接抄 16550 驱动改寄存器地址就搞定了。

4.5 定时器驱动移植:让系统有「时间感」

ATF 依赖通用定时器(Generic Timer)来提供时间基准。这部分通常不需要你写驱动,因为 ARMv8-A 架构强制实现了通用定时器。你只需要在 platform_def.h 里定义系统时钟频率:

/* 系统时钟频率,单位 Hz */
#define PLAT_SYSTEM_COUNTER_FREQ    24000000

这个值必须和芯片的 system counter 频率一致。怎么查?看芯片手册的「Generic Timer」章节,或者问芯片原厂的 FAE。

我曾经犯过的错: 我把频率写成了 24MHz(24000000),但实际 system counter 是 24MHz 经过一个分频器后的值,只有 1MHz。结果 ATF 的延时函数全乱了,本来等 1 毫秒,实际等了 24 毫秒。调试时发现串口打印间隔明显不对,才追查到这里。

如果你的芯片没有 system counter(比如某些老旧的 ARMv8-A 芯片),你需要自己实现 plat_get_syscnt_freq2() 函数:

uint32_t plat_get_syscnt_freq2(void)
{
    /* 从某个寄存器读取频率,或者硬编码 */
    return 24000000;
}

4.6 本章知识体系总览

下面这张图,把平台适配的核心流程串起来了:

ATF 平台适配核心流程 ATF 平台适配核心流程 1. 平台目录结构 plat/<company>/ 2. 平台宏定义 platform_def.h 3. 内存映射配置 plat_mem_map.h 4. UART 驱动移植 console_uart_*() 5. 定时器驱动移植 PLAT_SYSTEM_COUNTER_FREQ 6. 编译 & 烧录验证 make PLAT=mychip DEBUG=1 💡 核心原则: 先让板子「开口说话」(UART),再让板子「知道时间」(定时器), 最后才是功能调试。顺序别搞反,否则出了问题你都不知道从哪查起。

4.7 避坑清单:我替你踩过的雷

最后,我把这些年遇到过的平台适配问题整理成一份清单。你照着检查,能省不少调试时间:

问题现象 可能原因 解决方案
编译报错 undefined reference 平台宏定义缺失或拼写错误 对照 ARM FVP 的 platform_def.h 逐项检查
烧录后无任何串口输出 UART 地址映射错误或未初始化 确认 plat_mem_map.h 中 UART 地址正确,检查波特率配置
串口输出乱码 UART 时钟频率或波特率不匹配 核对芯片手册的 UART 时钟源,调整 console_uart_register 参数
系统卡在 BL31 入口 内存映射未包含 BL31 代码段 检查 BL31_BASE 和 BL31_LIMIT 是否在映射区域内
延时函数不准确 PLAT_SYSTEM_COUNTER_FREQ 错误 用示波器或逻辑分析仪测量 system counter 实际频率
多核启动失败 PLATFORM_CORE_COUNT 与实际不符 核对芯片手册的 MPIDR 寄存器布局

嗯,这一章内容不少。你消化一下,把目录结构搭好,宏定义写对,内存映射配准,UART 和定时器跑通。做到这步,你的 ATF 就已经「活」了一半。下一阶段,我们就要让它真正跑起来,进入 BL31 的初始化流程了。