简介:面向新能源汽车驱动电机软件开发与嵌入式控制学习者的旋变软解码MCU代码包,围绕旋变励磁信号生成与输出信号软件解码展开,适合需要理解位置反馈闭环、并在STM32平台上实践PWM励磁与ADC采样解码的开发者参考。包内共1269个文件,以724个C源文件、341个头文件为主,辅以97个汇编文件、73个文本说明及24个ICF链接配置等,覆盖底层驱动、外设初始化与工程构建配置,压缩包约7.1MB。已有1402人学习,说明该主题在电机控制方向具有较高关注度。代码基于STM32G4系列外设资源组织,涉及HRTIM、TIM、ADC、I2C、UART、SPI等模块,可用于梳理励磁波形参数设置、模拟信号采集、滤波解调与角度位置计算的实现路径,并观察工程目录划分和HAL库调用方式。对源码学习与问题排查较有帮助。
1. 伺服上电,角度先在原地跳了 ±3°
伺服驱动器上电做闭环,电机还没转,上位机的角度曲线先在原地跳了 ±3°。查了两天,不是编码器干扰,也不是地线没接好,问题出在旋变解码这条路上:励磁和采样各跑各的定时器,相位漂一点,解出来的包络就跟着飘,最后 atan2 出来的角度自然不稳。这类问题在低成本伺服、云台、机器人关节、电驱台架里反复出现,专用解码芯片能解决,但 BOM 成本和差分走线都上去了。这份旋变软解码 MCU 代码走的是另一条路:MCU 自己发 10 kHz 励磁,同一套定时器触发 sin/cos 双通道 ADC 采样,同步解调出包络,再用跟踪环算出角度和速度,全程不依赖外部解码芯片。适合已经上手电机控制、想把旋变这套信号链接进自己板子、并且愿意为一两个参数调半天的开发者。
2. 技术路线与整体架构:为什么把解码从芯片搬回 MCU
2.1 旋变的信号本质是两条被调制的正弦波
先把数学前提摆清楚,不然后面代码看不懂。旋变转子绕组上加一路高频励磁,常见是 10 kHz、2~4 Vrms 的正弦:
V_exc(t) = A · sin(ωt)定子的 sin / cos 两路绕组感应出来的信号,幅度被转子角度 θ 调制,载波还是那路励磁:
V_sin(t) = k · A · sin(ωt) · sin(θ) V_cos(t) = k · A · sin(ωt) · cos(θ)k 是变压比,多数旋变在 0.5 左右。注意这里的关键点:角度信息藏在包络的幅度比里,不在频率里。所以解码的本质是两步——先把包络从载波上剥下来,再对两个包络做反正切或跟踪。
剥包络的办法就是同步解调。把采样到的 V_sin 乘上同相的励磁参考 sin(ωt),用积化和差展开:
V_sin · sin(ωt) = kA·sin(θ)·sin²(ωt) = (kA/2)·sin(θ)·[1 - cos(2ωt)]结果是一个直流分量加一个二倍频分量。低通滤掉二倍频,剩下的 (kA/2)·sin(θ) 就是纯包络。cos 通道同理。这一步用模拟乘法器做就是硬件解调,用 ADC 采样后在代码里乘就是软解码,两条路的数学完全一样,差别只在实现位置。
提示:励磁参考的相位必须和实际加到旋变上的励磁对齐。中间隔了 DAC、功放、隔离变压器、RC 滤波,相位一定会偏,这个偏移量在 3.3 节和 4.3 节都会再遇到。
2.2 三条实现路线的取舍
做旋变解码的路子基本就三条,选哪条取决于你的成本红线、转速范围和板子空间。我在不同项目上三条都用过,下面这张表是踩过坑之后的实际体感,不是理论值。
| 维度 | 专用解码芯片 | 硬件解调 + ADC | 全软件同步解调(本项目) |
|---|---|---|---|
| BOM 成本 | 高,一颗芯片占大头 | 中,需要乘法器/模拟开关 + 运放 | 低,只吃 MCU 的 ADC 和算力 |
| 角度延迟 | 小,芯片内部硬件流水 | 中,取决于低通截止频率 | 可调,靠滤波器带宽和跟踪环带宽换 |
| 低速精度 | 好 | 好 | 好,前提是采样和励磁严格同步 |
| 高速表现 | 受芯片内部带宽限制 | 受模拟低通限制 | 受采样率和解调延迟限制,需要相位补偿 |
| 参数灵活性 | 低,靠外围电阻定 | 中 | 高,全在代码里改 |
| 抗干扰 | 好,差分输入做得完善 | 中 | 取决于布线和采样时刻 |
| 适合场景 | 批量大、要过认证、不想调算法 | 老平台改造、MCU 算力紧张 | 成本敏感、需要自定义带宽和诊断 |
选软解码最实际的动因往往不是省钱,而是可控。角度延迟、带宽、故障阈值、零点偏移全在代码里,换一款电机不用动硬件,调参数就行。代价是采样时刻、相位补偿、通道一致性这些细节全得自己兜住,兜不住就是玄学抖动。
2.3 代码的数据流与模块划分
整个工程按信号流切成五块,从定时器一路串到角度输出:
TIM 励磁定时器 ──> 正弦查表 ──> DAC/PWM ──> 功放 ──> 旋变 │ └── 同一定时器触发 ──> ADC 双通道(sin/cos)──> 偏置/增益标定 │ v 同步解调(乘参考 + 累加) │ v 包络 sinθ / cosθ │ v Type-II 跟踪环 ──> θ、ω关键在左半边那条虚线:励磁生成和 ADC 触发必须共用同一个定时器基准。这是整个方案能不能稳的地基。如果励磁走 TIM1、采样走 TIM3,两个定时器哪怕配置成同一个频率,晶振和分频的微小差异也会让相对相位慢慢漂移,表现出来的现象就是角度误差按拍频周期来回摆,非常像"干扰",但示波器上看信号干干净净。这类问题我后面第 5 章单独列了一条。
模块划分上,代码分成四层,方便移植:
bsp_resolver.c:定时器、DAC、ADC 的初始化和底层读写,换 MCU 只改这一层exc_gen.c:正弦表、相位累加器、励磁幅值控制demod.c:同步解调、低通/整周期累加、包络输出angle_track.c:跟踪环、角度归一化、速度估计、故障诊断
层与层之间只传结构体,不传全局变量。这个习惯在做多轴控制的时候特别值钱,一套代码能同时跑三路旋变而不用复制粘贴。
3. 核心模块拆解:从励磁波形到一个稳定的角度值
3.1 励磁生成:查表法 + 相位累加器,点数要对齐采样
励磁波形不用实时算 sin,太费时间,标准做法是查表。表长取 2 的整数次幂,相位累加器溢出的位就是查表索引,这样连取模都省了。
/* 励磁参数:10 kHz 励磁,每周期固定 64 个点,DAC 更新率 640 kHz */ #define EXC_TABLE_BITS 8 /* 256 点正弦表 */ #define EXC_POINTS_PER_CYCLE 64 /* 一个励磁周期采/发 64 点 */ #define EXC_PHASE_STEP ((1u << EXC_TABLE_BITS) / EXC_POINTS_PER_CYCLE) static const int16_t sin_table[1u << EXC_TABLE_BITS] = { /* 生成好的 256 点 */ }; static uint16_t exc_phase; /* 相位累加器,0~255 循环 */ static uint16_t exc_amp = 2048; /* 励磁幅值,对应 DAC 中点偏置 */ /* 定时器中断里调用,频率 = 励磁频率 × EXC_POINTS_PER_CYCLE */ void exc_isr(void) { uint16_t idx = (exc_phase >> 0) & ((1u << EXC_TABLE_BITS) - 1); int32_t v = (int32_t)sin_table[idx] * exc_amp >> 12; dac_write(DAC_CH_EXC, (uint16_t)(2048 + v)); /* 12 位 DAC,中点 2048 */ exc_phase += EXC_PHASE_STEP; }这段代码有三个参数值得拎出来说。
EXC_POINTS_PER_CYCLE决定了一个励磁周期被切成多少份,也决定了 ADC 的采样节奏。取 64 意味着 DAC 更新率 640 kHz,对多数电机控制 MCU 的 DAC 来说偏高,实际更常见的是 32 点(320 kHz)或者把励磁降到 5 kHz。点数越多,整周期累加的抑制效果越好,但对 DAC 和 ADC 的吞吐要求也越高,得按你手上芯片的规格倒推。
exc_amp是在线可调的。这个变量存在的意义是标定:不同旋变的变压比、不同板子的功放增益都不一样,靠改电阻太笨,直接在代码里乘一个系数最省事。调到输出包络峰值占 ADC 量程的 60%~70% 比较合适,太低了信噪比差,太高了削顶。
exc_phase必须和 ADC 触发严格同源。常见做法是不用两个定时器,而是让这个定时器在更新 DAC 的同时,用同一个计数器的比较事件去触发 ADC。这样从物理上保证了每个励磁周期的第 n 个点,采样点的相位永远一致,不会漂。
3.2 ADC 同步采样与通道标定
ADC 这边要采两路差分信号,sin 和 cos。差分转单端在片上运放或者外部运放完成,代码只需要处理偏置和增益。
typedef struct { int32_t offset; /* 通道偏置,ADC 码值 */ int32_t gain_q15; /* 增益归一化系数,Q15 */ } chan_cal_t; static chan_cal_t cal_sin = { 2048, 32768 }; static chan_cal_t cal_cos = { 2048, 32768 }; /* 在励磁周期固定相位点被 ADC 转换完成中断调用 */ void adc_isr(void) { int32_t raw_s = adc_read(ADC_CH_SIN); int32_t raw_c = adc_read(ADC_CH_COS); /* 去偏置 + 增益归一化,输出范围约 ±2048 */ int32_t s = ((raw_s - cal_sin.offset) * cal_sin.gain_q15) >> 15; int32_t c = ((raw_c - cal_cos.offset) * cal_cos.gain_q15) >> 15; demod_push(s, c, exc_phase); /* 连同当前励磁相位一起送进解调器 */ }offset是零位偏置。旋变的 sin/cos 差分输出理论上零点在电源中点,但运放的失调、ADC 的零点误差会让实际零点偏几十个码值。这几十个码值在 atan2 里会变成固定角度误差,低速时尤其明显——电机指令给 0 速,实测一直在 ±0.5° 来回蹭,很多"驱动器抖动"的锅其实是这个偏置没标。
gain_q15是两路增益失配的补偿。旋变两路绕组的匝数、运放电阻的精度都不可能完全一致,增益差 1% 就意味着角度误差里有二次谐波成分,误差曲线一个电周期内波动两次。这个系数靠 4.3 节的标定流程测出来,不用手工算。
把exc_phase一起送进解调器,是为了让解调用的参考相位和发出去的波形对齐。如果你用的是半周期积分那种方案,这一步可以省,但用点乘解调就必须带上。
3.3 同步解调:整周期累加比 IIR 低通更稳
解调有两种写法,我都写过,最后基本都换成了整周期累加。
第一种是逐点乘参考再走低通:
/* 逐点解调 + 一阶 IIR 低通,截止频率 = 励磁频率的 1/10 左右 */ static int32_t lp_s, lp_c; void demod_point(int32_t s, int32_t c, uint16_t phase) { int32_t ref = sin_table[(phase >> 0) & 0xFF]; /* 与励磁同相的参考 */ int32_t d_s = (s * ref) >> 12; /* 乘参考,含二倍频 */ int32_t d_c = (c * ref) >> 12; /* 一阶低通,alpha 越小越平滑、延迟越大 */ lp_s += ((d_s - lp_s) * DEMOD_ALPHA) >> 8; lp_c += ((d_c - lp_c) * DEMOD_ALPHA) >> 8; }DEMOD_ALPHA是低通系数。这个参数调起来很别扭:取小了,二倍频滤不干净,包络上叠着 20 kHz 的纹波,跟踪环跟着抖;取大了,包络的相位延迟上去了,转速一高角度就滞后。而且一阶 IIR 对二倍频的抑制只有 -20 dB/dec,想滤干净就得把截止频率压得很低,延迟根本受不了。
所以我一般用第二种——整周期累加。原理很简单:把一整个励磁周期的采样值乘上对应的参考值再累加,二倍频分量在整周期上积分正好为零,天然不需要滤波器。
/* 整周期同步解调:N 个点累加完输出一次包络 */ #define DEMOD_N 64 /* 必须等于每周期采样点数 */ static int32_t acc_s, acc_c; static uint16_t acc_cnt; void demod_push(int32_t s, int32_t c, uint16_t phase) { int32_t ref = sin_table[(phase >> 0) & 0xFF]; acc_s += (s * ref) >> 12; acc_c += (c * ref) >> 12; acc_cnt++; if (acc_cnt >= DEMOD_N) { /* 整周期到,输出包络 */ int32_t env_s = acc_s / (DEMOD_N / 2); /* 归一化,恢复幅值量纲 */ int32_t env_c = acc_c / (DEMOD_N / 2); angle_track_update(env_s, env_c); /* 送跟踪环 */ acc_s = acc_c = 0; acc_cnt = 0; } }DEMOD_N必须严格等于一个励磁周期内的采样点数,这是整个算法成立的前提。如果采样率和励磁频率不是整数倍关系,累加窗口里就凑不齐整周期,二倍频残留会直接漏到包络上,表现为角度误差按差频周期波动。这也是为什么 3.1 节强调采样触发要和励磁同源——不同源就保证不了这个整数关系。
env_s / (DEMOD_N / 2)这个归一化系数是从前面的推导来的:点乘之后直流分量是幅值的一半,再乘点数,所以要除以 N/2 才能还原成原始幅值量纲。写成右移也行,但 N/2 不一定是 2 的幂,编译器优化不了,除法在这里一个周期只执行一次,开销可以忽略。
整周期累加还有一个附带好处:它天然是一个矩形窗 FIR,对励磁频率整数倍的干扰有很强的抑制,工频和开关频率的耦合只要不在整数倍上,衰减都很可观。代价是输出率等于励磁频率(10 kHz 励磁就是 10 kHz 更新一次包络),对电流环 10~20 kHz 的控制周期来说够用,再快就得上更高的励磁频率。
3.4 角度跟踪环:从包络到 θ 和 ω
拿到 sinθ、cosθ 两个包络之后,最直接的办法是atan2(env_s, env_c)。能出结果,但我一般不这么用,原因有两个:一是 atan2 在包络过零附近对噪声极其敏感,噪声被放大成角度尖峰;二是速度只能靠角度差分,10 kHz 的更新率下差分噪声很大,低速时速度环基本没法用。
标准做法是 Type-II 跟踪环,本质是一个二阶锁相环:
typedef struct { int32_t theta; /* 估计角度,Q15 定点,满量程 = 2π */ int32_t omega; /* 估计速度,每个控制周期的角度增量 */ int32_t kp; /* 比例增益 */ int32_t ki; /* 积分增益 */ } pll_t; static pll_t pll; void angle_track_update(int32_t env_s, int32_t env_c) { /* 叉积求相位误差:sin(θ-θ̂) ≈ θ-θ̂,小误差下线性 */ int32_t sin_hat = sin_q15(pll.theta); int32_t cos_hat = cos_q15(pll.theta); int32_t err = ((env_s * cos_hat) - (env_c * sin_hat)) >> 15; /* 二阶环:积分项出速度,速度再积分出角度 */ pll.omega += (err * pll.ki) >> 15; pll.theta += pll.omega + ((err * pll.kp) >> 15); /* 角度归一化到 [0, 2π),用掩码实现,避免取模 */ pll.theta &= 0x7FFF; }err这一行是整个环的核心。用叉积而不是直接减角度,是因为它天然带 sin 特性,大误差时自动限幅,不会像直接相减那样在跨零时产生 2π 的跳变。误差小的时候 sin(Δθ) ≈ Δθ,环就退化成一个线性二阶系统,好分析也好调。
kp和ki决定环的带宽和阻尼。有个经验公式:先定带宽 ω_b(单位 rad/s),再取 kp ≈ 2ζω_b·T、ki ≈ ω_b²·T²,T 是包络更新周期,ζ 取 0.707。实际调的时候我一般从 200 Hz 带宽起步,看阶跃响应有没有过冲,再往上加。带宽越高动态越好,但包络上的残余纹波会直接进速度输出,低速时速度反馈会毛。伺服应用里 100~300 Hz 是常见区间。
pll.theta &= 0x7FFF这个掩码要求角度用 15 位表示满量程 2π,也就是 32768 对应 360°。定点的好处是三角函数查表和归一化都变成移位,坏处是所有增益系数都要按 Q15 换算,写的时候容易把小数点位置搞错。我建议在代码里统一约定:凡是角度都用 int32 存 Q15,凡是增益都用 Q15,凡是幅值都用 ADC 码值,三层量纲不要混。
速度输出pll.omega的单位是"每包络周期转过的 Q15 角度",要换算成 rpm 得乘上更新率和 60/(2π),再除以电机极对数。这个换算建议单独写一个函数,别散在业务代码里,否则改励磁频率的时候会漏改。
4. 跑起来:工程结构、参数配置与上电标定
4.1 工程目录与编译入口
工程按可移植的思路分层,目录结构大致这样:
resolver_sw_decode/ ├── bsp/ │ ├── bsp_resolver.c # 定时器/DAC/ADC 初始化与读写 │ ├── bsp_resolver.h │ └── board_cfg.h # 引脚、通道、时钟配置 ├── algo/ │ ├── exc_gen.c # 正弦表、相位累加器 │ ├── demod.c # 同步解调与整周期累加 │ ├── angle_track.c # Type-II 跟踪环 │ └── resolver_cal.c # 偏置/增益/相位标定 ├── app/ │ ├── main.c # 初始化、主循环、上位机通信 │ └── shell_cmd.c # 参数在线修改与波形输出 └── tools/ ├── sin_table_gen.py # 生成正弦表头文件 └── cal_collect.py # 采集标定数据、拟合补偿系数编译入口只有一个main.c,上电顺序有讲究,不能乱:
int main(void) { clock_init(); /* 先定系统时钟,后面所有分频都依赖它 */ bsp_gpio_init(); exc_gen_init(); /* 建正弦表、复位相位累加器 */ bsp_resolver_init(); /* 定时器/DAC/ADC,采样触发与励磁同源 */ angle_track_init(200.0f); /* 跟踪环带宽 200 Hz */ resolver_cal_load(); /* 从 Flash 读上次标定的偏置和增益 */ bsp_timer_start(); /* 一旦启动,励磁和采样同时跑起来 */ while (1) { shell_poll(); /* 上位机命令:读角度、改参数、触发标定 */ fault_check(); /* 包络幅值、超速、断线诊断 */ } }顺序里最容易出错的是resolver_cal_load()的位置。它必须在定时器启动之前完成,否则前几个周期的解调用的是默认系数,跟踪环会先被带偏,虽然之后能收敛回来,但如果你的故障检测在启动瞬间就使能,会直接误报一次故障。我在一个项目上就因为这个顺序问题,上电偶尔报"旋变信号异常",复位又好了,查了很久。
sin_table_gen.py这个工具别省。正弦表手工写容易错,而且不同项目可能需要不同的表长和幅值,用脚本生成头文件,一行命令搞定,比复制粘贴靠谱。
4.2 参数配置表:一次改对这几个数
软解码调不起来,八成是这几个参数里有一个不对。下面这张表是我每次上新板子必对的清单:
| 参数 | 典型值 | 改动影响 | 出错现象 |
|---|---|---|---|
| 励磁频率 | 10 kHz | 越高动态越好,但对 DAC/ADC 要求越高 | 太低导致包络更新慢,电流环跟不上 |
| 每周期点数 | 32 / 64 | 决定累加窗口和采样率 | 与采样率不匹配时二倍频残留,角度周期性波动 |
| 励磁幅值 | 量程的 60%~70% | 影响信噪比 | 太低噪声大,太高削顶后角度畸变 |
| 跟踪环带宽 | 100~300 Hz | 越高动态越好、噪声越大 | 太高速度毛刺,太低高速相位滞后 |
| 阻尼系数 | 0.707 | 影响阶跃响应过冲 | 太小过冲振荡,太大响应迟钝 |
| 相位补偿 | 1~3 个采样点 | 补偿模拟通路延迟 | 未补偿时角度误差随转速线性增大 |
| 电机极对数 | 按电机填 | 机械角/电角度换算 | 填错导致换相完全错乱,电机堵转 |
| 初始角偏移 | 标定得出 | 电角度零点对齐 | 未标定时电机可能反转或启动无力 |
相位补偿这一项值得单独说。从 DAC 输出到 ADC 采样,信号穿过了功放、可能还有隔离变压器、RC 抗混叠滤波,每一级都有群延迟。10 kHz 下,一个 2 kHz 截止的 RC 滤波器就能贡献十几度的相位偏移。如果不补,解调出来的包络不是纯 sinθ 和 cosθ,而是带固定相移的混合,等价于角度上叠了一个随转速变化的误差。补偿的办法很简单,在解调时给参考相位加一个固定偏移:
/* 相位补偿:根据实测标定值调整参考相位 */ #define PHASE_COMP 3 /* 补偿 3 个采样点,按实测填 */ void demod_push(int32_t s, int32_t c, uint16_t phase) { uint16_t ref_phase = phase + PHASE_COMP * EXC_PHASE_STEP; int32_t ref = sin_table[(ref_phase >> 0) & 0xFF]; /* ... 后续累加逻辑同 3.3 节 ... */ }PHASE_COMP的单位是"采样点",不是度,这样换励磁频率的时候不用重算。标定方法在 4.3 节。
4.3 上电标定:偏置、增益、相位延迟三步走
标定是软解码能不能达到标称精度的分水岭。工厂里用转台当然最准,但多数人手上没有,我用的是"手动匀速转 + 数据拟合"这套土办法,精度够用。
第一步标偏置。让电机静止在任意位置,采集 1000 个包络点求平均。理论上静止时包络是常数,平均值就是偏置和真实包络的叠加,所以要转几个不同位置分别采,取最小值作为偏置估计——因为包络是正弦,最小值出现在信号过零附近,此时读数基本就是纯偏置。更稳的做法是转一整圈,对每个通道的数据做正弦拟合,拟合出的直流分量就是偏置。
第二步标增益。同样转一整圈,对 sin 和 cos 的包络分别求峰峰值。理想情况下两路峰峰值应该相等(都等于 kA),实际会有偏差。把 cos 通道的增益系数设成sin_pp / cos_pp(Q15 换算),让两路幅值对齐。这一步做完,角度误差里的二次谐波应该明显下去。
第三步标相位。这一步最容易被跳过,但对高速精度影响最大。方法是用外部设备拖动电机匀速转,记录角度误差随转速的变化曲线。如果误差和转速成正比,就是相位补偿不够,逐步加大PHASE_COMP直到误差曲线在正反转下基本对称。没有拖动设备的话,用驱动器自己开速度环,让电机在 10%~30% 额定转速下正反转各跑一遍,看角度误差的符号,也能粗调。
注意:三步标定的顺序不能换。偏置没标就标增益,拟合出来的峰峰值是偏的;增益没对齐就标相位,误差曲线会被二次谐波污染,根本看不出线性趋势。
标定结果存 Flash,上电加载。resolver_cal.c里建议加一个版本号和 CRC,防止 Flash 擦写异常导致加载了半截数据——这种问题现象是"昨天还好好的,今天角度全乱了",非常难查。
4.4 联调验证:三个动作判断解调链路是否正常
代码跑起来之后,按下面三个动作验证,基本能定位问题在哪一层。
第一个动作:不接电机,只看励磁输出。示波器测 DAC 输出和功放输出,确认频率、幅值、波形失真度。如果功放输出削顶或者有明显交越失真,后面的解调全是白搭。这一步花五分钟,能省掉后面几小时的排查。
第二个动作:手转电机,看包络。把env_s和env_c通过 DA 或者串口打到上位机,手动匀速转一圈。正常现象是两路包络都是干净的正弦,相位差 90°,幅值基本相等,转一圈正好一个周期。如果包络上有高频毛刺,查采样时刻和 ADC 配置;如果两路幅值差很多,回 4.3 节标增益;如果包络不是纯正弦而是有平顶,查励磁幅值是不是太大导致削顶。
第三个动作:看角度线性度。把解算出的角度和一路参考(比如电机自带的增量编码器,或者干脆用手转 + 分度头)对比,画误差曲线。理想的误差曲线是一条平直的线,实际会看到一次、二次谐波成分。一次谐波主要来自初始角偏移和相位补偿,二次谐波主要来自两路增益失配和偏置。这条曲线的形状就是你的调参指南针。
5. 避坑与排查:软解码最容易翻车的五个点
5.1 角度低频摆动,周期几秒到几十秒
现象:电机静止或者匀速运行时,角度读数以一个很慢的周期来回摆,幅度可能有 1°~3°,示波器上看 sin/cos 信号干干净净,找不到干扰源。
原因:励磁和采样走了两个独立定时器。哪怕两个定时器配成同一频率,只要时钟源分频后有微小差异,相对相位就会持续漂移,漂一圈的时间就是拍频周期。这时候解调参考和实际载波的相位差在变,解出来的包络幅值就跟着变。
解决:把 ADC 触发改成由励磁定时器的比较事件产生,物理上同源。如果硬件上确实做不到(比如 ADC 触发源被别的外设占了),退而求其次,用同一个定时器的不同通道,或者定期用励磁过零点重新同步采样相位累加器。
5.2 转速越高角度误差越大,正反转不对称
现象:低速时角度挺准,一到中高速角度就滞后,而且正转和反转的误差方向不一样。
原因:模拟通路的相位延迟没补偿。DAC、功放、隔离、RC 滤波加起来有固定群延迟,转速越高,同样的时间延迟折算成的角度误差越大(误差 ≈ 转速 × 延迟)。正反转时误差符号相反,所以不对称。
解决:按 4.3 节第三步标PHASE_COMP。补到正反转误差曲线基本对称就行。如果补偿后高速还是差,检查跟踪环带宽是不是太低,环本身的相位滞后也会叠加进来。
5.3 角度在特定扇区跳变,误差曲线有明显二次谐波
现象:角度整体是线性的,但在某些角度附近会突然跳一下,误差曲线一个电周期内波动两次。
原因:sin 和 cos 两路增益不一致,或者其中一路有残余偏置。增益失配会让包络的圆变成椭圆,atan2 或者跟踪环在椭圆上解出来的角度自然带二次谐波;残余偏置会把圆心挪走,误差曲线出现一次和二次的混合。
解决:重做 4.3 节的第一步和第二步。偏置用整圈正弦拟合的直流分量,别用单点采样;增益用峰峰值比。标完之后再看误差曲线,二次谐波应该降到 0.1° 以内。
5.4 上电角度随机,电机启动时抖动或者反转
现象:每次上电,读到的电角度都不一样,闭环启动时电机抖一下甚至反转,开环强拖能转起来,切闭环就炸。
原因:初始角偏移没标定,或者极对数配错了。旋变本身是机械角,电机控制要的是电角度,两者差一个极对数倍率和初始偏移。另外还有一种可能是上电时跟踪环还没收敛就使能了闭环。
解决:先确认极对数填对,再用"给 U 相通直流、转子定位到已知角度"的办法标初始偏移。软件上,上电后先让跟踪环空跑 50~100 ms,等pll.omega稳定在零附近再使能闭环。
5.5 角度噪声大,电机有高频啸叫
现象:角度读数上叠着明显的高频噪声,电流波形毛刺多,电机有啸叫,尤其在低速轻载时明显。
原因:ADC 采样时刻撞上了功率管的开关沿。软解码的采样是周期性的,如果采样点正好落在 PWM 开关瞬间,共模干扰会直接进 ADC,差分也压不住。
解决:把 ADC 触发时刻挪到 PWM 周期的中点,避开开关沿;硬件上检查差分走线是否等长、共模电容是否合理;软件上可以在解调前加一级限幅或者中值滤波,把明显的野值剔掉。如果这些都不行,考虑在开关频率上做同步,让采样率和 PWM 频率成整数倍关系。
6. 二次开发:把这套解码改成你自己平台的模块
移植这套代码,最忌讳的是直接改bsp_resolver.c里的寄存器。我的做法是先抽一层薄接口,把硬件相关的东西全收口到五个函数里:
/* 移植层:换平台只实现这五个函数 */ typedef struct { void (*dac_write)(uint16_t ch, uint16_t val); uint16_t (*adc_read)(uint16_t ch); void (*timer_start)(void); uint32_t (*get_tick_us)(void); void (*cal_save)(const void *buf, uint32_t len); } resolver_port_t;这五个函数之外的所有代码——解调、跟踪环、标定、诊断——都是纯算法,不碰寄存器,可以直接复用。get_tick_us看着多余,其实是给故障诊断用的:幅值异常要判断持续了多久才报故障,没有时间基准就只能数中断次数,换平台又得改。
移植完之后,建议做两件事。
第一件是加诊断。软解码相比硬件解码芯片,最大的短板是没有现成的故障标志位,得自己判。至少加三条:包络幅值低于阈值(旋变断线或者励磁丢了)、包络幅值超上限(短路或者增益异常)、角度跳变率超限(跟踪环失锁)。每条诊断都要有确认计数和恢复计数,别用单次采样就报故障,否则电机一震动就报警。
第二件是用信号注入做验证。这套代码最大的好处是解耦——解调器和跟踪环的输入是两个 int32。你可以写个测试函数,直接给angle_track_update喂理想的正弦包络,扫一遍 0~360°,看输出角度误差。这一步能把算法问题和硬件问题彻底分开:注入测试过了,说明代码没问题,剩下的都是板子和信号链的事;注入测试不过,说明参数或者定点换算写错了,别去动硬件。我在一个项目上就是靠这个测试发现kp的 Q15 换算少移了一位,环带宽实际是设想的 32 倍,难怪一直抖。
还有一个小技巧:把励磁频率、采样点数、跟踪环带宽、极对数这几个参数集中到一个resolver_cfg.h里,用宏定义,别散落在各个 .c 文件。换电机的时候只改这一个文件,编译一次就能验证。我吃过亏,有次改励磁频率漏改了速度换算里的常数,电机跑起来转速显示只有实际的一半,现场调了半天才想起来。
从那以后我每次上电调试都强制走一遍流程:先看励磁波形,再看包络正弦度,最后看角度线性度,三步都过了才敢使能闭环。希望帮到你。
本文还有配套的精品资源,点击获取