【stm32无感FOC理论与实践:滑模观测器】【03 代码实践】

目录

本文代码在该硬件上测试运行:点此查看硬件

在该FOC框架基础上修改:stm32_foc_2以及openloop

本文完整Keil+stm32f103c8t6+HAL库工程:smo分支

默认你已经了解前文【无感FOC开环拖动V/F和I/F】

电机参数

先准备好电机参数和电路参数和软件参数。由于增加了无感观测器,增加了不少计算耗时,把有感FOC基础框架的重复计数器(点此重复计数器的讲解)改大,给计算留出更多空间。

c 复制代码
//conf.h
#pragma once

// 电机物理参数
#define POLE_PAIRS 7 // 极对数
#define PSI_F 0.00257f // 磁链
#define MOTOR_RS 5.5f // 相电阻,Ω
#define MOTOR_LS 0.00085f // 相电感,H

// 电机控制参数
#define MOTOR_MAX_CURRENT 1.0f //软件限制的电机最大电流,安培A
#define MOTOR_MAX_SPEED 200.0f // 软件限制的电机最大转速,弧度每秒

// 电路参数
#define U_BUS 12.0f // 母线电压,伏
#define R_SHUNT 0.02f           // 电流采样电阻,欧姆
#define OP_GAIN 50.0f           // 运放放大倍数
#define ADC_REFERENCE_VOLT 3.3f // 电流采样adc参考电压,伏
#define ADC_BITS 12             // ADC精度,bit
#define ENCODER_BITS 14         // 磁编码器精度,bit

// 单片机配置参数
#define motor_pwm_freq 40000       // 驱动桥pwm频率,Hz
#define motor_pwm_repetition 8     // pwm溢出多少次后,产生一次更新事件(adc触发事件)
#define motor_adc_dt (1.0f / (motor_pwm_freq / (motor_pwm_repetition + 1.0f))) // adc中断周期,秒
#define motor_adc_freq (motor_pwm_freq / (motor_pwm_repetition + 1.0f) / 1000.0f)  //adc中断频率,kHz
#define motor_speed_calc_freq 500 // 电机速度计算频率,Hz

// 软件参数
#define sin_table_bit 11 // 三角函数查找表的位数
#define sin_table_size (1 << sin_table_bit) // 三角函数查找表的大小

u α , u β u_\alpha,u_\beta uα,uβ计算

u α , u β u_\alpha,u_\beta uα,uβ是用三相电压clark变换来的,svpwm输出的占空比不是相电压,而是相对GND的电压,因此需要先算出中心点电压,再得到相电压,再进行clark变换:

c 复制代码
void set_pwm_duty(float d_u, float d_v, float d_w)
{
  htim1.Instance->CCR1 = d_u * htim1.Instance->ARR;
  htim1.Instance->CCR2 = d_v * htim1.Instance->ARR;
  htim1.Instance->CCR3 = d_w * htim1.Instance->ARR;
  float d_mid = (d_u + d_v + d_w) / 3.0f;
  float u_u = (d_u - d_mid) * U_BUS;
  float u_v = (d_v - d_mid) * U_BUS;
  arm_clarke_f32(u_u, u_v, &motor_u_alpha, &motor_u_beta);
}

ADC零漂计算

由于无感观测器是观测电流,特此增加ADC零漂计算,但是实际测试下来发现零漂约等于无,该功能加不加都可以。

c 复制代码
//adc.c
...
float adc_offset_u;
float adc_offset_v;

// adc零漂计算
void adc_offset_calibration()
{
  size_t adc_offset_calib_count = 512;
  static float adc_offset_sum_u;
  static float adc_offset_sum_v;

  for (size_t i = 0; i < adc_offset_calib_count; i++)
  {
    HAL_ADC_Start(&hadc1);
    HAL_ADC_Start(&hadc2);
    HAL_ADC_PollForConversion(&hadc1, 10);
    HAL_ADC_PollForConversion(&hadc2, 10);
    adc_offset_sum_u += (HAL_ADC_GetValue(&hadc1) * (ADC_REFERENCE_VOLT / (1 << ADC_BITS)) - ADC_REFERENCE_VOLT / 2.0f);
    adc_offset_sum_v += (HAL_ADC_GetValue(&hadc2) * (ADC_REFERENCE_VOLT / (1 << ADC_BITS)) - ADC_REFERENCE_VOLT / 2.0f);
  }
  adc_offset_u = adc_offset_sum_u / adc_offset_calib_count;
  adc_offset_v = adc_offset_sum_v / adc_offset_calib_count;
}
...

零漂计算放在桥臂开启前:

c 复制代码
//main.c
...
  HAL_ADCEx_Calibration_Start(&hadc1);
  HAL_ADCEx_Calibration_Start(&hadc2);
  adc_offset_calibration();
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
...

滑模观测器函数

根据前文【01 观测反电动势】的推导结果:

1.计算 i ~ α = i α − i ^ α \tilde{i}\alpha = i\alpha-\hat{i}_\alpha i~α=iα−i^α

2.计算 z α = − K ⋅ sat ( i ~ α ) z_\alpha = -K \cdot \text{sat}(\tilde{i}_\alpha) zα=−K⋅sat(i~α)

3.计算精确离散化 i α ^ [ k + 1 ] = i α ^ [ k ] e − R s T s L s + ( u α [ k ] − z α [ k ] ) ∗ 1 − e − R s T s L s R s \hat{i_\alpha}[k+1] = \hat{i_\alpha}[k] e^{-\frac{R_sT_s}{L_s}} + (u_\alpha[k] - z_\alpha[k])*\frac{ 1 - e^{-\frac{R_sT_s}{L_s}}}{R_s} iα^[k+1]=iα^[k]e−LsRsTs+(uα[k]−zα[k])∗Rs1−e−LsRsTs

4.将 z α z_\alpha zα输入低通滤波得到并更新 e α ^ \hat{e_\alpha} eα^

滑模函数就是将以上几个步骤翻译为C语言。

先按照【01 观测反电动势】所说的将常数 e − R s T s L s e^{-\frac{R_sT_s}{L_s}} e−LsRsTs和 1 − e − R s T s L s R s \frac{1 - e^{-\frac{R_sT_s}{L_s}}}{R_s} Rs1−e−LsRsTs提前计算好:

c 复制代码
//smo.c
void smo_var_init()
{
    smo_i_decay = expf(-(MOTOR_RS * motor_adc_dt) / MOTOR_LS);
    smo_i_drive_gain = (1.0f - smo_i_decay) / MOTOR_RS;
}

再定一下几个公式里的常数。K要覆盖住最大的反电动势=最大转速*磁链常数;sat的线性区通常很小,设置为10%的最大电流:

c 复制代码
#define SMO_K (MOTOR_MAX_SPEED * POLE_PAIRS * PSI_F) // 覆盖住最大的反电动势=最大转速*磁链常数
#define SMO_SAT_A (MOTOR_MAX_CURRENT / 10.0f)        // 用1/10的最大电流

然后是正式的滑模观测器函数:

c 复制代码
static void smo()
{
    // 对应公式\tilde{i}_\alpha = i_\alpha-\hat{i}_\alpha
    smo_tilde_i_alpha = motor_i_alpha - smo_hat_i_alpha;
    smo_tilde_i_beta = motor_i_beta - smo_hat_i_beta;

    // 对应公式z_\alpha = -K \cdot \text{sat}(\tilde{i}_\alpha)
    if (smo_tilde_i_alpha > smo_sat_a)
        smo_z_alpha = -smo_k;
    else if (smo_tilde_i_alpha < -smo_sat_a)
        smo_z_alpha = smo_k;
    else
        smo_z_alpha = -(smo_k / smo_sat_a) * smo_tilde_i_alpha;

    if (smo_tilde_i_beta > smo_sat_a)
        smo_z_beta = -smo_k;
    else if (smo_tilde_i_beta < -smo_sat_a)
        smo_z_beta = smo_k;
    else
        smo_z_beta = -(smo_k / smo_sat_a) * smo_tilde_i_beta;

    // 可以直接用sat函数,但是上述展开后能减少计算量
    // smo_z_alpha = -smo_k * sat(smo_tilde_i_alpha / smo_sat_a);
    // smo_z_beta = -smo_k * sat(smo_tilde_i_beta / smo_sat_a);

    // 对应步骤:精确离散化
    smo_hat_i_alpha = smo_i_decay * smo_hat_i_alpha + smo_i_drive_gain * (motor_u_alpha - smo_z_alpha);
    smo_hat_i_beta = smo_i_decay * smo_hat_i_beta + smo_i_drive_gain * (motor_u_beta - smo_z_beta);

    // 对应步骤将$z_\alpha$输入低通滤波得到并更新$\hat{e_\alpha}$
    smo_hat_e_alpha = low_pass_filter(smo_z_alpha, smo_hat_e_alpha, smo_z_lpf_alpha);
    smo_hat_e_beta = low_pass_filter(smo_z_beta, smo_hat_e_beta, smo_z_lpf_alpha);
}

PLL函数

滑模观测器后立刻接PLL函数,PLL函数本质就是一个pid函数。这里将smo()直接整合到smo_pll()函数中。将前文【02 PLL】最后的控制框图翻译为C语言。

先配置好控制框图中的常量参数,理论上 K p = 2 ζ ∗ 2 π f , K i = ( 2 π f ) 2 K_p = 2\zeta*2\pi f,K_i = (2\pi f)^2 Kp=2ζ∗2πf,Ki=(2πf)2,其中 f f f是设计带宽, ζ \zeta ζ通常设置为0.707,理论带宽通常为FOC计算周期的 1 10 \frac{1}{10} 101到 1 20 \frac{1}{20} 201之间。低通滤波理论公式是 α = 1 − e − 2 π f l p T s \alpha = 1 - e^{-2\pi f_{lp} T_s} α=1−e−2πflpTs,取滤波 f l p = 250 H z f_{lp}=250Hz flp=250Hz。本文硬件使用stm32f103c8t6浮点计算,计算能力有限,实际测试时将PLL带宽设置到了30Hz左右,公式算出的参数也要根据实际情况进行调节。这些理论公式是怎么来的属于另一门学问,这里先不深入。

c 复制代码
#define SMO_Z_LPF_ALPHA 0.33f
#define SMO_PLL_KP 128.0f
#define SMO_PLL_KI 30000.0f

接下来是PLL框图的C语言版本:

c 复制代码
void smo_pll()
{
    smo();
    
    int hat_theta_hex = rad_to_index(smo_hat_theta_e);
    float sin_theta = sin_table(hat_theta_hex);
    float cos_theta = cos_table(hat_theta_hex);

    arm_park_f32(smo_hat_e_alpha,
                 smo_hat_e_beta,
                 &smo_hat_e_d,
                 &smo_hat_e_q,
                 sin_theta,
                 cos_theta);

    // 对应公式err=\frac{\hat{e_d}}{\omega \psi_f}
    smo_pll_err = -smo_hat_e_d / (smo_hat_speed_e * PSI_F + 1e-6f); // 1e-6f是为了防止除以0

    //积分项,积分饱和clamp截断
    smo_pll_integral =
        clamp(smo_pll_integral + smo_pll_ki * smo_pll_err * motor_adc_dt, -SMO_PLL_OMEGA_MAX, SMO_PLL_OMEGA_MAX);
    smo_hat_speed_e = clamp(smo_pll_integral + smo_pll_kp * smo_pll_err, -SMO_PLL_OMEGA_MAX, SMO_PLL_OMEGA_MAX);

    //角度周期化到0到2pi之间
    smo_hat_theta_e = wrap_0_2pi_fast(smo_hat_theta_e + smo_hat_speed_e * motor_adc_dt);
}

开环切入

反电动势观测器需要电机达到一定转速后才能切入闭环观测器,因此一开始不能直接使用观测器,但是观测器可以全程在后台跑,只不过切入的时候foc再使用观测器的输出。

这里简单一点,直接使用开环转速进行判断切入,你也可以加上其他条件,比如要判断q轴反电动势smo_hat_e_q要达到某个阈值,或者PLL误差smo_pll_err要小于某个阈值等等。

c 复制代码
bool smo_is_ready()
{
    return fabsf(openloop_speed_e) > SMO_CHECK_READY_SPEED;
}

开环切入观测器的实时性要求不高,我们可以将这个判断逻辑放在主函数中,减少adc中断计算压力。切入时,才真正设置FOC控制模式,比如速度环之类的。注意,常规的反电动势观测器由于需要电机在转,所以无法做到位置环。注意,切入时要给观测器赋初值,很多参数的初值并不是0,比如切入时,理论上d轴反电动势是0,q轴反电动势是 ω ∗ ψ f \omega*\psi_f ω∗ψf。

c 复制代码
//main.c
...
while(1)
{
    // 无感开环启动切到闭环观测器,实时性要求不高,计算能力受限,放在while(1)里
    if (sensorless_control_state == sensorless_control_state_openloop && smo_is_ready())
    {
      //观测器很多参数初始不是零,需要赋初值
      smo_hat_e_d = 0;
      smo_hat_e_q = motor_dir * SMO_CHECK_READY_SPEED * PSI_F;
      smo_hat_theta_e = openloop_theta_e;
      smo_hat_speed_e = openloop_speed_e;
      smo_pll_integral = openloop_speed_e;
      sensorless_control_state = sensorless_control_state_closeloop;

      //粗调
      set_motor_pid(
          0.0f, 0.0f, 0.0f,
          0.4f, 0.4f, 0.0f,
          0.3f, 0.5f, 0.0f,
          0.3f, 0.5f, 0.0f);
      motor_mode_speed(motor_dir * 100.0f);
      // motor_mode_torque(motor_dir * 0.0f, motor_dir * 0.8f);

      // 无感串级时,pid给弱,否则电流容易被冲烂
      // set_motor_pid(
      //     0.0f, 0.0f, 0.0f,
      //     0.4f, 0.1f, 0.0f,
      //     0.3f, 0.5f, 0.0f,
      //     0.3f, 0.5f, 0.0f);
      // motor_mode_speed_torque(motor_dir * 100.0f, 0.7f);

      arm_pid_reset_f32(&pid_position);
      arm_pid_reset_f32(&pid_speed);
      arm_pid_reset_f32(&pid_torque_d);
      arm_pid_reset_f32(&pid_torque_q);
    }

ADC中断

滑模观测器代码一定要放在稳定的计算周期里,这里放在ADC中断里刚计算出 i α , i β i_\alpha,i_\beta iα,iβ的后面。用了一个状态机,在切入观测器后,将FOC转子角度从开环转子角度转为滑模观测出的转子角度。

c 复制代码
//adc.c
...
    arm_clarke_f32(motor_i_u, motor_i_v, &motor_i_alpha, &motor_i_beta);
    //这里设置smo全程计算,也可以切入观测器后再启动smo计算
    smo_pll();

    int theta_e_hex_norm = rotor_logic_angle_hex_norm;
    if (is_sensorless)
    {
      switch (sensorless_control_state)
      {
      case sensorless_control_state_openloop:
        if (openloop_type == openloop_type_VF)
          openloop_VF();
        else
          openloop_IF();
        theta_e_hex_norm = rad_to_index(openloop_theta_e);
        break;
      case sensorless_control_state_closeloop:
          theta_e_hex_norm = rad_to_index(smo_hat_theta_e);
          break;
      default:
          return;
      }
    }
    
arm_park_f32(motor_i_alpha, motor_i_beta, &motor_i_d, &motor_i_q
...

失踪监测

无感FOC不像有感FOC,可以知道真实的转子角度,一旦无感FOC失去了转子角度跟踪,它就会把错误的转子角度输入到foc中,导致电机运行异常抖动或发热等,因此我们加上失踪监测功能,一旦监测到失踪,就停止运行。失踪判断条件可以有很多种,这里将 e d ^ , e q ^ \hat{e_d},\hat{e_q} ed^,eq^作为监测量,理论上观测器正常运行时 e d ^ \hat{e_d} ed^在0附近, e q ^ \hat{e_q} eq^具有一定的值,如果 e d ^ \hat{e_d} ed^太大或者 e q ^ \hat{e_q} eq^太小,则说明观测器失踪。还进行了强滤波防止噪声干扰。

c 复制代码
//smo.c
...
float smo_check_lose_e_d;
float smo_check_lose_e_q;
// 无感观测保护,防止观测器失踪后还在长时间运行电机
bool check_smo_lose()
{
    //失踪检测实时性要求不高,给予低通滤波
    smo_check_lose_e_d = low_pass_filter(smo_hat_e_d, smo_check_lose_e_d, 0.005f);
    smo_check_lose_e_q = low_pass_filter(smo_hat_e_q, smo_check_lose_e_q, 0.005f);

    return fabsf(smo_check_lose_e_d) > 0.4f || fabsf(smo_check_lose_e_q) < (SMO_CHECK_READY_SPEED * PSI_F * 0.7f);
}

这个功能对实时性要求不高,我们还是放在主函数运行,减轻adc中断计算压力。这个功能在你调pid参数的时候可以暂时关闭。

因为刚切入观测器时,还不稳定,直接监测可能造成误保护,所以延时了500ms再开启监测,因此还需要在切入观测器的逻辑里加上时间标记。

c 复制代码
//main.c
...
  while (1)
  {
    static uint32_t tik;
    // 无感开环启动切到闭环观测器,实时性要求不高,计算能力受限,放在while(1)里
    if (sensorless_control_state == sensorless_control_state_openloop && smo_is_ready())
    {
      //观测器很多参数初始不是零,需要赋初值
      smo_hat_e_d = 0;
      smo_hat_e_q = motor_dir * SMO_CHECK_READY_SPEED * PSI_F;
      smo_check_lose_e_d = smo_hat_e_d;//这个是失踪监测用的
      smo_check_lose_e_q = smo_hat_e_q;
      smo_hat_theta_e = openloop_theta_e;
      smo_hat_speed_e = openloop_speed_e;
      smo_pll_integral = openloop_speed_e;
      sensorless_control_state = sensorless_control_state_closeloop;
      
      //粗调
      set_motor_pid(
          0.0f, 0.0f, 0.0f,
          0.4f, 0.4f, 0.0f,
          0.3f, 0.5f, 0.0f,
          0.3f, 0.5f, 0.0f);
      motor_mode_speed(motor_dir * 100.0f);
      // motor_mode_torque(motor_dir * 0.0f, motor_dir * 0.8f);

      // 无感串级时,pid给弱,否则电流容易被冲烂
      // set_motor_pid(
      //     0.0f, 0.0f, 0.0f,
      //     0.4f, 0.1f, 0.0f,
      //     0.3f, 0.5f, 0.0f,
      //     0.3f, 0.5f, 0.0f);
      // motor_mode_speed_torque(motor_dir * 100.0f, 0.7f);

      arm_pid_reset_f32(&pid_position);
      arm_pid_reset_f32(&pid_speed);
      arm_pid_reset_f32(&pid_torque_d);
      arm_pid_reset_f32(&pid_torque_q);
      tik = ms_tik;//这个是失踪监测用的
    }

    if(sensorless_control_state == sensorless_control_state_closeloop)
    {
      static bool check_smo_lose_enable = false;

      if ((ms_tik - tik) > 500)//切入观测器500ms后开启监测
        check_smo_lose_enable = true;

      // 监测观测器是否失踪,实时性要求不高,计算能力受限,放在while(1)里
      if (motor_control_context.type != control_type_null 
        && check_smo_lose_enable == true 
        && check_smo_lose())
      {
        led_blink_period = 40;
        motor_control_context.type = control_type_null;
        set_pwm_duty(0.0f, 0.0f, 0.0f);
      }
    }
...

开环启动

在while(1)前面启动开环拖动,实测开环IF需要独立的一套电流环pid,不然开环阶段可能抖动:

c 复制代码
//main.c
...
  set_motor_pid(
      0.0f, 0.0f, 0.0f,
      1.0f, 0.2f, 0.0f,
      0.19f, 0.02f, 0.0f,
      0.19f, 0.02f, 0.0f);
  motor_dir = -1; //+1代表正转,-1代表反转
  motor_control_context.type = control_type_openloop;
  openloop_type = openloop_type_IF; // 使用IF开环拖动
  // 【无感模式】 在while(1)中从开环切换到无感观测器
  is_sensorless = true;
  while(1)
  {
  //适机切入观测器
...

滑模成功的数据查看

可以通过多种数据分析滑模是否运行成功。本文使用Ballet软件进行波形查看。

1.查看转子角度追踪情况。本文硬件放置了一个磁编码器,可以进行追踪对比。对比smo_hat_theta_eindex_to_rad(rotor_logic_angle_hex_norm)。可以看到两者基本重合,有时360度跨圈差属于正常。

2.查看dq轴反电动势。查看smo_check_lose_e_dsmo_check_lose_e_q,可以看到smo_check_lose_e_d在0附近,smo_check_lose_e_q具有一定值。这里不直接查看smo_hat_e_dsmo_hat_e_d是因为该二者是滑模开关函数少量低通滤波来的,波动很大。

3.查看转速跟踪情况。本文硬件放置了一个磁编码器,可以进行追踪对比真实的转子转速。查看smo_hat_speed_eencoder_speed*极对数,可以看到均值是对的上的,这里的波动再加一个低通滤波后就能降低。

4.电机实际运行状态。关闭板载编码器或者将电机拿出来看运行情况。


本文代码在该硬件上测试运行:点此查看硬件

本文完整keil工程:smo分支

相关推荐
一路往蓝-Anbo1 小时前
第一章:嵌入式TDD-环境搭建
网络·stm32·单片机·嵌入式硬件·tdd
SmartRadio13 小时前
STM32WLE5 LoRa 射频匹配优化(V1.1 版)
stm32·单片机·嵌入式硬件·阻抗匹配
chao18984415 小时前
基于 STM32 的 Modbus RTU 串口通讯程序
stm32·单片机·嵌入式硬件
fie888916 小时前
基于 STC15F104E 的 T12 白光烙铁控制器方案
stm32·单片机
yuan1999716 小时前
基于 STM32 的工程级扫地机器人方案
stm32·嵌入式硬件·机器人
qq_4112624217 小时前
wifi自适应
stm32·单片机·嵌入式硬件
洋九八17 小时前
STM32 (NVIC)中断
stm32·单片机·嵌入式硬件
12.=0.18 小时前
【stm32_9.2】FreeRTOS的任务管理:任务策略,调度器启用,任务创建、删除、挂起、恢复
c语言·stm32·单片机·嵌入式硬件
洋九八19 小时前
STM32 串口(USART)配置
stm32·单片机·嵌入式硬件