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/
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 为例,移植步骤:
- 在
plat/mychip/drivers/uart/下新建mychip_uart.c - 实现初始化函数:配置波特率、数据位、停止位、校验位
- 实现 putc:轮询发送保持寄存器为空,然后写入数据
- 实现 getc:轮询接收数据寄存器就绪,然后读取
- 在
plat_mem_map.h中映射 UART 地址空间 - 在
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);
}
4.5 定时器驱动移植:让系统有「时间感」
ATF 依赖通用定时器(Generic Timer)来提供时间基准。这部分通常不需要你写驱动,因为 ARMv8-A 架构强制实现了通用定时器。你只需要在 platform_def.h 里定义系统时钟频率:
/* 系统时钟频率,单位 Hz */
#define PLAT_SYSTEM_COUNTER_FREQ 24000000
这个值必须和芯片的 system counter 频率一致。怎么查?看芯片手册的「Generic Timer」章节,或者问芯片原厂的 FAE。
如果你的芯片没有 system counter(比如某些老旧的 ARMv8-A 芯片),你需要自己实现 plat_get_syscnt_freq2() 函数:
uint32_t plat_get_syscnt_freq2(void)
{
/* 从某个寄存器读取频率,或者硬编码 */
return 24000000;
}
4.6 本章知识体系总览
下面这张图,把平台适配的核心流程串起来了:
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 的初始化流程了。