做双系统开发Linux和LiteOS各自跑起来只是第一步真正让人头疼的是让这两个系统对话。HI3556V200这颗芯片上Linux负责应用生态和复杂协议栈LiteOS负责实时控制和底层硬件响应两边各有各的活但业务往往是连在一起的——你在Linux侧收到一条网络指令需要通知LiteOS去转动某个电机或者LiteOS采集了一组传感器数据要交给Linux去做算法处理。这时候就必须有一整套可靠的通信机制。我这次记录的是双系统学习系列的第4篇主题就是通信具体涉及三种手段IPCM、virt_tty、sharefs。这也是我在实际项目里踩坑最多、查资料最久的部分分享出来希望后来的人能少走点弯路。1. 双系统通信的整体思路与机制选型1.1 为什么双系统之间需要一套专用通信方案很多人刚开始接触双系统会有一个疑问两边不都是跑在同一个芯片上吗直接共用内存、共用外设不就行了理论上是这样但实际操作中问题很多。Linux和LiteOS是两个完全独立的内核各自管理自己的内存映射、中断资源和外设驱动你如果在Linux侧直接去读写某个LiteOS正在管理的物理地址两边可能会因为缓存一致性问题Cache一致性问题导致数据错乱还有可能踩到对方正在运行的关键代码区域直接造成系统崩溃。更麻烦的是两个系统之间缺少一种约定好的握手方式你不知道对方什么时候写好了数据、什么时候读完了数据这种同步问题单靠裸寄存器操作根本解决不了。所以芯片厂商提供了一整套已经定义好的通信机制专门用于双核之间交换数据。在HI3556V200这套方案里通信手段被整理成了三条主线IPCM负责消息级的控制指令传递virt_tty提供一条类似串口的流式数据传输通道sharefs则让两边像访问同一个U盘一样共享文件。这三条线各有侧重点不能说谁替代谁实际项目中往往是组合使用。我最早拿到开发板时先把三个demo分别跑通然后再考虑工程上怎么选——这个顺序很重要先熟悉每个机制的行为特征再设计整体通信方案会省掉后面很多返工。1.2 三种机制的分工与定位消息、流、文件在深入代码之前建议你先建立一个直观的认知模型。IPCM本质上是共享内存中断通知的组合适合传小数据量的控制指令比如开始录像切换码流进入低功耗模式这类语义明确、要求低延迟的短消息virt_tty则更适合连续性的数据流比如把LiteOS侧的日志实时打印到Linux侧或者把Linux下发的音频流持续送给LiteOS去播放sharefs的定位就非常实在了它是给你存文件用的LiteOS把抓拍图片写进共享文件系统Linux直接按路径去读双方不用关心底层传输过程。三个机制覆盖了三种典型的数据形态这一点心里先要有数。我在实际项目中就犯过一次错误因为图省事想用IPCM传一个比较大的配置文件结果发现共享内存区设得太大导致LiteOS侧内存紧张而且数据量一大中断次数暴增实时性反而下降。后来改成用sharefs放文件、IPCM只传文件已就绪的短消息两个机制一配合整个系统的吞吐量和实时性都正常了。所以选型千万不要只图单一机制的全能模块之间各管一摊才是双系统通信的正常活法。2. IPCM处理器间通信的核心通道2.1 IPCM的工作原理与数据流转过程IPCM的全称是Inter Processor Communication Module它的实现逻辑拆开看其实不算复杂。核心是一块预先划定的共享物理内存区域加上一条中断通知链路。举个例子Linux侧要把消息发给LiteOS做的事情是先把数据写入共享内存中的某个约定区域然后写一个消息有效的标志位再触发一个硬件中断给LiteOSLiteOS的中断服务程序收到通知后到共享内存里读取数据处理完成后再往同一个区域写一个已消费的响应标志同时触发反向中断通知Linux。这样一来一回就完成了一次完整的请求-应答式通信。这里有个关键点共享内存的地址必须在两个系统中保持一致而且双方都要把这部分物理内存配置为非缓存或者设备内存属性防止数据被CPU缓存住迟迟没落盘导致对方读到旧数据。我在第一次调试时就遇到过这种情况——Linux侧明明把数据写进去了LiteOS读出来还是上一次的值后来查了一圈才发现是Cache没有做一致性处理。芯片提供的SDK里一般会有一个专门的IPCM内存配置宏把物理地址和大小定义好顶层代码通过系统接口申请的是非缓存内存这块一定不能省。2.2 创建Linux侧接收通道的配置步骤在HI3556V200的SDK里IPCM的通道一般是通过设备树和内核模块配合启用的。设备树里会声明一个IPCM节点里面包含物理基地址、中断号、通道数量等参数。我用的方案是Linux侧把IPCM封装成了一个平台设备驱动启动时自动注册用户态程序通过操作设备节点的方式实现收发不需要直接在内核里改代码方便很多。配置步骤大致如下先在设备树里找到ipcm节点确认base地址与SDK文档一致默认一般是芯片内部SRAM的一段区域不要跟LiteOS侧代码存放区重叠然后确认使能了相关中断中断号需要跟LiteOS侧配置保持一致否则会出现Linux发了消息LiteOS完全没反应的情况驱动起来之后检查生成的设备节点接上LiteOS端的测试程序先发一个最简单的ping型消息验证链路通不通。这里提醒一下首次调试IPCM时不要去想你自己的业务数据先用固定字节的测试消息来回收发确认通路OK再开始设计消息格式这样能把通信问题和业务逻辑问题的边界分清楚。2.3 IPCM消息格式与收发实操示例我习惯把IPCM的消息定义成一个结构体里面包含消息类型、序列号、长度、数据和校验字段。这样的好处是方便做消息分类和丢消息排查。实际操作中Linux侧发送的代码骨架如下#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/ioctl.h #define IPC_DEV_NODE /dev/ipcm_ctrl #define IPC_SEND_CMD 0x1001 #define IPC_RECV_CMD 0x1002 typedef struct { unsigned int msg_type; unsigned int seq; unsigned int data_len; unsigned char data[64]; unsigned int checksum; } ipcm_message_t; int main(void) { int fd open(IPC_DEV_NODE, O_RDWR); if (fd 0) { perror(open ipcm node failed); return -1; } ipcm_message_t msg; memset(msg, 0, sizeof(msg)); msg.msg_type 0x01; /* 自定义开机握手 */ msg.seq 1; msg.data_len 8; memcpy(msg.data, HI3556, 6); msg.checksum 0x5A5A; /* 简单校验 */ int ret ioctl(fd, IPC_SEND_CMD, msg); if (ret ! 0) { perror(ipcm send failed); close(fd); return -1; } ipcm_message_t rsp; memset(rsp, 0, sizeof(rsp)); ret ioctl(fd, IPC_RECV_CMD, rsp); if (ret 0) { printf(recv ack type0x%x seq%u\n, rsp.msg_type, rsp.seq); } close(fd); return 0; }实际开发中你不需要每次都走一次用户态到内核态的ioctl来收发消息SDK一般会提供一个守护进程把IPCM封装成socket或者消息队列给应用层用。但前期验证阶段我建议直接用ioctl把底层链路吃透这样后面遇到问题时心里有底。LiteOS侧没有文件系统那套概念直接在中断回调里节流读取共享内存把收到的消息挂到消息队列上应用任务再取出来处理。需要注意两个坑第一消息体里的数据长度字段一定要包含在共享内存里传递接收方先确认长度再拷贝防止越界读第二seq字段用于检测消息有没有丢包或者乱序双系统通信链路在极端负载下出现丢包并不稀奇如果对可靠性要求高消息层需要设计重传机制。2.4 IPCM实时性与性能边界IPCM的延迟理论上能做到几十微秒到百微秒级别因为它本质就是写共享内存硬中断整个过程没有复杂的协议栈开销。但实际性能受两方面限制共享内存的带宽上限和中断响应的调度延迟。如果LiteOS侧当前正在处理一个更高优先级的中断那么IPCM的响应就会往后排这个延迟不是固定的你的业务逻辑要做好容忍抖动的准备。我实测过在HI3556V200上IPCM单次收发不携带大数据纯控制消息的往返时间大概在50-200微秒之间跟系统的中断负载关系很大。如果你想榨干性能可以把Linux侧的收发线程绑核设置实时调度优先级同时让LiteOS侧的IPCM中断服务函数尽量短小——不要在中断里做复杂业务处理只把数据拷到内存队列就退出后续业务由独立任务处理。这条经验是从一个做视频处理的同事那里学来的他们的双系统控制链路对延迟极其敏感把所有延后处理全放到了任务级中断里几乎不干活。3. virt_tty把LiteOS变成Linux的“虚拟串口”3.1 virt_tty的通信原理与调度方式virt_tty从名字就能猜个大概虚拟串口。它的思路是让双系统之间看起来像通过一条串口线连接Linux侧打开一个字符设备节点读写就相当于从串口收发数据而物理上数据走的是共享内存和中断由底层IPCM机制承载。对上层应用来说通信模型被简化成了标准的串口模型开发量很小这也是virt_tty最大的价值——你用传统的串口编程思路就能打通双系统通信链路。LiteOS侧会创建虚拟串口设备把需要发给Linux的数据写入发送缓冲区通知底层发送Linux侧对应的驱动收到中断后把数据拷贝到自己的接收队列应用层通过read()读出来。反过来也一样Linux侧write()的数据经过驱动下沉到共享内存LiteOS那边收到后唤醒接收任务。整个过程对两个系统各自的上层应用来说就是一个普通的字符设备读写完全屏蔽了双核通信细节。3.2 打开virt_tty需要配置哪些东西开启virt_tty的工程配置比IPCM要稍微多一些。LiteOS侧需要创建一个虚拟串口实例指定它对应的内存缓冲区和中断通道然后在系统初始化时注册到设备管理框架中。Linux侧则在设备树里增加virt_tty节点声明缓冲区物理地址、中断号以及通道索引这些必须和LiteOS侧严格对齐。我遇到的绝大多数virt_tty不工作的问题归根结底都是两侧参数对不上——地址差个偏移、中断号错一个或者通道编号不匹配。参数对齐这件事建议做成一个公共头文件两个系统的工程各自include同一份定义而不是各写各的。有些开发者习惯在LiteOS侧加了一个字段就忘了同步到Linux侧结果数据错位排查起来非常痛苦。我后来定了一个规矩凡是涉及双系统通信的结构体、宏定义、地址声明统一维护在一个路径下任何一侧改动都必须提交另一侧的对应改动这个习惯救了我很多次。设备树里virt_tty节点大致长这样virt_tty { compatible vendor,virt-tty; reg 0x0 0x93A00000 0x0 0x10000; /* 共享缓冲区地址 */ interrupt-parent gic; interrupts 0 47 4; /* 发送完成中断 */ channel-index 0; /* 通道号 */ status okay; };3.3 用标准串口API进行数据收发实测配置好之后Linux侧不需要装任何特殊驱动系统会生成一个类似 /dev/virt_tty0 的节点直接用open/read/write操作就行。我这边写了个简单的自测程序先把LiteOS侧的自测任务跑起来它会周期性通过virt_tty发送一帧带时间戳的数据Linux侧用下面的代码接收然后回发一个应答帧#include stdio.h #include fcntl.h #include unistd.h #include string.h #include errno.h int main(void) { int fd open(/dev/virt_tty0, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd 0) { perror(open virt_tty failed); return -1; } char rx_buf[256]; char tx_buf[] ACK: data received; for (int i 0; i 100; i) { memset(rx_buf, 0, sizeof(rx_buf)); int n read(fd, rx_buf, sizeof(rx_buf) - 1); if (n 0) { rx_buf[n] \0; printf([%d] recv %d bytes: %s\n, i, n, rx_buf); int w write(fd, tx_buf, strlen(tx_buf)); if (w 0) { perror(write virt_tty failed); } } else if (n 0 errno ! EAGAIN) { perror(read virt_tty failed); break; } usleep(100 * 1000); } close(fd); return 0; }实测下来virt_tty的传输速率在几百KB/s到数MB/s这个量级之间浮动跟缓冲区大小、中断频率和两个系统的负载都有关系。它的一个明显优势是因为走的是串口读模型所以天然带字节流语义不需要像IPCM那样在应用层拼拆包。劣势是数据的帧边界需要自己做协议处理——你把一批数据write进去对方read出来可能是分了段的接收侧要有缓冲和重组逻辑不能假设一次read就是完整的一帧。3.4 和物理串口相比注意哪些不同点用virt_tty不能完全套用物理串口的经验。物理串口有波特率、流控这一套参数virt_tty没有这些概念配置寄存器里不用关心波特率但你也享受不到物理串口的电气隔离和长距离传输能力它是芯片内部通信物理上距离非常短对干扰的容忍度远不如真串口。另一个容易被忽略的点是virt_tty的缓冲区是共享内存数据写入之后如果对方不及时读取新数据会覆盖旧数据或者是缓冲区满了写不进去。物理串口有FIFO和流控机制数据满了会暂停发送virt_tty的流控一般需要上层自己实现或者依赖底层IPCM的消息确认机制。如果业务上是大量突发数据往里灌建议在LiteOS侧做双缓冲或者环形缓冲配合信号量控制读写节奏否则很容易出现数据被覆盖的问题。4. sharefs双系统共享文件系统实战4.1 sharefs的实现原理与初始化流程sharefs是三种机制里最接近人类直觉的一种。它在双系统之间划出一块内存区域初始化成一个精简的文件系统两边的内核都挂载到这个文件系统上对应用层来说就是访问一个磁盘目录可以做文件的创建、打开、读写、删除。底层的数据存储就是那块共享内存文件系统逻辑保证目录结构和数据块都被正确管理。第一次用sharefs的人容易把它当成普通文件系统完全忽略它底层的同步问题。其实sharefs的两端是各自独立内核在同时操作同一个内存区域如果没有锁机制两个系统同时写一个文件时文件内容就可能是混合的。所以初始化sharefs之后第一件事是定义好区块划分和访问规则哪些目录是Linux只写的哪些目录是LiteOS只写的哪个文件作为双方的状态标志位这些应该在项目初期就确定下来。在HI3556V200的SDK中sharefs的使用步骤是先在LiteOS侧配置一块共享内存大小和地址调用初始化接口创建文件系统实例然后在Linux侧通过挂载命令把同一块区域挂载到某个目录下。挂载成功后Linux侧就可以用标准文件API访问LiteOS写入的文件了。这里有一个容易踩的坑LiteOS侧如果先创建了某些目录或文件Linux侧目录项缓存可能没刷新需要用sync或者重新挂载才能看到。我在联调时就遇到过Linux上ls看不到LiteOS刚生成的文件折腾了好一会才发现是缓存问题不是传输失败。4.2 共享文件系统的读写一致性保障对于sharefs来说数据一致性比通信速率更值得关注。因为文件系统是基于内存的一旦某个系统崩溃或者复位内存里的目录元数据可能处于不一致状态另一侧读到的基本就是坏数据。我在实际项目里采用的策略是每个系统在写文件时按照先写数据到临时文件再通过rename操作切换成正式文件的方式。这样做能最大程度避免对端读到半个文件的情况因为rename操作在文件系统层是原子性的对端只会看到旧文件或者新文件不会看到一个写了一半的中间文件。共享内存的大小也要提前规划好。它不需要覆盖所有业务文件只需要放那些真正需要在系统间交换的数据。比如我的项目里LiteOS采集传感器数据并计算出一个结果结果文件只有几KB那给它分配1MB的共享区就绰绰有余了。如果把整个文件系统都塞进共享内存会导致内存预算非常紧张而且大文件读写占用的带宽和锁时间也会拖累两个系统的其他实时任务。共享区规划和业务功能一样需要单独评审。4.3 与IPCM和virt_tty组合使用的经典场景共享文件系统很少单独使用实际工程里更常见的模式是sharefs负责传大块数据IPCM负责传控制信号。举个例子LiteOS侧抓拍一张JPEG图片图片大小可能几百KB或者更大直接通过IPCM传这种数据是非常低效的——共享内存区域得开得很大一次传输中断次数太多CPU时间全耗在拷贝上了。合理的做法是LiteOS先把图片写入sharefs的某个约定目录然后通过IPCM发送一条短消息给Linux消息体里带着文件名、大小和时间戳Linux收到消息后去sharefs的目录里读取对应文件然后再通过IPCM回一个已接收的确认。整个过程数据量大的部分全走sharefs控制开销全部走IPCM各干各的活系统整体效率就上来了。还有人会把virt_tty和sharefs结合LiteOS把日志写入sharefs的文本文件Linux定期读取同时virt_tty作为实时流通道用于交互命令行。比如在LiteOS侧开一个调试shell通过virt_tty把命令输出发送到Linux侧映射成一个远程终端这种交互模式在调试阶段特别爽可以直接用Linux上的终端软件跟LiteOS交互不用单独接调试串口线。既保留了串口交互的习惯又不需要额外的物理连线。4.4 目录规划与容量管理的工程经验sharefs在实际项目中目录结构一定要在一开始就定死。我见过不少半路接手项目的人由于双方各自往共享目录写文件文件名又不加前缀最后分不清哪些文件是哪个系统写的只能靠内容去猜。我建议的做法是在共享区创建子目录时直接用前缀区分比如/linux_to_liteos、/liteos_to_linux然后各自只写属于自己的目录。如果需要设置访问权限那就更严格一些Linux侧对LiteOS写目录只读LiteOS对Linux写目录只读两边各占一块互不越界。容量管理上建议在应用启动时做一次共享区的使用统计并把剩余量上报给监控程序。共享文件系统满的时候不像普通磁盘会报错很多时候就是write返回失败或者文件内容截断这种问题在双系统联调时很难快速定位到文件系统满了这个原因。我自己吃过一次亏系统跑了一上午突然通信异常排查半天发现是sharefs空间被日志文件占满了。从那以后我在代码里加了共享区容量检测低于阈值就自动清理过期文件这个功能对长期运行的设备非常关键。5. 实测对比与实战选型建议5.1 三种机制的量化对比我把三种机制放在一起做了个实测数据是在HI3556V200上、两个系统都处于正常业务负载下测出来的具体数值会因负载和配置而异但大致量级可以参考。对比项IPCMvirt_ttysharefs通信模型消息/结构体字节流文件读写单次数据量≤几KB不限流式不限受共享区容量限制往返延迟50~200μs稍高于IPCM较高文件系统开销吞吐量低适合控制指令中等数百KB/s ~ 数MB/s高适合大块数据开发复杂度中需定义消息格式低类串口API低类文件API丢数据风险需要消息层重传需要流控和缓冲需要锁和一致性策略适用场景控制指令、握手、事件通知日志、音频流、交互终端配置文件、图片、批量数据看完这张表你会发现三种机制不是简单的好坏之分而是各有各的适用区间。IPCM胜在低延迟、事件语义明确但是传大量数据会显得很笨重virt_tty赢在编程体验接近物理串口适合管道式数据流sharefs最贴近应用层习惯但它的实时性最差如果你需要毫秒级响应别指望靠它。5.2 实际项目里怎么做通信架构设计我给出的选型建议是把控制面和数据面分开设计。控制面用IPCM负责双系统之间的状态同步、命令发送、ACK确认数据面根据数据类型选择virt_tty或sharefs。比如实时性要求中等的传感器数据流用virt_tty就足够了离线批量文件、配置参数这类延迟不敏感的直接走sharefs。不要试图只用一种机制解决所有问题那样做系统的复杂度反而更高。在一个从零开始的项目里我会按以下顺序推进通信方案先把IPCM通道跑通实现双系统之间的基础握手和心跳确认中断和共享内存正常然后叠加virt_tty把日志输出切到虚拟串口这样两边调试时能互相看到对方的运行状态最后按需引入sharefs用它承载需要持久化存储的配置文件或大块媒体数据。这个顺序其实是由底层到上层、由简单到复杂的推进思路。先搞定最基础的控制通路系统就有了神经后面再逐步扩展血管和消化系统。如果在第一步就急着把三种机制全接上一旦出问题你根本不知道是先排查中断配置还是先排查文件锁调试效率会变得极低。5.3 常见问题排查与避坑记录我在双系统通信联调阶段遇到过不少问题挑几个典型的记录下来应该能帮大家少踩坑。现象一Linux侧发送IPCM消息后LiteOS侧收不到中断但共享内存里的数据其实已经写进去了。排查思路先确认共享内存地址在两侧一致再确认中断号一致最后确认缓存一致性。数据能写进去但收不到中断多半是中断路由配置的问题或者LiteOS侧的中断服务程序还没注册成功。有一次我折腾了半天结果发现是设备树里中断号的偏移量写反了Linux侧用的中断号和LiteOS侧请求的不是同一个物理中断线。现象二virt_tty传大数据时接收侧收到的数据出现乱序或者粘包。这个问题本质上是流式传输的经典问题不是virt_tty坏了。处理方案是定义自己的帧格式比如每帧头部包含魔数、长度、序号接收侧根据帧头做切割和校验。我早期偷懒没在数据里加序号结果出问题后根本无法判断是数据丢了还是重排了后来老老实实做了协议封装问题立刻可定位。现象三sharefs文件写入后对端读不到内容。常见原因是文件系统缓存没刷新或者对端挂载时的权限配置有问题。先试试同步操作不行再看目录项缓存最后检查两边挂载选项是否一致。如果是长时间运行后出现这种问题还要考虑共享内存区域是否被其他机制占用了有一种情况是IPCM和sharefs使用了重叠的物理内存映射导致数据互相覆盖这种问题极其隐蔽需要查看内存映射表才能发现。5.4 关于调试工具和日志埋点的一点心得双系统调试最大的困难是看不到对方的世界。Linux侧可以打印日志LiteOS侧的日志你可能看不到反过来也一样。我的习惯是在通信代码的每一层关键位置都加日志开关尤其是中断入口、数据写入点、数据读取点每个点打印一行带方向标记的日志IN/OUT这样一旦出现问题就能快速判断是发送方没发出去还是接收方没收到又或者是中间传输丢了。另外在共享内存区域设计一个通信统计区会很有用。这里记录双方的收发计数、错误计数、最后一条消息的时间戳和序列号两个系统都能读取这份统计信息。当业务出现异常时第一时间去看这个统计区里的数据能比翻日志更快地定位问题方向。如果统计区显示收发计数不等就说明有数据在传输链路中丢失了如果计数一致但业务不对那问题很可能出在消息内容本身不在通信链路。最后再分享一个小技巧如果你在用这套双系统方案做项目我强烈建议在项目启动阶段就做一轮通信压力测试不要等到功能开发到一半才想起验证通信可靠性。压力测试的脚本不难写就是让LiteOS侧以不同频率、不同数据量持续向Linux侧发送数据同时记录丢包率、延迟、最大延迟这几个指标。我在一个安防项目里实测过当IPCM消息频率超过某个阈值之后系统中断负载急剧上升导致Linux侧的视频编码帧率明显下降——这个现象如果不提前测出来等整机联调时再发现排查起来会非常折磨人。通信这事不算复杂复杂的是在工程里把它用得可靠。IPCM、virt_tty、sharefs这三个机制每一个单独看都不难怎么组合、怎么规避边界问题、怎么在出问题时快速定位才是真正值钱的经验。希望这篇记录能帮到正在跟HI3556V200双系统较劲的同行们少走点弯路。