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

RPMsg(实现A7-M4通信)

rpmsg是用于多处理器之间的信息传输它是基于共享内存来工作的对于不同核之间会划分一片区域用作共享内存包含vring0、vring1、数据缓冲池buffer pool其中buffer pool切分成了一个个512byte的小格子每个小格子中的16byte作为rpmsg的帧头496byte用于存放有效bytevring0是用于发送的队列vring1作为接收的队列每个vring包含desc、avail、used部分其中vring-desc为描述符表是一个数组每个元素记录了一个数据buffer的物理地址、存储长度、接下来存放的buffer号vring-avail是可用环发送方准备好数据后会写入可用环告诉接收方哪些描述符已经装好了数据可以读了还有接下来可写的buffer号vring-used是已用环接收方使用完数据后可以写入已用环告诉发送方哪些描述符已经读完了内存可以进行回收了VRing 在物理内存中 100% 是连续的数组但在逻辑使用上结合了“静态链表”与“环形队列”desc数组通过next索引实现静态链表串联avail与used则是通过累加索引idx实现的环形 FIFO 队列。一、 VRing 在物理共享内存中的整体排布一个完整的 VRing 在共享内存中是连续存放的三大结构体末尾紧接着数据缓冲池----------------------------------------------------------------------------------------------- | 物理共享内存连续空间 | -------------------------------------------------------------------------------------------- | 1. vring_desc 数组 | 2. vring_avail 结构体 | 3. vring_used 结构体 | 4. Buffer Pool (数据池) | | (描述符表: N 项) | (可用环: 包含 ring[N])| (已用环: 包含 ring[N])| (切分成 N 个 512B 缓冲区) | --------------------------------------------------------------------------------------------二、 三大核心结构体内部排布与图解1. 描述符表vring_desc固定长度的静态数组在内存中是一个长度为 $N$如 256的结构体数组。每个元素占用 16 字节记录对应 Buffer 的物理位置与串联关系struct vring_desc 数组 (下标 0 ~ N-1) ┌───────┬──────────────────┬──────────┬──────────┬──────────┐ │ Index │ addr (64位) │ len (32位│flags(16位│ next(16位│ ─── 物理上是一块连续数组 ├───────┼──────────────────┼──────────┼──────────┼──────────┤ │ desc 0│ 0x2FFC 0000 ────┼──────────┼──────────┼──── 1 ───┼──┐ 指向下一个 desc 索引 │ desc 1│ 0x2FFC 0200 ────┼──────────┼──────────┼──── 2 ───┼──┼──┐ (静态链表机制) │ desc 2│ 0x2FFC 0400 ────┼──────────┼──────────┼── 0xFFFF ┼──┤ │ (0xFFFF 表示链表尾) │ ... │ ... │ ... │ ... │ ... │ │ │ │descN-1│ 0x2FFC E000 │ 512 │ 0 │ 0xFFFF │ │ │ └───────┴────────┬─────────┴──────────┴──────────┴──────────┘ │ │ │ │ │ ▼ (指向真实的物理内存块) ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Buffer 0 │ (512 字节) │ 多个描述符拼接│ ├──────────────┤ │ 存放超长数据 │ │ Buffer 1 │ (512 字节) └──────────────┘ └──────────────┘addr该 Buffer 在共享内存中的物理地址起始指针。len该 Buffer 区域的长度单块通常为 512 字节。flagsVRING_DESC_F_NEXT (1)当前数据超过 512 字节通过next字段链接了下一个 desc。VRING_DESC_F_WRITE (2)标记此 Buffer 对接收方是只读还是只写。next链表指针记录下一个 desc 数组下标。空闲时通过next串联成空闲链表拼接大数据时串联成数据链表。2. 可用环vring_avail发送方写的就绪队列发送方如 A7装好数据后将 desc 编号写入此结构体通知对方取件struct vring_avail ┌──────────────────────────────────────────────────────────────┐ │ flags (16位) : 是否禁止对方产生中断通知 (0: 允许, 1: 屏蔽) │ ├──────────────────────────────────────────────────────────────┤ │ idx (16位) : 生产者计数器 (单调递增: 0, 1, 2, 3, 4 ... ) │ ├──────────────────────────────────────────────────────────────┤ │ ring[N] 数组 : 记录待处理的 desc 索引值 │ │ ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────┐ │ │ │ ring[0] │ ring[1] │ ring[2] │ ring[3] │ ... │ring[N] │ │ │ │ 0 │ 5 │ 2 │ (等待) │ │ │ │ │ └────┬────┴────┬────┴────┬────┴─────────┴─────────┴────────┘ │ └──────┼─────────┼─────────┼───────────────────────────────────┘ ▼ ▼ ▼ desc[0] desc[5] desc[2] (告诉接收方依次去读这几个描述符)idx发送方每塞入一个新数据包idx就会自增 1。ring[]一个环形索引数组。例如ring[0] 5代表“排在第 0 位的任务在desc[5]对应的 Buffer 里”。3. 已用环vring_used接收方写的完成队列接收方如 M4消费完数据后将处理结果填入此结构体通知发送方释放内存struct vring_used ┌──────────────────────────────────────────────────────────────┐ │ flags (16位) : 是否禁止对方产生中断通知 │ ├──────────────────────────────────────────────────────────────┤ │ idx (16位) : 消费者计数器 (单调递增: 0, 1, 2, 3, 4 ... ) │ ├──────────────────────────────────────────────────────────────┤ │ ring[N] (元素为 struct vring_used_elem 数组) │ │ ┌──────────────────────┬──────────────────────┬────────────┐ │ │ │ ring[0] │ ring[1] │ ... │ │ │ │ ├─ id 0 (已读完) │ ├─ id 5 (已读完) │ │ │ │ │ └─ len 64 (有效长) │ └─ len 128 │ │ │ │ └───────────┬──────────┴───────────┬──────────┴────────────┘ │ └─────────────┼──────────────────────┼─────────────────────────┘ ▼ ▼ 释放 desc[0] 释放 desc[5]id对应被处理完毕的vring_desc下标。len接收方实际写入或读取的字节数。三、 整体协作流动全景图以A7 发送一条步态指令给 M4为例[ A7: 产生新数据 ] │ ▼ 1. 从空闲链表取出一个未使用的 desc (假设下标为 3) 把 496 字节步态数据写入 desc[3].addr 所指向的 Buffer[3] │ ▼ 2. 写入 vring_avail: avail.ring[avail.idx % N] 3; avail.idx; │ ▼ 3. 触发片内核间中断 (IPCC Mailbox 门铃) ───[硬件中断]───► M4 收到中断进入 ISR │ ▼ 4. M4 读取 avail.ring 发现有索引 3 直接读取 Buffer[3] 物理内存数据 交由 1 kHz 控制环路去执行 │ ▼ 5. 消费完毕写入 vring_used: used.ring[used.idx % N].id 3; used.idx; │ ▼ A7 收到回传中断 ◄────────────────────────────── 6. M4 触发核间中断通知 A7 │ ▼ 7. A7 检查 used.ring 发现 id3 已被读完 将 desc[3] 放回空闲链表完成 Buffer 回收循环四、 核心概念速查表面试问答标准口径结构名称物理本质逻辑角色核心包含内容vring_desc固定长度结构体数组储物柜信息清单兼静态链表addr物理地址指针、len长度、next链表下个索引vring_avail结构体含ring[]数组待取件任务环发送方推入idx生产计数器、ring[]就绪的 desc 编号vring_used结构体含元素数组已完成签收环接收方推入idx消费计数器、id已处理完毕的 desc 编号Buffer Pool连续物理内存大块真实数据存放箱切割成 $N$ 个固定 512 字节的小块16B 报头 496B 负载硬件中断的产生机制STM32MP157 IPCC 模块数据写入共享内存后中断是通过芯片内部的硬件邮箱外设——IPCCInter-Processor Communication Controller产生的A7 写寄存器拉高电平A7 准备好 Buffer 并更新完avail.idx后Linux 内核驱动会向 IPCC 寄存器如IPCC_C1SCR的对应通道位写1。硬件电信号触发IPCC 硬件电路检测到写操作后直接向 Cortex-M4 的中断向量控制器NVIC拉起硬件中断信号线。M4 进入 ISR 唤醒任务M4 硬件捕获到中断立即暂停当前低优先级任务跳转执行IPCC_RX_IRQHandler()在中断里释放 FreeRTOS 二值信号量唤醒后台 RPMsg 处理任务去读 VRing。反向通知释放M4 消费完成后同理向 IPCC 对应通道写寄存器触发 A7 的通用中断控制器GIC通知 A7 回收 Buffer。RPMsg / VRing 核心结构体与内存定义1. 核心结构体定义标准 OpenAMP / Linux 内核规范#include stdint.h #include stdbool.h #define VRING_SIZE 256 // 队列长度: 256 项 #define RPMSG_BUF_SIZE 512 // 单个 Buffer 大小: 512 字节 #define RPMSG_HEADER_SIZE 16 // RPMsg 报头: 16 字节 #define RPMSG_PAYLOAD_SIZE (RPMSG_BUF_SIZE - RPMSG_HEADER_SIZE) // 496 字节 #define VRING_DESC_F_NEXT 1 // 标志位: 存在下一个链接的描述符 #define VRING_DESC_F_WRITE 2 // 标志位: 缓冲区对设备只写 // 1. RPMsg 数据报头 (16 字节) #pragma pack(push, 1) struct rpmsg_hdr { uint32_t src; // 源端口号 uint32_t dst; // 目的端口号 uint32_t reserved; // 保留字段 uint16_t len; // 有效载荷(Payload)实际长度 uint16_t flags; // 标志位 uint8_t data[]; // 真实载荷起始地址 (最多 496 字节) }; // 2. 描述符表项 (16 字节) struct vring_desc { uint64_t addr; // 物理 Buffer 的基地址 uint32_t len; // Buffer 块长度 (512) uint16_t flags; // 描述符标志 uint16_t next; // 链表指向的下一个 desc 下标 }; // 3. 可用环 (Avail Ring) struct vring_avail { uint16_t flags; // 0: 允许对方产生中断; 1: 屏蔽中断 uint16_t idx; // 生产者计数器 (单调递增) uint16_t ring[VRING_SIZE]; // 存放待消费 desc 索引的环形数组 }; // 4. 已用环元素与已用环 (Used Ring) struct vring_used_elem { uint32_t id; // 处理完毕的 desc 索引 uint32_t len; // 处理的数据总长度 }; struct vring_used { uint16_t flags; // 0: 允许产生中断通知 uint16_t idx; // 消费者计数器 (单调递增) struct vring_used_elem ring[VRING_SIZE]; // 已完成项的环形数组 }; #pragma pack(pop)2. 共享内存区域的物理布局与内存映射// 完整的单向 VRing 内存布局结构体 #pragma pack(push, 1) typedef struct { // 控制面 struct vring_desc desc_table[VRING_SIZE]; // 256 * 16B 4096B // 这是定义一个vring_desc结构体类型的数组 数组中每个元素都是这个结构体 struct vring_avail avail_ring; // 4B 256*2B 516B (需对齐) uint8_t pad0[3580]; // 填充对齐到 4KB 边界 struct vring_used used_ring; // 4B 256*8B 2052B uint8_t pad1[2044]; // 填充对齐到 4KB 边界 // 数据面 uint8_t buffer_pool[VRING_SIZE][RPMSG_BUF_SIZE]; // 256 * 512B 128KB } VringMemoryLayout_t; #pragma pack(pop) // 假设共享内存基地址 (通过设备树 reserved-memory 分配) #define SHM_VRING0_BASE_ADDR 0x2FFC0000 // 实例化指针指向物理映射区 static volatile VringMemoryLayout_t *g_vring0 (VringMemoryLayout_t*)SHM_VRING0_BASE_ADDR; static uint16_t g_free_head 0; // 空闲链表头 static uint16_t g_last_avail_idx 0; // 接收端上次处理到的位置写入过程代码发送端 A7 / 生产者写入过程包含 5 个核心步骤分配空闲 desc ---- 填充报文数据 ---- 挂入avail.ring---- 累加avail.idx---- 触发 IPCC 硬件中断。// 硬件邮箱触发宏 (向 IPCC 寄存器写通道 1) #define IPCC_CPU1_TOCPU2_TRIGGER() (*((volatile uint32_t*)0x5C00100C) (1 0)) int rpmsg_send_packet(uint32_t src, uint32_t dst, const void *payload, uint16_t payload_len) { if (payload_len RPMSG_PAYLOAD_SIZE) { return -1; // 超过单包 496 字节上限 } // 1. 从空闲链表取出一个可用描述符 uint16_t desc_idx g_free_head; if (desc_idx 0xFFFF) { return -2; // 内存池已满无空闲 Buffer } g_free_head g_vring0-desc_table[desc_idx].next; // 更新空闲链表头 // 2. 定位对应的物理 Buffer 地址并装配 RPMsg 报文 uint8_t *buf_addr (uint8_t*)g_vring0-buffer_pool[desc_idx]; struct rpmsg_hdr *hdr (struct rpmsg_hdr*)buf_addr; hdr-src src; hdr-dst dst; hdr-len payload_len; hdr-flags 0; memcpy(hdr-data, payload, payload_len); // 拷贝有效载荷 // 3. 配置当前描述符的物理地址与长度 g_vring0-desc_table[desc_idx].addr (uint64_t)(uintptr_t)buf_addr; g_vring0-desc_table[desc_idx].len sizeof(struct rpmsg_hdr) payload_len; g_vring0-desc_table[desc_idx].flags 0; // 无链式下一包 // 4. 写入 Avail Ring 队列并更新生产者计数器 uint16_t avail_slot g_vring0-avail_ring.idx % VRING_SIZE; g_vring0-avail_ring.ring[avail_slot] desc_idx; // 内存屏障确保内存数据写入完成后再更新计数器 __sync_synchronize(); g_vring0-avail_ring.idx; // 5. 触发硬件中断通知 M4 if (!(g_vring0-avail_ring.flags 1)) { // 检查接收方未屏蔽中断 IPCC_CPU1_TOCPU2_TRIGGER(); } return 0; }读取过程代码接收端 M4 / 消费者读取过程在 M4 端由IPCC 硬件中断唤醒包含比对avail.idx---- 获取 desc 并解析数据 ---- 交付应用层 ---- 写入used.ring---- 触发中断通知 A7 回收。// 硬件邮箱清除中断与反向回传宏 #define IPCC_CPU2_CLEAR_RX_IRQ() (*((volatile uint32_t*)0x5C001014) (1 0)) #define IPCC_CPU2_TOCPU1_TRIGGER() (*((volatile uint32_t*)0x5C00101C) (1 0)) // 1. M4 硬件中断服务函数 (ISR) void IPCC_RX_IRQHandler(void) { IPCC_CPU2_CLEAR_RX_IRQ(); // 清除当前中断挂起标志 // 释放二值信号量唤醒 FreeRTOS 任务进行消费处理 BaseType_t xHigherPriorityTaskWoken pdFALSE; xSemaphoreGiveFromISR(xRpmsgSem, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } // 2. M4 端后台消费任务 void rpmsg_rx_task(void *pvParameters) { while (1) { // 等待 ISR 信号量 xSemaphoreTake(xRpmsgSem, portMAX_DELAY); // 检查是否有未处理的新消息 (对比 idx) while (g_last_avail_idx ! g_vring0-avail_ring.idx) { // 2.1 从 avail.ring 取出待处理的 desc 索引 uint16_t avail_slot g_last_avail_idx % VRING_SIZE; uint16_t desc_idx g_vring0-avail_ring.ring[avail_slot]; // 2.2 根据描述符地址直接读取 Buffer struct rpmsg_hdr *hdr (struct rpmsg_hdr*)(uintptr_t)g_vring0-desc_table[desc_idx].addr; // 2.3 根据目的端口分发处理数据 (如 1kHz 控制环路执行) if (hdr-dst 50) { process_robot_joint_angles(hdr-data, hdr-len); } // 2.4 填入 Used Ring表示当前 desc 已消费完毕 uint16_t used_slot g_vring0-used_ring.idx % VRING_SIZE; g_vring0-used_ring.ring[used_slot].id desc_idx; g_vring0-used_ring.ring[used_slot].len sizeof(struct rpmsg_hdr) hdr-len; __sync_synchronize(); g_vring0-used_ring.idx; g_last_avail_idx; // 本地消费游标自增 } // 2.5 触发 IPCC 中断通知 A7 释放对应 Buffer IPCC_CPU2_TOCPU1_TRIGGER(); } }速记要点中断产生路径写入数据----更新avail.idx----写IPCC寄存器----硬件拉高对方NVIC中断线----触发 ISR 唤醒任务。零拷贝读写A7 只是将结构体直接写入预先映射好的物理内存buffer_pool传给 M4 的仅仅是desc_idx下标M4 通过物理地址直接读取无需内核向用户空间反复拷贝数据。这一步是分配内存了吗没有分配内存。这行代码的本质是“绝对地址寻址指针强制类型转换”而不是内存分配。static volatile VringMemoryLayout_t *g_vring0 (VringMemoryLayout_t*)SHM_VRING0_BASE_ADDR;它做了什么g_vring0只是一个指针变量在 32 位系统上仅占用 4 字节的全局指针内存。这行代码直接把硬件物理地址常量如0x2FFC0000强转成结构体指针告诉编译器“把0x2FFC0000这个地址开始的内存空间按照VringMemoryLayout_t结构体的排布规则去解释”。真正的共享内存是谁分配的物理内存在系统启动前就已经存在了例如 STM32MP157 内部的SRAM4/SysRAM或是 Linux 设备树reserved-memory在 DDR 中强行抠出的一段物理连续内存。这里的代码只是在“认领”这块已存在的硬件物理空间。static的作用仅仅是限制g_vring0这个指针变量本身的作用域为当前.c文件内部链接避免与其他文件的同名全局变量冲突与共享内存本身的生命周期无关。加上volatile的真正作用是什么面试核心考点你说的“避免编译器优化、强制从内存读”完全正确。在异构双核A7 与 M4通信场景下如果不加volatile会引发致命的系统死锁。1. 致命场景死循环死锁编译器把变量缓存在寄存器中在接收端如 M4通常有一段轮询或判断新消息的代码while (g_last_avail_idx ! g_vring0-avail_ring.idx) { // 处理新消息 }如果不加volatile开启编译器-O2/-O3优化时编译器在分析 M4 的代码时发现当前循环体内部没有任何语句会去修改g_vring0-avail_ring.idx。编译器会“自作聪明”地认为“既然你这个 CPU 没改它那它的值就不会变为了省时间我只在进入循环前从内存读一次idx放到 CPU 寄存器R0里后面直接拿R0比较就行了。”后果即使 A7 在物理内存里把idx从 0 改成了 1M4 的 CPU 却永远只看寄存器R0值依然是 0M4 永远感知不到 A7 发来了新数据程序陷入永久死锁。加上volatile后强制告诉编译器“这个地址背后的数据随时可能被外部硬件或其他 CPU 核心修改”编译出的汇编指令在每次判断条件时必须执行LDRLoad Register指令穿透到物理内存去重新读取最新值。2. 防止编译器“指令重排”和“写操作被合并丢弃”防止写操作被优化消除如果代码连续向某个状态标志写值如先写0x00再写0x01没有volatile编译器可能认为前一次写入是“无用代码”Dead Code而直接优化删掉导致硬件或对方核心漏掉状态变迁。保证读写顺序不被乱序优化volatile确保编译器在生成汇编代码时严格按照 C 语言代码的书写顺序生成内存访问指令防止出现“先更新了avail.idx后写入 Buffer 数据”的致命时序错乱。加分拓展volatile与硬件 Cache 屏障Memory Barrier的区别如果面试官追问底层细节答出下面这一层会非常亮眼volatile解决的是“编译器级别”的优化问题保证编译器生成的汇编指令每次都老老实实读写内存指令LDR/STR不把变量死锁在 CPU 寄存器里。内存屏障如__sync_synchronize()/DMB指令解决的是“CPU 硬件流水线与缓存”问题A7 核心有 L1/L2 Cache 和乱序执行流水线即使编译器指令排对了CPU 硬件在执行时也可能把数据暂存在 Cache 中没刷入物理 SRAM。因此在双核通信中volatile防编译器优化 内存屏障/关闭共享内存 Cache防硬件乱序与缓存不一致是保证核间通信数据 100% 正确落盘的黄金组合。一、 CPU 读取缓存Cache机制CPU 执行指令的速度极快纳秒级而访问外部内存DDR/SRAM相对很慢几十到上百纳秒。为了消除这种“速度鸿沟”CPU 内部引入了L1、L2、L3 多级高速缓存SRAM 材质。[ CPU 寄存器 ] ◄── (1 cycle) ──► [ L1 Cache ] ◄── (3-5 cycles) ──► [ L2 Cache ] │ [ 主内存 (DDR/SRAM) ] ◄───────── (数十~上百 cycles) ─────────────► [ L3 Cache ]1. 核心基本单元Cache Line缓存行CPU 从内存读取数据时绝不是只读 1 个字节或 4 个字节而是以“Cache Line缓存行”为最小单位一次性打包搬运。在绝大多数现代 ARM如 Cortex-A7和 x86 处理器中一个 Cache Line 的大小固定为64 字节。两大局部性原理时间局部性刚刚被访问过的数据不久后大概率还会被访问保存在 Cache 中防止重复读内存。空间局部性访问了内存地址 $X$其相邻地址 $X1 \sim X63$ 的数据很大概率马上也会被访问一次性把整整 64 字节加载到 Cache。2. CPU 怎么在 Cache 里找数据寻址三部曲当 CPU 想要读取物理地址0x2FFC0040的变量时硬件会将 32 位地址拆分成三段31 12 11 6 5 0 ┌─────────────────────────────────┬──────────────┬──────────────┐ │ Tag (标签) │ Index (组索引)│ Offset (块内偏移) └─────────────────────────────────┴──────────────┴──────────────┘寻组Index根据地址中间的 Index 位定位到 Cache 的某一个集合Set。匹配Tag对比该集合中缓存行的 Tag。如果相等且有效位为 1说明Cache Hit命中直接把数据给 CPU耗时仅 1~3 个时钟周期。缺失Cache Miss如果 Tag 对不上触发缺失中断CPU 暂停流水线向总线发起请求从主内存读取整整 64 字节填入 Cache Line再返回给 CPU。二、 结构体中pad填充字节用于隔开的底层原理在之前的代码中看到了如下结构struct vring_avail avail_ring; // A7 写入, M4 读取 uint8_t pad0[3580]; // 填充对齐到 4KB / Cache Line 边界 struct vring_used used_ring; // M4 写入, A7 读取这里的padPadding绝不是浪费空间而是为了解决多核/异构系统中最致命的性能与数据一致性问题避免伪共享False Sharing和防止 Cache 回写覆盖。原理 1消除“伪共享False Sharing”【没有 pad 隔开的情况两个不同变量挤在同一个 64 字节 Cache Line】 ┌─────────────────────────────────────────────────────────────┐ │ 同一个 64 字节 Cache Line │ │ ┌─────────────────────────────┬───────────────────────────┐ │ │ │ 变量 A (Core 1 频繁修改) │ 变量 B (Core 2 频繁修改) │ │ │ └─────────────────────────────┴───────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ▲ ▲ │ (Core 1 改了 A导致整行失效) │ (Core 2 读写 B 频繁 Cache Miss)现象假设变量 Aavail_ring由 Core 1 写变量 Bused_ring由 Core 2 写。如果它们在物理内存上紧挨着被打包进了同一个 64 字节 Cache Line。后果Core 1 修改变量 A 时硬件一致性协议MESI会强制将 Core 2 中的整个 Cache Line 标记为无效Invalid。当 Core 2 想去读取或修改变量 B 时发现 Cache 失效被迫重新从内存慢速加载。两个核心在总线上来回争抢同一个 Cache Line 的所有权Cache 颠簸导致多核并行性能暴跌数十倍。原理 2异构双核A7 与 M4下的“Cache 回写踩踏”在 STM32MP157 异构双核场景下pad隔开更为关键A7 核心带 MMU 和二级 Data Cache。A7 写数据时通常采用Write-Back写回模式数据先暂存在 A7 的 Cache 中稍后整行64B刷回物理内存。M4 核心通常不带 Cache直接对物理 SRAM 进行读写。致命踩踏灾难如果avail_ringA7 写和used_ringM4 写放在同一个 64B Cache Line 内。M4 在物理内存中更新了used_ring的数据。随后A7 执行了 Cache 清洗FlushA7 Cache 里旧的used_ring历史数据会随着整整 64 字节一起强行覆写到物理 SRAM 中。结果M4 辛辛苦苦写好的最新数据被 A7 的脏 Cache 行瞬间覆盖篡改原理 3硬件内存边界对齐4KB / 64B 对齐MMU 页对齐4KBLinux 内核以 4KB 页面为单位管理虚拟内存。如果将avail和used隔开并对齐到 4KB 边界A7 可以将avail所在的页面映射为带 Cache 的加速区将used所在的页面配置为严禁 Cache 的 DMA/一致性内存区Non-cacheable。总线突发传输效率对齐到 32 字节或 64 字节边界的数据结构DMA 控制器与 AXI 总线能以单次 Burst 传输直接拉取吞吐率最高。标准回答模板提炼版“CPU 读缓存机制CPU 并非按单个字节读写而是基于时间与空间局部性原理以64 字节的 Cache Line为最小粒度从内存加载到 L1/L2 Cache 中。在共享内存结构体中使用pad填充隔开核心目的有三点避免多核‘伪共享’False Sharing确保不同核心独立写入的变量分布在不同的 Cache Line 中防止一方修改触发另一方 Cache 频繁失效颠簸防止异构双核 Cache 回写踩踏A7 开启 CacheM4 直写物理内存通过pad隔开可以防止 A7 在刷新 Cache Line 时意外将 M4 写入物理内存的数据覆盖满足硬件与 MMU 页对齐要求便于对不同区域施加独立的内存属性配置如 Cacheable 与 Non-cacheable提升总线突发传输效率。”一、 Cortex-M4 与 Cortex-A7 的 Cache 差异在 STM32MP157 芯片上你的理解完全正确Cortex-M4 确实没有 Data/Instruction Cache只有 Cortex-A7 才有多级 Cache。Cortex-M4无 Cache读什么就只读什么当 M4 执行一条 32 位的LDR汇编指令读取一个 4 字节的整型变量时总线AHB 总线就只向物理 SRAM 发起一次 4 字节的读取请求绝对不会自动多抓取后续的 64 字节。确定性极高硬实时因为没有 Cache 命中或缺失Miss带来的时间波动M4 每次读写内存的周期数都是固定且可预测的非常适合做 1 kHz 的硬实时闭环控制。Cortex-A7有 L1/L2 CacheA7 属于应用级处理器为了在跑 Linux 复杂操作系统时提升吞吐量配备了32 KB L1 指令 Cache、32 KB L1 数据 Cache 以及 256/512 KB L2 Cache。A7 每次发生 Cache Miss 时硬件会自动以64 字节Cache Line为单位打包加载。二、 什么是 MMU原理三4KB 对齐的通俗解释MMUMemory Management Unit内存管理单元是 Cortex-A7 内部的一个纯硬件模块专门负责两件事地址转换把 Linux 应用程序看到的“虚拟地址”翻译成芯片真实的“物理地址”。属性配置与权限管理给某块内存打上标签比如是否可读写、是否允许启用 Cache。为什么说 4KB 对齐很重要MMU 的管理粒度是 4KB一页MMU 在给内存配置属性如“开不开启 Cache”时最小只能按 4096 字节4KB为一整块来配置无法单独给某几个字节单独配置。如果avail_ring和used_ring挤在同一个 4KB 页面里A7 只能把这整个 4KB 统一设为“开启 Cache”或“关闭 Cache”。如果开启 CacheA7 读 M4 写的used_ring时容易读到 Cache 里的旧数据缓存不一致如果关闭 CacheA7 访问自己的数据速度又会变慢。用pad做 4KB 对齐的本质把它们彻底隔开在不同的 4KB 物理内存页上。这样 A7 就可以通过 MMU 把used_ring所在的 4KB 页面单独配置为Non-cacheable严禁使用 Cache强制直读物理内存从硬件根源上彻底杜绝读脏数据。三、 纠偏DDR、SRAM 与 Flash 的真实区别“DDR 属于存储、像 Flash 一样掉电不丢失”是严重的概念混淆务必纠正过来存储介质全称与本质掉电是否丢失读写速度在系统中的实际角色SRAM静态随机存取内存掉电丢失RAM极快纳秒级片内高速内存如 STM32 内部几十/几百 KB 的 RAM核间共享内存DDR (SDRAM)双倍速率同步动态内存掉电丢失RAM快百纳秒级系统主运行内存如开发板上 512MB/1GB 的内存条/芯片供 Linux 跑程序Flash (eMMC/NOR)闪存 / 非易失存储掉电不丢失ROM/Storage慢微秒/毫秒级外部硬盘/存储相当于电脑 SSD用来存放 Linux 内核镜像、文件系统、设备树DDR 属于真正的内存RAM断电后里面的数据瞬间全部清空它需要不断动态刷新电容来维持数据。Flash或 eMMC、SD 卡才属于“存储”代码和固件保存在 Flash 中开机时 Linux 内核被从 Flash 搬运到 DDR内存中运行。四、 RPMsg 的传输时间与传统总线对比RPMsg 几乎没有物理线缆上的串行传输时间。传统外设通信如 UART、CAN、SPI流程CPU 把数据写进外设发送 FIFO $\rightarrow$ 外设移位寄存器按照波特率如 UART 115200bps、CAN 1Mbps一位一位地在铜线上通过电平翻转慢慢发出去。延迟传输 100 字节数据UART 至少需要耗费接近10 毫秒硬件传输延迟极大。RPMsg 共享内存通信流程A7 执行memcpy把数据写入芯片内部的共享 SRAM走芯片内部数十 GHz 带宽的 64 位 AXI 总线耗时几纳秒A7 写一个 IPCC 寄存器耗时几个时钟周期IPCC 硬件逻辑瞬间直接把 M4 的中断信号线电平拉高M4 的 NVIC 中断控制器捕获中断进入中断服务函数ISR耗时数十纳秒。结论整个跨核交互在芯片物理总线上是“零线缆传输延迟”的。唯一的耗时只有内存拷贝的微秒级开销和操作系统上下文切换的时间通常总共在 $10 \sim 20\ \mu\text{s}$ 以内速度比传统的 CAN/串口快成百上千倍。
分享:

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

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