第三章 代码架构设计:分层架构、模块化设计、状态机与任务调度
好,咱们进入第三章。说实话,这一章是我个人觉得整个课程里最「值钱」的部分之一。为什么?因为很多做电机控制的工程师,算法写得溜,但代码架构一塌糊涂。我见过太多项目,前期跑得欢,后期改需求时直接崩盘。代码架构,说白了就是给软件搭骨架。骨架歪了,后面再怎么填肉都别扭。
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%的调试和维护时间。这笔账,怎么算都划算。
好了,这一章的内容就到这里。下一章我们会深入代码优化的具体技巧,包括循环展开、查表法、定点数运算等实战内容。到时候见。