☰
基于MCU的旋变软解码:PWM励磁、ADC同步采样与跟踪环实现
2026/10/26 4:30:40 网站建设 项目流程

简介:这是一份面向新能源汽车驱动电机软件开发者的旋变软解码MCU工程代码包,围绕旋变励磁信号生成与输出信号软解码展开,适合嵌入式电机控制工程师、STM32开发者及相关课题学习者参考。压缩包共1269个文件,约7.1MB,以724个C源文件和341个头文件为主体,另含97个汇编启动文件、73个说明文本、24个链接脚本及工程配置与少量C++文件,覆盖底层驱动、外设初始化、算法实现和编译链接等模块。已有1402人学习,说明其在旋变软解码方向具有一定参考热度。代码基于STM32G4系列平台,涉及HRTIM、TIM、ADC、SPI、I2C等外设驱动和HAL库组件,可帮助读者梳理励磁PWM配置、ADC采样、滤波解调、位置信息提取及电机闭环控制之间的对应关系,也便于按工程结构移植、裁剪和调试,理解软解码相较硬解码的灵活配置方式,对学习旋变励磁波形、采样时序和解码算法实现具有较好参考价值。

1. 旋变软解码头一件事:把解码芯片从 BOM 上删掉

某开发者接了个小功率伺服驱动器的活,图纸画到一半发现,那颗专用旋变解码芯片交期排到半年后,单价还顶得上半块主控。他索性把芯片删了:励磁用 MCU 的 PWM 打出来,旋变的正余弦反馈进 ADC,角度和转速全在中断里算。这套东西就是旋变软解码——省掉解码芯片和它周边一圈外围器件,代价是把时序、噪声、相位这些活全揽到软件身上。

这份 MCU 代码干的就是这件事:一路定时器输出 10 kHz 正弦励磁,两路 ADC 与载波严格同步采样,中断里先做同步解调把载波剥掉,再用一个二阶跟踪环把角度和转速一起解出来,直接喂给 FOC。适合手上已经有 Cortex-M4 级别主控、BOM 卡得紧、又想自己把控解算环节的人;也适合原本用解码芯片、但一直没搞明白角度到底怎么算出来的工程师。

2. 整体框架:励磁、采样、解算三条链路怎么串

软解码不是"读两个 ADC 算个 atan2"这么简单。旋变输出的是被励磁载波调制的信号,正余弦两路的瞬时值一直在跟着 10 kHz 正弦上下摆,你要的是那个摆动的包络。所以整个工程必须解决三件事:励磁要干净、采样要和励磁同相位、解算要把载波相关掉。这三件事对应三条链路,任何一条的时序对不上,角度就是错的。

我一般会把这三条链路全部挂到同一个定时器家族上,让硬件去保证相位关系,软件只负责算。这是软解压能落地的前提,也是它比想象中好调的原因——你不需要在代码里追时序,时序是硬件刻出来的。

2.1 三条链路的职责与共用时基

励磁链路负责产生幅度恒定、频率恒定的正弦电压,加到旋变初级。定子两相正交绕组感应出幅度按 sinθ、cosθ 调制的信号,这是旋变的物理特性。采样链路在励磁波形的固定相位点上把这两路抓回来,转成数字量。解算链路拿一串采样点,先做同步解调得到 Vs、Vc 两个直流量,再用跟踪环解出角度和角速度。

关键在于共用时基。励磁 PWM 的载波周期是 6.25 μs,一个励磁周期 16 个载波周期,正好 100 μs。ADC 的触发源直接选励磁定时器的比较事件,采样点永远落在载波周期的同一个位置,不会因为中断延迟漂移。这一点如果用软件定时器去触发 ADC,抖动轻松上到几百纳秒,低速时角度噪声立刻显形。

参数取值说明
主控主频168 MHzCortex-M4 级别,带 FPU 更省事
PWM 载波160 kHz中心对齐,ARR = 524
励磁频率10 kHz载波 / 16 点
每周期点数16正弦表长度
解算率10 kHz每个励磁周期解算一次
ADC12 bit,双路同步由定时器比较事件触发
有效角度分辨率实测 11~12 bit受信噪比限制,不是理论值

2.2 工程目录:模块怎么切

这种带模拟前端的工程,最怕的是把所有东西塞进一个 main.c。我习惯按"驱动 / 算法 / 应用 / 配置"四层切,配置和标定参数单独放一个头文件,标定完直接改常量,不用满工程找。

resolver_fw/ ├── app/ │ ├── main.c 上电初始化、慢速任务调度 │ ├── motor_ctrl.c FOC 主环,消费 g_elec_angle / g_elec_speed │ └── diag.c 断线、幅值窗口、超速诊断 ├── drv/ │ ├── exc_pwm.c 励磁 PWM 配置 + DMA 循环搬运 │ ├── adc_sync.c 与励磁同步的 ADC 触发与读取 │ └── uart_dbg.c 内部变量上报,调参时用 ├── algo/ │ ├── resolver.c 同步解调 + 跟踪环 │ ├── calib.c 偏置、增益、相位标定 │ └── trig_lut.c sin/cos 查表,替掉 sinf/cosf ├── cfg/ │ ├── resolver_cfg.h 极对数、励磁频率、环参数 │ └── exc_tbl.c 正弦表,由脚本生成,不要手改 └── tools/ └── gen_exc_table.py 正弦表生成脚本

exc_tbl.c一定要标成"生成文件"。我见过有人手改了几个数想让波形好看点,结果表长对不上 16 点,解调参考相位整个错位,查了两天才发现。

2.3 一快一慢两个节拍:中断里到底放什么

ADC 转换完成中断跑在 160 kHz,这个频率下每进一次中断只做最少的事:读两个结果、去偏置、乘累加、下标加一。累加到 16 点才真正调用一次解算,等于解算率 10 kHz。这个分层很重要,160 kHz 的中断里塞浮点运算,主频再高也扛不住。

/* 160 kHz:只做搬运和乘累加,全部整数运算 */ void ADC_IRQHandler(void) { int32_t s = (int32_t)ADC_SIN_RESULT - g_sin_ofs; int32_t c = (int32_t)ADC_COS_RESULT - g_cos_ofs; uint8_t k = g_phase_idx; /* 0..15,与正弦表下标严格对应 */ g_acc_s += s * g_sin_ref[k]; /* 与参考正弦相关 */ g_acc_c += c * g_cos_ref[k]; /* 与参考余弦相关 */ if (++g_phase_idx >= EXC_POINTS) { /* 凑满一个励磁周期 */ g_phase_idx = 0; g_vs = g_acc_s >> 4; /* 16 点平均,等价除以 16 */ g_vc = g_acc_c >> 4; g_acc_s = g_acc_c = 0; resolver_solve(g_vs, g_vc); /* 10 kHz 进跟踪环 */ } }

s * g_sin_ref[k]里,g_sin_ref是一张与励磁表同源的正弦表,取值放大到 Q12 定点,避免整数乘法溢出。>> 4是 16 点累加后的归一化。这里没有做sqrt,因为后面跟踪环只用Vs、Vc的比值关系,幅值信息单独走一路慢速 AGC 处理。

慢速任务放在主循环里,1 ms 跑一次:更新幅值估计、跑诊断、把内部变量打包上报。调试期这路上报就是后悔药,出问题能回放波形,比拿示波器一帧帧抓强太多。

3. 励磁信号:从一张占空比表到干净的正弦

励磁是整个软解码的地基。励磁不干净,后面算法再花哨也白搭。很多人在这一步翻车,不是 PWM 配错,是三个数的关系没算清楚:主频、载波频率、励磁频率。它们之间不是独立可调的,载波频率决定了每个励磁周期能塞多少个点,点数决定了谐波含量,谐波含量决定了你那级 RC 滤波要多狠。

3.1 三个数的算术:载波、点数、励磁频率

中心对齐模式下,PWM 周期是计数器从 0 走到 ARR 再走回来,所以f_pwm = f_clk / (2 * ARR)。以 168 MHz、ARR = 524 算,载波大约 160 kHz。要让励磁是 10 kHz,每个励磁周期正好 16 个载波周期,也就是正弦表 16 个点。

16 个点够不够?对旋变这种感性负载来说够用。旋变初级的电感本身就是一级低通,加上功放后级的 RC,160 kHz 的载波残余会被压到毫伏级。如果功放级允许跑更高载波,把点数提到 32、载波提到 320 kHz,总谐波失真会明显好一截,但开关损耗和驱动芯片的要求也跟着上去了。我一般先用 16 点验证通路,确认解算没问题再考虑提点数。

提示:16 点正弦的 THD 大约在 5% 量级,看起来很差,但旋变的解调只关心基波分量,谐波在同步解调里会被自然滤掉。前提是采样点和表点严格一一对应,错一个点,谐波就会混进解调结果。

3.2 正弦表怎么生成:脚本算,别手写

表一定要用脚本生成,而且要能一眼看出参数改了会影响什么。下面这段 Python 直接把 C 数组打出来,改N和MOD就能重新生成。

import math N = 16 # 每个励磁周期的 PWM 点数 ARR = 524 # 中心对齐:f_pwm = f_clk / (2 * ARR) MOD = 0.42 # 调制比,给功放和 RC 滤波留余量 MID = ARR // 2 vals = [] for i in range(N): s = math.sin(2.0 * math.pi * i / N) vals.append(int(MID + MID * MOD * s)) # 占空比围绕 50% 上下摆 print("const uint16_t g_exc_tbl[%d] = {" % N) for i in range(0, N, 8): print(" " + ", ".join("%3d" % v for v in vals[i:i + 8]) + ",") print("};")

MID是 50% 占空比对应的比较值,MOD决定摆幅。为什么不用 1.0?因为功放级是单电源供电,占空比贴近 0 或 100% 时,MOS 的导通压降和死区会把波形削顶,反而引入偶次谐波。留到 0.42 左右,波形顶底都还有余量。

生成出来的数组丢进exc_tbl.c,注意类型是uint16_t,DMA 搬运按半字宽配。有人图省事用uint8_t,ARR 一旦超过 255 直接截断,波形变成方波还找不到原因。

3.3 定时器配置与 DMA 循环搬运

表生成好之后,要让硬件自己把它一个个喂给比较寄存器。做法是把定时器配成中心对齐,更新事件触发 DMA,DMA 循环模式把整张表反复搬进 CCR。

void exc_pwm_init(void) { EXC_RCC_ENABLE(); EXC_TIM->PSC = 0; /* 不分频,168 MHz 直出 */ EXC_TIM->ARR = 524; /* 载波 ≈ 160 kHz */ EXC_TIM->CCR1 = 262; /* 上电先给 50%,无直流冲击 */ EXC_TIM->CCMR1 = (6u << 4) | (1u << 3); /* PWM 模式 1 + 预装载使能 */ EXC_TIM->CCER = 1u; /* 打开 CH1 输出 */ EXC_TIM->CR1 |= (1u << 5); /* 中心对齐模式 1 */ EXC_TIM->DIER |= (1u << 8); /* 更新事件请求 DMA */ EXC_TIM->CR1 |= 1u; /* 启动计数器 */ } void exc_dma_start(void) { DMA_STREAM->CR = 0; DMA_STREAM->PAR = (uint32_t)&EXC_TIM->CCR1; /* 目标:比较寄存器 */ DMA_STREAM->M0AR = (uint32_t)g_exc_tbl; /* 源:正弦表 */ DMA_STREAM->NDTR = EXC_POINTS; /* 16 个半字 */ DMA_STREAM->CR = (1u << 8) /* 循环模式,搬完自动回头 */ | (1u << 10) /* 内存地址自增 */ | (1u << 13) /* 内存到外设,方向位按手册核对 */ | 1u; /* 使能 */ }

两个细节必须确认。第一,CCMR1里的预装载使能位一定要开,DMA 写的是影子寄存器,下一个更新事件才生效,波形才连续;如果关掉预装载,DMA 在一次 PWM 周期中途写进去,那个周期会出现毛刺。第二,方向位各家的定义不一样,写之前翻一眼手册,方向配反了现象是 DMA 传输错误标志置位,但波形一动不动。

3.4 幅值在线调整与软启动

励磁幅值不是恒定不变的,标定时要调,温漂补偿时也要小幅度调。调的方式是重算整张表再重启 DMA,16 个点的重算开销在微秒级,随便什么时候都能做。

/* 上电 200 ms 内把调制比从 0 拉到额定,避免功放和旋变初级受冲击 */ void exc_soft_start(void) { static uint16_t tick = 0; if (tick >= EXC_RAMP_MS) return; tick++; uint32_t mod = (uint32_t)EXC_MOD_NOM * tick / EXC_RAMP_MS; exc_tbl_rebuild(mod); /* 按新调制比重算表 */ exc_dma_restart(); /* 重启 DMA,从表头开始 */ }

少了这一步,上电瞬间励磁直接跳到满幅,功放级的浪涌电流会把电源拉下来,主控跟着复位,现象是"每次上电偶发死机"。这类玄学问题查电源纹波能查一整天,加个软启动就没了。

4. 采样与解算:把载波剥掉,剩下角度和转速

到这里励磁已经稳了,接下来是把旋变返回的信号变成角度。这一段是软解码真正的核心,也是精度差异的主要来源。同一套硬件,解调方式选错、跟踪环参数没调,角度噪声能差出十几倍。

4.1 采样触发点:为什么不能随手采

旋变输出是V = K · E · sin(ωt) · sin(θ),你要的是sin(θ),那就必须知道采样那一刻的sin(ωt)是多少。如果采样点和励磁相位的关系是随机的,这个乘积就没法还原。

做法是把采样触发挂在励磁定时器的比较通道上。中心对齐模式下,计数器走到顶部(CNT = ARR)时,输出电平处于稳定区间,离两次开关沿都最远,采到的值最干净。把通道 2 配成比较模式但不输出引脚,比较值设在顶部附近,用它去触发 ADC 注入组。

void adc_trig_init(void) { /* 复用励磁定时器的 CH2,只做内部触发,不占引脚 */ EXC_TIM->CCR2 = 520; /* 落在计数器顶部附近,避开开关沿 */ EXC_TIM->CCMR1 |= (7u << 12); /* CH2 配成比较模式 */ EXC_TIM->CCER &= ~(1u << 4); /* 关掉 CH2 的引脚输出 */ /* ADC 注入组外部触发源选该比较事件,一次同时采 SIN / COS */ ADC1->JSQR = ADC_JSRC_EXC_CC2 | (1u << 20) | (2u << 16); }

CCR2 = 520不是随便填的,它要保证触发点落在第 k 个 PWM 周期的中点,同时避开死区。改完这个值拿示波器看一眼采样脉冲和驱动波形的相对位置,心里才有底。

4.2 偏置、增益与正交误差的校准

运放前端的偏置、两路增益的失配、绕组本身的机械正交误差,这三样加起来能让角度误差到 ±2°。校准方式很土但有效:让电机匀速转一整圈,记录两路包络的极大极小值。

typedef struct { int32_t min; int32_t max; } range_t; static range_t r_sin = { 32767, -32768 }; static range_t r_cos = { 32767, -32768 }; void calib_feed(int32_t s, int32_t c) { if (s < r_sin.min) r_sin.min = s; if (s > r_sin.max) r_sin.max = s; if (c < r_cos.min) r_cos.min = c; if (c > r_cos.max) r_cos.max = c; } void calib_apply(void) { g_sin_ofs = (r_sin.max + r_sin.min) / 2; /* 偏置 = 中点 */ g_cos_ofs = (r_cos.max + r_cos.min) / 2; int32_t amp_s = (r_sin.max - r_sin.min) / 2; /* 半峰峰值 = 幅值 */ int32_t amp_c = (r_cos.max - r_cos.min) / 2; g_sin_gain = (65536L) / amp_s; /* Q16 定点增益 */ g_cos_gain = (65536L) / amp_c; }

calib_feed要跑满一整圈机械周期才能收全极值,所以标定时得让电机转够。增益用 Q16 定点存,运行期只做乘法和移位,不做除法——12 bit ADC 的数据乘 Q16 增益在 32 位里不会溢出,但除法在 160 kHz 中断里就是灾难。

4.3 同步解调:乘回载波再累加

解调的本质是相关检测。把采到的信号分别乘以同相位的参考正弦和参考余弦,累加一个整周期,交流项互相抵消,只剩下直流的包络分量。

/* 传入一个励磁周期内的 16 个采样点,输出 Vs、Vc */ void demod_block(const int32_t *raw_s, const int32_t *raw_c, int32_t *vs_out, int32_t *vc_out) { int64_t acc_s = 0, acc_c = 0; for (uint8_t k = 0; k < EXC_POINTS; k++) { int32_t s = (raw_s[k] - g_sin_ofs) * g_sin_gain >> 16; int32_t c = (raw_c[k] - g_cos_ofs) * g_cos_gain >> 16; acc_s += (int64_t)s * g_sin_ref[k]; /* 与参考正弦相关 */ acc_c += (int64_t)c * g_cos_ref[k]; /* 与参考余弦相关 */ } *vs_out = (int32_t)(acc_s >> 4); /* 16 点归一化 */ *vc_out = (int32_t)(acc_c >> 4); }

g_sin_ref和g_cos_ref是两张 Q12 定点的参考表,与励磁表同源。因为励磁波形本身有相位滞后(功放 + RC + 旋变初级电感),参考表要整体移相一个角度 δ,δ 用示波器量出来或者用扫描法找最大值。不移相直接解调,Vs、Vc会带一个固定的角度偏差,表现为"转一圈角度整体偏几度"。

4.4 跟踪环:一个二阶环节同时给出角度和转速

拿到Vs、Vc之后,最直接的做法是atan2。但atan2有两个问题:一是角度跳变,二是没有速度信息,速度还得再差分一次,噪声翻倍。工程上更常用的是跟踪环,本质是一个二阶锁相环。

typedef struct { float theta; /* 电角度,rad,-π ~ +π */ float omega; /* 电角速度,rad/s */ float kp, ki; float integ; /* 积分项 */ float ts; /* 调用周期,100 us */ } pll_t; static pll_t g_pll = { .theta = 0.0f, .omega = 0.0f, .kp = 900.0f, .ki = 40000.0f, .integ = 0.0f, .ts = 0.0001f }; void resolver_solve(float vs, float vc) { /* 鉴相:sin(θ - θ̂) ≈ θ - θ̂,小误差下线性 */ float e = vs * cosf(g_pll.theta) - vc * sinf(g_pll.theta); /* 二阶环:比例给速度,积分消静差 */ g_pll.integ += g_pll.ki * e * g_pll.ts; if (g_pll.integ > OMEGA_LIMIT) g_pll.integ = OMEGA_LIMIT; if (g_pll.integ < -OMEGA_LIMIT) g_pll.integ = -OMEGA_LIMIT; g_pll.omega = g_pll.kp * e + g_pll.integ; g_pll.theta += g_pll.omega * g_pll.ts; /* 回绕,防止 float 精度随圈数退化 */ if (g_pll.theta > 3.1415926f) g_pll.theta -= 6.2831853f; if (g_pll.theta < -3.1415926f) g_pll.theta += 6.2831853f; g_elec_angle = g_pll.theta; g_elec_speed = g_pll.omega; }

kp决定环的阻尼和带宽,ki决定静差。ki给小了,匀速跟踪时角度会有一个固定的滞后;给大了,阶跃响应会过冲甚至振荡。按ts = 100 μs、目标带宽 200 Hz 估,kp在 600~1200 之间、ki在 2e4~6e4 之间是比较安全的起调区间。

cosf、sinf每次调用在 M4 上大概 1~2 μs,10 kHz 中断里两次就是 20% 的 CPU。实际工程里我会换成查表插值或者用上一次的 sin/cos 做增量旋转,把这一段压到几百纳秒。

上电第一次千万别让环从 0 爬。用一次atan2f(vc, vs)直接给theta赋初值,环立刻进入锁定状态,用户看不到任何启动抖动。

5. 避坑与排查:软解码在现场最容易翻车的五件事

这套代码在台架上跑通和在现场跑住是两回事。下面五条是调试笔记里反复出现的,每一条都带着现象和排查路径。

5.1 信号链路上的三个坑

坑一:低速抖动明显,高速反而好。现象是电机在 100 rpm 以下时角度噪声大到听得到,转到 1000 rpm 就干净了。原因是采样点和 PWM 开关沿撞上了,低速时有用信号幅度小,开关噪声占比大。解决是把CCR2往计数器顶部再挪几个计数,用示波器确认采样脉冲落在死区之外、离两个开关沿都有余量。

坑二:上电就有固定角度偏差,转一圈偏差不变。现象是无论怎么转,反馈角度永远比实际少 3°。先怀疑相位补偿,再看偏置校准。八成是g_sin_ofs、g_cos_ofs没标定,两路运放的静态工作点不一致,导致Vs、Vc不在同一个零点上。跑一遍calib_feed转满一圈再calib_apply就正常了。

坑三:励磁幅度随温度往下掉,角度跟着变。现象是冷机正常,跑半小时后角度缓慢漂移。根因是功放级直接吃系统 5 V,电机一热,5 V 跟着掉,励磁幅度一起掉。解算里如果没有做幅值归一化,跟踪环的等效增益也跟着变,动态响应就变了。解决办法是功放级单独供电或者加基准,同时在软件里对Vs、Vc做慢速 AGC 归一化,双保险。

5.2 解算与诊断上的两个坑

坑四:跟踪环锁不住,角度乱跳。现象是上电后角度在几个值之间乱蹦,电机完全没法闭环。原因通常是角度定义不一致——atan2返回 -π~+π,而下游 FOC 期望 0~2π,中间没做统一。另一个原因初值从 0 开始,而实际角度在 π 附近,环要绕大半圈才锁上。解决是统一角度域,并且上电用atan2直接赋初值。

坑五:断线没检测,直接飞车。现象是旋变线松了,控制器还以为角度正常,输出电流一路顶到限幅。旋变的健康判据其实很简单:`Vs² + Vc²

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询