
简介本资源是面向嵌入式Linux驱动开发工程师与ZYNQ平台初学者的SDK级双核AMP驱动实践包聚焦ZYNQ 7010 SoC上ARM Cortex-A9双核协同运行的底层实现解决多核任务调度、核间通信及BSP适配等关键问题。压缩包共1430个文件含399个头文件.h与313个源码文件.c构成完整驱动框架55个Makefile支撑多阶段编译24个TCL脚本用于硬件平台配置另有设备树DTS、约束文件XDC、位流BIT及可执行镜像ELF等配套资源整体31.54MB。已有88人学习下载资源包含可直接导入Xilinx SDK的工程结构、libxil.a等基础库链接文件、system.bd系统级设计以及runme.bat一键运行脚本目录组织清晰覆盖从硬件抽象层到用户空间调用的全链路实现特别适合开展AMP模式下的中断同步、内存共享与核间消息传递等深度实践。1. 先想清楚dual_core_amp 到底解决什么问题1.1 AMP 和 SMP 的分界ZYNQ 7010 这颗片子最容易被忽略的资源就是内部那两个 Cortex-A9 核。很多人拿到板子第一件事是跑 Linux跑起来之后以为双核已经在工作了其实那是 SMP对称多处理——Linux 调度器把两个核当成对等资源谁有空谁干活应用层根本感知不到核心切换。AMPAsymmetric Multi-Processing非对称多处理不是这个玩法。AMP 是让两个核各自独立运行不同的程序甚至一个跑 Linux、一个跑裸机两个程序之间没有操作系统层面的关联只通过硬件机制协同。ZYNQ 的 dual_core_amp 驱动就是要把这套各自启动、各自调度、互相通信底层的 SDK 驱动链全都打通。很多第一次接触的人以为这就是在 SDK 里建两个 Application 工程各写一个 main 就完事了实际动手才发现两个核要独立启动、独立配中断、独立访问外设还得互相传数据中间的材料比想象中多得多。1.2 什么场景才值得用 AMP我接触过的项目里用 AMP 的动机基本逃不出这几类实时性隔离。Core0 跑 Linux 处理网络协议、文件系统、人机交互Core1 跑裸机程序做 1ms 级别的伺服控制、电机电流环或者高速 ADC 采集。Linux 的调度延迟在最坏情况下不可控但裸机程序的中断响应时间是确定的。安全隔离。把关键控制逻辑和业务逻辑物理隔离在两个核上即使一边崩了另一边还能继续工作这在工业设备和医疗设备里很常见。省掉双芯片方案。原来用主控 MCU Linux SoC两片器件的方案可以直接用一颗 ZYNQ 替代成本、功耗、板面积都降下来了。反过来也要泼一盆冷水如果两边任务只是要共享数据又都不需要硬实时直接用 Linux 多线程甚至 OpenAMP 的 RPMsg 可能更省事。AMP 的通信、调试、维护成本都不低别为了用双核而用双核。1.3 7010 的资源底子ZYNQ 7010 在 Zynq-7000 家族里是入门款但双核 Cortex-A9、SCU、GIC 中断控制器、私有定时器、OCM、DDR 控制器全都在。做 dual_core_amp 需要的硬件底层资源一个不少唯一的限制是逻辑资源CLB、BRAM比较少不过 AMP 主要用的是 PS 侧PL 侧通常只挂一点简单的接口逻辑所以 7010 完全够用。2. 启动链路第二个核是怎么被喊醒的2.1 上电后的默认状态ZYNQ 上电后BootROM 先接管 CPU0从启动介质QSPI、SD、JTAG读取 FSBL 并跳转执行。这时候 CPU1 在干什么它被硬件保持在 WFEWait For Event状态一直在原地等待一个 event。这是 ARM Cortex-A9 双核启动的一个基本约定CPU0 是主核负责初始化 DDR、时钟、外设CPU1 从复位开始就进入等待状态直到有人给它发一个 event 信号。这个等待-唤醒的握手动作就是 dual_core_amp 启动链路的核心。2.2 FSBL 与 image header 的约定如果走正规启动流程SD 卡或 QSPI 启动FSBL 需要知道两件事CPU0 的 app 放在哪、CPU1 的 app 放在哪。SDK 里生成 Boot Image 时BIF 文件可以这样写image: { [bootloader] zynq_fsbl.elf app_cpu0.elf [core1] app_cpu1.elf }关键就是[core1]这个属性。FSBL 在启动时解析 image header table发现某个分区标记了 core1就会把该分区的内容加载到 ELF 里指定的地址然后把 CPU1 应用的入口地址写入 BootROM 约定的释放地址0xFFFFFFF0最后执行sev指令唤醒 CPU1。CPU1 被唤醒后回到复位向量重新取指跳转到我们链接时指定的入口地址整个启动过程就完成了。整个过程对应用层完全透明——你在 main 里写代码根本不用关心自己是被谁拉起来的。2.3 手工方式在 FSBL 里塞一段启动代码有些场景不想走 [core1] 这条路比如 CPU1 的 app 还没完全调好只想在调试时手动拉起。这种时候可以直接在 FSBL 里加一段代码#define CPU1_APP_START_ADDR 0x10000000U #define CPU1_RELEASE_ADDR 0xFFFFFFF0U void start_cpu1(void) { /* 先把 CPU1 的入口地址写到 BootROM 约定位置 */ Xil_Out32(CPU1_RELEASE_ADDR, CPU1_APP_START_ADDR); dmb(); /* 保证写入对其他核可见 */ sev(); /* 发事件唤醒 CPU1 */ }这段代码的原理是CPU1 在 BootROM 阶段会不停轮询0xFFFFFFF0这个地址一旦发现内容不是 0就把它当作自己的跳转入口。所以只要确保 CPU1 的 app 已经被拷贝到0x10000000再执行上面的函数CPU1 就会跑起来。提示手工方式适合调试阶段正式发布还是建议用 [core1]让 FSBL 自动完成加载和释放少一份自定义代码就少一份风险。3. SDK 工程搭建两个 App 共用一套硬件的正确姿势3.1 工程结构规划在 Vivado 里完成 ZYNQ PS 配置、导出硬件Generate Output Products - Export Hardware打开 SDK 之后我习惯建四个工程zynq_fsblFSBL标准模板生成不需要改代码走 [core1] 路径的话。app_cpu0跑在 ps7_cortexa9_0 上的裸机程序处理主业务。app_cpu1跑在 ps7_cortexa9_1 上的裸机程序处理实时任务。一个共享的头文件目录或公共库放通信协议和地址宏定义。这里最容易犯的错是建第二个 app 的时候没注意选处理器两个工程都默认建在ps7_cortexa9_0上。新建 Application Project 的向导里Processor 下拉框一定要选ps7_cortexa9_1。3.2 BSP 的 USE_AMP 配置CPU1 工程的 Board Support Package 需要做一个关键配置在 BSP Settings 里找到 standalone 库的extra_compiler_flags加上-DUSE_AMP1。这个宏的作用是让 standalone 的启动代码和 cache 库函数以 AMP 模式编译。默认情况下裸机程序初始化时会以为自己独占整个系统会去配置 L2 Cache 控制器、SCU、GIC 等全局资源。如果 CPU0 和 CPU1 都这么干两边会互相踩踏表现就是系统启动到一半死机或者中断完全乱掉。加上-DUSE_AMP1之后CPU1 的启动代码会跳过对全局控制器的重复配置只初始化自己私有的部分。这一点不改的话后面会遇到很多匪夷所思的诡异问题。3.3 内存地址划分与链接脚本两个裸机程序不能放在同一段 DDR 地址上否则 CPU0 的代码和数据会被 CPU1 的启动过程覆盖。以常见的 1GB DDR地址从 0x00100000 开始为例我通常这样划分地址区间用途归属0x00100000 - 0x0FFFFFFFCPU0 代码、数据、堆栈Core00x10000000 - 0x1FFFFFFFCPU1 代码、数据、堆栈Core10x20000000 - 0x200FFFFF共享内存区Core0 Core10x30000000 - 0x3FFFFFFFDMA 缓冲等预留按需分配CPU0 的链接脚本lscript.ld基本不用动默认就是从ps7_ddr_0_S_AXI_BASEADDR也就是 0x00100000开始的。CPU1 的链接脚本必须改把原本指向 0x00100000 的 DDR region 改成 0x10000000MEMORY { ps7_ddr_0_S_AXI_BASEADDR : ORIGIN 0x10000000, LENGTH 0x10000000 ps7_ram_0_S_AXI_BASEADDR : ORIGIN 0x00000000, LENGTH 0x00030000 ps7_ram_1_S_AXI_BASEADDR : ORIGIN 0x00080000, LENGTH 0x00030000 }同时要确认_vector_table、栈顶_stack、堆_heap都在新的 DDR region 范围内。改完链接脚本后再看 CPU1 程序的 map 文件确保没有哪段落在 0x00100000 附近否则启动必挂。3.4 运行与调试两种模式最省事的验证方式是做成 SD 卡启动用 SDK 的 Create Boot Image 生成 BOOT.BINBIF 里带上两个 app 和 [core1]。把 BOOT.BIN 拷到 SD 卡板子拨到 SD 启动模式。上电后两个核自动跑起来串口 0 会看到两边的打印。如果是 SDK 在线调试流程会麻烦一点先把位流下载到 PL然后单独启动 CPU0 的 app接着再启动 CPU1 的 app。启动 CPU1 的时候Debug Configuration 里要注意 Reset Type 选择 No Reset否则整个系统会被重新复位CPU0 就白跑了。4. 共享内存与核间中断驱动层的通信底座4.1 共享内存区的属性设置两个核要通信第一件事是确定共享内存区。地址我们已经在第 3 章规划好了但光有地址还不够还要处理缓存一致性问题。Cortex-A9 的 L1 Cache 是每个核私有的L2 Cache 是共享的。如果没有特殊处理CPU0 往共享内存写了一个值这个值可能还留在 CPU0 的 L1 D-Cache 里CPU1 读的时候读到的是 L1 Cache 里的旧值——这就是经典的数据看不见问题。最稳妥的办法是把共享内存区配成 non-cacheable不可缓存#include xil_mmu.h #define SHM_BASE 0x20000000U #define SHM_SIZE 0x100000U void shm_init(void) { Xil_SetTlbAttributes(SHM_BASE, NORM_NON_CACHE); }Xil_SetTlbAttributes会修改 MMU 页表项把这个地址区间的内存属性改成普通非缓存。这样两个核读写共享区都是直接操作 DDR不会经过 Cache也就不存在脏数据问题。注意这个操作依赖 MMU 已经使能。SDK 的 standalone BSP 默认会开启 MMU所以直接调用即可。如果你想用 Cache 加速也可以保持 Cache 属性但每次读写后都得手动调Xil_DCacheFlushRange/Xil_DCacheInvalidateRange工程上更容易出错。4.2 一个简单的环形队列共享内存区里具体放什么我建议先从一个最朴素的环形队列开始不要一上来就整复杂协议#define SHM_BASE 0x20000000U #define RING_BUF_SIZE 4096 typedef struct { volatile uint32_t wr; volatile uint32_t rd; volatile uint8_t buf[RING_BUF_SIZE]; } shm_ring_t; #define SHM_RING ((shm_ring_t *)SHM_BASE)CPU0 作为生产者写数据int ring_write(uint8_t *data, uint32_t len) { uint32_t i; volatile shm_ring_t *ring SHM_RING; for (i 0; i len; i) { ring-buf[ring-wr % RING_BUF_SIZE] data[i]; dmb(); ring-wr (ring-wr 1) % RING_BUF_SIZE; } return 0; }CPU1 作为消费者轮询读取int ring_read(uint8_t *data, uint32_t len) { uint32_t i; volatile shm_ring_t *ring SHM_RING; for (i 0; i len; i) { if (ring-rd ring-wr) { return i; /* 没有新数据 */ } data[i] ring-buf[ring-rd % RING_BUF_SIZE]; dmb(); ring-rd (ring-rd 1) % RING_BUF_SIZE; } return len; }这里wr和rd都用了volatile并且写数据、更新索引之间加了dmb()本文还有配套的精品资源点击获取