当前位置: 首页 > news >正文

嵌入式软件架构一:一个能让人放心接手的嵌入式项目,骨架长什么样

摘自:一枚嵌入式码农
链接:https://mp.weixin.qq.com/s/i31qtuJ3jiksJdirwR2F9w

目录

  • 一、先把"可维护"翻译成可衡量的东西
  • 二、目录结构:第一眼能不能看出"长什么样"
  • 三、模块依赖:不要让代码"互相钩着"
  • 四、构建系统:从"能编"到"一键出货"
  • 五、可观测性:出问题能"看见"
  • 六、测试:不靠"拿块板子试一下"
  • 七、文档:写在哪、写到哪种程度
  • 八、写在最后

写嵌入式有个怪现象:项目交接时的体感,往往比代码本身更能反映团队水平。
同样是十万行规模的工程,有的项目你 clone 下来当天就能编译出固件,第二天能定位到要改的模块;而有的项目,你光是搞清楚"main 在哪、外设是怎么初始化的、版本号是怎么定义的"就要折腾一周,更别提加新功能了。

这之间的差别,就是大家常说的"可维护性"。

但"可维护"这个词本身太虚。说一个项目"可维护",等于什么都没说——就像评价一道菜"好吃"。真正有意义的,是把它拆开来:究竟是哪些具体的特征,让一个嵌入式项目变得好维护?

这篇文章想聊的就是这件事。不谈大而无当的"工程师精神",也不堆 SOLID、KISS 之类的口号,而是把一个可维护的嵌入式项目从骨架到血肉拆给你看——目录怎么摆、依赖怎么连、构建怎么跑、调试怎么做、文档写到哪种程度,每一项都对着实际工程里会遇到的具体麻烦。

一、先把"可维护"翻译成可衡量的东西

"可维护性"这个词太大,但其实它落到嵌入式工程里,就是四件具体的事:


接手成本看的是第一眼:clone 下来的项目,能不能让人在没有原作者陪同的情况下,自己摸到"从哪里下手"。这个成本主要由目录结构、构建脚本、README 决定。

修改成本看的是改一处的影响半径:你想加一个传感器,是不是只动两个文件就行,还是要在十几个地方搜索替换。这个成本由模块边界和依赖关系决定。

验证成本看的是敢不敢改:改完之后,没办法确认是否影响了其他功能,就只能祈祷。这个成本由测试覆盖、可观测性、错误处理决定。

演进成本看的是长期生命力:项目能不能跟着硬件迭代往前走,还是每次硬件变动就要重写。这个成本由抽象层次和接口设计决定。

这四个成本,任何一项偏高,都会让项目慢慢退化成"没人敢动"的状态。下面要讲的所有具体做法,都是围绕降低这四个成本展开的。

二、目录结构:第一眼能不能看出"长什么样"

新人接手第一件事就是 tree -L 2 看一眼。这一眼看到什么,决定了他对这个项目的初始印象——以及他要花多久才能开始干活。

一个好的嵌入式项目目录,应该让任何一个有经验的工程师不看 README 也能猜出八成的用途。下面是一个比较通用的骨架:


这个结构里有几条原则值得单独说一下。

第一条:第三方代码必须隔离。 这是嵌入式项目最容易翻车的地方。很多团队为了"调试方便",把 FreeRTOS 的源码改了几行直接塞到自家 src 里——半年后你想升级到新版本,发现根本分不清哪些改动是自己加的、哪些是原版的。third_party/ 目录的存在不是装饰,它是一道"不能动"的红线。

第二条:易变和稳定要分开。app/ 是改动最频繁的层,hal/ 和 arch/ 一旦稳定基本不动。把它们放在不同目录,新人改业务时根本不用进底层目录,误伤的概率自然低。

第三条:配置必须集中。config/ 目录承担一个看不见但极其重要的角色——让一套代码支持多种硬件版本和客户定制。比如同一个网关有低端版(128K Flash)和高端版(512K Flash),可以这样组织:


构建时通过参数选板型,业务代码完全无感。这种设计在量产阶段能省下大量重复劳动。

第四条:构建产物绝不混入源码。build/ 目录用来放编译生成的 .o、.elf、.bin,并且写入 .gitignore。源码目录里看到 .o 文件是个非常糟糕的信号——它意味着 make clean 不彻底,也意味着没人在乎 git 里干不干净。

三、模块依赖:不要让代码"互相钩着"

目录摆得整齐只是表面功夫,真正决定项目能不能改的,是模块之间的依赖关系。

理想状态下,依赖是单向流动的——上层用下层,下层不知道上层。这种结构画出来是清晰的、自上而下的:


循环依赖的可怕之处在于:它让"独立修改、独立编译、独立测试"全部失效。你想改驱动,发现它依赖某个 service 的回调;想测 service,发现它要先初始化 app 里的全局变量。最终的结果就是,整个项目变成一个不可分割的"大泥球"。

要避免这种情况,有几个简单可执行的规则:

规则一:上层可以调下层接口,下层绝不调上层函数。 如果驱动层确实需要通知上层(比如中断里收到数据),用回调函数注册或者事件队列这种"反向控制"模式,而不是直接 extern 一个 app 层的函数过来用。

规则二:模块间的接口收敛在头文件里。 一个模块对外只暴露一个 xxx.h,其余实现细节都关在 .c 文件或 xxx_internal.h 里。判断一个项目模块化做得好不好,有个朴素的标准——任意拿出一个模块的 .h 文件,能不能脱离整个工程独立看明白?

规则三:警惕全局变量和巨型 common.h。 这是嵌入式项目里最常见的"暗依赖"。一旦 common.h 里塞满了几百个宏、几十个 extern 变量,几乎所有源文件都会 include 它——这等于让整个项目变成一张全连接图。

判断模块依赖健康度,有个非常简单的方法:在项目根目录跑一句 grep,统计每个 .c 文件 include 了多少个外部头文件。如果某个文件 include 超过 10 个非系统头文件,多半设计有问题。这个数字在干净的项目里通常是 3-5 个。

四、构建系统:从"能编"到"一键出货"

四、构建系统:从"能编"到"一键出货"

很多嵌入式项目对构建系统的要求停留在"能编出 bin"——往往是 IDE 工程文件加几个手动配置项就上路了。这种做法在团队规模小、芯片单一的时候没问题,但只要项目稍微复杂一点,就会暴露各种问题:

• 换台机器编译,因为 IDE 版本不同,结果不一样
• 想接入 CI 自动构建,发现没法命令行编译
• 同一份代码要出 debug、release、客户 A 版、客户 B 版,全靠手动改宏
• 编译完不知道每个模块占了多少 ROM/RAM,出了大小问题查半天

一个可维护的项目,构建系统至少要做到这几件事:

一行命令出固件。 不管是 Makefile、CMake 还是 SCons,从干净的 git 仓库 clone 下来,应该一条命令就能生成可烧录的 bin:


这件事的价值不在"省时间",而在"消除环境依赖"。任何"在我电脑上能编"的项目都是定时炸弹。

构建产物可追溯。 编译出来的固件里要能查到:是哪个 git commit、谁编的、什么时间、用了什么配置。简单的做法是构建时把这些信息塞到一个 version 区段:

constcharbuild_info[]__attribute__((used))="Version: 1.2.3\n""Commit: "GIT_HASH"\n""Time: "BUILD_TIME"\n""Board: "BOARD_NAME"\n";

现场一台设备出问题,先把固件读出来看 build_info,立刻知道是哪个版本——这种小细节能省下大量"对版本"的扯皮时间。

资源占用可见。 构建结束后自动打印一份 arm-none-eabi-size 的报告,让 ROM/RAM 占用一目了然。配合 CI 还能做"每次提交后对比上次大小",超出阈值就警告。这对避免"不知不觉撑爆 Flash"很有用。

五、可观测性:出问题能"看见"

嵌入式调试最头疼的不是 bug 本身,而是bug 出在没人在场的现场——客户那边的设备每周死机一次,你拿不到调试器、看不到串口输出、复现不了。

要让这种场景下还能定位问题,项目里得有一套"自我陈述"的机制。具体落到代码上,主要是三件事:

第一件事:分级日志。 不是 printf(“here\n”) 满地撒,而是有层级、有开关、可裁剪:


每条日志带上模块名和时间戳,关键路径还要带上"上下文"(哪个会话、哪个连接、哪个任务)。日志不是越多越好——信噪比比数量重要得多。

第二件事:错误码体系。 函数返回错误别只用 -1,而是定义有意义的错误码,并且能从错误码反查到"是哪个模块、什么类型的错"。常见做法是用高位区分模块:


第三件事:关键状态快照。 在出错时把当前的关键状态(任务状态、关键变量、最近几条事件)存到一块固定的 RAM 或者 Flash 区域,重启后能读出来。这种"黑匣子"机制对解决偶发问题极其有用。

六、测试:不靠"拿块板子试一下"

嵌入式工程师对单元测试有个常见的抗拒心理——“我的代码要操作硬件,怎么测?” 这其实是把"测试"和"硬件相关测试"混在了一起。

实际上,一个项目里能在主机上跑的代码远比想象中多。协议解析、状态机、参数管理、算法模块——这些不直接碰寄存器的部分,都可以脱离硬件单独测试。HAL 层抽象做好之后,连一部分驱动逻辑都能在 PC 上模拟。

主机端测试的回报率高得惊人:改完代码 5 秒钟跑完 200 个用例,比烧录到板子上调试快两个数量级。它不能替代板上验证,但能拦下大部分逻辑层的 bug。

七、文档:写在哪、写到哪种程度

写嵌入式的人对文档有两种极端:要么完全不写,要么强迫给每个函数都写 doxygen 注释。两者都不太对。

真正值得花时间维护的文档,其实就那么几样:

• README:怎么 clone、怎么编、怎么烧、产物在哪。这是新人看到的第一份文档,也是最容易过期的。每次改动构建脚本时顺手更新它。
• 架构图:一张能讲清楚"系统由哪些模块、它们怎么连"的图。手画拍照也行,关键是要新。
• 接口契约:每个对外模块的 xxx.h 文件头部,写清楚这个模块"做什么、不做什么、调用约束"。这比函数级注释更值得花精力。
• 决策记录:当年为什么选 RTOS A 不选 B,为什么 ADC 不走 DMA,为什么状态机这样划分——这些"为什么"比"是什么"更稀缺,也最容易随着人员流失消失。
代码本身能讲清楚"做了什么",但完全讲不清楚"当年为什么这么做"。而接手者最缺的,恰恰是后者。一份哪怕只有几页的"决策记录",价值往往超过一万行注释。

至于函数级注释,秉持一个原则就行——只在 WHY 不明显的地方写。uint32_t crc16(const uint8_t *data, size_t len); 这种不需要注释。但 “为什么这里要 delay 50ms”、“为什么这个寄存器要写两次”,必须写下来,否则后人改代码时第一反应一定是"这是不是冗余?删了试试"。

八、写在最后

回过头看,可维护的嵌入式项目并没有什么神奇配方。它无非是一些朴素的工程实践叠加起来——清晰的目录、单向的依赖、自动化的构建、可见的运行状态、跑得起来的测试、写下了"为什么"的文档。

每一项单独看都不复杂,难的是长期坚持。一个项目从开始的几千行到后来的几万行,过程中有无数次"先这么写吧,以后再改"的妥协。这些妥协单看都合理,加在一起就是后来人接手时的那一声叹气。

回到开头那张"四种成本"的尺子。下次你看自己的项目,可以拿它对照一下:

四个问题里有两个让你心里一沉,那大概率就是项目开始走下坡路的信号。这时候不需要推倒重来——往往只需要从这四个维度里挑最痛的一个,花一两周整顿一下,就能感觉到明显的好转。

可维护性不是一次性的工程,而是日积月累的习惯。它换不来短期的功能交付速度,但能保住这个项目两年、三年、五年后还能继续走下去的可能性。 对嵌入式这种动辄生命周期十年起步的产品,这个可能性比什么都重要。

http://www.jsqmd.com/news/800009/

相关文章:

  • MinerU 实战训练营:RAG 数据预处理的最后一块拼图
  • 阿里:时序课程解决多轮蒸馏不稳定
  • 手把手调SVPWM:如何根据你的直流母线电压Udc设置正确的调制比不炸管?
  • 从关中到汉中:用Python+DEM数据,分析古代行军路线的地理可行性
  • Awesome List自动化生成:从手工整理到工业化生产的效率革命
  • 健身直播必备:手表心率如何实时显示在手机拍摄画面上?
  • YOLO26引入Dual-ViT自注意力:局部与全局两条主线的完美交汇
  • 基于Agent-Next框架的Polymarket预测市场模拟交易系统构建指南
  • 告别重复劳动:手把手教你用SAP LSMW为MM模块创建第一个数据导入程序
  • 四轴飞行器入门:BNO055与JY901传感器模块选型及实测对比
  • 2026年4月国内知名的数字化服务平台源头厂家推荐,KYN28-12铠装移开式金属封闭开关柜,数字化服务平台公司哪家好 - 品牌推荐师
  • TinyML实战:tiny-ai-client在MCU上的轻量级AI推理部署指南
  • 效率翻倍!依据2026白皮书,这样部署OpenClaw最快(移动云电脑版)
  • 别再死记硬背了!用Python+NumPy图解NCHW与NHWC,彻底搞懂数据排布
  • C++ 入门核心语法|从 Hello World 到基础特性一次性吃透
  • HIOKI-3272 日置 3272 电源 用于3273-50 3274 3275 3276探头
  • LocalChat:零门槛本地部署开源大语言模型,实现隐私安全的离线AI对话
  • 别再花钱买Token了!手把手教你免费申请Wechaty Token,15天体验版保姆级教程
  • 从Excel舍入到IEEE754:你的财务计算和游戏物理引擎可能都错了
  • 电力管供应商/热浸塑电力管厂家哪家靠谱?2026年热浸塑钢管厂家推荐:福派安领衔,口碑好的热浸塑电缆保护管厂家优质盘点 - 栗子测评
  • 收藏!小白程序员必看:LLM推理延迟的“快慢”真相与优化秘籍
  • 2026年4月做得好的网架直销厂家口碑推荐,国内网架口碑推荐,结构稳固,网架承载能力超强大 - 品牌推荐师
  • 2025届必备的五大AI学术工具解析与推荐
  • 为什么你的Perplexity Science搜索总错过最新预印本?——基于arXiv/medRxiv/SSRN实时源的3层校验机制(含Python自动化脚本)
  • BUUCTF实战:从加密流量到明文Flag——[DDCTF2018]流量分析全解析
  • IP6546_FB 3A 输出电流的高效同步降压 DCDC
  • ARM GICD_ITARGETSR寄存器解析与多核中断分发
  • OpenClaw智能体安全防护实战:ClawKeeper三层纵深防御架构解析
  • 2026花岗岩透水板厂家推荐:陶瓷透水砖厂家实力榜单推荐-设计感与品质兼具 - 栗子测评
  • 3D-DRAM加速器技术与LLM推理优化解析