第二章:EPS软件架构——分层架构、模块划分、接口定义原则
好,咱们直接进入正题。EPS软件架构,说白了就是一套“怎么搭积木”的规则。你想想看,一个转向系统里,有传感器信号进来,有电机要控制,有CAN总线要通信,还有故障诊断要处理——这么多东西挤在一起,如果不分层、不划模块,那代码就是一团乱麻。
我个人习惯,做EPS架构设计时,脑子里先画三张图:分层图(谁在上谁在下)、模块图(谁负责什么)、接口图(谁跟谁怎么说话)。这三张图画清楚了,代码写起来就顺了。
2.1 分层架构:为什么非要分三层?
EPS软件通常分三层:应用层、抽象层、硬件驱动层。这不是拍脑袋定的,是血的教训换来的。
我记得刚入行那会儿,有个项目直接把电机控制代码和CAN通信代码写在一起。结果呢?换了个MCU型号,整个代码重写了一遍。从那以后,我坚决要求分层。
| 层级 | 职责 | 典型模块 | 依赖关系 |
|---|---|---|---|
| 应用层 | 实现转向策略、控制算法 | 助力计算、回正控制、阻尼控制 | 只调用抽象层接口 |
| 抽象层 | 封装硬件差异,提供统一API | 传感器抽象、电机抽象、通信抽象 | 调用驱动层,被应用层调用 |
| 硬件驱动层 | 直接操作寄存器、外设 | ADC驱动、PWM驱动、SPI驱动 | 最底层,不依赖上层 |
为什么要这么分?说白了就是解耦。应用层的工程师不需要知道扭矩传感器是SPI还是I2C接口,他只需要调用 GetTorqueSensorValue() 就行。硬件换了,改驱动层就好,应用层代码动都不用动。
核心原则:上层只能调用相邻下层的接口,不能跨层调用。应用层不能直接操作寄存器,驱动层不能包含控制逻辑。
2.2 模块划分:每个模块只干一件事
模块划分有个黄金法则——单一职责原则。一个模块只负责一个功能领域,不要搞“万能模块”。
我在项目中遇到过最头疼的事,就是有人把故障诊断逻辑散落在各个模块里。助力模块里写一段诊断,通信模块里又写一段。最后查个故障码,得翻遍整个工程。后来我强制要求:所有诊断逻辑集中到一个模块。
典型的EPS模块划分如下:
- 传感器管理模块:负责扭矩传感器、角度传感器、车速信号的采集与滤波。输出的是“干净”的信号值。
- 助力控制模块:根据车速、扭矩、方向盘转角,计算目标助力电流。这是核心算法模块。
- 电机控制模块:接收目标电流,执行PID控制,输出PWM占空比。它不管助力策略,只管“让电机转到这个电流”。
- 通信模块:处理CAN/LIN总线收发,解析网络管理报文。它只负责数据搬运,不参与控制。
- 故障诊断模块:监控所有模块的健康状态,记录故障码,触发安全响应。
- 系统管理模块:负责状态机切换(上电、运行、休眠、故障),协调各模块的初始化与关闭。
我的小技巧:每个模块的代码文件命名,用“模块名_功能”的格式。比如 motor_control.c、sensor_torque.c。这样找文件时一目了然,不用猜。
2.3 接口定义原则:接口就是契约
接口定义,是架构设计里最容易吵架的地方。我见过一个团队,为了一个函数参数是int还是uint8_t,争论了半小时。其实核心原则就几条,想清楚就好。
原则一:接口要稳定
接口一旦定义好,就不要轻易改。尤其是应用层和抽象层之间的接口。改一个接口,可能意味着所有上层模块都要跟着改。我曾经因为把 GetVehicleSpeed() 的返回值从km/h改成m/s,导致三个模块的标定参数全部要重新算——教训深刻。
原则二:参数要明确
接口的参数,能明确就明确。不要用 void* 传一切,也不要用一个int传多种含义。比如:
// 不好的写法:参数含义不明确
void SetMotorCurrent(int value, int mode);
// 好的写法:参数类型明确,含义清晰
typedef enum {
CURRENT_MODE_TARGET = 0,
CURRENT_MODE_LIMIT = 1
} MotorCurrentMode_t;
void SetMotorCurrent(int16_t current_mA, MotorCurrentMode_t mode);
原则三:返回值要统一
所有接口的返回值,建议统一用 Std_ReturnType(E_OK / E_NOT_OK)。不要有的返回int,有的返回bool,有的返回指针。统一了,上层调用时错误处理逻辑就一致了。
原则四:接口要可测试
你想想看,如果接口内部直接操作了全局变量,或者依赖了某个硬件状态,那单元测试怎么写?接口设计时就要考虑:能不能用桩函数替换底层?比如电机控制接口,应该允许传入模拟的电流值,而不是非得接真实电机。
避坑指南:我曾经在一个项目里,接口定义时没考虑时序问题。结果两个模块互相调用,形成了循环依赖——A模块调用B模块,B模块又回调A模块。最后系统在特定工况下死锁了。从那以后,我规定:接口调用必须是单向的,不允许回调。
2.4 一个实际例子:助力控制模块的接口设计
光说理论没意思,咱们看个具体的。假设我们要设计助力控制模块的接口:
// 助力控制模块对外接口
// 输入接口
Std_ReturnType EPS_Assist_SetVehicleSpeed(uint16_t speed_kmh);
Std_ReturnType EPS_Assist_SetSteeringTorque(int16_t torque_Nm);
Std_ReturnType EPS_Assist_SetSteeringAngle(int16_t angle_deg);
// 输出接口
Std_ReturnType EPS_Assist_GetTargetCurrent(int16_t* current_mA);
Std_ReturnType EPS_Assist_GetControlMode(AssistMode_t* mode);
// 配置接口
Std_ReturnType EPS_Assist_SetParameter(uint8_t param_id, uint32_t value);
Std_ReturnType EPS_Assist_GetParameter(uint8_t param_id, uint32_t* value);
你看,每个接口的职责都很清楚。输入接口只负责接收数据,输出接口只负责提供计算结果。配置接口单独拎出来,不影响主控制流程。这样设计,上层调用时逻辑清晰,底层实现时也容易维护。
2.5 总结一下
EPS软件架构,说白了就是三件事:分层让代码好移植,模块划分让代码好维护,接口定义让代码好协作。这三件事做好了,后面写代码就是水到渠成的事。
嗯,这里要注意:架构设计不是一蹴而就的。我建议你在项目初期先搭一个“骨架”,然后随着需求明确,逐步细化。别想着一次设计完美——软件架构是长出来的,不是设计出来的。
下一章,咱们聊聊具体的接口实现细节,包括怎么处理异步调用、怎么设计回调机制。到时候我会拿一个实际项目的代码片段来拆解,保证干货满满。