
做电机控制的工程师尤其是要对代码抠到极致的朋友一定有过这种体验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.8us4.7us下降约40%电流采集耗时3.5us0.2usDMA数据已就绪ClarkePark变换2.1us0.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项目提供一些借鉴。