4、系统架构设计:整体模块划分
好,咱们接着往下走。需求分析做完了,功能清单也列好了。下一步是什么?
我个人习惯,是先画一张大图。把整个车门控制系统拆成几大块。就像盖房子,先搭骨架,再填砖头。
说白了,就是系统架构设计。这一步要是没想清楚,后面写代码就是一团乱麻。我在项目里见过太多人,上来就写代码,写到一半发现模块之间耦合太紧,改一处动全身。嗯,那滋味可不好受。
4.1 四大模块:各司其职
我把车门控制系统拆成了四个核心模块。你想想看,一辆车的车门,要感知、要决策、要执行、还要跟外界通信。这不正好对应四个模块吗?
- 传感模块:负责感知。比如门把手有没有被拉动,车窗位置在哪,车门是开是关。
- 控制模块:负责决策。收到传感信号后,判断该干什么。比如“用户拉了两下门把手,我该解锁了”。
- 执行模块:负责干活。控制模块下了指令,它就去驱动电机、点亮指示灯。
- 通信模块:负责交流。跟车身CAN总线、或者蓝牙钥匙说话。
这四块,缺一不可。我当年做第一个项目时,把通信模块跟控制模块写在一起,结果调试的时候,通信一卡顿,整个控制逻辑都乱了。后来才老老实实分开。
4.2 模块间接口定义:规矩要立好
模块分好了,它们之间怎么说话?这就是接口定义。没有规矩,不成方圆。
我建议,接口要尽量简单、稳定。别今天改明天改,不然上下游都得跟着遭殃。
下面这张表,是我常用的接口定义方式。你看一眼就明白了。
| 源模块 | 目标模块 | 接口名称 | 数据类型 | 说明 |
|---|---|---|---|---|
| 传感模块 | 控制模块 | sensor_door_status | uint8_t | 0=关闭, 1=打开, 2=半开 |
| 传感模块 | 控制模块 | sensor_handle_pull | uint8_t | 0=未拉, 1=拉一次, 2=拉两次 |
| 控制模块 | 执行模块 | actuator_lock_cmd | uint8_t | 0=无操作, 1=上锁, 2=解锁 |
| 控制模块 | 执行模块 | actuator_window_cmd | uint8_t | 0=停止, 1=上升, 2=下降 |
| 通信模块 | 控制模块 | comm_remote_lock | uint8_t | 0=无, 1=远程上锁, 2=远程解锁 |
你看,每个接口都清清楚楚。源模块只管发,目标模块只管收。中间不掺杂任何业务逻辑。
核心原则:接口就是契约。一旦定下来,就不要轻易改。改接口的成本,比你想象的高得多。
4.3 控制模块:大脑怎么工作?
控制模块是核心。它接收传感模块和通信模块的信号,然后做出判断,再给执行模块下指令。
它的内部,我习惯用一个状态机来管理。比如车门有“已锁”、“已解锁”、“正在上锁”等状态。不同状态下,对同样的信号,反应是不一样的。
举个例子:
- 车门已锁,用户拉一下门把手 → 控制模块不理你(防止误操作)。
- 车门已锁,用户拉两下门把手 → 控制模块判断:这是解锁意图,发解锁指令。
- 车门已解锁,用户拉一下门把手 → 控制模块判断:这是开门意图,发开门指令。
你看,同样的“拉门把手”信号,状态不同,处理结果完全不同。这就是状态机的妙处。
我的小技巧:写状态机时,先把所有可能的状态列出来,画个状态转移图。别急着写代码。图画清楚了,代码就是翻译工作。
4.4 执行模块:只管干活,不问为什么
执行模块很简单。它就是个“工具人”。控制模块让它上锁,它就驱动电机。让它开窗,它就转马达。
它不需要知道为什么上锁,也不需要知道什么时候该上锁。它只做一件事:收到指令,执行指令。
这样做的好处是,执行模块可以独立测试。我当年调试车窗电机时,直接给执行模块发PWM信号,看电机转不转。根本不用管控制模块的逻辑对不对。
注意:执行模块一定要有超时保护。比如车窗上升,如果一直收到“上升”指令,电机一直转,可能会烧掉。我见过一个案例,就是没加超时,车窗卡住了,电机还在转,最后换了整个总成。
4.5 传感模块:真实世界的眼睛
传感模块负责把物理世界的信号,变成电信号。比如门把手的位置,是用一个霍尔传感器检测的。车窗位置,是用一个电位器检测的。
这里有个坑:信号抖动。你想想看,机械开关在闭合的瞬间,会弹跳好几次。如果不做处理,控制模块会收到一连串的“拉门把手”信号,以为用户在疯狂拉门。
我曾经就被这个坑过。调试时发现,明明只拉了一下门把手,日志里却记录了5次拉门事件。后来加了软件消抖,才解决。
消抖的方法很简单:检测到信号变化后,等20毫秒再读一次。如果两次一致,才认为是有效信号。
// 软件消抖示例
uint8_t debounce(uint8_t raw_signal) {
static uint8_t last_stable = 0;
static uint8_t count = 0;
if (raw_signal == last_stable) {
count = 0;
return last_stable;
} else {
count++;
if (count >= 5) { // 连续5次采样一致
last_stable = raw_signal;
count = 0;
}
return last_stable;
}
}
4.6 通信模块:跟外界打交道
通信模块负责跟车身网络、或者手机蓝牙通信。它把收到的远程指令,转成内部接口能识别的格式,然后发给控制模块。
比如,手机发来一条“解锁”指令。通信模块收到后,解析出指令内容,然后调用 comm_remote_lock = 2。控制模块看到这个值,就知道该解锁了。
这里要注意协议兼容性。不同车型、不同年份,CAN总线上的协议可能不一样。我建议在通信模块里做一个协议适配层,把外部协议统一成内部接口。这样,换一个车型,只需要改适配层,控制模块不用动。
4.7 总结一下
系统架构设计,说白了就是分而治之。把大问题拆成小问题,每个小问题交给一个模块去解决。模块之间通过接口通信,互不干扰。
这样做的好处,你以后会慢慢体会到:
- 每个模块可以独立开发、独立测试。
- 出了问题,能快速定位是哪个模块的锅。
- 换一个模块,不影响其他模块。
嗯,架构设计这一步,值得多花点时间。磨刀不误砍柴工嘛。