电机 | BLDC 控制

注:本文为 “电机 | BLDC 控制” 相关合辑。
略作重排,未整理去重。
如有内容异常,请看原文。


跟着官方学电机,BLDC 两种控制策略

不说二话的自家人于 2022-06-28 16:16:51 发布

控制 BLDC 速度转动

方法一:构建一个控制器控制电压的大小

首先,先建立一个霍尔传感器获取电机的角位置和速度,将转子内的区域划分为 6 个扇区,霍尔传感器不会提供转子的具体位置信息,但是可以检测转子何时从一个扇区转到另一个扇区,而且实际上只要知道位置信息,就可以对电机进行换向,这个时候又不知道换 A/B/C 的哪两个,因此需要采用换向逻辑,通过此逻辑确定三相逆变器开关,如下图 2

img

img

换向逻辑与三相逆变器的相互作用

首先是换向逻辑中的 A、B、C 分别代表 A 相,B 相,C 相,而 H、L 则代表三相逆变器里高压侧和低压侧,如上面第一个逻辑 100001,则代表 A 相高压侧和 C 相低压侧开关打开,如下图:通过换向逻辑来使能三相逆变器,从而达到使能电机旋转的目的

img

控制速度

上述已经完成了对电机旋转的基本控制,但仍旧达不到我们想要的按既定目的速度运行,改变速度就要改变电压大小,因此我们需要控制电压的大小,我们可以采用适当的闭环控制来进行,通过期望速度与实际速度之间的差异输出再加上 pid 控制器调整,控制电压的大小使得电机运转速度达到期望速度,这一部分基本上也都是属于算法控制的,包括霍尔传感器的 6 个逻辑控制。


img

扭矩响应的波动是 BLDC 梯形控制的缺点之一,产生原因进行分析:

img

可以看三相电流,随着 A/C 相磁场、电流增大,达到峰值左右时,突然断开,从而将电流转移到 B 相上,则如下图:

img

由于 A 相突然断相,则图中红色线则变为 0,绿色线开始跳变,蓝色线也开始跳变,可以看到都是在峰值的一半左右,这是因为 B/C 两处的场强在 50%时达到平衡,这样我们可以在实际应用中看到三相电压的尖峰,称为感应反激

img

整体模型如下:

img

方法二:使用 PWM

在实际应用中,我们一般采用的都是恒定的直流电压源,而非上面所讲的理想可调电压源,因此采用 PWM 控制就变得很有必要了,首先先认识一下 PWM 是什么:

img

PWM 全称脉冲宽度调制 pulse width modulation,如上图是以一种方波的信号形式,且以一种频率重复自身,一个周期即从一个上升沿到下一个上升沿为一个周期,占空比即一个周期内第一个上升沿到第一个下降沿的区域占据一个周期的大小,上图为 50%,占空比也可以说是在给定一个周期内 pwm 信号打开的时间百分比

如下图:若给定的电压范围在 0~100V 区间,占空比设定为 50%,那么获得的平均电压就为 50V

img

需要注意的是,在选取 pwm 频率时,不能采取的太低,不然获得的电压就不是平均电压,而是方波的电压,这就导致转速无法跟上设定速度,电机就会出现加速、减速,相反,我们将频率设定在一个合理的地方,那么电压就会被平均化,从而提高调速性能,具体两种状态分析如下图 1、2

img

图 1 频率过低时

img

图 2 频率合理时

中间是的黄色纹波是由于 pwm 打开会产生的纹波效应,通常 BLDC 采用的频率一般为几千赫兹,且应远大于电机时间常数的倒数

img

两种 pwm 的构建方法

  1. 模型如下:

img

区别于第一种理想电压调节的方法的模型图,该模型增加了一个 buck converter (降压转换器)实现 pwm 控制,这个模块就是用于电压调节控制,以改变电机速度

这是一种 pwm 和降压变换器相互配合为三相逆变器输入所需直流电压

2、这是第二种方式,直接调制相电压,模型如下

img

在这里,pwm 直接位于换向逻辑子系统中,pwm 发生器根据转子所在的扇区通过脉冲方式开启或关闭,以确保正确的相电压输出

img

过程如何我们可以直接运行模型观测各数据点数据图形可以看出。


BLDC 的基本控制

牛牛 ly 于 2022-10-07 16:36:31 发布

1.直流无刷电机简介

说到直流无刷电机 (BLDC) 就不得不说一下直流有刷电机 (BDC)。直流有刷电机顾名思义就是有电刷与换向器。换相是通过电刷完成的。而直流无刷电机是没有电刷的。换相如果是有感的话利用霍尔传感器与编码器检测转子的位置来换相。无感的话通过反电动势的读取。

img

有刷直流电机与无刷电机的最大结构区别:无刷没有电刷以及换向器;转子与定子反过来了。

如下是对比图:

img

2.直流无刷电机分类

BLDC 都是方波驱动的。分为外转子式 BLDC 与内转子式 BLDC。

1.外转子式 BLDC:转轴与外壳固定。

2.内转子式 BLDC:转轴与外壳不固定固定。

3.直流无刷电机的主要参数(主要的)

极对数:转子磁铁 NS 级的对数,此参数和电机旋转速度有关:电子速度 = 电机实际速度 * 极对数。

KV 值:值越大电机转速越大。电机转速 = KV 值*工作电压。

额定转速:额定的电流下的空载转速,通常单位用 RPM 表示。

转矩:电机中转子产生的可以带动机械负载的驱动力矩。通常单位为:N-M。

4.直流无刷电机的应用

无刷电机的应用场景很广泛,如电动车、无人机、风扇、鼓风机、抽油烟机等等。

img

5.直流无刷电机的驱动原理 (重点)

内部 BLDC 的图如下:

img

简化如下:可以看出此电机有四对极。而 A,B,C 绕组各自的四个绕组都是串联在一起的。串联起来的绕组一端都到公共端。

img

为了方便分析只用一对磁对极。这并不影响。

img

这样,通电的线圈会产生各自的磁场,他们的合成磁场满足矢量合成的原则。如下图:

img

直流无刷电机的 6 拍工作方式,线圈产生旋转磁场,见图

img

通过上图我们可以明显看出,想要控制 BLDC 旋转,根本的问题就是产生这 6 拍工作方式的电压信号(称为 BLDC 的六步控制)。举个例子来说明,假定一个 BLDC 的额定电压为 24V,电机三根线就定义为 A、B、C:

(1)为 A 接 24V、B 悬空、C 接 GND,此时对应图中的①,电机转轴被固定在一个位置;

(2)在(1)的基础上,我们修改接线方式,为 A 接 24V、B 接 GND、C 悬空,此时对应图中的 ②,电机转轴就在(1)基础上旋转一个角度,达到另外一个位置;

(3)在(2)的基础上,我们修改接线方式,为 A 悬空、B 接 GND、C 接 24V,此时对应图中的③,电机转轴就在(2)基础上旋转一个角度,达到另外一个位置;

(4)在(3)的基础上,我们修改接线方式,为 A 接 GND、B 悬空、C 接 24V,此时对应图中的④,电机转轴就在(3)基础上旋转一个角度,达到另外一个位置;

(5)在(4)的基础上,我们修改接线方式,为 A 接 GND、B 接 24V、C 悬空,此时对应图中的⑤,电机转轴就在(4)基础上旋转一个角度,达到另外一个位置;

(6)在(5)的基础上,我们修改接线方式,为 A 悬空、B 接 24V、C 接 GND,此时对应图中的⑥,电机转轴就在(6)基础上旋转一个角度,达到另外一个位置。

(7)然后又是以(1)方式给电,如此循环,电机就可以转动起来。

但是如何实现上面所描述的三相极性的切换呢?

有了上面的原理分析,现在想让 BLDC 旋转起来的一个问题就是如何任意的控制 A、B、C 线的电压,参考之前的直流有刷电机驱动设计,就会马上想到可以用三个半桥(6 个桥臂:3 个上桥臂+3 个下桥臂)构成的一个三相逆变器,这里的每个桥臂都有一个电子开关,电子开关可以选择是功率 MOSFET 或者 IGBT,IGBT 用于超大功率电机驱动。最终搭建起来的电路见图:

img

只要控制六个电子开关的导通或截止就可以实现六步换相。如下图:

img

把 A,B,C 三相不是公共端的各自分别接到每个上下半桥的中间。并且每个电子开关用 PWM 来驱动。但是还会多一个 MOS 驱动器这是因为控制器 (MCU)输出的占空比电压是 5v 或者 3.3V。而电子开关如果是 MOSFET 管的话是不能使其导通。相当于电压不够。所以 MOS 驱动器可以提升电压到 15V 左右。使其满足 MOSFET 管的导通条件。

下面引出另一个问题什么时候换相。要怎么准确的得到转子的位置。假如现在从 0 度转到 60 度怎么知道到 60 度了。这样才能在到 60 度时准确换相。如果换快了。电机可能就会乱转。换慢了就可能会停一下又转一下。这样会使电机很不平稳。所以转子的位置显得很关键。

方法如下:

有感方案:通过安装霍尔传感器与编码器得到转子反馈位置。

无感方案:比如读取反电动势的大小来得到转子的位置。

下面介绍安装霍尔传感器。霍尔传感器就是利用根据霍尔效应制作的一种磁场传感器。有正向磁场时输出 1,反相磁场输出 0。如图:

img

霍尔传感器与 BLDC 电机安装示意图见图。

img

安装时霍尔传感器要与转子的南极,北极对应好。否则可能不准。并且是要有三个才能判断出转子的位置。

安装方法有两种三个霍尔传感器电角度相差 120 度与 60 度。如图:

img

看波形可以找到 120 度安装 H1,H2,H3 不会同时都为 0 或者同时为 1.而 60 度安装会有。其实 60 度安装的 H3 等于 120 度的 H2。H2 等于 120 安装 H3 的反相。

当只有一对磁对数时。每个霍尔传感器会经过六次磁极对应。

img

电机按一定方向转动时,3 个霍尔的输出会按照 6 步的规律变化如下:

img

结合之前介绍的 BLDC 六步控制,在每个霍尔信号都对应一个 BLDC 控制步,使得 BLDC 旋转一个角度,这样可以制作下表

img

特别注意,一般 BLDC 厂家都会给出一个霍尔传感器和绕组得电情况对应关系表,不一定跟上面两个表都完全对应一致,但是原理分析都是一致的。

接下来一个问题怎么控制电机的速度?

答案当然是控制占空比的大小。但 PWM 控制直流无刷电机的方法有不同的模式。

如下图的四种方法。不同控制方式在性能上有不同的效果,当然针对实际的应用场合可以尝试多种调制方式,然后选择最优方式。

img

H_on-L_pwm:就是上桥臂用高电平,下桥臂用 PWM。H_pwm-L_on 正好与 H_on-L_pwm 相反。

6.无刷驱动板的介绍。

无论直流有刷电机还是无刷电机都会有专门的驱动板去控制。因为 MCU 的输出电压,电流是有限的。而电机的控制一般需要较高的电压或者电流。下面介绍一个半桥电路。另外的半桥都是一样的。

img

Ctrl_PWM_U_H 与 Ctrl_PWM_U_L 接 MCU 一个定时器的互补通道。

首先 TLP715 是 5M 的高速隔离光耦:作用是隔离保护以及把 PWM 的电压抬升到 15V。

当 TLP715 输入高电平那么 Vout 就输出 15v。如果输入低电平那么输出 0v。有两个 TLP715 的输出到 IR2110S.一个做高(HIN)一个做低端输入(LIN)。接着 IR2110S 的有两个输出一个高端输出 (HO) 去控制上桥臂 (Q1)。而低端输出 (LO) 作为下桥臂 (Q2)。对于 Q2 来说要导通是比较容易的。因为 S 是接地的所以 G 点电压只要大于 VGS(看手册)的就可以导通。但对于 Q1 来说 S 极是没有接地的。而 D 极与 S 极之间的电阻比较小。可以近似短路。相当于 S 极就是 VCC_POWER=24V。所以 G 极必须要大于 VCC_POWER+VGS 才能导通。从而引出自举电路。就是把 HO 的电压抬高到 VCC_POWER+VGS 使得 Q1 导通。IR2110S 还有一个使能引脚 SD 并且高电平有效。当为高电平无输出。

采样电流采集电路:只是其中一相。

img

Current_V_amp 通过 ADC 采集得到然后计算出 Isensor_V 的电压。在除以采样电阻就是采样电流的值。MCP6024 是运算放大因为采样电阻两端的电压太小了只是一个放大功能。而 D16 是把电压钳位在 0~3.3v。

总线电压的检测:

img

其实 VCC_POWER=24v 是固定的。但是但上下两个桥臂短路就会变化。通过检测 ADC 采集 V_DC-BUS 就可以计算出 VCC_POWER 是否有变化。

温度的检测:

img

热敏电阻阻值是随温度变化的。通 ADC 采集 V_Thermal 就可以计算出温度。

过流保护:

img

LMV331 是电压比较器:如果 Isensor 的电压大于 Vref 那么 Ctrl_SHUTDOWN 输出低电平。否则高电平。从而去判断是否过流。

7.基本控制代码

思路:用定时器 5 的霍尔传感器接口每当其中任意一相发生边沿跳变捕获并且产生中断。在中断里

先获取引脚的相位接着进行对应的相位切换。定时器 8 用来产生 6 路 PWM。

定时器 5.h

#ifndef __BSP_HALL_H_
#define __BSP_HALL_H_
/* 包含头文件 -*/
#include "stm32f4xx_hal.h"
/* 类型定义 ---*/
/* 宏定义 --*/
/ 定时器霍尔接口 TIM 参数定义,TIMx CH1,CH2,CH3 */
/* 注意:更换 "霍尔接口定时器"的时候需要修改 "PWM 输出定时器 (TIM1/TIM8)" 的触发源  */
#define HALL_TIMx                        TIM5
#define HALL_TIM_RCC_CLK_ENABLE()        __HAL_RCC_TIM5_CLK_ENABLE()
#define HALL_TIM_GPIO_AF                 GPIO_AF2_TIM5
#define HALL_TIM_GPIO_RCC_CLK_ENABLE()   __HAL_RCC_GPIOH_CLK_ENABLE()
#define HALL_TIM_CH1_PORT                GPIOH          // CH1 的引脚
#define HALL_TIM_CH1_PIN                 GPIO_PIN_10
#define HALL_TIM_CH1                     TIM_CHANNEL_1
#define HALL_TIM_CH2_PORT                GPIOH          // CH2 的引脚
#define HALL_TIM_CH2_PIN                 GPIO_PIN_11
#define HALL_TIM_CH2                     TIM_CHANNEL_2
#define HALL_TIM_CH3_PORT                GPIOH          // CH3 的引脚
#define HALL_TIM_CH3_PIN                 GPIO_PIN_12
#define HALL_TIM_CH3                     TIM_CHANNEL_3
#define HALL_TIM_IRQn                    TIM5_IRQn
#define HALL_TIM_IRQHanler               TIM5_IRQHandler
// 定义定时器预分频,定时器实际时钟频率为:84MHz/(HALL_TIMx_PRESCALER+1)
#define HALL_TIM_PRESCALER               83  // 实际时钟频率为:1MHz
// 定义定时器周期,当定时器开始计数到 HALL_TIMx_PERIOD 值是更新定时器并生成对应事件和中断
#define HALL_TIM_PERIOD                  0xFFFF //
/* 扩展变量 ---*/
extern TIM_HandleTypeDef htimx_hall;
/* 函数声明 ---*/
void HALLSensor_TIMx_Init(void); // 初始化
int32_t HALL_GetPhase(void);     // 获取相位

定时器 5.c

#include "hall/bsp_hall.h"
#include "bldc/bsp_bldc.h"
/* 私有类型定义 --*/
TIM_HandleTypeDef htimx_hall;
/**
  * 函数功能: 定时器 HALL 接口引脚初始化
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 无
  */
void YS_TIM_HALL_Base_MspInit(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  HALL_TIM_GPIO_RCC_CLK_ENABLE();
  GPIO_InitStruct.Pin       = HALL_TIM_CH1_PIN;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = HALL_TIM_GPIO_AF;
  GPIO_InitStruct.Pull      = GPIO_PULLUP;
  HAL_GPIO_Init(HALL_TIM_CH1_PORT, &GPIO_InitStruct);
  GPIO_InitStruct.Pin       = HALL_TIM_CH2_PIN;
  HAL_GPIO_Init(HALL_TIM_CH2_PORT, &GPIO_InitStruct);
  GPIO_InitStruct.Pin       = HALL_TIM_CH3_PIN;
  HAL_GPIO_Init(HALL_TIM_CH3_PORT, &GPIO_InitStruct);
  HAL_NVIC_SetPriority(HALL_TIM_IRQn, 1, 1);
  HAL_NVIC_EnableIRQ(HALL_TIM_IRQn);
}
/**
  * 函数功能: 定时器霍尔传感器接口初始化
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 无
  */
void HALLSensor_TIMx_Init(void)
{
  TIM_HallSensor_InitTypeDef sHallConfig = {0};
  /* 通用定时器外设时钟使能 */
  HALL_TIM_RCC_CLK_ENABLE();
  /* 初始化板载设备,定时器通道引脚,使能时钟 */
  YS_TIM_HALL_Base_MspInit();
  //
  /* 配置定时器基础计数功能 */
  htimx_hall.Instance           = HALL_TIMx;          // 定时器 TIM
  htimx_hall.Init.Prescaler     = HALL_TIM_PRESCALER; // PSC 设置预分频值
  htimx_hall.Init.CounterMode   = TIM_COUNTERMODE_CENTERALIGNED1;// 中心对齐模式
  htimx_hall.Init.Period        = HALL_TIM_PERIOD;    // 计数周期
  htimx_hall.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  sHallConfig.Commutation_Delay = 0x06; // 延迟触发,7us,实际测试需要 7us
  sHallConfig.IC1Filter         = 0x0F; // 输入滤波;
  sHallConfig.IC1Polarity       = TIM_ICPOLARITY_RISING;// 输入捕获极性//不起作用
  sHallConfig.IC1Prescaler      = TIM_ICPSC_DIV1;       // 输入捕获预分频
  HAL_TIMEx_HallSensor_Init(&htimx_hall, &sHallConfig);
}
/**
  * 函数功能: 读取霍尔引脚状态
  * 输入参数: 无
  * 返 回 值: 霍尔引脚状态
  * 说    明: 直接读取引脚的状态,数据字节的低三位分别对应 UVW(HALL) 的电平状态
  */
int32_t HALL_GetPhase()
{
  int32_t tmp = 0;
  tmp |= HAL_GPIO_ReadPin(HALL_TIM_CH1_PORT, HALL_TIM_CH1_PIN);//U(A)
  tmp <<= 1;
  tmp |= HAL_GPIO_ReadPin(HALL_TIM_CH2_PORT, HALL_TIM_CH2_PIN);//V(B)
  tmp <<= 1;
  tmp |= HAL_GPIO_ReadPin(HALL_TIM_CH3_PORT, HALL_TIM_CH3_PIN);//W(C)
  return (tmp & 0x0007); // 取低三位
}
/**
  * 函数功能: 霍尔传感器回调函数
  * 输入参数: @htim,霍尔传感器接口定时器
  * 返 回 值: 无
  * 说    明:
  */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  int32_t RT_hallPhase = 0; // 霍尔信号
  RT_hallPhase = HALL_GetPhase();     // 获取霍尔引脚的相位
  /* 换相控制 */
  BLDCMotor_PhaseCtrl(RT_hallPhase);
}

定时器 8.h

#ifndef __BLDCMOTOR_TIM_H__
#define __BLDCMOTOR_TIM_H__
/* 包含头文件 -*/
#include "stm32f4xx_hal.h"
/* 类型定义 ---*/
/* 电机方向定义 */
typedef enum
{
  MOTOR_DIR_CW = 0, // 顺时针转动
  MOTOR_DIR_CCW     // 逆时针转动
}MotorDir_Typedef;  // 方向定义
/* 电机使能定义 */
typedef enum
{
  MOTOR_ENABLE  = 0,
  MOTOR_DISABLE
}MotorSta_Typedef ;
/* 宏定义 --*/
#define USE_PMSMMOTOR   // 定义使用的是 PMSM 电机
/ 通用定时器 TIM 参数定义,TIM8 CH1,CH2,CH3 */
#define BLDCMOTOR_TIMx                        TIM8
#define BLDCMOTOR_TIM_RCC_CLK_ENABLE()        __HAL_RCC_TIM8_CLK_ENABLE()
#define BLDCMOTOR_TIM_GPIO_AF                 GPIO_AF3_TIM8
#define BLDCMOTOR_TIM_GPIO_RCC_CLK_ENABLE()   {__HAL_RCC_GPIOI_CLK_ENABLE();\
                                              __HAL_RCC_GPIOH_CLK_ENABLE();}
#define BLDCMOTOR_TIM_CH1_PORT                GPIOI          // TIM8_CH1 的引脚
#define BLDCMOTOR_TIM_CH1_PIN                 GPIO_PIN_5
#define BLDCMOTOR_TIM_CH1                     TIM_CHANNEL_1
#define BLDCMOTOR_TIM_CH2_PORT                GPIOI          // TIM8_CH2 的引脚
#define BLDCMOTOR_TIM_CH2_PIN                 GPIO_PIN_6
#define BLDCMOTOR_TIM_CH2                     TIM_CHANNEL_2
#define BLDCMOTOR_TIM_CH3_PORT                GPIOI          // TIM8_CH3 的引脚
#define BLDCMOTOR_TIM_CH3_PIN                 GPIO_PIN_7
#define BLDCMOTOR_TIM_CH3                     TIM_CHANNEL_3
/* 三个互补通道 */
#define BLDCMOTOR_TIM_CH1N_PORT               GPIOH          // TIM8_CH1N 的引脚
#define BLDCMOTOR_TIM_CH1N_PIN                GPIO_PIN_13
#define BLDCMOTOR_TIM_CH2N_PORT               GPIOH          // TIM8_CH2N 的引脚
#define BLDCMOTOR_TIM_CH2N_PIN                GPIO_PIN_14
#define BLDCMOTOR_TIM_CH3N_PORT               GPIOH          // TIM8_CH3N 的引脚
#define BLDCMOTOR_TIM_CH3N_PIN                GPIO_PIN_15
/* short down 控制 */
#define BLDCMOTOR_SHORTDOWN_RCC_CLK_ENABLE()  __HAL_RCC_GPIOH_CLK_ENABLE()
#define BLDCMOTOR_SHORTDOWN_PORT              GPIOH
#define BLDCMOTOR_SHORTDOWN_PIN               GPIO_PIN_9
#define BLDCMOTOR_ENABLE()                   HAL_GPIO_WritePin(BLDCMOTOR_SHORTDOWN_PORT,BLDCMOTOR_SHORTDOWN_PIN,GPIO_PIN_SET)
#define BLDCMOTOR_DISABLE()                  HAL_GPIO_WritePin(BLDCMOTOR_SHORTDOWN_PORT,BLDCMOTOR_SHORTDOWN_PIN,GPIO_PIN_RESET)
/** 触发源选择,TIM1 作为从定时器,可以连接到 TIM3,TIM4 的 TRGO,由主定时器触发 com 事件
  * TIM5   TIM_TS_ITR3
  * 选择 TIM2 的时候,触发源为 TIR1,选择 TIM4 则触发源是 TIM_TS_ITR2
  */
#define BLDMOTOR_TIM_TS                       TIM_TS_ITR3 // 定时器触发源,ITR3  TIM5->TIM8
// 定义定时器预分频,定时器实际时钟频率为:168MHz/(BLDCMOTOR_TIMx_PRESCALER+1)
#define BLDCMOTOR_TIM_PRESCALER            0  // 实际时钟频率为:168MHz
// 定义定时器周期,当定时器开始计数到 BLDCMOTOR_TIMx_PERIOD 值是更新定时器并生成对应事件和中断
#define BLDCMOTOR_TIM_PERIOD               4200  // 定时器更新频率为:168MHz/(8400)=20KHz,即 50us 定时周期
// 定时器通道 1 初始占空比,实际并没有意义,在代码中实时修改了
#define BLDCMOTOR_TIM_CH1_PULSE            0   // 初始占空比
/* 扩展变量 ---*/
extern TIM_HandleTypeDef htimx_BLDCM;
extern MotorSta_Typedef Motor_State ; // 电机使能状态
extern MotorDir_Typedef Motor_Dir  ;  // 电机方向
/* 函数声明 ---*/
void BLDCMOTOR_TIMx_Init(void);
void BLDCMotor_Start(void);
void BLDCM_Inertia_brake(void);
void BLDCMotor_braking_LowerShort(void);
void BLDCMotor_unbraking_LS(void);
void BLDCMotor_SetSpeed(float speed);
void BLDCMotor_PhaseCtrl(int32_t HALLPhase );
#endif	/* __BLDCMOTOR_TIM_H__ */

定时器 8.c

#include "bldc/bsp_bldc.h"
#include "hall/bsp_hall.h"
/* 私有类型定义 --*/
/* 私有宏定义 -*/
/* 私有变量 ---*/
TIM_HandleTypeDef htimx_BLDCM;
/* 扩展变量 ---*/
MotorSta_Typedef Motor_State = MOTOR_DISABLE; // 电机使能状态
MotorDir_Typedef Motor_Dir   = MOTOR_DIR_CCW;  // 电机方向 ,顺时针
float PWM_Duty   = 0.25f;        // 25%占空比
/* 私有函数原形 --*/
/* 函数体 --*/
/**
  * 函数功能: 通用定时器硬件初始化配置
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 无
  */
void YS_TIM_Base_MspInit(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* 定时器通道功能引脚端口时钟使能 */
  BLDCMOTOR_TIM_GPIO_RCC_CLK_ENABLE();
  /* 定时器通道 1 功能引脚 IO 初始化 */
  GPIO_InitStruct.Pin       = BLDCMOTOR_TIM_CH1_PIN;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = BLDCMOTOR_TIM_GPIO_AF;
  HAL_GPIO_Init(BLDCMOTOR_TIM_CH1_PORT, &GPIO_InitStruct);
  /* 定时器通道 2 功能引脚 IO 初始化 */
  GPIO_InitStruct.Pin       = BLDCMOTOR_TIM_CH2_PIN;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = BLDCMOTOR_TIM_GPIO_AF;
  HAL_GPIO_Init(BLDCMOTOR_TIM_CH2_PORT, &GPIO_InitStruct);
  /* 定时器通道 3 功能引脚 IO 初始化 */
  GPIO_InitStruct.Pin       = BLDCMOTOR_TIM_CH3_PIN;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = BLDCMOTOR_TIM_GPIO_AF;
  HAL_GPIO_Init(BLDCMOTOR_TIM_CH3_PORT, &GPIO_InitStruct);
  /* 定时器互补通道 1 功能引脚 IO 初始化 */
  GPIO_InitStruct.Pin       = BLDCMOTOR_TIM_CH1N_PIN;
  HAL_GPIO_Init(BLDCMOTOR_TIM_CH3N_PORT, &GPIO_InitStruct);
  /* 定时器互补通道 1 功能引脚 IO 初始化 */
  GPIO_InitStruct.Pin       = BLDCMOTOR_TIM_CH2N_PIN;
  HAL_GPIO_Init(BLDCMOTOR_TIM_CH3N_PORT, &GPIO_InitStruct);
  /* 定时器互补通道 1 功能引脚 IO 初始化 */
  GPIO_InitStruct.Pin       = BLDCMOTOR_TIM_CH3N_PIN;
  HAL_GPIO_Init(BLDCMOTOR_TIM_CH3N_PORT, &GPIO_InitStruct);
  /* shortdown 控制引脚初始化 */
  BLDCMOTOR_SHORTDOWN_RCC_CLK_ENABLE();
  BLDCMOTOR_ENABLE(); // 将引脚设置为有效电平,是 PWM 能控制电机
  GPIO_InitStruct.Pin       = BLDCMOTOR_SHORTDOWN_PIN;
  GPIO_InitStruct.Mode      = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = 0;
  GPIO_InitStruct.Pull      = GPIO_NOPULL;
  HAL_GPIO_Init(BLDCMOTOR_SHORTDOWN_PORT, &GPIO_InitStruct);
  BLDCMOTOR_ENABLE(); // 将引脚设置为有效电平,使 PWM 能控制电机
}
/**
  * 函数功能: 高级定时器初始化
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 无
  */
void BLDCMOTOR_TIMx_Init(void)
{
  TIM_OC_InitTypeDef sConfigOC = {0};
  /* 通用定时器外设时钟使能 */
  BLDCMOTOR_TIM_RCC_CLK_ENABLE();
  /* 初始化板载设备,定时器通道引脚,使能时钟 */
  YS_TIM_Base_MspInit();
  /* 配置定时器基础计数功能 */
  htimx_BLDCM.Instance           = BLDCMOTOR_TIMx;          // 定时器 TIM
  htimx_BLDCM.Init.Prescaler     = BLDCMOTOR_TIM_PRESCALER; // PSC 设置预分频值
  htimx_BLDCM.Init.CounterMode   = TIM_COUNTERMODE_CENTERALIGNED1;// 中心对齐模式
  htimx_BLDCM.Init.Period        = BLDCMOTOR_TIM_PERIOD;    // 计数周期
  htimx_BLDCM.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htimx_BLDCM);
  /* 配置比较输出为 PWM1 模式 */
  sConfigOC.OCMode       = TIM_OCMODE_PWM1;        // PWM1 模式
  sConfigOC.Pulse        = BLDCMOTOR_TIM_CH1_PULSE;// CHx 的输出脉冲宽度
  sConfigOC.OCPolarity   = TIM_OCPOLARITY_HIGH;    // 输出极性
  sConfigOC.OCNPolarity  = TIM_OCNPOLARITY_HIGH;   // 互补通道输出极性
  sConfigOC.OCIdleState  = TIM_OCIDLESTATE_RESET;  // 空闲状态极性
  sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; // 互补空闲状态极性
  sConfigOC.OCFastMode   = TIM_OCFAST_DISABLE;     // 快速输出模式,禁止
  /* 这里使用的是 HAL_TIM_OC_ConfigChannel,而不是 HAL_TIM_PWM_ConfigChannel
   * 如果使用 HAL_TIM_PWM_ConfigChannel 会使能比较器的预装载功能,也就是修改比较值
   * 之后不是立即生效,这将会造成换相之后的第一个脉冲脉宽不固定
   * 这两个函数实际的配置几乎完全一样,只是 HAL_TIM_OC_ConfigChannel 不会去配置
   * OCFastmode 和比较器的预装载使能.
   *
   */
  HAL_TIM_OC_ConfigChannel(&htimx_BLDCM, &sConfigOC, BLDCMOTOR_TIM_CH1);
  /* CH2 CH2N*/
  HAL_TIM_OC_ConfigChannel(&htimx_BLDCM, &sConfigOC, BLDCMOTOR_TIM_CH2);
  /* CH3 CH3N */
  HAL_TIM_OC_ConfigChannel(&htimx_BLDCM, &sConfigOC, BLDCMOTOR_TIM_CH3);
  /* 配置 COM 事件 (commutation)
   * 由主定时器的 TRGO 触发 TIM1 的 COM 事件
   * 当触发 COM 事件的时候将会统一更新定时器通道输出的使能控制位,避免由于程序执行
   * 的延迟导致换相时刻下桥臂出现负窄脉冲.
   */
  //HAL_TIMEx_ConfigCommutationEvent(&htimx_BLDCM, BLDMOTOR_TIM_TS, TIM_COMMUTATION_TRGI);
}
/**
  * 函数功能: 无刷电机相位控制
  * 输入参数: @HALLPhase 霍尔信号相位
  * 返 回 值: 无
  * 说    明: 控制定时器输出 PWM 波形换相,定义定时器输出通道 CH1 为 A 相 (U)
  *           CH2 为 B 相 (V),CH3 为 C 相 (W),配置下一次霍尔换相的时候的波形
  */
void BLDCMotor_PhaseCtrl(int32_t HALLPhase )
{
#ifndef USE_PMSMMOTOR
  /* 顺时针的霍尔顺序与逆时针的顺序是关于 7 互补,所以可以使用 7 减逆时针的顺序得到
   * 顺时针的霍尔顺序,这里使用异或代替减法
   */
  if(MOTOR_DIR_CW == Motor_Dir)
    HALLPhase = 0x07 ^ HALLPhase;// 将低三位异或 111b ^ 010b -> 101b
#else
  /* PMSM 的旋转顺序跟 BLDC 刚好是相反的,设定为 MOTOR_DIR_CCW 的时候 BLDC 是逆时针旋转
   * PMSM 是顺时针旋转,如果需要设定为 MOTOR_DIR_CCW 的时候 PMSM 为逆时针旋转,则使用
   * 下面语句代替上面的语句
   */
  if(MOTOR_DIR_CCW == Motor_Dir)
    HALLPhase = 0x07 ^ HALLPhase;// 将低三位异或 111b ^ 010b -> 101b
#endif
  switch(HALLPhase)
  {
    /* 定义电机的 U(A),V(B),W(C) 三相分别对应是 CH1,CH2,CH3;
     *  A+,A-分别表示 CH1 控制的上,下桥臂导通
     */
    case 5: //B+  A-
    {
      /*  Channe3 configuration */
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      /*  Channe2 configuration  */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH2,BLDCMOTOR_TIM_PERIOD * PWM_Duty);
      HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      /*  Channe1 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH1,BLDCMOTOR_TIM_PERIOD +1);
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
    }
    break;
    case 4:// C+ A-
    {
      /*  Channe2 configuration */
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      /*  Channe3 configuration  */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH3,BLDCMOTOR_TIM_PERIOD * PWM_Duty);
      HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      /*  Channe1 configuration  */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH1,BLDCMOTOR_TIM_PERIOD +1);
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
    }
      break;
    case 6://C+ B-
    {
      /*  Channe1 configuration  */
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      /*  Channe3 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH3,BLDCMOTOR_TIM_PERIOD * PWM_Duty);
      HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      /*  Channe2 configuration  */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH2,BLDCMOTOR_TIM_PERIOD +1);
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
    }
      break;
    case 2: // A+ B-
    {
      /*  Channe3 configuration */
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      /*  Channe1 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH1,BLDCMOTOR_TIM_PERIOD * PWM_Duty);
      HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      /*  Channe2 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH2,BLDCMOTOR_TIM_PERIOD +1);
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
    }
      break;
    case 3:// A+ C-
    {
      /*  Channe2 configuration */
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      /*  Channe1 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH1,BLDCMOTOR_TIM_PERIOD * PWM_Duty);
      HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      /*  Channe3 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH3,BLDCMOTOR_TIM_PERIOD +1);
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
    }
      break;
    case 1: // B+ C-
    {
      /*  Channe1 configuration */
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
      /*  Channe2 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM,BLDCMOTOR_TIM_CH2, BLDCMOTOR_TIM_PERIOD * PWM_Duty);
      HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
      /*  Channe3 configuration */
      __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH3, BLDCMOTOR_TIM_PERIOD +1);
      HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
      HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
    }
    break;
  }
}
/**
  * 函数功能: 启动电机
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 启动无刷电机
  */
void  BLDCMotor_Start()
{
  int32_t hallPhase = 0;
  BLDCMOTOR_ENABLE();// 使能允许输出 PWM
  /* 下桥臂导通,给自举电容充电 */
  BLDCMotor_braking_LowerShort();
  HAL_Delay(9); //充电时间, 大概值 不需要准确
  /* 启动 HALL 传感器接口中断 */
  __HAL_TIM_CLEAR_FLAG(&htimx_hall,TIM_FLAG_CC1);
  HAL_TIMEx_HallSensor_Start_IT(&htimx_hall);
  hallPhase = HALL_GetPhase(); // 获取霍尔信号相位
  /* 配置当前霍尔信号对应的 PWM 相位 */
  BLDCMotor_PhaseCtrl(hallPhase);  // 配置输出 PWM
  HAL_TIM_GenerateEvent(&htimx_BLDCM, TIM_EVENTSOURCE_COM); // 软件生成 COM 事件
  __HAL_TIM_CLEAR_FLAG(&htimx_BLDCM, TIM_FLAG_COM);
  Motor_State = MOTOR_ENABLE;
}
/**
  * 函数功能: 惯性刹车
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 自由停机,就是直接切断输出,依靠惯性是电机停下来
  */
void BLDCM_Inertia_brake()
{
  BLDCMOTOR_DISABLE(); // 使用驱动芯片的 shutdown 引脚切断输出
  Motor_State = MOTOR_DISABLE;
}
/**
  * 函数功能: 刹车制动
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 使用下桥臂使电机短路,属于能耗制动的一种方式
  */
void BLDCMotor_braking_LowerShort()
{
  /* 先禁止中断,防止刹车过程触发 COM 事件重新输出 */
  HAL_TIMEx_HallSensor_Stop_IT(&htimx_hall);
  /**
    * 直接关闭 MOE,使下桥臂输出高电平
    * 直接控制 MOS 管下桥臂导通,上桥臂关闭
    */
  /* 下桥臂导通 */
  __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH1, 0);// 如果需要导通上桥臂
  __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH2, 0);// 占空比设置为 100%
  __HAL_TIM_SET_COMPARE(&htimx_BLDCM, BLDCMOTOR_TIM_CH3, 0);// 即可
  HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
  HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
  HAL_TIM_PWM_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
  HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
  HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
  HAL_TIMEx_PWMN_Start(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
  HAL_TIM_GenerateEvent(&htimx_BLDCM, TIM_EVENTSOURCE_COM);
  __HAL_TIM_CLEAR_FLAG(&htimx_BLDCM, TIM_FLAG_COM);
  Motor_State = MOTOR_DISABLE;
}
/**
  * 函数功能: 取消刹车制动
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 刹车的时候使用下桥臂导通的方式,确认停下来之后取消刹车制动模式
  */
void BLDCMotor_unbraking_LS()
{
  /* 关闭输出 */
  HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
  HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
  HAL_TIM_PWM_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
  HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH1);
  HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH2);
  HAL_TIMEx_PWMN_Stop(&htimx_BLDCM, BLDCMOTOR_TIM_CH3);
  //HAL_TIM_GenerateEvent(&htimx_BLDCM, TIM_EVENTSOURCE_COM);
}
/**
  * 函数功能: 设置电机转速
  * 输入参数: @speed PWM 的占空比
  * 返 回 值: 无
  * 说    明: 无
  */
void BLDCMotor_SetSpeed(float speed)
{
  if(speed > 1.0f)
  {
    speed = 1.0f;
  }
  else if(speed < 0.0f)
  {
    speed = 0.0f;
  }
  PWM_Duty = speed;
}

main.c

#include "main.h"
#include "stm32f4xx_hal.h"
#include "bldc/bsp_bldc.h"
#include "hall/bsp_hall.h"
#include "key/bsp_key.h"
/* 私有类型定义 --*/
float MotorSpeed = 0.0f ;// 电机转速,这里是占空比,
/* 私有宏定义 -*/
/* 私有变量 ---*/
/* 扩展变量 ---*/
extern MotorSta_Typedef Motor_State; // 电机使能状态
extern MotorDir_Typedef Motor_Dir;  // 电机方向 ,顺时针
extern float PWM_Duty;        // 25%占空比
/* 私有函数原形 --*/
/* 函数体 --*/
/**
  * 函数功能: 系统时钟配置
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 无
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  __HAL_RCC_PWR_CLK_ENABLE();                                     // 使能 PWR 时钟
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);  // 设置调压器输出电压级别 1
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;      // 外部晶振,8MHz
  RCC_OscInitStruct.HSEState       = RCC_HSE_ON;                  // 打开 HSE
  RCC_OscInitStruct.PLL.PLLState   = RCC_PLL_ON;                  // 打开 PLL
  RCC_OscInitStruct.PLL.PLLSource  = RCC_PLLSOURCE_HSE;           // PLL 时钟源选择 HSE
  RCC_OscInitStruct.PLL.PLLM = 8;                                 // 8 分频 MHz
  RCC_OscInitStruct.PLL.PLLN = 336;                               // 336 倍频
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;                     // 2 分频,得到 168MHz 主时钟
  RCC_OscInitStruct.PLL.PLLQ = 7;                                 // USB/SDIO/随机数产生器等的主 PLL 分频系数
  HAL_RCC_OscConfig(&RCC_OscInitStruct);
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource   = RCC_SYSCLKSOURCE_PLLCLK;     // 系统时钟:168MHz
  RCC_ClkInitStruct.AHBCLKDivider  = RCC_SYSCLK_DIV1;             // AHB 时钟: 168MHz
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;               // APB1 时钟:42MHz
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;               // APB2 时钟:84MHz
  HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
  HAL_RCC_EnableCSS();                                            // 使能 CSS 功能,优先使用外部晶振,内部时钟源为备用
  // HAL_RCC_GetHCLKFreq()/1000    1ms 中断一次
  // HAL_RCC_GetHCLKFreq()/100000	 10us 中断一次
  // HAL_RCC_GetHCLKFreq()/1000000 1us 中断一次
  HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000);               // 配置并启动系统滴答定时器
  /* 系统滴答定时器时钟源 */
  HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
  /* 系统滴答定时器中断优先级配置 */
  HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
/**
  * 函数功能: 主函数.
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明: 无
  */
int main(void)
{
  /* 复位所有外设,初始化 Flash 接口和系统滴答定时器 */
  HAL_Init();
  /* 配置系统时钟 */
  SystemClock_Config();
  HAL_Delay(100);
  /* 按键初始化 */
  KEY_GPIO_Init();
  /* 初始化定时器各通道输出 */
  BLDCMOTOR_TIMx_Init();
  /* 霍尔传感器初始化 */
  HALLSensor_TIMx_Init();
  __HAL_DBGMCU_FREEZE_TIM8(); // debug 的时候停止定时器时钟
  __HAL_DBGMCU_FREEZE_TIM5(); // debug 的时候停止定时器时钟
  Motor_Dir = MOTOR_DIR_CW;
  /* 无限循环 */
  while (1)
  {
    /* 启动电机 */
    if(KEY1_StateRead() == KEY_DOWN)
    {
      MotorSpeed = 0.15f;//15%
      BLDCMotor_SetSpeed( MotorSpeed );
      BLDCMotor_Start();
    }
    /* 改变方向 */
    if(KEY2_StateRead() == KEY_DOWN)
    {
      /* 必须先停止,然后再重新启动 */
      if(  Motor_State == MOTOR_DISABLE)
      {
        if(Motor_Dir == MOTOR_DIR_CW)
        { Motor_Dir = MOTOR_DIR_CCW; }
        else
        { Motor_Dir = MOTOR_DIR_CW; }
      }
    }
    /* 加速 */
    if(KEY3_StateRead() == KEY_DOWN)
    {
      if( MotorSpeed == 0.0f )
      {
        MotorSpeed += 0.15f; //  15% 启动
        BLDCMotor_SetSpeed( MotorSpeed );
        BLDCMotor_Start();
      }else{
        MotorSpeed += 0.05f; // 速度递增 5%
        if( MotorSpeed >= 1.0f ){
           MotorSpeed = 1.0f;
        }
        BLDCMotor_SetSpeed( MotorSpeed );
      }
    }
    /* 减速 */
    if(KEY4_StateRead() == KEY_DOWN)
    {
      MotorSpeed -= 0.05f; // 速度递减 5%
      if( MotorSpeed <= 0.0f ){
         MotorSpeed = 0.0f;
      }
      BLDCMotor_SetSpeed( MotorSpeed );
    }
    /* 刹车 */
    if(KEY5_StateRead() == KEY_DOWN)
    {
      BLDCMotor_braking_LowerShort();
      HAL_Delay(100);// 等 100ms, 电机停下来之后才取消刹车控制
      BLDCMotor_unbraking_LS();
    }
  }
}

8.总结

按下 KEY1:电机开始旋转

按下 KEY2:改变方向(只能在电机停止的时候按 KEY2 才有效,禁止在转动过程中突变方向)

按下 KEY3:加速,速度递增 5%

按下 KEY4:减速, 速度递减 5%

按下 KEY5:IR2110S 的 SD 引脚使能无输出。电机停止。

按键程序太简单就不粘贴了。

直流无刷的实物图

img

对于六步换相来说。每次转 60 度就换一次相精度是比较差的。所以一般很少有位置环的控制。并且每次换相前。都要先断开上一次的相。所以电流会不连续。就会导致力矩的不平稳。这也是六步换相所不足的地方。但 FOC 算法就很好的克服了。


【无刷电机学习】BLDC 与 PMSM 的基本驱动原理及六部换相与 FOC 控制精讲(附 DSP28335 相关代码)

置顶白白与瓜已于 2025-09-25 17:35:27 修改

本文是在本人 2024 年本科毕业设计期间的自学记录,更多是对网上资料的摘抄引用整理,不出于任何商业目的,如有侵权,请联系删除,谢谢!
本文写作时间较为匆忙,存在许多表述不严谨甚至错误之处,欢迎在评论区指正!感谢广大网友!

本文推荐前往 PC 端网页食用。

※ 强烈推荐阅读:

  • 夏长亮 著《无刷直流电机控制系统》

  • 袁雷 著《现代永磁同步电机控制原理及 MATLAB 仿真》

0 主要引用出处

1 基本概念

1.1 BLDC 与 PMSM

永磁无刷电机,也称为电子换向电动机,是一种没有电刷和换向器的电动机,根据转子永磁体位置调整定子电流以产生相应转矩。夏长亮老师在其书《无刷直流电机控制系统》中写道,国内外对无刷直流电机的定义一般有两种:

  • 一种定义认为只有梯形波/方波无刷直流电机才可以被称为无刷直流电机BLDC,即 Brushless DC Motor),而正弦波无刷电机则被称为永磁同步电机PMSM,即 Permanent Magnet Synchronous Motor);

img

  • 另一种定义认为梯形波/方波无刷电机和正弦波无刷电机都是无刷直流电机。

但迄今为止,还没有一个公认的统一标准对无刷直流电机进行准确的分类或者定义。本文采用第一种定义,以反电动势**(可对转子外施扭矩,测量定子开路的 A 相电压从而得反电势波形)区分二者。一般来说,BLDC 的定子绕组通常采用集中整距绕组**,具有梯形波反电势;而 PMSM 则往往使用分布短距绕组,具有正弦波反电势。不过,二者对应绕组并不绝对,若还要深究二者区别可参看:知乎彻底搞懂 BLDC 与 PMSM 的区别

img

电机PMSM(永磁同步电机)BLDC(无刷直流电机)
换相方式正弦波换相(Sine wave commutation梯形波换相(Trapezoidal commutation
控制算法通常使用 FOC(磁场定向控制)通常使用六步换相控制
电动势波形(EMF)正弦波梯形波
转矩输出更平滑(几乎无力矩波动)有一定转矩脉动
复杂度和成本控制更复杂,成本略高控制简单,成本较低
应用场景高频精密控制、电动汽车电机等泵、风扇、电动工具、小型电动车

1.2 物理结构

永磁有刷直流电动机和永磁无刷直流电动机,在结构上除了有无电刷之外,还有一个重要区别在于,无刷电机中将电枢和磁极的位置进行了互换,即电枢固定不动放在定子上,而磁极放到旋转的转子上,这样电机结构简单,便于电子换向的实现。

永磁有刷电机:img 永磁无刷电机(内转子式): img

无刷电机由电动机本体位置传感器控制器三部分组成。电动机本体的主要部件是定子和转子。转子由永磁体、铁芯和支撑部件构成。永磁体通常采用径向充磁的铁氧体或多铁硼,做成瓦片形或环形,贴装在转子铁芯表面,这种结构称之为表面贴装式

img

img

1.3 工作特性(可以先跳过)

无刷电机具有串励直流电机启动特性并励直流电机调速特性。无刷电机通常无励磁绕组,但是其特性与有刷电机的特性有相通之处:

  • 启动特性串励直流电机相似

img

串励电动机有很大的起动转矩很强的过载能力。启动瞬间,电机转速接近零,反电动势极小,E ≈ 0,电枢电流 Ia 达到最大值。由于励磁电流与电枢电流相同,电流同时增强了主磁场和电枢磁场,产生极大转矩,此时铁芯还未饱和,有 T\propto\Phi\cdot I_a\propto I_a\cdot I_a=I_a^2

img

虽然无刷电机的磁场由永磁体提供(固定),但启动电流可以很大(由控制器设定)。此外,电子控制器可以选择最佳换相角度,最大化电磁转矩。所以,启动瞬间的转矩输出接近串励电机的**“强磁+大电流”**特性。

  • 调速特性并励直流电机相似

img

由于励磁绕组与电枢绕组是并联接入电源的,所以并励电动机如果端电压 U 不变, 不变,则 If 也不变,当负载电流很小时,电枢反应去磁作用也很小,可认为磁通 为常数, 根据 电磁转矩 T 和电枢电流 Ia 成正比 是通过坐标原点的直线,如下图中的实线所示。另外,直线末端的弯曲则是因为,当负载电流较大时,由于电枢反应的去磁作用增大(近似可看成与负载电流成正比),使每极磁通减少,这时电磁转矩略有减小。

img

无刷电机的永磁转子提供固定磁场(类似并励的“恒励磁”),控制器通过调节供电电压/占空比/PWM 频率,线性地改变绕组电流反电动势,从而进行线性调速。所以,无刷电机的速度控制特性就像并励电机一样**“线性、可控”**。

2 各电机比较

《电机学》书中写道,由于电刷和换向器存在机械接触,换回时产生的换向火花会引起电刷和换回器磨损、电磁干扰、噪声等问题,导致电机可靠性较差,易产生故障需要经常维护,限制了有刷直流电动机的应用场合。要根本解决这些问题,就必须去掉电刷和换向器消除机械接触,这就促成了永磁无刷直流电动机的出现和发展。

各电机比较具体可看笔者的另一篇博客:【无刷电机学习】各种电机优势比较-优快云 博客 ,本文不再赘述。

特性无刷直流电机永磁有刷直流电动机交流感应电动机
定子多相绕组永磁多相绕组事
转子永磁绕组线绕组或笼型绕组
转子位置传感器需要不需要不需要
电滑动接触火花有,换向器与电刷无,或可能有集电环
EMC 干扰较低
可闻噪声较低
电子控制器必需不是必需,调速时需要不是必需,调速时需要
使用电源DCDCAC
使用电压范围高,受功率器件耐压限制较低,受换向器耐压限制
机械特性接近线性线性非线性
起动转矩倍数较高较高较低

3 基本原理

推荐先观看 b 站 up 主爱上半导体视频:无刷直流电机的工作原理 ,本部分作为视频笔记帮助回顾。

3.1 单相无刷电机

初中关于通电螺线管的物理小实验大家应该都不太陌生,基于右手螺旋定则可判断通过直流电螺线圈的极性方向:

img

下图中的单相无刷电机(外转子式)则正是通过变换流入 a、b 的电流方向,从而改变线圈极性,并基于“同性相斥、异性相吸”产生转动。

img

其中,定子的上下线圈的绕向相反,则通电时极性相同

img

流入 a、b 的电流方向则是通过 H 桥上下桥臂的开关组合进行改变(电流的方向可控制电机正反转):

img

利用单片机进行控制时,H 桥上的 S1 - S4 这四个开关由 MOS 管来代替,这样便可通过对 MOS 管进行脉宽调制PWM,即 Pulse Width Modulation)来控制转速:保持 S2 导通,按一定频率开断 S3 ,即对 S3 门极输入 PWM 波进行控制。

【(知识复习)PWM 调速】

img

显然,占空比越高,等效输出的电压就越高。电机线圈电压越大显然线圈电流越大,对应的磁场强度越大,则电机扭矩/转速越大。

img

【为什么 PWM 频率 f_PWM 应该远高于电机时间常数** τ **的倒数?】

当开关频率 f_PWM 较低时,电机表现为反复加速和减速,这是由于电感电流(下图黄线)会发生较明显的升降,导致输出转矩变大变小。

img

而当我们将 PWM 的频率 f_PWM 增加至一个合理值时,电感电流将平稳许多,这将提高调速性能。该合理值应远大于 1/τ,为什么呢?

img

电机常数 τ(τ = L / R,将目前电路视为一阶零状态响应 RL 串联电路模型),用于表示电流变化的快慢,即电流每次要明显上升或下降,至少需要 τ 秒。若 PWM 周期 T ≪ τ(即 f_PWM ≫ 1/τ),则说明电流在每个 PWM 周期内变化很少、较为平滑。

所以,PWM 频率要足够快(远大于 1/τ),才能让电流来不及产生明显的波动,近似恒定,从而使控制效果稳定。

3.2 三相无刷电机

三相无刷电机的三个线圈则是彼此独立的。由于依次导通单个线圈的方式线圈利用率低,故常采取星形连接一次导通两相或三相。

外转子

img

内转子

img

显然,之前的四个 MOS 管已经无法满足驱动要求,此时就需要采用三相逆变器Three-Phase Inverter)对电机进行驱动:

img

其中一个磁极受力矢量动图如下:

img

4 驱动方法及相关控制代码

4.1 BLDC——六步换相控制

4.1.1 基本原理

从 U 相向 W 相通电,则会产生 2 个方向不同的磁场矢量,而这两个磁场矢量可以合成一个指向右下 30° 方向的总磁场。

img

【(知识复习)磁感应强度(磁通密度)B 与磁通量 辨析】

磁场的大小和方向可用基本物理量磁感应强度来描述,用符号 B 表示,单位是 T(特斯拉),是一个矢量。文中的磁场矢量指的就是 B。

通过磁场中某一面积的磁感应线数称为通过该面积的磁通量,简称磁通,用符号 表示。在国际单位制中它的单位是 Wb (韦伯),它是一个标量

img

极对数Number of Pole Pairs为 1 的内转子 BLDC 为例。如上述所示,每次同时控制两个线圈导通,按顺序从 1 - 6 变更通电模式,则合成磁场矢量将顺时针旋转。一般将切换这 6 种通电模式来控制电机的控制方法称为“六步换相控制Six-Step Commutation)”,或称“梯形控制Trapezoidal Control)”。

【(知识复习)极对数 p】

极对数是磁极的对数,决定了电角度与机械角度之间的关系:

\theta_\text{electrical}=p\cdot\theta_\text{mechanical}(4 - 1)

其中,\theta_\text{electrical}Electrical Angle):表示的是旋转磁场的角度,它决定了驱动波形、换相等;\theta_\text{mechanical}(Mechanical Angle):表示的是转子实际旋转的角度,对应物理旋转角度。

【换相】

电机转动时,为了始终让转矩方向合适,需要不断切换供电相位(U、V、W 三相),这个操作叫“换相”。

img img

将“通电模式 1”改为“通电模式 2”,由图可知合成磁场矢量的方向将变化 60°,转子在磁力作用下发生旋转。接下来,从“通电模式 2”改为“通电模式 3”,则合成磁场的方向将再次变化 60°,转子将再次被该变化所吸引而转动。

六步换相这样“通电两相、关闭一相”的控制方式,刚好适配 BLDC 这种“恒定电压段 + 快速过渡段”的梯形波 EMF。该模式控制方法简单、成本低,且不需要复杂矢量变换,但是其旋转动作较为生硬,有时还会发出噪音

img

下图三个电机均采用六步换相控制,从左到右依次为每 60°、每 30°、每 15°(均指机械角度)进行一次换相:

img

【此处的 3 个角度如何得到?】

由前文可知,单极对电机转子旋转一圈需换相 6 次,故每个极对需要进行 6 次换相,我们可以推知,极对数为 2 的转子旋转一圈需完成 12(2 * 6)次换相,则每隔 30°(360° / 12)的机械角度换相一次;极对数为 4 的转子旋转一圈需完成 24(4 * 6)次换相,则每隔 15°(360° / 24)的机械角度换相一次。

由此观之,极对数越多控制精度越高

下图所示即换向逻辑,图中 A(U)、B(V)、C(W) 三个字母代表相(Phase);H 和 L 分别代表高侧(High Side)和低侧(Low Side):

img

基于 DSP28335 的控制各扇区对应开关管导通代码编写如下:

void MOS_Q41PWM(void)
{   // 通电相位:V- U+
    EALLOW;
	EPwm1Regs.AQCSFRC.bit.CSFA = 0; // 1A 无效
	EPwm1Regs.AQCSFRC.bit.CSFB = 1; // 1B 强制低
	EPwm2Regs.AQCSFRC.bit.CSFA = 1; // 2A 连续低(在下一个 TBCLK 边沿发生作用)
	EPwm2Regs.AQCSFRC.bit.CSFB = 2; // 2B 连续高
	EPwm3Regs.AQCSFRC.bit.CSFA = 1;
	EPwm3Regs.AQCSFRC.bit.CSFB = 1;
	EPwm1Regs.AQCTLA.bit.CAU = AQ_SET;  // CTR = CAU 时,将 ePWM1A 置高
	EPwm1Regs.AQCTLA.bit.CAD = AQ_CLEAR;// CTR = CAD 时,将 ePWM1A 置低
	EPwm1Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBU = AQ_SET;  // CTR = CBU 时,将 ePWM2B 置高
	EPwm2Regs.AQCTLB.bit.CBD = AQ_SET;  // CTR = CBD 时,将 ePWM2B 置高
	EPwm3Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EDIS;
}
void  MOS_Q16PWM(void)
{   // 通电相位:U+ W-
    EALLOW;
	EPwm1Regs.AQCSFRC.bit.CSFA = 0;
	EPwm1Regs.AQCSFRC.bit.CSFB = 1;
	EPwm2Regs.AQCSFRC.bit.CSFA = 1;
	EPwm2Regs.AQCSFRC.bit.CSFB = 1;
	EPwm3Regs.AQCSFRC.bit.CSFA = 1;
	EPwm3Regs.AQCSFRC.bit.CSFB = 2;
	EPwm1Regs.AQCTLA.bit.CAU = AQ_SET;  // CTR = CAU 时,将 ePWM1A 置高
	EPwm1Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBU = AQ_SET;  // CTR = CBU 时,将 ePWM3B 置高
	EPwm3Regs.AQCTLB.bit.CBD = AQ_SET;
	EDIS;
}
void MOS_Q63PWM(void)
{   // 通电相位:V+ W-
    EALLOW;
	EPwm1Regs.AQCSFRC.bit.CSFA = 1;
	EPwm1Regs.AQCSFRC.bit.CSFB = 1;
	EPwm2Regs.AQCSFRC.bit.CSFA = 0;
	EPwm2Regs.AQCSFRC.bit.CSFB = 1;
	EPwm3Regs.AQCSFRC.bit.CSFA = 1;
	EPwm3Regs.AQCSFRC.bit.CSFB = 2;
	EPwm1Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm1Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAU = AQ_SET;  // CTR = CAU 时,将 ePWM2A 置高
	EPwm2Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBU = AQ_SET;  // CTR = CBU 时,将 ePWM3B 置高
	EPwm3Regs.AQCTLB.bit.CBD = AQ_SET;
	EDIS;
}
void MOS_Q32PWM(void)
{   // 通电相位:V+ U-
    EALLOW;
	EPwm1Regs.AQCSFRC.bit.CSFA = 1;
	EPwm1Regs.AQCSFRC.bit.CSFB = 2;
	EPwm2Regs.AQCSFRC.bit.CSFA = 0;
	EPwm2Regs.AQCSFRC.bit.CSFB = 1;
	EPwm3Regs.AQCSFRC.bit.CSFA = 1;
	EPwm3Regs.AQCSFRC.bit.CSFB = 1;
	EPwm1Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm1Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBU = AQ_SET;  // CTR = CBU 时,将 ePWM1B 置高
	EPwm1Regs.AQCTLB.bit.CBD = AQ_SET;
	EPwm2Regs.AQCTLA.bit.CAU = AQ_SET;  // CTR = CAU 时,将 ePWM2A 置高
	EPwm2Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EDIS;
}
void MOS_Q25PWM(void)
{   // 通电相位:U- W+
    EALLOW;
	EPwm1Regs.AQCSFRC.bit.CSFA = 1;
	EPwm1Regs.AQCSFRC.bit.CSFB = 2;
	EPwm2Regs.AQCSFRC.bit.CSFA = 1;
	EPwm2Regs.AQCSFRC.bit.CSFB = 1;
	EPwm3Regs.AQCSFRC.bit.CSFA = 0;
	EPwm3Regs.AQCSFRC.bit.CSFB = 1;
	EPwm1Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm1Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBU = AQ_SET;  // CTR = CBU 时,将 ePWM1B 置高
	EPwm1Regs.AQCTLB.bit.CBD = AQ_SET;
	EPwm2Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm3Regs.AQCTLA.bit.CAU = AQ_SET;  // CTR = CAU 时,将 ePWM3A 置高
	EPwm3Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EDIS;
}
void MOS_Q54PWM(void)
{   // 通电相位:V- W+
    EALLOW;
	EPwm1Regs.AQCSFRC.bit.CSFA = 1;
	EPwm1Regs.AQCSFRC.bit.CSFB = 1;
	EPwm2Regs.AQCSFRC.bit.CSFA = 1;
    EPwm2Regs.AQCSFRC.bit.CSFB = 2;
	EPwm3Regs.AQCSFRC.bit.CSFA = 0;
	EPwm3Regs.AQCSFRC.bit.CSFB = 1;
	EPwm1Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm1Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm1Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAU = AQ_CLEAR;
	EPwm2Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm2Regs.AQCTLB.bit.CBU = AQ_SET;
	EPwm2Regs.AQCTLB.bit.CBD = AQ_SET;
	EPwm3Regs.AQCTLA.bit.CAU = AQ_SET;
	EPwm3Regs.AQCTLA.bit.CAD = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBU = AQ_CLEAR;
	EPwm3Regs.AQCTLB.bit.CBD = AQ_CLEAR;
	EDIS;
}
4.1.2 系统框图及代码

img

基于 DSP28335 的控制开关管占空比代码编写如下(占空比从何而来将会在后文转速测算处给出):

void Svpwm_Outpwm(Uint16 duty)
{
   EPwm1Regs.CMPA.half.CMPA = duty;
   EPwm1Regs.CMPB = duty;
   EPwm2Regs.CMPA.half.CMPA = duty;
   EPwm2Regs.CMPB = duty;
   EPwm3Regs.CMPA.half.CMPA = duty;
   EPwm3Regs.CMPB = duty;
}

另附:

【1】基于 DSP28335 的六步换向 ePWM 初始化代码:

#define  ISR_FREQUENCY      12.5
#define  SYSTEM_FREQUENCY   150
float32 T = 0.001/ISR_FREQUENCY;
// T 为采样周期 (s),其中开关频率 ISR_FREQUENCY 数值为 12.5(kHz),故转换为 s 作为单位时需要*1/1000。
// 开关频率 ISR_FREQUENCY 此处设为 12.5(12.5kHZ),则采样周期 T 为 0.00008s 即 0.08ms(80us)。
// 在电机控制中,采样频率一般与开关频率相同。
void EPWM_int(void)
{
    // 150MHz,即 1s 之中计数 150M 次,则一个采样周期内计数 (150M*T) 次
    // 注意!赋予寄存器的为计数值!
    // 因为在向上下模式计数时,Tpwm = 2*TBPRD*T(TBCLK),所以 TBPRD(即 PeriodMax) 为一个采样周期计数值的 1/2,即 (150M*T)/2 次
    PWM_PeriodMax  = SYSTEM_FREQUENCY*1000000*T/2;  // 6000
//    PWM_HalfPerMax = PWM_PeriodMax/2;   // HalfPerMax 为 TBPRD/2
    PWM_Deadband   = 2.0*SYSTEM_FREQUENCY;
    EALLOW;
    /* 初始化 EPWM1-EPWM3 时基周期寄存器 */
    EPwm1Regs.TBPRD = PWM_PeriodMax;    // Set timer period   1500
    EPwm2Regs.TBPRD = PWM_PeriodMax;    // Set timer period   1500
    EPwm3Regs.TBPRD = PWM_PeriodMax;    // Set timer period   1500
    /* 初始化 EPWM1-EPWM3 时基相位寄存器 */
    EPwm1Regs.TBPHS.half.TBPHS = 0x0000;    // Phase is 0
    EPwm2Regs.TBPHS.half.TBPHS = 0x0000;    // Phase is 0
    EPwm3Regs.TBPHS.half.TBPHS = 0x0000;    // Phase is 0
    // Clear counter
    EPwm1Regs.TBCTR = 0x0000;
    EPwm2Regs.TBCTR = 0x0000;
    EPwm3Regs.TBCTR = 0x0000;
    /* 初始化 EPWM1-EPWM3 时基控制寄存器 */
    // 计数模式 CTRMODE,0x2(10):向上-下计数
    // 计数寄存器装载相位寄存器使能位 PHSEN,0x0:禁止装载
    // 高速时基时钟分频位 HSPCLKDIV,0x0:/1
    // 时基时钟分频位 CLKDIV,0x0:/1
    // TBCLK = SYSCLKOUT/(HSPCLKDIV × CLKDIV)
    EPwm1Regs.TBCTL.bit.CTRMODE = 0x2;
    EPwm1Regs.TBCTL.bit.PHSEN   = 0x0;
    EPwm1Regs.TBCTL.bit.HSPCLKDIV = 0x0;
    EPwm1Regs.TBCTL.bit.CLKDIV    = 0x0;   //??  0         60M /  1*1*2  / 2*1500  =  10K
    EPwm2Regs.TBCTL.bit.CTRMODE = 0x2;
    EPwm2Regs.TBCTL.bit.PHSEN   = 0x0;
    EPwm2Regs.TBCTL.bit.HSPCLKDIV = 0x0;
    EPwm2Regs.TBCTL.bit.CLKDIV    = 0x0;
    EPwm3Regs.TBCTL.bit.CTRMODE = 0x2;
    EPwm3Regs.TBCTL.bit.PHSEN   = 0x0;
    EPwm3Regs.TBCTL.bit.HSPCLKDIV = 0x0;
    EPwm3Regs.TBCTL.bit.CLKDIV    = 0x0;
    /* 初始化 EPWM1-EPWM3 计数比较控制寄存器 */
    EPwm1Regs.CMPCTL.bit.SHDWAMODE = 0x0;
    EPwm1Regs.CMPCTL.bit.SHDWBMODE = 0x0;   //Active Counter-CompareA(CMPA) Load From Shadow Select Mode   0
    EPwm1Regs.CMPCTL.bit.LOADAMODE = 0x0;   //HIKE, P113 Load registers every ZERO   0  TBCTR=0
    EPwm1Regs.CMPCTL.bit.LOADBMODE = 0x0;
    EPwm2Regs.CMPCTL.bit.SHDWAMODE = 0x0;
    EPwm2Regs.CMPCTL.bit.SHDWBMODE = 0x0;   //Active Counter-CompareA(CMPA) Load From Shadow Select Mode   0
    EPwm2Regs.CMPCTL.bit.LOADAMODE = 0x0;   //HIKE, P113 Load registers every ZERO   0  TBCTR=0
    EPwm2Regs.CMPCTL.bit.LOADBMODE = 0x0;
    EPwm3Regs.CMPCTL.bit.SHDWAMODE = 0x0;
    EPwm3Regs.CMPCTL.bit.SHDWBMODE = 0x0;   //Active Counter-CompareA(CMPA) Load From Shadow Select Mode   0
    EPwm3Regs.CMPCTL.bit.LOADAMODE = 0x0;   //HIKE, P113 Load registers every ZERO   0  TBCTR=0
    EPwm3Regs.CMPCTL.bit.LOADBMODE = 0x0;
    // Setup compare
    EPwm1Regs.CMPA.half.CMPA = 0x0;  //   1350 占空比/  1500
    EPwm1Regs.CMPB = 0x0;             //  同样
    EPwm2Regs.CMPA.half.CMPA = 0x0;  //   1350 占空比/  1500
    EPwm2Regs.CMPB = 0x0;
    EPwm3Regs.CMPA.half.CMPA = 0x0;  //   1350 占空比/  1500
    EPwm3Regs.CMPB = 0x0;
    // 死区控制寄存器 DBCTL
    // 死区模块输出控制 OUT_MODE,3(11):使能双边延时
    EPwm1Regs.DBCTL.bit.OUT_MODE = 3;   // DB_FULL_ENABLE;//Dead-bandis fully enabledfor both rising-edge delay onoutput EPWMxA and falling-edge  死去延时对于 EPWMxA 上升沿      EPWMxB 下降沿
    EPwm2Regs.DBCTL.bit.OUT_MODE = 3;   // DB_FULL_ENABLE;//Dead-bandis fully enabledfor both rising-edge delay onoutput EPWMxA and falling-edge  死去延时对于 EPWMxA 上升沿      EPWMxB 下降沿
    EPwm3Regs.DBCTL.bit.OUT_MODE = 3;   // DB_FULL_ENABLE;//Dead-bandis fully enabledfor both rising-edge delay onoutput EPWMxA and falling-edge  死去延时对于 EPWMxA 上升沿      EPWMxB 下降沿
    // 极性选择控制 POLSEL,0:都不翻转
    EPwm1Regs.DBCTL.bit.POLSEL = 0;     // Active low (AL)mode.Both EPWMxA and EPWMxB are   不可以反相     inverted
    EPwm2Regs.DBCTL.bit.POLSEL = 0;     // Active low (AL)mode.Both EPWMxA and EPWMxB are   不可以反相     inverted
    EPwm3Regs.DBCTL.bit.POLSEL = 0;     // Active low (AL)mode.Both EPWMxA and EPWMxB are   不可以反相     inverted
    // 死区模块输入控制 IN_MODE,2(10):ePWMxA 是上升沿延时输入源,ePWMxB 是下降沿输入源
    EPwm1Regs.DBCTL.bit.IN_MODE = 2;    // EPWMxA In (from the action-qualifier)is the source for both falling-edge and rising-edge delay 输入死去延时信号
    EPwm2Regs.DBCTL.bit.IN_MODE = 2;    // EPWMxA In (from the action-qualifier)is the source for both falling-edge and rising-edge delay 输入死去延时信号
    EPwm3Regs.DBCTL.bit.IN_MODE = 2;    // EPWMxA In (from the action-qualifier)is the source for both falling-edge and rising-edge delay 输入死去延时信号
    /* 初始化 EPWM1-EPWM3 死区上升沿、下降沿延时寄存器 */                              \
    /* PWM_Deadband = 2.0*SYSTEM_FREQUENCY */                                 \
    /* 计算边沿延时的计算公式:FED=DBFED*T(TBCLK); RED=DBRED*T(TBCLK) */
    EPwm1Regs.DBRED = PWM_Deadband; //EPWM1_MIN_DB  //Dead-Band Generator Rising Edge Delay Register
    EPwm1Regs.DBFED = PWM_Deadband; //EPWM1_MIN_DB;
    EPwm2Regs.DBRED = PWM_Deadband; //EPWM1_MIN_DB  //Dead-Band Generator Rising Edge Delay Register
    EPwm2Regs.DBFED = PWM_Deadband; //EPWM1_MIN_DB;
    EPwm3Regs.DBRED = PWM_Deadband; //EPWM1_MIN_DB  //Dead-Band Generator Rising Edge Delay Register
    EPwm3Regs.DBFED = PWM_Deadband; //EPWM1_MIN_DB;
    // 动作连续软件强制寄存器 AQCSFRC
    EPwm1Regs.AQCSFRC.all = 0x00;
    EPwm2Regs.AQCSFRC.all = 0x00;
    EPwm3Regs.AQCSFRC.all = 0x00;
    EDIS; // Disable EALLOW
}

【2】基于 DSP28335 的过功率保护代码:

void HVDMC_Protection(void)
{
    EALLOW;
    EPwm1Regs.TZSEL.bit.CBC6 = 0x1;
    EPwm2Regs.TZSEL.bit.CBC6 = 0x1;
    EPwm3Regs.TZSEL.bit.CBC6 = 0x1;
    EPwm1Regs.TZSEL.bit.OSHT1 = 1;  //enable TZ1 for OSHT
    EPwm2Regs.TZSEL.bit.OSHT1 = 1;  //enable TZ1 for OSHT
    EPwm3Regs.TZSEL.bit.OSHT1 = 1;  //enable TZ1 for OSHT
    EPwm1Regs.TZCTL.bit.TZA = TZ_FORCE_LO; // EPWMxA will go low
    EPwm1Regs.TZCTL.bit.TZB = TZ_FORCE_LO; // EPWMxB will go low
    EPwm2Regs.TZCTL.bit.TZA = TZ_FORCE_LO; // EPWMxA will go low
    EPwm2Regs.TZCTL.bit.TZB = TZ_FORCE_LO; // EPWMxB will go low
    EPwm3Regs.TZCTL.bit.TZA = TZ_FORCE_LO; // EPWMxA will go low
    EPwm3Regs.TZCTL.bit.TZB = TZ_FORCE_LO; // EPWMxB will go low
    EDIS;
}

【3】基于 DSP28335 的控制电机启停代码:

void STOP_CAR(void) // 上下桥臂全为低,关闭六个管
{
	EALLOW;
    EPwm1Regs.AQCSFRC.bit.CSFA=1;
    EPwm1Regs.AQCSFRC.bit.CSFB=1;
    EPwm2Regs.AQCSFRC.bit.CSFA=1;
    EPwm2Regs.AQCSFRC.bit.CSFB=1;
    EPwm3Regs.AQCSFRC.bit.CSFA=1;
    EPwm3Regs.AQCSFRC.bit.CSFB=1;
    EDIS;
}
void START_CAR(void)   //   上下桥臂 对称互补
{
    EALLOW;
    EPwm1Regs.AQCSFRC.all = 0x00;
    EPwm2Regs.AQCSFRC.all = 0x00;
    EPwm3Regs.AQCSFRC.all = 0x00;
    EDIS;
}
4.1.3 速度和扭矩波动原因

下图中青色矢量表示转子(永磁体)磁场方向与大小、紫色矢量表示定子磁场方向与大小:

img

从图中可以看出,六步换相存在两个主要问题:

  • 定子的磁动势矢量被固定在了 6 个方向上,在各方向之间的切换是跳变的,无法连续旋转;

  • 二者磁场夹度一直在 60° 和 120° 之间波动,这使得我们无法持续获得最大扭矩(磁场夹度为 90° 时,扭矩最大)。

这些便是速度和扭矩波动背后的原因,而磁场定向控制(即后文的 FOC 控制)便可很好地解决这些问题。

img

4.2 PMSM——磁场定向控制(FOC)

关于 FOC 控制大佬们已写得足够详尽,具体可以阅读:

矢量控制,又称磁场定向控制FOC,即Field-Oriented Control),其中定子三相电流被变换分解为两个正交分量,其中一个分量定义了电机的磁场,另一个分量定义了转矩。控制系统根据速度控制部分给出的磁通和转矩参考值,计算出相应的电流分量参考值。

首先,让我们对 FOC 的控制过程有个大致了解。

img

由动图观之,FOC 控制过程可归纳如下

  1. 三相电流采样Ia、Ib、Ic(或记作IU、IV、IW);

  2. 应用 Clarke 变换Park 变换将三相电流 Ia、Ib、Ic 转换为 IqId 电流(Ia、Ib、IcClarke 变换得到 ,再经 Park 变换得到 IqId);

  3. 将所得电流 IqId 与期望值 Iq_refId_ref(由上分析知,Id_ref 一般是 0)进行比较计算,得出的误差作为 PI 控制器输入

  4. PI 控制器输出电压 Vq、Vd。此时电压仍为旋转坐标系中的变量,所以在将电压给到电机之前,需要将其转换为三相电压;

  5. VqVd反 Park 变换得到 、Vβ**,再经反 Clarke 变换或其他方式合成电压空间矢量,输入 SVPWM 模块进行调制,输出控制三相逆变器的 MOS 管开关的编码值,驱动电机;

  6. 循环上述过程。

让我们一个一个来。

4.2.1 三相电流采样

由于电机工作的电流一般很大,所以采样电阻的阻值非常小,甚至和导线的电阻接近了,因而实际的采样电路 PCB 设计的时候还有一些讲究,比如使用开尔文接法(Kelvin Connections)【可参阅知乎博文:开尔文接法在电力电子中的应用有哪些?,而开尔文接法的实际应用案例可参看笔者博客【硬件设计】电流、电压采样电路硬件方案(附实例)中的 1.2.3.3】。根据基尔霍夫电流定律(在任一时刻,流入节点的电流之和等于流出节点的电流之和:Ia + Ib + Ic = 0),我们实际电路设计时可以不使用三个采样器,只需要两个就够了。

img

4.2.2 ★Clarke、Park 变换
4.2.2.1 id = 0 控制策略思路*

我们知道,当转子和定子磁场夹角重合时,力矩为 0;而当二者夹角逐渐增大到 90° 时,便可获得最大力矩

img

下图中紫色矢量为定子磁场矢量,而灰色矢量则指向与转子磁场相同的方向。我们期望紫色矢量领先灰色矢量 90°。假设此时紫色矢量仅领先 45°,而时序图上对应的相位波形亦超前 45°。此时虽有助于产生力矩,但并非是我们想要的最大力矩。

img

接下来,我们将紫色矢量沿着两个正交轴进行分解(注意这个变换分解,后面要考):沿着灰/蓝色矢量或转子磁场方向的轴称为直轴(d 轴),而与直轴正交的另一轴称为交轴(q 轴)

【(知识复习)直轴和交轴的位置】

d 轴位于磁极,即永磁体中心线上;q 轴位于两极的中心线上,即相邻两块永磁体的中心线上。

img

我们由图可知,交轴电流 Iq 有助于产生扭矩,而直轴电流 Id 则不会产生任何扭矩,因此,为了获得最大扭矩,我们可以使用两个 PI 控制器:一个强制使 Id 归零,而另一个使 Iq 最大化。当直轴分量完全减小至零时,定子磁场矢量便于转子磁场矢量正好成 90°:

img

这便是我们常说的 id** =* 0 控制策略,即令直轴参考量 Id_ref = 0。**

【电枢反应角度】

img

id =* 0 控制策略令电枢反应只有交轴电枢反应,没有直轴反应,即电枢合成磁动势 \vec{F}_{a} 正好落在 q 轴上,电流超前角 γ 等于 0。

4.2.2.2 变换公式

欸等一下,不是三相电吗?怎么直接从紫色矢量正交分解然后完事儿了?

实际上,这个紫色矢量是由三相电合成的一个空间矢量(将会在后文中推导其具体形式)

img

上图中,红、绿、蓝仨矢量分别代表A 相、B 相和 C 相电压矢量,三者合成的紫色总矢量为一空间矢量(可代表定子磁场和合成电压矢量)。灰色矢量仍表示为转子磁场方向。蓝色和黄色箭头则分别表示直轴和交轴方向。而强制直轴分量为零、同时允许交轴分量增长的操作,在这些矢量上的影响就是这样:

img

显然,我们根本不想将三个正弦量加入控制,因为要对于非线性的信号进行准确控制就要使用复杂的高阶控制器,这对于建模成本、处理器算力、控制实时性等都是不利的。而 Clarke 变换Park 变换将静止的定子参考坐标转换为旋转参考坐标,使我们不再需要直接控制交流电流,只需直接控制直轴和交轴电流即可。

这两个变换在动图中所表现的形式正是紫色矢量的正交分解:

img

以下部分**[灯哥 ](http://dengfoc.com/#/dengfoc/灯哥手把手教你写 FOC 算法/序无刷电机的 FOC 软件控制原理)**的解释和推导都非常非常清楚,笔者会将链接附上,并截选大致思路与公式在此。

4.2.2.2.1 Clarke 变换

img

就像将视在功率分为有功功率和无功功率一样,Clarke 变换可视为将三相电流转换为产生扭矩的电流 和产生磁通的电流

——Vector control for dummies — Switchcraft

所谓 Clarke 变换,实际上就是降维解耦的过程,把三相静止坐标系 A-B-C 变换为两相静止坐标系 α-β(公式推导详见灯哥的 [3.1 克拉克变换 ](http://dengfoc.com/#/dengfoc/灯哥手把手教你写 FOC 算法/3.1 克拉克变换)),变换公式如下:

\begin{bmatrix}I_\alpha\I_\beta\end{bmatrix}=C_{3S/2S}\begin{bmatrix}I_a\I_b\I_c\end{bmatrix}

式中,Clarke 变换矩阵为 C_{3S/2S}=\frac{2}{3}{\begin{bmatrix}1&-\frac{1}{2}&-\frac{1}{2}\0&\frac{\sqrt{3}}{2}&-\frac{\sqrt{3}}{2}\end{bmatrix}} ,系数 2/3 是通过等幅值约束条件得到的,表明在变换前后电流的幅值不变;而当采用功率不变作为约束条件时,该系数则应为 \sqrt{2/3}

【系数 2/3 怎么来的?】

假设 Ia = -1,根据基尔霍夫电流定律,有 Ia + Ib + Ic = 0,又电路为三相对称绕组,则Ib = Ic = 1/2。将这三个参数带入上式中,得到:

[\begin{array}{c}\mathrm{\mathit{I}}_\alpha\\mathrm{\mathit{I}}_\beta\end{array}]=\frac{2}{3}[\begin{array}{ccccc}1&&-\frac{1}{2}&&-\frac{1}{2}\0&&\frac{\sqrt{3}}{2}&&-\frac{\sqrt{3}}{2}\end{array}][\begin{array}{c}-1\\frac{1}{2}\\frac{1}{2}\end{array}]=\frac{2}{3}[\begin{array}{c}-\frac{3}{2}\0\end{array}]=[\begin{array}{c}-1\0\end{array}]

我们可以发现,如果没有 2/3 这一系数, 将是 Ia3/2 倍,而我们希望而二者大小相等,所以乘上 2/3 进行抵消。

通过基尔霍夫电流定律,我们可以消去变量 Ic,从而对 Clarke 变换进行化简如下:

\begin{cases}I_{\alpha}=I_{a}\I_{\beta}=\frac{1}{\sqrt{3}}\times(2I_b+I_a)&\end{cases}

img

4.2.2.2.2 Park 变换

Clarke 变换减少了一个维度,且变换前后电流的幅值与角频率一致,但是新的变量还是非线性的(正弦),Park 变换的工作便是将它们线性化。这个“从静止参考系移动到旋转参考系”的过程通俗地来说,就是我们现在要从旋转木马旁边的地上,跳到其中一匹木马背上,这样就方便我们锁定和我们一同旋转的其他木马。

img

其中,Iq-Id 坐标系随转子转动,d 轴在此处设定为指向电机的 N 极,Iq-Id 坐标系因转动而造成的与 - 坐标系(固定在定子上)的差角 θ,即称为电角度(该值就是编码器测得的转子实时旋转角度)。通过简单几何推导(不会推可以看[3.3 帕克变换 ](http://dengfoc.com/#/dengfoc/灯哥手把手教你写 FOC 算法/3.3 帕克变换)和**Park 变换和反 Park 变换的公式推导 **),可得:

\begin{bmatrix}i_d\i_q\end{bmatrix}=P_{2s/2r}\begin{bmatrix}i_\alpha\i_\beta\end{bmatrix}

式中,P_{2s/2r}=\begin{bmatrix}\cos\theta_e&\sin\theta_e\-\sin\theta_e&\cos\theta_e\end{bmatrix}\theta_e 为电机电角度。相应地,反 Park 变换矩阵即为 Park 变换矩阵求逆所得:P_{2r/2s}=P_{2s/2r}^{-1}=\begin{bmatrix}\cos\theta_{e}&-\sin\theta_{e}\\sin\theta_{e}&\cos\theta_{e}\end{bmatrix}

img

Vector control for dummies — Switchcraft 中的动图做总结:

img

img

基于 DSP28335坐标变换代码编写如下:

void  CLARKE_Cale(p_CLARKE  pV)
{
    // 前提为满足基尔霍夫电流定律:ia+ib+ic=0
    // Ialpha = ia;
    // Ibeta  = sqrt(3)/3 * (ia + 2*ib);
	pV->Alpha = pV->As;
	pV->Beta = _IQmpy((pV->As + _IQmpy2(pV->Bs)), _IQ(0.57735026918963));   // sqrt(3)/3 = 0.577
}
void  PARK_Cale(p_PARK pV)
{
    // Id =  Ialpha * cos(theta) + Ibeta  * sin(theta);
    // Iq =  Ibeta  * cos(theta) - Ialpha * sin(theta);
	pV->Ds = _IQmpy(pV->Alpha,pV->Cosine) + _IQmpy(pV->Beta,pV->Sine);
    pV->Qs = _IQmpy(pV->Beta,pV->Cosine) - _IQmpy(pV->Alpha,pV->Sine);
}
void  IPARK_Cale(p_IPARK pV)
{
    // Ualpha = Ud * cos(theta) - Uq * sin(theta);
    // Ubeta  = Ud * sin(theta) + Uq * cos(theta);
    pV->Alpha = _IQmpy(pV->Ds, pV->Cosine) - _IQmpy(pV->Qs, pV->Sine);
	pV->Beta  = _IQmpy(pV->Ds, pV->Sine)   + _IQmpy(pV->Qs, pV->Cosine);
}

其中,所用结构体封装如下:

typedef struct {  _iq  As;  		// Input: phase-a
				  _iq  Bs;			// Input: phase-b
				  _iq  Cs;			// Input: phase-c
				  _iq  Alpha;		// Output:  a-axis
				  _iq  Beta;		// Output:  b-axis
		 	 	} CLARKE ,*p_CLARKE ;
#define  CLARKE_DEFAULTS {0,0,0,0,0}
typedef struct {  _iq  Alpha;  		// Input:  a-axis
		 		  _iq  Beta;	 	// Input:  b-axis
		 	 	  _iq  Angle;		// Input:  angle (pu)
		 	 	  _iq  Ds;			// Output:  d-axis
		 	 	  _iq  Qs;			// Output:  q-axis
		 	 	  _iq  Sine;
		 	 	  _iq  Cosine;
		 	 	} PARK , *p_PARK ;
#define  PARK_DEFAULTS {0,0,0,0,0,0,0}
typedef struct {  _iq  Alpha;  		// Output:  d-axis
		 	 	  _iq  Beta;		// Output:  q-axis
		 	 	  _iq  Angle;		// Input:  angle (pu)
		 	 	  _iq  Ds;			// Input:  d-axis
		 	 	  _iq  Qs;			// Input:  q-axis
		 	      _iq  Sine;		// Input: Sine
		 	      _iq  Cosine;		// Input: Cosine
		 	    } IPARK , *p_IPARK;
#define  IPARK_DEFAULTS {0,0,0,0,0,0,0}
4.2.3 ★空间矢量脉宽调制(SVPWM)

前文曾提过,PMSM 的反电动势波形为正弦波,也就是说,当匀速转动它的转子时,用示波器观察它开路的三相定子输出电压,将会看到两两相位差为 120° 的三相正弦电压。那么反过来,我们可以通过在定子上输入三相正弦电压,来驱动转子平稳运行。

可是我们最终的控制对象是 MOS 管的开通和关断,即只有电压最大值和 0 两个状态,那该如何生成连续变化的正弦波呢?在《电力电子技术》中我们学到,可以采用正弦脉宽调制SPWM,即 Sinusoidal PWM),再通过低通滤波器,便可得到等效的正弦波。

【(知识复习)SPWM】

三相桥式 SPWM 逆变电路的控制是用一条等腰三角波与三条幅值及频率相同但相位各相差 120° 的正弦波进行比较,从而得到三个桥臂的控制信号,即控制信号 uA 与三角波比较得到 T1 的控制信号;uB 与 uC 同三角波比较得到 T3 和 T5 的通断控制信号。

img

但是,在 FOC 控制中我们一般采用的是空间矢量脉宽调制SVPWM,即 Space Vector Pulse Width Modulation)而不是 SPWM。乎天斟在《电机控制——聊聊 SPWM 和 SVPWM》中一针见血地指出:“SVPWM 与 SPWM 的思路相反,一开始就从产生旋转矢量的这一结果入手。由于 SPWM 最终的目的就是为了产生一个空间上的旋转磁动势,那么 SVPWM 就不拘泥于单独产生三相电压,而是通过设置开关管的通断,直接在电机中形成一个旋转的空间矢量,从而产生一个旋转的磁动势。”

4.2.3.1 空间矢量

问题来了,这个由三相正弦电压合成的空间矢量具体是什么样子呢?

  • 【时间上】设三相正弦电压瞬时值为:

\begin{cases}u_a=U_m\cos(\omega t)\u_b=U_m\cos(\omega t-\frac{2\pi}{3})\u_c=U_m\cos(\omega t+\frac{2\pi}{3})&\end{cases}

  • 【空间上】三相绕组在空间互差 120°,其物理位置可用复数表示:

    • a 相轴指向 0°:单位向量 e_{a}=e^{j0}=1

    • b 相轴相对 a 相逆时针转 120°:e_b=e^{j2\pi/3}=-\frac{1}{2}+j\frac{\sqrt{3}}{2}

    • c 相轴相对 a 相顺时针转 120°:e_{c}=e^{-j2\pi/3}=-\frac{1}{2}-j\frac{\sqrt{3}}{2}

img

定义初始合成矢量为

V_{raw}=u_a\cdot1+u_b\cdot e^{j\frac{2\pi}{3}}+u_c\cdot e^{-j\frac{2\pi}{3}}

将三相电压代入可得

u a = U m cos ⁡ θ u b ⋅ e j 2 π 3 = U m cos ⁡ ( θ − 2 π 3 ) ⋅ ( − 1 2 + j 3 2 ) u c ⋅ e − j 2 π 3 = U m cos ⁡ ( θ + 2 π 3 ) ⋅ ( − 1 2 − j 3 2 ) \begin{aligned}u_{a}&=U_{m}\cos\theta\\u_b\cdot e^{j\frac{2\pi}{3}}&=U_m\cos\left(\theta-\frac{2\pi}{3}\right)\cdot \left(-\frac{1}{2}+j\frac{\sqrt{3}}{2}\right)\\u_c\cdot e^{-j\frac{2\pi}{3}}&=U_m\cos\left(\theta+\frac{2\pi}{3}\right)\cdot \left(-\frac{1}{2}-j\frac{\sqrt{3}}{2}\right)\end{aligned} uaubej32πucej32π=Umcosθ=Umcos(θ32π)(21+j23 )=Umcos(θ+32π)(21j23 )

把 cos⁡(θ∓2π3) 展开

cos ⁡ ( θ − 2 π 3 ) = cos ⁡ θ cos ⁡ 2 π 3 + sin ⁡ θ sin ⁡ 2 π 3 = − 1 2 cos ⁡ θ + 3 2 sin ⁡ θ cos ⁡ ( θ + 2 π 3 ) = cos ⁡ θ cos ⁡ 2 π 3 − sin ⁡ θ sin ⁡ 2 π 3 = − 1 2 cos ⁡ θ − 3 2 sin ⁡ θ \cos\left(\theta - \frac{2\pi}{3}\right) = \cos\theta\cos\frac{2\pi}{3} + \sin\theta\sin\frac{2\pi}{3} = -\frac{1}{2}\cos\theta + \frac{\sqrt{3}}{2}\sin\theta\\ \cos\left(\theta+\frac{2\pi}{3}\right)=\cos\theta\cos\frac{2\pi}{3}-\sin\theta\sin\frac{2\pi}{3}=-\frac{1}{2}\cos\theta-\frac{\sqrt3}{2}\sin\theta cos(θ32π)=cosθcos32π+sinθsin32π=21cosθ+23 sinθcos(θ+32π)=cosθcos32πsinθsin32π=21cosθ23 sinθ

把它们代回去并分别按实、虚部相加

  • 实部:

[ ℜ { V r a w } = U m cos ⁡ θ + U m [ − 1 2 ( − 1 2 cos ⁡ θ + 3 2 sin ⁡ θ ) − 1 2 ( − 1 2 cos ⁡ θ − 3 2 sin ⁡ θ ) ] = U m cos ⁡ θ + U m [ 1 4 cos ⁡ θ − 3 4 sin ⁡ θ + 1 4 cos ⁡ θ + 3 4 sin ⁡ θ ] = U m cos ⁡ θ + U m [ 1 2 cos ⁡ θ ] = 3 2 U m cos ⁡ θ [\begin{aligned}\Re\{{V}_{raw}\}&=U_m\cos\theta+U_m\left[-\frac{1}{2}\left(-\frac{1}{2}\cos\theta+\frac{\sqrt3}{2}\sin\theta\right)-\frac{1}{2}\left(-\frac{1}{2}\cos\theta-\frac{\sqrt3}{2}\sin\theta\right)\right]\\&=U_m\cos\theta+U_m\left[\frac{1}{4}\cos\theta-\frac{\sqrt{3}}{4}\sin\theta+\frac{1}{4}\cos\theta+\frac{\sqrt{3}}{4}\sin\theta\right]\\&=U_m\cos\theta+U_m\left[\frac{1}{2}\cos\theta\right]=\boxed{\frac{3}{2}U_m\cos\theta}\end{aligned} [{Vraw}=Umcosθ+Um[21(21cosθ+23 sinθ)21(21cosθ23 sinθ)]=Umcosθ+Um[41cosθ43 sinθ+41cosθ+43 sinθ]=Umcosθ+Um[21cosθ]=23Umcosθ

  • 虚部:

ℑ { V r a w } = U m [ 3 2 ( − 1 2 cos ⁡ θ + 3 2 sin ⁡ θ ) − 3 2 ( − 1 2 cos ⁡ θ − 3 2 sin ⁡ θ ) ] = U m [ − 3 4 cos ⁡ θ + 3 4 sin ⁡ θ + 3 4 cos ⁡ θ + 3 4 sin ⁡ θ ] = 3 2 U m sin ⁡ θ \begin{aligned}\Im\{{V}_{raw}\}&=U_m\left[\frac{\sqrt{3}}{2}\left(-\frac{1}{2}\cos\theta+\frac{\sqrt{3}}{2}\sin\theta\right)-\frac{\sqrt{3}}{2}\left(-\frac{1}{2}\cos\theta-\frac{\sqrt{3}}{2}\sin\theta\right)\right]\\&=U_m\left[-\frac{\sqrt{3}}{4}\cos\theta+\frac{3}{4}\sin\theta+\frac{\sqrt{3}}{4}\cos\theta+\frac{3}{4}\sin\theta\right]\\&=\boxed{\frac{3}{2}U_m\sin\theta}\end{aligned} {Vraw}=Um[23 (21cosθ+23 sinθ)23 (21cosθ23 sinθ)]=Um[43 cosθ+43sinθ+43 cosθ+43sinθ]=23Umsinθ

于是有

V r a w = 3 2 U m ( cos ⁡ θ + j sin ⁡ θ ) = 3 2 U m e j ω t {V}_{raw}=\frac{3}{2}U_{m}\left(\cos\theta+j\sin\theta\right)=\boxed{\frac{3}{2}U_{m}e^{j\omega t}} Vraw=23Um(cosθ+jsinθ)=23Umet

该初始空间矢量(下方动图中黑色的矢量)幅值不变,为相电压幅值 Um 的 1.5 倍,旋转角速度为 ω(= 2πf), 旋转方向由三相电压的相序决定。

由于电压方向和其所产生的磁场方向一致(可通过前文所给的通电螺线管辅助理解),如果三个相绕组结构一样,那么产生的合成磁场的大小也是每个线圈产生磁场幅值的 1.5 倍。因为转子永磁体会努力旋转到内部磁力线和外部磁场方向一致,所以这个矢量其实就可以表征我们希望转子旋转到的方向,也即所需要生成的磁场方向

img

但是为了归一化使矢量幅值等于相电压幅值 Um,从而简化控制系统设计,空间电压矢量一般会人为地乘上一个 2/3 系数,即让

∣ V ∣ = ∣ 2 3 V r a w ∣ = U m |{V}|=\left|\frac{2}{3}{V}_{raw}\right|=U_m V= 32Vraw =Um

故有

V = 2 3 ( u a + u b e j 2 π 3 + u c e − j 2 π 3 ) \boxed{{V}=\frac{2}{3}\left(u_a+u_be^{j\frac{2\pi}{3}}+u_ce^{-j\frac{2\pi}{3}}\right)} V=32(ua+ubej32π+ucej32π)

【等价于 Clarke 变换的复数形式】

直接将上式用欧拉公式
e ± j 2 π 3 = − 1 2 ± j 3 2 e^{\pm j\frac{2\pi}{3}}=-\frac{1}{2}\pm j\frac{\sqrt3}{2} e±j32π=21±j23
展开,你将会发现:

V = 2 3 [ u a + u b ( − 1 2 + j 3 2 ) + u c ( − 1 2 − j 3 2 ) ] = 2 3 ( u a − 1 2 u b − 1 2 u c ) ⏟ v α + j 2 3 ( 3 2 u b − 3 2 u c ) ⏟ v β \begin{gathered}\mathrm{V}=\frac{2}{3}\left[u_a+u_b\left(-\frac{1}{2}+j\frac{\sqrt{3}}{2}\right)+u_c\left(-\frac{1}{2}-j\frac{\sqrt{3}}{2}\right)\right]\\=\underbrace{\frac{2}{3}\left(u_a-\frac{1}{2}u_b-\frac{1}{2}u_c\right)}_{v_\alpha}+\underbrace{j\frac{2}{3}\left(\frac{\sqrt3}{2}u_b-\frac{\sqrt3}{2}u_c\right)}_{v_\beta}\end{gathered} V=32[ua+ub(21+j23 )+uc(21j23 )]=vα 32(ua21ub21uc)+vβ j32(23 ub23 uc)

V ⃗ s = v α + j v β , { v α = 2 3 ( u a − 1 2 u b − 1 2 u c ) v β = 3 3 ( u b − u c ) \vec{V}_s=v_\alpha+jv_\beta,\quad\begin{cases}v_\alpha=\frac{2}{3}\left(u_a-\frac{1}{2}u_b-\frac{1}{2}u_c\right)\\v_\beta=\frac{\sqrt{3}}{3}(u_b-u_c)&\end{cases} V s=vα+jvβ,{vα=32(ua21ub21uc)vβ=33 (ubuc)

这里的系数 2/3 和前面等幅值形式的 2/3 作用一致,都是用于归一化。

综上,我们可以得到:

img

所以,我们怎么才能单单用 PWM 来重现这个空间电压矢量 V 呢?

4.2.3.2 基本原理

该部分将大量引用稚晖君 的文字,以及西南交通大学宋文胜老师课件《第六章 脉宽调制技术》。

推荐阅读:知乎玻璃伞 彻底吃透 SVPWM 如此简单

相比 PWM 和 SPWM,基于 SVPWM 的逆变器将逆变器在降低电压谐波和损耗方面将逆变器的控制性能提升到一个新的高度,且易于数字化实现,适合 DSP 等高性能处理器进行数字控制。该部分将详细讲解 SVPWM 的基本原理。

首先,让我们从三相桥式逆变器入手,以其中一种电路通断状态为例(此时即为矢量 **V1(100)**状态):

img

此时电源及电机绕组等效电路如下图,其中 Udc 为直流电源电压:

img

因此状态 V1(100)时电机中三个相电压(相电压是每相相对于电机中间连接点的电压)可以表示为:UaN = 2/3Udc**,UbN = UcN = -1/3Udc**(其实就是一个分压电路)。

img

电路有 2^3 = 8 个开关状态,对应着 8 种工作模式。8 种工作模式又分别对应 8 种矢量,其中包括 6 个非零矢量:V1(100)V2(110)V3(010)V4(011)V5(001),**V6(101)**和 2 个零矢量:V0(000)V7(111)。(这里非零矢量的编号顺序看似很奇怪,其实是按下面图中矢量逆时针排布的顺序。另一种常见的则是用 SaSbSc 对应的二进制进行编号。)

img

仍以上文提到的空间矢量 V1(编号 100,UaN = 2/3UdcUbN = UcN = -1/3Udc)**为例,如下图进行矢量合成:

img

由图可知,合成矢量大小应为 Udc,方向水平指向右侧。和前文一致地,此处同样人为地乘上 2/3 系数,则矢量 V1 大小被修改为 2/3Udc。故 8 种开关状态对应的空间矢量如下图所示,空间电压矢量 Vk 定义如下:

img

上面的第二个公式就有很强的既视感了,和我们在 4.2.1 引言中提到的空间电压矢量公式形式完全一致,不过只是将三个正弦电压替换成了开关函数、前面的相电压幅值被替换为了直流电源电压。矢量**V1(100)**的编号代入公式可得

V 1 = 2 3 U d c ( 1 + 0 e j 2 π / 3 + 0 e − j 2 π / 3 ) = 2 3 U d c , V_{1}=\frac{2}{3}U_{\mathrm{dc}}(1+0e^{j2\pi/3}+0e^{-j2\pi/3})=\frac{2}{3}U_{\mathrm{dc}}, V1=32Udc(1+0ej2π/3+0ej2π/3)=32Udc,

和前述一致。

6 个有效矢量2 个零矢量画出。相邻的有效矢量在空间上相差 π/3,这六个矢量形成一个正六边形。这六个有效矢量将复平面分成六个区域,分别记为 I,II,III,IV,V,VI

img

可以注意到,两个零矢量(V0(000)V7(111))其实和原点重合了,因为这两个状态下电机中产生力矩的磁场为 0

那么这里问题就来了:由这 6 个空间电压矢量只能产生 6 个方向的力矩,我们怎么产生任意方向的力矩呢?答案就是:使用这 6 个空间电压矢量作为基向量合成任意矢量。在每一个扇区,选择相邻两个电压矢量以及零矢量,按照伏秒平衡原则来合成每个扇区内的任意电压矢量

∫ 0 T U r e f d t = ∫ 0 T x U x d t + ∫ T x T x + T y U y d t + ∫ T x + T y T U 0 ∗ d t \int_0^TU_{ref}dt=\int_0^{T_x}U_xdt+\int_{T_x}^{T_x+T_y}U_ydt+\int_{T_x+T_y}^TU_0^*dt 0TUrefdt=0TxUxdt+TxTx+TyUydt+Tx+TyTU0dt

离散化后等效为下式:

U r e f ⋅ T = U x ⋅ T x + U y ⋅ T y + U 0 ∗ ⋅ T 0 ∗ U_{ref}\cdot T=U_x\cdot T_x+U_y\cdot T_y+U_0^*\cdot T_0^* UrefT=UxTx+UyTy+U0T0

式中,各变量含义如下:

  • Vref:我们期望得到的电压矢量;

  • T:一个 PWM 周期;

  • UxUyTxTy:看完后面所举的例子就懂了;

  • U0:指的是两个零矢量,可以是 V0 也可以是 V7,零矢量的选择比较灵活,主要考虑通过合理选择使得开关状态变化尽可能少,以降低开关损耗,并让空间电压矢量的切换更平顺。

所以上面公式的含义就是:我们可以周期性地在不同空间电压矢量之间切换,只要合理地配置不同基向量在一个周期中的占空比,就可以合成出等效的任意空间电压矢量了。

假设现在需要产生电压矢量 Vref,其位置位于扇区 I 内, 介于 V1V2 之间。设 VaVb 分别是 V1V2 上的矢量,二者合成得到 Vref

img

在一个周期 Tc 内,由伏秒平衡可得(把前面的式子左侧的 T 除到右边去了):

{ V r e f = V a + V b = V 1 T 1 T c + V 2 T 2 T c + V 0 , 7 T 0 , 7 T c T c = T 1 + T 2 + T 0 , 7 \begin{cases}V_{\mathrm{ref}}=V_{a}+V_{b}=V_{1}\frac{T_{1}}{T_{\mathrm{c}}}+V_{2}\frac{T_{2}}{T_{\mathrm{c}}}+V_{0,7}\frac{T_{0,7}}{T_{\mathrm{c}}}\\T_{\mathrm{c}}=T_{1}+T_{2}+T_{0,7}&\end{cases} {Vref=Va+Vb=V1TcT1+V2TcT2+V0,7TcT0,7Tc=T1+T2+T0,7

正弦定理(各边和它所对角的正弦值的比相等**)** ,我们可以得到:

V r e f s i n 2 π 3 = V 2 T 2 T c sin ⁡ θ = V 1 T 1 T c sin ⁡ ( π 3 − θ ) \frac{V_{\mathrm{ref}}}{sin\frac{2\pi}{3}}=\frac{V_{2}\frac{T_{2}}{T_{\mathrm{c}}}}{\sin\theta}=\frac{V_{1}\frac{T_{1}}{T_{\mathrm{c}}}}{\sin(\frac{\pi}{3}-\theta)} sin32πVref=sinθV2TcT2=sin(3πθ)V1TcT1

又由 |V1| = |V2| = 2/3Udc,所以可以计算得到 T1T2(具体推导见下):

\begin{cases}T_1=\mathrm{\mathit{m}}T_\mathrm{c}\sin(\frac{\pi}{3}-\theta)\T_2=\mathrm{\mathit{m}}T_c\sin\theta\T_{0,7}=T_\mathrm{c}-T_1-T_2&\end{cases}

其中,m 为 SVPWM 的调制系数(即调制比,也称调制度):

m = 3 V r e f U d c m=\frac{\sqrt{3}V_{\mathrm{ref}}}{U_{\mathrm{dc}}} m=Udc3 Vref

显然在电流环控制过程中 m 设置得越大,代表了期望力矩越大(正比)

【具体推导步骤(以求解 T1 为例)】
∣ V ⃗ r e f ∣ sin ⁡ 2 π 3 = ∣ V ⃗ 1 ∣ T 1 T c sin ⁡ ( π 3 − θ ) \frac{|\vec{V}_{\mathrm{ref}}|}{\sin\frac{2\pi}{3}}=\frac{|\vec{V}_{1}|\frac{T_{1}}{T_{c}}}{\sin\left(\frac{\pi}{3}-\theta\right)} sin32πV ref=sin(3πθ)V 1TcT1

代入已知量:
V r e f 3 2 = ( 2 3 U d c ) T 1 T c sin ⁡ ( π 3 − θ ) \frac{V_{\mathrm{ref}}}{\frac{\sqrt{3}}{2}}=\frac{\left(\frac{2}{3}U_{dc}\right)\frac{T_{1}}{T_{c}}}{\sin\left(\frac{\pi}{3}-\theta\right)} 23 Vref=sin(3πθ)(32Udc)TcT1

化简:

2 V ref 3 = 2 3 U d c ⋅ T 1 T c sin ⁡ ( π 3 − θ ) \frac{2V_{\text{ref}}}{\sqrt{3}} = \frac{\frac{2}{3}U_{dc} \cdot T_1}{T_c \sin \left(\frac{\pi}{3} - \theta\right)} 3 2Vref=Tcsin(3πθ)32UdcT1

T 1 = 2 V ref 3 ⋅ 3 T c sin ⁡ ( π 3 − θ ) 2 U d c = 3 V ref T c sin ⁡ ( π 3 − θ ) U d c T_1 = \frac{2V_{\text{ref}}}{\sqrt{3}} \cdot \frac{3T_c \sin \left(\frac{\pi}{3} - \theta\right)}{2U_{dc}} = \frac{\sqrt{3} V_{\text{ref}} T_c \sin \left(\frac{\pi}{3} - \theta\right)}{U_{dc}} T1=3 2Vref2Udc3Tcsin(3πθ)=Udc3 VrefTcsin(3πθ)

在一个开关周期 Tc 内,设 T0T7 分别是零矢量 V0V7 的作用时间,其表达式如下:

{ T 0 = k T 0 , 7 T 7 = ( 1 − k ) T 0 , 7 0 ≤ k ≤ 1 \begin{cases}T_0=kT_{0,7}\\T_7=(1-k)T_{0,7}&\end{cases}\quad0\leq k\leq1 {T0=kT0,7T7=(1k)T0,70k1

另外,如果我们将 PWM 波形设定为中央对齐模式对称配置零矢量,则此时

T_0=T_7=\frac{1}{2}(T_c-T_1-T_2)

现在一个周期内所有状态的持续时间我们都得到了,还差一个顺序,也就是各个状态切换的顺序。问题来了,反正是做积分,重要的是持续时间而不是顺序,难道不是任意顺序都可以嘛?是的,理论上任何切换顺序都是可行的,但是实际中我们需要考虑更多限制,比如因为 MOS 管存在开关损耗,所以我们希望能尽量减少 MOS 管的开关次数。另外,当 PWM 输出波形是对称的时(即采用七段式 SVPWM 调制法V0V1V2V7V2V1V0 )),谐波主要集中在开关频率和两倍开关频率的附近,这种模式下谐波幅值是三种排列模式中最小的。结合以上因素考虑,我们就可以设计出下面的切换顺序(一个开关周期内,共有 6 次开关切换):

img

imgimg

至此,SVPWM 的工作完成了,我们得到了每一时刻所需要的空间电压矢量以及它们持续的时间,在处理器中赋值给对应通道的捕获比较寄存器产生相应的三个 PWM 波形,控制 MOS 管的开关,进而产生我们期望的电压电流及力矩。

Space Vector PWM Intro — Switchcraft 中的动图做总结(动图中的空间矢量按二进制编号):

img

void SVPWM_Cale(p_SVPWM pV)
{
    // Vref1 = Ubeta;
    // Vref2 = (sqrt(3) * Ualpha - Ubeta) / 2;
    // Vref3 = (-sqrt(3) * Ualpha - Ubeta) / 2;
    pV->tmp1 = pV->Ubeta;
    pV->tmp2 = - _IQdiv2(pV->Ubeta) + _IQmpy(_IQ(0.866), pV->Ualpha); // 0.866 = sqrt(3) / 2
    pV->tmp3 = - _IQdiv2(pV->Ubeta) - _IQmpy(_IQ(0.866), pV->Ualpha);
    if(pV->tmp1 > _IQ(0.0))
        pV->tmpNA = 1;
    else
        pV->tmpNA = 0;
    if(pV->tmp2 > _IQ(0.0))
        pV->tmpNB = 1;
    else
        pV->tmpNB = 0;
    if(pV->tmp3 > _IQ(0.0))
        pV->tmpNC = 1;
    else
        pV->tmpNC = 0;
    pV->tmpN =  pV->tmpNA + 2*pV->tmpNB + 4*pV->tmpNC;
    switch(pV->tmpN)
    {
        case 3:
            pV->VecSector = 1;
        break;
        case 1:
            pV->VecSector = 2;
        break;
        case 5:
            pV->VecSector = 3;
        break;
        case 4:
            pV->VecSector = 4;
        break;
        case 6:
            pV->VecSector = 5;
        break;
        case 2:
            pV->VecSector = 6;
        break;
    }
    pV->tmpA = _IQmpy(_IQ(1.73205081*0.001/12.5/24), pV->Ubeta);
    pV->tmpB = _IQmpy(_IQ(1.73205081*0.001/12.5/24), ( _IQmpy( _IQ(0.86602540), pV->Ualpha) +  _IQdiv2(pV->Ubeta) ) );
    pV->tmpC = _IQmpy(_IQ(1.73205081*0.001/12.5/24), (-_IQmpy( _IQ(0.86602540), pV->Ualpha) +  _IQdiv2(pV->Ubeta) ) );
    switch(pV->VecSector)
    {
        case 1:
        {
            pV->T1 = -pV->tmpC;
            pV->T2 =  pV->tmpA;
        }
        break;
        case 2:
        {
            pV->T1 =  pV->tmpC;
            pV->T2 =  pV->tmpB;
        }
        case 3:
        {
            pV->T1 =  pV->tmpA;
            pV->T2 = -pV->tmpB;
        }
        break;
        case 4:
        {
            pV->T1 = -pV->tmpA;
            pV->T2 =  pV->tmpC;
        }
        break;
        case 5:
        {
            pV->T1 = -pV->tmpB;
            pV->T2 = -pV->tmpC;
        }
        break;
        case 6:
        {
            pV->T1 =  pV->tmpB;
            pV->T2 = -pV->tmpA;
        }
        break;
    }
    // 过调制处理
    if(pV->T1 + pV->T2 > _IQ(0.001/12.5))
    {
        pV->T1 = _IQdiv(_IQmpy(_IQ(0.001/12.5), pV->T1), (pV->T1 + pV->T2));
        pV->T2 = _IQdiv(_IQmpy(_IQ(0.001/12.5), pV->T2), (pV->T1 + pV->T2));
    }
    else
    {
        pV->T1 = pV->T1;
        pV->T2 = pV->T2;
    }
    // 扇区内合成矢量切换点时间计算
    // 此处为 7 段式,两个零矢量 000 111 111 插在中间,000 均分插在两端
    pV->ta = _IQdiv4((_IQ(0.001/12.5) - (pV->T1 + pV->T2)));
    pV->tb = pV->ta + _IQdiv2(pV->T1);
    pV->tc = pV->tb + _IQdiv2(pV->T2);
    // 输出调制信号
    switch(pV->VecSector)
    {
        case 1:
        {
            pV->Tcm1 = pV->ta;
            pV->Tcm2 = pV->tb;
            pV->Tcm3 = pV->tc;
        }
        break;
        case 2:
        {
            pV->Tcm1 = pV->tb;
            pV->Tcm2 = pV->ta;
            pV->Tcm3 = pV->tc;
        }
        break;
        case 3:
        {
            pV->Tcm1 = pV->tc;
            pV->Tcm2 = pV->ta;
            pV->Tcm3 = pV->tb;
        }
        break;
        case 4:
        {
            pV->Tcm1 = pV->tc;
            pV->Tcm2 = pV->tb;
            pV->Tcm3 = pV->ta;
        }
        break;
        case 5:
        {
            pV->Tcm1 = pV->tb;
            pV->Tcm2 = pV->tc;
            pV->Tcm3 = pV->ta;
        }
        break;
        case 6:
        {
            pV->Tcm1 = pV->ta;
            pV->Tcm2 = pV->tc;
            pV->Tcm3 = pV->tb;
        }
        break;
    }
    // 调制信号处理,生成输入到 MCU 中的调制信号
    pV->Tcm1 = _IQmpy(6000, _IQdiv(pV->Tcm1, _IQ(0.001/12.5/2)) );
    pV->Tcm2 = _IQmpy(6000, _IQdiv(pV->Tcm2, _IQ(0.001/12.5/2)) );
    pV->Tcm3 = _IQmpy(6000, _IQdiv(pV->Tcm3, _IQ(0.001/12.5/2)) );
}

另附:

【1】ePWM 初始化:

#define ISR_FREQUENCY     12.5
#define SYSTEM_FREQUENCY  150
float32 T = 0.001/ISR_FREQUENCY;
// T 为采样周期 (s),其中开关频率 ISR_FREQUENCY 数值为 12.5(kHz),故转换为 s 作为单位时需要*1/1000。
// 开关频率 ISR_FREQUENCY 此处设为 12.5(12.5kHZ),则采样周期 T 为 0.00008s 即 0.08ms(80us)。
// 在电机控制中,采样频率一般与开关频率相同。
void EPWM3_int(void)
{
    // 150MHz,即 1s 之中计数 150M 次,则一个采样周期内计数 (150M*T) 次
    // 注意!赋予寄存器的为计数值!
    // 因为在向上下模式计数时,Tpwm = 2*TBPRD*T(TBCLK),所以 TBPRD(即 PeriodMax) 为一个采样周期计数值的 1/2,即 (150M*T)/2 次
    PWM_PeriodMax  = SYSTEM_FREQUENCY*1000000*T/2;  // 6000
    PWM_HalfPerMax = PWM_PeriodMax/2;               // HalfPerMax 为 TBPRD/2
    PWM_Deadband   = 2.0*SYSTEM_FREQUENCY;
    EALLOW;
    /* SYNCOSEL:同步信号输出选择。00:同步输出信号与该模块的同步输入信号 ePWMxSYNCI 相同 */
    EPwm1Regs.TBCTL.bit.SYNCOSEL = 0;
    EPwm2Regs.TBCTL.bit.SYNCOSEL = 0;
    EPwm3Regs.TBCTL.bit.SYNCOSEL = 0;
    /* PHSEN:计数寄存器装载相位寄存器使能位。1:当同步信号到来的时候,计数寄存器装载相位寄存器的值 */
    EPwm1Regs.TBCTL.bit.PHSEN = 1;
    EPwm2Regs.TBCTL.bit.PHSEN = 1;
    EPwm3Regs.TBCTL.bit.PHSEN = 1;
    /* 初始化 EPWM1-EPWM3 时基周期寄存器 */
    /* PeriodMax = SYSTEM_FREQUENCY*1000000*T/2,即全周期计数值的 1/2 */
    EPwm1Regs.TBPRD = PWM_PeriodMax;    // 6000
    EPwm2Regs.TBPRD = PWM_PeriodMax;
    EPwm3Regs.TBPRD = PWM_PeriodMax;
    /* 初始化 EPWM1-EPWM3 时基相位寄存器 */
    EPwm1Regs.TBPHS.half.TBPHS = 0;
    EPwm2Regs.TBPHS.half.TBPHS = 0;
    EPwm3Regs.TBPHS.half.TBPHS = 0;
    /* 初始化 EPWM1-EPWM3 时基控制寄存器 */
    // 0xA00A:10 1 000 000 0 00 1 0 10
    // 仿真模式位 FREE,SOFT - 10:当仿真事件到来时时基计数器自由运行;
    // 相位方向位 PHSDIR - 1:当时基计数器配置为向上-下模式时,这个位才起作用。这个位置 1,则当同步信号到来时计数器装载相位寄存器的值后向下计数。
    // 时基时钟分频位 CLKDIV - 000:/1。
    // 高速时基时钟分频位 HSPCLKDIV - 000:/1。这两位决定了时基时钟分频值 TBCLK = SYSCLKOUT / (HSPCLKDIV × CLKDIV) = SYSCLKOUT。
    // 软件强制同步脉冲 SWFSYNC - 0:写 0 没有效果
    // 同步信号输出选择 SYNCOSEL - 00:选择 ePWMxSYNCO 信号输出源为 ePWMxSYNCI。
    // 周期寄存器装载影子寄存器选择 PRDLD - 1:禁止使用影子寄存器。
    // 计数寄存器装载相位寄存器使能位 PHSEN - 0:禁止装载。
    // 计数模式 CTRMODE - 10:向上-下计数。一般情况下,计数模式只设置一次,如果需要改变模式,那么将会在下一个 TBCLK 的边沿生效
    EPwm1Regs.TBCTL.all = 0xA00A;
    EPwm2Regs.TBCTL.all = 0xA00A;
    EPwm3Regs.TBCTL.all = 0xA00A;
    /* 初始化 EPWM1-EPWM3 计数比较控制寄存器 */
    EPwm1Regs.CMPCTL.all = 0;
    EPwm2Regs.CMPCTL.all = 0;
    EPwm3Regs.CMPCTL.all = 0;
    /* 初始化 EPWM1-EPWM3 动作控制寄存器 A 寄存器 */
    // 00 00 01 10 00 00
    // CBD + CBU - 0000:当向上和下计数时,时基计数器的值与 CMPB 寄存器的值相等时,不动作。
    // CAD - 01:当向下计数时,时基计数器的值与 CMPA 寄存器的值相等时,清零:使 ePWMxA 输出低
    // CAU - 10:当向上计数时,时基计数器的值与 CMPA 寄存器的值相等时,置位:使 ePWMxA 输出高    该设置下,比较值越小,占空比越大,因为:比较值越小,则越先开始作用这个矢量,自然其占空比就越大
    // PRD - 00:当时基计数器的值与周期寄存器的值相等时不动作
    // ZRO - 00:当时基计数器的值等于 0 时不动作。
    EPwm1Regs.AQCTLA.all = 0x0060;
    EPwm2Regs.AQCTLA.all = 0x0060;
    EPwm3Regs.AQCTLA.all = 0x0060;
    /* 初始化 EPWM1-EPWM3 的死区控制寄存器 */
    // 死区模块输入控制 IN_MODE - 00:ePWMxA 是双边沿延时输人源
    // 极性选择控制 POSEL - 10:ePWMxA 不翻转,ePWMxB 翻转
    // 死区模块输出控制 OUT_MODE - 11:使能双边沿延时
    EPwm1Regs.DBCTL.all = 0x000B;
    EPwm2Regs.DBCTL.all = 0x000B;
    EPwm3Regs.DBCTL.all = 0x000B;
    /* 初始化 EPWM1-EPWM3 死区上升沿、下降沿延时寄存器 */
    /* v.Deadband = 1.5*SYSTEM_FREQUENCY */
    /* 计算边沿延时的计算公式:FED=DBFED*T(TBCLK); RED=DBRED*T(TBCLK) */
    EPwm1Regs.DBFED =  PWM_Deadband;
    EPwm1Regs.DBRED =  PWM_Deadband;
    EPwm2Regs.DBFED =  PWM_Deadband;
    EPwm2Regs.DBRED =  PWM_Deadband;
    EPwm3Regs.DBFED =  PWM_Deadband;
    EPwm3Regs.DBRED =  PWM_Deadband;
    EPwm1Regs.PCCTL.all = 0;
    EPwm2Regs.PCCTL.all = 0;
    EPwm3Regs.PCCTL.all = 0;
    EPwm1Regs.TZSEL.all = 0;
    EPwm2Regs.TZSEL.all = 0;
    EPwm3Regs.TZSEL.all = 0;
    EDIS;                         /* Disable EALLOW*/
}

【2】PWM 输出:

**注意!!!一定要结合电机实际绕组顺序对各高桥臂开关管进行 PWM 输出选择!**如下图中,逆时针来看电机绕组顺序为 A、C、B,则输出时将 SVPWM 的 tcm2 和 tcm3 对调如下方代码所示。

img

void Svpwm_Outpwm(void)
{
    EPwm1Regs.CMPA.half.CMPA = Svpwmdq.Tcm1;
    EPwm2Regs.CMPA.half.CMPA = Svpwmdq.Tcm3;
    EPwm3Regs.CMPA.half.CMPA = Svpwmdq.Tcm2;
}
4.2.4 ★比例积分(PI)双环控制
4.2.4.1 比例积分微分(PID)控制原理

推荐阅读:优快云 博主**skythinker616PID 超详细教程——PID 原理+串级 PID+C 代码+在线仿真调参-优快云 博客 **

先让我们来浅浅复习一下 PID 控制相关的知识。以下文段部分截取自夏长亮老师的《无刷直流电机控制系统》。

img

标准 PID 控制器的基本原理是根据设定值实际值之间的偏差 e(t),按比例-积分-微分的线性组合关系构成控制量 x(t) ,利用控制量 x(t) 再对控制对象进行控制。连续控制系统 PID 控制规律形式为

u(t)=K_{\mathrm{P}}\left(e(t)+\frac{1}{T_{\mathrm{I}}}\int_{0}^{t}e(t)\mathrm{d}t+T_{\mathrm{D}}\frac{\mathrm{d}e(t)}{\mathrm{d}t}\right)

式中,K_{\mathrm{P}} 为比例常数;T_{\mathrm{I}} 为积分时间常数;T_{\mathrm{D}} 为微分时间常数。

实际控制系统中,PID 控制器不一定都包含比例、积分和微分 3 个环节,可以是比例、比例积分和比例微分等多种组合形式,无刷直流电机控制系统中最常见的是比例积分(PI)形式,因为微分环节虽然能有效地减小超调和缩小最大动态偏差,但同时易使系统受到高频干扰影响

【为何微分环节易使系统受到高频干扰影响?】

  • 从时域理解

微分环节就是对输入信号做导数:

u_D(t)=K_D\cdot\frac{de(t)}{dt}

如果输入信号里包含高频成分(比如传感器噪声),那么微分会让这些高频分量被放大。

  • 从频域理解

在频域里,微分相当于一个 高通滤波器

D(s)=K_D{\cdot}s

其中 s=j\omega,所以增益 = K_{D}\cdot\omega,故有:高频分量(ω 大) → 增益很大 → 被大幅放大;低频分量(ω 小) → 增益有限 → 放大不明显。

所以,引入微分环节就意味着系统里本来有的高频噪声(采样电流、位置传感器、开关噪声)会被其大大放大,而且放大后的噪声将直接进入控制器输出,表现为电流抖动、转矩脉动、机械振动

现代电机控制系统为了提高系统的可靠性,一般使用数字式 PID 控制器。因此,连续型 PID 控制算法不能直接使用,需要对上式进行离散化处理,得到离散 PID 控制律的差分方程 (也称位置式 PID 控制算法

\begin{gathered}u(k)=K_{\mathrm{P}}\left[e(k)+\frac{T}{T}\sum_{j=0}{k}e(j)+\frac{T_{\mathrm{D}}}{T}(e(k)-e(k-1))\right]\=K_\mathrm{P}e(k)+K_\mathrm{I}\sum_{j=0}ke(j)+K_\mathrm{D}(e(k)-e(k-1))\end{gathered}

式中,K_\mathrm{I} 为积分系数;K_\mathrm{D} 为微分系数;T 为采样周期;e(k)e(k-1) 为第 kk-1 采样时刻输入的偏差值。

有一个网站可以通过自己设置模拟飞行器参数,来直观理解 PID:Webpack App (rossning92.github.io),可以玩一玩感受一下。

4.2.4.2 FOC 中的双环控制

img

在 4.2.2.1 分析中我们得出的结论:“交轴电流 Iq 有助于产生扭矩,而直轴电流 Id 则不会产生任何扭矩”。因此,为了获得最大扭矩,我们可以使用两个 PI 控制器:一个使 Id 归零,而另一个使 Iq 最大化。由于我们采用 id = 0* 策略,故 Id 的参考量已知为 0,那么 Iq 的参考量如何得到呢?因为 Iq 控制转矩电流,直接对应电机的输出转矩,而速度又和扭矩正相关,所以我们就可以把速度误差转化为 Iq 的电流参考,再交给电流环去调节。

综上,我们需要 2 个电流环(内环):一个控制 d 轴电流Id),一个控制 q 轴电流Iq);1 个转速环(外环):将速度 PI 的输出作为转矩电流 Iq 参考值。

  • 内层 电流环 → 响应最快,抑制电流波动,保证转矩控制精度。

  • 外层 速度环 → 调整转速误差,给电流环提供参考值。

img

此处为何只用到了比例积分(PI)控制而没有引入微分(D)呢?除了前文所说的微分环节易使系统受到高频干扰外,稚晖君 在文章中说:“如果推导一下电压和电流的传递函数会发现这其实就是一个一阶惯性环节(而且实际上我们可以通过零极点对消来简化掉 PI 参数,只需要控制一个参数即电流带宽即可)”。

【(知识复习)一阶惯性环节】

一阶惯性环节数学形式是:

G(s)=\frac{K}{Ts+1}

  • K:系统的稳态增益;

  • T:时间常数(决定响应快慢)。

【为何此处传递函数为一阶惯性环节?】

如果我们忽略转速相关项,只看电阻和电感部分,那么就得到类似电感串联电阻(RL 电路)的微分方程:

u=Ri+L\frac{di}{dt}

对这个方程整理,可以得到电流对电压的传递函数:

\frac{I(s)}{U(s)}=\frac{1}{Ls+R}

这就是一个一阶惯性环节,且其惯性环节的时间常数为:\tau=\frac{L}{R}

  • 电流环

以下引用稚晖君 的配图。

img

其中的 IqIdIq_RefId_Ref ,前两者大家知道是通过 Clarke 变换Park 变换得到的,而后两者是我们希望前两者达到的期望值。通过 PID 控制器使用上述输入(电流采样值、编码器位置)和输出(MOS 管开关状态)完成对电机电流的闭环控制。

  • 速度环

img

在上图中, 左上角的 Speed_Ref速度设定值ω 是电机的转速反馈。速度反馈可以通过电机编码器或者霍尔传感器等计算得。需要注意的是,这个新得到的速度不可以直接用于速度控制,需要进行滤波,否则跳动的速度信号将导致电机振荡*(吱吱吱…)*。滤波过程详见灯哥的[6.2 速度低通滤波 ](http://dengfoc.com/#/dengfoc/灯哥手把手教你写 FOC 算法/6.12 闭环速度_低通滤波)。

img

将得到的电机速度 ω 与速度设定值 Speed_Ref 进行误差值计算,代入速度 PI 环,计算的结果作为电流环的输入,就实现了速度-电流的双闭环控制

其中,仅有 P 环是不够的,因为单纯的比例运算会导致在同等输出力矩下,大负载时达到稳定速度会变慢(载大负载时惯性大),而小负载时稳定速度会变快。换言之,仅有 P 环无法使得电机根据负载自适应调整力矩输出。而 PI 控制器中的 I 环就为我们解决了这个问题。

I 环实际上就是由一个系数 Ki 和一个对误差在时间上进行不断积分的积分项组成的。也就是说,当这个误差如果存在的时间越长这个积分值就会越来越大,直到变为 0 为止。最后,这个积分值会乘上系数 Ki,进行一个 Ki 的比例缩放后叠加在电机力矩上。

总而言之,当有了 I 环后,一切就不同了:当这个误差很久都没有被 P 环调节过来时,I 环的积分就会不断的积分这个误差,使得电机的输出力越来越大,最终让电机实现更快速的纠偏。

(配图待选)

【(附加)位置环】(待更新)

img

上图中位置控制 PID 只用了 P 项(也可以使用 PI)。在实际使用中,由于编码器无法直接返回电机转速 ω ,因此可以通过计算一定时间内的编码值变化量来表示电机的转速:(本时刻的编码器值-上时刻的编码器值)/走过这个角度所用时间(也即用平均速度代表瞬时速度)。当电机转速比较高的时候,这样的方式是可以的;但是,在位置控制模式的时候,电机的转速会很(因为是要求转子固定在某个位置),这时候用平均测速法会存在非常大的误差(转子不动或者动地很慢,编码器就没有输出或者只输出 1、2 个脉冲)。

所以,为避免速度环节带来的误差,在做位置控制的时候可以只使用位置和电流组成的双环进行控制,不过此时需要对位置环做一定的变化,控制框图如下:

img

由于去掉了速度环,这里的位置环我们使用完整的 PID 控制,即把微分项加上(因为位置的微分就是速度,这样可以减小位置控制的震荡加快收敛;积分项的作用是为了消除静态误差)。

基于 DSP28335 的 FOC 算法代码编写如下

// 编码器角度计算
    QEPEncoder_Cale((p_EQEP) &EQEPPare);
    // 输出电角度 θe
    Speed_QEPPare.ElecTheta = EQEPPare.ElecTheta;
    Speed_QEPPare.DirectionQep = (int32)(EQEPPare.DirectionQep);
    // 速度计算
    Speed_QEP_Cale((p_Speed_QEP) &Speed_QEPPare);
    // 输出机械角速度 ωm
    // ADC 采样
	ADC_Sample();
	TaskTimePare.pwmisr_conut++;
    // 一个采样周期结束中断一次。T = 0.08ms;  25*T = 2ms
	if(TaskTimePare.pwmisr_conut == 25)
    {
	    TaskTimePare.pwmisr_conut = 0;
        // 速度环 2ms 刷新一次 =========================================================
        // ωm*
	    knob_control();    // 通过调节电位器旋钮输入速度目标值
	    pi_spd.Ref = pi_spd.Ref*20.0;    // 该系数可自己调节一下
	    // ωm
	    //pi_spd.Fbk = Speed_QEPPare.Speed;//Q24
	    pi_spd.Fbk = _IQ(Speed_QEPPare.SpeedRpm/60)-_IQ(2.5);
		PI_Controller((p_PI_Control) &pi_spd);
		pi_spd.OutF = _IQmpy(FilK1,pi_spd.OutF)+_IQmpy(FilK2,pi_spd.Out);//Q24
		// 输出 iq*
		// =======================================================================
    }
    // 【FOC 步骤 1】进行 Clarke 变换==============================================
	// 对 PMSM 进行电流采样
	ClarkeI.As = ADCSampPare.PhaseA_Curr;
	ClarkeI.Bs = ADCSampPare.PhaseB_Curr;
	// Clarke 变换
	CLARKE_Cale((p_CLARKE) &ClarkeI);
	// ====================================================================
	// 【FOC 步骤 2】进行 Park 变换================================================
	// 输入 Clarke 变换所获得的 Iα 与 Iβ
	ParkI.Alpha = ClarkeI.Alpha;
	ParkI.Beta  = ClarkeI.Beta;
	// 通过编码器读取电角度信息
	ParkI.Angle = EQEPPare.ElecTheta;
	ParkI.Sine   = _IQsinPU(ParkI.Angle);//Q24
	ParkI.Cosine = _IQcosPU(ParkI.Angle);//Q24
	// Park 变换
	PARK_Cale((p_PARK) &ParkI);
	// ====================================================================
	// 【FOC 步骤 3】进行 D、Q 轴的 PI 闭环运算=======================================
	// D 轴电流环
	// id* = 0
	pi_id.Ref = _IQ(0.0);   // 强制直轴分量为零
	// D 轴 PI 控制
	pi_id.Fbk = ParkI.Ds;
	PI_Controller((p_PI_Control) &pi_id);
	pi_id.OutF = _IQmpy(FilK1, pi_id.OutF) + _IQmpy(FilK2, pi_id.Out);
	// 输出 ud
	// Q 轴电流环
	// iq*
	pi_iq.Ref = pi_spd.Out; // 允许交轴分量增长
	// Q 轴 PI 控制
	pi_iq.Fbk = ParkI.Qs;
	PI_Controller((p_PI_Control) &pi_iq);
	pi_iq.OutF = _IQmpy(FilK1, pi_iq.OutF) + _IQmpy(FilK2, pi_iq.Out);
	// 输出 uq
	// 【1】纯开环运行模式
	if(logicContr.Run_mode == 1)
	{
	    // ud = 0
	    IparkU.Ds = 0;
	    // uq = ωm*
	    IparkU.Qs = pi_spd.Ref;
	}
	// 【2】速度和 Id 电流闭环正转运行模式
	if(logicContr.Run_mode == 2)
	{
	    // ud = D 轴 PI 输出
	    IparkU.Ds = pi_id.OutF;
	    // uq = iq*
	    IparkU.Qs = pi_spd.OutF;
	}
	// 【3】速度和 Id 电流闭环反转运行模式
	if(logicContr.Run_mode == 3)
	{
	    // ud = - D 轴 PI 输出
	    IparkU.Ds= -pi_id.OutF;
	    // uq = - iq*
	    IparkU.Qs= -pi_spd.OutF;
	}
//	// 【4】速度、id 和 iq 电流闭环运行模式
//	if(logicContr.Run_mode == 4)
//    {
//	    // ud = D 轴 PI 输出
//	    IparkU.Ds= pi_id.OutF;
//	    // uq = Q 轴 PI 输出
//	    IparkU.Qs= pi_iq.OutF;
//    }
	// ====================================================================
	// 【FOC 步骤 4】进行 Park 反运算==============================================
	// 角度信息
	IparkU.Sine   = ParkI.Sine;     // = _IQsinPU(ParkI.Angle);
	IparkU.Cosine = ParkI.Cosine;   // = _IQcosPU(ParkI.Angle);
	IPARK_Cale((p_IPARK) &IparkU);
	// 输出 uα、uβ
	// ====================================================================
	// 【FOC 步骤 5】将 SVPWM 输出================================================
	Svpwmdq.Ualpha = IparkU.Alpha;//Q24
	Svpwmdq.Ubeta  = IparkU.Beta; //Q24
	SVPWM_Cale((p_SVPWM) &Svpwmdq);
	Svpwm_Outpwm();
	// ====================================================================
	// 【FOC 算法结束】========================================================

各参数整定可参看:PI 调节器参数整定(双闭环 FOC) - 知乎 【PMSM】一. 经典电流环、速度环设计(上) - 知乎 永磁矢量控制_沉沙丶的博客-优快云 博客 。此外,袁雷老师的《现代永磁同步电机控制原理及 MATLAB 仿真》 一书中也有详细描述。

关于如何调试 FOC 可参看知乎问题:如何调试永磁同步电机有感 foc? - 知乎 (zhihu.com)

5 转子位置信息的获取和转速测算

那么如何保持转子和定子二者磁场正交呢?

  1. 确定转子位置;

  2. 基于转子位置,确定定子磁场矢量的期望方向,使它与转子磁场正交;

  3. 对三相电流进行控制,使其产生所需的定子磁场矢量。

img

img

无刷电机的控制是配合着转子(永磁体)的位置(角度)进行的,那如何获知控制所需的转子的位置信息?一般有有传感器无传感器两种方案:

  • 有传感器方案一般采用霍尔元件、光栅编码器、转速反馈频率信号(FG trace)、旋转变压器 (Resolver)等方式。
电机类型传感器种类主要用途特征
BLDC霍尔效应传感器梯形波、120 度通电控制每 60 度获取一次信号,价格较低不耐热
PMSM光电编码器正弦波控制、矢量控制增量型(可得知原位置开始的移动距离)和绝对型(可得知当前位置的角度)两种。分辨率高,但抗震性防尘能力较弱成本较高
转角传感器正弦波控制、矢量控制分辨率高适用于恶劣环境
  • 无传感器方案一般采用反电动势检测等方式。

5.1 BLDC——霍尔传感器

霍尔传感器在 N 极靠近时输出高电平、S 极靠近时输出低电平:

img

霍尔传感器并不会提供转子在扇区内的精确位置,但可以检测转子何时从一个扇区过渡到另一个扇区。以单相无刷电机为例,每当转子转过 90 度,霍尔信号输出电平交换一次。旋转一周则改变 4 次。

img

一般采用加装霍尔元件的方法,来防止在临界位置时电平紊乱输出的干扰:

img

多数无刷电机在其非驱动端上的定子中嵌入了三个霍尔传感器。根据这三个霍尔传感器信号的组合,就能决定换向的精确顺序。每次换向,都有一个绕组连到控制电源的正极,第二个绕组连到负极,第三个处于失电状态。其中,绕组加电的顺序由六步换向定义。下图为三个 60°间距霍尔传感器输出的高低电平波形及实际安装位置:

img

或采用 120°间距排布:

img

可利用三相霍尔元件的高低电平状态来进行扇区判断:

Hall_Three.HallUVW[0] = GpioDataRegs.GPCDAT.bit.GPIO67 & 0x01; // HALL1 U
Hall_Three.HallUVW[1] = GpioDataRegs.GPCDAT.bit.GPIO68 & 0x01; // HALL2 V
Hall_Three.HallUVW[2] = GpioDataRegs.GPCDAT.bit.GPIO69 & 0x01; // HALL3 W
Hall_Three.Hall_State = Hall_Three.HallUVW[0] + (Hall_Three.HallUVW[1] << 1) + (Hall_Three.HallUVW[2] << 2);

通过霍尔元件进行转速测算的原理用一句话概括,即计算两次换相事件的相隔时间。基于 DSP28335 的测速代码编写如下:

if(Hall_Three.Hall_State != Hall_Three.OldHall_State)   // 换相时刻
{
    Hall_Three.static_count = 0;
    // CPU 定时器计数寄存器 (TIMH:TIM)。
    // TIM 寄存器保存当前 32 位定时器计数值的低 16 位。每隔 (TDDRH:TDDR+1) 个时钟周期,TIMH:TIM 减 1,其中 TDDRH:TDDR 为定时器预定标分频系数。
    // 当 TIMH:TIM 递减到 0 时,TIMH:TIM 寄存器重新转载 PRDH:PRD 寄存器保存的周期值,并产生定时器中断 TINT 信号
    Hall_Three.Time0count = CpuTimer0Regs.TIM.all;  // 最大 30000000
    // 假定转速大于 (1/6/2 / 0.2s)(r/s)=0.4167r/s=25r/m
    // 两次换相时刻发生于同一 CpuTimer0 周期内
    Hall_Three.Speed_timecount = Hall_Three.Old_Time0count - Hall_Three.Time0count; // 两次换相的时间间隔计数差值
    if(Hall_Three.Speed_timecount < 0)  // 两次换相时刻发生于不同 CpuTimer0 周期
    Hall_Three.Speed_timecount = Hall_Three.Speed_timecount + 30000000;
    // T0 = (一周期计数值 / 150)us = (30000000 / 150)us = 200000us
    // 间隔计数值/150 = (间隔计数值/一周期计数值 * 一周期计数值 / 150)us = 间隔计数值/一周期计数值 * T0 = 间隔 t us
    Hall_Three.Speed_timecount = Hall_Three.Speed_timecount / 150;  // 将计数差值处理为单位为 us 的时间间隔
    Hall_Three.Speed_timecountFitter = _IQ10mpy(HallK1, Hall_Three.Speed_timecountFitter)+_IQ10mpy(HallK2, Hall_Three.Speed_timecount);
    Hall_Three.Old_Time0count = Hall_Three.Time0count;  // 储存当前换相时刻的 Time0count
    // n(RPS) = (1/6/极对数)(r) / (换相间隔时间)(s)
    // n(RPM) = 60 * (1/6/极对数)(r) / (换相间隔时间)(s) = (10 / 极对数 / 换相间隔时间 s)(RPM)
    // Speed_RPM = speed_coff / Speed_timecountFitter = (1/极对数)*2^24 / 换相间隔时间 us = (2^24 / 10 * 1/10^6) * 10/极对数/换相间隔时间 s
    //           = 1.6777248*n(RPM) = 1.6777248*60*n(RPS) = 100.663488*n(RPS)
    Hall_Three.Speed_RPM = (Hall_Three.speed_coff/Hall_Three.Speed_timecountFitter);
    Hall_Three.IQSpeed_RPM = _IQ12toIQ(Hall_Three.Speed_RPM);
}
else if(Hall_Three.Hall_State == Hall_Three.OldHall_State)  // 位置检测和上次处于同一区间
{
    Hall_Three.static_count++;
    if(Hall_Three.static_count >= 5000)
    {
        Hall_Three.static_count = 0;
        STOP_CAR( );
        Hall_Three.Speed_RPM = 0;
    }
}
Hall_Three.OldHall_State = Hall_Three.Hall_State;   // 储存当前位置

其中,两极对电机的极对数及相关参数设置如下:

Hall_Three.Poles = 2;
Hall_Three.speed_coff = _IQ(0.166667) * 6 / Hall_Three.Poles;
// _IQ(0.166667) * 6 / Hall_Three.Poles = 0.166667 * 16777216.0 * 6 / 2 = 8388624.8 约等于 2^24 / 2 = 8388608

5.2 PMSM——增量式编码器

5.2.1 编码器测速代码

基于DSP28335的代码编码器相关代码及注释书写如下:


#define MOTOR_LINE 1000 // 编码器线数(光栅数),即旋转一圈产生的脉冲数
#define PI         3.14159265358979
extern  EQEP       EQEPPare;
extern  Speed_QEP  Speed_QEPPare;
extern  float32    T;
void  QEPEncoder_init(void)
{
    EALLOW;
	EQep1Regs.QDECCTL.all = 0x0000;         // QEP 解码控制寄存器
	// QSRC(15~14 位) - 00:位置计数器选择正交计数模式
	// XCR(11 位)  - 0:2 倍外部时钟频率,上下边沿计数
	// SWAP(10 位) - 0:内部不交换正交脉冲信号
	EQep1Regs.QEPCTL.all  = 0x821E;         // QEP 控制寄存器
	// 1000 0010 0001 1110:
	// 1000:
	// FREE,SOFT - 10:仿真挂起对其无影响,调试中断时不复位 EQEP;
	// PCRM - 00:位置计数器复位模式为当索引(Index)事件发生时触发 QPOSCNT 复位。QPOSCNT 计数值从 0~4N 线性发化,即仍位置计数寄存器中可直接得到不转子位置成比例关系的位置计数值 QP0SCNT。
	// 0010:
	// SEI - 00:索引事件初始位置计数器 SEI 不动作;
	// IEI - 10:索引事件初始位置计数器 IEI 的初始位置计数器在 QEPI(即 Z 信号)上升沿
	// 0001:
	// SW1 - 0:软件初始化位置计数器不动作;
	// SEL - 0:在 QEPS 的上升沿锁存位置计数器的值;
	// IEL - 01:在索引信号上升沿锁存位置计数器
	// 1110:
	// QPEN - 1:正交位置计数器使能;
	// QCLM - 1:当单位时间事件发生时,锁存数据;
	// UTE  - 1:使能单位定时器;
	// WDE  - 0:禁止 QEP 看门狗
	EQep1Regs.QPOSCTL.all = 0x0000;  // QEP 位置比较控制寄存器
	// PCSHDW - 0:禁止映射寄存器,立即加载;映射寄存器的加载
	EQep1Regs.QUPRD = 1500000;  // QEP 单位时间周期寄存器。此寄存器中的值为单位时间周期值,1500000 代表设置频率为 100Hz
	EQep1Regs.QCAPCTL.all = 0x8075;  // QEP 捕获控制寄存器
	// 1000 0000 0111 0101:
	// CEN  -    1:使能 QEP 捕捉单元
	// CCPS -  111:CAPCLK = SYSCLK / 128,CAP 捕获 128 分频
	// UPPS - 0101:UPEVNT = QCLK / 32,单位位置 32 分频
	EQep1Regs.QPOSMAX = 4 * MOTOR_LINE; // QEP 最大位置计数器寄存器。乘 4 是因为:通过梱测 QEPA 和 QEPB 信号的边沿为位置计数器提供计数时钟 QCLK,因此 eQEP 逻辑产生的时钟频率是输入时钟频率的 4 倍。
	// 位置计数器的值是脉冲的累积,当位置计数器达到这个值时,会自动回滚到零。
	EDIS;
	// EQEP
	EQEPPare.PolePairs = 2;
	EQEPPare.initial_angle = _IQ(0.0);
	// Speed_QEP
	// n = BaseRpm = 60*BASE_FREQ/(极对数 PolePairs)
	// BASE_FREQ = n*PolePairs/60
	Speed_QEPPare.BaseRpm = 3000;
	// 速度系数 K1 计算 ==========================================================
	// 单圈总脉冲数为 C(=4*1000),每 T(=0.08ms) 进行一次测算,T 时间内统计到的编码器脉冲数为 M0
	// M 法测速:转速 n = (M0/(C*T))(r/s) = (60 * (极对数*M0/C) * (C/极对数) / (C * T))(r/min) = (1 / ((PolePairs/60)*T) * deltaE)(r/min)
	// 机械角度增量 deltaM = M0/C (pu)     电角度增量 deltaE = 极对数*deltaM = 极对数*M0/C(pu)
	// n = (1 / ((PolePairs/60)*T)) * deltaE
	// 1 = (1 / ((n*PolePairs/60)*T)) * deltaE = (1 / (BASE_FREQ*T)) * deltaE = K1 * deltaE
	// 即,如果 K1 = 1 / (BASE_FREQ*T),则输出的 Speed = _IQ(1.0)
	Speed_QEPPare.K1 = _IQ21( 1 / ((Speed_QEPPare.BaseRpm*EQEPPare.PolePairs/60)*T) );    // 1 / (BASE_FREQ*T)
	// =====================================================================
	// 滤波系数 K2、K3 计算 =======================================================
	// 采样周期 T = 0.00008s = 80us;截止频率 fc = 5Hz,时间常数 Tc = 1/(2*pi*fc)
	// 一阶低通滤波器
	// s 域:Y(s)/R(s) = 1/(Tc*s + 1)
	// 时域:dY(t)/dt = 1/Tc * [R(t) - Y(t)]
	// 离散化[Y(k)-Y(k-1)]/T = 1/Tc * [R(k) - Y(k)]
	// 所以有:Y(k) = Tc/(Tc + T) * Y(k-1) + T/(Tc + T) * R(k)
	// 令 K2 = Tc/(Tc + T) = 1/(1 + T * 2*pi*fc),K3 = 1 - K2
	Speed_QEPPare.K2 = _IQ(1 / (1 + T * 2*PI*100));
	Speed_QEPPare.K3 = _IQ(1) - Speed_QEPPare.K2;
	// =====================================================================
}
void  QEPEncoder_Cale(p_EQEP pV)
{
    // 状态寄存器 QEPSTS
    // 正交方向标志位 QDF:0-逆时针;1-顺时针
    pV->DirectionQep = EQep1Regs.QEPSTS.bit.QDF;
    // EQEP1 的位置计数器寄存器 QPOSCNT
    // 用 QEP 的计数值 RawTheta 来表示的电机实际角度
    pV->RawTheta = EQep1Regs.QPOSCNT + pV->CalibratedAngle; // CalibratedAngle 电机 A 相绕组和码盘 Index 信号之间的夹角的对应计数值
    if(pV->RawTheta < 0)
        pV->RawTheta = pV->RawTheta + EQep1Regs.QPOSMAX;
    else if(pV->RawTheta > EQep1Regs.QPOSMAX)
        pV->RawTheta = pV->RawTheta - EQep1Regs.QPOSMAX;
    // 计算机械角度 ====================================================================================================
    // MechTheta = (0.9999/total count) * 角度计数值
    // 即用一无量纲比例数 (pu) 来表示机械角度。例如,如果求得机械角度=0.6,即完整一周旋转的 60%。
    pV->MechTheta = pV->MechScaler * pV->RawTheta;//Q24
    // ================================================================================================================
    // 计算电角度 ======================================================================================================
    // 机械角除以一整圈(360 度)时的余数。可确保生成的电角度始终位于 _IQ(0.0) 和 _IQ(1.0) 之间。
    // 如果 MechTheta 为 _IQ(0.6)(完整旋转的 60%) ,则电角度将为 (2 * _IQ(0.6) - _IQ(1.0)) = _IQ(0.2)。
//    pV->ElecThetaYS = pV->MechTheta % _IQ(1.0);
    pV->ElecThetaYS = (pV->PolePairs * pV->MechTheta) % _IQ(1.0);
    pV->ElecTheta = pV->ElecThetaYS + pV->initial_angle;//Q24
//    if(pV->ElecTheta > _IQ(1.0))
//        pV->ElecTheta -= _IQ(1.0) ;
//    else if(pV->ElecTheta < _IQ(0.0))
//        pV->ElecTheta += _IQ(1.0);
    // ================================================================================================================
    // QEP 中断标志寄存器 QFLG
    if(EQep1Regs.QFLG.bit.IEL == 1) // 索引事件锁存中断标志 IEL - 1:中断产生
    {
        pV->IndexSyncFlag = 0x00F0;
        pV->QepCountIndex = EQep1Regs.QPOSILAT; // 当索引事件发生时,位置计数器的值会加载到这个寄存器(QEP 索引位置加载寄存器) QPOSILAT 中
        // QEP 中断清除寄存器 QFLG
        EQep1Regs.QCLR.bit.IEL = 1;	// 清除索引事件锁存中断标志 IEL - 1:清除中断标志
    }
    if(EQep1Regs.QFLG.bit.UTO == 1) // 单位时间事件中断标志 UTO - 1:中断产生
    {
        // QEP 状态寄存器 QEPSTS
        // COEF 捕获溢出错误标志位:0-无意义;1-在 QEP 捕获计时器发生溢出
        // CDEF 捕获方向错误标志位:0-无意义;1-在捕获事件发生时,方向改变
        if((EQep1Regs.QEPSTS.bit.COEF || EQep1Regs.QEPSTS.bit.CDEF))
            EQep1Regs.QEPSTS.all = 0x000C; // C(1100):COEF-1;CDEF-1
        // QEP 捕获周期锁存寄存器 QCPRDLAT —— QEP 边沿捕获周期锁存寄存器
        else if(EQep1Regs.QCPRDLAT != 0xffff)
            pV->QepPeriod = EQep1Regs.QCPRDLAT;
    }
}
void  Speed_QEP_Cale(p_Speed_QEP pV)
{
    if((pV->ElecTheta < _IQ(0.9)) & (pV->ElecTheta > _IQ(0.1))) //
        pV->Tmp = _IQmpy(pV->K1, (pV->ElecTheta - pV->OldElecTheta));//Q21
    else // 0.9-0.0-0.1 的过零过程不可用上式计算速度
        pV->Tmp = _IQtoIQ21(pV->Speed);//Q21
    // 速度滤波(K2、K3 推导见上)
    pV->Tmp = _IQmpy(pV->K2, pV->Tmp) + _IQmpy(pV->K3, _IQtoIQ21(pV->Speed));//Q21
	// 限幅在 (-1, 1) 之间
    pV->Tmp=_IQsat(pV->Tmp, _IQ21(1), _IQ21(-1));//Q21
    pV->Speed = _IQ21toIQ(pV->Tmp);//Q24
    pV->OldElecTheta = pV->ElecTheta;//Q24
    pV->SpeedRpm = _IQmpy(pV->BaseRpm, pV->Speed);//Q0
    if(pV->SpeedRpm < 0)
        pV->SpeedRpm = -pV->SpeedRpm;
}

代码中对所得速度采用了一阶滤波,其相关资料如下:

5.2.2 转子初始角度修正

上文代码中,有一极易忽视又至关重要的变量 CalibratedAngle。该变量为电机 A 相绕组和码盘 Z 相信号(Index 信号)之间的夹角的对应计数值,用于对转子初始角度进行修正。

在电机通电前,电机转子处于一未知位置,而 FOC 算法的关键便是基于转子位置生成与之方向正交的磁场以驱动转子旋转,故而在驱动电机旋转之前必须要对电机转子初始角度进行修正。转子初始角度修正一般采用磁定位的方法,即通过给定子绕组通入一已知大小和方向的电流 is ,以产生恒定的磁场,吸引转子旋转至与定子绕组磁链重合位置,从而得到转子的初相位。在此,笔者将自己本科毕业论文截取部分进行说明:

img

电机各绕组抱轴代码及注释如下:


void AnglePhase_init(void)
{
    // 当转子磁链与 A 轴重合的时候,逆变器的开关状态为:SA:SB:SC—1:0:0
    // 因此可以得到 ia = iDC,ib = ic = −iDC/2(iDC 为母线电流)
    // 通过 Clarke 变换有:
    // Ialpha = ia = iDC
    // Ibeta  = sqrt(3)/3 * (ia + 2*ib)
    //        = sqrt(3)/3 * (iDC + 2*(−iDC/2)) = 0
    // theta = 0,则通过 Park 变换有:
    // Id = Ialpha * cos(theta) + Ibeta  * sin(theta)
    //    = iDC * cos(theta) = iDC
    // Iq = Ibeta  * cos(theta) - Ialpha * sin(theta)
    //    = -iDC * sin(theta) = 0
    // theta = -90°,则通过 Park 变换有:Id = 0;Iq = iDC
    if(mod == 1)    // 与 A 轴重合
    {
        IparkU.Ds   = _IQ(10);
        IparkU.Qs   = 0;
        ParkI.Angle = 0;
    }
    else if(mod == 2)
    {
        IparkU.Ds   = 0;
        IparkU.Qs   = _IQ(10);
        ParkI.Angle = _IQ(-0.25);
    }
    // 当转子磁链与 B 轴重合的时候,逆变器的开关状态为:SA:SB:SC—0:1:0
    // 因此可以得到 ib = iDC,ia = ic = −iDC/2(iDC 为母线电流)
    // 通过 Clarke 变换有:
    // Ialpha = ia = −iDC/2
    // Ibeta  = sqrt(3)/3 * (ia + 2*ib)
    //        = sqrt(3)/3 * (−iDC/2 + 2*(iDC)) = sqrt(3)/2 * iDC
    // theta = 0,则通过 Park 变换有:
    // Id = Ialpha * cos(theta) + Ibeta  * sin(theta)
    //    = −iDC/2
    // Iq = Ibeta  * cos(theta) - Ialpha * sin(theta)
    //    = sqrt(3)/2 * iDC
    else if(mod == 3)
    {
        IparkU.Ds   = _IQ(-5);
        IparkU.Qs   = _IQ(0.866025404*10);
        ParkI.Angle = 0;
    }
    // 当转子磁链与 C 轴重合的时候,逆变器的开关状态为:SA:SB:SC—0:0:1
    // 因此可以得到 ic = iDC,ia = ib = −iDC/2(iDC 为母线电流)
    // 通过 Clarke 变换有:
    // Ialpha = ia = −iDC/2
    // Ibeta  = sqrt(3)/3 * (ia + 2*ib)
    //        = sqrt(3)/3 * (−iDC/2 + 2*(−iDC/2)) = -sqrt(3)/2 * iDC
    // theta = 0,则通过 Park 变换有:
    // Id = Ialpha * cos(theta) + Ibeta  * sin(theta)
    //    = −iDC/2
    // Iq = Ibeta  * cos(theta) - Ialpha * sin(theta)
    //    = -sqrt(3)/2 * iDC
    else if(mod == 4)
    {
        IparkU.Ds   = _IQ(-5);
        IparkU.Qs   = _IQ(-0.866025404*10);
        ParkI.Angle = 0;
    }
    ParkI.Sine   = _IQsinPU(ParkI.Angle);//Q24
    ParkI.Cosine = _IQcosPU(ParkI.Angle);//Q24
}
5.2.3 编码器差分信号处理

部分增量式编码器输出的是两两反相的 3 对差分脉冲信号:A+和 A-、B+和 B-、Z+和 Z-。针对这 3 对差分脉冲信号,需设计差分电路或采用现成芯片处理为 3 路单端信号,再输出给 MCU。推荐参考资料如下:

若期望输出信号电压为 3.3V,笔者推荐采用 TI 公司的 AM26LV32 芯片进行处理。

5.3 反电动势

在既定电机磁场和绕组数固定的情况下,反电动势的幅度电机的旋转速度成正比。当电机在控制模式下运转多个换向周期直到获得一定速度后,无传感器测量便能够确定转子位置。无传感器控制的无刷电机适合安装在难以检修的位置,或在多灰尘、多油的环境中运行,但不适合需要较低速度的应用,因为此时反电机势很小而难以测量(故其启动需要特殊方法),会造成工作效率不高。

img

一般监测未被驱动相的反电动势变化信息(过零点信息)推算转子角度:

img

基于反电动势测量转子位置的控制器,在启动时会面临额外的挑战:转子静止时不会产生反电动势(只有当转子磁场切割定子线圈时才会产生反电动势)。目前经常采用的启动方法为“三段式”启动。一般来说,三段式启动包括定位、加速、切换三个过程。

以下引用一篇硕士论文(吴财源.基于 DSC 的无刷直流伺服电机驱动器设计与研究[D]. 华南理工大学, 2011)举例说明反电势过零检测的硬件电路设计。

img

反电动势过零点硬件检测电路原理图如图 2-14 所示,端电压分压信号经过滤波后输入至比较器,与参考电压 Uref 进行比较。当非导通相反电势 eC = 0 时,有 UCG = 0,此时通过比较器 ZC 输出低电平,即得到反电势过零点信号。

img

关于构建中性点和过零检测参考资料:

6 系统设计

6.1 考虑因素

img

功率管设计需考虑以下问题:

img

其中开关损耗有如下公式:

img

6.2 硬件与仿真资料推荐

【推荐开源工程】


PID 超详细教程——PID 原理+串级 PID+C 代码+在线仿真调参

by skythinker 于 2022.2.21 发布 已于 2024-05-24 21:24:36 修改

前言

很多人应该都听说过 PID,它的运算过程简单,并能在大多情况下实现较好的控制效果,因此它是工程实践中使用最广泛的控制方法之一。

抛开公式,我将带你从案例出发,详细了解 PID 的工作原理和使用方法。

注:阅读本文不需要有过多的基础知识,只需中学物理和数学知识就能看懂(当然如果有高等数学知识和单片机知识的话理解起来会更容易)

仿真调参环境

我专门为本文搭了一个在线仿真环境,下面使用的案例都来自这个环境,读者可以搭配使用

注:如果该网页打不开,可以手动下载这个项目后在本地运行

案例引入——小球位置控制

任务介绍

我们假设有一个一维的坐标轴(向右为正方向),在上面上有一个小球(可以看作质点),小球不受任何阻力,可以自由左右滑动;另外,我们还为小球规定了一个目标位置(图中的绿色标线):

小球控制虚拟环境

现在我们有下述任务:

  • 目标:在小球上施加一个水平方向的力(称为控制力),使小球在偏离目标位置时回到目标位置
  • 已知条件:小球的实时坐标、目标位置坐标

看到这里的你可以停下来想一想,应该用什么样的策略来计算这个力呢?

这里大多数人应该都能想出这样的方法:

“当小球在目标左边的时候向右施力,当小球在目标右边的时候向左施力,就可以保证小球一直在目标位置上了”

思路是非常正确的,但这个策略仍不够完善。由于小球存在惯性,我们施加的力将小球拉回目标位置后小球还会具有一定的速度继续运动,并不会直接停在目标位置。

用 PID 完成任务

接下来我们来看看如果使用 PID,我们应该如何计算出这个控制力呢?

误差计算

计算 PID 的第一步就是计算误差(Error):误差=目标值-反馈值。在这个例子中,目标值是目标位置坐标,反馈值是小球实时位置坐标,那么误差就是小球当前位置与目标间的距离。

接下来的运算我们都会围绕误差进行,分为三个步骤使用误差分别算出一个分力,并将三个分力一起施加在小球上。

比例环节

第一个环节是比例环节P(Proportion),这个环节产生的分力是:

F p = k p ∗ E r r o r F_p=k_p*Error Fp=kpError

即:分力大小与误差成正比,且当小球在目标左边的时候分力向右,当小球在目标右边的时候分力向左,其中k_p 是比例系数。

比例环节的计算方法其实与上面大家通过直觉得出的方法差不多,如果只有这个分力作用的话,会产生什么效果呢?

大家可能会发现,这就与中学物理里的弹簧滑块模型是一样的,力与距离成正比,显然小球会以目标位置为中心进行左右摆动(简谐振动)(注:图中蓝色短线表示控制力):

只有比例环节时的小球运动

微分环节

那么如何让小球能够静止在目标点呢?这就要请出 PID 的另一个环节:微分环节D(Differential)

微分环节也会计算出一个分力,计算方法是:

F d = k d ∗ d E r r o r d t F_d=k_d*\frac{\mathrm{d} Error}{\mathrm{d} t} Fd=kddtdError

也就是说,这个分力与误差的变化速度有关。假设目标位置不变,小球向右运动时误差减小,即误差变化速度为负,分力向左;反之当小球向左运动时分力向右;综合看来,微分环节产生的分力始终阻碍小球的运动。

因此如果在刚刚的基础上加入微分产生的分力,就会产生一个阻尼效果,小球会仿佛始终受到一个阻力,因此左右摆动的幅度会逐渐减小,最终收敛到目标位置上:

有比例和微分环节时的小球运动

由公式还可以看出,微分系数k_d 可以影响这个“阻力”的大小,因此如果我们把系数调大一些,就可以让小球的运动收敛得更快一些:

调大 kd 后的小球运动

到这里,其实我们已经完成我们的目标任务了,小球可以在驱动力的作用下运动到目标位置。

积分环节

但现在,我们更希望在小球有一些外部干扰时也能实现上面的效果,比如我们在小球上加上一个水平向右的恒力,此时会发生什么呢?

恒力干扰下小球静止状态

小球在运动过程中仍然会像之前一样接近目标点,但在最终停下来时我们会发现,小球无法精确停在目标点上,而是像上图一样停在离目标点有一定距离的地方。此时控制力与干扰恒力平衡,小球静止。

稍加分析我们就能发现,此时小球静止,微分环节产生的分力为零,控制力完全由比例环节产生,且若距离更小则比例环节的输出更小,更无法平衡干扰力,因此小球无法继续向目标点接近。

此时就需要我们的第三个环节出场了:积分环节I(Integral),它的计算方法是:

F i = k i ∗ ∫ E r r o r d t F_i=k_i*\int Error\mathrm{d}t Fi=kiErrordt

也就是说积分环节产生的分力正比于误差的积分,当误差持续存在时,这个分力会逐渐变大,试图消除误差。

加入积分作用,我们的 PID 就能完美实现在有恒力干扰的情况下对小球的控制了:

PID 作用下小球控制效果

抛开案例——更专业地理解 PID

常用术语

  • 被控对象:需要控制的对象,案例中指小球
  • 目标值:期望被控对象达到的状态量,案例中指目标位置的坐标
  • 反馈值:被控对象当前时刻的状态量,案例中指小球的实时位置坐标
  • 输出量:PID 的计算结果,案例中指控制力
  • 误差:目标值-反馈值
  • 稳态误差:系统稳定状态下仍存在的误差,如案例中加入干扰恒力后小球静止时仍存在的误差

不同参数下系统的阶跃响应(源:百度百科)

  • 阶跃输入:在稳定状态下目标值发生突然变化(上图目标值在 0 时刻由 0 跃升到虚线位置)
  • 阶跃响应:阶跃输入后被控对象的跟随状态,能够代表系统的控制性能(上图彩色线条)
  • 响应速度:阶跃输入后被控对象再次到达目标值的速度
  • 超调量:阶跃输入后,被控对象到达目标值后超出目标值的距离(上图各彩色线条第一个峰值与目标值的距离)

PID 计算过程

PID 信号框图

上图就是 PID 的信号框图,表示了 PID 的运行过程:

  1. 为系统指定一个目标值
  2. PID 将目标值与被控对象当前的反馈量作差得到误差
  3. PID 将误差值分别经过三个环节计算得到输出分量,三个分量加起来得到 PID 的输出
  4. 将 PID 的输出施加到被控对象上,使反馈量向目标值靠拢

PID 三个环节的作用

由控制小球案例我们可以总结出 PID 三个环节各自的主要作用和效应:

  • 比例环节:起主要控制作用,使反馈量向目标值靠拢,但可能导致振荡
  • 积分环节:消除稳态误差,但会增加超调量
  • 微分环节:产生阻尼效果,抑制振荡和超调,但会降低响应速度

PID 中物理量的设计

我们在设计 PID 时主要关注三个量:目标值反馈值输出值,PID 会根据目标值和反馈值计算输出值。

需要强调的是,PID 并不知道被控对象是什么,它仅负责进行数值计算,而我们——作为控制系统的设计者,就需要为 PID 指定这三个量所对应的实际物理量,这在不同的控制系统中是不一样的。

那么如何确定实际物理量呢,我为大家总结了一个常用准则:

  • 目标值和反馈值通常为同种物理量,就是你需要控制的物理量
  • 输出值通常是直接驱动被控对象的控制量
  • 输出量作用在被控对象上需要经过时间积累才会产生反馈量的变化,换言之输出值通常为反馈值对于时间的低阶物理量。比如:目标值和反馈值为位置,则输出值可以为速度或加速度
  • 对于线性关系的两个物理量(只差一个系数),可以直接替换。比如:目标和反馈值为小球位置,根据上一条准则,输出值可以为加速度。但我们无法直接控制加速度,只能控制驱动力大小,由于驱动力与加速度只差一个系数(F=ma),因此可以将输出值直接定为驱动力

接下来给出几个例子:

  • 任务一:对小球进行速度控制

    可用条件:已知小球的实时速度,并且可施加一个力来改变小球的速度
    PID 目标值:需要小球达到的速度
    PID 反馈值:小球的实时速度
    PID 输出值:施加在小球上的力
    分析:小球加速度是小球速度的低阶物理量,而施加的力正比于小球加速度

  • 任务二:对电机转速进行控制

    可用条件:已知电机的实时转速,并且可控制电机中流过的电流大小
    PID 目标值:需要电机达到的转速
    PID 反馈值:电机的实时转速
    PID 输出值:电机中流过的电流大小
    分析:电机中流过的电流大小近似正比于电机的扭矩,也就近似正比于电机角加速度的大小,是转速的低阶物理量,因此可以用电流大小作为输出值

  • 任务三:液位高度控制

    描述:容器有进水口和出水口,需要通过进水口的阀门控制容器内液位的高度
    可用条件:容器内液位实时高度,可控制进水口阀门液体流速
    PID 目标值:需要达到的液位高度
    PID 反馈值:液位实时高度
    PID 输出值:阀门液体流速
    分析:阀门液体流速正比于液位高度的变化速度,是液位高度的低阶物理量

由虚到实——代码编写

程序流程

根据上面的步骤,我们其实很容易就能得出程序的执行流程了,就是在计算误差后逐个进行 PID 各环节的计算。要注意的是整个采样-计算-输出的流程需要定时执行,可以在每次流程运行完后延时一段固定的时间(一般而言计算频率不高于传感器反馈频率,但也不能太低,否则会使控制精度下降)。

代码流程图

实现细节

有一个问题没有解决,积分和微分应该怎么算呢?毕竟微积分都是连续的,而我们采样得到的是离散的数据点。其实也很简单,离散状态下的积分计算其实就是把过去采样得到的所有误差加在一起,而微分计算就是把这一轮计算得到的误差与上一轮的误差相减。

最后,我们一般还会对 PID 的积分和输出进行限幅(规定上下限),积分限幅可以减小积分引起的超调,输出限幅可以保护执行机构或被控对象。

C 语言代码

//首先定义 PID 结构体用于存放一个 PID 的数据
typedef struct
{
   	float kp, ki, kd; //三个系数
    float error, lastError; //误差、上次误差
    float integral, maxIntegral; //积分、积分限幅
    float output, maxOutput; //输出、输出限幅
}PID;

//用于初始化 pid 参数的函数
void PID_Init(PID *pid, float p, float i, float d, float maxI, float maxOut)
{
    pid->kp = p;
    pid->ki = i;
    pid->kd = d;
    pid->maxIntegral = maxI;
    pid->maxOutput = maxOut;
}

//进行一次 pid 计算
//参数为 (pid 结构体,目标值,反馈值),计算结果放在 pid 结构体的 output 成员中
void PID_Calc(PID *pid, float reference, float feedback)
{
 	//更新数据
    pid->lastError = pid->error; //将旧 error 存起来
    pid->error = reference - feedback; //计算新 error
    //计算微分
    float dout = (pid->error - pid->lastError) * pid->kd;
    //计算比例
    float pout = pid->error * pid->kp;
    //计算积分
    pid->integral += pid->error * pid->ki;
    //积分限幅
    if(pid->integral > pid->maxIntegral) pid->integral = pid->maxIntegral;
    else if(pid->integral < -pid->maxIntegral) pid->integral = -pid->maxIntegral;
    //计算输出
    pid->output = pout+dout + pid->integral;
    //输出限幅
    if(pid->output > pid->maxOutput) pid->output =   pid->maxOutput;
    else if(pid->output < -pid->maxOutput) pid->output = -pid->maxOutput;
}

PID mypid = {0}; //创建一个 PID 结构体变量

int main()
{
    //...这里有些其他初始化代码
    PID_Init(&mypid, 10, 1, 5, 800, 1000); //初始化 PID 参数
    while(1)//进入循环运行
    {
        float feedbackValue = ...; //这里获取到被控对象的反馈值
        float targetValue = ...; //这里获取到目标值
        PID_Calc(&mypid, targetValue, feedbackValue); //进行 PID 计算,结果在 output 成员变量中
        设定执行器输出大小 (mypid.output);
        delay(10); //等待一定时间再开始下一次循环
    }
}

最后一步——PID 参数调整

在完成控制器代码编写后,就要连接好系统进行调参了,我们需要确定最合适的k_pk_ik_d 使控制效果最优。

通常还是使用经验法调参,通俗而言就是“试参数”,测试多个参数选取最好的控制效果,一般的步骤如下:

  1. 先将所有参数置零
  2. 将输出限幅设为执行机构能接受的最大值
  3. 增大 p 参数,使响应速度达到比较好的水平
  4. 若存在稳态误差,逐渐增加 i 参数和积分限幅,使稳态误差消失
  5. 若希望减少超调或振荡,逐渐增加 d 参数,在保证响应速度的前提下尽可能降低超调

此时大家可以使用上述的小球仿真环境体验一下各参数对系统的影响。到这里,我们就已经能够使用 PID 来控制各种对象了。

总结——使用 PID 的步骤

  1. 确定需要控制的对象,确定需要控制的物理量,确定反馈量的获取方式,确定被控对象的控制方式
  2. 检查目标值、反馈值、输出值对应物理量的关系是否符合上面说的准则
  3. 编写代码,将上述三个值的数值变量传入 PID 进行运算,并将 PID 运算结果输出到执行机构
  4. 进行参数调整

更进一步——串级 PID

从单级到串级

当我们在进行小球的位置控制时,我们可能经常会发现一个问题,如果小球与目标之间的距离较远的话,小球在运动过程中的速度会很快,会导致较大的超调,而且不论怎么修改参数都很难让系统的表现更好一些。

单级 PID 控制小球效果

这时你可能会想,如果运动过程中的速度没这么快就好了,这样就不会冲过头了。没错,这就要用到串级 PID 了。

我们上面所说的算法其实就是单级 PID,目标值和反馈值经过一次 PID 计算就得到输出值并直接作为控制量,但如果目标物理量和输出物理量之间不止差了一阶的话,中间阶次的物理量我们是无法控制的。比如:目标物理量是位置,输出物理量是加速度,则小球的速度是无法控制的。

而串级 PID 就可以改善这一点。串级 PID 其实就是两个单级 PID“串”在一起组成的,它的信号框图如下:

串级 PID 信号框图

图中的外环和内环就分别是一个单级 PID,每个单级 PID 就如我们之前所说,需要获取一个目标值和一个反馈值,然后产生一个输出值。串级 PID 中两个环相“串”的方式就是将外环的输出作为内环的目标值。

串级 PID 的物理量

如果将串级 PID 看作一个整体,可以看到他有三个输入和一个输出,而此时被控对象也需要提供两个反馈量,那么它们都应该对应些什么物理量呢?

首先我们回到最开始的小球案例中,如果用串级 PID 完成同样的任务,应该这样设计:

可用条件:小球实时位置、小球实时速度、施加在小球上的控制力
目标值:小球目标位置
外环反馈:小球实时位置
内环反馈:小球实时速度
输出值:施加在小球上的控制力

此时的信号框图会变成这样:

串级 PID 控制小球信号框图

可以发现,内环与小球构成了一个恒速系统,PID 内环负责小球的速度控制;而如果把内环和小球看作一个整体被控对象,外环又与这个对象一起构成了一个位置控制系统,外环负责位置控制;总体来说,外环负责根据小球位置误差计算出小球需要达到的速度,而内环负责计算出控制力使小球达到这个目标速度,两个环协同工作,就可以完成任务了。

如果不局限于这个案例来说,串级 PID 的内环一般负责低阶物理量的调节,而外环负责高阶物理量的调节并计算出低阶物理量的目标值,比如下面这个例子:

  • 任务:对电机进行串级角度控制
    可用条件:电机实时角度、电机实时转速、可以控制电机电流大小
    外环目标值:需要电机达到的角度
    外环反馈值:电机的实时角度
    内环反馈值:电机的实时速度
    输出值:电机电流大小
    分析:外环负责电机角度控制,根据电机目标角度和反馈角度计算出目标转速;内环负责转速控制,根据速度反馈和目标转速计算出电流*

串级 PID 的效果

回到我们的小球控制案例,之前说使用串级之后我们就可以对速度进行控制了,如何进行控制呢?其实就是对外环 PID 的输出进行限幅,因为外环 PID 输出的是目标速度,限制外环输出就相当于限制了小球目标速度的最大值,内环也就会维持小球的速度不超过这个最大值了。

串级 PID 控制小球效果

可以看到,使用串级 PID 后小球不再像之前那样“着急”地奔着目标而去,而是以近似匀速运动到达目标点。由于位置误差很大,外环输出在大部分时间内都处于限幅的最大值,因此小球在运动中接近匀速,这个速度就是所设定的外环输出限幅。而且由于运动速度变慢了,超调也几乎消失了。这就是我们想要的“控制位置的同时还能控制速度”的效果。

串级 PID 的 C 语言代码

//此处需要插入上面的单级 PID 相关代码

//串级 PID 的结构体,包含两个单级 PID
typedef struct
{
    PID inner; //内环
    PID outer; //外环
    float output; //串级输出,等于 inner.output
}CascadePID;

//串级 PID 的计算函数
//参数 (PID 结构体,外环目标值,外环反馈值,内环反馈值)
void PID_CascadeCalc(CascadePID *pid, float outerRef, float outerFdb, float innerFdb)
{
    PID_Calc(&pid->outer, outerRef, outerFdb); //计算外环
    PID_Calc(&pid->inner, pid->outer.output, innerFdb); //计算内环
    pid->output = pid->inner.output; //内环输出就是串级 PID 的输出
}

CascadePID mypid = {0}; //创建串级 PID 结构体变量

int main()
{
    //...其他初始化代码
    PID_Init(&mypid.inner, 10, 0, 0, 0, 1000); //初始化内环参数
    PID_Init(&mypid.outer, 5, 0, 5, 0, 100); //初始化外环参数
    while(1) //进入循环运行
    {
        float outerTarget = ...; //获取外环目标值
        float outerFeedback = ...; //获取外环反馈值
        float innerFeedback = ...; //获取内环反馈值
        PID_CascadeCalc(&mypid, outerTarget, outerFeedback, innerFeedback); //进行 PID 计算
        设定执行机构输出大小 (mypid.output);
        delay(10); //延时一段时间
    }
}

串级 PID 的调参

一般而言,需要先断开两环的连接,手动指定内环目标值,进行内环调参,当内环控制效果较好后再接上外环进行外环调参,具体的调参方法与单级 PID 相同。

此时大家也可以使用小球仿真环境体验一下串级控制的效果。


via:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值