第三章:软件架构设计——分层架构、模块化设计、状态机设计

各位做电机控制的同行,咱们今天聊聊软件架构。说实话,我见过太多工程师一上来就撸代码,结果项目后期改得想哭。我自己也踩过这个坑——有一次做无刷直流电机驱动,代码写到一半发现逻辑全搅在一起,一个bug修了三天,最后发现是中断和主循环的变量冲突。从那以后,我养成了一个习惯:先画架构图,再写代码。

电机控制的软件架构,说白了就是三件事:分层、分模块、管状态。咱们一个一个说。

3.1 分层架构:把复杂问题拆成三层

我习惯把电机控制软件分成三层,就像盖房子一样:

  • 硬件抽象层(HAL):直接跟寄存器、外设打交道。比如PWM配置、ADC采样、GPIO控制。
  • 中间层(Middleware):做算法和逻辑处理。比如FOC变换、PID计算、速度估算。
  • 应用层(Application):负责业务逻辑和用户交互。比如启动/停止、速度设定、故障处理。

为什么要分层?你想想看,如果哪天换了MCU,你只需要重写HAL层,中间层和应用层基本不用动。我在一个项目中把STM32换成GD32,只改了底层驱动,上层代码直接复用,省了至少两周时间。

核心原则:上层不能直接调用下层硬件。中间层只能通过HAL提供的接口访问外设。应用层只能调用中间层的API。

下面是我常用的分层结构图,用SVG画了一个:

应用层(Application) 启动/停止 | 速度设定 | 故障处理 | 通信协议 中间层(Middleware) FOC变换 | PID调节 | 速度估算 | 电流环 硬件抽象层(HAL) PWM配置 | ADC采样 | GPIO控制 | 定时器 硬件层(MCU + 驱动电路)

我的习惯:在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接口使能输出。整个过程就像流水线一样,层层递进,互不干扰。

我的建议:刚开始做架构时,不要追求完美。先画一个简单的分层图,定义好接口,然后逐步细化。架构是迭代出来的,不是一次设计出来的。我第一个电机控制项目,架构改了三次才稳定下来。

好了,这一章的内容就到这里。记住:好的架构不是为了好看,是为了让你在项目后期少掉头发。下一章咱们聊聊代码优化,到时候见。


专注资料整理