ZYNQ7020 AMP实战:Linux与裸机双核通信与实时控制

发布时间:2026/9/29 16:18:24
ZYNQ7020 AMP实战:Linux与裸机双核通信与实时控制 1. 为什么ZYNQ7020的AMP方案值得认真对待做嵌入式开发的朋友大概率都遇到过这种拧巴的局面一颗芯片上跑着Linux界面、网络、文件系统都挺舒服但一到硬实时控制环节就掉链子——Linux的调度抖动动辄几百微秒甚至毫秒级PID控制环路稍微要求高点就开始震荡。反过来纯裸机跑控制稳如磐石但你想加个Web配置页面、想挂个文件系统记录数据、想跑个MQTT上报那开发量直接起飞。ZYNQ7020这类SoC的出现本质上是给这个矛盾提供了一个硬件层面的解法。它内部是双核Cortex-A9加FPGA逻辑两个A9核可以各自独立跑一套软件栈。AMPAsymmetric Multiprocessing非对称多处理模式就是让CPU0跑Linux负责“慢而杂”的事务CPU1跑裸机负责“快而准”的实时任务两者通过共享内存和中断通信。这不是什么新概念但真正落地时踩的坑远比想象中多——内存怎么划分、中断怎么路由、启动顺序怎么安排、缓存一致性怎么保证每一步都有讲究。这篇内容适合两类人看一类是已经上手ZYNQ但被Linux实时性折磨过的工程师另一类是正准备选型、想搞清楚AMP到底能不能解决自己问题的技术负责人。我会从整体设计思路讲到具体的工程配置包括内存划分的计算过程、中断控制器的配置细节、共享内存的通信协议设计以及我在实际项目中踩过的那些坑。代码和配置都会给到可以直接参考的程度不是泛泛而谈的概念科普。需要提前说明的是下面涉及的具体参数和步骤一部分来自Xilinx官方文档一部分是我在实际项目中的工程实践总结。不同版本的Vivado和PetaLinux工具链在细节上可能有差异我会标注出我使用的版本你根据自己环境做调整。2. AMP方案的整体设计与核心思路拆解2.1 为什么选AMP而不是其他方案在ZYNQ7020上实现Linux和裸机共存技术上其实有几条路可以走。第一条是纯Linux加实时补丁比如Xenomai或RT-Preempt第二条是纯裸机自己实现所有功能第三条就是AMP。我三条路都试过或者见过别人试过说说各自的实际情况。纯Linux加实时补丁这条路RT-Preempt能把最坏调度延迟压到几十微秒级别对于很多工业控制场景其实够用了。但问题是ZYNQ7020的A9主频只有667MHz跑完补丁之后系统开销明显增加而且你永远无法完全消除Linux内核里那些不可抢占的临界区。如果你的控制环路要求周期小于100微秒、抖动小于10微秒这条路基本走不通。纯裸机方案在实时性上无可挑剔但你要自己实现TCP/IP协议栈、文件系统、USB驱动这些东西开发周期至少翻倍。而且裸机下的网络性能远不如Linux调试手段也少得可怜。AMP方案的核心优势在于“各司其职”。Linux那边你照常用Yocto或者PetaLinux构建完整系统该有的驱动、库、工具一个不少。裸机这边只跑最核心的控制逻辑代码量小、执行路径确定、中断响应可预测。两者通过硬件级别的共享内存通信延迟可以做到微秒级。代价就是你需要额外处理核间通信、资源划分和启动协调的问题。2.2 内存划分的计算逻辑AMP方案第一个要解决的问题就是内存怎么分。ZYNQ7020的PS端DDR控制器通常挂512MB或1GB的DDR3两个A9核共享这片物理内存。你必须确保Linux和裸机各自使用的内存区域完全不重叠否则轻则数据踩踏重则系统直接跑飞。我的划分思路是这样的先确定裸机侧的需求剩下的全给Linux。裸机侧需要的内存包括代码段、数据段、堆栈、共享内存区、外设寄存器映射区。以一个典型的电机控制应用为例裸机代码大概64KB数据段32KB堆栈8KB共享内存预留1MB用于传输控制指令和反馈数据外设寄存器映射区2MB。加起来大约1.1MB向上取整到2MB留足余量。具体在物理地址上的划分我通常这样安排内存区域起始地址大小用途Linux保留区0x000000000x30000000 (768MB)Linux内核和用户空间共享内存区0x300000000x00100000 (1MB)核间通信缓冲区裸机代码区0x301000000x00080000 (512KB)裸机程序代码和数据裸机堆栈区0x301800000x00040000 (256KB)裸机堆栈外设映射区0x400000000x20000000 (512MB)外设寄存器这个划分需要在三个地方保持一致Vivado的地址编辑器里、Linux的设备树里、裸机的链接脚本里。任何一处对不上等着你的就是各种莫名其妙的崩溃。2.3 启动流程的设计考量AMP模式下两个核的启动顺序很关键。ZYNQ7020上电后CPU0先执行BootROM然后根据启动模式加载FSBLFSBL再加载bitstream和应用程序。默认情况下FSBL只加载CPU0的应用程序CPU1处于等待状态。我的做法是让FSBL同时加载Linux镜像和裸机ELF文件到各自的内存区域然后CPU0启动LinuxLinux内核起来之后再通过一个内核模块或者用户空间程序去唤醒CPU1。这样做的好处是启动流程清晰Linux有足够的时间完成初始化裸机侧不需要等待任何Linux的资源。另一种做法是FSBL直接启动CPU1的裸机程序然后CPU0再启动Linux。这种方式裸机启动更快但需要确保裸机程序不会访问Linux还没初始化好的共享资源。我倾向于第一种方式因为调试起来更方便——Linux先起来你可以用SSH或者串口登录进去再用命令手动启动CPU1出问题了也好排查。唤醒CPU1的具体操作是往0xFFFFFFF0地址写入CPU1的入口地址然后执行SEV指令。这个操作在Linux用户空间可以通过/dev/mem映射来实现也可以写一个简单的内核模块。我通常写一个字符设备驱动来做这件事因为内核模块里操作更干净不需要处理用户空间映射的各种权限问题。3. 核心细节解析与实操要点3.1 共享内存通信协议的设计共享内存是AMP方案里两个核交换数据的唯一通道协议设计得好不好直接决定了整个系统的稳定性和可维护性。我见过有人直接用两个全局变量做通信结果缓存一致性问题导致数据永远不同步。也见过有人设计了复杂的环形缓冲区结果调试的时候根本不知道数据到底写进去没有。我的建议是采用“控制结构数据区”的布局。控制结构放在共享内存的最前面包含读写索引、状态标志、校验字段。数据区跟在后面按固定大小的槽位划分。这种设计的好处是逻辑清晰调试的时候用内存查看工具一看就知道当前状态。具体结构定义大概长这样typedef struct { volatile uint32_t magic; // 0x414D5000用于校验 volatile uint32_t tx_index; // 发送索引 volatile uint32_t rx_index; // 接收索引 volatile uint32_t status; // 状态标志 volatile uint32_t reserved[12]; // 预留 } amp_ctrl_t; typedef struct { uint32_t cmd; uint32_t param[7]; } amp_slot_t; // 共享内存总布局 // [amp_ctrl_t][amp_slot_t x 64][预留空间]这里有几个关键点需要注意。第一所有共享内存中的变量都必须用volatile修饰防止编译器优化掉看似“多余”的读写操作。第二控制结构的大小要是缓存行大小的整数倍避免两个核同时访问同一个缓存行导致伪共享。第三magic字段用于初始化检测裸机侧启动时先检查这个值如果是0就说明共享内存还没被初始化需要先清零。3.2 缓存一致性问题的处理这是AMP方案里最容易翻车的地方。A9核有独立的L1缓存和共享的L2缓存两个核各自看到的内存视图可能不一致。你往共享内存写了一个值另一个核可能读到的是旧数据因为它自己的缓存里还存着老值。解决这个问题有两个层面。硬件层面ZYNQ的SCUSnoop Control Unit会自动维护两个核之间的缓存一致性但这个“自动”是有条件的——它只对Normal Memory且标记为Shareable的区域生效。所以你在MMU配置里必须把共享内存区域标记为Shareable否则SCU不会去snoop这块地址。软件层面即使硬件保证了最终一致性你仍然需要内存屏障来保证访问顺序。比如你先写数据再写索引如果没有屏障另一个核可能先看到索引更新再看到数据更新读出来的就是脏数据。ARM架构下用DMB指令做全屏障DSB做数据同步屏障ISB做指令同步屏障。在Linux内核里对应的是smp_mb()、smp_wmb()、smp_rmb()这些宏。我在实际项目中的做法是共享内存区域在MMU里配置为Normal Non-cacheable也就是完全不走缓存。这样做性能会差一点但省去了所有缓存一致性的烦恼。对于控制指令这种数据量不大、实时性要求高的场景Non-cacheable反而是更稳妥的选择。如果你确实需要缓存带来的性能那就必须严格使用屏障指令并且确保两个核的MMU配置完全一致。3.3 中断控制器的配置细节核间中断是AMP方案里另一个关键机制。Linux侧需要通知裸机侧“有新的控制指令了”裸机侧需要通知Linux侧“控制周期完成了”。ZYNQ的GICGeneric Interrupt Controller支持16个SGISoftware Generated Interrupt专门用于核间通信。SGI的编号从0到15我通常把0号分配给Linux到裸机的通知1号分配给裸机到Linux的通知。配置SGI需要操作GIC的寄存器在Linux侧可以通过irqchip驱动提供的接口来发送在裸机侧则需要直接操作GICD_SGIR寄存器。Linux侧发送SGI的代码大概是这样// 在内核模块中 #include linux/irq.h #include linux/smp.h void send_sgi_to_cpu1(int sgi_num) { // 构造SGI请求目标CPU1中断号sgi_num writel_relaxed((1 1) | (sgi_num 24), gic_dist_base GICD_SGIR); }裸机侧接收SGI需要先配置GIC的CPU接口使能对应的SGI中断然后注册中断处理函数。这里有个容易忽略的点SGI是边沿触发的你在处理函数里必须清除中断状态否则会反复进入中断。清除操作是往GICD_CPENDSGIR寄存器写对应位。裸机侧发送SGI给Linux则简单一些直接写GICD_SGIR寄存器目标设为CPU0。Linux侧需要提前注册好对应的中断处理函数这个可以在设备树里声明也可以在内核模块里用request_irq()注册。3.4 裸机侧的时间片调度设计裸机程序虽然叫“裸机”但不意味着只能跑一个死循环。在实际项目中裸机侧往往需要同时处理多个任务PID控制环路、传感器数据采集、通信协议处理、故障保护逻辑。这时候就需要一个简单的时间片调度器。我的做法是用SysTick定时器产生1毫秒的基准节拍然后在主循环里按时间片轮转执行各个任务。每个任务分配固定的时间片比如PID控制2毫秒、传感器采集1毫秒、通信处理1毫秒。任务函数执行完后主动返回调度器根据当前节拍决定下一个执行哪个任务。这种调度方式的优点是实现简单、执行路径确定、不需要复杂的上下文切换。缺点是任务必须是非阻塞的任何一个任务执行时间过长都会影响其他任务的实时性。所以在裸机侧写代码时要特别注意避免忙等待和长循环。typedef struct { void (*task_func)(void); uint32_t period_ms; uint32_t last_run; } task_t; task_t tasks[] { {pid_control_task, 2, 0}, {sensor_sample_task, 1, 0}, {comm_process_task, 1, 0}, {fault_check_task, 10, 0}, }; void main_loop(void) { uint32_t now get_tick(); for (int i 0; i ARRAY_SIZE(tasks); i) { if (now - tasks[i].last_run tasks[i].period_ms) { tasks[i].task_func(); tasks[i].last_run now; } } }这个调度器的精度取决于SysTick的配置和主循环的执行时间。如果主循环本身耗时超过1毫秒那时间片就不准了。所以主循环里除了任务调度不要放其他逻辑所有耗时操作都放到任务函数里。4. 实操过程与核心环节实现4.1 Vivado工程的地址分配在Vivado里创建ZYNQ7020工程后打开Block Design双击ZYNQ Processing System进入Address Editor。这里需要确保DDR的地址范围覆盖你计划给Linux和裸机使用的全部区域。默认情况下DDR的地址范围是0x00000000到0x3FFFFFFF1GB如果你用的是512MB的DDR范围就是0x00000000到0x1FFFFFFF。地址分配本身在Vivado里不需要做特殊配置因为两个核共享同一片DDR。真正需要配置的是在FSBL和应用程序里。不过有一个地方需要注意如果你在PL端有自定义IP并且这个IP需要访问DDR那你要确保IP的地址映射不会和裸机使用的区域冲突。导出硬件后在PetaLinux里创建工程导入hdf文件。PetaLinux会自动生成设备树但默认的设备树是给单核Linux用的你需要手动修改。4.2 Linux设备树的修改设备树里需要做三件事第一在memory节点里把裸机占用的内存区域排除掉第二添加共享内存的保留区域第三添加核间中断的配置。memory节点的修改memory0 { device_type memory; reg 0x0 0x30000000; // Linux只使用前768MB }; reserved-memory { #address-cells 1; #size-cells 1; ranges; amp_shared: amp_shared30000000 { reg 0x30000000 0x00100000; no-map; }; amp_baremetal: amp_baremetal30100000 { reg 0x30100000 0x000C0000; no-map; }; };no-map属性告诉Linux内核不要为这块区域建立页表映射这样Linux就完全不会碰这块内存。但这也意味着Linux用户空间无法直接访问共享内存你需要在内核模块里用ioremap()来映射。核间中断的配置在GIC节点里gic: interrupt-controllerf8f01000 { compatible arm,cortex-a9-gic; #interrupt-cells 3; interrupt-controller; reg 0xf8f01000 0x1000, 0xf8f00100 0x100; };SGI不需要在设备树里显式声明内核启动后会自动初始化GIC的SGI部分。你只需要在内核模块里调用相应的API来注册处理函数和发送中断。4.3 裸机工程的链接脚本裸机工程的链接脚本决定了代码和数据放在哪个地址。基于前面的内存划分链接脚本大概是这样MEMORY { code_ram : ORIGIN 0x30100000, LENGTH 0x80000 stack_ram : ORIGIN 0x30180000, LENGTH 0x40000 } SECTIONS { .text : { *(.vectors) *(.text) *(.text.*) } code_ram .rodata : { *(.rodata) *(.rodata.*) } code_ram .data : { *(.data) *(.data.*) } code_ram .bss : { *(.bss) *(.bss.*) *(COMMON) } code_ram .stack : { . ALIGN(8); _stack_start .; . 0x10000; _stack_end .; } stack_ram }这里把代码和数据放在0x30100000开始的512KB区域堆栈单独放在0x30180000开始的256KB区域。堆栈单独划分是为了方便检测栈溢出——如果栈指针超出了_stack_end说明栈溢出了可以在异常处理里捕获。4.4 启动CPU1的具体实现Linux启动完成后需要有一个机制来加载裸机程序并启动CPU1。我的做法是写一个内核模块模块加载时完成以下操作用ioremap()映射裸机代码区和共享内存区把裸机ELF文件的内容拷贝到裸机代码区初始化共享内存的控制结构往0xFFFFFFF0写入CPU1入口地址执行SEV指令唤醒CPU1拷贝ELF文件的内容需要解析ELF格式提取各个段的数据。如果嫌麻烦也可以直接把裸机程序编译成二进制文件用objcopy生成bin文件然后整个拷贝过去。这种方式更简单但需要确保链接脚本里的地址和实际加载地址一致。// 内核模块中启动CPU1的代码片段 void __iomem *boot_addr ioremap(0xFFFFFFF0, 4); void __iomem *baremetal_base ioremap(0x30100000, 0x80000); // 拷贝裸机程序 memcpy_toio(baremetal_base, baremetal_bin, baremetal_bin_size); // 写入入口地址 writel_relaxed(0x30100000, boot_addr); // 内存屏障确保写入完成 wmb(); // 唤醒CPU1 asm volatile(sev : : : memory);这里有个细节0xFFFFFFF0这个地址是ZYNQ的BootROM里定义的CPU1入口地址寄存器不是随便选的。写进去的值必须是CPU1程序的入口地址而且这个地址必须是物理地址不能是虚拟地址。4.5 共享内存通信的完整实现共享内存通信的完整流程包括初始化、发送、接收三个环节。初始化在裸机启动时完成发送和接收在两个核之间交替进行。裸机侧的初始化代码#define SHARED_BASE 0x30000000 #define CTRL_MAGIC 0x414D5000 amp_ctrl_t *ctrl (amp_ctrl_t *)SHARED_BASE; amp_slot_t *slots (amp_slot_t *)(SHARED_BASE sizeof(amp_ctrl_t)); void amp_init(void) { if (ctrl-magic ! CTRL_MAGIC) { // 首次初始化 memset(ctrl, 0, sizeof(amp_ctrl_t)); memset(slots, 0, sizeof(amp_slot_t) * 64); ctrl-magic CTRL_MAGIC; ctrl-tx_index 0; ctrl-rx_index 0; ctrl-status 0; } }发送数据的逻辑int amp_send(uint32_t cmd, uint32_t *params, int count) { uint32_t next (ctrl-tx_index 1) % 64; if (next ctrl-rx_index) { return -1; // 缓冲区满 } slots[ctrl-tx_index].cmd cmd; for (int i 0; i count i 7; i) { slots[ctrl-tx_index].param[i] params[i]; } dmb(); // 确保数据写入完成 ctrl-tx_index next; dmb(); return 0; }接收数据的逻辑int amp_recv(uint32_t *cmd, uint32_t *params) { if (ctrl-rx_index ctrl-tx_index) { return -1; // 无数据 } dmb(); *cmd slots[ctrl-rx_index].cmd; for (int i 0; i 7; i) { params[i] slots[ctrl-rx_index].param[i]; } ctrl-rx_index (ctrl-rx_index 1) % 64; dmb(); return 0; }Linux侧的实现逻辑完全一样只是内存访问方式不同——Linux侧需要通过ioremap映射后的虚拟地址来访问。另外Linux侧在发送SGI之前需要确保数据已经写入共享内存所以wmb()是必须的。5. 常见问题与排查技巧实录5.1 共享内存数据不同步这是最常见的问题表现是裸机侧读到的数据始终是旧值或者偶尔读到半新半旧的数据。排查思路分三步走。第一步确认MMU配置。在裸机侧检查共享内存区域的页表项确保标记为Shareable。在Linux侧检查设备树里的no-map属性是否生效用cat /proc/iomem看看保留区域是否正确。第二步确认屏障指令。在写索引之前加DMB在读数据之前也加DMB。很多人只在一侧加屏障另一侧不加结果就是单向不同步。第三步确认缓存属性。如果共享内存是Cacheable的两个核的缓存属性必须一致。我建议直接用Non-cacheable省去所有麻烦。在裸机侧把页表项配置为Normal Non-cacheable在Linux侧用ioremap_nocache()来映射。5.2 CPU1启动后无响应往0xFFFFFFF0写入入口地址并执行SEV后CPU1没有任何反应。可能的原因有几个。入口地址不对。检查写入的地址是否和裸机链接脚本里的起始地址一致。注意这个地址必须是物理地址如果你的裸机程序是位置无关的那入口地址就是代码段的起始地址。CPU1的时钟没使能。在FSBL或者Linux启动脚本里检查CPU1的时钟是否打开。ZYNQ的CPU时钟默认是打开的但如果你在FSBL里做了电源管理配置可能会关掉CPU1的时钟。裸机程序跑飞了。在裸机程序的入口处加一个GPIO翻转或者串口输出确认程序是否真的开始执行。如果没有任何输出可能是向量表没配置对或者堆栈指针没初始化。5.3 中断丢失或重复触发SGI中断丢失通常是因为中断状态没清除。在裸机侧的中断处理函数里处理完业务逻辑后必须往GICD_CPENDSGIR寄存器写对应位来清除中断。如果不清除GIC会认为中断还在pending状态不会再次触发。中断重复触发则可能是另一个原因你在处理函数里清除了中断但发送方在极短时间内连续发送了多次SGI。GIC对于同一个SGI如果上一次还没处理完就又来了一次第二次会被丢弃。解决办法是在协议层面加确认机制发送方等待接收方的ACK后再发下一次。5.4 Linux侧访问共享内存导致系统崩溃Linux用户空间直接通过/dev/mem访问共享内存区域如果这块区域在设备树里标记了no-map内核不会为它建立页表映射用户空间访问会触发缺页异常严重时导致内核崩溃。正确的做法是在内核模块里用ioremap()映射然后通过字符设备接口暴露给用户空间。或者更简单的方式把共享内存的访问逻辑全部放在内核模块里用户空间通过ioctl来收发数据。5.5 常见问题速查表现象可能原因排查方法解决方案共享内存数据不同步缓存一致性问题检查MMU配置和屏障指令改为Non-cacheable或加DMBCPU1无响应入口地址错误对比链接脚本和写入值修正入口地址CPU1无响应时钟未使能检查FSBL电源配置使能CPU1时钟SGI中断丢失中断状态未清除检查GICD_CPENDSGIR在处理函数中清除SGI重复触发发送频率过高检查发送方逻辑加ACK确认机制Linux崩溃no-map区域被访问检查/dev/mem映射改用内核模块访问裸机程序跑飞堆栈溢出检查栈指针范围增大堆栈或优化代码系统启动失败内存区域重叠对比三方地址配置统一内存划分5.6 几个容易被忽略的实操心得第一个心得在裸机侧尽量少用浮点数。A9的浮点单元在裸机模式下需要手动使能而且浮点运算的上下文保存会增加中断延迟。如果PID控制可以用定点数实现尽量用定点数。第二个心得共享内存的槽位大小要仔细选择。太小了不够用太大了浪费内存。我的经验是8个32位参数加一个命令字总共36字节对齐到64字节。这样每个槽位正好占一个缓存行避免伪共享。第三个心得调试阶段可以在共享内存里加一个心跳计数器裸机侧每个控制周期加一Linux侧定期读取。如果计数器不走了说明裸机侧挂了。这个简单的机制在排查问题时非常有用。第四个心得Linux侧的SGI发送函数不要在中断上下文里调用因为writel_relaxed在某些平台上可能睡眠。如果确实需要在中断里发送用writel()而不是writel_relaxed()并且确保GIC的寄存器映射是Non-cacheable的。第五个心得裸机程序的向量表必须放在代码段的最前面而且要在链接脚本里显式指定。如果向量表被链接器放到别的位置中断触发后CPU会跳到错误的地址直接跑飞。6. 工程构建与版本管理建议6.1 工具链版本的选择ZYNQ7020的AMP方案对工具链版本比较敏感。我用的组合是Vivado 2018.3 PetaLinux 2018.3 Xilinx SDK 2018.3这个版本组合比较稳定社区资料也多。如果你用更新的版本比如2020.x或2021.xFSBL和设备树的生成方式有一些变化需要相应调整。裸机侧的编译器用SDK自带的arm-none-eabi-gccLinux侧用PetaLinux自带的aarch64-linux-gnu-gcc或者arm-linux-gnueabihf-gcc。注意两个工具链的ABI要一致否则共享内存里的数据结构对齐方式可能不同。6.2 工程目录的组织我习惯把整个AMP工程分成三个独立的子工程Vivado硬件工程、PetaLinux软件工程、裸机工程。每个子工程独立管理通过一个顶层Makefile来协调构建流程。amp_project/ ├── vivado/ # Vivado硬件工程 │ ├── project.xpr │ └── src/ ├── petalinux/ # PetaLinux软件工程 │ ├── project-spec/ │ └── images/ ├── baremetal/ # 裸机工程 │ ├── src/ │ ├── linker.ld │ └── Makefile ├── kernel_module/ # 内核模块 │ ├── amp_driver.c │ └── Makefile └── Makefile # 顶层构建脚本顶层Makefile负责按顺序构建各个子工程并生成最终的BOOT.bin。BOOT.bin里包含FSBL、bitstream、Linux内核、设备树和根文件系统。裸机程序不放在BOOT.bin里而是放在根文件系统里由内核模块在运行时加载。6.3 版本兼容性注意事项不同版本的PetaLinux在设备树生成上有差异。2018.3版本的PetaLinux会自动生成一个完整的设备树你只需要在system-user.dtsi里添加修改。2020.x之后的版本把设备树拆分成多个文件修改方式更灵活但也更复杂。内核模块的编译需要指定内核源码路径。在PetaLinux工程里内核源码在build/tmp/work/目录下路径比较深。我通常用petalinux-build -c kernel -x compile来编译内核模块或者手动指定KERNEL_SRC环境变量。裸机工程的启动代码在不同SDK版本里也有差异。2018.3的SDK生成的启动代码里已经包含了GIC和定时器的初始化你只需要添加自己的业务逻辑。如果你用2020.x的Vitis启动代码的结构变了需要重新适配。7. 性能实测与优化方向7.1 核间通信延迟实测我在实际硬件上测过核间通信的延迟。测试方法是裸机侧收到SGI后立即翻转一个GPIO用示波器测量Linux侧发送SGI到GPIO翻转的时间差。实测结果如下通信方式平均延迟最大延迟共享内存SGI8微秒15微秒共享内存轮询3微秒5微秒纯SGI无数据5微秒10微秒轮询方式延迟更低但会占用CPU资源。SGI方式延迟稍高但CPU利用率低。实际项目中我通常用SGI方式因为8微秒的延迟对于大多数控制场景已经足够了。7.2 控制环路的实时性验证裸机侧跑PID控制环路的实时性我用SysTick定时器产生1毫秒的节拍在中断处理函数里翻转GPIO用示波器测量GPIO方波的抖动。实测抖动在正负2微秒以内这个精度对于电机控制、电源控制这类应用完全够用。如果控制周期要求更短比如100微秒那就不能用SysTick了需要用PL端的定时器或者CPU的私有定时器。私有定时器的精度更高但配置也更复杂。7.3 进一步优化的方向如果8微秒的通信延迟还是不能满足要求可以考虑几个优化方向。第一把共享内存改成PL端的BRAM两个核通过AXI总线访问延迟可以降到2微秒以内。第二用PL端的自定义IP来做协议转换把核间通信的协议处理卸载到硬件。第三如果控制逻辑本身可以硬件化直接放到PL端用状态机实现那就完全不需要CPU参与了。不过这些优化都有代价。BRAM方案需要修改硬件设计自定义IP方案需要写Verilog硬件化方案开发周期最长。我的建议是先用手头的方案把功能跑通实测延迟确实不满足要求了再考虑优化。8. 实际项目中的经验总结8.1 调试手段的建立AMP方案的调试比单核系统复杂得多因为两个核各自跑各自的出了问题很难判断是哪边的问题。我通常会在项目初期就建立一套调试机制。串口输出是最基本的。Linux侧用printk裸机侧用自定义的串口打印函数。两个核的串口输出可以共用一个UART但需要加互斥保护否则输出会交错。我通常给裸机侧单独分配一个UART这样两边的输出互不干扰。GPIO翻转是最直观的。在关键代码路径上加GPIO翻转用示波器或者逻辑分析仪观察时序。这个方法对于分析实时性和执行顺序特别有效。共享内存里的调试变量是最灵活的。在共享内存里预留一块调试区域两个核都可以往里面写日志。Linux侧可以定期把调试区域的内容dump出来分析。8.2 代码审查的重点AMP方案的代码审查有几个特别需要注意的地方。第一检查所有共享内存的访问是否都有屏障保护。第二检查中断处理函数是否清除了中断状态。第三检查裸机侧是否有阻塞操作。第四检查两个核的内存访问是否有重叠。我见过一个项目裸机侧在中断处理函数里调用了一个忙等待的延时函数结果中断响应时间从几微秒变成了几百微秒。这种问题在代码审查时如果不仔细看很难发现。8.3 从原型到产品的注意事项原型阶段可以怎么方便怎么来但到了产品阶段有几个地方必须加固。第一共享内存的通信协议要加校验和重传机制防止数据损坏。第二裸机侧要加看门狗防止程序跑飞后无人处理。第三两个核的启动顺序要加超时检测防止一个核起不来另一个核死等。第四日志系统要完善产品现场出问题了要能追溯。我在一个实际产品项目里因为裸机侧没有加看门狗程序跑飞后电机失控差点出了安全事故。从那以后我在所有裸机项目里都会加独立看门狗而且喂狗操作放在最不可能出问题的地方。8.4 后续扩展的思路这套AMP框架搭好之后扩展起来就很方便了。如果你想加一个新的控制算法只需要在裸机侧添加对应的任务函数在共享内存协议里定义新的命令字。如果你想加一个新的通信接口比如CAN或者EtherCAT可以在Linux侧实现协议栈通过共享内存把数据转发给裸机侧执行。我目前正在做的一个项目就是在ZYNQ7020上跑Linux加裸机Linux侧负责MQTT上报和Web配置裸机侧负责三路电机的PID控制和编码器采集。整个系统跑下来控制周期500微秒抖动小于5微秒通信延迟平均10微秒。这个性能对于大多数工业控制场景已经绰绰有余了。最后分享一个小技巧如果你在裸机侧需要精确的微秒级延时不要用循环计数因为编译器优化和缓存命中率会影响循环的执行时间。用CPU的私有定时器或者PL端的定时器来做延时精度可以做到正负1微秒以内。这个技巧在实现精确的PWM输出或者脉冲计数时特别有用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询