FOC基础)
笔记前声明文本用到的文章或图片等大都来自“灯哥开源”本人会在文本末尾附上原创链接保护作者原创也方便大家更深度地学习一、FOC基础1.概念引入经过前两节的学习,我们知道要控制电机,归根结底是控制MOS管的导通与关断,而这些控制由单片机程序决定。怎样才能让既高效又精准地驱动MOS管呢这就引出了FOC控制算法。FOC并不只是“简化”电机模型,它通过数学变换——将磁场和扭矩分开控制。这样一来,我们就能很直观地计算每个时刻该给哪个MOS管发什么开关信号,从而让电机运行得更平稳、效率更高。2.克拉克变换仍以最简单的模型为例(一对磁极对和三个相隔120°的线圈)。三个通电线圈的电流相位差为120°,且电流大小方向不停变,直接控制太麻烦。所以我们需要简化电机模型。首先把相位差为120°的电流抽象化为三个间隔120°的矢量,然后利用三角函数对矢量进行降维降维到两个坐标轴,将复杂的三相变化问题降解为α-β坐标轴上的数值变化问题。这个降维为的过程就叫克拉克变换。如图:左侧就是将电流抽象化后的矢量,我们需要把这三个矢量投影到右侧的坐标轴上。根据这个矢量分解图,我们可以很清晰的分析出:α轴:β轴:将上面的结果联立写成矩阵形式,就是:克拉克变换基本形式但在使用中,更常见的是克拉克变换等幅值形式。用α相电流输入1A电流来举例,当电流输入时,根据基尔霍夫电流定律:设:同样可以转换为矩阵形式:代入上述克拉克变换基本形式,并计算化简可得:经分析可知,由于b和c相电流投影的存在,导致a相输入1A电流时,反应在α轴上的电流并不是等幅值的1A,而是−3/2A。为了让式子等幅值,所以在克拉克变换基本形式前乘一个等幅值变换系数:2/3,得到克拉克变换等幅值形式:联立上面两式,进行计算和化简,我们最终就能得到α、β、ia、ib、ic之间的关系:综上:分析式子,可知由于基尔霍夫定律,只需要知道三相电流中的其中两相,就可以求出另一项相电流,在硬件层面就可以节省一路的电流检测器。3.克拉克逆变换不过,真正让电机转起来的还是三个线圈里的电流,调节的占空比本的质也是在控制这些电流的大小。所以光有α-β轴上的数值还不够,还得知道怎么把它们“翻译”回三相电流,这个反向计算的过程就叫克拉克逆变换。其实克拉克逆变换没有复杂之处,只是把刚才的关系式做进一步计算就可得:4.帕克(逆)变换通过克拉克变换,我们成功将三相电流降维为两相静止坐标系α-β轴,但是电机在旋转时,磁场与转子同步旋转,静止坐标系无法适应这种旋转关系。我们还需要将静止的α-β轴映射到随转子同步旋转的d-q轴上建立起与电机实际转动相匹配的数学模型。为了能够正确驱动电机,我们需要知道和电流输入规律,帕克变换正是解决这一问题的工具:它能在任意旋转角度下计算出所需的、,再通过克拉克逆变换,就能反推出对应时刻三相电流的实际输入值。帕克变换具体过程就是,在电机定子上的α-β坐标系上,另外新建了一个q−d坐标系这个坐标系随电机转子一起转动:那么我们就有了两个坐标系,一个是固定在定子上的α-β坐标系,一个是固定在转子上的q−d坐标系。我们将两个坐标系平移在一起:如图D轴在此处设定为指向电机的N级,q−d坐标系因转动与α-β坐标系形成的差角θ就被称为电角度。同理还是对矢量进行分解,就能把q−d坐标系上的值映射到α-β坐标系。我们再次整理成矩阵形式:同理,为了反求和,我们还需要通过帕克逆变换:计算并化简最终得到:因此通过上式,我们就能在知道、和电角度(机械角度x转子极对数)反求和。又因为机械角度可由编码器实时测出,所以在简单应用中,我们可以把设为0,只控制的大小。这样我们就能通过帕克逆变换求出和,再通过克拉克逆变换求出ia,ib,ic。至此理论知识完美闭环,这就是FOC的基本过程。最后在实际应用前还要强调的是:上述的分析全是基于电流矢量,但电机中的三相逆变器本质上是电压源型装置。它通过PWM输出的物理量直接就是电压幅值和频率,所以实际代码控制中要用电压值,但是电压矢量与电流矢量本质上没有区别,只需在每相电流上都乘上一个相电阻R,所以在控制方法上完全等价,不需要额外调整。二、开环速度控制(注:代码是基于Arduino ESP32平台)//灯哥开源转载请注明出处 //仅在DengFOC上测试过 //PWM输出引脚定义 int pwmA 32; int pwmB 33; int pwmC 25; //定义宏 _constrain 就是通过三目运算符,确保用户输入值在可控范围high和low之间 #define _constrain(amt,low,high) ((amt)(low)?(low):((amt)(high)?(high):(amt))) //相关变量名定义 float voltage_power_supply12.6;//输入电压 float shaft_angle0,open_loop_timestamp0; //zero_electric_angle 是一个电角度偏移校准值(认为初始值是0但实际情况可能会有偏差所以要校准) //闭环控制时会由编码器测量先预留为0方便后续闭环继续开发 float zero_electric_angle0,Ualpha,Ubeta0,Ua0,Ub0,Uc0,dc_a0,dc_b0,dc_c0;初始化所有硬件外设和全局状态:void setup() { // put your setup code here, to run once: Serial.begin(115200); //PWM设置 pinMode(pwmA, OUTPUT); pinMode(pwmB, OUTPUT); pinMode(pwmC, OUTPUT); ledcAttachPin(pwmA, 0); ledcAttachPin(pwmB, 1); ledcAttachPin(pwmC, 2); ledcSetup(0, 30000, 8); //pwm频道, 频率, 精度 ledcSetup(1, 30000, 8); //pwm频道, 频率, 精度 ledcSetup(2, 30000, 8); //pwm频道, 频率, 精度 Serial.println(完成PWM初始化设置); delay(3000); }电角度求解:float _electricalAngle(float shaft_angle, int pole_pairs) { return (shaft_angle * pole_pairs); }归一化角度到 [0,2PI]:float _normalizeAngle(float angle){ float a fmod(angle, 2*PI); //取余运算,用于归一化 return a 0 ? a : (a 2*PI);//当角度小于0时,增大2PI }设置控制器PWM输出:void setPwm(float Ua, float Ub, float Uc) { //应用_constrain将占空比限制在(0-1) dc_a _constrain(Ua / voltage_power_supply, 0.0f , 1.0f ); dc_b _constrain(Ub / voltage_power_supply, 0.0f , 1.0f ); dc_c _constrain(Uc / voltage_power_supply, 0.0f , 1.0f ); //写入PWM到PWM 0 1 2 通道 ledcWrite(0, dc_a*255); ledcWrite(1, dc_b*255); ledcWrite(2, dc_c*255); }利用帕克逆变换和克拉克逆变换计算出电机控制的相电压,并传递给控制器生成对应PWM:void setPhaseVoltage(float Uq,float Ud, float angle_el) { angle_el _normalizeAngle(angle_el zero_electric_angle); // 帕克逆变换 Ualpha -Uq*sin(angle_el); //因为简化把Id设为0,所以只有Uq(Iq)这一项 Ubeta Uq*cos(angle_el); // 克拉克逆变换 //voltage_power_supply/2是输入电压的一半,加上该值是为了将控制电压抬离原点(零电压) //从6.3~-6.3 变为 0~12.6避免出现负电压 Ua Ualpha voltage_power_supply/2; Ub (sqrt(3)*Ubeta-Ualpha)/2 voltage_power_supply/2; Uc (-Ualpha-sqrt(3)*Ubeta)/2 voltage_power_supply/2; setPwm(Ua,Ub,Uc); }开环速度设置函数float velocityOpenloop(float target_velocity){ //获取运行时间单位是微秒 unsigned long now_us micros(); //计算当前每个Loop的运行时间间隔乘1e-6f是把微秒转换乘秒 //target_velocity 的单位是 rad/s弧度每秒 float Ts (now_us - open_loop_timestamp) * 1e-6f; //避免程序出现卡顿导致时间间隔过长一步运行弧度过大 //合成的三相电压相位会发生突变严重会击穿MOS管 if(Ts 0 || Ts 0.5f) Ts 1e-3f; //目标角度当前角度运转速度x运行时间(运行角度) shaft_angle _normalizeAngle(shaft_angle target_velocity*Ts); //Uq最大值为voltage_power_supply/2否则ua,ub,uc会超出供电电压限幅 //这里保守设置Uq数值为voltage_power_supply/3 float Uq voltage_power_supply/3; //setPhaseVoltage(float Uq,float Ud, float angle_el) //_electricalAngle(float shaft_angle, int pole_pairs) //计算电角度(这里用的电机的磁对数是7),并传递函数值给PWM设定函数 setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, 7)); //用于计算下一个时间间隔 open_loop_timestamp now_us; return Uq; }设定速度目标值并循环执行void loop() { // put your main code here, to run repeatedly: velocityOpenloop(5); }最后附上源码//灯哥开源转载请著名出处 //仅在DengFOC上测试过 //PWM输出引脚定义 int pwmA 32; int pwmB 33; int pwmC 25; //初始变量及函数定义 #define _constrain(amt,low,high) ((amt)(low)?(low):((amt)(high)?(high):(amt))) //宏定义实现的一个约束函数,用于限制一个值的范围。 //具体来说该宏定义的名称为 _constrain接受三个参数 amt、low 和 high分别表示要限制的值、最小值和最大值。该宏定义的实现使用了三元运算符根据 amt 是否小于 low 或大于 high返回其中的最大或最小值或者返回原值。 //换句话说如果 amt 小于 low则返回 low如果 amt 大于 high则返回 high否则返回 amt。这样_constrain(amt, low, high) 就会将 amt 约束在 [low, high] 的范围内。 float voltage_power_supply12.6; float shaft_angle0,open_loop_timestamp0; float zero_electric_angle0,Ualpha,Ubeta0,Ua0,Ub0,Uc0,dc_a0,dc_b0,dc_c0; void setup() { // put your setup code here, to run once: Serial.begin(115200); //PWM设置 pinMode(pwmA, OUTPUT); pinMode(pwmB, OUTPUT); pinMode(pwmC, OUTPUT); ledcAttachPin(pwmA, 0); ledcAttachPin(pwmB, 1); ledcAttachPin(pwmC, 2); ledcSetup(0, 30000, 8); //pwm频道, 频率, 精度 ledcSetup(1, 30000, 8); //pwm频道, 频率, 精度 ledcSetup(2, 30000, 8); //pwm频道, 频率, 精度 Serial.println(完成PWM初始化设置); delay(3000); } // 电角度求解 float _electricalAngle(float shaft_angle, int pole_pairs) { return (shaft_angle * pole_pairs); } // 归一化角度到 [0,2PI] float _normalizeAngle(float angle){ float a fmod(angle, 2*PI); //取余运算可以用于归一化列出特殊值例子算便知 return a 0 ? a : (a 2*PI); //三目运算符。格式condition ? expr1 : expr2 //其中condition 是要求值的条件表达式如果条件成立则返回 expr1 的值否则返回 expr2 的值。可以将三目运算符视为 if-else 语句的简化形式。 //fmod 函数的余数的符号与除数相同。因此当 angle 的值为负数时余数的符号将与 _2PI 的符号相反。也就是说如果 angle 的值小于 0 且 _2PI 的值为正数则 fmod(angle, _2PI) 的余数将为负数。 //例如当 angle 的值为 -PI/2_2PI 的值为 2PI 时fmod(angle, _2PI) 将返回一个负数。在这种情况下可以通过将负数的余数加上 _2PI 来将角度归一化到 [0, 2PI] 的范围内以确保角度的值始终为正数。 } // 设置PWM到控制器输出 void setPwm(float Ua, float Ub, float Uc) { // 计算占空比 // 限制占空比从0到1 dc_a _constrain(Ua / voltage_power_supply, 0.0f , 1.0f ); dc_b _constrain(Ub / voltage_power_supply, 0.0f , 1.0f ); dc_c _constrain(Uc / voltage_power_supply, 0.0f , 1.0f ); //写入PWM到PWM 0 1 2 通道 ledcWrite(0, dc_a*255); ledcWrite(1, dc_b*255); ledcWrite(2, dc_c*255); } void setPhaseVoltage(float Uq,float Ud, float angle_el) { angle_el _normalizeAngle(angle_el zero_electric_angle); // 帕克逆变换 Ualpha -Uq*sin(angle_el); Ubeta Uq*cos(angle_el); // 克拉克逆变换 Ua Ualpha voltage_power_supply/2; Ub (sqrt(3)*Ubeta-Ualpha)/2 voltage_power_supply/2; Uc (-Ualpha-sqrt(3)*Ubeta)/2 voltage_power_supply/2; setPwm(Ua,Ub,Uc); } //开环速度函数 float velocityOpenloop(float target_velocity){ unsigned long now_us micros(); //获取从开启芯片以来的微秒数它的精度是 4 微秒。 micros() 返回的是一个无符号长整型unsigned long的值 //计算当前每个Loop的运行时间间隔 float Ts (now_us - open_loop_timestamp) * 1e-6f; //由于 micros() 函数返回的时间戳会在大约 70 分钟之后重新开始计数在由70分钟跳变到0时TS会出现异常因此需要进行修正。如果时间间隔小于等于零或大于 0.5 秒则将其设置为一个较小的默认值即 1e-3f if(Ts 0 || Ts 0.5f) Ts 1e-3f; // 通过乘以时间间隔和目标速度来计算需要转动的机械角度存储在 shaft_angle 变量中。在此之前还需要对轴角度进行归一化以确保其值在 0 到 2π 之间。 shaft_angle _normalizeAngle(shaft_angle target_velocity*Ts); //以目标速度为 10 rad/s 为例如果时间间隔是 1 秒则在每个循环中需要增加 10 * 1 10 弧度的角度变化量才能使电机转动到目标速度。 //如果时间间隔是 0.1 秒那么在每个循环中需要增加的角度变化量就是 10 * 0.1 1 弧度才能实现相同的目标速度。因此电机轴的转动角度取决于目标速度和时间间隔的乘积。 // 使用早前设置的voltage_power_supply的1/3作为Uq值这个值会直接影响输出力矩 // 最大只能设置为Uq voltage_power_supply/2否则ua,ub,uc会超出供电电压限幅 float Uq voltage_power_supply/3; setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, 7)); open_loop_timestamp now_us; //用于计算下一个时间间隔 return Uq; } void loop() { // put your main code here, to run repeatedly: velocityOpenloop(5); }本文仅是让电机转起来(假速度控制),真实速度未闭环;编码器PI 的速度闭环后续内容见本文章旨在记录学习电机相关知识的心得和体会,无任何获利倾向,如有错误欢迎批评指正!1.灯哥开源文档2.灯哥开源视频讲解