ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 FOC无刷电机控制程序:外设配置、三环整定与避坑

STM32 FOC无刷电机控制程序:外设配置、三环整定与避坑

简介:面向无刷电机控制学习者与嵌入式工程师的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 * Ts

Kt是转矩常数,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 输出我从来不拆,哪怕产品交付也留着测试点,用宏控制是否输出。参数这东西,波形对了才是真的对了,感觉对了不算。希望这套流程能帮到你,少在同一个坑里待两个晚上。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进