第三章:软件架构设计——分层架构、模块化设计、状态机设计
各位做电机控制的同行,咱们今天聊聊软件架构。说实话,我见过太多工程师一上来就撸代码,结果项目后期改得想哭。我自己也踩过这个坑——有一次做无刷直流电机驱动,代码写到一半发现逻辑全搅在一起,一个bug修了三天,最后发现是中断和主循环的变量冲突。从那以后,我养成了一个习惯:先画架构图,再写代码。
电机控制的软件架构,说白了就是三件事:分层、分模块、管状态。咱们一个一个说。
3.1 分层架构:把复杂问题拆成三层
我习惯把电机控制软件分成三层,就像盖房子一样:
- 硬件抽象层(HAL):直接跟寄存器、外设打交道。比如PWM配置、ADC采样、GPIO控制。
- 中间层(Middleware):做算法和逻辑处理。比如FOC变换、PID计算、速度估算。
- 应用层(Application):负责业务逻辑和用户交互。比如启动/停止、速度设定、故障处理。
为什么要分层?你想想看,如果哪天换了MCU,你只需要重写HAL层,中间层和应用层基本不用动。我在一个项目中把STM32换成GD32,只改了底层驱动,上层代码直接复用,省了至少两周时间。
核心原则:上层不能直接调用下层硬件。中间层只能通过HAL提供的接口访问外设。应用层只能调用中间层的API。
下面是我常用的分层结构图,用SVG画了一个:
我的习惯:在HAL层定义一组标准接口,比如 hal_pwm_set_duty()、hal_adc_get_value()。中间层只调用这些接口,绝不直接操作寄存器。这样换芯片时,你只需要改HAL层的实现。
3.2 模块化设计:每个模块只做一件事
模块化设计,说白了就是「高内聚、低耦合」。每个模块只负责一个功能,模块之间通过接口通信。
我一般把电机控制拆成这几个模块:
| 模块名称 | 职责 | 对外接口 |
|---|---|---|
| 电流采样模块 | 采集相电流,做滤波和偏置校正 | get_phase_current() |
| 位置估算模块 | 根据霍尔/编码器/无传感器算法计算角度和速度 | get_rotor_angle()、get_speed() |
| FOC变换模块 | Clark/Park变换,逆Park/逆Clark变换 | clark_transform()、park_transform() |
| PID调节模块 | 电流环、速度环、位置环的PID计算 | pid_update()、pid_reset() |
| PWM生成模块 | 根据电压矢量计算占空比,配置PWM寄存器 | pwm_set_voltage_vector() |
| 故障保护模块 | 过流、过压、过温检测,执行保护动作 | fault_check()、fault_clear() |
每个模块内部是独立的.c和.h文件。模块之间通过结构体传递数据,不共享全局变量。举个例子:
// 电流采样模块接口
typedef struct {
float ia;
float ib;
float ic;
float ia_offset;
float ib_offset;
float ic_offset;
} current_sample_t;
current_sample_t current_sample_get(void);
// FOC变换模块接口
typedef struct {
float alpha;
float beta;
} clark_output_t;
clark_output_t clark_transform(current_sample_t *curr);
注意:千万不要在模块之间直接传递全局变量。我曾经在一个项目中,两个模块共用一个全局数组,结果一个模块改了数组长度,另一个模块没同步,导致内存越界。从那以后,我强制要求所有模块数据通过函数参数传递。
3.3 状态机设计:让电机行为可预测
电机控制本质上是一个状态机。启动、运行、停止、故障——每个状态下的行为都不一样。我见过有人用一堆if-else来管理状态,代码写出来跟意大利面一样。嗯,这里我强烈推荐用状态机。
一个典型的电机状态机包含以下状态:
- IDLE(空闲):电机未使能,等待启动指令
- STARTING(启动中):执行预定位、开环强拉、切换到闭环
- RUNNING(运行中):正常闭环控制,响应速度/转矩指令
- STOPPING(停止中):执行减速、制动
- FAULT(故障):检测到异常,执行保护动作,等待复位
我习惯用枚举定义状态,用函数指针实现状态切换:
typedef enum {
MOTOR_STATE_IDLE,
MOTOR_STATE_STARTING,
MOTOR_STATE_RUNNING,
MOTOR_STATE_STOPPING,
MOTOR_STATE_FAULT
} motor_state_t;
typedef struct {
motor_state_t current_state;
void (*entry_func)(void);
void (*run_func)(void);
void (*exit_func)(void);
} state_handler_t;
// 状态表
state_handler_t state_table[] = {
{MOTOR_STATE_IDLE, idle_entry, idle_run, idle_exit},
{MOTOR_STATE_STARTING, starting_entry, starting_run, starting_exit},
{MOTOR_STATE_RUNNING, running_entry, running_run, running_exit},
{MOTOR_STATE_STOPPING, stopping_entry, stopping_run, stopping_exit},
{MOTOR_STATE_FAULT, fault_entry, fault_run, fault_exit},
};
状态切换的逻辑放在一个统一的函数里:
void motor_state_machine_run(void) {
motor_state_t next_state;
// 执行当前状态的运行函数
state_table[current_state].run_func();
// 判断是否需要切换状态
next_state = check_state_transition(current_state);
if (next_state != current_state) {
// 退出当前状态
state_table[current_state].exit_func();
// 进入新状态
current_state = next_state;
state_table[current_state].entry_func();
}
}
避坑指南:我曾经在状态切换时忘记调用exit_func,导致退出RUNNING状态时没有关闭PWM输出,电机直接飞车。所以记住:进入状态做初始化,退出状态做清理,这个习惯能救你一命。
状态机的优势很明显:代码逻辑清晰,每个状态的行为独立,新增状态不会影响现有逻辑。而且调试时,你只需要看当前状态是什么,就知道电机应该做什么。
3.4 三者如何配合
分层架构、模块化设计、状态机设计,这三者不是孤立的。我一般这样组织:
- 应用层:运行状态机,管理电机状态切换
- 中间层:包含各个功能模块(FOC、PID、采样等),每个模块内部也是状态机
- HAL层:提供硬件抽象接口,让上层不关心具体芯片
举个例子,当状态机从IDLE切换到STARTING时,应用层调用中间层的「启动模块」,启动模块再调用HAL层的PWM接口使能输出。整个过程就像流水线一样,层层递进,互不干扰。
我的建议:刚开始做架构时,不要追求完美。先画一个简单的分层图,定义好接口,然后逐步细化。架构是迭代出来的,不是一次设计出来的。我第一个电机控制项目,架构改了三次才稳定下来。
好了,这一章的内容就到这里。记住:好的架构不是为了好看,是为了让你在项目后期少掉头发。下一章咱们聊聊代码优化,到时候见。