第二章 软件架构设计原则
做电机控制这些年,我最大的体会就是:架构设计决定了你能走多远。刚入行那会儿,我也喜欢把所有代码堆在一个文件里,觉得这样效率高。结果呢?项目一迭代,改个参数都要翻半天代码,调试起来更是欲哭无泪。后来我慢慢悟出一个道理——好的架构不是束缚,而是解放。
这一章,我们就来聊聊永磁同步电机控制软件的核心架构原则。说白了,就是怎么把代码组织得既清晰又灵活,让你在项目迭代时少掉头发。
2.1 分层架构:应用层/算法层/驱动层
分层架构,我习惯把它比作一个三层的蛋糕。每一层各司其职,互不干扰。你想想看,如果蛋糕的每一层都混在一起,那吃起来是什么味道?代码也是一样。
2.1.1 为什么需要分层?
我在项目中遇到过最典型的问题:一个同事把PWM占空比的计算逻辑直接写在了定时器中断里。乍一看没问题,但后来要换一个MCU平台,整个控制逻辑都得重写。这就是典型的「耦合过紧」。
分层架构的核心思想就一句话:上层依赖下层,下层不依赖上层。具体来说:
- 应用层:负责业务逻辑,比如速度环、位置环的控制策略
- 算法层:封装核心算法,比如FOC变换、SVPWM、观测器
- 驱动层:直接操作硬件,比如PWM输出、ADC采样、编码器读取
重要原则:驱动层永远不知道应用层在做什么。它只负责「把电压值写到寄存器」和「把电流值读回来」。至于这些值怎么用,那是上层的事。
2.1.2 各层的职责划分
我一般这样划分各层的职责,你可以参考一下:
| 层级 | 典型模块 | 特点 |
|---|---|---|
| 应用层 | 速度环、位置环、电流环、故障处理 | 与具体硬件无关,可移植性强 |
| 算法层 | Clark/Park变换、SVPWM、滑模观测器 | 纯数学计算,不涉及硬件操作 |
| 驱动层 | PWM驱动、ADC驱动、编码器驱动 | 直接操作寄存器,平台相关 |
举个例子,你写一个SVPWM算法,它只接收两个电压值(Ualpha, Ubeta),然后输出三个占空比。它不需要知道这些电压值是从哪里来的,也不需要知道占空比最终怎么写到寄存器里。这就是算法层的纯粹性。
我的习惯:在算法层里,所有输入输出都用结构体传递。比如 svpwm_input_t 和 svpwm_output_t。这样接口清晰,测试也方便。
2.2 模块化设计
模块化设计,说白了就是「高内聚、低耦合」。每个模块只做一件事,并且把这件事做好。我见过很多代码,一个函数写了上千行,什么功能都往里塞。调试的时候,改一个地方可能影响十个地方,那叫一个酸爽。
2.2.1 模块划分的原则
我个人习惯按照功能来划分模块。比如:
- 电流采样模块:负责ADC触发、数据读取、滤波
- 角度观测模块:负责编码器/霍尔信号处理、角度估算
- FOC控制模块:负责电流环、坐标变换、SVPWM
- 速度控制模块:负责速度环PI调节、速度滤波
- 故障诊断模块:负责过流、过压、堵转等检测
每个模块都提供一组接口函数,其他模块只能通过这些接口来交互。嗯,这里要注意:模块之间不要互相调用内部变量。我曾经见过一个项目,模块A直接修改了模块B的全局变量,结果排查问题花了整整两天。
2.2.2 模块的初始化与去初始化
每个模块都应该有统一的初始化函数和去初始化函数。我一般这样命名:
// 模块初始化
void motor_current_sampling_init(void);
void motor_angle_observer_init(void);
void motor_foc_control_init(void);
// 模块去初始化(用于安全关闭)
void motor_current_sampling_deinit(void);
void motor_angle_observer_deinit(void);
void motor_foc_control_deinit(void);
避坑指南:我曾经在项目里忘记调用去初始化函数,导致电机停机时PWM还处于使能状态,结果MOS管烧了。从那以后,我每个模块都强制要求实现 _deinit() 函数,并在系统关闭时按顺序调用。
2.3 接口规范
接口规范,是团队协作的基石。你想想看,如果每个人写的函数命名风格都不一样,参数顺序也随心所欲,那代码还怎么维护?
2.3.1 命名规范
我建议采用这样的命名规则:
- 模块前缀:每个函数都以模块名开头,比如
foc_、svpwm_、encoder_ - 动词+名词:比如
foc_set_target_current()、encoder_get_angle() - 返回值统一
举个例子:
// 好的命名
int32_t foc_set_target_current(float id_ref, float iq_ref);
int32_t encoder_get_angle(float *angle_rad);
// 不好的命名
void setCur(float a, float b); // 谁知道a和b是什么?
int getAng(); // 返回值是什么单位?
2.3.2 参数传递规范
我个人习惯:少于3个参数用函数参数,多于3个用结构体。比如:
// 参数少,直接传
int32_t svpwm_calc(float u_alpha, float u_beta, svpwm_output_t *out);
// 参数多,用结构体
typedef struct {
float id_ref;
float iq_ref;
float id_fb;
float iq_fb;
float speed;
} current_loop_input_t;
int32_t current_loop_run(current_loop_input_t *input, current_loop_output_t *output);
小技巧:所有结构体都用 _t 后缀,一眼就能看出是类型定义。这个习惯我从做Linux驱动时养成的,一直用到现在。
2.4 状态机设计
电机控制,本质上就是一个状态机。从初始化到运行,再到故障处理,每个阶段都有明确的状态。我见过有人用一堆if-else来处理状态切换,结果代码又长又乱,还容易出bug。
2.4.1 状态定义
我一般这样定义电机控制的状态:
typedef enum {
MOTOR_STATE_IDLE, // 空闲状态
MOTOR_STATE_INIT, // 初始化
MOTOR_STATE_ALIGN, // 对齐(预定位)
MOTOR_STATE_OPEN_LOOP, // 开环运行
MOTOR_STATE_CLOSED_LOOP, // 闭环运行
MOTOR_STATE_FAULT, // 故障状态
MOTOR_STATE_STOP // 停止状态
} motor_state_t;
每个状态都有明确的进入条件和退出条件。比如从IDLE到INIT,需要满足「系统上电完成」和「参数加载成功」两个条件。
2.4.2 状态切换的实现
我习惯用一个二维表格来实现状态切换。这样代码清晰,也方便扩展:
// 状态切换表
// 行:当前状态,列:触发事件
static const motor_state_t state_transition[STATE_NUM][EVENT_NUM] = {
// 当前状态\事件: INIT_EVT RUN_EVT FAULT_EVT STOP_EVT
[MOTOR_STATE_IDLE] = {INIT, IDLE, FAULT, IDLE},
[MOTOR_STATE_INIT] = {INIT, OPEN_LOOP, FAULT, STOP},
[MOTOR_STATE_OPEN_LOOP] = {OPEN_LOOP,CLOSED_LOOP,FAULT, STOP},
[MOTOR_STATE_CLOSED_LOOP] = {CLOSED_LOOP,CLOSED_LOOP,FAULT, STOP},
[MOTOR_STATE_FAULT] = {FAULT, FAULT, FAULT, IDLE},
[MOTOR_STATE_STOP] = {STOP, STOP, FAULT, IDLE},
};
核心思想:状态机的好处是,你永远知道当前系统在做什么,以及下一步能做什么。不会出现「莫名其妙跑到一个不该去的状态」的情况。
2.4.3 状态机的主循环
每个状态对应一个处理函数,主循环里根据当前状态调用对应的函数:
void motor_control_task(void) {
motor_event_t event = get_current_event();
// 检查状态切换
motor_state_t next_state = state_transition[current_state][event];
if (next_state != current_state) {
state_exit(current_state); // 退出当前状态
state_enter(next_state); // 进入新状态
current_state = next_state;
}
// 执行当前状态的处理
state_run(current_state);
}
避坑指南:我曾经在状态切换时忘记调用 state_exit(),导致PWM在切换状态时没有及时关闭,电机瞬间抖动了一下。从那以后,我强制要求每个状态都必须实现 enter、run、exit 三个函数。
2.5 小结
这一章我们聊了四个核心原则:分层架构、模块化设计、接口规范、状态机设计。说白了,这些原则都是为了一个目标——让代码好写、好改、好维护。
我个人觉得,架构设计没有标准答案,但有一些通用的「好味道」。比如:代码读起来像讲故事,每个函数只做一件事,修改一个功能不需要改十个文件。如果你能做到这些,那你的架构就合格了。
下一章,我们会深入到具体的代码实现,看看这些原则怎么落地到实际的FOC控制代码中。到时候我会拿一个完整的工程来拆解,保证你看完就能用。