第27章OC
为了进一步提升无刷电机的可控性,引入了FOC控制方式。本章主要以PMSM为例带领大家一起学习FOC的控制方式。
本章分为如下几个小节:
27.1 FOC原理介绍
27.2 FOC硬件平台
27.3 FOC开发工具
27.4 FOC例程创建
27.5 FOC下载验证
27.6 ST MCSDK库源码介绍
27.7 FOC例程分析
27.1 FOC原理介绍
FOC(field-oriented control)为磁场定向控制,又称为矢量控制(vectorcontrol),是目前无刷直流电机(BLDC)和永磁同步电机(PMSM)高效控制的最佳选择。FOC可以精确地控制磁场大小与方向,使得电机转矩平稳、噪声小、效率高,并且具有高速的动态响应。目前已在很多应用上逐步替代传统的控制方式,在运动控制行业中备受瞩目。
我们都知道,电流可以产生磁场,并且磁场大小与电流大小成正比,因此为了使定子构造最合适的旋转磁场,需要精确控制线电流。想要磁场旋转就需要线电流做着正弦变化,而3组线圈绕组的角度差,就使得三相电流需要时刻做相位差为120度的正弦变化,这时可使定子构造最合适的旋转磁场,显然简单的6步换向无法控制三相电流做正弦变化,转矩在一定程度上会有跳变,这样无法输出稳定转矩,因此需要FOC来保持转子的扭矩时刻连续稳定可调。下面总结六步换向和FOC控制方式的对比表,如表27.1.1:
| 控制方式 | 优点 | 缺点 | 
| 六步换向 | 控制算法相较简单 | 精度差、运转不流畅、转矩波动大、存在一定的电流噪声、适用于对电机转动性能要求不高的场合 | 
| FOC | 转矩平稳、效率高、噪声小、动态响应快 | 硬件成本较高、对MCU性能有较高要求、控制算法难度大。 | 
表27.1.1 控制方式对比表
FOC框图介绍

图27.1.1.1 电流闭环控制FOC框图
图27.1.1.1是以电流闭环控制为例,也就是让电机始终保持一个恒定力矩(力矩与电流成正比)。
从上图可以看到最左边的Iq_Ref和Id_Ref两个变量经过PID控制器进行反馈调节,其中涉及到几个变换模块,包括:Clarke 变换、Park变换以及反Park变换,最后是SVPWM模块作用到三相逆变器进而控制电机旋转。相信大家对上面这些过程不是很理解,没关系,我们先来大致的概括下FOC的整个控制框图,后面会对整体框架进行拆分讲解。
- 对电机三相电流进行采样得到:ia、ib、ic;
- 将ia、ib、ic经过clarke变换得到iα、iβ;
- 将iα、iβ经过park变换得到iq、id;
- 计算iq、id和其设定值iq_Ref、id_Ref的误差;
- 将上述误差输入到两个PID(只用到PI)控制器,得到输出的控制电压Vq、Vd;
- 将Vq、Vd进行反park变换得到Vα、Vβ;
- 将Vα、Vβ输入SVPWM模块进行调制,合成电压空间矢量,输出该时刻三个半桥的开关状态进而控制电机旋转;
- 循环上述步骤。
下面我们来分别介绍每一步的具体操作以及意义。
FOC坐标变换
假设我们将一个PMSM电机手动让其匀速旋转,此时使用示波器观察它的三相输出电压(反电动势),我们会发现示波器上会得到三组正弦波,并且三组正弦波之间两两相位差为120°。如图27.1.2.1:

图27.1.2.1 PMSM电机运转时三相电流波形
所以反过来我们在三相无刷电机的三相线圈上输入上述三相正弦电压,那么就可以驱动无刷电机平稳高效地旋转了。而这正是FOC驱动无刷电机的基本手段,但是从控制的角度来看,我们根本就不想跟三个正弦波打交道,因为对于非线性的信号进行准确控制就要使用复杂的高阶控制器,这对于建模成本、处理器算力、控制实时性等都是非常不利的,那么有没有什么方法可以将被控制量线性化呢?答案是当然有的,只需要应用一些数学技巧。
Clarke变换
当PMSM电机匀速运转时,将采集到相位相差120°的三相电流Ia、Ib、Ic使用三相坐标系表示,如下图绿色线所示:

图27.1.2.1.1 三相坐标系
我们可以利用一些数学小技巧,将三相坐标系变换成直角坐标系,我们把新的直角坐标系命名为α-β坐标系(如图27.1.2.1.1红色线所示),变换原则是电流产生的磁场相等。
Clarck变换用于将三相静止坐标系变换到两相静止坐标系,变换前后在坐标系中产生的磁场等效。

图27.1.2.1.2 Clarck变换

经过Clarke变换后就变成了直角坐标系啦!变换前后的波形如图27.1.2.1.3:

图27.1.2.1.3 经过Clarke变换前后的波形
可以看到变换后还是正弦波,虽然少了一个需要控制的变量,但是新的变量还是非线性的(正弦),控制它依旧难度很大,那有没有办法把它们线性化呢,当然有的,接着就看下Park变换
Park变换以及反Park变换
Park变换可以将电机从两相静止坐标系变换到两相旋转坐标系(dq坐标系),从而解耦出电机转矩分量和电机的励磁分量,这两个分量相互垂直、互不影响。反Park变换即为Park的逆变换。
首先我们来看下Park变换的原理,这里将Clarke变换后的α—β坐标系旋转θ度,其中θ为转子旋转的角度,如下图:

图27.1.2.2.1 Park变换
图27.1.2.2.1中,d轴方向与转子内磁场方向重合,称为直轴;q轴方向与转子内磁场方向垂直,称为交轴。如图27.1.2.2.2所示

图27.1.2.2.2 d-q轴示意图
此时我们将α—β坐标系变换到d-q坐标系,即Park变换;依据坐标变换,Park变换矩阵为:

变换公式如下: 

在将Park变换后的结果在经过PID控制器,PI运算后的输出结果在进行反Park变换,反Park变换矩阵为:

变换公式如下:

首先α—β坐标系经过Park变换后,即为d-q坐标系,该坐标系是始终跟着转子旋转的,旋转角度θ需通过编码器/霍尔传感器读取。经过这一步的变换,我们会发现,一个匀速旋转向量在这个坐标系下变成了一个定值!(因为参考系相对于该向量静止了),这个坐标系下两个控制变量都被线性化了!Park变换前后波形如图27.1.2.2.3所示:

图27.1.2.2.3 Park变换前后波形
Park变换后的控制量为Iq、Id,很显然线性化后的控制量我们就可以使用PID来进行控制了;但是为什么我们还要进行反Park变换,将其又变换为α—β坐标系呢?因为SVPWM算法的实现需要用到静止的α—β坐标系,所以当我们完成了控制信号的PID运算后,还需进行反Park变换。关于PID控制部分这里就不赘述了,大家可以查看第八章的PID专题讲解。
27.1.3 FOC的目的
经过前面的一通操作我们将转子磁链进行了解耦,分解为转子旋转的径向和切向这两个方向的变量:Iq以及Id,那这两个控制量到底代表什么含义呢?我们通过图27.1.3.1来进行讲解。

图27.1.3.1 Iq、Id的含义
我们可以看到对于图27.1.3.1中的人来说,他想驴能一直往前走,即iq是他需要的驱动力,iq越大驴走得越快,iq越小驴走得越慢,而id对于他来说无关紧要。对于电机来说也是一样的道理,即FOC的目的如图27.1.3.2所示:

图27.1.3.2 FOC的目的
经过上述分析,我们得知:
1、Iq是我们需要的。代表了期望的力矩输出;
2、Id是我们不需要的。我们希望尽可能把它控制为0;
至此整个FOC框架基本捋清楚了,只差最后一个步骤就是“SVPWM”,下面我们开始对SVPWM做一个详细的介绍。
27.1.4 SVPWM解析
从前面的学习,我们知道如果要平稳地驱动三相电机转动,我们就需要生成三个相位相差120度的正弦波
为了使输出电流近乎于正弦波,可以使用PWM的方式,通过调整占空比使等效电流近似为正弦波,这种PWM也就是SPWM。SPWM是从电源的角度出发,构造出旋转所需的正弦电压,这种方式比较适合用作逆变器,但是对于无刷或者永磁同步电机,调节过的电源实际在定子线圈中产生的电流并不一定是所需的结果。这也就是所谓的开环,是没有反馈的。所以这种控制方式还是不能较为准确的控制定子电流。因此这种永磁无刷电机直接通三相交流电并不一定能很平稳的旋转起来。不然的话纯硬件就可以产生规则的电流,哪还需要微控制器做复杂运算。
隆重介绍,SVPWM,是的,它还是PWM,既然用MOS管调制就离不开PWM,它和SPWM的区别是SVPWM依靠的是MOS管的开关顺序和开关时间来调制有效电流。而SPWM不依赖开关顺序,3相独立调制。SVPWM是依赖开关顺序的,需要把控整体的3相开关,让它们组合出不同顺序和时间的开关序列,以此模拟出正弦电流。SVPWM m优点主要有:优化谐波程度比较高,消除谐波效果要比SPWM好,实现容易,可以提高电压利用率。比较适合数字化控制系统。
SVPWM的主要思想是以三相对称正弦波电压供电时三相对称电机定子理想磁链圆为参考标准,以三相逆变器不同开关模式作适当的切换,从而形成PWM波,以所形成的实际磁链矢量来追踪其准确磁链圆。传统的SPWM仅电源的角度出发生成一个可调频调压的正弦波电源,而SVPWM将逆变系统和异步电机看作一个整体来考虑,模型比较简单,也便于微处理器的实时控制。
普通的三相全桥是由六个开关器件构成的三个半桥。这六个开关器件组合起来(同一个桥臂的上下半桥的信号相反)共有8种安全的开关状态. 其中000、111(这里是表示三个半桥的开关状态)这两种开关状态在电机驱动中都不会产生有效的电流。因此称其为零矢量。另外6种开关状态分别是六个非零矢量。它们将360度的电压空间分为60度一个扇区,共六个扇区,利用这六个基本非零矢量和两个零量,可以合成360度内的任何矢量。
当要合成某一矢量时先将这一矢量分解到离它最近的两个基本矢量,而后用这两个基本矢量去表示,而每个基本矢量的作用大小就利用作用时间长短去代表。用电压矢量按照不同的时间比例去合成所需要的电压矢量。从而保证生成电压波形近似于正弦波。
在变频电机驱动时,矢量方向是连续变化的,因此我们需要不断的计算矢量作用时间。为了计算机处理的方便,在合成时一般是定时器计算(如每0.1ms计算一次)。这样我们只要算出在0.1ms内两个基本矢量作用的时间就可以了。由于计算出的两个时间的总和可能并不是0.1ms(比这小),而那剩下的时间就按情况插入合适零矢量。 由于在这样处理时,合成的驱动波形和PWM很类似。因此我们还叫它PWM,又因这种PWM是基于电压空间矢量去合成的,所以就叫它SVPWM了。
SVPWM的主要特点有:
1.在每个扇区虽有多次开关切换,但每次开关切换只涉及一个器件,所以开关损耗小。
2.利用电压空间矢量直接生成三相PWM波,计算简单。
3.逆变器输出线电压基波最大值为直流侧电压,比一般的SPWM逆变器输出电压高15%
27.1.4.1 SVPWM算法讲解


图27.1.4.1.2 U4(100)导通情况
此时等效电路如图27.1.4.1.3所示:

图27.1.4.1.3 U4(100)等效电路
根据串联分压定理,得:


剩下的三相开关组合情况同理,可得各矢量开关状态、线电压、相电压如下:
| 
 | 
 | 
 | 矢量符号 | 线电压 | 相电压 | ||||
| 
 | 
 | 
 | 
 | 
 | 
 | ||||
| 0 | 0 | 0 | 
 | 0 | 0 | 0 | 0 | 0 | 0 | 
| 1 | 0 | 0 | 
 | 
 | 0 | 
 | 
 | 
 | 
 | 
| 1 | 1 | 0 | 
 | 0 | 
 | 
 | 
 | 
 | 
 | 
| 0 | 1 | 0 | 
 | 
 | 
 | 0 | 
 | 
 | 
 | 
| 0 | 1 | 1 | 
 | 
 | 0 | 0 | 
 | 
 | 
 | 
| 0 | 0 | 1 | 
 | 0 | 
 | 
 | 
 | 
 | 
 | 
| 1 | 0 | 1 | 
 | 
 | 
 | 0 | 
 | 
 | 
 | 
| 1 | 1 | 1 | 
 | 0 | 0 | 0 | 0 | 0 | 0 | 
表27.1.4.1.1 各矢量线电压和相电压对比


图27.1.4.1.4 矢量扇区
在每一个扇区,用相邻俩电压矢量及零矢量按照伏秒平衡原则来合成每个扇区内的任意电压矢量。


图27.1.4.1.5 第一扇区

现在一个周期内所有状态的持续时间我们都得到了,还差一个顺序,也就是各个状态切换的顺序。
以减少开关次数为目标,基本矢量作用顺序原则为:在每次开关状态转换时只改变其中一个相的开关状态,并且平均分配零矢量,使PWM对称、降低PWM的谐波分量。常用方式有7段式和5段式SVPWM。我们这里主要讲解7段式SVPWM。
以第Ⅰ扇区为例,三相调制波形在时间段Ts时段中如下图所示。

图27.1.4.1.6 扇区1通电顺序


图27.1.4.1.7 扇区2通电顺序
然后按照Ⅱ扇区开关切换顺序合成角度递增的新的矢量,直至超过120°范围,进入下一个扇区,直至旋转360,然后依次循环。 
通过上述推导可知,要实现信号调制,首先要知道参考电压矢量所在扇区位置,然后利用扇区相邻两个非零矢量和零矢量来合成,然后以旋转矢量为目标,在每个扇区内通过不同的开关顺序合成矢量来跟随目标旋转矢量。
空间矢量调制第一步需要判断空间电压矢量所处的扇区,假设合成电压矢量在Ⅰ扇区,则0°<θ<60°,由

,可知θ=

,所以:

且满足:

可求知,当合成电压矢量在Ⅰ扇区时的充要条件是:

,

。其他扇区同理,可得:
| 扇区 | 落在扇区的充要条件 | 
| Ⅰ | 
 ,   | 
| Ⅱ | 
 ,且  | 
| Ⅲ | 
 ,   | 
| Ⅳ | 
 ,   | 
| Ⅴ | 
 ,且  | 
| Ⅵ | 
 ,   | 
表27.1.4.1.2 扇区判别条件
由上可以看出,电压矢量扇区由三个式子决定。

整理得出

进行化简,得:

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

图27.1.4.1.8 矢量边界
当俩零电压矢量作用时间为0时,一个PWM周期内非零电压作用时间最长,合成空间电压矢量幅值最大,其最大值不会超过正六边形边界,否则就会过调,输出电压波形就会失真,SVPWM调制模式下能够输出的最大不失真旋转电压矢量为正六边形的内切圆,赋值为

,相对SPWM最大为

,因此SVPWM方式电压利用率高出

15.47%。
到目前为止,为了使电机旋转起来,还需要知道电机转子的位置,根据位置来合成旋转磁场矢量,使转子跟着旋转,对于一般的直流无刷电机,可以依据霍尔传感器来反馈转子位置,如果通过编码器来获取转子位置则更加精确,不过只使用编码器还需要启转前的校准,也就是找到转子对齐定子时刻的编码器位置,一般是通一段时间的小电流,将转子吸过去,然后将变化之后的编码器位置设置为零点,不过每次断电重启时都需要对齐一次,如果同时具备霍尔传感器和编码器,就可以相辅相成。而如果霍尔传感器和编码器都没有就有点麻烦了,和编码器方式一样,先通电流利用旋转磁场把转子带起来,通过电流的采集估测转子位置,转子位置采用观测器来估算,常用的有滑膜环观测器法、隆伯格观测器法、高频注入法等。
不同电流模式下的控制策略主要有:最大转矩电流比控制(保持转矩需求的情况下调节直轴交轴电流比例使电机定子输入电流最小)、id=0控制(标贴式无刷电机常用策略)、弱磁控制(电机转速超过额定转速时通过减小直轴电流降低电枢电流从而减小反电动势以提供扭矩电流进一步加速)、cosφ=1控制(功率因数恒为1)等。
至此,FOC的理论基础已经全部介绍完毕。下面接着介绍FOC所需的硬件平台。
FOC硬件平台
27.2.1 驱动板硬件电路介绍
这部分内容跟无刷驱动板是完全一致的,所以这里不再赘述。简单介绍下驱动器板载功能,驱动器主要搭载了电流检测单元、过零检测单元、传感器反馈单元、反电动势检测单元、刹车单元等,可以有效的反馈驱动板电压、驱动板温度、三相电流及反电动势、过零信号、霍尔及编码器信号。大家如果想了解可以查看无刷专题驱动硬件介绍,更多关于ATK-PD6010B 驱动板原理介绍请查看《ATK-PD6010B直流无刷驱动板用户手册.pdf》。
- 实物接线
下面我们将手把手地教大家搭建FOC硬件平台,这一部分内容十分重要,大家一定要掌握。首先我们需要准备一些材料,如图27.2.1.1所示:

图27.2.1.1 硬件平台搭建所需材料
图27.2.1.1中,不同的序号对应的材料如下:
- 电机开发板;
- 直流无刷驱动板;
- 传感器接口8pin端子;
- 电机插口转接板(PMSM电机专用);
- PMSM或者BLDC电机;
- 专用24pin排线;
由于PMSM电机的传感器接口为DB15,所以为了方便连接至驱动板我们使用转接板进行转接(如果是BLDC电机直接按照无刷专题的接法即可)。除了之外,我们还需要准备一个符合电机工作电压的直流电源,例如我们教程中所使用的无刷电机的工作电压是24V,这里就准备一个24V的直流电源即可。
有了这些材料之后,我们接下来就进行实物的接线。这里以开发板的直流有刷/无刷电机驱动接口1为例,接线如图27.2.1.2所示:

图27.2.1.2 实物连接图
27.2.2 电机参数介绍
由于BLDC以及PMSM电机都是支持FOC控制的,所以下面我们列出了关于我们店铺这两款电机的一些技术参数,如下表:
| 电机类型 | BLDC | PMSM | 
| 磁级数 | 4 | 8 | 
| 相数 | 3 | 3 | 
| 额定电压(VDC) | 24 | 24 | 
| 额定转速 (RPM) | 3000 | 3000 | 
| 额定电流 (Amps) | 6.6 | 4 | 
| 反电势(V/kRPM) | 6.6 | 4.3 | 
| 线电阻(Ohm) | 0.4 | 1.02 | 
| 线电感(mH) | 0.8 | 0.59 | 
| 编码器线 | 无 | 1000 | 
| 霍尔安装角度 | 120° | 120° | 
| 输出功率 (W) | 105 | 64W | 
| 额定扭矩 (N-m) | 0.33 | 0.2 | 
表27.2.2.1 店铺的BLDC与PMSM电机参数
FOC开发工具
相信经过前面的学习,大家已经对FOC的理论基础部分有了一定的了解,但是想要实现FOC算法部分还是有一定的难度,所以我们可以借助ST提供的FOC开发套件——“X-CUBE-MCSDK”,来帮助我们生成FOC控制代码。
X-CUBE-MCSDK:ST推出的电机控制软件开发套件。其中包括永磁同步电机(PMSM)固件库(FOC控制)以及STM32电机控制工作台(用于配置FOC固件库参数)。如下:

图27.3.1 X-CUBE-MCSDK组件
其中FOC驱动库就是FOC的核心算法实现,MCWorkbench为一款PC软件,用于配置FOC固件库参数。
27.3.1 Motor Control Workbench简介 
Motor Control Workbench是一款PC软件,它可以大大减少STM32 PMSM FOC固件配置所需的设计工作量和时间。用户通过GUI生成项目文件,并根据应用程序的需要,初始化对应的库。而且可以对所使用算法的某些变量实时监控和更改。
27.3.2 Motor Control Workbench获取与安装
前面介绍了很多,接着我们开始来安装FOC开发所需的这些软件,首先安装“Motor Control Workbench”软件。它的软件安装包获取方法有两种:
1、在ST的官网:https://www.st.com中直接搜索关键词“MC SDK”,如下图27.3.2.1所示:
2、可在光盘直接找到软件并安装,路径:“A盘\6,软件资料\3,MCSDK”。

图27.3.2.1 搜索结果
可以看到上图共有两种版本: X-CUBE-MCSDK 和 X-CUBE-MCSDK-FUL,其中 X-CUBE-MCSDK 在一些核心的算法使用 lib 的形式提供给用户,可以直接下载;则是完整的源码,需要官方验证用户信息后才可下载。我们资料已提供完整版文件X-CUBE-MCSDK-FUL,可在路径“电机A盘\6,软件资料\3,MCSDK”中获取。安装过程很简单双击X-CUBE-MCSDK-FUL_5.4.4.exe 的安装包,之后只需要一直点击下一步即可。注意安装路径一定不能有中文!!! 
安装完成后会在桌面生成 MotorControlWorkbench 5.4.4 和 Motor Profiler 5.4.4 两个软件, 其中Motor Profiler 5.4.4 是用于自动测量电机参数的软件,不过该软件针对的是使用 ST 相关的主板和电机驱动板,这里就不介绍该软件的使用方法。 MotorControl Workbench 5.4.4 才是我们需要的软件,可以使用该软件配置电机驱动板等参数后并生成源代码。
完成了X-CUBE-MCSDK-FUL的安装,接着我们还需要安装STM32CubeMX,因为使用X-CUBE-MCSDK-FUL配置完之后,在生成代码时需要使用到STM32CubeMX。
27.3.3 STM32CubeMX的获取与安装
由于CubeMX是运行在JAVA环境下的,所以安装CubeMX之前需要先安装JAVA(JAVA8及以上版本)。对于Java运行环境,大家可以到Java官网www.java.com下载最新的Java软件,也可以直接从我们光盘复制安装包,路径是“A盘\6,软件资料\4,JAVA”,双击“chromeinstall-8u77.exe”即可进行安装。

图27.3.3.1 java安装包
这里大家需要注意,STM32CubeMX的Java运行环境版本必须是V1.7及以上,如果你的电脑安装过V1.7以下版本,请先删掉后重新安装最新版本。
对于Java运行环境安装,我们这里就不做过多讲解,大家直接双击安装包,根据提示安装即可。安装完成之后提示界面如下图27.3.3.2:

图27.3.3.2安装成功提示界面
安装完Java运行环境之后,为了检测是否正常安装,我们可以打开Windows的命令输入框,输入:java –version 命令,如果显示Java版本信息,则安装成功。提示信息如图27.3.3.3所示。

图27.3.3.3查看Java版本
JAVA环境已经安装完成以后就可以安装CubeMX,可以选择在线下载或者使用光盘软件资料的安装包直接安装,官网下载地址: https://www.st.com/en/development-tools/stm32cubemx.html?sc=stm32cubemx,资料盘路径:“A盘\6,软件资料\2,CubeMX”,双击“SetupSTM32CubeMX-5.5.0.exe”即可进行安装。

图27.3.3.4 CubeMX安装包
至此已经完成CubeMX软件的安装。注意:请确保上述的三个软件安装路径均没有中文以及特殊字符!!
27.4 FOC例程创建
打开ST Motor Control Workbench软件,然后点击新建一个电机控制工程,如下。

图27.4.1 新建工程
选择应用类型,单/双电机控制以及硬件类型等,如下。

图27.4.2 选择应用类型
针对不同负载类型的FOC可选应用类型,可选通用、泵、压缩机、空调、洗碗机、风扇、无人机,作为示例,我们直接选择通用类型即可。主设计界面如下。

图27.4.3 主界面
然后根据无刷驱动板输入电源范围设置母线电压,如下

图27.4.4 设置母线电压
然后修改控制器型号为板载的STM32F407IG,如下。

图27.4.5 修改MCU
接着点击电机参数配置,不同电机参数不同,需根据电机手册的参数填写,我们店铺的PMSM/BLDC电机技术参数可查看27.2.2小节。

图27.4.6 电机参数设置
传感器设置如下,这里使用的是霍尔传感器,大家可根据自己的电机硬件(传感器类型)选择编码器或者霍尔传感器,勾选对应选项即可。如需使用无感控制,则都不勾选。注意本店铺的PMSM电机内置霍尔传感器以及编码器;BLDC内置霍尔传感器。

图27.4.7 传感器设置
以霍尔反馈方式为例,速度反馈使用霍尔传感器采集,则速度传感器设置如下。

图27.4.8 速度反馈设置
MOS管设置,高电平有效,三相驱动配置完全一致,如下。

图27.4.9 MOS管设置
MOS管最大开关频率和死区时间设置如下。

图27.4.10 MOS管频率设置
母线电压检测设置,根据无刷电机驱动板原理图设置如下。

图27.4.11 电压检测设置
温度传感器及保护设置如下,由于此设置只针对ST的温度传感器,所以直接默认设置即可。

图27.4.12 温度保护设置
电流传感器设置,需根据驱动板的电流采集硬件电路设置对应参数,我们驱动板使用的是0.02Ω的采样电阻,差分电路的放大倍数为6倍,所以设置如下。

图27.4.13 电流传感器设置
电流采集运放可按照驱动板参数设置,Calculate选框主要协助计算放大倍数,鉴于我们已知放大倍数,所以我们直接填写倍数值即可,Calculate选框可以不设置,默认参数如下。

图27.4.14 运放设置
右击Fireware Driver Management选择驱动设置,设置驱动参数设置如下。

图27.4.15 驱动设置
控制模式选择速度模式作为演示,PWM频率选择16KHz,左下侧速度环PID参数是我们测试过的数据,运转情况良好,为了直接生成即可运行我们就直接输入调试测试过的PID参数。接着设置电压保护如下。

图27.4.16 电压保护设置
用户接口设置如下。

图27.4.17 用户接口设置
数字输入输出设置根据开发板连接关系进行选择,以电机接口1为例,PWM使用定时器1,霍尔传感器使用定时器5,调试串口使用串口1,使用按键KEY2用于启停开发板,相应管脚设置如下。

图27.4.18 数字输入输出接口设置
电流采集模拟接口设置如下。

图27.4.19 电流采集设置
电源电压采集模拟接口设置如下。

图27.4.20 母线电压采集设置
温度采集模拟接口设置如下。

图27.4.21 温度采集设置
DAC设置用于调试时候观测FOC数据,例如将电机的电角度、交轴电流通过DAC方式输出,这样就可以使用示波器实时观测数据的变化,一般无需使用时直接选择不用即可,如下。

图27.4.22 DAC设置
配置完之后,就要检测IO分配是否有误,点击选项

,IO分配无误后显示check ok。如下图:

图27.4.23 引脚检测
最后就可以保存工程,注意路径不能含有中文。

图27.4.24 保存工程
点击

按钮,生成CubeMX工程,第一次点击生成,选择编译器平台,后续在该工程下进行的修改只需要更新即可。

图27.4.25 生成过程
开始生成工程,如下。

图27.4.26 工程生成中


图27.4.27 工程生成完成
生成结束后可以打开CubeMX添加自己的设置,由于无刷驱动板默认不输出,因此需要通过CubeMX打开SHUTDOWN引脚,并且配置为输出功能,点击RUN STM32Cube…打开CubeMX,设置关断引脚,如下。

图27.4.28 使能关断引脚功能
直接将SHUTDOWN引脚默认状态设置成拉高,即使能SHUTDOWN引脚,半桥芯片可正常输出,然后点击保存即可。

图27.4.29 保存CubeMX工程
点击CLOSE关闭生成完成窗口,重新回到MCWorkbench,点击

按钮,生成工程,由于工程文件已经存在,为了避免覆盖用户配置,直接点击UPDATE更新即可。

图27.4.30 更新工程
更新完成后关闭窗口,打开前面保存项目路径的文件夹找到MDK工程并打开,可直接编译并将程序下载进开发板。
27.5 FOC下载验证
编译上述创建好的FOC控制工程并下载程序到开发板,连接开发板和驱动板及电机。由于永磁同步电机传感器接口为DB15接口,可以使用转接模块连接,比较方便,实物连接如下。

图27.5.1 FOC验证实物连接图
将开发板串口1用USB线连接至电脑。

图27.5.2 串口连接至电脑USB
打开上位机的监控功能,如下。

图27.5.3 打开监控界面
选择正确的串口号并点击插头按钮连接到开发板,如下:

图27.5.4 连接开发板
此时显示已连接状态,并且显示MCSDK版本,由于未连接驱动板电压,这时左侧状态栏可能会报低压警告,给驱动板通电并且点击右侧的Fault Ask按钮即可消除警告。

图27.5.5 串口连接成功
点击右侧的Start Motor按钮即可启动电机,电机开始旋转,拖动界面中间的圆形旋钮即可调节转速,同时可以看到测量到的母线电压、温度、实际转速等信息。通过板子上的KEY2按键也可以控制电机的启动和停止。

图27.5.6 FOC运转

图27.5.7 电机运行情况
点击

按钮可以打开折线图窗口,用于观测速度变化,如下。

图27.5.8 速度监控折线图
如果需要进一步调节PID,可以点击Advanced按钮切换到参数调整,如下。

图27.5.9 参数调整界面
修改好PID之后可以点击

按钮把参数上传到开发板,观测运行效果。如果使能了DAC输出,还可以选择DAC设置,默认是输出a和b电流。
27.6 ST MCSDK库源码介绍
以5.4版本的MCSDK库为例,源码目录如下。

图27.6.1 电机库总目录

图27.6.2 SDK源码目录
其中MCLib文件夹存放的是核心程序,其目录下有两个目录,如下。

图27.6.3 MCLib目录
其中Any文件夹存放的是公共核心程序,F4xx文件夹存放的是F4系列的单片机和硬件相关的驱动源文件,如果是以F1单片机则会生成F1xx的文件夹,Any文件夹中包含Src和Inc文件夹,分别存放核心算法的源文件和头文件,源文件内容如下。

图27.6.4 MCSDK核心程序源文件
头文件如下。

图27.6.5 MCSDK核心程序头文件
F4xx目录同样分为源文件和头文件目录,内容如下。

图27.6.6 MCSDK核心程序F4相关源文件
SystemDriveParams文件夹用于存放驱动参数,没有相应改动的话一般为空。UILibrary文件夹同样分为源文件、头文件目录,源文件如下。源文件主要功能是方便上位机或者用户和SDK通信,早起版本是支持屏幕交互的,也可能是此原因保留了文件夹命名为UILibrary。

图27.6.7 MCSDK通信相关源文件
学习SDK最好的方式便是阅读帮助文档,通过ST MC Workbench打开帮助文档的方法如下。

图27.6.8 MC Workbench帮助文档

图27.6.9 SDK文档
文档详细描述了SDK的文件构成、API介绍等内容,对于学习SDK有帮助。
27.7 FOC例程分析
以foc_pmsm_m1_encoder_os为例,即电机接口1使用编码器反馈速度、位置在FreeRTOS下实现FOC。
FOC实现在系统下一共创建了两个任务,startMediumFrequencyTask和StartSafetyTask。
/* Create the thread(s) */
/* definition and creation of mediumFrequency */
osThreadDef(mediumFrequency, startMediumFrequencyTask, osPriorityNormal, 0, 
128);
mediumFrequencyHandle = osThreadCreate(osThread(mediumFrequency), NULL);
/* definition and creation of safety */
osThreadDef(safety, StartSafetyTask, osPriorityAboveNormal, 0, 128);
safetyHandle = osThreadCreate(osThread(safety), NULL);
可能第一步跳转的是main函数的函数实现,这也是由于编译器的查找优先顺序决定的,不过main函数中的函数实现是弱定义的,真正的实现在mc_task.c中。
/* startMediumFrequencyTask function */
void startMediumFrequencyTask(void const * argument)
{
/* USER CODE BEGIN MF task 1 */
/* Infinite loop */
for(;;)
{
/* delay of 500us */
vTaskDelay(1);
MC_RunMotorControlTasks();
}
/* USER CODE END MF task 1 */
}
中频任务startMediumFrequencyTask的内容为系统演示调用MC_RunMotorControlTasks函数。而MC_RunMotorControlTasks函数负责循环调度一些任务,主要是电机控制调度和用户接口调度。
__weak void MC_RunMotorControlTasks(void)
{
if ( bMCBootCompleted ) {
/* ** Medium Frequency Tasks ** */
MC_Scheduler();
/* ** User Interface Task ** */
UI_Scheduler();
}
}
其中MC_Scheduler主要由一个变量计数器来实现真正的任务调度,真正负责电机控制调度的实现是TSK_MediumFrequencyTaskM1函数。
__weak void TSK_MediumFrequencyTaskM1(void)
{
/* USER CODE BEGIN MediumFrequencyTask M1 0 */
/* USER CODE END MediumFrequencyTask M1 0 */
State_t StateM1;
int16_t wAux = 0;
(void) ENC_CalcAvrgMecSpeedUnit( &ENCODER_M1, &wAux );
PQD_CalcElMotorPower( pMPM[M1] );
StateM1 = STM_GetState( &STM[M1] );
switch ( StateM1 )
{
case IDLE:
if ( EAC_GetRestartState( &EncAlignCtrlM1 ) )
{
/* The Encoder Restart State is true: the IDLE state has been entered
* after Encoder alignment was performed. The motor can now be started. */
EAC_SetRestartState( &EncAlignCtrlM1,false );
/* USER CODE BEGIN MediumFrequencyTask M1 Encoder Restart */
/* USER CODE END MediumFrequencyTask M1 Encoder Restart */
STM_NextState( &STM[M1], IDLE_START );
}
break;
case IDLE_START:
…… ……
内容过多这里只截取了部分函数。中频任务函数主要由状态机实现,通过不同的状态来切换控制内容,例如硬件刚刚初始化的时候就是ICLWAIT状态,硬件初始化OK可以运行的时候就是IDLE状态,等等。各状态的定义和注释如下。
typedef enum
{
ICLWAIT = 12, /*!< Persistent state, the system is waiting for ICL
deactivation. Is not possible to run the motor if
ICL is active. Until the ICL is active the state is
forced to ICLWAIT, when ICL become inactive the state
is moved to IDLE */
IDLE = 0, /*!< Persistent state, following state can be IDLE_START
if a start motor command has been given or
IDLE_ALIGNMENT if a start alignment command has been
given */
IDLE_ALIGNMENT = 1, /*!< "Pass-through" state containg the code to be 
executed
only once after encoder alignment command.
Next states can be ALIGN_CHARGE_BOOT_CAP or
ALIGN_OFFSET_CALIB according the configuration. It
can also be ANY_STOP if a stop motor command has been
given. */
ALIGN_CHARGE_BOOT_CAP = 13,/*!< Persistent state where the gate driver boot
capacitors will be charged. Next states will be
ALIGN_OFFSET_CALIB. It can also be ANY_STOP if a stop
motor command has been given. */
ALIGN_OFFSET_CALIB = 14,/*!< Persistent state where the offset of motor 
currents
measurements will be calibrated. Next state will be
ALIGN_CLEAR. It can also be ANY_STOP if a stop motor
command has been given. */
ALIGN_CLEAR = 15, /*!< "Pass-through" state in which object is cleared 
and
set for the startup.
Next state will be ALIGNMENT. It can also be ANY_STOP
if a stop motor command has been given. */
… …
} State_t;
状态切换由STM_NextState函数实现。StartSafetyTask函数主要做电机控制过程的保护作用。
硬件初始化程序在main函数中,如下。
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ADC1_Init();
MX_ADC2_Init();
MX_DAC_Init();
MX_TIM1_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
MX_MotorControl_Init();
/* Initialize interrupts */
MX_NVIC_Init();
以电机控制使用的定时器1为例,初始化硬件程序如下。
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* htim)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(htim->Instance==TIM1)
{
/* USER CODE BEGIN TIM1_MspPostInit 0 */
/* USER CODE END TIM1_MspPostInit 0 */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/**TIM1 GPIO Configuration
PA10 ------> TIM1_CH3
PA9 ------> TIM1_CH2
PA8 ------> TIM1_CH1
PB13 ------> TIM1_CH1N
PB14 ------> TIM1_CH2N
PB15 ------> TIM1_CH3N
*/
GPIO_InitStruct.Pin = M1_PWM_WH_Pin|M1_PWM_VH_Pin|M1_PWM_UH_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = M1_PWM_UL_Pin|M1_PWM_VL_Pin|M1_PWM_WL_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* USER CODE BEGIN TIM1_MspPostInit 1 */
/* USER CODE END TIM1_MspPostInit 1 */
}
}
管脚定义在main.h头文件中,通过宏的方式可以较为方便的改动硬件。
#define Start_Stop_Pin GPIO_PIN_4
#define Start_Stop_GPIO_Port GPIOE
#define Start_Stop_EXTI_IRQn EXTI4_IRQn
#define UART_RX_Pin GPIO_PIN_7
#define UART_RX_GPIO_Port GPIOB
#define UART_TX_Pin GPIO_PIN_6
#define UART_TX_GPIO_Port GPIOB
#define M1_PWM_WH_Pin GPIO_PIN_10
#define M1_PWM_WH_GPIO_Port GPIOA
#define M1_PWM_VH_Pin GPIO_PIN_9
#define M1_PWM_VH_GPIO_Port GPIOA
#define M1_PWM_UH_Pin GPIO_PIN_8
#define M1_PWM_UH_GPIO_Port GPIOA
#define M1_ENCODER_B_Pin GPIO_PIN_7
#define M1_ENCODER_B_GPIO_Port GPIOC
#define SHUTDOWN2_Pin GPIO_PIN_2
#define SHUTDOWN2_GPIO_Port GPIOF
#define M1_ENCODER_A_Pin GPIO_PIN_6
#define M1_ENCODER_A_GPIO_Port GPIOC
#define SHUTDOWN_Pin GPIO_PIN_10
#define SHUTDOWN_GPIO_Port GPIOF
#define M1_TEMPERATURE_Pin GPIO_PIN_0
#define M1_TEMPERATURE_GPIO_Port GPIOA
#define DBG_DAC_CH1_Pin GPIO_PIN_4
#define DBG_DAC_CH1_GPIO_Port GPIOA
#define M1_CURR_AMPL_V_Pin GPIO_PIN_6
#define M1_CURR_AMPL_V_GPIO_Port GPIOA
#define DBG_DAC_CH2_Pin GPIO_PIN_5
#define DBG_DAC_CH2_GPIO_Port GPIOA
#define M1_OCP_Pin GPIO_PIN_12
#define M1_OCP_GPIO_Port GPIOB
#define M1_PWM_UL_Pin GPIO_PIN_13
#define M1_PWM_UL_GPIO_Port GPIOB
#define M1_CURR_AMPL_W_Pin GPIO_PIN_3
#define M1_CURR_AMPL_W_GPIO_Port GPIOA
#define M1_BUS_VOLTAGE_Pin GPIO_PIN_1
#define M1_BUS_VOLTAGE_GPIO_Port GPIOB
#define M1_CURR_AMPL_U_Pin GPIO_PIN_0
#define M1_CURR_AMPL_U_GPIO_Port GPIOB
#define M1_PWM_VL_Pin GPIO_PIN_14
#define M1_PWM_VL_GPIO_Port GPIOB
#define M1_PWM_WL_Pin GPIO_PIN_15
#define M1_PWM_WL_GPIO_Port GPIOB
为了便于平台生成程序,电机控制所相关的中断全部通过stm32f4xx_mc_it.c实现,函数声明如下。
/* Public prototypes of IRQ handlers called from assembly code ----------*/
void ADC_IRQHandler(void);
void TIMx_UP_M1_IRQHandler(void);
void TIMx_BRK_M1_IRQHandler(void);
void SPD_TIM_M1_IRQHandler(void);
void USART_IRQHandler(void);
void HardFault_Handler(void);
void SysTick_Handler(void);
void EXTI4_IRQHandler (void);
以前版本的FOC程序支持显示屏,之后和现在的版本均不支持显示屏控制了,不过还支持按键控制启停的功能。以开发板的按键2(PE4)作为启停开关,启停通过外部中断的方式实现,程序如下。
/**
* @brief This function handles Button IRQ on PIN PE4.
*/
void EXTI4_IRQHandler (void)
{
/* USER CODE BEGIN START_STOP_BTN */
if ( LL_EXTI_ReadFlag_0_31(LL_EXTI_LINE_4) )
{
LL_EXTI_ClearFlag_0_31 (LL_EXTI_LINE_4);
UI_HandleStartStopButton_cb ();
}
}
中断程序首先清除中断标志位,然后调用函数UI_HandleStartStopButton_cb,即用户接口启停按钮回调,程序通过判断电机状态决定是启动电机还是停止电机,函数实现如下。
__weak void UI_HandleStartStopButton_cb (void)
{
/* USER CODE BEGIN START_STOP_BTN */
if (MC_GetSTMStateMotor1() == IDLE)
{
/* Ramp parameters should be tuned for the actual motor */
MC_StartMotor1();
}
else
{
MC_StopMotor1();
}
同UI_HandleStartStopButton_cb一样,很多函数很多情况下都是弱定义的也就是可以由用户覆写以实现更灵活的功能。
串口中断函数主要用于与上位机的通信,在接收到数据的时候通过UFCP_RX_IRQ_Handler函数来将有效数据放置到缓存中。接收到完整的数据帧后通过回调函数MCP_ReceivedFrame来解析数据帧并实现交互。接收数据帧解析赋值函数通过MCP_Init函数来初始化,如下。
__weak void MCP_Init( MCP_Handle_t *pHandle,
FCP_Handle_t * pFCP,
FCP_SendFct_t fFcpSend,
FCP_ReceiveFct_t fFcpReceive,
FCP_AbortReceiveFct_t fFcpAbortReceive,
DAC_UI_Handle_t * pDAC,
const char* s_fwVer )
{
pHandle->pFCP = pFCP;
pHandle->pDAC = pDAC;
pHandle->s_fwVer = s_fwVer;
FCP_SetClient( pFCP, pHandle,
(FCP_SentFrameCallback_t) & MCP_SentFrame,
(FCP_ReceivedFrameCallback_t) & MCP_ReceivedFrame,
(FCP_RxTimeoutCallback_t) & MCP_OnTimeOut );
pHandle->fFcpSend = fFcpSend;
pHandle->fFcpReceive = fFcpReceive;
pHandle->fFcpAbortReceive = fFcpAbortReceive;
MCP_WaitNextFrame(pHandle);
}
由于例程使用了编码器用于反馈速度和位置,所以有SPD_TIM_M1_IRQHandler函数,也就是TIM3_IRQHandler,用于更新编码器计数溢出次数。
/********** AUXILIARY ENCODER TIMER MOTOR 1 *************/
#define M1_PULSE_NBR ( (4 * (M1_ENCODER_PPR)) - 1 )
#define M1_ENC_IC_FILTER 9
#define SPD_TIM_M1_IRQHandler TIM3_IRQHandler
TIMx_BRK_M1_IRQHandler中断函数用于刹车,也就是TIM1_BRK_TIM9_IRQHandler,刹车信号有效时调用R3_2_BRK_IRQHandler函数,关断输出信号并设置过流标志。
/**
* @brief It contains the Break event interrupt
* @param pHandle: handler of the current instance of the PWM component
* @retval none
*/
__weak void *R3_2_BRK_IRQHandler(PWMC_R3_2_Handle_t *pHandle)
{
if ((pHandle->pParams_str->LowSideOutputs)== ES_GPIO)
{
LL_GPIO_ResetOutputPin(pHandle->pParams_str->pwm_en_u_port, 
pHandle->pParams_str->pwm_en_u_pin);
LL_GPIO_ResetOutputPin(pHandle->pParams_str->pwm_en_v_port, 
pHandle->pParams_str->pwm_en_v_pin);
LL_GPIO_ResetOutputPin(pHandle->pParams_str->pwm_en_w_port, 
pHandle->pParams_str->pwm_en_w_pin);
}
pHandle->OverCurrentFlag = true;
return &(pHandle->_Super.Motor);
}
TIMx_UP_M1_IRQHandler函数用于在定时器更新中断时使用ADC注入通道获取采样电流值用于FOC计算的电流反馈。也就是TIM1_UP_TIM10_IRQHandler。
/************************* IRQ Handler Mapping *********************/
#define TIMx_UP_M1_IRQHandler TIM1_UP_TIM10_IRQHandler
#define DMAx_R1_M1_IRQHandler DMA2_Stream4_IRQHandler
#define DMAx_R1_M1_Stream DMA2_Stream4
#define TIMx_BRK_M1_IRQHandler TIM1_BRK_TIM9_IRQHandler
TIMx_UP_M1_IRQHandler函数首先清除更新中断,然后运行R3_2_TIMx_UP_IRQHandler回调函数,函数实现如下。
/**
* @brief It contains the TIMx Update event interrupt
* @param pHandle: handler of the current instance of the PWM component
* @retval none
*/
__weak void * R3_2_TIMx_UP_IRQHandler( PWMC_R3_2_Handle_t * pHandle)
{
TIM_TypeDef* TIMx = pHandle->pParams_str->TIMx;
ADC_TypeDef * ADCx_1 = pHandle->pParams_str->ADCx_1;
ADC_TypeDef * ADCx_2 = pHandle->pParams_str->ADCx_2;
uint32_t ADCInjFlags;
/* dual drive check */
ADCInjFlags = ADCx_1->SR & (LL_ADC_FLAG_JSTRT|LL_ADC_FLAG_JEOS);
if ( ADCInjFlags == LL_ADC_FLAG_JSTRT )
{
/* ADC conversion is on going on the second motor */
do
{
/* wait for end of conversion */
ADCInjFlags = ADCx_1->SR & (LL_ADC_FLAG_JSTRT|LL_ADC_FLAG_JEOS);
} 
while ( ADCInjFlags != (LL_ADC_FLAG_JSTRT|LL_ADC_FLAG_JEOS) );
}
else if ( ADCInjFlags == 0 )
{
/* ADC conversion on the second motor is not yet started */
while ( ( TIMx->CNT ) < ( pHandle->pParams_str->Tw ) )
{
/* wait for a maximum delay */
}
ADCInjFlags = ADCx_1->SR & (LL_ADC_FLAG_JSTRT|LL_ADC_FLAG_JEOS);
if ( ADCInjFlags == LL_ADC_FLAG_JSTRT )
{
/* ADC conversion is on going on the second motor */
do
{
/* wait for end of conversion */
ADCInjFlags = ADCx_1->SR & (LL_ADC_FLAG_JSTRT|LL_ADC_FLAG_JEOS);
}
while ( ADCInjFlags != (LL_ADC_FLAG_JSTRT|LL_ADC_FLAG_JEOS) );
}
}
else
{
/* ADC conversion on the second motor is done */
}
/* Disabling trigger to avoid unwanted conversion */
LL_ADC_INJ_StopConversionExtTrig(ADCx_1);
LL_ADC_INJ_StopConversionExtTrig(ADCx_2);
/* Set next current channel according to sector */
ADCx_1->JSQR = pHandle->pParams_str->ADCConfig1[pHandle->_Super.Sector];
ADCx_2->JSQR = pHandle->pParams_str->ADCConfig2[pHandle->_Super.Sector];
LL_ADC_INJ_SetTriggerSource(ADCx_1,pHandle->ADC_ExternalTriggerInjected);
LL_ADC_INJ_SetTriggerSource(ADCx_2,pHandle->ADC_ExternalTriggerInjected);
/* enable ADC trigger source */
LL_TIM_CC_EnableChannel(TIMx, LL_TIM_CHANNEL_CH4);
LL_ADC_INJ_StartConversionExtTrig(ADCx_1, pHandle->ADCTriggerEdge);
LL_ADC_INJ_StartConversionExtTrig(ADCx_2, pHandle->ADCTriggerEdge);
/* reset default edge detection trigger */
pHandle->ADCTriggerEdge = LL_ADC_INJ_TRIG_EXT_RISING;
return &( pHandle->_Super.Motor );
}
ADC_IRQHandler函数用于输出调试变量,通过UI_DACUpdate实现,通过判断TSK_HighFrequencyTask函数返回值判断执行FOC循环的电机id来决定输出的模拟值以适配不同电机的不同变量。因此中断时也执行一次TSK_HighFrequencyTask,即FOC高频任务,也就是FOC循环的主体。函数实现如下。
__weak uint8_t TSK_HighFrequencyTask(void)
{
/* USER CODE BEGIN HighFrequencyTask 0 */
/* USER CODE END HighFrequencyTask 0 */
uint8_t bMotorNbr = 0;
uint16_t hFOCreturn;
ENC_CalcAngle(&ENCODER_M1); /* if not sensorless then 2nd parameter is MC_NULL*/
/* USER CODE BEGIN HighFrequencyTask SINGLEDRIVE_1 */
/* USER CODE END HighFrequencyTask SINGLEDRIVE_1 */
hFOCreturn = FOC_CurrControllerM1();
/* USER CODE BEGIN HighFrequencyTask SINGLEDRIVE_2 */
/* USER CODE END HighFrequencyTask SINGLEDRIVE_2 */
if(hFOCreturn == MC_FOC_DURATION)
{
STM_FaultProcessing(&STM[M1], MC_FOC_DURATION, 0);
}
else
{
/* USER CODE BEGIN HighFrequencyTask SINGLEDRIVE_3 */
/* USER CODE END HighFrequencyTask SINGLEDRIVE_3 */
}
/* USER CODE BEGIN HighFrequencyTask 1 */
/* USER CODE END HighFrequencyTask 1 */
return bMotorNbr;
}
FOC循环的过程就是和之前介绍的流程一致,通过ENC_CalcAngle获取转子电角度,通过PWMC_GetPhaseCurrents获取a相以及b相电流,然后进行Clarke变换,实现函数:MCM_Clarke;接着进行Park变换,实现函数:MCM_Park;接着就是对d轴以及q轴进行PI运算,运算后的结果在进行反Park变换,实现函数:MCM_Rev_Park;接着就是进行SVPWM的运算,实现函数:PWMC_SetPhaseVoltage,整个过程都在FOC_CurrControllerM1函数中实现,它的功能主要用于控制电流环,函数实现如下: 
inline uint16_t FOC_CurrControllerM1(void)
{
qd_t Iqd, Vqd;
ab_t Iab;
alphabeta_t Ialphabeta, Valphabeta;
int16_t hElAngle;
uint16_t hCodeError;
SpeednPosFdbk_Handle_t *speedHandle;
speedHandle = STC_GetSpeedSensor(pSTC[M1]);
hElAngle = SPD_GetElAngle(speedHandle);
PWMC_GetPhaseCurrents(pwmcHandle[M1], &Iab);
Ialphabeta = MCM_Clarke(Iab);
Iqd = MCM_Park(Ialphabeta, hElAngle);
Vqd.q = PI_Controller(pPIDIq[M1],
(int32_t)(FOCVars[M1].Iqdref.q) - Iqd.q);
Vqd.d = PI_Controller(pPIDId[M1],
(int32_t)(FOCVars[M1].Iqdref.d) - Iqd.d);
Vqd = Circle_Limitation(pCLM[M1], Vqd);
hElAngle += SPD_GetInstElSpeedDpp(speedHandle)*REV_PARK_ANGLE_COMPENSATION_FACTOR;
Valphabeta = MCM_Rev_Park(Vqd, hElAngle);
hCodeError = PWMC_SetPhaseVoltage(pwmcHandle[M1], Valphabeta);
FOCVars[M1].Vqd = Vqd;
FOCVars[M1].Iab = Iab;
FOCVars[M1].Ialphabeta = Ialphabeta;
FOCVars[M1].Iqd = Iqd;
FOCVars[M1].Valphabeta = Valphabeta;
FOCVars[M1].hElAngle = hElAngle;
return(hCodeError);
}
函数PWMC_SetPhaseVoltage计算过后的结果,通过函数pFctSetADCSampPointSectX设置定时器各通道的比较值大小,进而控制电机。然后又通过R3_2_WriteTIMRegisters函数通过检查TIMx CH4是否启用。如果标志置位则发生更新事件因此FOC速率太高,可能过流。如果过流则执行过流处理,即STM_FaultProcessing函数。
__weak uint16_t PWMC_SetPhaseVoltage( PWMC_Handle_t * pHandle, alphabeta_t Valfa_beta )
{
int32_t wX, wY, wZ, wUAlpha, wUBeta, wTimePhA, wTimePhB, wTimePhC;
wUAlpha = Valfa_beta.alpha * ( int32_t )pHandle->hT_Sqrt3;
wUBeta = -( Valfa_beta.beta * ( int32_t )( pHandle->PWMperiod ) ) * 2;
wX = wUBeta;
wY = ( wUBeta + wUAlpha ) / 2;
wZ = ( wUBeta - wUAlpha ) / 2;
/* Sector calculation from wX, wY, wZ */
if ( wY < 0 )
{
if ( wZ < 0 )
{
…… ……
if ( pHandle->Ic > 0 )
{
pHandle->CntPhC += pHandle->DTCompCnt;
}
else
{
pHandle->CntPhC -= pHandle->DTCompCnt;
}
}
return ( pHandle->pFctSetADCSampPointSectX( pHandle ) );
}
其中pFctSetADCSampPointSectX赋值如下:
PWMC_R3_2_Handle_t PWM_Handle_M1=
{
{
.pFctGetPhaseCurrents = &R3_2_GetPhaseCurrents,
.pFctSwitchOffPwm = &R3_2_SwitchOffPWM,
.pFctSwitchOnPwm = &R3_2_SwitchOnPWM,
.pFctCurrReadingCalib = &R3_2_CurrentReadingCalibration,
.pFctTurnOnLowSides = &R3_2_TurnOnLowSides,
.pFctSetADCSampPointSectX = &R3_2_SetADCSampPointSectX,
.pFctIsOverCurrentOccurred = &R3_2_IsOverCurrentOccurred,
.pFctOCPSetReferenceVoltage = MC_NULL,
.pFctRLDetectionModeEnable = &R3_2_RLDetectionModeEnable,
.pFctRLDetectionModeDisable = &R3_2_RLDetectionModeDisable,
.pFctRLDetectionModeSetDuty = &R3_2_RLDetectionModeSetDuty,
.hT_Sqrt3 = (PWM_PERIOD_CYCLES*SQRT3FACTOR)/16384u,
…… ……
};
R3_2_SetADCSampPointSectX函数定义如下:
__weak uint16_t R3_2_SetADCSampPointSectX( PWMC_Handle_t * pHdl )
{
PWMC_R3_2_Handle_t * pHandle = ( PWMC_R3_2_Handle_t * )pHdl;
uint16_t hCntSmp;
uint16_t hDeltaDuty;
register uint16_t lowDuty = pHdl->lowDuty;
register uint16_t midDuty = pHdl->midDuty;
/* Check if sampling AB in the middle of PWM is possible */
if ( ( uint16_t )( pHandle->Half_PWMPeriod - lowDuty ) > pHandle->pParams_str->hTafter )
{
pHandle->_Super.Sector = SECTOR_4;
/* set sampling point trigger in the middle of PWM period */
hCntSmp = ( uint32_t )( pHandle->Half_PWMPeriod ) - 1u;
}
else
{
… …
}
return R3_2_WriteTIMRegisters( &pHandle->_Super, hCntSmp );
}
接着看下返回值R3_2_WriteTIMRegisters的函数定义:
__STATIC_INLINE uint16_t R3_2_WriteTIMRegisters( PWMC_Handle_t * pHdl, uint16_t hCCR4Reg )
{
PWMC_R3_2_Handle_t * pHandle = (PWMC_R3_2_Handle_t *) pHdl;
TIM_TypeDef * TIMx = pHandle->pParams_str->TIMx;
uint16_t hAux;
LL_TIM_OC_SetCompareCH1( TIMx, pHandle->_Super.CntPhA );
LL_TIM_OC_SetCompareCH2( TIMx, pHandle->_Super.CntPhB );
LL_TIM_OC_SetCompareCH3( TIMx, pHandle->_Super.CntPhC );
LL_TIM_OC_SetCompareCH4( TIMx, hCCR4Reg );
/* Limit for update event */
/* Check the if TIMx CH4 is enabled. If it is set, an update event has occurred
and thus the FOC rate is too high */
if (LL_TIM_CC_IsEnabledChannel(TIMx, LL_TIM_CHANNEL_CH4))
{
hAux = MC_FOC_DURATION;
}
else
{
hAux = MC_NO_ERROR;
}
if ( pHandle->_Super.SWerror == 1u )
{
hAux = MC_FOC_DURATION;
pHandle->_Super.SWerror = 0u;
}
return hAux;
}
MC WorkBench上位机目前仅支持速度控制和扭矩控制模式,主要是通过调节电流的方式实现,主要逻辑就是在FOC_CurrControllerM1函数中实现。
inline uint16_t FOC_CurrControllerM1(void)
{
qd_t Iqd, Vqd;
ab_t Iab;
alphabeta_t Ialphabeta, Valphabeta;
int16_t hElAngle;
uint16_t hCodeError;
SpeednPosFdbk_Handle_t *speedHandle;
speedHandle = STC_GetSpeedSensor(pSTC[M1]);
hElAngle = SPD_GetElAngle(speedHandle);
PWMC_GetPhaseCurrents(pwmcHandle[M1], &Iab);
Ialphabeta = MCM_Clarke(Iab);
Iqd = MCM_Park(Ialphabeta, hElAngle);
Vqd.q = PI_Controller(pPIDIq[M1],
(int32_t)(FOCVars[M1].Iqdref.q) - Iqd.q);
Vqd.d = PI_Controller(pPIDId[M1],
(int32_t)(FOCVars[M1].Iqdref.d) - Iqd.d);
Vqd = Circle_Limitation(pCLM[M1], Vqd);
hElAngle += SPD_GetInstElSpeedDpp(speedHandle)*REV_PARK_ANGLE_COMPENSATION_FACTOR;
Valphabeta = MCM_Rev_Park(Vqd, hElAngle);
hCodeError = PWMC_SetPhaseVoltage(pwmcHandle[M1], Valphabeta);
FOCVars[M1].Vqd = Vqd;
FOCVars[M1].Iab = Iab;
FOCVars[M1].Ialphabeta = Ialphabeta;
FOCVars[M1].Iqd = Iqd;
FOCVars[M1].Valphabeta = Valphabeta;
FOCVars[M1].hElAngle = hElAngle;
return(hCodeError);
}
首先通过STC_GetSpeedSensor函数获取速度传感器句柄,然后通过SPD_GetElAngle函数和速度传感器句柄获取到转子角度hElAngle,然后通过函数PWMC_GetPhaseCurrents获取定子电流到Iab,实际上是通过电流获取函数指针指向的R3_2_GetPhaseCurrents函数实现的,获取电流之后通过MCM_Clarke函数进行Clarke变换到直角坐标系,然后再经过MCM_Park函数进行Park变换得到DQ轴电流Iqd,例程使用速度控制模式,不过速度环经过PID的输出还是要附加到DQ轴电流上,用于最终达到设定速度。PI控制使用PI_Controller函数实现,速度环PI控制器输出DQ轴电流限制使用Circle_Limitation函数实现。然后经过MCM_Rev_Park进行反Park变换得到直角坐标系电流值,最终通过PWMC_SetPhaseVoltage函数把电流通过SVPWM输出到电机。
输出限制函数Circle_Limitation实现如下。
__weak qd_t Circle_Limitation( CircleLimitation_Handle_t * pHandle, qd_t Vqd )
{
uint16_t table_element;
uint32_t uw_temp;
int32_t sw_temp;
qd_t local_vqd = Vqd;
sw_temp = ( int32_t )( Vqd.q ) * Vqd.q +
( int32_t )( Vqd.d ) * Vqd.d;
uw_temp = ( uint32_t ) sw_temp;
/* uw_temp min value 0, max value 32767*32767 */
if ( uw_temp > ( uint32_t )( pHandle->MaxModule ) * pHandle->MaxModule )
{
uw_temp /= ( uint32_t )( 16777216 );
/* wtemp min value pHandle->Start_index, max value 127 */
uw_temp -= pHandle->Start_index;
/* uw_temp min value 0, max value 127 - pHandle->Start_index */
table_element = pHandle->Circle_limit_table[( uint8_t )uw_temp];
sw_temp = Vqd.q * ( int32_t )table_element;
local_vqd.q = ( int16_t )( sw_temp / 32768 );
sw_temp = Vqd.d * ( int32_t )( table_element );
local_vqd.d = ( int16_t )( sw_temp / 32768 );
}
return ( local_vqd );
}
程序通过计算DQ轴电流的平方和和最大限制输出占比的平方做比较,判断是否需要进行限制。最大限制输出占比初始赋值如下。
CircleLimitation_Handle_t CircleLimitationM1 =
{
.MaxModule = MAX_MODULE,
.MaxVd = (uint16_t)(MAX_MODULE * 950 / 1000),
.Circle_limit_table = MMITABLE,
.Start_index = START_INDEX,
};
其最大输出占比宏定义如下。
/* MMI Table Motor 1 MAX_MODULATION_89_PER_CENT */
#define START_INDEX 50
#define MAX_MODULE 29162
程序限制在89%输出,即32767*0.89=29162.63≈29162。计算结果除以65536除以256即16777216将数值限制在0~255,然后减去50定位到限制数组中,结果乘以原始DQ轴电流再除以32768以得到最终限制输出电流值。
PWMC_SetPhaseVoltage用以将直角坐标系电流值赋值到定时器以输出SVPWM,将Valfa_beta乘以hT_Sqrt3转换成三相坐标系wX, wY, wZ。hT_Sqrt3定义为0xDDB4即56756,
即根号3/2*65536≈56,755.840862。
接着看下开方快速计算因子赋值:
PWMC_R3_2_Handle_t PWM_Handle_M1=
{
{
.pFctGetPhaseCurrents = &R3_2_GetPhaseCurrents,
.pFctSwitchOffPwm = &R3_2_SwitchOffPWM,
.pFctSwitchOnPwm = &R3_2_SwitchOnPWM,
.pFctCurrReadingCalib = &R3_2_CurrentReadingCalibration,
.pFctTurnOnLowSides = &R3_2_TurnOnLowSides,
.pFctSetADCSampPointSectX = &R3_2_SetADCSampPointSectX,
.pFctIsOverCurrentOccurred = &R3_2_IsOverCurrentOccurred,
.pFctOCPSetReferenceVoltage = MC_NULL,
.pFctRLDetectionModeEnable = &R3_2_RLDetectionModeEnable,
.pFctRLDetectionModeDisable = &R3_2_RLDetectionModeDisable,
.pFctRLDetectionModeSetDuty = &R3_2_RLDetectionModeSetDuty,
.hT_Sqrt3 = (PWM_PERIOD_CYCLES*SQRT3FACTOR)/16384u,
… … 
};
其中计算因子宏定义为:
#define SECTOR_1 0u
#define SECTOR_2 1u
#define SECTOR_3 2u
#define SECTOR_4 3u
#define SECTOR_5 4u
#define SECTOR_6 5u
#define SQRT3FACTOR (uint16_t) 0xDDB4 /* = (16384 * 1.732051 * 2)*/
通过比较其大小判断所处扇区。然后独立计算三相定时器的比较值大小,设置定时器各通道的比较值,这样就可以实现,控制三相逆变电路的开关了,从而实现FOC控制。最后再附加死区控制完成完整的SVPWM。
至此,整个DMF407电机开发板的FOC实验内容就讲完了,其中关于FOC的内容理解也参考了不少网上的资料,对此表示衷心的感谢,同时也希望我们这些代码,可以让大家有所受益,能开发出更强更好的产品。
总的来说我们的教程里介绍的电机种类是非常多的,所以关于电机的相关代码也是比较多的,希望大家,慢慢理解,各个攻破,最后祝大家身体健康、学习进步!
正点原子
2022-5-14
于广州






































