拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ZYNQ双核AMP裸机开发实战:从SDK驱动到BOOT.BIN

简介本资源是面向嵌入式Linux驱动开发工程师与ZYNQ平台进阶学习者的SDK级双核AMP驱动实践包聚焦ZYNQ 7010 SoC上ARM Cortex-A9双核协同运行的底层实现问题解决多核任务调度、核间通信及BSP适配等关键难点。压缩包共1430个文件含399个头文件.h定义硬件接口与数据结构、313个C源码.c实现驱动核心逻辑含初始化、中断处理、核间同步、55个Makefile支撑交叉编译流程以及设备树.dts、TCL脚本.tcl、XDC约束.xdc等配套工程文件整体体积31.54MB。已有88人下载学习资源包含完整可编译工程结构、libxil.a等Xilinx官方库链接支持、system.bd系统级设计文件及runme.bat一键执行脚本特别适合需要快速构建双核AMP环境、理解SDK驱动开发全流程并复现核间通信机制的开发者。 上周刚把一块ZYNQ 7010板子上的dual_core_amp项目收了尾趁热把这个过程整理出来。这阵子不少做ZYNQ的朋友都在问AMP怎么做、Xilinx SDK里如何给两个核分别配驱动、CPU1怎么启动、共享内存怎么保证一致我干脆把这一版完整方案写出来从需求拆解到SDK驱动编写再到BOOT.BIN烧写一步一步讲清楚。如果你正准备在ZYNQ上做裸机双核AMP这篇文章应该能帮你少踩几个坑。先交代一下背景。板子是一块国产ZYNQ 7010开发板PS侧挂512MB DDR3PL侧加了AXI GPIO和一个UART Lite。需求很简单一个核负责通信和主控另一个核专门响应PL侧的高速中断并做数据采集两个核之间通过一段共享内存传状态和命令。为什么不直接上Linux因为裸机方案开发周期短、行为可控也没有复杂驱动栈对资源紧张的项目来说非常合适。1. AMP方案设计与整体架构拆解1.1 为什么不能用SMP而要选AMP刚开始接触ZYNQ双核的人第一反应通常是“A9不是双核吗直接上SMP跑Linux多线程不就行了”这话在应用层没错但在我们需要两个核完全独立、互不干扰、各自实时响应的场景下SMP反而不好做。SMP模式下Linux会把两个CPU核心统一调度线程跑在哪个核上由内核决定我们很难保证高优先级的中断处理和数据采集任务永远被CPU1执行。更重要的是如果裸机外设驱动被两个核同时访问需要大量锁机制开发复杂度直接翻倍。AMP则是“非对称多处理”两个核各自运行独立程序可以一个跑裸机、一个跑RTOS甚至两个都跑裸机资源边界在编译链接阶段就已经固定下来。对比项SMPAMP核间关系共享OS统一调度各自独立运行互不干扰实时性受调度器影响核独占外设响应确定资源隔离依赖驱动和Linux机制编译期强制隔离简单可靠开发难度需要内核/驱动知识裸机开发即可适用场景通用应用、业务复杂电机控制、数据采集、通信转发这次项目里CPU0要跑UART通信和命令解析CPU1要盯PL侧的中断并搬运数据两者实时性要求都不低。用AMP把这两个任务物理隔开代码写起来直观调试也省心。所以选AMP不是跟风而是按需求倒推出来的结论。1.2 总体架构与资源划分定了AMP之后第一件事就是把芯片资源切成两块。我把PS侧资源做了明确划分CPU0运行主控程序占用UART1作为调试和命令串口初始化GIC负责系统启动和CPU1的启动引导。CPU1运行数据采集程序占用PL侧UART Lite作为独立日志串口响应PL GPIO中断把采集结果写入共享内存。共享内存DDR高端划出64KB区域由两个核共同访问用于传递控制命令、状态标志和数据块地址。内存布局参考如下起始地址大小归属用途0x00100000256MBCPU0主控程序运行区、堆栈0x10000000240MBCPU1采集程序运行区、堆栈0x1F00000064KB共享核间共享内存0x1F010000其余保留预留扩展这里最容易被忽略的是DDR起始地址。ZYNQ的CPU虽然能访问以0x00000000开头的一段地址但最低1MB被BootROM和OCM占用所以用户DDR区域实际从0x00100000开始。做链接脚本时如果直接从0x00000000拉地址轻则FSBL加载异常重则上电就跑飞。1.3 双核启动链路设计AMP能不能跑起来关键看CPU1怎么从复位状态进入用户程序。ZYNQ上电后BootROM只负责引导CPU0CPU1默认停在WFE等待状态。要让它跑起来必须有人把CPU1的入口地址写到0xFFFFFFF0这个特殊寄存器然后发送SEV事件唤醒。启动链路我分成了两条路径路径一FSBL自动启动CPU1。在生成BOOT.BIN时把CPU1的elf设置为Destination CPU为A9_1FSBL在加载完镜像后会自动执行上述操作。这是最省事的方式。路径二CPU0在应用里手动启动CPU1。适用于JTAG调试或FSBL版本太老的情况代码里直接写0xFFFFFFF0并触发SEV。具体到本次项目我走的是路径一但在代码里保留了路径二的备用函数。这样量产用BOOT.BIN启动调试时可以直接从SDK里Launch两条路都通。在共享内存里我还定义了一套简单的握手协议CPU1启动后先在共享内存里写一个MAGIC数字CPU0轮询到这个MAGIC才认为CPU1在线否则就认为启动失败。不要小看这一步没有握手协议两个核各跑各的出了问题根本定位不到是启动失败还是通信失败。2. SDK驱动开发与核心机制2.1 BSP层怎么为两个核分别配驱动很多人在SDK里创建完CPU1工程后发现外设驱动居然和CPU0冲突了原因就是BSP配置里两个核都勾选了同一套外设驱动。ZYNQ的SDK会为每个处理器生成独立的BSP在新建应用工程时要明确选择Processor是ps7_cortexa9_0还是ps7_cortexa9_1。我的做法是CPU0的BSP只勾选UART1、SCU GIC、DDR必要的驱动其他PS外设一律不选。CPU1的BSP只勾选UART Lite、AXI GPIO、SCU GIC驱动PS端UART不勾选。这样在驱动层就已经把外设所有权分清了两个核各自初始化自己的外设不会出现同一个UART被初始化两遍、寄存器互相覆盖的情况。在BSP设置里每个外设驱动都有一个“Processor”关联选项。比如UART Lite这个IP虽然挂在PL侧但它通过AXI总线和PS相连在BSP中默认会关联到core0需要手动把它改到core1。这一步是在platform的system.mss里或者通过BSP Settings里的drivers配置调整。改完之后CPU1的应用里才能正常调用UART Lite的驱动API。如果你要自己写驱动模块比如一个自定义的按键扫描驱动可以直接放到CPU1应用工程里的src目录不一定要进BSP。但要注意凡是需要操作硬件寄存器、且只归一个核管理的驱动建议做成独立的.c/.h文件编译进对应核的镜像里而不是两边都包含否则后面资源隔离就会乱掉。2.2 共享内存驱动的缓存一致性处理共享内存是AMP的命脉也是最容易翻车的地方。ZYNQ的Cortex-A9默认开启MMU和CacheCPU0写入共享内存的数据可能先停在L1 Cache里CPU1在另一侧读到的还是DDR里的旧值。这个问题如果不解决你会发现“明明写了对面就是读不到”或者“读到的数据是几分钟前的”。我采用的方法是把共享内存区域设置为Non-Cacheable。在BSP中有现成的API#define SHARED_MEM_BASE 0x1F000000 #define SHARED_MEM_SIZE 0x00010000 Xil_SetTlbAttributes(SHARED_MEM_BASE, NORM_NON_CACHE);这段代码在CPU0和CPU1的main函数开头都必须执行因为每个核有自己的TLB和Cache属性设置。只在一个核里设置另一个核照样会踩到Cache一致性的坑。有人会问那我不关Cache每次读写前后用Xil_DCacheFlush和Xil_DCacheInvalidate手动刷行不行技术上可以但实际用起来非常痛苦因为你要保证所有读写路径都覆盖到漏一处就是偶发bug而且这种bug极难复现。我的建议是共享内存区域直接关Cache共享内存区域之外的私有数据继续享受Cache加速两不耽误。除了Cache编译器优化也是一个坑。共享内存变量一定要加volatile修饰否则编译器可能把循环里的轮询优化掉导致CPU0死等都等不到CPU1的状态变化。我在工程里都用一个头文件定义共享结构体typedef struct { volatile uint32_t magic; volatile uint32_t cmd; volatile uint32_t status; volatile uint32_t data_len; uint8_t data_buf[4096]; } amp_shared_t; #define AMP_SHARED_BASE ((amp_shared_t *)0x1F000000)两个核都包含这个头文件访问的都是同一块DDR地址配合Non-Cacheable设置数据交互就稳了。2.3 串口、中断和互斥机制AMP下多核访问同一个外设必须有明确的互斥方案。最典型的就是串口打印。如果两个核共用同一个UART又同时打印日志输出会交叉混在一起严重时还会操作发送FIFO导致数据错乱。本次项目硬件上给了两路串口CPU0走PS UART1CPU1走PL UART Lite天然隔离这是最舒服的方案。如果你板子上只有一个串口也有办法就是写一个简单的自旋锁打印前后加锁释放。裸机下用ARM的LDREX/STREX指令实现static volatile int uart_lock 0; void uart_lock_acquire(void) { while (1) { int old __LDREXW(uart_lock); if (old 0 __STREXW(1, uart_lock) 0) { __DMB(); return; } } } void uart_lock_release(void) { __DMB(); uart_lock 0; }不过要说明这个锁只能防两个核同时写FIFO不能解决“两个核分别初始化同一个串口”的问题。所以归根结底驱动所有权还是要按BSP划分清楚一个外设只允许一个核做初始化配置。中断路由也要特别注意。ZYNQ的GIC默认把SPI中断都发给CPU0如果你希望某个PL中断直接由CPU1处理需要手动修改GIC中断目标寄存器。我在CPU1初始化代码里做了这样一件事#define GIC_DIST_BASE 0xF8F01000 #define PL_IRQ_ID 61 void route_irq_to_cpu1(uint32_t irq_id) { uint32_t reg_offset 0x1800 (irq_id / 4) * 4; uint32_t shift (irq_id % 4) * 8; volatile uint32_t *reg (volatile uint32_t *)(GIC_DIST_BASE reg_offset); uint32_t val Xil_In32((UINTPTR)reg); val ~(0xFFU shift); val | (0x02U shift); // CPU1 mask Xil_Out32((UINTPTR)reg, val); }这样配置后该中断只会触发CPU1的GIC CPU接口CPU1里注册的中断处理函数才能正常工作。注意自己的GIC CPU接口也要先enable可以调用XScuGic_CPUInterfaceEnable否则中断到达了但CPU1不响应。3. 完整实操记录从Vivado到SDK再到BOOT.BIN3.1 Vivado工程准备整个工程从Vivado开始。我建了一个最简单的Block Design只添加了ZYNQ7 Processing System和AXI GPIO、AXI UART Lite两个PL IP。ZYNQ7 IP核里勾选了DDR3控制器和UART1其他PS外设按需关闭。PL侧AXI GPIO接了几个按键和LED用于模拟高速采样输入UART Lite接到FPGA的引脚作为CPU1的调试串口。注意要把AXI GPIO和UART Lite的中断都连到PS的GIC输入上。在Block Design中直接连线到ZYNQ7处理器的IRQ_F2P端口即可系统会自动分配一个SPI中断号。点Generate Output Products、Create HDL Wrapper之后再导出硬件。File - Export Hardware勾选Include bitstream这样SDK里就能同时拿到硬件描述文件和FPGA配置文件。这一步导出的hdf文件后面SDK全都要用。Vivado版本我用的是2019.2SDK也是配套的2019.2两者必须版本对应。如果你用更新的Vitis界面会有些差异但核心步骤是通的。3.2 SDK工程创建与配置打开SDK先创建一个Platform工程选择导入的hdf文件。加载完成后SDK里能看到两个处理器实例ps7_cortexa9_0和ps7_cortexa9_1这正是后面区分两个核的基础。然后创建两个应用工程cpu0_app基于Hello World模板Processor选择ps7_cortexa9_0。cpu1_app基于Empty Application模板Processor选择ps7_cortexa9_1。这里容易踩坑很多人创建CPU1应用时忘了把Processor切到ps7_cortexa9_1结果两个工程用的都是core0的BSP编译虽然能过但烧进去根本起不到AMP效果。工程创建后打开BSP Settings按2.1节的方式配置外设。然后修改链接脚本。CPU0使用默认的DDR起始地址0x100000即可CPU1则需要把.text、.data等段都放到0x10000000之后避免和CPU0的镜像重叠。我在lscript.ld里设置了如下关键段_DDR_BASE_ADDRESS 0x10000000; _STACK_SIZE 0x2000; _HEAP_SIZE 0x2000;CPU1的代码、数据、堆栈全部从0x10000000开始共享内存在0x1F000000两者之间有足够空间存放采集数据缓冲。代码部分CPU0和CPU1各自实现自己的逻辑。CPU0在main里先初始化UART和GIC然后设置共享内存的Non-Cacheable属性再通过FSBL已经完成的启动流程等待CPU1上线。如果FSBL没有自动启动CPU1可以用备用函数手动启动void start_cpu1(void) { Xil_Out32(0xFFFFFFF0, 0x10000000); dmb(); sev(); }CPU1的main则简单很多设置共享内存属性、初始化UART Lite和AXI GPIO、注册中断然后把MAGIC写入共享内存进入主循环等待命令。3.3 生成BOOT.BIN并烧写验证应用代码调试OK后就是生成启动镜像。用SDK的Create Boot Image工具按顺序添加分区FSBL.elf来自fsbl工程。系统bitstream文件即Vivado导出的.bit。cpu0_app.elf设置Destination CPU为Cortex-A9 #0。cpu1_app.elf设置Destination CPU为Cortex-A9 #1。CPU1这个分区的Destination CPU设置很关键。如果漏了或者配成A9_0FSBL加载完CPU0镜像后根本不会去启动CPU1现象就是CPU1完全没日志、共享内存里永远读不到MAGIC。生成BOOT.BIN之后我习惯先放到SD卡里测试TF卡做成FAT32格式BOOT.BIN放根目录开发板拨到SD卡启动模式上电看串口输出。如果SD卡启动正常再用SDK里的Program Flash Memory烧到QSPI Flash实现板载启动。这样调试阶段用SD卡方便量产用QSPI可靠。验证结果很直观CPU0串口打印出“CPU0 ready, waiting CPU1...”几毫秒后CPU1通过UART Lite打印“CPU1 online, irq registered”同时共享内存状态位变成RUNNING。PL侧按键按下时CPU1触发中断把采样数据写入共享内存CPU0轮询到数据后通过UART1打印出来。整个AMP环路正常工作。4. 实际踩坑与排查方法4.1 CPU1完全没有启动这是我第一次做AMP时最常遇到的问题现象很明确上电后只有CPU0的日志CPU1的打印一个都没有共享内存的MAGIC也永远读不到。排查顺序我总结成这样先看BOOT.BIN里CPU1分区的Destination CPU是否设置成A9_1。这个最容易漏。然后查FSBL串口日志看它有没有识别到CPU1镜像并打印“CPU1 started”之类的信息。再用JTAG连上去看CPU1的PC停在哪里。如果停在0xFFFFFFF0附近说明启动地址没写对如果停在0x10000000说明镜像加载了但代码执行异常。最后检查CPU1的链接脚本首地址是否与BOOT.BIN里设置的加载地址一致。不一致的话FSBL会把镜像加载到一个地方CPU1却跳到另一个地方必然跑飞。我这次遇到的就是Destination CPU没选改过来就正常了。听起来很简单但debug时因为同时怀疑FSBL和链接脚本多花了大半天。4.2 共享内存数据错乱或读不到最新值共享内存的问题往往比启动问题更难缠。现象通常是CPU0往共享内存写了一个数值CPU1通过串口打印出来还是旧值或者CPU0轮询CPU1的状态永远卡住。先确认共享内存变量加了volatile再确认两个核都调用了Xil_SetTlbAttributes把共享区域设置为Non-Cacheable。两个条件缺一不可。还有一种隐蔽情况如果PL侧的DMA也在访问这段共享内存还要考虑DMA是否支持snoop以及DMA控制器自身的数据缓冲问题。不过这次项目没有用DMA搬共享内存只是中断触发后CPU1自己拷贝数据所以不涉及。我的排查习惯是在CPU0和CPU1各写一个自检任务CPU0往共享内存写递增计数CPU1读回来再打印如果等差数列乱了基本锁定Cache一致性问题。确认后逐段检查TlbAttributes调用覆盖整个共享区域别只设置了地址没设置长度。4.3 两个核一起用UART导致输出交叉如果你的板子只有一个串口两个核都要打印一定会遇到这个问题。我这次项目因为是两路串口没踩到但之前一个项目只有一个串口CPU0和CPU1同时打印时日志彻底没法看。解决办法有三个层次上层打印时加自旋锁保证同一时刻只有一个核在写FIFO。驱动层只让一个核初始化串口另一个核通过共享内存把要打印的字符发给这个核代打但实现复杂。硬件层PL侧加一个UART Lite把CPU1日志引到第二路串口。这是最省心、最推荐的做法。UART Lite在SDK里驱动很成熟BSP配置好就能用CPU1只要调用XUartLite_Send即可输出日志。如果你的PCB还没有预留第二路串口引脚那就在设计阶段提前留好不要等到调AMP了再改板。4.4 中断不响应或中断被CPU0抢走CPU1的中断不触发我在一次从CPU0迁中断到CPU1时遇到。仔细看代码GIC初始化在CPU1里做了中断处理函数也注册了但PL侧按下按键CPU1就是不进回调。最后定位到是GIC中断目标寄存器没改。ZYNQ的GIC默认把SPI中断路由到CPU0即使CPU1的CPU interface已经使能中断也不会分发到CPU1。必须手动改GICD_ITARGETSR寄存器把目标CPU mask改成CPU1。如果调试时发现中断两个核都能收到那多半是目标寄存器写错了写成了0x03两个核都响应。这种情况更危险因为两个核会同时处理同一个中断导致共享数据被重复操作。另外还要检查外设驱动是否在两个核的BSP里都生成了。如果AXI GPIO驱动在CPU0和CPU1的BSP里都勾选两个核都会去配置这个IP中断就会被乱七八糟的初始化干扰。正确的做法是外设驱动只归一个核管另一个核的BSP里不要勾选。4.5 常见问题速查表现象可能原因解决方案CPU1无日志、MAGIC读不到BOOT.BIN的CPU1分区Destination CPU没设成A9_1重新生成BOOT.BIN检查分区配置CPU1启动后PC异常链接脚本首地址与加载地址不一致统一CPU1入口地址为0x10000000共享内存读写旧值Cache未关或只在一个核上关闭两个核都调用Xil_SetTlbAttributes两个核抢同一串口没有互斥或共享单串口加锁或PL加UART Lite第二串口CPU1中断不触发GIC目标寄存器未配置手动修改GICD_ITARGETSR路由到CPU1中断两个核都响应目标寄存器写成0x03改为0x02只路由到CPU1驱动初始化互相覆盖两个BSP勾选了同一外设驱动每个外设只在归属核的BSP中勾选4.6 补充无DDR板子的OCM加载思路有些ZYNQ板子为了省成本DDR颗粒没贴只有片上256KB的OCM。这种板子上做AMP思路会有点变化因为OCM地址从0x00000000到0x0003FFFF容量很小两个核的应用都放进去基本不可能。常见做法是CPU0和CPU1的启动代码和关键数据放OCM运行时的数据缓冲继续依赖外部存储或者只做简单的命令交互。FSBL本身也要放在OCM里跑所以留给应用的OCM空间非常紧张。链接脚本需要手工把OCM地址拆成两段CPU0用低地址段CPU1用高地址段共享区再单独预留。这种方案适合小规模的AMP演示或极简控制对正常需求还是建议把DDR焊上。最后再分享一个小习惯这次做完整个双核AMP工程我最大的体会是资源边界一定要在动手写代码之前就划清楚尤其是驱动层的所有权。两个核各自用哪些外设、哪些中断、哪段内存整理成一张表贴到工程文档里后面就算换人接手也不会乱。另外如果你打算长期在SDK里调试双核建议从一开始就把BSP的外设勾选当成工程规范来管不要图省事让两个核都选全量驱动。这个规范花不了十分钟但能帮你省下后面好几天的排查时间。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门