HI3556V200双系统核间通信:IPCM/virt_tty/sharefs选型与实战

📅 发布时间:2026/10/17 1:06:53
HI3556V200双系统核间通信:IPCM/virt_tty/sharefs选型与实战
做 HI3556V200 双系统开发的人基本都会在某个阶段被同一件事卡住A 核上 Linux 的业务和 M 核上 LiteOS 的实时逻辑到底怎么把消息和文件传过来尤其第一次看到 SDK 里同时出现 IPCM、virt_tty、sharefs 三个通信方案时心里大概率会冒出一串问号这三者有什么不同该用哪个能不能混着用我是在调一个视频分析模块的时候被这个问题绊住的。Linux 侧负责网络和算法调度LiteOS 侧负责底层采集和实时控制两侧必须交换参数和运行状态。最开始的方案是直接往固定内存地址写数结果 cache 一致性问题、地址冲突问题一个接一个冒出来最后才意识到这套平台上已经有现成的、经过验证的核间通信框架只是需要先搞清楚每个通道的脾气。这篇笔记就是把我在 IPCM / virt_tty / sharefs 三条通路上实际调试的经验整理出来给同样在啃双系统通信的人做个参照。1. 双系统的定位与通信需求先知道谁在说话1.1 两个 CPU 各管一摊HI3556V200 的双系统本质上就是一颗 SoC 里面放了两种不同定位的核心。A 核这一侧跑完整 Linux拥有网络协议栈、文件系统、丰富的用户态库适合做算法调度、网络服务、业务逻辑这些重而复杂的事情M 核这一侧跑 LiteOS任务切换快、中断响应确定、没有复杂内核机制兜底适合做寄存器级控制、高频采集、硬件事件响应这些快而固定的事情。我在实际项目里的感受是双系统开发最容易犯的错误不是把活分错了边而是把通信想得太简单。两边各自跑起来之后总有一些数据是必须跨核流动的。比如 LiteOS 侧检测到异常输入需要立刻上报给 Linux 侧Linux 侧下发了新参数LiteOS 侧要实时收到并生效一段调试日志两边都要能看一套算法模型文件Linux 侧下载后 LiteOS 侧要能访问。这些需求看似都是传数据但数据形态完全不同所以引出了三条不同的通道。1.2 三种通信通道的宏观定位在深入代码之前我建议先把这三者的定位用一个类比记下来IPCM 像打电话传达指令、状态、事件内容短强调要说清楚、有应答是控制面。virt_tty 像传真专线一条持续的双工字节流不管内容格式强调源源不断地传是数据面。sharefs 像共享文件夹把一块物理内存包装成文件系统Linux 侧可以像读写普通文件一样操作适合传大块静态数据比如日志文件、模型文件、配置文件。这个类比在选型时非常管用。我见过有人在 IPCM 上硬传大块数据结果把消息队列撑爆也见过有人用 sharefs 做高频小消息交互结果文件读写开销远大于通信本身。数据形态和通道形态对不上后面全是坑。另外还要明确一点这三条通信通道并不是只能选其一。实际项目中它们往往是配合使用的后面第五部分我会讲我在项目里的具体组合方式。2. IPCM 通道控制消息的主干道2.1 IPCM 的设计形态与初始化细节IPCMInter-Processor Communication Module在 SDK 里对应的是一套完整的核间消息传递框架。它不走普通内存共享的裸读写而是把消息按照邮箱或者消息队列的语义来管理。每次发送的基本单元是一条带消息 ID、源地址、目的地址、长度和内容数据的消息。接收方按消息 ID 注册回调消息到达后被框架主动派发。我理解 IPCM 的设计初衷是它希望两侧开发者不要直接操弄物理地址而是像调用一个模块接口一样完成跨核通信。Linux 侧的设备树里通常有一个专用节点节点里描述 IPCM 所需的中断号、寄存器地址、内存池大小等资源。初始化时驱动要把本端可用的消息通道注册进去并指定消息池的深度和单条消息的最大长度。这里有一处容易忽略的配置。消息池深度和单条消息长度直接影响收发成功率而不是越大越好。池子太大浪费内存池子太小遇到流量峰值会丢消息或阻塞。常见的做法是先按业务峰值估算假设对端最多每秒上报 200 条事件每条消息体不超过 256 字节那么消息池至少要比峰值速率留出 1.5 到 2 倍余量单条消息长度设成 512 字节留足协议头扩展空间。设备树节点一般长这样这是从 SDK 示例里拆出来的具体寄存器地址以你的 SDK 为准communicate_ipcm: ipcm { compatible vendor,ipcm-controller; reg 0x5A000000 0x10000; interrupts 0 45 4; // 消息池数量、单条消息大小可在这里通过属性配置 message-pool-size 1024; message-max-size 512; };设备树配置好之后Linux 侧驱动正常工作会生成 /dev/ipcm 或者类似命名的节点。我建议启动后先确认这个节点存在再继续往下调。2.2 用户态收发代码的参考写法用户态代码通常用 open ioctl write/read 这套组合。不同 SDK 的 ioctl 命令字不一样但是语义类似。一个典型的发送流程是构造消息结构体、填写消息 ID 和目的模块、填入数据、调用发送接口、等待发送结果。#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include sys/ioctl.h #include unistd.h #define IPC_MSG_SRC_LINUX 1 #define IPC_MSG_DST_LITEOS 2 #define IPC_MSG_TYPE_CMD 100 typedef struct { unsigned int src; unsigned int dst; unsigned int type; unsigned int len; unsigned char data[512]; } ipcm_message_t; int main(void) { int fd open(/dev/ipcm, O_RDWR); if (fd 0) { perror(open ipcm failed); return -1; } ipcm_message_t msg; memset(msg, 0, sizeof(msg)); msg.src IPC_MSG_SRC_LINUX; msg.dst IPC_MSG_DST_LITEOS; msg.type IPC_MSG_TYPE_CMD; msg.len 32; strcpy((char *)msg.data, switch_to_1080p); int ret write(fd, msg, sizeof(msg)); if (ret 0) { perror(write ipcm failed); close(fd); return -1; } close(fd); return 0; }注意我使用的是 write 而不是 ioctl是因为部分 SDK 把整条消息直接当成一块数据写入驱动驱动内部再解析成一条 IPCM 消息。如果 SDK 明确要求用 ioctl 传命令字那就按照 SDK 的 ioctl 方式写。核心思路都是先把消息体填好再交给驱动。接收方向一般会有一个后台线程阻塞在 read 上。LiteOS 侧回消息后Linux 侧驱动把消息从内核态拷出来read 返回的字节数就是实际消息长度。我在调试阶段发现接收线程里的处理逻辑一定不能做耗时操作比如写日志到磁盘、同步调网络接口。否则消息积压在队列里对端感知的响应延迟会变大甚至会触发超时重传。2.3 调 IPCM 时最容易翻车的几个地方第一个坑是阻塞超时。IPCM 的发送接口默认可能是阻塞模式对端如果长时间不消费消息发送线程会一直卡住。我当时从现象看过去是程序停住不动一开始还以为是死锁后来查了驱动的发送超时时间才发现是对端根本没有注册对应的消息 ID 回调消息发过去没人接收池子被占满。解决办法就是给发送操作加超时超时时间建议设置成 200 到 500 毫秒超过就报错并打印对端状态方便定位。第二个坑是消息结构体字节对齐。结构体里如果有成员长度不对齐两个核之间编译器默认的 packed 规则不一样消息内容会错位。最典型的例子是 Linux 侧默认 4 字节对齐发过去LiteOS 侧按 1 字节紧凑对齐接收解析出来的数据完全错乱。我的习惯是所有跨核消息结构体显式按 4 字节对齐并且用固定长度的数组代替指针。千万不要在消息里直接传指针A 核的指针在 M 核那边毫无意义。第三个坑是消息 ID 的分配管理。项目初期大家各写各的模块消息 ID 冲突了以后LiteOS 侧同一个 ID 只能注册一个回调后注册的会把先注册的覆盖掉导致某个功能莫名其妙失效。后来我把所有跨核消息 ID 集中放进一个头文件统一管理每个模块划分独立区间比如采集模块从 1 到 100算法模块从 101 到 200才彻底解决了冲突问题。3. virt_tty 通道把字节流当串口用3.1 虚拟串口到底是什么virt_tty 在很多文档里被写作 virtio-serial 或者虚拟串口逻辑上和普通 UART 很像但物理介质不是引脚而是内存中的一块环形缓冲区。Linux 侧认到的设备节点是 /dev/ttyVIRT*对应用层来说它就是串口可以 open可以配置 termios可以 read/write甚至可以用 select、epoll 监听。我一开始不太理解平台上为什么要专门做一条这样的通道后来想明白了控制消息用 IPCM但视频分析过程中的候选框数据、特征向量、实时日志流这些数据量大、格式任意、不需要逐条应答用 IPCM 的消息池根本撑不住。这时候 virt_tty 的字节流模型就非常合适。它天然支持流式读写应用层想按照自己的帧格式切片都行。这套东西还有一个隐藏优势你可以把任何现成的串口调试工具直接对接上来。在 PC 上经常用串口助手的人到了板子上仍然可以用类似思路去验证 virt_tty 通不通。换句话说virt_tty 把核间通信的门槛降到了你只要会开串口就会用的程度。3.2 打开虚拟串口的标准姿势打开 /dev/ttyVIRT0 之后强烈建议立刻把终端设置为原始模式raw mode否则串口驱动可能做一些行缓冲、信号处理、回车换行转换之类的额外行为。跨核数据里面如果包含二进制内容一旦被驱动做了换行转换数据就变了。#include stdio.h #include fcntl.h #include unistd.h #include termios.h #include string.h int main(void) { int fd open(/dev/ttyVIRT0, O_RDWR | O_NOCTTY); if (fd 0) { perror(open virt_tty failed); return -1; } struct termios tty; tcgetattr(fd, tty); cfmakeraw(tty); tty.c_cc[VTIME] 0; tty.c_cc[VMIN] 1; tcsetattr(fd, TCSANOW, tty); const char *msg hello from linux; int n write(fd, msg, strlen(msg)); printf(write %d bytes to virt_tty\n, n); char buf[256]; n read(fd, buf, sizeof(buf)); if (n 0) { printf(received %d bytes\n, n); } close(fd); return 0; }cfmakeraw 这个函数是 Linux 内核提供的 POSIX 扩展接口本质上就是把 raw 标志、关闭回显、关闭软件流控、关闭信号字符全部设置好。如果你在某个库里面找不到 cfmakeraw也可以通过手动设置 c_iflag、c_oflag、c_lflag 来实现相同效果网上有很多现成的配置代码。我在实际代码里很少直接在收发线程中做字符串处理因为一个完整的业务数据包往往需要多次 read 才能读完。虚拟串口不保证一次 read 返回的就是完整应用层消息所以必须在应用层自己定义帧边界。我的做法是固定消息头如 4 字节魔数 2 字节长度收到头部后按长度继续读接收端做成状态机这样可以稳定处理黏包和半包。3.3 流控、缓冲区和丢数据的教训virt_tty 用起来比普通串口简单但也不是完全没有坑。我第一次用的时候发现从 Linux 侧一次性写 8KB 数据到 LiteOS 侧LiteOS 侧只收到了前面一小段后面全丢了。排查后确认虚拟串口底层环形缓冲区的大小是有限的对端消费速度跟不上发送速度时新数据会直接覆盖尚未读取的旧数据。这种丢包很难从驱动层解决需要从应用层做两手准备。第一是减小单次发送量每秒发送的总量不要超过物理通道能够吸收的上限第二是在业务协议里加入序列号或者数据块编号对端收到后发现编号跳动就知道中间丢数据了然后请求重传。另外一个值得注意的点是 write 返回值的含义。write 返回的字节数只代表数据进入了驱动缓冲区不代表对端应用已经收到和处理完。对延迟敏感的业务最好的办法是在协议层加确认发送方写入数据后等待一个 ACK超时后选择重发或报错。我见过有人忽略了这一层在调试时看到 write 全部成功就认为对端已经处理结果业务表现和数据对不上排查了很久才发现是对端在忙时丢了 ACK。还有一个通信方向上的问题是全双工链路的两端对应关系。virt_tty 设备通常分成多路Linux 侧的 /dev/ttyVIRT0 可能对应 LiteOS 侧的逻辑串口 0LiteOS 侧的逻辑串口 1 可能会映射到 Linux 侧的 /dev/ttyVIRT1。开始调之前一定要先看 SDK 的端口映射表否则可能出现Linux 发给 VIRT0LiteOS 在串口 1 上等永远等不到数据的尴尬局面。4. sharefs 通道用文件系统掩盖内存细节4.1 共享内存为什么要套一层文件系统sharefs 的思路其实很优雅把一块预留的 DDR 物理内存用文件系统的形式暴露给 Linux 用户态。用户不再需要关心这块内存的物理地址在哪里只需要 mount 一个共享文件系统然后在挂载目录下读写文件即可。LiteOS 侧则通过 SDK 提供的接口直接访问同一块物理内存上对应的文件数据。我当时第一次看到 sharefs 的用法时觉得这个东西在同场景下有替代方案比如 Linux 侧直接 mmap 一段共享内存LiteOS 侧也映射同样物理地址两边各自读写。但后来我发现 mmap 方式有几个麻烦物理地址分配要手动协调内存保护要靠自己实现文件和数据的组织也要自己做而 sharefs 把这些都封装掉了。文件这个抽象层对开发者非常友好天然具备定位、截断、命名、多文件隔离的能力。举个例子Linux 侧下载了一个新算法模型大小可能几十 MB。如果走 IPCM消息池根本承载不了如果走 virt_tty传输时间可以接受但需要把数据流重新组装成文件还得管理写一半断电的情况。而用 sharefsLinux 侧直接把模型文件写入共享目录LiteOS 侧从共享目录读取整个过程既不需要协议组帧也不需要序列化和反序列化文件系统自己把大象装进了冰箱。4.2 挂载与读写实践sharefs 的挂载路径在 SDK 里通常是预留好的。启动后Linux 侧执行挂载命令把共享内存块挂载到一个空目录。以下命令只是示意具体节点名、挂载类型和参数必须以 SDK 文档为准。mkdir -p /mnt/sharefs mount -t simfs none /mnt/sharefs挂载完成后在这个目录下创建的普通文件实际上就写在共享内存块里。我用一段简单代码演示 Linux 侧的读写#include stdio.h #include fcntl.h #include unistd.h #include string.h int main(void) { int fd open(/mnt/sharefs/config.bin, O_CREAT | O_RDWR, 0644); if (fd 0) { perror(open sharefs failed); return -1; } const char *cfg sensor_mode0\nae_enable1\n; ssize_t n write(fd, cfg, strlen(cfg)); fsync(fd); close(fd); printf(write %ld bytes to sharefs\n, n); return 0; }LiteOS 侧通常会有专门的接口打开和管理共享文件不需要在 LiteOS 里实现一套完整文件系统只需要能够按名字找到共享区域里的数据块然后按偏移量访问。在实际使用中我会在共享目录下按业务功能划分子目录比如 model/、config/、log/。这样子目录隔离的好处是当有多个模块同时往共享区写数据时文件名冲突概率被大幅降低。4.3 并发与一致性问题以及我的规避做法sharefs 最大的坑不是读写本身而是并发和一致性。首先是 cache 一致性问题。A 核和 M 核操作同一块 DDR 物理内存时如果某一侧开启了 cache另一侧写入的数据在 cache 没有失效之前可能读不到最新值。这个问题的解决方案在 SDK 里一般会被封装掉框架会主动做 cache 刷新或物理地址映射时关 cache。但如果你绕过 SDK 直接操作共享区域一致性就要自己承担。我建议优先使用 SDK 提供的接口不要自己造轮子。第二个问题是并发互斥。两个核同时写共享区的同一个文件文件内容可能交错、混乱。我在项目里用了一个很简单的约定共享文件按写入方向区分目录Linux 写 configLiteOS 只读 configLiteOS 写 statusLinux 只读 status。如果确实需要双向读写同一个文件就通过 IPCM 发送一个我要锁文件的命令另到边收到应答后再操作。共享内存自身通常不提供锁机制锁是要架在 IPCM 之上的。第三个问题是掉电残留。共享文件系统是内存态的系统重启之后之前写的文件可能还在也可能被初始化程序清空。我在项目里养成了一个习惯Linux 侧启动脚本里固定检查挂载目录如果发现上一次退出时留下的临时文件先按业务规则决定是清理还是保留。业务逻辑不能假设共享文件在上电后一定处于干净状态。5. 三种通道的实际选型与组合打法5.1 一张表说清差异调试完三条通道之后我把它们的核心差异整理成了一张表后面做技术方案评审时直接拿这张表来说事。维度IPCMvirt_ttysharefs数据模型消息队列/邮箱字节流文件典型消息大小几百字节以内不定长可持续流式大文件几十 MB 级别实时性高毫秒级中受字节流调度影响低文件读写耗时明显适用场景命令、状态、事件、应答视频结构化数据流、实时日志模型文件、配置文件、离线日志实现复杂度中需要管理消息 ID 和回调低串口编程体验低文件读写体验需要额外注意消息 ID 冲突、超时粘包半包、流控丢包cache 一致性、并发互斥这张表不是说哪条通道更好而是说哪条通道更合适。我见过有人在分享会上说我这一版只用了 IPCM完美解决了所有通信需求其实是因为他的业务恰好都是命令和状态数据流根本不存在。如果你的系统里有大块数据要跨核移动IPCM 硬扛一定会在某个临界点上崩溃。5.2 我项目里的组合用法我负责的视频分析模块最终采用了三条通道混合的架构。LiteOS 侧负责图像采集和底层曝光控制Linux 侧负责算法推理和网络服务。具体组合是这样的控制链路走 IPCM。Linux 下发切换分辨率、调节帧率、开启算法等命令LiteOS 上报曝光异常、采集帧计数、硬件错误事件。每条消息都很短实时性要求高。数据链路走 virt_tty。算法需要的图像帧描述信息、检测结果坐标、跟踪特征以自定义二进制帧格式持续传输。这里数据量大、持续流动适合字节流。模型和配置走 sharefs。Linux 侧把更新后的算法模型和摄像头参数文件写入共享目录LiteOS 侧在启动时或收到 IPCM 通知后读取。大文件不用走消息池也不用流式拆分。这套组合在业务上各司其职。每一条链路都只做自己最擅长的事通信压力被分散排障也快命令慢了查 IPCM数据丢了查 virt_tty文件坏了查 sharefs不会出现所有问题都挤在一坨串口日志里的情况。5.3 快速验证链路是否打通的办法最后分享几个我在联调阶段常用的验证手段这些手段能帮你快速判断是哪条链路出了问题而不是怀疑人生。第一验证 IPCM 是否连通。在 LiteOS 侧写一个回环回调收到类型为 ECHO 的消息后原样把消息体返回给 Linux。Linux 侧写一个小工具循环发 1000 条 ECHO 消息统计返回时间和丢包率。如果返回时间抖动大去看消息池是否满了或者对端中断触发是否及时。第二验证 virt_tty 是否丢包。Linux 侧连续按序号发送一堆小包比如发 10000 个包每个包里面带一个递增序号LiteOS 侧收到后回传带序号的确认。Linux 侧统计从 1 到 10000 有没有缺口。如果有缺口定位是环形缓冲区太小还是对端消费太慢。第三验证 sharefs 是否一致。Linux 侧往共享目录写一个带固定魔数的文件LiteOS 侧读取后把文件内容和自身状态合并成新文件写回另一个共享路径。Linux 侧多次轮询直到发现新文件再做内容校验。这个过程能顺手确认两侧访问共享内存的一致性逻辑是否正常。这些验证脚本不需要写得多复杂关键是能快速暴露问题。链路打通之后再往上面叠业务逻辑就心里有底了。我在几次双系统项目里最深的体会是通信框架本身只是半成品真正的工程质量来自应用层协议和调试习惯。消息格式提前定义好消息 ID 集中管理通道选型先考虑数据形态联调阶段做好回环验证这套东西做扎实了双系统通信其实比想象中稳定得多。