第二章 软件架构设计原则

做电机控制这些年,我最大的体会就是:架构设计决定了你能走多远。刚入行那会儿,我也喜欢把所有代码堆在一个文件里,觉得这样效率高。结果呢?项目一迭代,改个参数都要翻半天代码,调试起来更是欲哭无泪。后来我慢慢悟出一个道理——好的架构不是束缚,而是解放。

这一章,我们就来聊聊永磁同步电机控制软件的核心架构原则。说白了,就是怎么把代码组织得既清晰又灵活,让你在项目迭代时少掉头发。

2.1 分层架构:应用层/算法层/驱动层

分层架构,我习惯把它比作一个三层的蛋糕。每一层各司其职,互不干扰。你想想看,如果蛋糕的每一层都混在一起,那吃起来是什么味道?代码也是一样。

2.1.1 为什么需要分层?

我在项目中遇到过最典型的问题:一个同事把PWM占空比的计算逻辑直接写在了定时器中断里。乍一看没问题,但后来要换一个MCU平台,整个控制逻辑都得重写。这就是典型的「耦合过紧」。

分层架构的核心思想就一句话:上层依赖下层,下层不依赖上层。具体来说:

  • 应用层:负责业务逻辑,比如速度环、位置环的控制策略
  • 算法层:封装核心算法,比如FOC变换、SVPWM、观测器
  • 驱动层:直接操作硬件,比如PWM输出、ADC采样、编码器读取

重要原则:驱动层永远不知道应用层在做什么。它只负责「把电压值写到寄存器」和「把电流值读回来」。至于这些值怎么用,那是上层的事。

2.1.2 各层的职责划分

我一般这样划分各层的职责,你可以参考一下:

层级 典型模块 特点
应用层 速度环、位置环、电流环、故障处理 与具体硬件无关,可移植性强
算法层 Clark/Park变换、SVPWM、滑模观测器 纯数学计算,不涉及硬件操作
驱动层 PWM驱动、ADC驱动、编码器驱动 直接操作寄存器,平台相关

举个例子,你写一个SVPWM算法,它只接收两个电压值(Ualpha, Ubeta),然后输出三个占空比。它不需要知道这些电压值是从哪里来的,也不需要知道占空比最终怎么写到寄存器里。这就是算法层的纯粹性。

我的习惯:在算法层里,所有输入输出都用结构体传递。比如 svpwm_input_tsvpwm_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在切换状态时没有及时关闭,电机瞬间抖动了一下。从那以后,我强制要求每个状态都必须实现 enterrunexit 三个函数。

2.5 小结

这一章我们聊了四个核心原则:分层架构、模块化设计、接口规范、状态机设计。说白了,这些原则都是为了一个目标——让代码好写、好改、好维护

我个人觉得,架构设计没有标准答案,但有一些通用的「好味道」。比如:代码读起来像讲故事,每个函数只做一件事,修改一个功能不需要改十个文件。如果你能做到这些,那你的架构就合格了。

下一章,我们会深入到具体的代码实现,看看这些原则怎么落地到实际的FOC控制代码中。到时候我会拿一个完整的工程来拆解,保证你看完就能用。

专注资料整理