跳到主要内容STM32F407 CubeMX HAL 库三环串级 PID FOC 电控算法实现总结 | 极客日志C算法
STM32F407 CubeMX HAL 库三环串级 PID FOC 电控算法实现总结
STM32F407 基于 CubeMX 和 HAL 库实现三环串级 PID FOC 电控算法。涵盖电流环闭环控制、低侧采样电路配置、ADC 注入组触发与时序校准、Clarke 与 Park 变换及 SVPWM 生成。重点解决 D 轴强拖转子零位、PID 积分饱和处理及 PWM 占空比限制问题,提供关键代码实现与参数调整建议。
灭霸8.4K 浏览 电流环闭环控制概述
在基本了解无刷电机控制理论后,从电流闭环控制入手,逐步推导实现闭环控制所需的反馈信息及单片机外设配置。
实现电流环需要的闭环信息包括电机运转过程中的电流信息以及转子的电角度。其中电流信息应用于克拉克变换,电角度信息则应用于 Park 变换。
电角度 = 转子实际旋转角度 * 极对数,极对数根据购买的电机型号确定。
内转子的角度信息获取方式包括磁编码器、HALL 编码器通过速度估算角度、ABZ 编码器等。
将电流信息和转子位置信息分别带入克拉克变换和帕克变换之后,后续通过 PI 控制器 -> Repark 变换 -> SVPWM 得到三相 PWM 占空比,驱动三相逆变桥。


电流采集环节
实操过程中最重要的环节是电流采集。常见的方案为低侧采样电路,即采样电阻放在下半桥,只有在下半桥导通时才能获取相电流。需要精准选择下半桥导通时进行电流采样,并避免开关管时的电流波动影响。

电流采集关键点包括 STM32 的 ADC 采样时间、采用注入组通道还是规则组,以及电流采样芯片的放大倍数等。
示例采用 TI Drv8313 电驱芯片,只需输入上半桥驱动信号。若使用需 6 路 PWM 输出的芯片,则需考虑死区时间。
中央对齐模式 3 代表从 0 开始上升导通 ARR 再从 ARR 降低到 0 的过程中,上升部分和降低部分都会触发比较寄存器的电平转换。Channel 4 配置用于触发 ADC 进行电流采样。


注意:输出的 PWM 端口要配置成 Mode2,为了让 SVPWM 输出的占空比直接对应高电平的持续时间,CH4 则用于触发 ADC 的注入组进行电流采样。采样时间必须在下半桥导通的时间内,注意避免 MOS 管开关的时间。
ADC 配置如下:

偏置电压不能直接配置,需要在电机上电之前进行校准。
以下是关键实现代码:
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
void Current_Sense_init() {
printf("ADC 开始初始化\r\n");
__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_4,2000);
HAL_ADCEx_InjectedStart_IT(&hadc1);
Voltage_to_Current = 1.0f / (sampling_resistor * Current_gain);
Gain_a = Voltage_to_Current * -1;
Gain_b = Voltage_to_Current;
}
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
if(callback_counter <= 100) {
bias_voltage[0] += HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
bias_voltage[1] += HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);
if(callback_counter == 100) {
My_adcData[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
My_adcData[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);
bias_voltage_ia = (bias_voltage[0] / 100.0f);
bias_voltage_ib = (bias_voltage[1] / 100.0f);
differ_Ia = My_adcData[0] - bias_voltage_ia;
differ_Ib = My_adcData[1] - bias_voltage_ib;
adc_conversion_flag = 1;
My_adcData[0] = 0;
My_adcData[1] = 0;
HAL_ADCEx_InjectedStop_IT(&hadc1);
}
callback_counter++;
} else {
My_adcData[0] = (float)HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
My_adcData[1] = (float)HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);
current.I_a = (My_adcData[0] - bias_voltage_ia ) * ADC_CONVERSION * Gain_a ;
current.I_b = (My_adcData[1] - bias_voltage_ib ) * ADC_CONVERSION * Gain_a ;
float I_alpha = 0;
float I_beta = 0;
arm_clarke_f32(current.I_a,current.I_b,&I_alpha,&I_beta);
rotor_angle_encoder = Get_Position_EncoderMode();
float sin_value = arm_sin_f32(rotor_angle_encoder);
float cos_value = arm_cos_f32(rotor_angle_encoder);
float _motor_Iq = 0;
float _motor_Id = 0;
arm_park_f32(I_alpha,I_beta,&_motor_Id,&_motor_Iq,sin_value,cos_value);
float filter_alpha_Id = 0.2;
float filter_alpha_Iq = 0.2;
motor_Id = low_pass_filter(_motor_Id,motor_Id,filter_alpha_Id);
motor_Iq = low_pass_filter(_motor_Iq,motor_Iq,filter_alpha_Iq);
}
}
}
其中注入组的中断回调函数中,上电开始前的 bias_voltage_ib 即为偏置电压。一般电路会将其配置为 1.65V,但实际操作过程中会有波动,必须在上电前进行校准。用到的 Park 和 Clark 变换均为 ARM DSP 库函数。
此时可以先同开环旋转电机,打印电流信息是否符合相位差的情况。
D 轴强拖转子零位与电流闭环
确认电流采集正常后,开始电流闭环控制。此处涉及 PID 及 D 轴强拖转子零位。
void find_zeropoint() {
set_pwm_duty(0.4, 0, 0);
HAL_Delay(800);
set_pwm_duty(0, 0, 0);
HAL_Delay(800);
}
void set_pwm_duty(float d_u, float d_v, float d_w) {
d_u = min(d_u, 0.9f);
d_v = min(d_v, 0.9f);
d_w = min(d_w, 0.9f);
compare_1 = d_u * htim1.Instance->ARR;
compare_2 = d_v * htim1.Instance->ARR;
compare_3 = d_w * htim1.Instance->ARR;
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, d_u * htim1.Instance->ARR);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, d_v * htim1.Instance->ARR);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, d_w * htim1.Instance->ARR);
}
注意此处将占空比强行设置为 90%,不跑满。因为在旋转过程中会有下半桥完全不导通的情况,为了保证下半桥的电流采样,需要通过控制上半桥的导通时间,给下半桥的电流采样留足时间。
确定转子零位后,即可开始电机控制。此处涉及的电流环控制参考了相关工程实践代码并进行微调。在实际使用过程中发现 ARM 的 PID 函数一直会有积分过饱和的情况,因此自行编写了位置式 PID。
float PID_Calc_Current(PID_t *pid,float target,float actual) {
float error = target - actual / MAX_CURRENT;
pid->e = error;
pid->I += pid->Ki * pid->e;
if (pid->I > pid->I_max) pid->I = pid->I_max;
else if (pid->I < pid->I_min) pid->I = pid->I_min;
float u = pid->Kp * pid->e + pid->I + pid->Kd * (pid->e - pid->e_last);
pid->e_last = pid->e;
if (u > pid->out_max) u = pid->out_max;
else if (u < pid->out_min) u = pid->out_min;
return u;
}
typedef struct {
float Kp;
float Ki;
float Kd;
float e;
float e_last;
float I;
float out_min;
float out_max;
float I_min;
float I_max;
} PID_t;
void Torque_control(float torque_d , float torque_q) {
float d = PID_Calc_Current(&pid_torque_d,torque_d,motor_Id);
float q = PID_Calc_Current(&pid_torque_q,torque_q,motor_Iq);
foc_forward(d, q, rotor_angle_encoder);
}
void foc_forward(float d, float q, float rotor_rad) {
float d_u = 0;
float d_v = 0;
float d_w = 0;
svpwm(rotor_rad, d, q, &d_u, &d_v, &d_w);
set_pwm_duty(d_u, d_v, d_w);
}
static void svpwm(float phi, float d, float q, float *d_u, float *d_v, float *d_w) {
d = min(d, 1);
d = max(d, -1);
q = min(q, 1);
q = max(q, -1);
const int v[6][3] = {{1, 0, 0}, {1, 1, 0}, {0, 1, 0}, {0, 1, 1}, {0, 0, 1}, {1, 0, 1}};
const int K_to_sector[] = {4, 6, 5, 5, 3, 1, 2, 2};
float sin_phi = arm_sin_f32(phi);
float cos_phi = arm_cos_f32(phi);
float alpha = 0;
float beta = 0;
arm_inv_park_f32(d, q, &alpha, &beta, sin_phi, cos_phi);
bool A = beta > 0;
bool B = fabs(beta) > SQRT3 * fabs(alpha);
bool C = alpha > 0;
int K = 4 * A + 2 * B + C;
int sector = K_to_sector[K];
float t_m = arm_sin_f32(sector * rad60) * alpha - arm_cos_f32(sector * rad60) * beta;
float t_n = beta * arm_cos_f32(sector * rad60 - rad60) - alpha * arm_sin_f32(sector * rad60 - rad60);
float t_0 = 1 - t_m - t_n;
*d_u = t_m * v[sector - 1][0] + t_n * v[sector % 6][0] + t_0 / 2;
*d_v = t_m * v[sector - 1][1] + t_n * v[sector % 6][1] + t_0 / 2;
*d_w = t_m * v[sector - 1][2] + t_n * v[sector % 6][2] + t_0 / 2;
}
由于各个电机的 PID 参数不同,PID 的初始化函数未在此列出。上文中提到的 rotor_angle_encoder 是通过编码器得到的电角度信息。
SVPWM 部分参考了工程实践代码,并增加了均值分量的版本进行测试,均能正常工作。
至此电流环闭环基本实现。内环电流环实现后,后续的串级 PID 较为简单,因为速度的获取就是编码器的位置信息进行积分。