简介面向无刷电机控制学习者与嵌入式工程师的STM32 FOC工程包围绕磁场定向控制在三相无刷直流电机上的实现提供从底层外设驱动、电机参数处理到状态估计、PID调节与PWM输出的较完整软件框架可用于理解电流环与速度环的协作。压缩包为zip格式共184个文件约3.79MB以140个h头文件和40个c源文件为主体辅以2个汇编启动文件、1个Keil uvprojx工程文件和1个lib库文件便于在Keil等环境中组织编译与二次开发。内容涉及STM32F30x系列ADC、TIM、HRTIM、USART、CAN、SPI等外设配置以及电机控制任务调度和用户接口模块能帮助读者把霍尔传感器或无感位置估算与PWM调制、功率开关驱动联系起来。已有4081人学习下载适合具备一定STM32基础、希望从工程代码入手掌握BLDC控制流程的读者参考也可作为课程设计、无人机与机器人电机驱动项目的调试底稿。1. 从一块发烫的驱动板说起这套 STM32 FOC 程序到底解决什么去年帮人调一台 24V 云台电机六步换相跑起来低速一顿一顿电流波形像锯齿外壳半小时就烫手。换成 FOC 之后同一个电机、同一块板子噪声降了一截低速能稳稳转到 1 RPM 以下。这套 STM32 FOC 无刷电机控制程序就是把这套流程固化成可直接烧录的工程TIM1 六路互补 PWM、ADC 注入通道同步采样、Clarke/Park 变换、SVPWM、电流环与速度环双环 PI、过流与堵转保护一套走完。它适合已经能让电机转起来、但受够了六步换相噪声和低速抖动的人也适合第一次接触 FOC、想找一份能对照波形逐段理解的完整工程。前提是你手上有示波器或至少一个串口能回传 dq 电流否则调参基本靠猜。2. 硬件与外设底座TIM1 互补 PWM、ADC 注入采样与编码器接口怎么配FOC 能不能跑稳七成取决于外设配置算法本身反而是标准件。很多人卡在「电机能转但电流环一调就炸」回头查发现是采样时刻落在开关瞬间采回来的电流全是尖峰。这一章按配置顺序拆先定采样方案再配 PWM 与死区然后把 ADC 触发点对齐到下管导通区最后处理编码器零位。2.1 先定电流采样方案再决定用哪颗芯片电流采样有三种主流做法选错了后面所有参数都得跟着返工。单电阻采样成本最低但需要在每个 PWM 周期内做两次移相低速时采样窗口极窄代码里要多一层「最小窗口判断」双电阻省一路运放缺相时波形会畸变三电阻最稳采样时刻固定代价是多一路运放和 ADC 通道。我一般建议第一次做 FOC 的人直接上三电阻把算法跑通之后再考虑降成本。资源里的工程默认按三电阻写采样电阻放在下管源极到地之间阻值 5~10 mΩ配差分运放放大 20~50 倍输出偏置抬到 VDD/2 以支持双向电流。方案采样窗口低速表现额外代码量适用场景单电阻窄需移相差需最小窗口补偿大成本敏感的小功率双电阻中等一般中中功率量产三电阻宽固定时刻好小调试、样机、伺服芯片选型上必备条件是高级定时器带互补输出和死区、ADC 支持外部触发注入转换、有编码器接口。STM32F1/F4/G4 这几条都满足G4 额外带运放和比较器能把过流保护做成硬件级省掉一轮软件响应时间。2.2 TIM1 中心对齐模式与死区时间PWM 必须用中心对齐计数器先增后减因为只有中心对齐时开关管的开通时刻才关于周期中心对称ADC 采样点才有一个稳定的「下管全导通」窗口。边沿对齐模式下电流纹波集中在周期一端采样点很难躲开开关噪声。/* TIM1 中心对齐 互补输出 死区以 20kHz 开关频率为例 */ void FOC_PWM_Init(void) { htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period 4200 - 1; /* 168MHz / (2*4200) 20kHz */ htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 1; /* 上溢下溢各一次更新事件每周期一次 */ sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 2100; /* 初始 50% 占空比安全 */ sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET; /* 死区计数器 (DTG[7:0]) * tDTStDTS 1/168MHz ≈ 5.95ns */ sBreakDeadTimeConfig.DeadTime 60; /* ≈ 357ns按你的 MOS 关断时间取 1.5~2 倍 */ sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_ENABLE; HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); /* 三相六路都要启动 */ }逻辑上Period决定开关频率中心对齐模式下实际频率是f_clk / (2 * Period)这点跟边沿对齐不同是新手第一次算错的地方。RepetitionCounter 1是为了让更新事件每个完整 PWM 周期只产生一次否则上下溢各触发一次ADC 会在一个周期里被触发两次电流环频率直接翻倍。死区值按 MOS 管关断时间取 1.5~2 倍太小会直通炸管太大则低速时电流畸变明显因为有相当一部分时间电压被死区钳住。BreakState一定要开把过流比较器输出接到 BKIN 引脚硬件会在纳秒级把六路输出拉到空闲电平软件中断只负责记录故障码和重启。这条链路没接好软件里写再多过流判断都是事后诸葛亮。2.3 ADC 注入通道的触发时刻三电阻方案的采样点在 PWM 周期中心也就是计数器到达Period的时刻此时三相下管全开采样电阻上的电流才是真实相电流。触发源选 TIM1 的 TRGOTRGO 事件选OC4REF把 CCR4 设到比 Period 略小一点让 ADC 在窗口内完成转换。/* ADC1 注入组三通道由 TIM1 OC4 触发 */ void FOC_ADC_Init(void) { hadc1.Instance ADC1; hadc1.Init.ExternalTrigInjecConv ADC_EXTERNALTRIGINJECCONV_T1_CC4; hadc1.Init.ExternalTrigInjecConvEdge ADC_EXTERNALTRIGINJECCONV_EDGE_RISING; hadc1.Init.ScanConvMode ENABLE; hadc1.Init.NbrOfConversion 3; hadc1.Init.ContinuousConvMode DISABLE; /* 注入组本来就被触发别开连续 */ hadc1.Init.DiscontinuousConvMode DISABLE; /* 采样时间要算三电阻叠加运放压摆 ADC 采样保持 */ sConfigInjected.InjectedRank ADC_INJECTED_RANK_1; sConfigInjected.InjectedSamplingTime ADC_SAMPLETIME_28CYCLES_5; sConfigInjected.InjectedOffset 0; /* RANK_2 / RANK_3 同理分别对应 ib、icia 由基尔霍夫定律算出来 */ /* CCR4 比 Period 小 200 个计数给 ADC 留出启动延迟 */ __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_4, 4200 - 200); HAL_ADCEx_InjectedStart_IT(hadc1); }InjectedSamplingTime不能随手填。采样保持时间太短运放输出还没建立就被采样采回来的是上一个周期的残余值表现为电流环在某个固定占空比附近持续振荡。28.5 个周期的采样时间在 168MHz ADCCLK 下大约是 170ns配合 20kHz 开关频率下的下管导通窗口半个周期减去死区约 24μs绰绰有余。真正需要盯的是运放带宽选 1MHz 以上的轨到轨运放否则再长的采样时间也没用。只用两路 ADC 采样ib、icia -(ib ic)这是三电阻方案的常见省法因为三相电流和恒为零。代价是 ADC 少采一路节省出来的时间可以拉长采样窗口。2.4 编码器接口与电角度零位对齐TIM3 或 TIM4 配编码器模式AB 相四倍频直接读 CNT 就是机械角度计数值。关键在零位对齐编码器零点跟转子磁极的 d 轴并不重合差一个固定偏移θ_offset。对齐流程是这样的把id给定为额定电流的 30%iq给定 0θ固定为 0此时定子磁场强制指向 A 相轴线转子会被拉到这个位置并锁住。等 1~2 秒转子稳定读一次编码器计数值记作cnt_zero。之后每次算电角度时/* 电角度 机械角 * 极对数 零位偏移 */ float mech_angle (float)(enc_cnt - cnt_zero) / ENC_CPR * 2.0f * PI; float elec_angle mech_angle * POLE_PAIRS THETA_OFFSET; elec_angle fmodf(elec_angle, 2.0f * PI); /* 归一化到 0~2π */POLE_PAIRS一定要填对填成极数而不是极对数现象是电机能转但一转就正向反转交替像被什么东西拽住了。THETA_OFFSET取决于对齐时θ固定为 0 还是 90°我习惯固定 0 并在代码里把THETA_OFFSET直接置 0让对齐流程天然自洽少一个可调变量就少一个出错点。对齐电流要给足太小则齿槽转矩把转子拽不到位太大则发热。经验值是额定电流的 20%~40%保持 1.5 秒。对齐期间不要开速度环只开电流环否则速度环会以为电机在异常旋转并疯狂输出。3. 从三相电流到 dq 轴Clarke、Park 与 SVPWM 的代码骨架外设配好之后剩下的是每个 PWM 周期都要跑一遍的数学。20kHz 意味着整个电流环必须在 50μs 内跑完扣掉 ADC 转换和 PWM 装载真正留给计算的时间不到 30μs。这一章先讲变换怎么写成可执行代码再拆 SVPWM 的扇区判断和作用时间最后说清楚反 Park 结果怎么装载到比较寄存器。3.1 Clarke 与 Park 变换的定点化取舍浮点在 M4F 上跑这几个变换是够的但在 M0 或 M3 上会明显吃紧。工程里做了编译期开关用宏切换浮点与 Q15 定点两套实现。Q15 的核心是把 sin/cos 做成 1024 点查表角度用 uint16 表示 0~2π 的环形映射避免每次求余。/* 等幅值 Clarke Park浮点版本 */ typedef struct { float ia, ib, ic; float i_alpha, i_beta; float id, iq; float theta; /* 电角度弧度 */ } FOC_Current_t; void FOC_ClarkePark(FOC_Current_t *c) { /* 只用两路采样第三路由基尔霍夫定律推出 */ c-i_alpha c-ia; c-i_beta (c-ia 2.0f * c-ib) * ONE_BY_SQRT3; /* 1/√3 ≈ 0.57735 */ float s sinf(c-theta); float co cosf(c-theta); c-id c-i_alpha * co c-i_beta * s; c-iq -c-i_alpha * s c-i_beta * co; }这里用的是等幅值变换不是等功率变换。区别在于等幅值变换后id、iq的幅值与相电流峰值一致调 PI 参数时直观等功率变换多一个√(2/3)系数功率计算方便但幅值对不上。工程里统一用等幅值ONE_BY_SQRT3这个常数必须保持一致如果 Clarke 用等功率、Park 用等幅值现象是扭矩输出比预期大或小 1.22 倍且电机发热异常。sinf/cosf在 M4F 上有硬件指令单次约 20~30 个周期20kHz 下占不到 1% CPU。真正拖时间的是查表版本的插值如果做成 Q15建议 512 点表加线性插值误差控制在 0.1% 以内比 1024 点纯查表省一半 flash。3.2 SVPWM 的扇区判断与作用时间SVPWM 的目标是把vα、vβ合成一个落在六边形内的电压矢量用相邻两个基本矢量和零矢量在时间上平均出来。七段式是常见做法每周期内开关次数固定电流纹波小。/* 七段式 SVPWM输入 v_alpha/v_beta输出三相占空比 */ void FOC_SVPWM(float v_alpha, float v_beta, float vbus, float duty[3]) { /* 1. 扇区判断用三个中间变量避免 atan2 */ float V1 v_beta; float V2 SQRT3_BY_2 * v_alpha - 0.5f * v_beta; float V3 -SQRT3_BY_2 * v_alpha - 0.5f * v_beta; uint8_t sector 0; if (V1 0) sector | 0x01; if (V2 0) sector | 0x02; if (V3 0) sector | 0x04; /* 2. 作用时间T1/T2 为相邻矢量T0 为零矢量 */ float Ts 1.0f / PWM_FREQ; float k Ts / vbus; /* 电压归一化到母线 */ float T1, T2; switch (sector) { case 0x03: T1 k * V1; T2 k * V2; break; /* 扇区 I */ case 0x01: T1 -k * V3; T2 -k * V1; break; /* 扇区 II */ case 0x05: T1 k * V2; T2 k * V3; break; case 0x04: T1 -k * V1; T2 -k * V2; break; case 0x06: T1 -k * V3; T2 -k * V1; break; default: T1 -k * V2; T2 -k * V3; break; /* 扇区 VI */ } /* 3. 过调制限幅保证 T1T2 Ts */ if (T1 T2 Ts) { float scale Ts / (T1 T2); T1 * scale; T2 * scale; } /* 4. 三段式时间转成三相占空比中心对齐 */ float Ta (Ts - T1 - T2) * 0.5f; float Tb Ta T1; float Tc Tb T2; /* duty[] 的顺序由扇区决定这里省略映射表见下面说明 */ FOC_ApplySector(sector, Ta, Tb, Tc, Ts, duty); }扇区判断用三个符号位组合成sector值比用atan2快一个数量级也避免了atan2在vα、vβ同时趋近 0 时的精度问题。k Ts / vbus这一步把电压指令归一化所以vbus必须是实时采样的母线电压不能写死。母线掉压时如果还用固定值输出电压判断偏大轻则电流上不去重则过调制导致波形削顶。过调制限幅是必须的。速度环在阶跃给定下会瞬间输出一个远超母线能力的vq不做限幅T1 T2超过Ts占空比会溢出到负值或超过 100%下管全开就是一次直通。限幅的做法是等比例缩放保持电压矢量方向不变只压缩幅值。FOC_ApplySector里其实是一张六行的映射表每个扇区对应Ta/Tb/Tc到 A/B/C 三相的不同排列。这张表手写容易错我的建议是先用仿真或者 Excel 把六个扇区的波形在一个周期内排出来对着表逐行核对比事后示波器上找问题快得多。3.3 反 Park 与比较寄存器装载id、iq经 PI 调节后得到vd、vq反 Park 回到 αβ 平面再进 SVPWM。这一步的角度必须和采样时的角度对齐也就是同一周期的θ否则会引入一个跟转速成正比的相位误差高速时表现为扭矩下降、电流增大。void FOC_InvPark(float vd, float vq, float theta, float *va, float *vb) { float s sinf(theta); float co cosf(theta); *va vd * co - vq * s; *vb vd * s vq * co; } /* 在 ADC 注入转换完成中断里调用保证同一周期闭环 */ void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc) { FOC_Current_t *c g_foc; c-ia -(g_adc_ib g_adc_ic); /* 基尔霍夫补第三相 */ c-ib g_adc_ib; c-ic g_adc_ic; FOC_ClarkePark(c); /* 用本周期采到的 theta */ /* 电流环 PIid 参考为 0iq 参考来自速度环 */ float vd PI_Update(pi_d, 0.0f - c-id); float vq PI_Update(pi_q, g_iq_ref - c-iq); /* 电压限幅防止积分饱和 */ FOC_LimitVoltage(vd, vq, g_vbus * 0.577f); float va, vb; FOC_InvPark(vd, vq, c-theta, va, vb); float duty[3]; FOC_SVPWM(va, vb, g_vbus, duty); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, (uint32_t)(duty[0] * PWM_PERIOD)); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, (uint32_t)(duty[1] * PWM_PERIOD)); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_3, (uint32_t)(duty[2] * PWM_PERIOD)); }整个闭环塞在 ADC 注入完成中断里是因为这个中断的触发点就是采样点从采样到装载占空比之间的延迟固定且最短。如果把计算放到主循环或者普通定时器中断里延迟会随主循环负载波动电流环等效延迟变大相位裕度下降PI 参数怎么调都会在某个频率上振。FOC_LimitVoltage的限幅值取vbus * 0.577也就是1/√3对应 SVPWM 线性调制区的最大相电压幅值。超过这个值就进入过调制波形开始削顶电流谐波急剧上升。限幅之后还要做积分抗饱和否则给定持续偏大时积分项会累积到很大退饱和要花好几个周期表现为电流冲上去之后回不来。theta的更新时机要跟采样对齐。常见做法是在同一个中断里用编码器计数值更新theta但如果编码器读取和 ADC 采样之间隔了几个微秒高速时这个时间差乘以电角速度就是一个不可忽略的角度误差。严谨的做法是在 PWM 周期的固定点同时锁存编码器计数和触发 ADC让两者严格同相。4. 三环整定与死区补偿PI 参数怎么算、什么时候该加前馈参数整定是 FOC 里最容易玄学化的部分。有人给一组「万能参数」换台电机就炸有人全靠试凑调三天调不出想要的效果。实际是有计算路径的电流环按电机电感电阻算带宽速度环按惯量算位置环再往下压。这一章给出可复现的整定步骤以及死区补偿和抗饱和这两个必做的补丁。4.1 电流环 PI 的离散化与带宽选择电流环的被控对象近似为一阶 RL 环节传递函数1/(Ls R)。按零极点对消设计PI 的零点放在R/L处闭环带宽ωc由Kp/L决定于是Kp ωc * L Ki ωc * R * Ts 离散化后积分项系数其中ωc一般取开关频率的 1/10 到 1/20。20kHz 开关频率对应ωc约 1~2 krad/s保守取 1000 rad/s。L和R用万用表或 LCR 表测相间值再除以 2。typedef struct { float kp, ki; float integral; float out_max, out_min; } PI_t; float PI_Update(PI_t *pi, float err) { float p pi-kp * err; pi-integral pi-ki * err; /* 积分限幅上下限跟输出限幅一致最粗暴但有效 */ if (pi-integral pi-out_max) pi-integral pi-out_max; if (pi-integral pi-out_min) pi-integral pi-out_min; float out p pi-integral; /* 输出限幅 反算抑制退饱和过冲 */ if (out pi-out_max) { out pi-out_max; pi-integral out - p; } else if (out pi-out_min) { out pi-out_min; pi-integral out - p; } return out; }积分限幅和输出反算是两道保险缺一不可。只做积分限幅退饱和时积分项还是满的要等误差反向积回来电流会出现一个明显的平顶只做输出反算积分项在限幅期间还是会缓慢累积到很大反算时一次性拉回会造成输出跳变。两个都做退饱和过程才是平滑的。实际调试时先把ki置零只加kp给iq一个 10% 额定的小阶跃看响应。如果上升沿有明显过冲说明kp偏大或者带宽取得太高如果上升太慢加大kp。kp定下来之后再加ki从小往大调直到阶跃响应的稳态误差在一个周期内消掉且没有低频振荡。这套流程比盲调快得多也更容易复现。4.2 速度环带宽与惯量辨识速度环的带宽必须显著低于电流环常见比例是 1/5 到 1/10。假设电流环带宽 1000 rad/s速度环取 100~200 rad/s。速度环的 PI 参数跟转动惯量J相关Kp_speed ωs * J / Kt Ki_speed ωs * B / Kt * TsKt是转矩常数B是阻尼系数。J和B一般拿不到准确值实测方法是给一个已知的 torque 阶跃记录加速度J ΔT / Δω。工程里更实用的做法是先用一个保守的小kp观察速度阶跃的上升时间按比例调整到目标上升时间再补ki消稳态误差。速度环的输出是iq参考必须限幅到电机额定电流以内这个限幅同时充当了扭矩保护。限幅值要跟电流环的输出限幅分开设置速度环的限幅更小因为它是「请求」电流环的限幅是「能力上限」。速度反馈的滤波要小心。编码器四倍频之后每毫秒的计数变化量在低速时很小直接差分噪声极大。常用做法是用一个一阶低通截止频率取速度环带宽的 3~5 倍。滤波太狠会引入相位滞后速度环增益就上不去滤波太弱速度环会被噪声激励出高频抖动。这个折中点在每台电机上都不一样我的经验是先按 5 倍带宽设如果速度环出现高频颤振就往下降。4.3 死区补偿与电压前馈死区带来的电压误差跟电流方向相关表现为相电流在过零点附近出现平顶低速时尤其明显听感上是「咯噔咯噔」的周期性异响。补偿方法很直接根据电流符号给占空比加一个偏置。/* 死区补偿按相电流方向补偿占空比 */ void FOC_DeadTimeComp(float duty[3], const float cur[3], float tdead, float ts) { float comp tdead / ts; /* 归一化补偿量典型 0.01~0.03 */ for (int i 0; i 3; i) { if (cur[i] DEAD_ZONE_CURRENT) { duty[i] comp; } else if (cur[i] -DEAD_ZONE_CURRENT) { duty[i] - comp; } /* 零电流附近不补偿避免符号抖动引起振荡 */ if (duty[i] 0.98f) duty[i] 0.98f; if (duty[i] 0.02f) duty[i] 0.02f; } }DEAD_ZONE_CURRENT这个阈值是补偿的死区电流在这个范围内不补偿因为符号判断会随噪声反复跳变补偿量跟着跳反而制造出新的振荡。阈值取采样噪声峰峰值的 2~3 倍比较合适。除了死区补偿母线电压前馈也值得加上。SVPWM 里已经除以了vbus但 PI 输出的是电压指令如果母线跌落同样的指令对应的实际电压更低电流环会自己去补但补的过程有延迟。前馈的做法是把vbus的倒数直接乘进 PI 输出让电压指令对母线变化即时响应PI 只需要处理模型误差。5. 避坑排查五个现场踩过的坑从现象倒推原因调试现场的问题大多不是算法错而是某个环节的假设不成立。下面五条是我在不同板子上反复遇到的按「现象 → 原因 → 解决」记下来遇到类似症状可以先对照排查。5.1 上电第一次使能就触发过流复位后又能转现象第一次HAL_TIM_PWM_Start之后立刻进 break 中断示波器上能看到一个极窄的脉冲。复位重来有时正常有时不正常。原因六路 PWM 启动顺序有问题。先启动互补通道再启动主通道中间存在一个主输出还没使能但互补已经有效的窗口或者比较寄存器还没写入初始值默认 0占空比为 0 时下管常开、上管全关但互补通道极性配反就会出现直通。解决启动前置__HAL_TIM_SET_COMPARE写三相 50%再调HAL_TIM_PWM_Start和HAL_TIMEx_PWMN_Start最后开 MOE。中断里清标志的顺序也要对先读 break 状态再清否则清完状态位就丢了。5.2 低速时电流波形有台阶电机有周期性异响现象示波器看相电流过零点附近有一段平坦区转速越低越明显同时听到跟电频率同步的「咯噔」声。原因死区未补偿或者补偿量跟实际死区时间不匹配。死区期间输出电压由电流方向决定电流换向时电压突变被延迟形成平顶。解决先量实际死区时间示波器看互补两路的死区按tdead/ts算补偿量。如果补偿后仍有台阶检查采样电阻的运放偏置是否准确偏置偏了会让电流在零点附近的判断整体偏移补偿方向就错了。5.3 高速时电流上不去转速卡在某个值现象给定速度往上加iq已经到限幅转速不再上升vd却越来越负。原因没开弱磁。表贴式永磁电机在基速以上需要注入负id来削弱气隙磁通否则反电动势超过母线电压电流环饱和。解决加一个弱磁环用vbus/√3跟实际电压幅值做差超了就往下压id参考。注意id为负时铜损增加弱磁深度要限制在额定电流的 30% 以内同时监控绕组温度。5.4 编码器零位每次上电都不一样现象同一套代码每次上电对齐出来的cnt_zero差几十个计数低速时正反转不对称。原因对齐时给的电流不够转子没被完全拉到 d 轴齿槽转矩把最终位置拉偏了。或者对齐时间太短转子还在振荡就采样了。解决对齐电流提到额定值的 30%~40%保持 1.5 秒以上采样前先用 200ms 判断编码器计数是否稳定连续几次读数差值小于阈值。对齐完成后把cnt_zero存到 flash下次上电直接用只在首次烧录后做一次。5.5 电流环怎么调都振频率正好是开关频率的一半现象iq波形上有固定频率的振荡频率约等于PWM_FREQ/2加大kp更快发散。原因电流环执行频率和 PWM 频率不一致。比如 PWM 20kHz但 ADC 触发用了上下溢各一次实际电流环 40kHz或者反过来 PWM 中心对齐但中断里又跑了一遍计算采样和控制不同步。解决确认RepetitionCounter的取值确认 ADC 注入只被触发一次在中断入口翻转一个 GPIO用示波器量中断频率是否等于 PWM 频率。这个 GPIO 翻转是排查时序问题的后悔药建议一直留着用宏关掉即可。6. 进阶验证用阶跃响应和相电流波形判断控制环是不是真的调对了参数调完不等于调对。我见过太多「能转、能带载、但一上负载就啸叫」的工程问题都藏在波形细节里。这一章给三个可执行的验证动作每个都有明确的判断标准做完基本能确认电流环和速度环的真实水平。6.1 电流环阶跃看上升时间和过冲把速度环断开iq参考直接由一个阶跃发生器给幅度取额定电流的 10%用串口或 DAC 把iq实际值以 10kHz 回传画出来看。指标合理范围超标说明上升时间10%~90%1~3 个 PWM 周期太长带宽不足太短 kp 过大过冲 10%明显过冲说明 kp 偏大或积分饱和稳态纹波 额定 3%纹波大查采样时刻和运放带宽调节时间进入 ±5% 5 个 PWM 周期退饱和慢查抗饱和逻辑回传数据用串口 115200 明显不够10kHz 采样至少需要 1Mbps 以上或者直接用 DAC 把变量打到示波器上。我一般用 DAC 打两路一路iq_ref一路iq_fb示波器上直接看阶跃和跟随比事后处理数据快得多。/* 简易阶跃发生器挂在 1kHz 定时器里 */ void StepGen_1kHz(void) { static uint32_t tick 0; tick; if (tick 100) { g_iq_ref 0.0f; } else if (tick 200) { g_iq_ref RATED_CURRENT * 0.1f; /* 10% 阶跃 */ } else { g_iq_ref 0.0f; } if (tick 400) tick 0; }阶跃幅度不要超过 20% 额定幅度太大会触发电压限幅看到的就不是线性响应而是限幅特性。周期取 400ms足够响应完全稳定也方便示波器触发。6.2 相电流波形看正弦度和过零点用电流探头夹一相或者用采样电阻上的电压。低速比如 100 RPM下看波形重点看三件事正弦度、过零点台阶、三相幅值是否一致。正弦度差谐波明显通常是 SVPWM 扇区表写错或者死区补偿量不对。过零点台阶就是 5.2 那条。三相幅值不一致查采样电阻精度和运放增益是否匹配三路不一致会造成扭矩脉动表现为低速转动时的手感不均匀。如果手边没有电流探头可以退一步看id、iq的纹波。id理论上应该恒定在 0 附近不弱磁时如果id上出现跟电频率同步的波动说明角度有偏差或者三相采样不一致这个误差会被 Park 变换搬到 d 轴上。6.3 速度环阶跃与负载扰动电流环确认之后接上速度环给速度阶跃同样用 DAC 打speed_ref和speed_fb。看上升时间是否匹配设计带宽看稳态是否有静差看突加负载时的转速跌落和恢复时间。一个容易被忽略的点是速度反馈的量化。低速时编码器每毫秒的计数可能只有个位数分辨率决定了速度环的下限。如果目标转速对应的计数值小于 20建议改用 M/T 法测速或者加长测速周期否则速度环的反馈本身就在跳参数再好也没用。6.4 我自己的检查习惯每次改完参数或者换电机我都会强制走一遍上面三步先看iq阶跃再看低速相电流最后看速度阶跃三步各花五分钟但能把 90% 的问题拦在带载之前。有一回图省事跳过了相电流那一步直接上负载结果低速爬行时电机每隔几秒抖一下回头查是死区补偿阈值设成了实际噪声的十倍补偿完全没生效。从那以后示波器上那两路 DAC 输出我从来不拆哪怕产品交付也留着测试点用宏控制是否输出。参数这东西波形对了才是真的对了感觉对了不算。希望这套流程能帮到你少在同一个坑里待两个晚上。本文还有配套的精品资源点击获取