第三章 代码架构设计:分层架构、模块化设计、状态机与任务调度

好,咱们进入第三章。说实话,这一章是我个人觉得整个课程里最「值钱」的部分之一。为什么?因为很多做电机控制的工程师,算法写得溜,但代码架构一塌糊涂。我见过太多项目,前期跑得欢,后期改需求时直接崩盘。代码架构,说白了就是给软件搭骨架。骨架歪了,后面再怎么填肉都别扭。

3.1 分层架构:把代码像洋葱一样包起来

先聊聊分层架构。我个人习惯把电机控制代码分成三层:应用层中间层硬件抽象层。你想想看,这就像做菜——应用层是决定今天吃红烧肉还是清蒸鱼,中间层是切菜配菜,硬件抽象层是控制灶台火候。各管各的,互不干扰。

核心原则:上层只能调用下层,下层绝不能反向依赖上层。这是铁律。

我在项目中遇到过最典型的反面教材:有人把PWM寄存器操作直接写在速度环PI控制器里。结果换了个MCU平台,整个速度环代码全得重写。这就是没分层的代价。

来看一个实际的分层结构示例:

// 硬件抽象层 (HAL)
void hal_pwm_set_duty(channel_t ch, float duty);
float hal_adc_get_voltage(channel_t ch);
void hal_gpio_set_pin(pin_t pin, bool level);

// 中间层 (Middleware)
typedef struct {
    float kp;
    float ki;
    float integral;
    float output_limit;
} pi_controller_t;

float pi_update(pi_controller_t *pi, float error);

// 应用层 (Application)
void motor_speed_control_task(void) {
    float speed = hal_encoder_get_speed();
    float error = target_speed - speed;
    float torque = pi_update(&speed_pi, error);
    float duty = torque_to_duty(torque);
    hal_pwm_set_duty(MOTOR_U, duty);
}

嗯,这里要注意:分层不是越多越好。三层对于电机控制来说刚刚好。层数太多,调用链路过长,实时性反而会受影响。

3.2 模块化设计:高内聚、低耦合

模块化设计,说白了就是「把大象装进冰箱分几步」——每个模块只干一件事,但要把这件事干到极致。

我建议每个模块遵循以下原则:

  • 单一职责:一个.c文件只负责一个功能实体。比如pid.c只做PID计算,encoder.c只处理编码器数据。
  • 接口清晰:头文件里只暴露必要的函数和结构体,内部实现细节全部隐藏。
  • 无全局变量依赖:模块之间通过函数参数传递数据,别搞什么extern全局变量满天飞。

我的小技巧:每个模块都提供一个初始化函数和一个去初始化函数。这样在系统启动和关闭时,可以按顺序优雅地管理所有模块。我曾经在一个项目中因为没写去初始化函数,导致热重启时硬件状态混乱,查了三天bug。

来看一个模块化设计的头文件示例:

// pid.h
#ifndef __PID_H__
#define __PID_H__

typedef struct {
    float kp;
    float ki;
    float kd;
    float integral;
    float prev_error;
    float output_min;
    float output_max;
} pid_ctx_t;

void pid_init(pid_ctx_t *ctx, float kp, float ki, float kd);
float pid_calc(pid_ctx_t *ctx, float target, float feedback);
void pid_reset(pid_ctx_t *ctx);

#endif

你发现没有?这个头文件里没有任何硬件相关的定义。这意味着你可以把这个pid.h和pid.c原封不动地移植到任何项目里。这就是模块化的价值。

3.3 状态机:让电机控制变得可预测

电机控制天然就是一个状态机。启动、运行、停止、故障保护——每个状态下的行为完全不同。用状态机来管理,代码逻辑会清晰很多。

我个人最喜欢用有限状态机(FSM)来实现。为什么?因为FSM的每个状态都是确定的,输入事件也是确定的,输出行为更是确定的。调试的时候,你只需要看当前状态和触发事件,就能定位问题。

我曾经在一个无刷直流电机项目中,用状态机管理六个换相状态。一开始觉得用switch-case就够了,后来发现状态多了之后,代码越来越臃肿。最后改用查表法,清爽多了。

// 状态枚举
typedef enum {
    MOTOR_IDLE,
    MOTOR_STARTING,
    MOTOR_RUNNING,
    MOTOR_STOPPING,
    MOTOR_FAULT
} motor_state_t;

// 状态转移表
typedef struct {
    motor_state_t current_state;
    event_t event;
    motor_state_t next_state;
    void (*action)(void);
} state_transition_t;

// 状态转移表定义
state_transition_t motor_fsm[] = {
    {MOTOR_IDLE,     EVT_START,  MOTOR_STARTING, motor_start_procedure},
    {MOTOR_STARTING, EVT_READY,  MOTOR_RUNNING,  motor_enable_control},
    {MOTOR_RUNNING,  EVT_STOP,   MOTOR_STOPPING, motor_stop_procedure},
    {MOTOR_STOPPING, EVT_DONE,   MOTOR_IDLE,     motor_disable_control},
    {MOTOR_ANY,      EVT_FAULT,  MOTOR_FAULT,    motor_fault_handler},
    {MOTOR_FAULT,    EVT_RESET,  MOTOR_IDLE,     motor_reset}
};

避坑指南:我曾经在状态机里忘记处理「非法状态转移」的情况。结果系统在异常状态下收到一个不该来的事件,直接跑飞了。记住:每个状态都要有默认的「事件未处理」分支,哪怕只是记录一下日志。

3.4 任务调度:让一切按时发生

电机控制对实时性要求极高。电流环通常需要10kHz-20kHz的更新频率,速度环1kHz左右,位置环100Hz。怎么让这些任务和谐共处?答案就是任务调度。

我建议采用时间触发协作式调度。为什么不用抢占式RTOS?因为对于大多数电机控制应用来说,协作式调度已经足够,而且避免了优先级反转、死锁等复杂问题。

来看一个简单的调度器实现:

#define TASK_MAX     8

typedef struct {
    void (*func)(void);
    uint32_t period_ticks;
    uint32_t remaining_ticks;
    bool enabled;
} task_t;

static task_t task_list[TASK_MAX];
static uint32_t task_count = 0;

void scheduler_init(void) {
    task_count = 0;
    memset(task_list, 0, sizeof(task_list));
}

void scheduler_add_task(void (*func)(void), uint32_t period_ms) {
    if (task_count >= TASK_MAX) return;
    task_list[task_count].func = func;
    task_list[task_count].period_ticks = period_ms;
    task_list[task_count].remaining_ticks = 0;
    task_list[task_count].enabled = true;
    task_count++;
}

void scheduler_tick(void) {
    // 在定时器中断中调用,每1ms一次
    for (uint32_t i = 0; i < task_count; i++) {
        if (task_list[i].enabled) {
            task_list[i].remaining_ticks++;
        }
    }
}

void scheduler_run(void) {
    for (uint32_t i = 0; i < task_count; i++) {
        if (task_list[i].enabled && 
            task_list[i].remaining_ticks >= task_list[i].period_ticks) {
            task_list[i].func();
            task_list[i].remaining_ticks = 0;
        }
    }
}

嗯,这里要注意:所有任务函数的执行时间总和,必须小于最短任务周期。否则调度器就会「忙不过来」。我在项目中一般会预留30%的CPU余量,给突发情况留点空间。

3.5 实战经验总结

最后,我把这些年做电机控制代码架构的经验,整理成一张表格,方便你对照参考:

架构要素 推荐做法 常见坑点
分层架构 应用层/中间层/HAL三层分离 上层直接操作硬件寄存器
模块化设计 每个模块一个.c/.h,接口最小化 全局变量满天飞,模块间耦合严重
状态机 查表法实现FSM,状态转移清晰 遗漏非法状态处理,导致系统跑飞
任务调度 时间触发协作式调度,简单可靠 任务执行时间超过调度周期

核心观点:好的代码架构,不是写出来的,是「设计」出来的。花30%的时间做架构设计,能省下后面70%的调试和维护时间。这笔账,怎么算都划算。

好了,这一章的内容就到这里。下一章我们会深入代码优化的具体技巧,包括循环展开、查表法、定点数运算等实战内容。到时候见。