3. 代码架构设计:分层架构与模块化实战
做FOC嵌入式开发,代码架构这件事,我踩过的坑比你们想象的多。早期我做无刷电机驱动时,代码全写在一个文件里,看起来挺爽。结果客户要换一个MOS管驱动芯片,好家伙,改代码改到凌晨三点。从那以后,我彻底明白了——分层架构不是花架子,是救命稻草。
3.1 为什么要分层?
说白了,分层就是为了解耦。你想想看,FOC算法本身跟硬件是强相关的吗?其实不是。电流采样用ADC还是用运放,PWM输出用高级定时器还是普通定时器——这些是硬件细节。而SVPWM、PI调节器、坐标变换这些,是算法逻辑。
把这两类东西混在一起写,后果就是:换个MCU,代码重写;换个驱动芯片,代码重写;甚至换个电流采样电阻,代码还得改。
我个人习惯把FOC代码分成四层:
| 层级 | 职责 | 典型内容 |
|---|---|---|
| HAL层 | 硬件抽象,屏蔽MCU差异 | GPIO、定时器、ADC、DAC的封装 |
| 驱动层 | 外设驱动,面向功能 | 电流采样、PWM生成、编码器读取 |
| 算法层 | 核心FOC算法 | Clark/Park变换、SVPWM、PI调节 |
| 应用层 | 业务逻辑与状态管理 | 速度环、位置环、故障处理 |
嗯,这里要注意:层与层之间只能单向依赖。应用层调用算法层,算法层调用驱动层,驱动层调用HAL层。反过来不行。这是铁律。
3.2 HAL层设计——把硬件藏起来
HAL层的核心目标就一句话:让上层代码不知道自己在哪个MCU上跑。
举个例子,我定义一组接口:
// hal_timer.h
typedef struct {
void (*init)(uint32_t freq_hz);
void (*start)(void);
void (*stop)(void);
uint32_t (*get_counter)(void);
} hal_timer_t;
// 具体实现:STM32版本
void stm32_timer_init(uint32_t freq_hz) {
// 配置TIM1,设置PWM频率
// 配置死区时间
}
// 具体实现:GD32版本
void gd32_timer_init(uint32_t freq_hz) {
// 配置TIMER0,设置PWM频率
// 注意GD32的寄存器地址不同
}
我在项目中遇到过最头疼的事:同一个算法,在STM32上跑得好好的,移植到国产MCU上,PWM波形全乱了。原因就是定时器配置差异。有了HAL层,这种问题只需要改底层实现,上层代码一行不动。
3.3 驱动层设计——面向功能封装
驱动层在HAL层之上,把零散的硬件操作组合成有意义的"功能模块"。
比如电流采样驱动:
// driver_current.h
typedef struct {
float phase_a; // A相电流,单位A
float phase_b; // B相电流,单位A
float phase_c; // C相电流,单位A
} current_sample_t;
// 驱动层接口
void current_driver_init(void);
current_sample_t current_driver_sample(void);
void current_driver_calibrate(void);
你看,驱动层返回的是物理量(安培),而不是原始ADC码值。这样算法层就不用关心ADC是12位还是16位,也不用关心参考电压是多少。
我曾经犯过一个错误:在驱动层里直接做了滤波处理。结果换了一个电机,噪声特性变了,滤波参数要改,但驱动层代码已经被多个模块依赖了。改起来牵一发动全身。后来我学乖了——驱动层只做"采集",不做"处理"。滤波、补偿这些事,交给算法层。
3.4 算法层设计——纯数学,无硬件
算法层是FOC的核心。这一层应该做到:给一组输入,算一组输出,完全不依赖任何硬件。
// algo_foc.h
typedef struct {
float id_ref; // d轴电流参考值
float iq_ref; // q轴电流参考值
float id_fb; // d轴电流反馈值
float iq_fb; // q轴电流反馈值
float theta; // 电角度
} foc_input_t;
typedef struct {
float u_alpha; // alpha轴电压
float u_beta; // beta轴电压
float vd; // d轴电压输出
float vq; // q轴电压输出
} foc_output_t;
foc_output_t foc_calculate(foc_input_t *input);
为什么算法层要纯数学?因为这样你可以在PC上仿真、调试、验证。我经常在VS Code里写个测试程序,输入一组数据,看输出对不对。等算法调好了,再移植到嵌入式平台。效率高得多。
3.5 应用层设计——状态机驱动
应用层是业务逻辑的集合。FOC电机控制通常有多个状态:启动、运行、刹车、故障保护等。用状态机来管理,清晰又可靠。
我常用的状态机设计:
typedef enum {
MOTOR_IDLE, // 空闲
MOTOR_CALIB, // 校准
MOTOR_ALIGN, // 对齐
MOTOR_START, // 启动
MOTOR_RUN, // 运行
MOTOR_STOP, // 停止
MOTOR_FAULT // 故障
} motor_state_t;
typedef struct {
motor_state_t current_state;
motor_state_t next_state;
uint32_t state_tick; // 当前状态持续时间
uint32_t timeout_ms; // 超时时间
uint8_t fault_code; // 故障码
} motor_state_machine_t;
状态切换的逻辑:
void motor_state_machine_run(motor_state_machine_t *sm) {
switch(sm->current_state) {
case MOTOR_IDLE:
if(/* 收到启动指令 */) {
sm->next_state = MOTOR_CALIB;
}
break;
case MOTOR_CALIB:
if(/* 校准完成 */) {
sm->next_state = MOTOR_ALIGN;
}
if(/* 校准超时 */) {
sm->next_state = MOTOR_FAULT;
sm->fault_code = FAULT_CALIB_TIMEOUT;
}
break;
case MOTOR_RUN:
if(/* 检测到过流 */) {
sm->next_state = MOTOR_FAULT;
sm->fault_code = FAULT_OVER_CURRENT;
}
break;
// ... 其他状态处理
}
}
3.6 接口定义规范——约定大于配置
层与层之间的接口,一定要有明确的规范。我总结了三条铁律:
- 参数类型统一:所有物理量都用float,所有状态都用枚举,所有配置都用结构体。
- 命名前缀清晰:HAL层用
hal_,驱动层用drv_,算法层用algo_,应用层用app_。 - 返回值规范:函数返回
int32_t,0表示成功,负数表示错误码。
// 接口示例
int32_t drv_current_init(void); // 返回0成功,-1失败
int32_t drv_current_sample(current_data_t *data); // 采样结果通过指针返回
int32_t algo_foc_run(foc_input_t *in, foc_output_t *out); // 输入输出都通过指针
嗯,这里有个细节:尽量用指针传递结构体,不要传值。嵌入式系统栈空间有限,传结构体容易栈溢出。我见过一个同事传了个200字节的结构体,结果递归调用几次,栈直接爆了。
3.7 模块化设计原则——高内聚低耦合
模块化不是把代码拆成多个文件就完事了。核心是:一个模块只做一件事,并且做好。
我常用的模块划分:
- current_sense:电流采样模块,负责ADC配置、采样触发、数据转换
- pwm_output:PWM生成模块,负责定时器配置、占空比更新、死区设置
- encoder:位置检测模块,负责编码器读取、速度计算、角度校准
- foc_core:FOC核心算法模块,坐标变换、SVPWM、PI调节
- motor_ctrl:电机控制模块,状态机、速度环、位置环
- fault_handle:故障处理模块,过流、过压、堵转检测
每个模块对外暴露的接口,尽量控制在5个以内。接口多了,耦合就重了。我见过一个模块暴露了20多个接口,结果改一个接口,十几个地方都要改。那叫一个酸爽。
3.8 整体架构图
下面这张图,是我做FOC项目时常用的分层架构。你可以看到数据流向和依赖关系:
这张图里,数据流向是从上到下。应用层调用算法层,算法层调用驱动层,驱动层调用HAL层。反过来,底层不能直接调用上层。这就是分层架构的精髓。
3.9 写在最后
代码架构这件事,说白了就是"先想清楚再动手"。我见过太多人上来就写代码,写到一半发现耦合太重,改也不是,不改也不是。与其这样,不如花半天时间把架构画清楚。
记住:好的架构,让你换MCU像换衣服一样简单。坏的架构,让你改一行代码像做手术一样痛苦。
嗯,这一章的内容就到这里。下一章我们会深入HAL层的具体实现,包括定时器、ADC、GPIO的封装技巧。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。
- 四层架构:HAL层 → 驱动层 → 算法层 → 应用层
- 层间单向依赖,禁止反向调用
- 接口规范:前缀清晰、参数统一、返回值规范
- 状态机设计:非阻塞、超时判断、故障处理
- 模块化:高内聚低耦合,每个模块只做一件事