搞电控的应该都有过这种体验:一套板子跑通了有感 FOC,觉得自己行了,结果一拔编码器,电机立刻开始表演——要么原地抽搐,要么直接飞车,要么低速抖得像筛糠。我前几年做一个多轴联动的小平台时就是这么被教育了一轮。后来接触到 SguanFOC 这套面向电机控制教学和实战的开源 FOC 框架,用它把滑模观测器加磁链观测器的无感方案跑起来,再把 CANopen 和 EtherCAT 这两套总线接进去,才算把"电控 + 通信"这条链路真正打通。这篇就把我从环境搭建、代码移植、观测器调参到总线接入的全过程摊开讲一遍,包括参数怎么算、波形怎么看、坑在哪里、为什么这么选。不管你是刚上手无感 FOC 的学生,还是想把手里的板子从"能转"推到"能带载、能联网"的工程师,应该都能从里面抄到点能直接用的东西。
1. 为什么这套方案值得折腾:从有感到无感的技术路径拆解
1.1 无感 FOC 到底省掉了什么,又难在哪里
先把定位说清楚。有感 FOC 是拿编码器或霍尔直接测转子角度,控制器只管按角度去分配 dq 轴电流;无感 FOC 是把位置传感器整个拿掉,靠电压、电流这些"外部可观测量"去反推转子在哪、转多快。省的是硬件成本、布线复杂度、传感器在高温高湿环境里的可靠性问题,代价是把一个原本"测量问题"变成了"估计问题"。
这个转变的影响面比想象中大。测量是确定性的,估计是条件性的——转速越低、负载越重、参数越不准,估计就越不可信。尤其是低速段,反电动势幅值和转速成正比,转速掉到额定的百分之几时,反电动势可能只有几十毫伏,而逆变器的死区压降、管压降、电流采样的零漂都在同一个量级甚至更大,信噪比直接崩掉。这就是为什么很多无感方案标称"最低转速 100rpm 起",而到了 30rpm 以下就完全靠不住。
所以做无感 FOC,真正要解决的不是"怎么写一个观测器",而是三件事:观测器本身要稳、低速段要有替代方案、全速域切换要平滑。这套方案里,滑模观测器负责中高速段的鲁棒估计,磁链观测器负责提供另一路独立的角度信息并改善动态响应,两者配合再加一套启动策略,才能把可用转速范围撑开。这一点想不明白,后面调参就是瞎调。
1.2 滑模观测器与磁链观测器:为什么这套组合要一起用
单看名字,很多人会以为这两个是"二选一"的关系。实际不是。它们拿到的信息源不一样,失效区间也不一样,凑在一起才互补。
滑模观测器(SMO)本质上是一个"强迫跟随"的观测器。它以 αβ 坐标系下的定子电流状态方程为模型,构造一个含切换项的估计器,用切换项把估计电流往实际电流上"摁"。系统进入滑模面之后,切换项的等效值就等于反电动势。它的优点是对电阻、电感参数变化不敏感,抗扰能力强,中高速段估计精度很稳;缺点是切换项天生带抖振,转速低的时候反电动势小,抖振占比大,滤波之后相位滞后明显。
磁链观测器走的是另一条路。它是电压模型:把端电压减去电阻压降积分起来,直接得到定子磁链矢量,角度就是atan2(ψβ, ψα)。这条路不需要电流环参与,动态响应快,而且磁链本身对角度是"直接解",不用经过锁相环,相位滞后小。问题是纯积分器有直流偏置和初值漂移问题,必须用一阶低通代替纯积分,或者加补偿;而且电阻误差会被积分放大,低速时候同样不可靠。
我实测下来,两者在中高速段(比如额定的 15% 以上)角度误差都在 2 到 5 度电角度这个量级,但特性不同:突加载的时候磁链观测器响应更快,稳态下 SMO 更平。所以这套框架的做法是——SMO 主导角度估计,磁链观测器作为交叉校验与快速响应的补偿源,在转速突变和负载突变时提供前馈。
下面这张表是我自己整理的方法对比,选型的时候可以直接对着看:
| 观测方法 | 适用转速区间 | 参数敏感性 | 动态响应 | 实现复杂度 | 典型问题 |
|---|---|---|---|---|---|
| 滑模观测器 SMO | 中高速(>15% 额定) | 低 | 中等 | 中等 | 抖振、滤波滞后 |
| 电压模型磁链观测器 | 中高速 | 中(对 Rs 敏感) | 快 | 低 | 积分漂移、直流偏置 |
| 电流模型磁链观测器 | 低速可用 | 高(对 Ld/Lq、ψf 敏感) | 中等 | 中等 | 依赖参数辨识 |
| 高频注入 HFI | 零速到低速 | 中 | 慢 | 高 | 噪声、凸极率要求 |
| 扩展卡尔曼 EKF | 全速域 | 高(Q/R 难调) | 中等 | 高 | 计算量大、调参玄学 |
看清楚这张表就能明白,"SMO + 磁链观测器"这套组合的定位很明确:它不打算解决零速问题,它要在中高速段做到又稳又快,然后用一套开环启动策略把零速到切入点的这段路走完。指望一套观测器包打全速域,基本不现实。
1.3 CANopen 与 EtherCAT 接入让电控从"单机"变"系统"
电机转起来只是第一步。真到了设备里,电控板不是孤立的,它要接受上位机的指令、上报状态、和别的轴做同步。这时候总线的选择就变成硬约束。
CANopen 是基于 CAN 的高层协议,走的是 CiA 301 那一套:对象字典、SDO、PDO、NMT 状态机、心跳。它的定位是"够用、便宜、皮实",1Mbps 下几十个字节的实时数据没问题,适合单机到几十个节点的小系统,比如一台缝纫设备、一个 AGV 的驱动轮、一台包装机的几根轴。我见过不少汇川、步科的驱动器,出厂就带 CANopen 口,接上去改几个对象字典就能跑。
EtherCAT 是整个层级往上跳一档的东西。它靠从站控制器(ESC)硬件在帧经过的瞬间完成读写,主站一个周期发一帧,所有从站的数据在一次遍历里全部更新完,周期可以做到 100 微秒甚至更低,而且有分布式时钟(DC)做同步,多轴插补的同步误差能压到纳秒到微秒级。代价是主站要求高——你得有一台带实时补丁的 Linux 主机(比如常见的 RK3568 平台配 6.6 内核加 PREEMPT_RT),还得跑 IgH EtherCAT Master 这类主站协议栈。
把这两套东西和 SguanFOC 放在一起的意义在于:FOC 内环跑在微秒级(比如 10kHz 到 20kHz),总线周期跑在毫秒级(CANopen 常见 1ms 到 10ms)或者百微秒级(EtherCAT),两者频率差两三个数量级,必须做彻底的解耦。解耦做不好,总线上随便来一次丢帧,电机就开始抽。这个后面第 6 节会详细讲。
2. 硬件与软件环境准备:移植之前必须搞清楚的地基
2.1 主控、驱动与电流采样链路的地基确认
代码移植翻车,八成不是算法的问题,是地基没核对清楚。我现在的习惯是拿一张纸,把下面这几项一条条对完再动代码:
- 主控型号、内核、主频、FPU 有没有、DSP 指令集有没有
- PWM 定时器用哪个,中心对齐还是边沿对齐,死区怎么配
- ADC 采样通道:是三电阻还是单电阻,还是两电阻,采样点落在 PWM 周期的什么位置
- 运放电路:增益、偏置电压、有没有加 RC 滤波、截止频率多少
- 母线电压范围、电流量程、过流保护阈值
- 编码器/霍尔接口是否保留(做有感和无感对比调试要用)
这里面最容易出问题的是电流采样。无感 FOC 对电流采样的相位延迟极其敏感,因为观测器是靠"模型预测电流"和"实测电流"的差去反推反电动势的,采样点偏一点,估计出来的角度就整体偏一个固定角度。常见的做法是把 ADC 触发点放在 PWM 计数的下溢点(中心对齐模式下三相下桥臂全开的时刻),这样采到的才是真实相电流。
注意:如果用的是单电阻采样,采样窗口很窄,必须保证最小占空比满足采样时间要求,否则低速小占空比时根本采不到电流,观测器直接失效。这是单电阻方案在无感低速段表现普遍不如三电阻的根本原因。
2.2 时钟树、PWM 与 ADC 触发时序对齐
这一块我踩过最深的坑。移植的时候我图省事,直接把原工程的 PWM 频率和 ADC 触发关系抄过来,结果主控主频高了一倍,ADC 采样点整体前移,角度估计偏了将近 20 度电角度,电机带载就失步。
正确的做法是反推。假设你要 10kHz 的电流环频率,中心对齐 PWM 的计数周期就是:
PWM_ARR = F_timer / (2 * f_pwm)比如定时器时钟 168MHz,电流环 10kHz,PWM_ARR = 168e6 / (2 * 10e3) = 8400。这里除以 2 是因为中心对齐模式下,一个上数一个下数才算一个完整周期。周期性中断(也就是 FOC 主循环)挂在计数下溢事件上,ADC 用同一个事件触发,采样时刻就落在下桥臂全开的窗口正中间,时序天然对齐。
再往上还有一层:控制周期 Ts 和中断周期的关系。我的经验是 Ts 必须和中断严格 1:1,不要在一个中断里跑两次 FOC,也不要把 Ts 设成中断周期的整数倍再自己计数,那样一旦中断抖动,电流环周期就跟着抖,观测器的离散化误差会放大。宁可把电流环频率降下来,也要保证周期绝对稳定。
还有一点,ADC 采样到 FOC 计算完成之间的延时,其实是有一个采样周期的滞后。这个滞后在高速段会造成明显的相位误差,工程上常见的处理是把估计角度做一个超前补偿:
theta_comp = theta + omega * 1.5 * Ts /* 1.5 拍的经验系数 */这个 1.5 是经验值,跟你的采样点位置和计算耗时相关,实测的时候微调。
2.3 母线电压、死区与保护电路核查
母线电压在无感方案里不是"电源参数",是观测器方程里的输入量。磁链观测器要算u - R*i,u 就是用母线电压乘以占空比重构出来的,母线电压测不准,磁链积分就整体偏移,低速段直接崩。所以母线电压采样必须标定,别信电阻分压的理论值,拿万用表实测几个电压点做线性拟合。
死区补偿这一项,很多人省掉了,然后抱怨低速估计不准。死区引起的电压误差方向与相电流方向相关,幅值一般在几伏这个量级。在高速段反电动势几十伏,几伏误差无所谓;在低速段反电动势可能只有几百毫伏,这个误差就是致命的。常见的补偿做法是在重构电压上按电流方向加减一个死区电压:
/* 简化死区补偿:按相电流极性修正占空比对应的电压 */ static inline float deadtime_comp(float i, float dt_volt) { if (i > DEAD_ZONE_I) return dt_volt; else if (i < -DEAD_ZONE_I) return -dt_volt; else return 0.0f; /* 电流过零附近不补偿,避免抖动 */ }提示:过零附近一定要留死区不补偿,否则电流在零点附近来回穿越的时候,补偿量会跟着符号翻转,反而引入高频抖动。这是我调试时用示波器一路看电流波形才发现的。
3. SguanFOC 代码结构拆解与移植清单
3.1 目录结构与模块职责
拿到 SguanFOC 的第一件事不是编译,是先看目录。这套框架的分层做得比较清楚,大致是这么几层:
| 层级 | 目录/文件 | 职责 | 移植时动不动 |
|---|---|---|---|
| 应用层 | main.c、app_task | 状态机、启停逻辑、命令行 | 基本重写 |
| 控制层 | foc_core.c、svpwm.c | 电流环、速度环、SVPWM | 极少改 |
| 观测器层 | smo.c、flux_obs.c、pll.c | 角度转速估计 | 参数要调 |
| 算法库 | math_utils.c、pid.c、lpf.c | PID、滤波、坐标变换 | 不改 |
| 硬件抽象 | hal_*.c、board.h | ADC、PWM、定时器、IO | 全部重写 |
| 通信层 | canopen/、ecat/ | 总线协议栈 | 按需接入 |
分清楚这个层次非常关键。控制层和观测器层是"与硬件无关"的,理论上直接搬;硬件抽象层必须按目标 MCU 全部重写;应用层和通信层看你自己的需求。
3.2 硬件抽象层:必须逐条改的宏与接口
移植最容易出乱子的就是这一层。我建议先把board.h里所有跟硬件相关的宏列一个清单,逐条改完再编译:
PWM_FREQ_HZ:电流环频率,同时决定中断频率PWM_PERIOD:定时器 ARR 值,按 2.2 的公式算DEADTIME_NS:死区时间,按驱动芯片和功率管选VBUS_NOMINAL:额定母线电压,用于标幺化ADC_FULL_SCALE:ADC 满量程对应电流CURRENT_OFFSET_A/B/C:三路电流零偏,必须实测标定POLE_PAIRS:极对数,写错电机会转但转速读数差整数倍RS_OHM、LS_H、FLUX_WB:电机参数,观测器全靠这几个
这里有一项特别想提醒:CURRENT_OFFSET千万别用理论值 2048(12 位 ADC 中值)。我实测过同一块板子的三路电流零偏,分别差了 8、-15、21 个 LSB,直接填理论值会让三相电流和不为零,dq 变换出来的直流分量被观测器当成反电动势,电机低速就开始缓慢漂移。
标定方法很简单:上电不使能,连续采 1000 次电流 ADC 值取平均,减去满量程一半,写进结构体。
void Current_Offset_Calib(void) { int32_t sum_a = 0, sum_b = 0, sum_c = 0; for (int i = 0; i < 1000; i++) { sum_a += ADC_Read_Channel(CH_A); sum_b += ADC_Read_Channel(CH_B); sum_c += ADC_Read_Channel(CH_C); } g_curr_offset.a = (float)sum_a / 1000.0f - ADC_HALF_SCALE; g_curr_offset.b = (float)sum_b / 1000.0f - ADC_HALF_SCALE; g_curr_offset.c = (float)sum_c / 1000.0f - ADC_HALF_SCALE; }3.3 浮点、定点与数学库的选择
SguanFOC 默认是浮点实现。如果你的主控带 FPU(Cortex-M4F、M7、RISC-V 带 F 扩展等),直接开浮点,编译选项加-mfpu=fpv4-sp-d16 -mfloat-abi=hard,性能完全够。三角函数的调用频率是电流环频率的两三倍,10kHz 下也不过三万次每秒,M4F 上跑sinf/cosf大概几十个周期,加上 SCV 指令集优化,占用不到主频的 5%。
如果主控没有 FPU,就必须转定点。转换的坑主要在 Q 格式的选择上:电流用 Q12 到 Q15,角度用 Q15 全圆表示(0 到 65535 对应 0 到 2π),磁链因为动态范围大,建议用 Q10 甚至 Q8。混用时一定要做显式的移位,别指望编译器帮你自动处理。
我个人的建议是:除非成本压到极致,否则别折腾定点。无感观测器调参本身就够复杂了,定点化会引入量化误差和溢出问题,把调试难度拉高一档。
3.4 移植完成后的自检清单
编译通过不等于移植成功。我自己的自检顺序是:
- 不接电机,示波器看三路 PWM,确认频率、死区、互补性正确
- 不使能,读电流采样值,确认零偏在 ±30 LSB 以内
- 开环给固定电压矢量,测三相电流波形,确认是正弦、对称、无直流偏置
- 用手转动电机,看观测器输出的角度是否跟着走、是否连续(重点看有没有跳变)
- 反电动势常数验证:匀速拖动电机,读
e_hat幅值,反推ω*ψf,和实测线电压对得上 - 闭环空载运行,看速度波动和电流波形
第 4 步特别重要,也是我强烈推荐的一个调试技巧:手转电机看观测器角度。这一步能验证观测器的极性、方向、连续性,而且不涉及功率输出,绝对安全。如果手转的时候角度跳变或者方向反了,后面所有工作都是白费。
4. 滑模观测器 + 磁链观测器的参数整定与调参实录
4.1 滑模增益与边界层函数的参数推导
滑模观测器的核心方程就这么几行:
/* 电流误差为滑模面 */ err_a = i_hat_a - i_a; err_b = i_hat_b - i_b; /* 用饱和函数代替符号函数,边界层厚度 SMO_BOUNDARY */ sat_a = SAT(err_a, SMO_BOUNDARY); sat_b = SAT(err_b, SMO_BOUNDARY); /* 电流状态方程前向欧拉离散 */ i_hat_a += (u_a - Rs * i_hat_a - K_slide * sat_a) * Ts / Ls; i_hat_b += (u_b - Rs * i_hat_b - K_slide * sat_b) * Ts / Ls; /* 切换项的等效值即反电动势估计 */ e_hat_a = K_slide * sat_a; e_hat_b = K_slide * sat_b;关键参数两个:K_slide和SMO_BOUNDARY。
滑模增益K_slide的选取原则是"必须大于反电动势峰值",否则滑模面到达不了。反电动势峰值就是ω_max × ψf。比如额定 3000rpm、极对数 4、磁链 0.08Wb,机械角速度 314rad/s,电角速度 1256rad/s,反电动势峰值约 100V。那K_slide至少要取 100,一般留 1.5 到 2 倍裕量,取 150 到 200。
但是!K_slide直接决定抖振幅值。增益越大估计越鲁棒,抖振也越大,经过低通滤波之后的相位滞后也越大。我实测下来,如果不对e_hat做滤波,直接送 PLL,增益取到反电动势峰值的 2 倍时,高速段角度抖动大概在 ±3 度电角度;如果增益取到 5 倍,抖动会涨到 ±8 度,电流波形上能明显看到毛刺。
SMO_BOUNDARY是饱和函数的边界层厚度,作用是把sign的硬切换变成软切换。取值太小等于没做滤波,抖振照旧;取值太大,观测器就退化成线性观测器,鲁棒性下降。经验取值是额定电流幅值的 5% 到 15%。
static inline float SAT(float x, float boundary) { if (x > boundary) return 1.0f; if (x < -boundary) return -1.0f; return x / boundary; }4.2 磁链观测器的低速困境与工程解法
磁链观测器的低速问题是个结构性问题,不是调参能解决的。原因拆开看有三条:
第一,反电动势和转速成正比,低速时信噪比低。第二,纯积分器有直流偏置累积,任何一点零漂或者参数误差,积分几秒之后磁链就飘到天上去了。第三,电阻压降占比在低速时变大——高速时R*i可能只占端电压的 5%,低速时能占到 30% 以上,电阻值偏 10%,磁链就偏 3%,角度就偏。
工程上的解法有几套,我在不同项目里都用过:
用一阶低通代替纯积分。这是最常用的,代价是引入幅值和相位误差。低通截止频率越低,误差越小,但抗漂移能力越差。我一般取wc = 0.05 * ω_rated,在额定转速的 10% 以上误差可以接受。
psi_a += (e_a - wc * psi_a) * Ts; psi_b += (e_b - wc * psi_b) * Ts;加直流偏置补偿环。用 dq 轴上的磁链理论值(q 轴为 ψf,d 轴为 0)作为参考,把观测出来的磁链偏差反馈回去修正积分器。这招效果好,但需要准确的磁链参数。
低速段切到电流模型或者高频注入。这是最彻底的方案,但复杂度高,而且高频注入有噪声,某些应用不允许。
最省事的做法:低速段不走观测器,走 I/F 开环强拖。这也是 SguanFOC 默认的做法,实测最稳,下面 4.4 会展开讲。
4.3 锁相环带宽与滤波器的配合
滑模观测器输出的是 αβ 系下的反电动势估计,本身带高频抖振,需要经过低通滤波后再送锁相环(PLL)提取角度。PLL 的结构是一个 PI 调节器加一个积分器,误差信号是:
float eps = -e_alpha * cosf(theta) - e_beta * sinf(theta);这个误差在 θ 和估计值接近时正比于角度误差,PI 调节器把误差压到零,积分器的输出就是角度,PI 输出的比例项加上积分项就是电角速度。
PLL 带宽的选择是"响应速度"和"抗噪能力"的权衡。带宽越高,转速突变时跟随越快,但反电动势上的噪声也更容易穿过;带宽越低越平滑,但动态响应慢,容易在突加载时失步。我的经验取值范围:
| 应用场景 | PLL 带宽(Hz) | 前置 LPF 截止(Hz) | 特点 |
|---|---|---|---|
| 风机、水泵类 | 20 到 50 | 200 到 500 | 平滑优先 |
| 通用伺服 | 50 到 150 | 500 到 1000 | 折中 |
| 高速主轴 | 150 到 300 | 1000 到 2000 | 响应优先 |
前置低通的截止频率一般取 PLL 带宽的 5 到 10 倍,太低会吃掉有用信号。还有一个细节:低通滤波会引入相位滞后,滞后量φ = atan(ω/wc),在额定转速下可能有几度到十几度。常用的补偿办法是把估计角度加上这个补偿量,或者干脆把低通的滞后等效成角度偏移,在标定时统一扣掉。
注意:滤波器和 PLL 是串联的,两级都有自己的滞后,不要把两级带宽设得一样高,否则在某一段频率上会形成共振,转速波形上会出现明显的振荡。我踩过这个坑,现象是电机在 800rpm 附近转速波动特别大,换了个带宽就没了。
4.4 启动策略:从 I/F 强拖到闭环的平滑切换
零速和极低速段,任何依赖反电动势的观测器都歇菜。所以必须有一段开环启动过程。常见的有 V/F 启动、I/F 启动、高频注入定位加开环加速三种。SguanFOC 走的是 I/F,也就是恒定电流、线性升频。
I/F 启动的逻辑很简单:给一个固定的电流矢量,让它按设定的频率旋转,电机被电流拖着同步加速。参数三个:启动电流幅值I_start、起始频率f_start、加速度df/dt。
I_start一般取额定电流的 30% 到 80%,太小带不动负载,太大会让电机在启动瞬间抖一下。f_start取额定频率的 2% 到 5%,太低容易堵转,太高容易失步。也可以是 0,从零频开始,但那样启动会慢。df/dt是最关键的。加速太快电机跟不上直接失步,太慢启动时间拉长。经验值是按你希望达到切换转速所用的时间来定,比如 0.5 秒到 2 秒。
切换到闭环的时机和方式决定了启动的成败。切换条件一般用"估计转速连续 N 次超过阈值且角度误差小于阈值"。切换方式有硬切和软切两种。硬切就是直接把角度源从开环切到观测器,简单但容易在切换瞬间产生转矩冲击。软切是用斜坡把两个角度源的权重从 0 和 1 慢慢过渡:
/* 软切换:w 从 0 线性增长到 1 */ theta_cmd = (1.0f - w) * theta_openloop + w * theta_observer;我实测下来,软切时间设在 100 到 200 个电流环周期(也就是 10 到 20ms),切换时的电流冲击能压到基本看不出来。
5. 运行效果实测:波形、转速范围与带载表现
5.1 空载与带载实测波形对比
调完之后我拿一台 750W 永磁同步电机做了实测,极对数 4,额定 3000rpm,母线 310V。用上位机把 dq 轴电流、估计转速、估计角度、实际角度(外接编码器只做对比,不参与控制)全部传到 PC,存下来分析。
空载 1500rpm 稳态下,估计角度和实际角度的误差在 ±3 度电角度以内,转速波动 ±5rpm,相电流 THD 用示波器粗看大概 6% 到 8%。切换点以下用 I/F 运行,电流是标准的正弦,频率跟随设定值。
带载 50% 额定转矩的时候,稳态角度误差涨到 ±5 到 7 度,转速波动 ±10rpm。突加满载时,估计转速有一个短暂的跌落和超调,恢复时间大概 80 到 150ms,这期间角度误差峰值能到 15 度左右,但没有失步。
这里有一个很重要的观察:负载越大,估计角度和实际角度之间的稳态偏差越大。原因是负载电流在定子电阻和电感上产生的压降被观测器当成了模型的一部分,而实际参数和模型参数有偏差。这个偏差可以通过在线辨识电阻来减小,但工程上更实用的是把它当成"已知偏差"直接补偿回去。
5.2 转速范围与切换点实测数据
我把这套方案在不同工况下跑了多组,整理出下面一组数据:
| 工况 | 切换转速 | 最低稳定转速 | 最高转速 | 稳态角度误差 | 备注 |
|---|---|---|---|---|---|
| 空载 | 150rpm | 120rpm | 3000rpm | ±3° | 120rpm 以下 I/F |
| 25% 额定负载 | 180rpm | 150rpm | 3000rpm | ±4° | 低速段略抖 |
| 50% 额定负载 | 220rpm | 180rpm | 3000rpm | ±6° | 推荐切换点 250rpm |
| 满载 | 300rpm | 250rpm | 2900rpm | ±9° | 满载高速段效率略降 |
看这组数据能得出几个很实际的结论。切换点不能设死,必须随负载调整,或者干脆设得保守一点,宁可开环多跑一段,也别在临界点上反复切换。满载时最高转速到不了 3000rpm,是因为观测器的相位滞后加上电流环带宽限制,需要额外的角度补偿才能把转速拉上去。
提示:切换点如果设得太低,会出现"切了又退、退了又切"的震荡现象,表现为电机在某个转速附近反复抖动。我处理这个问题的办法是加一个滞环,切入阈值和切出阈值差 30%,避免来回横跳。
5.3 异常波形速查与原因对照
调试过程中记录了一批典型异常,整理成速查表,遇到问题可以直接对照:
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 估计角度缓慢漂移 | 电流零偏未标定、积分器偏置 | 先查零偏,再查磁链积分 |
| 角度跳变 180° | 极性判断错误、atan2 符号约定不一致 | 查 sin/cos 约定,查 e_alpha/beta 定义 |
| 低速抖动大 | 死区未补偿、PLL 带宽过高 | 先补死区,再降带宽 |
| 高速电流毛刺 | 滑模增益过大、滤波不足 | 降 K_slide 或加滤波 |
| 带载失步 | 切换点过低、K_slide 不足 | 提高切换点,加增益裕量 |
| 转速读数差整数倍 | 极对数写错 | 对着铭牌核对 |
| 启动反向抖动 | 启动电流方向与初始角度不匹配 | 查初始角度和旋向定义 |
这张表是我自己攒的,比任何手册都管用。调试的时候先按表上顺序排一遍,能省下大量时间。
6. CANopen 与 EtherCAT 接入:从上位机到电控的网络化
6.1 CANopen 对象字典与 PDO 映射实操
CANopen 这一层,电机控制最常用的对象就那几个。下面是我实际项目里用到的核心对象:
| 索引 | 子索引 | 名称 | 说明 |
|---|---|---|---|
| 0x6040 | 0 | Controlword | 控制字,含使能、启动、停机位 |
| 0x6041 | 0 | Statusword | 状态字,反馈当前状态机状态 |
| 0x6060 | 0 | Modes of operation | 运行模式,速度模式一般填 3 |
| 0x607A | 0 | Target position | 目标位置(位置模式用) |
| 0x60FF | 0 | Target velocity | 目标速度,单位按对象字典约定 |
| 0x606C | 0 | Actual velocity | 实际速度反馈 |
| 0x6077 | 0 | Actual torque | 实际转矩反馈 |
| 0x6061 | 0 | Modes of operation display | 模式回读 |
SDO 用来配置,一次一个对象,速度慢但可靠;PDO 用来传实时数据,一帧里可以带多个对象,速度快。西门子的老控制器和汇川的驱动器我都接过,PDO 映射的做法基本一致:先用 SDO 把0x1A00(TPDO1 映射)里的条目数量、索引、子索引、位长一格格写好,再设0x1800的传输类型和禁止时间,最后给0x1A00写 1 激活映射。
NMT 状态机是必须理解的:上电后进Pre-operational,这时候可以用 SDO 配置,但 PDO 不传输。发 NMT 命令切到Operational,PDO 才能通信。很多新手卡在"配置都对但数据不动",就是忘了切Operational。
/* NMT 启动远程节点,进入 Operational 状态 */ void NMT_Start_Node(uint8_t node_id) { uint8_t frame[2] = { 0x01, node_id }; /* 0x01 = Start Remote Node */ CAN_Send(0x000, frame, 2); /* NMT 报文 COB-ID 固定为 0 */ }心跳和节点守护的选择上,我倾向用心跳(Heartbeat),每个节点周期性上报自己的状态,主站超时判离线。节点守护要主站轮询,占用总线带宽,节点多了不划算。
6.2 EtherCAT 从站接入与主站周期设计
EtherCAT 的门槛比 CANopen 高一截。从站侧,现在多数方案是用 ESC 芯片(常见的 ET1100、LAN9252 之类)配一片 MCU,MCU 通过 SPI 或并口访问 ESC 的 DPRAM,把电机状态写进去、把控制命令读出来。整个过程是纯内存操作,MCU 不需要参与任何协议解析,这是 EtherCAT 能做到超短周期的根本原因。
从站软件要做的主要是两件事:定义 ESI 文件(描述这个从站有哪些过程数据对象、用什么邮箱协议),以及实现 PDO 数据到本地变量的映射。在 TwinCAT 或者 IgH Master 那边,导入 ESI 文件后直接拖出从站,PDO 就能自动生成。
主站侧如果用的是 Linux 平台,IgH EtherCAT Master 是绕不开的。RK3568 这类平台的接入要点,我整理了几条:
- 内核需要带原生 EtherCAT 网卡驱动支持(比如 igc 驱动),否则得用 generic 模式,性能和实时性都打折扣
- 内核要打 PREEMPT_RT 实时补丁,否则应用层的周期抖动可能到几百微秒
- 网卡必须独占,不能配 IP 地址,也不能被 NetworkManager 接管
- 主站周期配置要和电机电流环周期做整数比,比如主站 1ms、电流环 100us,10:1 的关系
主站周期的选择要看同步精度要求。1ms 周期配 DC 同步,多轴同步误差能做到 1 微秒以内;要更高精度就上 250us 甚至 125us,但对 CPU 和网卡的要求就上去了。我实测在 RK3568 上跑 500us 周期,CPU 占用大概 15%,还能接受。
6.3 控制环与通信环解耦的实时性设计
这是整个系统里我最看重的一点,也是新手最容易忽视的一点。电机电流环跑 10kHz,也就是 100us 一次;总线周期 1ms。两者频率相差 10 倍,绝不能把电流环塞进总线中断里。
正确的结构是:总线中断(或者主站周期回调)只做两件事——把最新的目标值写进一个双缓冲结构,把上一次的快照读走。电流环中断只管从这个结构里取值。中间用一个旗帜性变量或者无锁环形缓冲来同步。
typedef struct { float target_speed; float target_current_q; float ctrl_word; uint8_t update_flag; } Cmd_Buffer_t; /* 总线侧写入 */ void Bus_OnCycle(void) { g_cmd_shadow.target_speed = pdo.target_velocity; g_cmd_shadow.update_flag = 1; /* 交换缓冲,原子操作 */ __disable_irq(); memcpy(&g_cmd_active, &g_cmd_shadow, sizeof(Cmd_Buffer_t)); __enable_irq(); /* 回写反馈 */ pdo.actual_velocity = g_fb_snapshot.speed; pdo.actual_current = g_fb_snapshot.iq; } /* 电流环侧读取快照 */ void FOC_ISR(void) { if (g_cmd_active.update_flag) { g_spd_ref = g_cmd_active.target_speed; } g_fb_snapshot.speed = est_speed; g_fb_snapshot.iq = idq.q; /* 其余 FOC 计算 */ }这个设计还有一个隐含好处:总线丢了或者超时的时候,电流环可以继续按上一次的参考值运行,或者按预设的减速曲线停机,不会因为总线断了电机就失控。我在做 AGV 项目时,这条设计救过一次——上位机死机,电机按 200ms 的斜坡停下来了,没有急停导致车体侧翻。
7. 常见问题排查速查与避坑心得
7.1 上电抖动、飞车、堵转的排查顺序
这三个现象是调试无感 FOC 最高频的翻车姿势,我按处理优先级排个序。
上电就抖。先看是不是初始角度和启动电流方向不匹配,其次是电流零偏有没有标定。还有一个隐蔽的原因是 ADC 采样点乱了,三相电流采出来全是错的,观测器在一开始就估了个错角度。
飞车(转速失控冲高)。十有八九是观测器角度旋转方向反了,或者极对数不对,或者速度环的反馈符号搞反了。遇到飞车立刻断电,不要指望软件保护,很多情况下软件保护也跟不上。排查的方法是先不开环,用手转电机看观测器角度是正转还是反转,方向对了再往下查。
堵转(给力但不转)。常见于启动阶段,I/F 的启动电流不够、加速度太大,或者初始角度和实际转子位置差了 90 度电角度。我一般会把启动电流先调到额定的一半,加速度调到最慢,确认能转起来再往上加。
注意:无感 FOC 调试必须断电保护。我建议在母线上串一个可调限流电源,把限流设在额定电流的 1.5 倍,虽然会牺牲一点动态性能,但能把飞车、短路这些事故的破坏力压到最低。
7.2 观测器估不准的高频原因清单
调了半天角度还是不准的,对着这份清单过一遍:
- 电机参数(Rs、Ls、ψf)是不是实测的?填手册值大概率不准,尤其 Rs 随温度能变 30%。
- 母线电压有没有标定?分压电阻的误差加上 ADC 参考电压的误差,累积可能到 3%。
- 死区补偿做了吗?没做的话低速段别指望准确。
- 采样点对不对?用示波器同时看 PWM 和 ADC 触发信号,确认落在下桥臂全开窗口里。
- 滤波器的滞后补偿了吗?PLL 和前置低通都有滞后,转速越高越明显。
- 坐标变换的符号约定一致吗?这个最容易出玄学问题,Clarke、Park、反 Park 三处的约定必须统一。
第 6 条我踩过最惨的一次。换了套代码库,Clarke 变换的系数从 2/3 变成 sqrt(2/3),整个 dq 坐标系的幅值尺度全变了,PID 参数完全失效,调了一整天才发现是约定不一致。
7.3 总线丢帧与不同步的处理手法
CANopen 这边,丢帧一般看三处:总线速率和线长是否匹配、终端电阻有没有接、节点ID有没有冲突。1Mbps 的速率下总线长度不能超过 25 米,超了必须降速。终端电阻必须两端各一个 120 欧,中间节点不能接。
EtherCAT 这边,丢帧和工作计数器(Working Counter)直接相关,主站每周期检查 WKC,不符就报错。常见原因是网线质量、从站供电不稳、DC 同步没配好。我遇到过最诡异的一次是网线屏蔽层没接好,跑了三个小时才出现一次丢帧,换了带屏蔽的工业网线就好了。
不同步的表现是多轴插补的时候轨迹有台阶。处理办法是先确认 DC 是否使能,再检查主站和从站的时钟偏移量。IgH Master 下有专门的命令可以看从站时钟状态,偏移超过几百纳秒就得查网卡和从站的晶振。
# 查看 EtherCAT 主站和从站的时钟状态 ethercat master ethercat slaves -v ethercat dc_ref_clock这几个命令我调 EtherCAT 的时候几乎每次都用,-v输出里能看到每个从站的时钟偏移和同步状态,非常直观。
说说我自己调这套东西最实在的体会。算法本身其实没那么玄,滑模观测器的公式翻来覆去就那么几行,磁链观测器更是简单到只有积分。真正决定成败的是"地基"——电流零偏标没标、采样点对不对、死区补没补、参数是不是实测的。我见过太多人上来就调 PLL 带宽、调滑模增益,调了一个月没结果,最后发现是电流采样点偏了半个周期。所以我现在的顺序永远是:先把硬件链路验证到位,再动一行算法代码。
另外一个建议是,别一上来就把 CANopen 和 EtherCAT 一起接。先把单机的无感 FOC 调稳,转速范围、带载能力、切换平滑度都满意了,再接总线。总线接入本身不难,难的是出问题的时候你分不清是电控的问题还是通信的问题。分开调,能省下大量互相甩锅的时间。
最后分享一个我一直在用的小工具思路:把电流环里的i_alpha、i_beta、e_hat_alpha、e_hat_beta、估计角度、实际角度这六个量,按每个电流环周期往一块 RAM 缓冲里写,攒满一帧再通过串口或者网口传到 PC。有了这套离线数据,任何异常波形都能回放分析,比盯着示波器猜原因高效太多。这套方案我前后在三个项目里复用过,每次出问题都是靠它定位的。