概述
ZYNQ是片SOC,将PL(FPGA)作为A9处理器的异构扩展。但因为习惯问题,总是在PL中完成大部分工作,将PS作为算力稍强的单片机使用。完成简单的配置功能和调度任务是够够的,但是总感觉很浪费,想进一步开发以下。这时就会发现,其实A9能够提供的算力也是差强人意,想要充分发挥zynq的性能,肯定是将两个A9核都利用起来。
参考文档
XAPP1079
UG585
UG821
PS启动过程
阶段0-运行BootROM中的启动代码
- BootROM代码运行于CPU0中,CPU1执行“等待事件”(WFE)指令。
- 启动代码配置CPU0和必要的外设来加载FSBL启动代码到OCM。
- 加载到OCM中的FSBL代码限制为192KB。
阶段1-执行FSBL启动代码
- FSBL总是运行在CPU0上,其将第二阶段启动代码或裸机应用(当镜像中包含两个个ELF文件时,两个ELF文件都在此阶段被加载)加载到DDR。FSBL会禁用缓存和MMU。
- 配置PL的bit文件在此阶段不是必须的,PS可以在不配置PL的情况下独立使用。可以自定义FSBL代码来使用其他PS外设(Ethernet/USB/STDIO)用于启动或者配置PL。
- 此时可以使用整个OCM的256KB。可以通过FSBL加载eMMC中的代码,需要将启动模式设置为QSPI,并将FSBL存储于QSPI中。FSBL加载第二阶段启动代码或裸机应用到DDR。
阶段2-用户程序
运行CPU0上的用户程序,在用户程序中启动CPU1,运行CPU1上的用户程序。
最小系统
官方示例中包含了不少功能,除了打印信息的串口外,还用到了中断、两片CPU间数据交换等。
为了直观了解问题的本质,我通常会从最小系统开始,仅包含必要的功能,排除其他信息的干扰。在多核开发中,我们的最小系统只要求CPU0和CPU1都在运行程序就可以了。
CPU0的启动是自然而然的,CPU1却需要我们从WFE状态唤醒,《UG585 —— 6.1.10 Starting Code on CPU 1》中表述如下:
- 将CPU1加载至DDR中的地址写入地址0xFFFFFFF0
- 执行SEV指令唤醒CPU1
步骤一
步骤一有两个要素,一个是CPU1应用程序加载地址,一个保存应用程序加载地址的地址0xFFFFFFF0。其中CPU1应用程序加载地址可以在src目录下的《lscript.ld》文件中设置,需要注意CPU0分配地址与CPU1分配地址不能冲突;
地址0xFFFFFFF0为OCM高地址映射,需要保证在执行SEV指令前数据同步,这里我们通过直接禁用数据缓存来保证同步。
步骤二
步骤二要素为SEV指令,这是一个汇编指令,涉及到在C语言中使用汇编语言。 Xilinx的SDK使用GCC编译器,其内嵌汇编语法为__asm__ ("assembly code")。
测试代码
CPU0运行后禁用缓存,打印测试信息,然后唤醒CPU1。
/*
* CPU0代码
* 打印测试信息
* 禁用数据缓存,0xFFFFFFF0为Cache的高地址映射
* 启动CPU1
*/
void start_app_u1(void)
{
u32 *addr_of_ptr;
addr_of_ptr = (u32 *)0xFFFFFFF0;
*addr_of_ptr = 0x10000000;
usleep(1000);
__asm__("sev");
}
int main()
{
init_platform();
Xil_DCacheDisable();
sleep(3);
print("Hello World from u0\n\r");
sleep(3);
print("Start u1\n\r");
sleep(3);
start_app_u1();
while(1) {
}
}
CPU1运行后即打印测试信息。
/*
* CPU1代码
* 启动后打印测试信息
*/
int main()
{
print("Hello World from u1\n\r");
while(1) {
}
}
测试结果
总结
至此,我们将CPU0和CPU1都跑起来了。至于如何充分利用双核,不过是在此基础上添加功能罢了,通过进一步了解CPU0和CPU1的私有资源和共用资源,可以完成CPU0和CPU1在运行各自功能的情况下保持交互。