简介:面向无刷电机控制学习者与嵌入式工程师的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]) * tDTS,tDTS = 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 的 TRGO,TRGO 事件选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、ic,ia 由基尔霍夫定律算出来 */ /* 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、ic,ia = -(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. 过调制限幅,保证 T1+T2 <= 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 */ /* 电流环 PI,id 参考为 0,iq 参考来自速度环 */ 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 输出我从来不拆,哪怕产品交付也留着测试点,用宏控制是否输出。参数这东西,波形对了才是真的对了,感觉对了不算。希望这套流程能帮到你,少在同一个坑里待两个晚上。
本文还有配套的精品资源,点击获取