国产ZYNQ双核通信实战:从零搭建AMP环境与SGI中断对话系统
在嵌入式开发领域,多核处理器的协同工作一直是提升系统性能的关键。国产ZYNQ系列芯片凭借其灵活的可编程逻辑和强大的ARM多核架构,为开发者提供了丰富的设计可能性。本文将带您一步步实现ZYNQ芯片上两个ARM核心之间的"对话"——通过AMP(非对称多处理)架构和SGI(软件生成中断)机制建立可靠的核间通信。
1. 环境准备与工程创建
开始之前,确保您已准备好以下工具和环境:
- 国产ZYNQ开发板(如紫光同创、复旦微等)
- Vivado设计套件(版本2018.3或更高)
- SDK开发环境
- USB转串口调试工具
创建独立的SDK工程是AMP开发的第一步。不同于对称多处理(SMP),AMP要求为每个核心创建单独的可执行文件。具体操作如下:
- 打开Vivado并完成基本的硬件设计(包括ZYNQ处理器的配置)
- 导出硬件平台到SDK
- 在SDK中创建两个独立的Application Project:
- 第一个工程命名为
CPU0_APP,处理器选择ps7_cortexa9_0 - 第二个工程命名为
CPU1_APP,处理器选择ps7_cortexa9_1
- 第一个工程命名为
- 两个工程都选择"Hello World"模板作为起点
注意:务必确认每个工程关联到正确的CPU核心,这是后续通信能正常工作的基础。
2. 内存配置与链接脚本修改
默认的链接脚本可能不适合AMP场景,我们需要手动调整以确保两个核心的程序能正确加载到DDR内存中。以下是关键修改步骤:
2.1 修改icf链接文件
对于CPU0_APP工程:
- 找到
lscript.ld文件(或对应的icf文件) - 修改内存区域定义,确保代码段(.text)和数据段(.data)位于DDR中
- 为堆栈分配足够空间,建议至少16KB
/* CPU0内存布局示例 */ MEMORY { ps7_ddr_0_S_AXI_BASEADDR : ORIGIN = 0x00100000, LENGTH = 0x1FF00000 ps7_ram_0_S_AXI_BASEADDR : ORIGIN = 0x00000000, LENGTH = 0x00030000 ps7_ram_1_S_AXI_BASEADDR : ORIGIN = 0xFFFF0000, LENGTH = 0x0000FE00 }对于CPU1_APP工程,需要确保内存区域不与CPU0冲突:
/* CPU1内存布局示例 */ MEMORY { ps7_ddr_0_S_AXI_BASEADDR : ORIGIN = 0x10100000, LENGTH = 0x1FF00000 ps7_ram_0_S_AXI_BASEADDR : ORIGIN = 0x00000000, LENGTH = 0x00030000 ps7_ram_1_S_AXI_BASEADDR : ORIGIN = 0xFFFF0000, LENGTH = 0x0000FE00 }2.2 中断栈大小调整
在AMP架构中,每个核心需要独立的中断处理能力。默认的栈大小可能不足,容易导致各种异常。建议在启动代码中增加栈大小:
#define IRQ_STACK_SIZE 0x2000 /* 8KB中断栈 */ #define FIQ_STACK_SIZE 0x1000 /* 4KB快速中断栈 */ #define SVC_STACK_SIZE 0x2000 /* 8KB管理模式栈 */3. SGI中断配置与实现
SGI(Software Generated Interrupt)是ARM架构中用于核间通信的重要机制。在ZYNQ芯片上,SGI中断号为0-15。下面我们实现CPU0和CPU1之间的双向中断通信。
3.1 中断号定义与初始化
首先在两个工程中定义统一的中断号:
/* 公共头文件或各自工程中的定义 */ #define SGI_CPU0_TO_CPU1 14 /* CPU0发给CPU1的中断号 */ #define SGI_CPU1_TO_CPU0 15 /* CPU1发给CPU0的中断号 */然后初始化GIC(通用中断控制器):
/* GIC初始化代码(以CPU0为例) */ Status = FGicPs_SetupInterruptSystem(&IntcInstance); if(Status != GIC_SUCCESS) { fmsh_print("GIC初始化失败\n\r"); return GIC_FAILURE; }3.2 中断处理函数注册
为每个核心注册对应的中断处理函数:
/* CPU0的中断处理函数 */ void SGI_CPU1_Handler(void *InstancePtr) { fmsh_print("CPU0收到来自CPU1的中断!\n\r"); /* 可以在这里设置标志位或进行其他处理 */ } /* CPU1的中断处理函数 */ void SGI_CPU0_Handler(void *InstancePtr) { fmsh_print("CPU1收到来自CPU0的中断!\n\r"); /* 可以在这里设置标志位或进行其他处理 */ }注册中断到GIC:
/* CPU0注册中断 */ Status = FGicPs_Connect(&IntcInstance, SGI_CPU1_TO_CPU0, (FMSH_InterruptHandler)SGI_CPU1_Handler, &IntcInstance); /* CPU1注册中断 */ Status = FGicPs_Connect(&IntcInstance, SGI_CPU0_TO_CPU1, (FMSH_InterruptHandler)SGI_CPU0_Handler, &IntcInstance);3.3 中断触发与测试
在两个核心的主循环中,我们可以定期触发中断来测试通信:
/* CPU0触发中断给CPU1 */ FGicPs_SoftwareIntr(&IntcInstance, SGI_CPU0_TO_CPU1, (1<<1)); /* CPU1触发中断给CPU0 */ FGicPs_SoftwareIntr(&IntcInstance, SGI_CPU1_TO_CPU0, (1<<0));4. 系统固化与调试技巧
完成代码开发后,需要将程序固化到开发板中进行测试。
4.1 生成启动文件
- 为每个核心生成独立的ELF可执行文件
- 创建包含两个程序的启动镜像(BOOT.BIN)
- 在镜像中指定每个程序运行的CPU核心
4.2 常见问题排查
在实际开发中,可能会遇到以下问题及解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 系统启动后无输出 | 程序未正确加载到指定核心 | 检查启动镜像配置,确认每个程序关联到正确的CPU |
| 中断不触发 | GIC配置错误或中断号冲突 | 确认两个核心使用不同的SGI中断号,检查GIC初始化代码 |
| 系统随机崩溃 | 栈大小不足或内存冲突 | 增加栈大小,检查链接脚本确保内存区域不重叠 |
| 通信不稳定 | 中断处理耗时过长 | 优化中断处理函数,避免复杂操作 |
4.3 高级应用:共享内存通信
除了中断通知外,双核之间通常还需要共享数据。可以使用片上内存(OCM)作为共享区域:
- 在Vivado中预留OCM空间(通常为256KB)
- 在链接脚本中定义共享区域
- 通过volatile指针访问共享内存
- 配合SGI中断实现高效的数据交换
/* 共享内存定义示例 */ #define SHARED_MEM_BASE 0xFFFF0000 volatile uint32_t *shared_data = (uint32_t *)SHARED_MEM_BASE;在实际项目中,我曾遇到一个有趣的调试案例:双核通信时偶尔会丢失中断。经过排查发现是因为中断处理函数中进行了耗时操作,导致后续中断被淹没。解决方案是简化中断处理,仅设置标志位,主循环中处理实际任务。这种"中断+轮询"的混合模式在嵌入式开发中非常实用。