第三章:TTU软件架构——嵌入式Linux系统裁剪、应用层与驱动层划分、关键进程管理

各位好,咱们接着聊TTU的软件架构。说实话,很多刚入行的朋友拿到TTU的板子,第一反应就是“这不就是个跑Linux的嵌入式设备吗?”——对,也不全对。TTU的软件架构,说白了就是要在有限的硬件资源里,把数据采集、处理、上报这些活儿安排得明明白白。我个人习惯把这一章分成三块来讲:系统怎么裁、层次怎么划、进程怎么管。

3.1 嵌入式Linux系统裁剪——别让“胖子”上战场

为什么一上来就讲裁剪?你想想看,TTU的Flash通常就128MB到256MB,RAM也就256MB到512MB。你要是把完整的Ubuntu或者Debian塞进去,那基本就啥也干不了了。嵌入式Linux裁剪,核心目标就一个:去掉所有不需要的东西,只保留能跑业务的最小集合

核心原则:“用不到的模块,一个都不要留。留了就是浪费资源,浪费资源就是给自己挖坑。”

我在项目中遇到过这么一件事:某款TTU在批量部署后,偶尔会出现系统卡死。查了半天,发现是内核里编译了完整的USB协议栈,而TTU根本就没接USB设备。这个协议栈在后台占用了不少内存,还时不时触发一些中断处理。嗯,这就是典型的“裁剪不到位”的教训。

3.1.1 内核裁剪要点

内核裁剪,我建议从这几个方面入手:

  • 文件系统支持:TTU一般用SquashFS或UBIFS做根文件系统,只保留ext4作为数据分区。别把NTFS、FAT32这些全编进去。
  • 网络协议栈:保留TCP/IP、UDP、ARP、ICMP就够了。IPv6如果暂时用不到,直接去掉。我记得有一次为了省空间,连IPv6模块都裁了,结果客户后来要求支持双栈,又得重新编译——所以这里要留个心眼,看客户需求。
  • 驱动模块:只保留TTU实际用到的硬件驱动。比如串口、SPI、I2C、以太网、4G/5G模块驱动。像什么显卡驱动、声卡驱动、蓝牙驱动,统统不要。
  • 调度器:嵌入式场景一般用CFS(完全公平调度器)就够了。实时性要求高的任务,可以启用PREEMPT_RT补丁。

我的小技巧:裁剪完成后,用size命令看看内核镜像大小。一个裁剪得当的TTU内核,压缩后应该在2MB到4MB之间。如果超过5MB,你就得回头看看是不是漏了什么不该留的东西。

3.1.2 根文件系统裁剪

根文件系统是另一个“吃空间”的大户。我一般用Buildroot或Yocto来构建,手动指定需要保留的库和工具。

举个例子,一个典型的TTU根文件系统,应该包含这些:

组件 说明 是否必须
busybox 提供基本的shell命令
glibc / musl C运行时库,musl更小
libpthread 多线程支持
libsqlite3 本地数据存储
libcurl HTTP/HTTPS通信
openssl 加密通信
gdb 调试工具,发布版去掉
perl / python 脚本语言,一般不用

我曾经为了省空间,把glibc换成了musl。结果发现某个第三方库对musl支持不好,跑起来各种段错误。最后又换回glibc,但把不需要的locale数据全删了。嗯,这就是“省小钱吃大亏”的典型。

3.2 应用层与驱动层划分——各司其职,别越界

TTU的软件架构,我习惯分成三层:驱动层、中间层、应用层。驱动层负责跟硬件打交道,应用层负责业务逻辑,中间层负责“翻译”和“调度”。

为什么要分这么清楚?说白了,就是解耦。你想想看,如果应用层代码直接操作寄存器,那换一个硬件平台,所有代码都得重写。这谁受得了?

3.2.1 驱动层:硬件的“翻译官”

驱动层主要干这些事:

  • 串口驱动:负责跟电能表、采集器通信。一般用RS-485接口,驱动里要处理好收发切换、波特率配置、数据帧校验。
  • SPI/I2C驱动:连接ADC、温度传感器、RTC等外设。我记得有一次,SPI驱动里的时钟极性配置错了,导致读回来的数据全是乱码。查了两天才发现是CPOL和CPHA没配对。
  • 网络驱动:管理以太网口和4G/5G模块。这里要注意,4G模块的驱动往往需要PPP拨号或者QMI方式,不同模块差异很大。
  • GPIO驱动:控制指示灯、继电器、看门狗等。

驱动层的设计原则:向上提供统一的接口,向下屏蔽硬件差异。比如,不管你是用哪家的电能表,驱动层都提供一个meter_read()函数,应用层只管调用就行。

3.2.2 应用层:业务的“大脑”

应用层是TTU真正干活的地方。它不关心数据是怎么从硬件读上来的,只关心数据怎么处理、怎么存储、怎么上报。

典型的应用层模块包括:

  • 数据采集模块:定时从驱动层读取电能表数据,包括电压、电流、功率、电量等。
  • 数据处理模块:做数据校验、异常检测、冻结数据生成。比如,发现某相电压突然掉到0,就要触发告警。
  • 通信管理模块:负责跟主站通信,支持DL/T 645、101/104等规约。规约解析是个细活,我见过不少因为规约解析错误导致数据上报错乱的案例。
  • 本地维护模块:提供本地调试接口,比如通过串口或者蓝牙进行参数配置。

注意:应用层和驱动层之间,一定要通过明确的API接口通信。千万不要在应用层代码里直接操作/dev/mem或者调用ioctl去控制硬件。我曾经接手过一个项目,前任工程师在应用层里直接写GPIO寄存器,结果换了颗主控芯片,整个应用层都得重写。教训深刻啊。

3.3 关键进程管理——谁该活着,谁该去死

TTU上跑的进程不少,但资源有限。怎么管好这些进程,让它们各司其职、不打架,是个技术活。

我一般把进程分成三类:核心进程、辅助进程、临时进程

3.3.1 核心进程:必须24小时在线

这些进程是TTU的命根子,挂了就得立刻重启:

  • 采集进程(collector):负责定时采集电能表数据。一般每15分钟采集一次,采集完写入本地数据库。
  • 通信进程(comm):负责跟主站建立长连接,上报数据、接收指令。这个进程如果挂了,TTU就失联了。
  • 看门狗进程(watchdog):监控其他核心进程的状态。发现某个进程挂了,就尝试重启它。如果连续重启失败,就触发系统复位。

对于核心进程,我建议用supervisor或者systemd来管理。设置好自动重启策略,比如“如果进程退出,等待3秒后重启,最多重启5次”。

3.3.2 辅助进程:随叫随到,但别占着茅坑不拉屎

这些进程不是一直需要,但关键时刻得能起来:

  • 本地维护进程(local_mgr):平时休眠,当检测到本地调试接口有连接请求时才启动。
  • 日志上传进程(log_upload):定时将本地日志打包上传到主站。上传完成后自动退出,释放资源。
  • 固件升级进程(ota):只有在收到升级指令时才启动。升级完成后自动退出。

对于辅助进程,我习惯用socket激活或者D-Bus激活的方式。说白了,就是“按需启动”,不用的时候就别占着内存。

3.3.3 进程间通信:别让它们鸡同鸭讲

进程之间怎么通信?我常用的方式有:

  • 共享内存:适合大数据量的传递,比如采集到的原始数据。但要注意加锁,防止数据竞争。
  • 消息队列:适合传递控制指令,比如“开始采集”、“停止上报”。
  • Socket:适合跨网络通信,比如跟主站通信。
  • 信号:适合简单的通知,比如“进程退出”、“配置变更”。但信号能携带的信息有限,别滥用。

避坑指南:我曾经在项目里用共享内存传递采集数据,结果两个进程同时写同一个内存区域,导致数据错乱。后来加了个信号量,问题就解决了。记住:共享内存不加锁,就是给自己找麻烦

3.3.4 进程监控与自愈

TTU部署在户外,环境恶劣,进程挂掉是常有的事。所以,自愈能力很重要。

我一般这样设计:

  1. 看门狗进程每隔10秒检查一次核心进程的状态。
  2. 如果发现某个进程挂了,先尝试重启它,并记录日志。
  3. 如果同一个进程在1小时内重启超过3次,就认为它“病得不轻”,触发系统复位。
  4. 系统复位后,如果还是反复重启,就进入“安全模式”,只保留基本通信功能,等待远程维护。

嗯,这套机制我在好几个项目里都用过,效果不错。至少能保证TTU在大部分异常情况下还能“喘口气”,不至于彻底罢工。

小结

这一章的内容,说白了就是三件事:系统要瘦身、层次要分明、进程要管好。裁剪系统是为了省资源,划分层次是为了好维护,管理进程是为了保稳定。这三件事做好了,TTU的软件架构就立住了。

下一章,咱们聊聊具体的采集流程和数据处理算法。到时候我会拿一个实际项目里的代码片段出来,给大家拆解一下。敬请期待。


公众号:蓝海资料掘金营,微信deep3321