做电机控制的工程师,尤其是要对代码抠到极致的朋友,一定有过这种体验:FOC电流环跑得不够快,或者偶尔抽风抖动,查到最后经常不是算法问题,而是电流采集链路出了问题。嵌入式性能优化做到后面,大家拼的往往不是算法的推公式能力,而是“数据能不能在被需要的时候,准确、准时、不浪费CPU地送到该去的地方”。
这次要聊的是一段FOC电流采集代码的实战优化过程。项目背景是一颗Cortex-M4内核的MCU,驱动一块低压PMSM电机,采用三电阻下桥采样方案,FOC执行频率设为20kHz。刚开始代码能跑,功能也能工作,但CPU占用率偏高,中断里还时不时出现采样值毛刺,速度环一旦拉高就容易出现电流波形畸变。整个优化做了三轮,最终让单次电流环中断耗时下降了接近40%,同时把采样干扰问题压到了一定程度以下。这篇文章会把从代码分析、方案选型,到逐段优化和踩坑排查的完整过程都拆开来讲。
1. 原始代码到底慢在哪里
做优化之前,第一步永远是搞清楚现状。不要上来就改,先把原始代码完整读一遍,理解每一条语句在做什么,然后再谈优化。
1.1 需求与背景:为什么FOC电流采集成瓶颈
FOC控制的核心闭环是电流环。电流环的执行频率越高,对电流波形的跟踪效果越好。常见的工业或机器人方案里,电流环频率在10kHz到20kHz不等,执行时间窗口通常只有50微秒甚至更短。在这段时间内,需要完成的事包括:读取ADC采样结果、做Clarke变换、做Park变换、运行PI调节器、做逆Park变换、更新PWM比较寄存器,外加各种保护逻辑和状态机处理。
细算下来,一次电流环中断里,真正留给“采集”的时间很少,但“采集”这个环节如果做得不好,后面的控制算法再精确也没有用。原始代码里的电流采样是在PWM中断中通过软件触发ADC并等待转换结束,然后立即读取结果进行计算。这个方案的逻辑很简单,但从性能角度来看并不理想。软件触发就需要在中断里添加等待循环,ADC采样时间加转换时间,再加上读取和换算,一路算下来可能占掉整个中断时间的20%~30%。如果还用了平均滤波或者做了毛刺丢弃,开销会更大。
另外,三电阻采样方案对ADC触发时刻的精度要求很高。因为PWM下桥导通窗口很短,低调制比或高转速场景下,采样窗口可能只剩几微秒,一旦触发时刻抖动,采样点位置就不稳定,读到的电流值会混入二极管续流、开关噪声甚至上下桥直通的干扰成分。这不仅仅是“慢”的问题,还是“乱”的问题。
1.2 原始实现的几个问题
当时接手的那份代码,采集相关逻辑集中在PWM中断处理函数里,看起来大致是这样:
void PWM_IRQHandler(void) { uint16_t adc1, adc2, adc3; ADC_SoftwareStartConv(ADC1); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET) { // 等待转换完成 } adc1 = ADC_GetConversionValue(ADC1); adc2 = ADC_GetConversionValue(ADC2); ADC_SoftwareStartConv(ADC2); while (ADC_GetFlagStatus(ADC2, ADC_FLAG_EOC) == RESET) {} adc2 = ADC_GetConversionValue(ADC2); adc3 = ADC_GetConversionValue(ADC3); ADC_SoftwareStartConv(ADC3); while (ADC_GetFlagStatus(ADC3, ADC_FLAG_EOC) == RESET) {} adc3 = ADC_GetConversionValue(ADC3); float ia = adc1 * 3.3f / 4095.0f; float ib = adc2 * 3.3f / 4095.0f; // 第三相电流不采集,由基尔霍夫定律计算 float ic = -ia - ib; // Clarke变换 float i_alpha = ia; float i_beta = (ia + 2.0f * ib) / 1.7320508f; // Park变换 float i_d = i_alpha * cos_theta + i_beta * sin_theta; float i_q = -i_alpha * sin_theta + i_beta * cos_theta; // PI调节器,更新PWM // ... }一眼扫过去可以找出几个明显的问题。第一,ADC启动和等待转换的过程是串行的,三路采样时间直接累加,浪费了很多时间。第二,使用浮点运算做电流标度变换和坐标变换。虽然Cortex-M4有FPU,但浮点乘法的开销仍然比定点运算大,尤其在寄存器压力高的时候,编译器还要穿插很多浮点栈操作。第三,等待EOC标志位期间,CPU在空转,这段循环白白占用了中断时间。第四,没有考虑ADC采样的相位对齐,PWM下桥开通和ADC触发之间没有硬件同步,采样点容易落在不稳定的区间。
当时实际测试,一次完整PWM中断从进入IRQ到退出,需要大约7到8微秒,其中电流采集相关占了接近3.5微秒。20kHz频率下,这意味着CPU在单个中断里的净负载比例并不低,但真正有用的控制计算时间反而没占多少。优化方向很明确:让ADC采集“并行”起来,把CPU从等待中解放出来,同时把数学运算改成更便宜的定点形式。
2. 优化设计与选型:先定方向再动手
2.1 明确优化目标:延迟、吞吐、功耗怎么权衡
性能优化最忌讳的就是“什么都想要”。在做任何改动之前,得先明确一个核心性能目标。对于FOC电流采集,摆在面前的几个目标其实是有矛盾的。
如果追求最低的采集到控制的延迟,可以选择在ADC采样完成后立即触发中断,中断里直接读取结果并计算,这样数据最新鲜。但如果追求最低CPU占用率,就需要用DMA搬运数据,在PWM周期结束时中断里直接读取已就绪的结果,省掉等待时间,代价是会引入一个固定的采样延迟。
实际权衡下来,我选择了DMA方式,因为FOC控制本身对电流采样的实时性容忍度比很多人想象的要高一点点。只要PWM周期和ADC触发时刻相对固定,采样延迟是固定值,可以在电流环设计时把它作为系统延迟的一部分考虑。而CPU占用率下降带来的好处是立竿见影的,因为节省出来的时间可以留给保护逻辑、通信、状态机,甚至给速度环提高频率。
此外还要考虑功耗。很多嵌入式工程师优化性能时只顾速度快,却忽略了低功耗和高速运行之间的矛盾。如果MCU可以在绝大部分时间内维持浅睡眠,只在PWM中断事件到来时醒来处理数据,功耗就能显著下降。硬件触发ADC配合DMA正好天然支持这种工作模式,CPU不需要全程参与采集过程。
2.2 硬件同步与外设协同是最划算的优化
嵌入式性能优化的第一优先级,永远是“让硬件做它擅长的脏活累活”。MCU外设的DMA控制器、事件触发系统、定时器主从同步机制,本来就是为这类场景设计的。放着这些硬件能力不用,非要靠中断和循环去软件实现,怎么可能跑得好。
FOC电流采集最适合的硬件方案是:PWM中心对齐模式,定时器溢出或比较匹配事件触发ADC注入组或规则组采样,ADC采样完成触发DMA搬运,DMA搬运完成再触发中断。这样一来,ADC三路采样的启动时间由硬件控制,精度在纳秒级;数据搬运由DMA完成,CPU全程不参与等待;中断里直接处理已经转换好的三路数据,马上进入Clarke和Park变换。
中心对齐PWM的另一个好处是,计数器在峰值时正好对应PWM周期的中间点,此时下桥开通时间最长,采样窗口最宽。把ADC触发点放在定时器周期的峰值时刻,能够最大化保证下桥导通,避免低占空比时采样点落在死区附近的风险。
2.3 “降采样”与“滤波”不是一回事,别用错
有些工程师遇到电流波形不好,第一反应是加滤波,什么一阶低通、滑动平均、中值滤波通通往代码里塞。做FOC电流采集时,这种做法要非常谨慎。滤波是会引入相位滞后的,而且这种滞后在电流环里会给系统增加额外的相位裕度损失,严重时甚至导致震荡。
性能优化过程中,要分清哪些是真的干扰,哪些是采样点位置不对带来的系统性错误。如果是采样点落在了开关噪声窗口里,那要做的是调整PWM触发ADC的相位,减少采样窗口内的噪声能量;如果确实存在高频共模噪声,可以在硬件上增加RC滤波,截止频率选在几十MHz级别,用来滤掉开关尖峰但不影响几kHz到几十kHz的电流基波。
软件滤波只能作为最后一步补充,而且优先选择对相位影响小的方式。比如用多次过采样累加后右移,相当于做了一个简单的平均值滤波,但采样次数不必太多,两次或三次就够。真正把采样时刻对准了,软件滤波的依赖度会大大降低。
3. 实战优化与关键步骤
3.1 用PWM触发ADC,去掉软件中断等待
第一步要做的就是把“软件触发+等待EOC”改成“硬件触发+DMA搬运”。以STM32平台为例,具体的配置思路是可以迁移到任何主流MCU上的。
首选将PWM定时器配置为中央对齐模式,并开启定时器溢出事件或某个比较通道的输出触发,作为ADC的触发源。ADC选择规则组,配置三个通道的采样时间,再开启扫描模式。然后将ADC的DMA请求置位,DMA通道配置为循环模式,缓冲区设置为三个uint16_t变量。
实际配置时,有几个细节很容易被忽略。ADC三个通道的采样顺序,要和PWM三相下桥的物理顺序对应好,避免DMA搬回来的数据对错位置。ADC的采样时间不宜太长,否则采样窗口会侵占PWM周期的可用时间,但也不宜太短,太短会采不到稳定的电压值。我当时选择的是采样保持时间约0.75us,三通道总采样时间约2.25us,加上ADC转换时间大概一共3.6us,放在20kHz周期内完全足够。
配置完成后,PWM中断里就可以直接读取DMA缓冲区的数据:
volatile uint16_t dma_adc_buf[3]; void PWM_IRQHandler(void) { uint16_t adc_ia = dma_adc_buf[0]; uint16_t adc_ib = dma_adc_buf[1]; // adc_buf[2] 作为备用或校验 // 直接进入变换环节,不再等待 // ... }中断服务的耗时瞬间降下来。原来需要被动等待的ADC转换时间,被DMA搬运过程全部隐藏到了后台,CPU在进入中断的那一刻,数据已经整整齐齐地摆在缓冲区里等着处理。
3.2 DMA搬运数据,主循环只做减法
DMA的好处不只是让CPU不用等待,还带来了一个更隐蔽的好处:数据一致性更好。软件触发方式下,读取三路ADC数据的过程中,如果触发时机不当,第一路和第三路之间可能相差好几微秒,采样时刻不一致会直接影响Clarke变换的准确性。而DMA的三次搬运由ADC转换完成事件自动触发,时间间隔由ADC通道配置决定,是严格确定的。
DMA缓冲区使用循环模式后,每次PWM周期自动更新数据。在中断里读取时需要注意数据一致性。因为ADC值和DMA搬运之间存在微小的时延,PWM中断触发时和DMA完成之间可能有一定的时间先后关系。稳妥的做法是使用两个缓冲区交替接收数据,用PWM中断来切换当前有效缓冲区,这样保证每次中断里读到的都是完整的、属于同一周期的数据。
双缓冲的实现并不复杂,核心是DMA可以配置为传输完成中断里切换目标地址,或者使用MCU的乒乓缓冲区功能。如果所用MCU没有硬件乒乓,也可以设置两个长度为3的缓冲区,在DMA传输过半和传输完成两个中断事件里切换使能位。对于FOC代码来说,更简洁的做法是利用定时器更新事件作为“帧同步”信号,在定时器中断中直接读取当前DMA计数器,判断数据是否完整,更简单的方式是在DMA传输完成中断里置位一个标志,PWM中断里检查这个标志,确认本轮数据有效后再计算。
3.3 定点化改造:Clarke变换等数学运算
数据拿到了,下一步是让计算更轻量。Cortex-M4带FPU,但浮点开销仍是可以用节省下来的。特别是Clarke变换和Park变换这些连续的高频计算,浮点乘法虽然只有几条指令,但每次都会有取数、换算、压栈等一系列动作。改成定点运算后,一方面能够减少中断时间,另一方面也让运算结果在不同编译器版本下保持一致,避免了浮点环境差异带来的隐含风险。
以Clarke变换为例,原来的代码使用浮点:
float i_alpha = ia; float i_beta = (ia + 2.0f * ib) / 1.7320508f;改成Q15定点后,可以使用近似的整数乘加:
#define SQRT3_INV_Q15 18919 // 1/sqrt(3) in Q15 format int16_t ia_q15 = (int16_t)(adc_ia << 2); int16_t ib_q15 = (int16_t)(adc_ib << 2); int16_t i_alpha = ia_q15; int32_t tmp = (int32_t)ia_q15 + 2 * (int32_t)ib_q15; int16_t i_beta = (int16_t)((tmp * SQRT3_INV_Q15) >> 15);为什么是Q15?因为ADC是12位,左移2位扩展成14位数据,在Q15下还有约一位余量,可以兼容电流过采样等情况。乘数18919是1/sqrt(3)乘以32768后四舍五入得到的结果。使用int32_t临时变量做乘法,避免int16_t乘法溢出,这个细节很重要很多初学者容易忽略。
Park变换同样处理。正弦和余弦值在运行时来自查表,表中的角度转换为Q15格式。这样整个电流环中就不再出现浮点运算,中断里的计算量明显下降。实测下来,单次Park变换的耗时从大约1.2微秒降到0.4微秒以内。
3.4 增加采样窗口与去毛刺处理的平衡
硬件触发解决的是“何时采样”的问题,但采样窗口内的信号质量还需要额外处理。三电阻采样方案里,采样对象是下桥MOSFET导通时的压降,而这一压降受寄生电感、二极管反向恢复、驱动电阻等影响非常大。尤其在死区切换前后,采样窗口边缘会落入各种尖峰干扰。
解决这类问题主要有三个手段。第一,用硬件来保证最小采样窗口。PWM配置时要限制最小占空比或最小下桥导通时间,如果计算出的导通时间小于ADC最短采样时间加安全裕量,就用修改PWM策略来处理,比如移到扇区切换点重新计算,或暂时冻结占空比。第二,在ADC采样保持时间内加一个延时通道,确保采样发生在开关管完全导通之后。有些MCU的定时器输出比较事件可以微调触发ADC的相位,可以在比较通道里配置一个偏移值。第三,对明显异常的采样值做简单校验,比如连续两次采样值之差超过阈值时,丢弃并沿用上一周期的值。
这套组合策略不是简单的“滤波”,而是在采样链路上逐级堵住干扰入口,既不会明显增加相位延迟,又能明显改善波形质量。实际测试中,电流波形上的高频尖峰幅度降低了一半以上,电流环的PI输出也平稳了很多。
4. 实测数据、踩坑与排查
优化不只是改代码,还要用数据证明改得有效。当时的实测环境是一个带负载的PMSM电机平台,供电电压48V,控制频率20kHz,电流环从采集到PWM更新全部在PWM中断内完成。为了对比方便,我保留了两套代码版本,一套是原始版本,一套是优化版本,用GPIO翻转法测量中断执行时间。
4.1 优化前后性能对比
| 测量项 | 原始版本 | 优化版本 | 备注 |
|---|---|---|---|
| 中断总耗时 | 7.8us | 4.7us | 下降约40% |
| 电流采集耗时 | 3.5us | 0.2us | DMA数据已就绪 |
| Clarke+Park变换 | 2.1us | 0.8us | 定点化优化 |
| CPU占用率 | 15.6% | 9.4% | 按20kHz计算 |
| 电流波形毛刺 | 明显 | 减小约60% | 采样点硬件同步 |
可以看到,中断总耗时优化的主要贡献来自两方面:一是把等待ADC转换的时间消灭掉,二是在数学运算上做定点化改造。两个步骤单独拆开测算过,DMA改造能减少约2微秒,定点化改造能减少约1.3微秒,其他细节优化贡献了剩下的部分。
CPU占用率从15.6%降到9.4%,意味着系统有了额外的6.2%性能余量,这部分余量可以用来做更复杂的保护逻辑、扩展通信协议,或者提高速度环的执行频率。
4.2 常见问题和排查技巧
再顺滑的优化方案,落地时也会遇到各种意想不到的问题。当时遇到掉的最多的几个坑,值得单独记录下来。
第一个坑是PWM触发ADC的初始状态不对。配置完ADC外部触发后,由于触发边沿极性设置反了,ADC在PWM周期的错误时刻触发,三路电流采样值全部跳变。排查了很久才发现问题其实出在定时器TRGO配置,而非ADC本身。排查思路是把ADC触发源临时改为软件触发,看采样值是否稳定,如果软件触发正常而硬件触发异常,问题基本出在触发边沿或触发事件配置上。
第二个坑是DMA缓冲区数据被更新一半时进入中断。虽然DMA速度很快,但三个通道的转换时间是有限的,如果PWM中断优先级高于DMA传输完成中断,就可能出现在三次转换尚未全部完成时PWM中断已经触发,读到的是新旧混合的数据。解决方法是增加DMA传输完成标志判断,或者直接把PWM中断优先级配置得低于DMA传输完成中断,确保数据完整性。
第三个坑是定点化后电流值标定不一致。ADC原始值左移位数和电流标度系数没有对齐,导致电流环PI参数需要重新整定。这里建议做定点化改造时,把电流从ADC码值到安培数的变换用一个统一的定标宏定义管理,不要在多个函数里散落着手写系数。
第四个坑是采样窗口位置与调制方式不匹配。使用SVPWM时,不同扇区下三相下桥的导通时间并不总是相同,三电阻采样在不同扇区,对电流采样的可行性也不一样。如果直接用同一套触发配置跨所有扇区,可能会在某个扇区内采不到完整的三相电流。当时调试时发现低速大扭矩工况下电流波形会周期性畸变,排查后确认就是扇区边界处采样窗口不足,修正方式是使用注入组配合比较事件,在不同的PWM比较时刻采样不同的两相,再计算第三相。
5. 可复用的工程经验
5.1 不同MCU平台下的移植注意点
这套优化思路几乎可以平移到所有主流MCU平台,但不同厂家的外设实现细节差异很大。最需要关注的一点是:ADC触发源是否支持定时器事件,DMA是否支持循环模式和传输完成中断,以及ADC是否有多个通道的注入组功能。
在STM32平台上,定时器触发ADC和DMA搬运是非常成熟的组合。在GD32、AT32等国产CM4平台上也基本兼容,库函数风格类似,移植成本低。如果换到NXP的Kinetis或RT系列,LPADC的触发配置和FIFO特性有所不同,但思路同样适用。如果是Cortex-M0系列,没有硬件除法器和DSP指令,定点化收益更加明显,但同时要额外注意乘法溢出问题。
5.2 后续还能怎么优化
这轮优化做完后,系统已经比最初轻了不少,但继续压榨性能的空间还是有的。如果后续还要进一步释放CPU资源,最激进的反而不是在采集环节,而是把电流环PI计算以后的部分全部交给PWM硬件比较寄存器预装载机制,让CPU在中断里只做“算法”而不做“外设写入”。
更高阶的做法是引入时间触发架构,让整个FOC控制过程按照一个固定的时间轴运行。PWM中断负责采样和电流环,DMA传输完成后自动触发另一个中断执行速度环或位置环,这样各环节的时间基准清晰,调试起来也简单。
另外,如果MCU支持ADC和定时器的联动以及硬件比较器,还可以把过流保护完全放到模拟比较器里做,不经过ADC,也不经过CPU。一旦电流超限,硬件立刻封锁PWM输出,这比任何软件上的快速响应都要可靠。
做嵌入式性能优化,尤其是FOC这种对实时性要求极高的场景,我的体会是:优化的核心不是炫技,而是让系统里的每一个硬件模块都发挥出它本来的作用。省下的那几微秒CPU时间,往往是系统稳定性和扩展能力的关键。代码越往后优化,越会意识到“正确”比“快”更重要,数据采准时了,电流环自然就稳了;CPU闲下来了,系统才有余裕去处理真正复杂的逻辑。希望这篇优化记录能给你手上的FOC项目提供一些借鉴。