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层,这种问题只需要改底层实现,上层代码一行不动。

核心原则:HAL层只做"硬件操作",不做"业务判断"。比如"读取ADC值"是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;
            
        // ... 其他状态处理
    }
}
避坑指南:我曾经在状态机里直接调用了HAL层的延时函数,结果导致整个系统卡死。记住:状态机里不要用阻塞延时。用状态计时器+超时判断的方式,非阻塞地处理时间相关逻辑。

3.6 接口定义规范——约定大于配置

层与层之间的接口,一定要有明确的规范。我总结了三条铁律:

  1. 参数类型统一:所有物理量都用float,所有状态都用枚举,所有配置都用结构体。
  2. 命名前缀清晰:HAL层用hal_,驱动层用drv_,算法层用algo_,应用层用app_
  3. 返回值规范:函数返回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项目时常用的分层架构。你可以看到数据流向和依赖关系:

FOC嵌入式代码分层架构 应用层 (Application Layer) 状态机管理 | 速度环/位置环 | 故障处理 | 通信协议 app_motor_ctrl.c, app_comm.c, app_fault.c 算法层 (Algorithm Layer) Clark/Park变换 | SVPWM | PI调节器 | 角度/速度估算 algo_foc.c, algo_pi.c, algo_svpwm.c 驱动层 (Driver Layer) 电流采样驱动 | PWM生成驱动 | 编码器驱动 | 温度检测 drv_current.c, drv_pwm.c, drv_encoder.c HAL层 (Hardware Abstraction Layer) GPIO | 定时器 | ADC | DAC | SPI/I2C/UART 依赖方向:上层调用下层

这张图里,数据流向是从上到下。应用层调用算法层,算法层调用驱动层,驱动层调用HAL层。反过来,底层不能直接调用上层。这就是分层架构的精髓。

3.9 写在最后

代码架构这件事,说白了就是"先想清楚再动手"。我见过太多人上来就写代码,写到一半发现耦合太重,改也不是,不改也不是。与其这样,不如花半天时间把架构画清楚。

记住:好的架构,让你换MCU像换衣服一样简单。坏的架构,让你改一行代码像做手术一样痛苦。

嗯,这一章的内容就到这里。下一章我们会深入HAL层的具体实现,包括定时器、ADC、GPIO的封装技巧。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。

本章核心要点:
  • 四层架构:HAL层 → 驱动层 → 算法层 → 应用层
  • 层间单向依赖,禁止反向调用
  • 接口规范:前缀清晰、参数统一、返回值规范
  • 状态机设计:非阻塞、超时判断、故障处理
  • 模块化:高内聚低耦合,每个模块只做一件事

专注资料整理