做双系统方案的人都会经历一个阶段:Linux这边跑得好好的,LiteOS那边也跑得好好的,但一到联调就抓瞎——两个核之间的数据到底怎么传。这篇笔记是HI3556V200双系统学习系列的第4篇,专门把双系统通信这块一次讲透,重点是三样东西:IPCM、virt_tty、sharefs。它们分别对应底层通信通道、虚拟串口、跨核文件系统,解决的是不同场景下的同一个问题:让两个系统高效、可靠地“对上话”。
如果你正卡在LiteOS和Linux互相看不见的阶段,这篇内容就是给你准备的。我会把原理、配置、实际命令和我踩过的坑一条条捋清楚。别急着开抄,先把第二节原理看完,否则遇到偶发丢数据或者莫名其妙重启的时候,你会怀疑人生。
1. 双系统通信整体设计:先分清三者的边界再动手
1.1 为什么Linux和LiteOS之间必须有“电话线”
板子上的两个核,一个像项目经理,一个像车间主任。项目经理(Linux)管网络、界面、文件系统,和外部世界打交道;车间主任(LiteOS)管传感器、曝光、帧同步,手里全是不能抖的实时活。平时各干各的没问题,但一旦涉及协作,就绕不开通信。
举几个实际场景:Linux要把AE/AF参数下发给LiteOS,LiteOS要定期把每个帧的统计信息回报给Linux;Linux需要跨核读日志,升级时还要把固件递给LiteOS去写Flash。这些需求里,有的是小消息,有的是大块数据,有的只是文件互操作。把它们都塞进同一条通道,迟早出问题。所以HI3556V200这类平台才把双系统通信拆成三层,各司其职。
1.2 IPCM、virt_tty、sharefs各自擅长什么
先给一张表,把三者边界划清楚。这张表花不了你一分钟,但能省掉后面一晚上排查。
| 机制 | 本质 | Linux侧形态 | LiteOS侧形态 | 典型场景 | 需要注意的点 |
|---|---|---|---|---|---|
| IPCM | 自定义消息通道 | 内核模块加API,或ioctl接口 | 收发接口和回调注册 | 控制指令、状态上报、小包业务消息 | 需要自己设计协议和通道规划 |
| virt_tty | 虚拟串口,字节流 | 设备节点,如/dev/ttyV0 | 流式读写API | 调试、日志、小流量连续数据 | 有缓冲区限制,需要流控 |
| sharefs | 跨核共享文件系统 | 挂载目录,如/mnt/sharefs | 文件操作接口 | 配置文件、升级包、批量日志 | 底层是内存,断电不保留 |
我的选型习惯很固定:控制消息走IPCM,调试数据走virt_tty,批量文件走sharefs。三者不是竞争关系,而是配合关系。有人总觉得sharefs很万能,想把所有通信都做成“写文件”,结果底层内存规划不合理,跑几天就出诡异问题。反过来,也有人所有消息都硬塞IPCM,协议越写越复杂,最后维护成本极高。按场景选工具,别让一个锤子砸所有钉子。
1.3 三种机制配合使用的典型链路
在某个便携摄像设备项目里,我们的架构是这样的:LiteOS负责把sensor的每帧AE统计值报给Linux,Linux跑3A算法后算出一组曝光参数,再下发给LiteOS执行。这条链路里,AE统计值是几十字节的周期消息,走IPCM最合适;两边的调试日志通过virt_tty互灌,方便我在PC上同时看两个系统的log;升级固件则是Linux先通过网口下载到rootfs,再写入sharefs,LiteOS侧发现文件版本变化后自己读走烧写。
这么拆完之后,每条链路的代码都简单到一眼能看懂。这是双系统通信设计里最值得花时间的部分:先把通道规划清楚,再动手写代码。
2. 核心原理拆解:mailbox门铃、共享内存与通道化设计
2.1 双核通信的两个物理基础
双核通信说穿了就两样东西:共享内存和硬件中断。共享内存是提前在DDR里圈出来的一块物理内存,固定给两个系统用。它不走文件系统,不经过网络,就是一块两边都能访问的物理区域。中断则负责“通知”对方——硬件上有一组叫mailbox的寄存器,可以配置成触发对方核的中断。用一句话概括:共享内存是房间,mailbox是门铃。
为什么不能只用mailbox传数据?因为mailbox寄存器数量有限,一次只能带过去几个值,就像门铃不能当货车用。而只靠共享内存也不行,因为对方不知道你什么时候写完了数据,总不能靠轮询去猜。这两个硬件特性组合起来,才是完整的通信机制。这也是我在新人培训时最常打的一个比方:你往公共房间放了一箱子货,不放门铃的话,对方永远不会知道货到了。
2.2 IPCM是如何把底层硬件封装成多条通道的
IPCM这个模块做的事情,听起来很简单:在共享内存里划分多个环形缓冲区,每个缓冲区配一套读写指针和状态字;发送方写数据、更新指针、触发中断;接收方在中断处理函数里读数据、更新自己的指针。但加上“通道”的概念后,事情就变得工程化了。
通道可以理解为在物理链路上开的多个“独立信箱”,不同业务的报文走不同通道,互不干扰。比如我们项目里开了三个通道:通道0管控制命令,通道1管统计上报,通道2留着扩展。这样做最大的好处是隔离性——某一个通道的协议乱了,不会别的通道也跟着乱。还有一点容易被忽视:每个通道的环形缓冲区大小可以单独配。命令通道只要几KB就够,日志通道可能得给几十KB。一视同仁地配,要么浪费内存,要么不够用。
提示:IPCM初始化时,两边必须使用相同的通道号和缓冲区大小配置。我在项目里就吃过亏,LiteOS侧配置了8个通道,Linux侧只注册了4个,结果后4个通道发送时一直没有响应,排查了半天才找到是配置没对齐。
2.3 数据面与控制面的时序关系
真正写代码前,还得理解一个时序问题:控制面(握手)和数据面(传输)是分开的。文件系统通信,比如配置1:IPCM通道先建立双向握手,“发起方——应答方”,两边都确认对方在线后,才允许业务数据流动。这个握手存在的作用是防止“数据早到、处理者不在”的竞态。
在HI3556V200上,一次短消息从LiteOS发出,到Linux侧应用层收到,实测大概在几十微秒量级,具体数值跟中断延迟和当前CPU负载有关系。这个速度对控制和状态类消息绰绰有余。大块数据想跑满带宽,一般会走共享内存的直接映射,再加上一次拷贝,吞吐能到接近DDR实际带宽。但前提是做好缓存一致性处理,否则数据错位会让你调到头秃。
实践心得:中断处理函数里尽量只做搬数据和置标志位,别在里面打印,别做复杂处理。一次我图方便在中断回调里加了个调试打印,直接把两边的时序打乱,小包消息狂掉,移除打印后立刻正常。
3. 环境准备与关键配置:内核选项、设备树与共享内存预留
3.1 内核菜单里要把哪些开关打开
双系统通信不是默认就能用的,编译内核时得先打开对应模块。以SDK自带的Linux内核为例,进入内核源码目录,使用SDK提供的交叉编译工具链执行make menuconfig,然后在搜索框里直接输入“IPCM”或者“SHAREFS”挨个找。
通常需要打开的选项包含:
- IPCM核心支持——所有上层机制都依赖它。
- IPCM的mailbox实现——也就是底层基于Mailbox硬件的通道。
- IPCM的virt_tty支持——生成虚拟串口设备节点。
- IPCM的sharefs支持——生成共享文件系统驱动。
菜单里的具体选项名在不同SDK版本里有差异,有些叫CONFIG_IPCM_VIRTIO_RPMSG,有些叫CONFIG_HI_IPC_MBX,你只要看到IPCM相关的大类就进去全选。我一般直接把整个IPCM选项组都编成'y',省得后面漏了哪个再重编。
LiteOS侧也有对应的模块选项,一般位于LiteOS的config或menuconfig里。两边必须同时打开,且编译成匹配的版本。曾经有一次我只编了Linux侧的内核,LiteOS固件没更新,结果是Linux能挂载、但LiteOS不回包,白白浪费了一个下午。
3.2 reserved-memory共享内存的规划与设备树写法
共享内存区域要在Linux设备树里预留,并且要从Linux常规内存管理中拿走,防止被其他驱动复用。HI3556V200上的典型做法是在reserved-memory节点里定义一块固定地址的内存。
下面是一个示意写法,具体地址和大小按你的板卡DDR布局调整:
reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; ipcm_reserved: ipcm@4B000000 { compatible = "shared-dma-pool"; reg = <0x0 0x4B000000 0x0 0x04000000>; no-map; }; };注意两个重点:reg里的起始地址要和boot阶段、LiteOS侧配置的内存基址完全一致,差一个字节都不行;no-map属性必须带上,表示这块内存不映射进Linux常规内存管理。少了no-map,系统跑一段时间后这块区域可能被Linux分配出去覆盖,数据被踩,问题极其难查。
大小怎么定?看你的业务需求。我们项目里留了64MB,其中IPCM环形缓冲区占了不到1MB,sharefs占了30MB,剩下的作为扩展预留。第一次做的时候留了16MB,结果升级包稍大一点sharefs就写满,后来扩到64MB才消停。这个数字别拍脑袋定,先把三种通信方式的预期负载列出来,再算出总量,乘上1.5的安全系数。
3.3 编译烧写顺序与版本匹配
编译顺序上,我建议先编译Linux内核,再编译LiteOS,最后打包烧写。原因很简单:Linux侧的驱动和设备树是运行时加载,LiteOS侧模块一旦编进去就要跟着固件走,两边版本必须对齐。
曾经有次版本管理没做牢,Linux内核是上周的,LiteOS是昨天的,结果virt_tty节点能看见但一发数据就没响应。用dmesg查,Linux侧报的是IPCM版本号不匹配。从那以后我强制要求每次打固件都记录好双系统各自的commit号,省得再次陷入“版本模糊”的泥潭。
4. 实操一条龙:把virt_tty打通,再挂上sharefs
4.1 LiteOS侧先把IPCM初始化跑起来
LiteOS侧的工作是先初始化IPCM模块,注册接收回调。代码风格大致是这个样子,函数名以你SDK里的头文件为准:
/* LiteOS侧:示意代码,API名称以SDK实际头文件为准 */ static int recv_cmd(uint32_t ch, void *data, uint32_t len) { /* 收到Linux下发的命令 */ return 0; } void ipcm_init_task(void) { ipcm_init(); ipcm_register_ch(IPC_CH_CMD, recv_cmd); ipcm_start(); }初始化完可以主动发一条握手消息,比如“LiteOS Ready”,让Linux侧知道从核已经上线。这一步很重要,它同时验证了mailbox中断、共享内存映射、通道注册三个环节是否都正常。
如果你手上的SDK版本提供的接口不是这个名字,不用慌。海思各代平台里的IPCM接口都长得很像,多在名字前缀上变化。你在头文件里搜“ipcm”关键词就能定位到相关接口,然后照葫芦画瓢就行。
4.2 Linux侧模块加载与设备节点确认
Linux侧要看编译时把IPCM编译成了内核模块还是直接编进去。如果是模块,启动后需要手动insmod,或者放入开机启动脚本。
# 加载模块 insmod /mnt/system/ipcm.ko # 确认驱动日志 dmesg | grep -i ipcm # 查看虚拟串口设备节点 ls -l /dev/ttyV*正常情况下dmesg里能看到IPCM初始化成功的日志,dev目录下会多出虚拟串口节点。这里提醒一句:不同SDK版本生成的节点名可能不一样,有的版本叫/dev/ttyV0,有的叫/dev/ttyIPCM。写应用层代码前先ls确认一下,别把设备名写死。
sharefs的检查方式也类似,加载成功后执行命令:
mkdir -p /mnt/sharefs mount -t sharefs none /mnt/sharefs df -h /mnt/sharefs如果mount报错,大概率是LiteOS侧还没就绪,或者内核config没开对应模块。这时回到第3节检查配置,别急着怀疑硬件。
4.3 virt_tty双向收发实测
virt_tty打通是双系统通信的“hello world”。LiteOS侧起一个循环任务,每500ms发一条“hello from liteos”,Linux侧用cat直接读:
cat /dev/ttyV0如果一切正常,终端里会像心跳一样刷出消息。这步能过的前提是第4.1和4.2都已经正常完成。如果LiteOS的这条消息一直没出来,先确认LiteOS侧任务真的在跑,再检查IPCM的中断是否绑定成功。
反向测试需要在Linux侧写一个小工具,标准文件接口就能操作:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(void) { int fd, n; char buf[64]; fd = open("/dev/ttyV0", O_RDWR | O_NOCTTY); if (fd < 0) { perror("open"); return -1; } strcpy(buf, "hello from linux"); write(fd, buf, strlen(buf)); memset(buf, 0, sizeof(buf)); n = read(fd, buf, sizeof(buf)); if (n > 0) printf("recv: %s\n", buf); close(fd); return 0; }编译后用交叉工具链编出来丢到板子上跑。一次能双向通,说明整条IT链路已经通了。后面你可以在上面的代码上扩展出超时、分包、粘包处理,做成你自己的虚拟串口通信库。
注意:read没有数据的场景下会阻塞,所以实用程序里要用select或poll加超时,避免任务卡死在读操作上。我见过同事在样板代码里直接read,然后整个线程就挂住了,找半天才意识到没有设置超时。
4.4 sharefs挂载与跨核文件交互
sharefs的原理是把共享内存封装成文件系统。mount起来之后,Linux侧把文件往里一写,LiteOS侧用文件接口就能读出来。
挂载命令走一遍:
mkdir -p /mnt/sharefs mount -t sharefs none /mnt/sharefs echo "ae_mode=auto" > /mnt/sharefs/ae.conf ls -l /mnt/sharefs/在这个例子里,ae.conf文件对LiteOS是可见的。LiteOS业务可以通过SDK提供的文件操作接口读取,解析后修改自身的AE模式。反过来也一样,LiteOS可以把状态信息写文件,Linux侧在/mnt/sharefs下直接读。
这套机制对“配置下发”和“日志导出”特别合适。我给产品做远程升级时,典型流程是Linux先下载固件包到rootfs,解压后写入sharefs,再通过IPCM发一条“升级包已就绪”消息给LiteOS。LiteOS收到消息后去sharefs里读文件、做校验、自己烧写。整个流程里,IPCM负责唤醒通知,sharefs负责传大文件,职责清晰,调起bug来也快。
4.5 一个综合例子:配置参数从Linux下发到LiteOS
把上面这些串起来,做一个最典型的联动:Linux运维界面修改一个参数,LiteOS实时生效。
Linux侧流程:Web配置界面把参数写入/mnt/sharefs/config/app.json,然后通过IPCM通道向LiteOS发“配置变更”消息。LiteOS侧流程:IPCM回调收到消息后,去sharefs里读app.json,解析出参数,更新到自己的全局配置结构体。后续LiteOS逻辑直接用新参数运行。
这个方案的优点是配置持久化和即时生效都兼顾了,缺点是引入了IPCM和sharefs两条链路。所以也要做好幂等设计:LiteOS收到通知但文件内容没变时,直接忽略,避免重复加载。
这个例子做完,你对三种机制的配合就已经有实操体感了。
5. 实踩过的坑:启动时序、数据一致性与内存撞车
5.1 Linux先起来导致“找不到设备”
双系统联调最常见的问题之一是启动顺序。多数场景下Linux启动比LiteOS快,Linux侧IPCM刚初始化完,LiteOS还没起来,这时mount sharefs或者open virt_tty节点都会失败,报“No such device”或者“Invalid argument”。
解决办法是等待LiteOS上报就绪。我们的做法是在应用层启动脚本里加一个等待循环,先通过virt_tty或者握手通道等待“LiteOS Ready”消息,再执行mount等操作。简单粗暴一点的可以用sleep,生产环境还是建议等一个握手状态。
另外一个编译期因素是LiteOS侧没把这个模块编进去。检查LiteOS固件里是否包含了IPCM和sharefs组件,别只看Linux侧。确认的方法是打印双方固件里的模块版本号,对上就说明两边都加载成功。
5.2 偶发丢数据与缓存一致性问题
virt_tty收发频率高了之后,偶尔会出现几个字节的错乱或丢失。这种现象十有八九是环形缓冲区的读写指针同步没有做好。
ARM核之间虽然硬件缓存一致性比老芯片好了很多,但跨核访问共享内存时,编译器优化和乱序仍然是隐患。处理办法是:
- 缓冲区指针必须用volatile修饰,防止编译器把访问优化掉。
- 写指针更新后加内存屏障,确保数据先写完再通知对方。
- 报文头部带自增序列号,接收方根据序列号判断是否丢包,重传或报警都行。
我在调试时曾经忽略过内存屏障,结果小包通信低频率下完全正常,频率一高就丢,很迷惑。后来补上屏障,再也没出现过大面积丢包。
5.3 共享内存和业务内存“撞车”的表现
共享内存地址规划错位的问题太典型了。症状表现为两种:一是系统跑几分钟后Linux突然报page fault或者bus error;二是LiteOS不定时复位,业务偶发异常但没有任何固定复现规律。
排查思路是确认启动阶段三个地址是否完全一致:bootLoader里的共享内存配置、Linux设备树reserved-memory、LiteOS侧内存基址宏定义。这三个地方但凡有一个不一致,共享内存就会被某个系统当普通内存分配出去。加上no-map属性也尽量避免Linux的伙伴系统把你预留的区域吞掉。
我在真实项目里碰到过一次这种问题,排查了很久,最后是在仿真器里给共享内存区域下了写断点,抓到罪魁祸首是Linux某个驱动一气之下写了越界地址。从那以后,我的新平台适配清单里永远有一条:“共享内存区域配置三处核对”。
5.4 常见问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| sharefs挂载报No such device | 内核没开对应模块,或LiteOS没就绪 | 检查内核config,等待LiteOS握手后再mount |
| virt_tty节点不存在 | 内核没开virt_tty,或设备树没注册 | 检查设备树节点和内核config |
| 收发频率高时丢包 | 缓冲区太小、缺内存屏障 | 增大环形缓冲区,补屏障,加序列号 |
| LiteOS收不到Linux消息 | 通道号不一致,或中断未通 | 对比两侧通道配置,检查mailbox中断绑定 |
| 系统运行后随机hang | 共享内存被踩 | 核对三处地址配置,加no-map,下写断点排查 |
| 共享区文件掉电丢失 | sharefs底层是内存 | 业务层做持久化,重要数据落到真实Flash |
速查表做出来后,建议贴在工位边上,或者直接写进项目wiki。这类问题很少来自某一个复杂逻辑,基本都是配置和时序的小失误。
最后分享一个我自己的调试习惯:遇到双系统通信问题,永远从最窄的一条路开始验证——先用mailbox短消息确认握手,再开virt_tty跑数据流,最后才上sharefs。三个都通了,再继续封装业务通信层。这个顺序看起来很慢,但实际是定位问题最快的路径。尤其是新板子刚点亮的时候,直接上sharefs,一旦失败你会搞不清楚是内存映射问题、内核配置问题还是LiteOS没跑起来。从短消息开始,一层层往上试探,每一层都确认无误,心里才有底。这个习惯帮我省下的调试时间,算起来比写业务代码的时间还多。