从大二暑假开始动手,到把完整的矢量控制油动/电动飞控算法系统跑通,前后花了差不多一年。这中间踩过的坑、推倒重来的模块、深夜示波器前抓电流波形的经历,远比课本上的公式来得具体。最近把这套系统整理成了系列笔记,正好借助这篇文章,把从 FOC 原理推导、软硬件架构、代码实现到 Mission Planner 联调的完整过程做一个系统化复盘。
这篇文章会尽量做到“照着能复现”:先讲清楚矢量控制与飞控结合的背景,再给出环境与硬件平台清单,然后拆解 Clark/Park 变换、SVPWM、PID 整定、MAVLink 通信等核心环节,最后附上高频问题排查表和工程化建议。无论你是准备飞控毕设、想入门电机控制,还是在做无人机底层算法开发,都应该能从中找到可复用的内容。
1. 背景与核心概念
1.1 为什么要做矢量控制飞控系统
传统无人机电调普遍采用方波驱动(六步换相法),控制简单、成本低,但有一个明显缺点:电机换相时转矩脉动大、噪声明显,动态响应也不够线性。随着开源飞控性能和传感器精度的提升,方波驱动的短板逐渐成为限制飞行性能的瓶颈。
矢量控制(Field-Oriented Control,FOC)通过坐标变换把三相交流电机的定子电流分解为励磁分量和转矩分量,实现解耦控制。这样一来,电机从外部看类似一台“直流电机”,转矩响应快、效率高、噪音小,尤其适合大疆这类高动态无人机电调。飞控系统采用矢量控制,可以显著改善悬停稳定性、抗风能力和油门响应。
我在这个项目里做的是“电机端 FOC + 飞控端姿态控制”的两级控制架构。飞控负责根据遥控器输入和 IMU 数据解算出期望转矩,底层矢量控制器则负责让电机电流快速、准确地跟踪转矩指令。这种架构正好和 PX4、Betaflight 等主流开源飞控的底层设计一致。
1.2 飞控与矢量控制的关系
这里先帮大家理清几个容易混淆的概念。
- 矢量控制(FOC):是电机驱动层的控制算法,核心对象是永磁同步电机(PMSM)或无刷直流电机(BLDC),处理的是“怎么让电机输出想要的力矩”。
- 飞控:是飞行控制系统的总称,通常包含姿态解算、位置估计、导航、任务规划等多个层次,处理的是“飞机应该怎么飞”。
- 飞行控制算法:一般指姿态环、速度环、位置环等控制器的设计,它输出的是期望力矩或期望油门。
所以,飞控系统最底层其实是电调。传统电调接收飞控的油门 PWM/DSHOT 信号,内部用方波换相;而矢量控制飞控系统则是把 FOC 算法直接嵌入电调或飞控一体化硬件,让飞控下发的“期望力矩指令”不经过油门到 PWM 的粗糙映射,而是直接作为矢量控制器的电流/力矩给定值。
1.3 本文完成后的能力范围
学完本文内容,你将能够:
- 掌握 FOC 的完整数学基础:Clarke 变换、Park 变换、逆 Park、SVPWM。
- 搭建一套基于 STM32 的电机矢量控制系统,包含电流采样、编码器读取、PWM 输出。
- 将 FOC 底层的电机控制任务嵌入飞控整体架构。
- 让飞控与地面站通信,通过 Mission Planner 发送 MAVLink 消息控制电机和飞行状态。
- 独立完成从硬件在环测试、电机参数辨识到闭环调参的全流程。
2. 环境准备与版本说明
2.1 硬件平台
这个项目最初是在一块自制 STM32F405 飞控板上完成验证的,后来又迁移到了 STM32F427 平台。硬件清单如下:
| 模块 | 型号/参数 | 用途 |
|---|---|---|
| 主控 MCU | STM32F405RGT6 / STM32F427VIT6 | 运行飞控与 FOC 算法 |
| 电机 | 2204 2300KV 无刷电机 / 2814 电机 | 拉力和惯量测试 |
| 驱动 | 分立式三相全桥 + DRV8301 驱动芯片 | 三相逆变桥与电流放大 |
| 电流采样 | 三电阻采样,采样电阻 0.005Ω | 相电流检测 |
| 位置传感器 | AS5047P 磁编码器(SPI) | 获取转子电角度 |
| IMU | MPU6500 + 磁力计 IST8310 | 姿态解算 |
| 遥控接收 | SBUS 接收机 | 遥控输入 |
| 数传 | 433MHz / 915MHz 数传模块 | 地面站通信 |
需要说明一点:2024 年以后 STM32 部分型号供货依然偏紧,如果买不到原厂芯片,可以换成国产替代型号,如 GD32F405、AT32F435 等,代码移植成本很低。
2.2 软件工具链
- 操作系统:Windows 10/11 + WSL Ubuntu 20.04(也可以在 Ubuntu 直接开发)
- 交叉编译链:arm-none-eabi-gcc 9.3.1
- IDE:STM32CubeIDE 1.13 或 VS Code + CMake
- 飞控固件:PX4 Firmware v1.13 分支(学习 FOC 集成时重点看 modules 结构)
- 地面站:Mission Planner 1.3.80 / QGroundControl 4.2
- Python 3.8+(用于数据分析、电机辨识、串口通信脚本)
- 上位机波形工具:Serial Studio 或自写 Python 串口曲线工具
版本这里不强制固定,关键点是你的编译工具链和 PX4 源码保持一致,否则会出现奇怪的编译错误。
2.3 示例项目结构
为了不让自己迷失在 PX4 庞大源码里,建议维护一个独立的 FOC 学习仓库,只保留与电机控制强相关的代码模块:
foc_flight_controller/ ├── Core/ │ ├── Inc/ │ └── Src/ │ ├── main.c │ ├── foc.c │ ├── foc.h │ ├── svpwm.c │ ├── svpwm.h │ ├── clark_park.c │ ├── current_sensor.c │ ├── encoder.c │ └── mavlink_bridge.c ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── BSP/ ├── Middlewares/ │ └── mavlink/ ├── PythonTools/ │ ├── motor_identify.py │ ├── foc_plot.py │ └── mavlink_cmd.py └── Docs/ └── 调试记录.md实际上 PX4 源码中的modules/control_block和src/lib/目录里就有 FOC 相关的实现可以对照阅读,但对于学习来说,从零写一个独立的最小实现更容易建立直觉。
3. 矢量控制核心原理拆解
3.1 FOC 的输入输出
FOC 的终极目标是让电机的电磁转矩精确跟踪给定值。电机的电磁转矩公式为:
[ T_e = \frac{3}{2} \cdot p \cdot [\psi_f \cdot i_q + (L_d - L_q) \cdot i_d \cdot i_q] ]
对于表贴式永磁同步电机(SPMSM),L_d ≈ L_q,转矩简化为:
[ T_e = \frac{3}{2} \cdot p \cdot \psi_f \cdot i_q ]
也就是说,如果能让直轴电流 i_d=0,那么转矩只和交轴电流 i_q 成正比。FOC 算法的核心任务就是通过坐标变换实现 i_d 和 i_q 的解耦,让 i_d=0,同时快速控制 i_q。
FOC 的输入是:
- 三相电流 ia、ib、ic(通常采样两相,第三相通过基尔霍夫定律计算)
- 转子电角度 θe(由磁编码器获得)
输出是:
- 三相逆变桥的占空比 DA、DB、DC
3.2 Clark 变换与 Park 变换
3.2.1 Clark 变换
Clark 变换把三相静止坐标系(abc)变换到两相静止坐标系(α-β):
[ \begin{bmatrix} i_\alpha \ i_\beta \end{bmatrix}
\begin{bmatrix} 1 & -\frac{1}{2} & -\frac{1}{2} \ 0 & \frac{\sqrt{3}}{2} & -\frac{\sqrt{3}}{2} \end{bmatrix} \cdot \begin{bmatrix} i_a \ i_b \ i_c \end{bmatrix} ]
如果采用等幅值变换,系数可以直接展开写成代码。注意这里有两种约定:等幅值变换和等功率变换,会影响后续 PI 参数的标定,建议统一使用等幅值。
3.2.2 Park 变换
Park 变换把静止 α-β 坐标系变换到旋转 d-q 坐标系:
[ \begin{bmatrix} i_d \ i_q \end{bmatrix}
\begin{bmatrix} \cos\theta_e & \sin\theta_e \ -\sin\theta_e & \cos\theta_e \end{bmatrix} \cdot \begin{bmatrix} i_\alpha \ i_\beta \end{bmatrix} ]
经过 Park 变换后,d 轴和 q 轴电流都是直流量,PI 控制器可以做到无静差跟踪。
3.2.3 最小可运行代码
// 文件路径:Core/Src/clark_park.c #include "clark_park.h" void clark_transform(float ia, float ib, float ic, float *i_alpha, float *i_beta) { *i_alpha = ia; *i_beta = (ia + 2.0f * ib) / 1.7320508f; } void park_transform(float i_alpha, float i_beta, float theta, float *i_d, float *i_q) { float sin_t = sinf(theta); float cos_t = cosf(theta); *i_d = i_alpha * cos_t + i_beta * sin_t; *i_q = -i_alpha * sin_t + i_beta * cos_t; } void inverse_park_transform(float v_d, float v_q, float theta, float *v_alpha, float *v_beta) { float sin_t = sinf(theta); float cos_t = cosf(theta); *v_alpha = v_d * cos_t - v_q * sin_t; *v_beta = v_d * sin_t + v_q * cos_t; }这里要注意,Clark 变换中如果只采样了 ia 和 ib,ic = -(ia+ib)。某些 MCU 的 ADC 采用双采样模式,也可以直接采到两相;如果采三相,建议做一次三相和校验,轻微误差可以接受,偏差过大说明采样时序或放大电路有问题。
3.3 SVPWM 调制
得到 v_alpha、v_beta 后,需要把它们转换成三相桥臂的占空比。空间矢量调制(SVPWM)比直接 SPWM 的母线电压利用率更高,约提高 15%,同时谐波含量更低。
SVPWM 的基本思路是:八个基本电压矢量(六个有效矢量+两个零矢量)在空间形成一个正六边形,任意目标电压矢量都可以由相邻两个有效矢量和零矢量在一个 PWM 周期内合成。
实现 SVPWM 的常见方式是:
- 根据 v_alpha、v_beta 判断目标矢量所在扇区。
- 计算相邻矢量的作用时间 T1、T2。
- 计算零矢量时间,并按照七段式或五段式方式插入。
// 文件路径:Core/Src/svpwm.c // 输入 v_alpha, v_beta, 输出三相占空比 d1,d2,d3(范围 0~1) void svpwm_calc(float v_alpha, float v_beta, float *d1, float *d2, float *d3) { float v1 = v_beta; float v2 = -0.5f * v_beta + 0.8660254f * v_alpha; float v3 = -0.5f * v_beta - 0.8660254f * v_alpha; float t1 = v1; float t2 = v2; float t3 = v3; float tmax = t1; if (t2 > tmax) tmax = t2; if (t3 > tmax) tmax = t3; float tmin = t1; if (t2 < tmin) tmin = t2; if (t3 < tmin) tmin = t3; float t_offset = 0.5f - 0.5f * (tmax + tmin); *d1 = t1 + t_offset; *d2 = t2 + t_offset; *d3 = t3 + t_offset; }这种简化实现适合学习,工程上更推荐查表法或预计算扇区法。实际调试时还要注意死区补偿,否则相电流在过零点附近会出现明显畸变。
3.4 电流环 PID 与增量式 PID
FOC 电流环通常使用 PI 控制器,因为电流环带宽高,微分项容易放大噪声。q 轴和 d 轴各自有一个 PI。
PID 公式:
[ u(t) = K_p \cdot e(t) + K_i \cdot \int_0^t e(\tau) d\tau + K_d \cdot \frac{de(t)}{dt} ]
数字实现时常用位置式和增量式。增量式 PID 输出的是控制量的增量,不容易出现积分饱和导致的超调问题,在电机控制中很常用:
// 文件路径:Core/Src/pid.c typedef struct { float Kp; float Ki; float Kd; float integral; float last_error; float output_limit; float integral_limit; } PID_TypeDef; float pid_incremental(PID_TypeDef *pid, float error, float dt) { float derivative = 0.0f; if (dt > 1e-6f) { derivative = (error - pid->last_error) / dt; } pid->integral += error * dt; if (pid->integral > pid->integral_limit) pid->integral = pid->integral_limit; if (pid->integral < -pid->integral_limit) pid->integral = -pid->integral_limit; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; if (output > pid->output_limit) output = pid->output_limit; if (output < -pid->output_limit) output = -pid->output_limit; pid->last_error = error; return output; }电流环 PI 参数可以先根据电机电感和电阻估算,再通过开环空载试验微调。如果出现高频啸叫,多半是 Kp 太大或采样相位滞后;如果 q 轴电流静差明显,需要适当增大 Ki 而不是继续增加 Kp。
3.5 完整的 FOC 控制循环
最终 FOC 控制循环放在 PWM 更新中断或 TIM 更新中断里执行,常见频率是 16kHz~32kHz。我的系统跑的是 20kHz 电流环、1kHz 速度环。
主循环伪代码如下:
// 文件路径:Core/Src/foc.c void foc_tick(void) { // 1. 采集两相电流 float ia = current_sensor_get_ia(); float ib = current_sensor_get_ib(); float ic = -(ia + ib); // 2. 读取编码器角度,换算电角度(注意极对数) float theta = encoder_get_elec_angle(); // 3. Clark + Park float i_alpha, i_beta, i_d, i_q; clark_transform(ia, ib, ic, &i_alpha, &i_beta); park_transform(i_alpha, i_beta, theta, &i_d, &i_q); // 4. 电流 PI(目标 id_ref 通常为 0,iq_ref 来自速度环/力矩指令) float v_d = pid_incremental(&pid_d, i_d_ref - i_d, dt); float v_q = pid_incremental(&pid_q, i_q_ref - i_q, dt); // 5. 反 Park + SVPWM float v_alpha, v_beta; inverse_park_transform(v_d, v_q, theta, &v_alpha, &v_beta); svpwm_calc(v_alpha, v_beta, &d1, &d2, &d3); // 6. 更新 PWM 占空比 pwm_update_duty(d1, d2, d3); }很多新手刚开始看 FOC 代码会觉得这是一个“黑盒”,其实把数据流走一遍后会发现每个模块都非常清晰:采样 -> 坐标变换 -> 控制器 -> 逆变换 -> 调制。
4. 飞控系统架构与代码实战
4.1 从纯电机控制扩展到飞控
电机控制跑通之后,下一步就是把 FOC 模块作为飞控的最底层执行器接入。飞控系统整体可以分为四层:
- 感知层:IMU、磁力计、气压计、GPS,用于获取姿态、位置、速度信息。
- 状态估计层:扩展卡尔曼滤波(EKF)或互补滤波,融合 IMU 数据得到当前姿态角和角速度。
- 控制层:外环位置/速度控制,内环姿态/角速度控制,最终输出期望力矩或期望油门。
- 执行层:FOC 控制器将期望力矩映射为电流指令,驱动电机旋转。
在这个架构里,飞控的姿态环输出的是“期望角加速度”,角加速度再通过混控器(Mixer)分配给各个电机。传统混控器输出的是油门百分比,矢量控制飞控里则可以直接转换成期望力矩/期望电流。
4.2 姿态控制基础:内外环 PID
姿态控制是飞控的核心算法之一。外环是角度环,内环是角速度环,外环输出作为内环给定。
角度环:
// 期望姿态角 roll_pitch_yaw 来自遥控器或任务规划 float angle_err = target_roll - current_roll; float rate_target = angle_pid[ROLL].Kp * angle_err;角速度环:
float rate_err = rate_target - gyro_rate; float output = rate_pid[ROLL].Kp * rate_err + rate_pid[ROLL].Ki * integral_rate_err;这两级 PID 的时间尺度差异很大:角度环通常 100~200Hz,角速度环 500~1000Hz。FOC 电流环则运行在 20kHz 左右。三层响应速度逐级加快,这就是飞控稳定性的关键所在。
4.3 MAVLink 通信与 Mission Planner 联调
飞控开发离不开地面站。我用的是 Mission Planner,通过 MAVLink 协议与飞控通信。MAVLink 是一种轻量级消息传输协议,消息以“帧”为单位,包含起始字节、长度、序列号、系统 ID、组件 ID、消息 ID、载荷和校验位。
项目初期,我只用 Mission Planner 读取飞控姿态数据和传感器数据,方便调参。后来实现了从 Mission Planner 端发送 MAVLink 指令给飞控,完成远程电机控制、模式切换、参数修改等操作。
4.3.1 用 Python 发送 MAVLink 指令给飞控
为了快速验证飞控行为,我写了一个 Python 脚本,利用 pymavlink 库让 Mission Planner 或独立脚本直接发送 MAVLink 消息给飞控:
# 文件路径:PythonTools/mavlink_cmd.py from pymavlink import mavutil import time # 连接飞控串口(需根据实际端口修改,Linux 下一般是 /dev/ttyUSB0 或 /dev/ttyACM0) master = mavutil.mavlink_connection('/dev/ttyUSB0', baud=115200) # 等待飞控发送心跳包 master.wait_heartbeat() print("已连接飞控,收到心跳包") # 切换飞控模式为 Stabilize(模式号 0 或 1 视具体固件而定) mode = 'STABILIZE' mode_id = master.mode_mapping().get(mode) if mode_id is None: print(f"未知模式: {mode}") else: master.set_mode(mode_id) print(f"切换到 {mode} 模式") # 发送电机throttle控制指令(MAVLink msg: SET_POSITION_TARGET_LOCAL_NED 或 MANUAL_CONTROL) # 这里以 MANUAL_CONTROL 为例,用于遥控器/手柄替代输入 master.mav.manual_control_send( master.target_system, 0, # x, 横滚方向 0, # y, 俯仰方向 0, # z, 油门(-1000 ~ 1000) 0, # r, 偏航 0 # buttons ) time.sleep(2) # 发送起飞指令(特定固件支持 MAV_CMD_COMPONENT_ARM_DISARM) master.mav.command_long_send( master.target_system, master.target_component, mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, 0, 1, # 解锁 0, 0, 0, 0, 0, 0 ) print("已发送解锁指令")如果是在 Mission Planner 的“MAVLink 控制台”里执行,也可以用类似的消息结构。实际飞行前务必在无桨安全环境下测试解锁逻辑,避免电机突然高速旋转造成意外。
4.3.2 飞控串口收包与解析
在飞控端,我通过串口 DMA 中断接收 MAVLink 帧,并调用 mavlink_parse_char() 逐字节解析消息。这个库可以自动处理粘包、CRC 校验和消息长度判断,比自己写解析逻辑稳定得多。
// 伪代码,示意 MAVLink 帧接收与解析 void mavlink_process_byte(uint8_t c) { mavlink_message_t msg; mavlink_status_t status; if (mavlink_parse_char(MAVLINK_COMM_0, c, &msg, &status)) { switch (msg.msgid) { case MAVLINK_MSG_ID_HEARTBEAT: // 记录飞控心跳,用于链路状态判断 break; case MAVLINK_MSG_ID_MANUAL_CONTROL: // 解析遥控通道值,作为姿态环和电流环参考输入 mavlink_manual_control_t manual; mavlink_msg_manual_control_decode(&msg, &manual); update_manual_control(manual.z, manual.x, manual.y, manual.r); break; default: break; } } }我最初踩过一个坑:MAVLink 消息里 payload 长度不是固定的,不同消息有不同的最小长度和 CRC extra,如果直接按固定长度解析会导致校验失败。这点使用官方 mavlink 库后完全规避了。
4.4 混控器与 FOC 指令映射
飞控和底层 FOC 之间需要有一个“混控器”,把飞控输出的滚转、俯仰、偏航力矩分配到每个电机轴。
以四旋翼 X 型布局为例,四个电机的期望推力(或期望电流)可以写成:
F1 = throttle + roll_cmd - yaw_cmd F2 = throttle - roll_cmd - yaw_cmd F3 = throttle + roll_cmd + yaw_cmd F4 = throttle - roll_cmd + yaw_cmd在实际代码里,roll_cmd、pitch_cmd、yaw_cmd 是经过角速度环计算出来的控制量。得到每个电机的期望推力后,再通过推力-电流曲线映射成 iq_ref 送给对应电机的 FOC 控制器。
这条映射曲线最好通过电机拉力测试台实验获取:测不同油门/电流下的推力和转速,然后拟合成多项式或查表函数。不要用线性假设,因为电机在低油门和中高油门的推力-电流特性差异非常明显。
4.5 代码整合步骤
如果你打算在自己的飞控项目里整合 FOC 模块,建议按以下步骤进行:
- 先做硬件验证:用磁编码器+电机+驱动板,跑通开环 SVPWM,确认电机能平稳转动。
- 电流闭环:加上电流采样和 PI 控制器,让电机在锁定转子或空载情况下能跟踪电流指令。
- 速度闭环:在电流环外面加速度环,验证加减速响应。
- 接入飞控姿态环:先只接一个电机,用姿态环输出的期望力矩控制该电机推力。
- 接入 MAVLink:完成从 Mission Planner 到飞控的指令链路。
- 整机带桨测试:在安全防护网内进行,先小油门解锁,逐步验证姿态响应和抗风性。
5. 常见问题与排查思路
这里把这一年迭代中最常遇到的问题整理成表格,方便你对照排查。
| 问题现象 | 常见原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 电机不转,只有高频啸叫 | SVPWM 扇区判断错误,或 PWM 频率落在音频范围 | 检查 PWM 频率和对地波形 | 确认 PWM 频率 20kHz 以上;用示波器查看六路 PWM 波形是否互补 |
| 电机转动但电流波形畸变 | 死区时间过大或死区补偿未做 | 检查桥臂上下管导通波形 | 针对母线电压和开关频率设置合理死区;必要时加入死区补偿 |
| 电流采样毛刺大 | ADC 采样时刻与 PWM 边沿重叠 | 查看相电流波形是否在 PWM 中心点采样 | 调整为 PWM 中心对齐采样,避开开关噪声 |
| 编码器角度跳变 | 电角度对齐不准,或 SPI 通信不稳定 | 打印编码器原始值和电角度变化曲线 | 重新校准零位偏移;检查 SPI 时钟频率 |
| 电机低速抖动 | iq 指令增益太小,或编码器分辨率不足 | 用上位机查看 iq 实际跟踪情况 | 增大电流环 Kp;检查编码器分辨率是否满足最低电角度分辨率 |
| 飞控可以解锁但电机无力 | 混控指令到电流指令的映射错误 | 查看每个电机的 iq_ref 是否随摇杆变化 | 检查混控器轴分配顺序和正负号 |
| Mission Planner 连不上飞控 | 串口波特率不匹配、USB 转串口驱动异常 | 查看设备管理器端口号 | 尝试 115200 或 57600 波特率;检查 MAVLink 版本是否一致 |
| MAVLink 包校验失败 | 消息长度或 CRC extra 不匹配 | 打开 mavlink 库中的 debug 输出 | 使用与地面站一致的 mavlink 版本(V1/V2) |
| 电机解锁瞬间猛转 | 姿态角初始误差过大,或 iq_ref 限制失效 | 增加安全限幅 | 代码里添加指令限幅和急停逻辑 |
| 飞控姿态发散 | 角速度环 PID 参数过激、传感器方向反 | 查看 IMU 原始数据和角速度环输出 | 先验证每个轴的传感器方向,再逐步加大 PID 增益 |
6. 最佳实践与工程建议
6.1 安全设计:解锁保护与急停
飞控最怕的就是上电后电机意外转动。我的习惯是设计一套三级安全保护:
- 上电默认锁定,只有检测到遥控器解锁命令或 MAVLink 解锁指令后才允许电机转动。
- 异常自动锁定,当传感器数据异常、电流超限、通信超时超过 200ms 时立即锁定。
- 物理急停,保留一路 SBUS 通道作为急停开关,优先级最高,可随时切断输出。
代码中所有电机输出在进入 PWM 寄存器之前都要经过限幅校验:
// 示例:电机指令安全校验 // 文件路径:Core/Src/safety.c bool safety_check_motor_command(float *motor_cmds, uint8_t motor_num) { for (uint8_t i = 0; i < motor_num; i++) { // 限制每个电机指令范围 if (motor_cmds[i] > MOTOR_CMD_MAX || motor_cmds[i] < MOTOR_CMD_MIN) { return false; } } // 总推力超限检测 float total = 0.0f; for (uint8_t i = 0; i < motor_num; i++) { total += motor_cmds[i]; } if (total > TOTAL_CMD_LIMIT) { return false; } return true; }6.2 电流采样标定与零漂处理
电流传感器的零漂是 FOC 中最容易被忽略的问题。温度变化、运放偏移、ADC 参考电压波动都会导致零漂,如果不做处理,d 轴电流会出现虚假偏置,导致电机发热。
建议在电机静止且不注入电流的状态下,采集 1000 次电流数据并取平均,得到零点偏移,然后在每次上电初始化时自动校正。
// 初始化时调用,采集电流零漂 void current_sensor_zero_offset_calibrate(void) { float sum_ia = 0.0f, sum_ib = 0.0f; const int samples = 1000; for (int i = 0; i < samples; i++) { sum_ia += adc_get_ia_raw(); sum_ib += adc_get_ib_raw(); } zero_offset_ia = sum_ia / samples; zero_offset_ib = sum_ib / samples; }6.3 电机参数辨识
FOC 控制器设计离不开电机参数:相电阻 Rs、相电感 Ls、反电动势常数 Ke、极对数 p。电机厂商通常不提供精确参数,所以需要自己测量。
我用两种方法:
- 静态测量:用 LCR 表测电感,用万用表测相间电阻。
- 动态辨识:对电机施加阶跃电压,用 Python 脚本记录电流响应曲线,然后用最小二乘法拟合电气时间常数 L/R。
测量得到的参数会直接影响电流环 PI 的初始值:
[ K_p = \frac{L \cdot \omega_{cc}}{3}, \quad K_i = \frac{R \cdot \omega_{cc}}{3} ]
其中 ω_cc 是期望的电流环带宽(rad/s)。这个估算只作为起点,最终还是要通过实际动态响应微调。
6.4 日志与调试数据链路
飞控开发没有日志就等于盲调。我建议至少记录以下数据:
- 电机命令(PWM/duty)
- 三相电流 ia、ib、ic
- 电流环给定和反馈 iq_ref、iq_fbk
- 电角度、电转速
- 姿态角、角速度
- 飞控状态机状态
- 通信链路状态(RSSI、丢包率、MAVLink 心跳时间戳)
我当时用的是“串口日志 + SD 卡存储”的双路方案:串口方便实时查看,SD 卡保证长时间飞行数据不丢失。Mission Planner 的“数据闪存日志”功能也可以直接保存飞行日志,但调试 FOC 时更推荐保留原始电流数据,便于离线分析。
6.5 代码结构与可维护性
飞控系统的代码量不小,模块化设计非常关键。我调整过两次架构,最终确定下来几条原则:
- 电机控制算法与硬件解耦:FOC 算法文件不直接调用 HAL 的 ADC/PWM 接口,而是通过
foc_hw.h这样的抽象层隔离。 - 控制频率分级管理:在 RTOS 或裸机主循环中明确每个任务的周期,避免长任务阻塞电流环中断。
- 参数集中管理:所有 PID 参数和限幅值集中在
param_config.h或参数服务器中,方便地面站在线调参。 - 版本控制:每个可飞版本打 tag,并记录对应的 PID 参数表。这个习惯在调参时非常有用,因为经常出现“新参数没完全比旧参数好”的情况。
6.6 硬件在环测试(HIL)
真正上电之前,建议先做硬件在环测试。将飞控的电机输出虚拟化,用仿真模型替代真实电机,注入真实传感器数据,验证 FOC 控制器和姿态控制逻辑。PX4 支持 Gazebo + jMAVSim 仿真,自己写的 FOC 系统也可以把电机模型封装成 C 语言接口,挂到测试框架里跑。
HIL 的价值在于:它能在没有桨叶和电机的情况下暴露算法逻辑错误、时序错误、数据溢出问题。我做过一次“整机逻辑在线调试”,通过 HIL 发现了一个姿态环与混控器坐标系不匹配的问题,这个错误如果直接上真机,大概率会导致翻机。
7. 总结与学习路线
到这里,这套历时一年开发的矢量控制飞控算法系统的核心内容已经全部梳理完了。整个系统看起来模块很多,但本质是一条清晰的数据流:飞控感知姿态 -> 姿态控制器计算期望力矩 -> 混控器分配电机指令 -> FOC 电流环让电机快速响应 -> 电机输出拉力 -> 飞行器姿态改变。这条链路每多一个环节,稳定性和可靠性就多一重考验。
如果你准备从零开始做类似项目,我的建议是不要直接去啃 PX4 源码,而是先按“三步走”:
- 先学会 FOC:能控制一个无刷电机平稳转动并跟踪电流指令。
- 再学会姿态控制:用串级 PID 让一个三轴/四轴模型在仿真环境里稳定悬停。
- 最后打通集成:把 FOC 嵌入飞控,先单电机验证,再逐步扩展到整机。
做飞控这一年我最深的体会是:这个领域没有捷径,每个环节的问题都需要亲自动手去测量和验证。示波器上的波形是骗不了人的,电机的声音也是。希望这篇文章能让你少走一些弯路。
如果本文对你有帮助,可以收藏备用。也欢迎在评论区分享你在飞控开发中遇到的奇怪问题,我们一起讨论。
祝早日飞起来。