Zynq-7000双核AMP裸机开发实战:从BIF配置到共享内存通信

发布时间:2026/10/3 16:05:01
Zynq-7000双核AMP裸机开发实战:从BIF配置到共享内存通信 做Zynq-7000系列的双核AMP最容易被绕进去的不是代码而是启动流程。我第一次在Vitis 2023.2上做7010的AMP实验时卡在“CPU1根本没跑起来”上整整两天后来发现是BIF里少了一句[corecpu1]的设置。这篇文章就把从环境搭建、内存规划、代码实现到尾调的全过程完整记录下来主环境是Vitis 2023.2/2024.1硬件平台覆盖Zynq-7010和7020两种常见型号适合正在研究Zynq双核AMP通信、或者手里有板子但不知道如何下手的开发者。内容偏裸机AMP实战不走OpenAMP的复杂框架直接教你把两个核用起来、把通信打通。1. 先搞清楚AMP这两个核是“各管一摊”而不是“一起干活”1.1 AMP和SMP的本质区别以及Zynq-7000为什么适合AMP很多人第一次听说Zynq-7010/7020是“双核Cortex-A9”第一反应是“那能不能让两个核一起跑一个Linux性能翻倍”如果你真去查Linux的启动日志会发现Zynq上确实能跑SMP对称多处理的Linux两个核被内核统一调度看起来像是“一起干活”。但AMP完全不同它的全称是Asymmetric Multi-Processing非对称多处理最直白的理解就是两个核各跑各的程序、各干各的活互不干扰只在需要时通过共享内存和中断来交换数据。Zynq-7000的PS端有两个Cortex-A9核心它们共享一套GIC中断控制器、共享DDR内存、共享SCUSnoop Control Unit这恰恰是AMP的理想硬件基础。因为两个核心能访问同一片物理内存所以核间通信不需要额外的硬件通道直接在DDR里划一块共享区域就行。再加上ARM的SGISoftware Generated Interrupt机制CPU0想通知CPU1写一个寄存器就能触发对方的中断通信延迟极低。我在实际项目中用AMP主要解决这么几类问题一类是实时控制与复杂运算分离比如CPU0跑裸机程序做电机控制、采集传感器CPU1跑一个轻量级TCP/IP协议栈或者图形界面逻辑两个任务时间上互不拖累另一类是安全隔离两个核的程序各自独立运行一个核跑崩了不会直接拖垮另一个核还有一类是兼容旧代码A核原来有一套BSP驱动直接搬进CPU1B核继续跑新代码。1.2 7010和7020的PS资源差异哪些板子能做这套实验Zynq-7010和7020在PS端可以说几乎没有差别双核A9、内存管理单元、GIC、UART、SPI、I2C、USB、千兆网这些外设完全一致。真正的差别在PL端7010的逻辑资源大约只有7020的一半。AMP实验本身几乎不用PL逻辑所以7010和7020跑AMP的代码完全通用你在一台7020开发板上写好的双核工程直接编出来扔到7010上也能跑。唯一需要留意的是DDR大小。Zynq-7010/7020开发板最常见的DDR配置是512MB或1GBAMP模式下CPU0的程序、CPU1的程序、共享内存都要在DDR里各占一块地址所以DDR小于256MB的板子基本不适合做AMP实验。我手里有块256MB的板子程序一多地址就打架后来还是换回512MB的板子才消停。做这套实验硬件上只需要三样东西一块Zynq-7010或7020的开发板带UART接口最好、一根Micro USB线用来连串口、一张SD卡因为最常见的裸机AMP启动方式就是从SD卡引导。如果你手头有Digilent的ZedBoard或Zynq-7020开发板那是非常合适的平台出厂自带的DDR基本都是512MB起步。1.3 通信方案三选一共享内存SGI、Mailbox、OpenAMP怎么挑在Zynq双核之间做通信市面上常见的方案大概有三种选型之前先分清楚它们的适用场景。第一种是共享内存软件中断SGI这也是我推荐大多数人入门的方案。思路很简单两个核都C语言直接访问同一段物理地址一个核写数据然后用SGI中断通知另一个核来读。优点是实现极其直观代码量小不依赖任何框架出现问题容易定位缺点是如果数据量大、双方访问频繁需要自己处理同步和防踩踏。第二种是硬核MailboxZynq PS内部自带两个Mailbox硬件IP专门用于核间消息传递。它能传递短的mailbox消息还自带中断比裸共享内存更安全。问题在于Mailbox的寄存器操作比较繁琐可传输的数据量也有限很多开发者用着用着就回去自己写共享内存了。第三种是OpenAMP开源的非对称多处理框架Vitis里直接提供模板工程它把共享内存的管理、rpmsg协议、virtio传输都封装好了开发者只需要调用接口收发消息。OpenAMP功能完备但上手成本高调试难度也大尤其是版本升级后API有变动老旧教程里的代码经常编译不过。我个人在裸机AMP阶段强烈建议先用共享内存SGI把底层机制吃透这一套原理懂了后面再去看OpenAMP的源码会豁然开朗。如果项目里需要Linux和一个裸机核通信那再考虑OpenAMP因为Linux端的remoteproc/rpmsg框架天然支持它。2. Vitis 2023/2024工程搭建从XSA到两个Application2.1 拿到XSA之后在Vitis里把Platform和FSBL配置好新版的Vitis把硬件平台和软件工程彻底分开了。第一步先用Vivado生成硬件描述文件XSA这个XSA里包含了PS配置、DDR参数、外设地址映射等全部信息。注意XSA的生成方式各版本略有区别Vivado 2023.2及以后版本可以在Export Hardware时选择“Include bitstream”这里我建议直接勾上省得后面再关联。拿到XSA之后打开Vitis在欢迎页选择“Create Platform Component”一路下一步加载刚才的XSA。Platform组件创建完成后里面会有一个FSBLFirst Stage Boot Loader子工程。FSBL是整个Zynq启动的“二传手”上电后BootROM先把FSBL加载进OCMFSBL再做DDR初始化、外设初始化、加载用户镜像。AMP启动最关键的一步就在FSBL这里标红加粗三个字别动它但要理解它。FSBL默认情况下只会引导CPU0的程序要让CPU1也跑起来有两种手段。第一种是在生成Boot Image时通过BIF文件的[corecpu1]参数声明——这是最常用的做法后面会详细说第二种是自己在FSBL里手动添加CPU1启动代码这种方式适用于要精确控制CPU1加载时机和起始地址的场合但对新手不太友好容易把FSBL改坏。2.2 CPU0和CPU1两个工程的启动方式SD卡启动与QSPI启动的区别Platform创建好之后在Component视图里新建Application名字建议用app_cpu0和app_cpu1区分。两个工程都选择同样的Platform都选择“Empty ApplicationC”模板一切从零开始写尽量不要勾选Vitis自带的复杂示例。这里有个非常容易被忽略的点两个应用程序的链接地址绝对不能重叠。新建工程之后Vitis默认生成的lscript.ld链接脚本会把可执行程序放在Platform里指定的DDR起始位置如果不手动改CPU0和CPU1的程序可能都默认放在同一个地址FSBL加载的时候直接互相覆盖CPU1跑起来全是乱码。我通常的做法是CPU0的lscript.ld保持DDR起始地址不变默认通常是0x02000000以你板子实际生成的为准CPU1的lscript.ld手动把DDR段的基地址改成0x10000000。这样两个核的程序一个在低位地址区一个在高位地址区互不交叉中间还能留出一大块空间做共享内存。启动介质方面裸机AMP最省事的就是SD卡。FSBL从SD卡读取BOOT.BIN按照BIF分区表依次加载bitstream、CPU0程序、CPU1程序。QSPI Flash也能启动但QSPI的烧写步骤更多而且FSBL在QSPI模式下加载大镜像速度明显更慢。我建议先用SD卡跑通功能最后再考虑固化到QSPI。2.3 生成BOOT.BIN和制作SD卡时容易踩的坑如果你的项目使用Vitis 2023/2024系列在Vitis界面里依次点击Xilinx菜单 -Create Boot Image会弹出Boot Image配置窗口。这里要特别注意亚搏体彩app新版Vitis的Boot Image生成窗口和旧版SDK长得有点不一样但核心机制不变。在Boot Image配置里先用Add按钮依次加入fsbl.elf来自Platform组件里的FSBL工程这一项通常会默认列出如果你的Vivado硬件工程里导出了bitstream就加入.bit文件app_cpu0.elf注意BIF中这一项对应CPU0app_cpu1.elf这一项必须手动把Core改为CPU1。我用的BIF文件最终长这样the_ROM_image: { [bootloader]fsbl.elf system_wrapper.bit [corecpu0]app_cpu0.elf [corecpu1]app_cpu1.elf }[corecpu1]这一句就是FSBL能同时引导两个核的开关。如果没写这一句FSBL默认把所有elf都当成CPU0的镜像顺序加载结果就是CPU1的elf被当成普通数据写进内存、CPU1根本不执行而CPU0的程序里又多了一段莫名其妙的“数据”跑起来必崩。BOOT.BIN生成之后制作SD卡就相对机械了。把SD卡格式化成FAT32然后把BOOT.BIN复制进去就行不用建任何目录结构。这里说一个实际操作心得如果SD卡之前写过Linux镜像分区结构会比较乱最省事的办法是先用fdisk把分区删掉重建一个FAT32分区再用mkfs.vfat格式化再拷贝文件。sudo fdisk /dev/sdX # 删除旧分区新建主分区类型cFAT32 LBA sudo mkfs.vfat -F 32 /dev/sdX1 sudo mount /dev/sdX1 /mnt sudo cp BOOT.BIN /mnt/ sync sudo umount /mnt这张SD卡插到板子上拨码开关设为SD卡启动上电后串口应该能看到FSBL的打印日志然后依次出现CPU0和CPU1程序的输出。如果只看到FSBL日志而CPU0程序没跑多半是BIF里FSBL格式不对如果CPU0跑起来但CPU1没反应八成就是[corecpu1]没写对或者两个elf的链接地址重叠了。有些朋友会用PetaLinux 2025.1配合Zynq做更复杂的启动比如生成boot.scr和image.ub那套流程跟纯裸机AMP不太一样。简单来说boot.scr是U-Boot的命令脚本image.ub是打包好的内核和设备树PetaLinux环境下你可以把CPU1的裸机程序做成一个独立镜像在Linux启动后通过remoteproc动态加载。这套机制细节很多本文先不展开但如果你的板卡计划从PetaLinux切回纯裸机开发记得把SD卡里的boot.scr和image.ub删干净只留BOOT.BIN否则U-Boot会先接管启动流程FSBL根本轮不到。3. 双核程序实现共享内存怎么分、中断怎么触发、数据怎么同步3.1 内存布局为什么CPU0的程序放0x02000000CPU1的程序放0x10000000AMP模式下的内存规划是整个工程的重中之重一旦地址设计不合理后面所有调试都是在浪费时间。我的建议是画一张内存地图照着地图写代码。以512MB DDR的板子为例DDR的物理起始地址通常在0x00100000附近但具体从哪个偏移开始使用要以你Vivado工程生成的地址映射为准。我习惯按下表划分区域起始地址示例大小使用者CPU0程序区0x02000000约64MBCPU0的elf加载与运行CPU1程序区0x10000000约64MBCPU1的elf加载与运行共享内存区0x200000001MB~4MBCPU0和CPU1共用预留区共享内存之后剩余空间扩展用为什么CPU0放在低地址、CPU1放在高地址因为FSBL默认的DDR链接地址通常就是低地址区间CPU0保持默认最省事而CPU1的elf被FSBL加载时会根据它的链接脚本把程序放到0x10000000FSBL解析elf里的段信息会自动把代码和数据拷贝到对应地址所以只需要在链接脚本里改一行。共享内存放在0x20000000是因为它离两个程序区都不近不远不会因为程序体积膨胀而被覆盖。我之前在另一块小DDR板子上把共享内存放在0x05000000结果CPU1程序编译出来体积稍微大了点直接覆盖到共享区调试了整整一个晚上才定位到问题。从那以后共享内存我至少离程序区留出32MB以上的安全距离。3.2 CPU0侧代码逻辑加载CPU1、释放CPU1、接收SGI中断CPU0的程序是整个双核系统的主控它负责做CPU1的启动触发同时作为通信主设备接收CPU1发来的数据。CPU1的启动触发在裸机AMP下有两种写法。一种是在FSBL阶段就完成FSBL根据BIF里的[corecpu1]项自动把CPU1的elf加载到0x10000000并释放CPU1的reset这种情况下CPU0的程序里不需要写任何启动CPU1的代码直接运行自己的逻辑即可。另一种是延迟启动比如CPU1的程序加载地址还是定的0x10000000但FSBL不做启动动作改由CPU0的程序在某个时机手动触发。我测试过一种很实用的延迟启动方式CPU0程序里用CPU1的起始地址初始化CPU1的reset向量然后释放CPU1复位#define CPU1_START_ADDR 0x10000000 #define SLCR_UNLOCK (0xF8000008) #define SLCR_LOCK (0xF8000004) #define SLCR_CPU1_RST_CTRL (0xF8000240) void start_cpu1(void) { volatile unsigned int val; Xil_Out32(SLCR_UNLOCK, 0xDF0D); // 将CPU1的启动地址写入寄存器不同板卡寄存器细节略有差异 Xil_Out32(SLCR_CPU1_RST_CTRL, CPU1_START_ADDR); // 释放CPU1复位 val Xil_In32(SLCR_CPU1_RST_CTRL); Xil_Out32(SLCR_CPU1_RST_CTRL, val | 0x2); Xil_Out32(SLCR_LOCK, 0x767B); }要注意寄存器的精确名字在不同BSP版本里可能不同而且直接用Xil_Out32访问SLCR寄存器之前最好确认BSP已经打开了访问权限。其实对大多数Vitis版本来说直接在start_cpu1里写这段代码就能奏效但如果你的BSP版本里已经有了XFsbl_StartCpu1之类的封装函数优先用它更稳。SGI中断的配置是通信的关键。Zynq的GIC支持16个SGI中断编号是0到15任一个核都能用ICDSGIR寄存器向自己或其他核触发SGI中断。在CPU0的GIC初始化里我注册SGI0作为接收中断#include xscugic.h static XScuGic Gic; static void Cpu0SgiHandler(void *CallbackRef) { volatile unsigned int *shared (volatile unsigned int *)0x20000000; // 从共享内存读取数据 unsigned int cmd shared[0]; unsigned int data shared[1]; xil_printf(CPU0: recv cmd0x%08x data0x%08x\r\n, cmd, data); } int init_cpu0_gic(void) { XScuGic_Config *cfg XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID); XScuGic_CfgInitialize(Gic, cfg, cfg-CpuBaseAddress); XScuGic_Connect(Gic, 0, (Xil_ExceptionHandler)Cpu0SgiHandler, NULL); XScuGic_Enable(Gic, 0); Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, Gic); Xil_ExceptionEnable(); return 0; }SGI在GIC里中断号就是0到15所以XScuGic_Connect(Gic, 0, ...)用的是中断号0意思是接收SGI0。至于这个SGI0是CPU0发给自己的、CPU1发给CPU0的都在同一个中断号上通过中断处理函数的执行核心可以判断来源也可以直接把处理函数放在两个核里共用。但为了逻辑清晰我习惯CPU0和CPU1各用一个独立的SGI编号比如CPU0用SGI0收数据CPU1用SGI1收数据这样在中断里不需要额外的判断。3.3 CPU1侧代码逻辑等待启动、发中断、读写共享内存CPU1的程序跟CPU0不同它没有一个“启动别人的职责”它只需要做两件事初始化自己的中断然后进入主循环等待被CPU0触发。但也因为CPU1是FSBL引导的“从核”它没法通过常规的XScuGic_Config流程完全复用CPU0的中断初始化代码需要特别留意GIC是全局共享的两个核不能在同一时间各自初始化GIC否则寄存器配置互相覆盖。我在实际工程里是这么处理的CPU1启动后先做GIC初始化代码和CPU0几乎一样但因为FSBL在启动CPU1之前已经由CPU0完成过一次系统级初始化CPU1只需要再次查找GIC配置然后连接中断即可。关键在于XScuGic_CfgInitialize每次都会把整个GIC寄存器重写一遍所以如果CPU0和CPU1的程序执行时机有重叠必须保证先启动的CPU0已经完成了GIC初始化后启动的CPU1再执行初始化不然后者的配置会覆盖前者的。CPU1主循环大致如下volatile unsigned int *shared (volatile unsigned int *)0x20000000; void Cpu1SgiHandler(void *CallbackRef) { unsigned int a shared[0]; unsigned int b shared[1]; shared[4] a b; xil_printf(CPU1: calculate %d %d %d\r\n, a, b, a b); } int main(void) { init_uart(); xil_printf(CPU1: start\r\n); init_cpu1_gic(); // 注册SGI1中断 shared[4] 0; while(1) { // 主循环可以干别的事 // CPU1在等待中断通知时也轮询共享内存标志两种方式可并行 } }为了演示双向通信CPU1在中断处理里把计算结果写回共享内存数组的第四个位置然后主动触发一个SGI0给CPU0。触发SGI的代码在两个核里通用写法如下#define ICDSGIR 0xF8F00F00 void send_sgi_to_cpu0(void) { // bit[15:0]选择SGI0bit[23:16]0x01表示目标CPU0 Xil_Out32(ICDSGIR, (0x01 16) | 0x01); }给CPU1发中断的SGI号换成SGI1即可void send_sgi_to_cpu1(void) { // bit[15:0]的bit1置位表示触发SGI1目标CPU1对应bit[18] Xil_Out32(ICDSGIR, (0x01 18) | 0x02); }这样设计之后整个通信链路是CPU0把命令和数据写入0x20000000开头的共享内存然后触发SGI1通知CPU1CPU1在中断处理函数里读数据、计算结果、写回结果再触发SGI0回告CPU0。整个过程不依赖任何操作系统完全裸机跑逻辑清晰可控。3.4 缓存一致性L1/L2 cache和SCU要不要关、数据丢失的根源AMP里最隐蔽的坑十个有八个挂在Cache一致性上。Cortex-A9有L1 Cache还有PL310的L2 Cache而Zynq的SCU负责维护两个核在共享内存上的一致性。听起来有SCU护着似乎没问题但裸机BSP默认情况下Cache可能是开启的而CPU0和CPU1各自对共享内存的读写不一定能实时同步到对方。解释一下CPU0往地址0x20000000写了一个数这个数可能只存在于CPU0的L1 Cache里还没有真正flush到DDR物理内存CPU1此时去读0x20000000读到的是DDR里的旧数据根本看不到CPU0刚写的值。这类问题表现出来就是“共享内存写进去读不出来”、“偶尔能通偶尔不能通”、“程序加一行打印就正常去掉打印就崩”。解决的思路有两个。最彻底但性能损失较大的办法是把共享内存区域设置为非Cache属性让对它的访问直接落到DDR上不使用缓存。在裸机BSP里可以用Xil_SetTlbAttributes来设置共享内存页的缓存属性#define SHARED_MEM_START 0x20000000 #define SHARED_MEM_SIZE (4 * 1024 * 1024) Xil_SetTlbAttributes(SHARED_MEM_START, 0x14c0);这里的0x14c0是和具体体系结构相关的页表描述符值不同BSP版本和Cache配置下可能会有差异直接用之前最好查一下你板载BSP里对该函数的说明。如果这个宏定义在你的BSP里不可用另一个更朴素的办法是手动对共享区域做Cache flush和invalidate操作Xil_DCacheFlushRange(SHARED_MEM_START, SHARED_MEM_SIZE); // 写共享区后调用 Xil_DCacheInvalidateRange(SHARED_MEM_START, SHARED_MEM_SIZE); // 读共享区前调用我在自己项目里是把这两种办法结合用的共享内存的1MB主区域设置成非Cache专门用来放高频交互的结构体如果需要传大块数据再开辟另一段Cache区域用flush/invalidate手动管理。这样既能保证实时性又能用Cache加速大数据拷贝。还有一种做法是直接关掉L1 Data Cache代码简单但性能损失较大不太推荐在生产项目里用。4. 调试实录双核程序跑不起来的几个典型症状4.1 用XSDB调试器连接两个CPU的正确打开方式Vitis 2023/2024里新起了一个调试服务兼容旧的XSDB命令。双核AMP调试最大的难点在于你没法像单核那样断点一打、全速一跑就完事你得能同时看到CPU0和CPU1各自的运行轨迹。我先说一个很多人踩过的坑把CPU0和CPU1的main函数断点都打在同一个行号然后点Resume结果两个核同时卡在同一处现场看着好端端的一F5全飞了然后CPU1再也进不了断点。原因是两个核跑的是不同的可执行文件断点却设在了同一个源码位置调试器会把它解释成同一个地址的断点CPU1的程序里那个地址可能根本没有任何东西。正确做法是在Vitis的Debug Configurations里为app_cpu0和app_cpu1分别创建两个调试配置分别关联对应的elf文件。启动调试时先启动CPU0配置再启动CPU1配置然后各自设置断点、单步、读寄存器。调试视图里会多出两个线程或者两个target分别对应两个核选择目标后再执行操作就不会互相干扰。命令行调试的话用XSCT脚本比较直观connect targets -set -filter {name ~ *A9*} rst -system load -file BOOT.BIN con这样启动的是整个BOOT流程CPU1由FSBL引导。如果你在Vitis GUI里调试可以同时打开两个Application的调试配置一个连CPU0的elf一个连CPU1的elf。不过要提醒一句CSS调试器里经常出现CPU1连接成功但无法命中断点的情况多半是链接地址和编译地址不一致重新构建CPU1工程再试。4.2 常见问题速查表启动失败、地址重叠、中断不触发、数据错乱症状现象可能原因解决办法CPU1完全不运行串口只有CPU0的打印CPU1无输出BIF里没有[corecpu1]CPU1的elf没有被FSBL加载检查BIF加入[corecpu1]并重新生成BOOT.BINCPU1运行但打印乱码能打印但内容全是0xDeadBeef之类CPU0和CPU1的链接地址重叠修改CPU1的lscript.ld让起始地址避开CPU0程序区共享内存读不到值CPU0写入后CPU1读出来永远是0Cache不一致对共享区设置非缓存属性或在读写前后做flush/invalidateSGI中断没反应两个核之间发中断无效GIC初始化被后启动的核覆盖保证只有一个核执行完整的GIC初始化另一个核只Enable中断程序偶发死机跑一段时间就卡死重启恢复共享内存越界程序区被覆盖加大程序区和共享区之间的隔离建议至少留32MB串口打印顺序混乱CPU0和CPU1的打印互相穿插两个核共用UART但未加锁在打印函数前后加自旋锁或简单关闭中断保护这张表看起来简单但每一条都是我或者同行朋友真金白银踩出来的。尤其第4条GIC是全局的CPU1启动时如果直接复制CPU0的GIC初始化函数经常把CPU0已经连接好的中断处理函数指针覆盖掉CPU0发中断时CPU1能进IRQ但CPU1调的是自己没初始化的处理函数直接跑飞。所以我建议只让CPU0执行完整的XScuGic_CfgInitializeCPU1只用XScuGic_Connect配合XScuGic_Enable把自己需要的中断挂上。第6条关于串口打印看似小问题但在AMP项目里很致命。两个核共用一个UART外设同时打印时字符会交错、乱码严重时阻塞整个程序。简单做法是在串口输出函数外加一个全局自旋锁变量大家都遵守“先拿锁再打印”的约定。对于裸机程序也可以用关中断的方式保护但关中断时间不宜太长。4.3 联动PetaLinux和上位机的一些补充说明如果你的板卡最终不是纯裸机而是希望CPU0跑PetaLinux、CPU1跑裸机AMP程序那整个体系会变得更复杂一些。PetaLinux 2025.1本身对Zynq-7000的支持已经很成熟生成boot.scr、image.ub的流程和更早版本差别不大。简单说boot.scr是U-Boot的启动脚本指明从哪里加载image.ub、怎么设置启动参数image.ub本质是把内核和设备树打包在一起的一个文件用U-Boot的bootm命令加载。在PetaLinux环境下让CPU1单独跑裸机程序常用方案是remoteproc。你需要给CPU1单独编一个elf然后在内核设备树里配置remoteproc节点指定elf的固件路径和加载地址。系统启动后CPU0侧Linux通过remoteproc框架把固件加载到指定地址然后启动CPU1。这套机制好处是动态管理、对上层应用友好缺点是配置复杂而且CPU0 Linux和CPU1裸机之间的通信通常要配合rpmsg协议调试门槛比纯裸机情况高了不少。如果你的上位机是Qt写串口程序想直接跟Zynq板卡交互那编译Qt serialport库属于常规操作在宿主机上交叉编译好libQt5SerialPort.so再放到rootfs里就能用。或者用库里的QSerialPort类写个简单的串口收发界面通过板卡上的USB转串口芯片和Zynq的UART对接。热词里还有“zynq裸机usb通信方案 基于libusb”这个思路也成立就是让Zynq的USB外设工作在设备模式用USB CDC类模拟串口上位机通过libusb访问好处是不依赖物理串口线坏处是USB驱动开发工作量明显大于直接用UART做双核通信原型验证阶段没必要给自己加这个负担。做这套东西板子型号不是核心Zynq-7010和7020在AMP实验上完全可以当同一种芯片处理。真正的核心是你得把FSBL、BIF、链接脚本、共享内存和SGI中断这五件事拧成一根绳。开发早期我吃过不少亏经常是CPU0跑得飞快CPU1那边像死了一样静悄悄。后来养成了习惯每改一次链接脚本就重新确认一次两个elf的加载地址每改一次共享内存布局就重新审视一遍Cache策略看起来啰嗦但能省下大把抓狂时间。方法已经完整写在这里照着搭一套纯裸机AMP通信链路少说能帮你在项目里省出两个周末祝调试顺利。