第三章:系统架构设计——模块划分与接口定义、状态机设计、数据流与电源管理架构

好,咱们今天聊聊系统架构。说实话,很多刚入行的工程师拿到需求就急着写代码,结果写到一半发现这里耦合太紧、那里接口对不上,最后只能推倒重来。我在项目里见过太多次这种悲剧了。

做嵌入式软件,尤其是牙刷这种消费电子产品,架构设计就是地基。地基歪了,后面再怎么装修都白搭。今天我就把我在多个量产项目中沉淀下来的经验,掰开了揉碎了讲给你听。

3.1 模块划分:别让代码变成一团乱麻

模块划分这事儿,说白了就是「高内聚、低耦合」。我个人的习惯是,先画一张功能框图,把牙刷要干的事列出来:

  • 电机控制模块:负责马达启停、转速调节、堵转检测
  • 电池管理模块:电量检测、充电控制、低电量保护
  • 按键与LED模块:按键扫描、指示灯控制、模式切换
  • 定时与模式管理模块:刷牙计时、2分钟提醒、30秒换区提示
  • 状态机模块:全局状态流转控制
  • 电源管理模块:休眠、唤醒、功耗优化

你想想看,如果把这些功能全塞到一个文件里,那维护起来得多痛苦?我曾经接手过一个项目,一个.c文件写了3000多行,光找bug就花了两周。后来我强制拆成6个模块,每个模块不超过500行,代码清晰多了。

核心原则:每个模块只做一件事,并且做好。模块之间通过接口通信,不要直接访问对方的全局变量。

3.2 接口定义:定好规矩,少踩坑

模块划分好了,接下来就是接口定义。接口就像模块之间的「合同」,写清楚了双方的责任边界。

我一般用结构体来定义接口,而不是散落的函数。举个例子:

/* 电池管理模块接口 */
typedef struct {
    uint8_t (*get_battery_level)(void);      /* 获取电量百分比 */
    bool    (*is_charging)(void);             /* 是否在充电 */
    void    (*enter_charging_mode)(void);     /* 进入充电模式 */
    void    (*exit_charging_mode)(void);      /* 退出充电模式 */
    void    (*low_battery_handler)(void);     /* 低电量回调 */
} Battery_Interface_t;

这样做的好处是啥?接口清晰,换模块的时候只要实现同样的结构体就行。我记得有一次客户要求换电池方案,我只需要重新实现这个接口,上层代码一行都不用改。

我的习惯:接口函数命名统一用「模块名_动作」的格式,比如 battery_get_level()、motor_set_speed()。这样看代码就知道是哪个模块的。

3.3 状态机设计:牙刷的「人生」四阶段

牙刷的状态其实很简单,就四个:待机、工作、充电、错误。但状态之间的流转,稍不注意就会出问题。

我画了一张状态转移图,你感受一下:

        +---------+
        |  待机   |<----------------+
        +----+----+                 |
             |                      |
        [按键按下]              [充电完成]
             |                      |
             v                      |
        +----+----+           +----+----+
        |  工作   |----------->|  充电   |
        +----+----+   [插入]   +----+----+
             |                      |
        [故障检测]              [故障检测]
             |                      |
             v                      v
        +----+----+           +----+----+
        |  错误   |<-----------|  错误   |
        +---------+           +---------+

嗯,这里要注意:错误状态是最高优先级。不管在哪个状态,只要检测到故障(比如电机堵转、电池过温),必须立刻进入错误状态。

我曾经犯过一个错:在充电状态下,没有处理电机堵转的检测。结果有一次样品测试,充电时牙刷掉地上,电机卡住了,但系统还在充电状态,温度一路飙升...还好只是烧了样品,没出安全事故。从那以后,我每个状态入口都加上了故障检测。

避坑指南:状态机一定要有「超时保护」。比如待机状态下,如果按键被卡住超过10秒,应该自动进入错误状态,而不是一直等待。我曾经遇到过按键弹簧卡死的情况,如果没有超时保护,电池会一直放电到亏电。

3.4 数据流:信息是怎么跑的

数据流说白了就是「谁产生了数据,谁消费了数据」。在牙刷系统里,数据流主要有三条:

数据流 生产者 消费者 传输方式
按键事件 按键模块 状态机模块 消息队列
电量信息 电池管理模块 LED模块、状态机模块 全局变量+互斥锁
故障信息 各模块 状态机模块、错误处理模块 回调函数

我个人建议用消息队列来处理事件型数据(比如按键),用全局变量+互斥锁来处理状态型数据(比如电量)。为什么?事件型数据有先后顺序,消息队列能保证顺序;状态型数据只需要最新值,全局变量更高效。

3.5 电源管理架构:省电就是省钱

牙刷是电池供电的产品,电源管理做不好,用户两天充一次电,谁受得了?

我一般把电源管理分成三级:

  • 运行模式:正常工作,所有模块都供电,电流约100mA
  • 待机模式:电机断电,MCU进入低功耗模式,只保留按键唤醒,电流约50μA
  • 休眠模式:几乎全部断电,只有RTC在跑,电流约5μA

你想想看,如果用户刷完牙忘了关机,系统应该怎么做?我的方案是:工作状态下,如果检测到2分钟没有按键操作,自动进入待机模式。如果待机超过30分钟,进入休眠模式。

关键点:从休眠模式唤醒时,系统要能快速恢复到之前的状态。我一般会在休眠前把关键参数(比如当前模式、剩余电量)保存到EEPROM里,唤醒后直接读取,不用重新初始化。

嗯,说到电源管理,还有一个容易被忽略的点:充电时的功耗。充电时电机不能工作,这个逻辑一定要在硬件和软件上都做保护。我见过一个方案,软件上做了限制,但硬件上没有,结果充电时误触按键,电机转了一下,把充电电流拉偏了,差点烧了充电芯片。

好了,系统架构这块就聊到这儿。下一章咱们会深入到每个模块的具体实现,到时候再细聊。记住一句话:架构设计花的时间,会在后面的开发中十倍地还回来

课后思考:如果牙刷要增加蓝牙功能,你觉得应该在哪个模块里加?接口要怎么设计才能不影响现有模块?