尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Zynq双核AMP开发实战:从Hello World到核间通信与共享内存

Zynq双核AMP开发实战:从Hello World到核间通信与共享内存 直接进入正题。搞过Zynq或者Zynq UltraScale MPSoC开发的朋友应该都有这种感受在Vitis里新建一个Hello World工程、点一下下载、串口里蹦出字符串一切顺利得像是解小学算术题。但如果你把同一个Hello World跑在双核ARM上并且让两个核各自打印信息、互不干扰事情就开始变得有意思了。这个标题看起来人畜无害背后牵出来的AMP架构、核间通信、启动流程、共享内存管理才是真正值得花时间琢磨的东西。这篇文章不打算把Xilinx官方文档抄一遍而是从我实际折腾双核AMP的过程出发把整个链路拆开讲清楚。适合刚接触Vitis、想从单核过渡到多核开发的嵌入式工程师也适合那些已经能熟练跑Linux但是对裸机多核协作仍然一头雾水的人。看完你会明白Hello World背后藏着的启动顺序、CPU1入口地址、IPI中断、共享内存Cache一致性才是在AMP架构下写代码真正绕不开的坎。1. AMP架构到底是怎么回事为什么双核Hello World值得研究1.1 多核处理器的三种基本玩法在动手写代码之前得先把概念对齐。多核ARM处理器在软件层面通常有三种组织方式SMP、AMP和BMP。SMP也就是对称多处理所有核心对等共享一份操作系统Linux默认就是这个玩法任务调度器会负责把线程分到不同核心上跑对应用层透明。AMP非对称多处理每个核心运行自己独立的程序甚至操作系统核心之间通过约定的机制协作没有统一的调度器。BMP则比较边缘化绑定式多处理可以理解为手动指定某个任务只跑在某个核心上的SMP变体。这三种方式里AMP是嵌入式裸机场景最常用也最灵活的一种。原因很简单SMP需要完整操作系统支持BMP依赖调度器的亲和性配置而AMP只需要你手里有一份链接脚本、一个启动地址、一段共享内存和几个中断就能让两个核心各干各的活。Zynq上的双核Cortex-A9天生支持AMP实际上很多工业控制、数据采集、协议转换方案就是这么做的——一个核跑实时控制另一个核跑网络协议栈两边通过共享内存交换数据。1.2 “双核Hello World”为什么不只是一个玩具很多人会觉得双核Hello World不就是新建两个工程、各自打印一句话吗表面看是这样但真正做过的人都知道难点根本不在打印本身而在“秩序”——CPU1是谁唤醒的它以什么地址启动它俩共享的DDR区域是否会发生访问冲突一个核写数据的时候另一个核正在读到底该不该刷新Cache这就像两个人合租一套房如果不事先约定谁住哪间、冰箱哪一层归谁、垃圾什么时候倒入住第一天就得打起来。双核Hello World的意义就是把你从“单核思维”里拽出来逼你面对这套规则的建立过程。你在Vitis里看到的Hello World模板只是冰山一角水面以下藏着的是启动引导、内存布局、异常路由和核间通信机制的完整设计。2. Vitis开发环境搭建与平台准备别在第一步就卡住2.1 开发套件选型与安装的常见坑做双核AMP开发一块带双核ARM的板子跑不了。目前最常见的三块Zynq-7000系列比如ZedBoard、MicroZed、正点原子启明星、Zynq UltraScale MPSoC系列ZCU104、Ultra96以及Kria系列。Zynq-7000是双核Cortex-A9MPSoC是四核A53加双核R5这里我以Zynq-7000为例来讲因为它的AMP机制更基础、更容易理解你把它吃透了再去碰MPSoC会轻松很多。Vitis的安装也有一点讲究。Vitis 2022.2之后的版本把嵌入式开发整合得比较干净但安装路径不能有中文和空格这个老生常谈的问题至今仍然会坑到新人。安装时建议选择Vitis IDE原Vivado的SDK功能已经被整合进去不需要装Vivado ML Enterprise版除非你还打算改硬件工程。如果你手头有老版本习惯注意Vitis 2023.1的调试器对某些仿真器的识别策略有变化驱动不更新可能就会出现下面要讲的“不识别芯片”问题。2.2 硬件平台与基础工程的准备思路在Vitis里创建双核工程之前你手里得有一个平台工程Platform Project。它相当于描述“这块板子长什么样”的元信息文件包括处理器核数、可用外设、DDR地址范围、中断控制器配置等。硬件部分可以用Vivado搭好导出XSA文件或者在Vitis里直接基于官方板级支持包BSP创建。我的建议是一开始不要自己画板子直接选官方平台的xsa文件先把AMP流程跑通再去操心自定义硬件。创建平台工程之后勾选A9_0和A9_1两个核的standalone支持分配DDR地址空间。默认情况下两个核共享完整的DDR映射但在AMP模式下这是不安全的后面会说到如何划分区域。3. 双核Hello World的实现全过程从CPU0到CPU1的引导细节3.1 单核Hello World的启动路径回顾先花半分钟回顾单核的启动流程因为双核是它的超集。Zynq上电后片内BootROM会从启动设备加载第一阶段引导程序FSBL。FSBL由Vitis根据BSP自动生成负责初始化DDR、配置PS端的MIO和时钟然后把应用程序加载到链接脚本指定的DDR地址跳转过去执行。单核程序里中断向量表、栈指针、数据段都会被链接脚本安排到固定位置C语言的main函数就是在这样的前提下被调用的。整个过程只有一个执行流不存在“谁去唤醒另一个核”的问题。但是到了AMP场景FSBL默认启动完CPU0后根本不会管CPU1如果你不在代码里手动唤醒CPU1就会一直停留在复位状态——这就是双核程序“运行不起来”的最常见原因。3.2 CPU1入口地址到底是怎么设置的Zynq的双核Cortex-A9里CPU0可以通过执行SEVSend Event指令唤醒CPU1。但CPU1被唤醒后去执行哪个地址的代码这需要事先约定好。Xilinx官方的AMP方案之一是让CPU0把CPU1要执行的入口地址写到约定的内存位置通常是0xFFFFFFF0这个地址附近然后触发一次SEV事件。CPU1醒来后从它自己的复位向量读取这个地址并跳转。寄存器写法和FSBL的修改方式在官方文档里有详细描述工程上我更推荐用XilMailbox库它把CPU1启动地址传递、中断触发、响应确认都封装好了直接用API调用即可。在Vitis里实操时你需要为CPU1单独创建一个应用工程把它的链接脚本中DDR段地址改到CPU0不会碰的区域。比如CPU0的程序放在0x00100000CPU1的放在0x02000000中间留出大段DDR作为共享内存区。这样两个核在物理上就不会互相踩踏了。3.3 核间通信的三种常用方式对比两个核各自跑起来之后问题来了它们怎么交换数据常用的方式有三种共享内存、IPI中断、Mailbox机制。共享内存是最直接的一段DDR两边都映射CPU0写入、CPU1读取但是要注意访问同步和Cache问题。IPI是硬件中断机制一个核可以给另一个核发中断属于“敲门”动作。Mailbox则可以理解为“插了信箱的共享内存”配合中断实现消息通知。三种方式经常组合使用共享内存负责传数据IPI负责通知对方“数据已经写好”Mailbox负责传输控制命令。下面这张表整理了它们的适用场景和注意事项。通信方式实现难度适用场景核心问题共享内存低大批量数据交换Cache一致性、访问冲突IPI中断中事件通知、简单命令中断路由配置Mailbox中高控制消息、状态交互协议设计与超时处理4. 核间通信的关键设计共享内存的正确打开方式4.1 划分共享内存区域把“公共区域”定义清楚AMP架构下共享内存并不是指物理上多出一块特殊内存而是DDR里被两个核都映射到的某个地址段。如果两个核的程序各自用链接脚本锁定了自己的专属区域再约定一段固定地址作为共享区域那么通信的基础就有了。我通常的做法是在DDR里画出一块4KB到64KB不等的区域专门存放通信结构体。这段区域可以用结构体来定义协议比如头部四个字节写消息类型和长度后续字节写数据再放几个标志位表示读写状态。两个核都把这套结构体定义在各自工程里保证声明一致、数据类型一致否则解析的就是一堆垃圾值。关键的是访问共享内存时要注意内存屏障。编译器的优化和CPU乱序执行可能让你的代码在写数据时发生重排在ARM上是需要barrier指令来保证顺序的。简单粗暴的做法是每次写完数据后执行__DSB()每次读之前执行__ISB()虽然牺牲一点性能但是能保证你不会不明不白地读到旧数据。4.2 Cache一致性问题为什么共享内存会“读不到数据”这是所有人都会踩的坑。Cortex-A9有L1 Cache如果CPU0往共享内存地址写数据而这段地址被标记为Cacheable数据会先停留在Cache里短时间不会刷到DDR。CPU1去读同样是Cacheable的地址读到的可能是它自己的Cache里缓存的旧数据。结果就是两边怎么都对不上。解决思路有三个方向。第一将共享内存地址配置为Device类型或Non-Cacheable这样每次读写都直接访问DDR一致性天然保证代价是性能打折扣。第二保留Cacheable但在写完后主动调用Xil_DCacheFlushRange把Cache刷到DDR读之前调用Xil_DCacheInvalidateRange让Cache失效强制从DDR重新加载。第三利用ARM的SCUSnoop Control Unit实现硬件一致性但这只对某些特定地址段有效裸机下配置起来比较绕。我在项目里通常采用第二种方案把共享内存划分成环形缓冲区每次写完一个数据包就刷新Cache读端收到通知后先做Invalidate再解析。这样既保证了正确性性能也不算太差在常见的控制类应用里完全够用。4.3 一个简单的双核通信协议设计实例说点可以直接抄作业的东西。假设CPU0是控制核CPU1是数据采集核通信协议就四步CPU1把采集到的数据按固定长度写入共享区的data缓冲然后在flag字段写入一个特殊值并触发IPI中断CPU0收到中断后读取flag确认数据有效然后解析data缓冲处理完CPU0把flag清零并写回一个ack标志CPU1看到ack就可以覆盖下一批数据。这个协议很粗糙但是它能用。你需要的全局变量和结构体大概长这样typedef struct { volatile uint32_t flag; // 0: 空闲, 1: 数据就绪, 2: 处理完成 volatile uint32_t len; // 有效数据长度 uint8_t data[1024]; // 数据缓冲区 } shared_mem_t; #define SHARED_MEM_BASE 0x10000000CPU0的中断处理函数里做一个判断如果flag是1就处理数据处理完把flag改成2。CPU1每写完一批数据把flag置1然后调用API发送IPI中断。这个状态下即使两边Cache没有严格刷新只要你在读写flag之前执行了对应的Flush或Invalidate整套流程就会非常稳定。当然这只是一个最简陋的协议。工程上还要考虑超时、握手校验、断线重连等核心思想是一样的明确状态机明确谁在什么状态下可以改哪一块数据。5. 实操中踩过的坑调试、烧写与常见问题排查5.1 “下载调试时识别不到芯片”的排查全过程这也是很多人在搜索栏里敲过的关键词“vitis 下载调试的时候 不识别芯片”。我实际遇到过折腾了三个小时才定位。当时环境是Vitis 2023.1加Digilent JTAG现象是点击Run后提示ERROR: Not a valid JTAG device。排查路径是这样一个顺序。先看电源指示灯确认板子供电正常。然后打开Vitis的Xilinx菜单中选择Program Device看有没有设备被识别。如果设备列表是空的基本就是JTAG链路问题。常见原因有三个USB线是纯充电线没有数据线功能JTAG端口选了错误的适配器目标板的电源没有完全起来JTAG电平不对。重点说第三个原因很多人忽略电源时序。Zynq PS端的VCCPINT先上电然后VCCPAUX和VCCPLL最后才是VCCO与PL端的Bank电压。如果PS端的几个电压之间有压差JTAG链路的电平就乱了调试器自然扫描不到芯片。当时我的板子就是FPGA端的Bank电压没起来Vivado能识别到PL器件但Vitis认不到PS的ARM核。把电源修好后一切恢复正常。更隐蔽的一个坑是多个JTAG设备并联比如板载FTDI调试器和外部Xilinx USB Cable同时存在软件可能选了错误的连接目标。在Vitis的Run Configuration里可以指定调试器也可以直接下拉选择当前活动的硬件服务器。遇到识别不到芯片先别急着换驱动把连接拓扑理清楚排除掉线材和电源问题再动手改配置。5.2 常见问题速查表下面这张表是双核AMP开发中容易碰到的高频问题按“现象-原因-解决”整理出来方便出问题时快速对照。问题现象可能原因解决办法CPU1程序不运行FSBL未启动CPU1或入口地址错误用XilMailbox唤醒CPU1指定正确入口两核互相覆盖数据链接脚本地址重叠为每个核分配独立DDR区间共享数据偶尔乱码Cache未刷新读写共享区前调用Flush/Invalidate打印乱码串口波特率不匹配检查platform的UART配置IPI中断收不到中断路由配置错误检查GIC的Interrupt ID分发设置JTAG扫描不到芯片电源时序/线缆/多设备冲突按顺序排查电源与连接拓扑程序跑飞或HardFault栈地址越界增大Stack Size或检查链接脚本5.3 调试技巧与个人心得调试双核程序比单核麻烦得多因为你要同时看两个核的执行状态。Vitis 2023.1之后新增了多核调试窗口可以在Run Configuration里勾选Debug all cores然后在Debug视图下拉切换当前聚焦的CPU上下文。这个功能对AMP开发尤其重要不然你根本不知道CPU1是卡在初始化还是卡在死循环里。还有一个技巧利用共享内存区域做日志缓冲区。让每个核把调试信息以环形缓冲的形式写入自己的专属日志区通过另一个核或上位机读取。这个做法在中断频繁、串口打印会干扰时序的时候非常有效。个人体会是AMP开发的重点不在写代码而在约定与纪律。你得在动手之前把内存地图画清楚把通信协议定明白把每个核的职责边界划出来否则后期调试会陷入一种“改代码-互相影响-再改代码”的死循环。双核Hello World是个很好的起点它逼你把这套流程走一遍后续再去做更复杂的MPSoC应用就有谱了。6. 从Hello World走向真实项目的方向跑通双核Hello World你其实已经掌握了一个完整的AMP应用骨架。接着往上扩展的方向很多把CPU1换成跑FreeRTOS让它处理实时任务或者把CPU0换成跑Linux用内核驱动来和CPU1共享内存交互更复杂一点可以在MPSoC的A53和R5之间搭OpenAMP框架实现异构多核通信。每一个方向都会引入新的知识点但核心回归到那几件事启动引导、地址隔离、中断路由和Cache管理。基础打牢了后面的路就会顺畅很多。如果你正在为双核程序“跑不起来”或者“数据偶发错乱”发愁不妨先把这篇文章里的清单过一遍大概率能帮你省下两三个小时的无用功。
返回列表