行业知识
基于开源项目ESP32 SVPWM驱动无刷电机开环速度测试
2026-11-12 11:08:52
来源:互联网转载
基于开源项目ESP32 SVPWM驱动无刷电机开环速度测试
- ✨本篇硬件电路和代码来源于此开源项目:https://github.com/MengYang-x/STM3F401-FOC/tree/main
- 硬件电路和项目介绍,立创开源广场:https://oshwhub.com/shadow27/tai-yang-neng-wu-ren-chuan
- 相关篇《基于开源项目HAL STM32F4 +DSP库跑SVPWM开环速度测试》
- 代码基于Arduino平台。
- ESP32 SVPWM开环测试效果:
- ⚗无刷电机运行正常运作过程中,代码二的测试波形效果:
- 驱动电路参考:
⚡如果是通过6路信号来驱动无刷电机的不支持。如果需要测试6路信号驱动的可以参考我上面的相关内容,有关STM32 通过高级定时器3路互补输出来实现SVPWM驱动无刷电机。
⚗模拟仿真测试
- ESP32在线SVPWM模拟仿真测试地址:https://wokwi.com/projects/396507548266030081
- 仿真代码
#include #include #define PI 3.14159265359#define PI_2 1.57079632679#define PI_3 1.0471975512#define _SQRT3 1.73205080757#define voltage_power_supply 12.0float normalizeAngle(float angle) { float a = fmod(angle, 2 * PI); return a >= 0 ? a : (a + 2 * PI);}void setPwm(float Ua, float Ub, float Uc) { Serial.print(Ua); Serial.print(","); Serial.print(Ub); Serial.print(","); Serial.println(Uc);}void setTorque(float Uq, float angle_el) { if (Uq驱动测试代码 一
- ✨此代码直接驱动无刷电机转动没有问题,但是开启Vofa+波形就不正常了,打印函数太占用时间,驱动无刷电机对SVPWM要求实时连续性很高,因任务执行所消耗的时间,一个loop循环下来,运行时间大大超出了预期值。波形输出的直接变成了正弦波,而不是马鞍波,导致电机不能转动,
- 不开启打印,一个loop循环下来。大概就是60us左右,也就是代码中 velocityOpenloop(2.5f);执行一遍的时间。
- 开启打印,如果波特率设置比较低,打印3个浮点类型数据,消耗的时间会超过1ms。
- 如果需要查看波形,串口通讯波特率尽可能的设置高一些,给定的预设的角度值大一些。
- 在驱动无刷电机前,调试前期,可以直接通过查看3路波形,即可预测驱动电机的实际效果。一定要是SVPWM波形(马鞍波),才能正常转起来。
/* * 日期:2023.7.22 * 开环速度控制代码 * 使用vofa+ 进行串口调试,波特率需要设置为57600 * 电机参数 A2212/15T的极对数:7 * */#include #include const int poles = 7; // 电机的极对数// PWM输出引脚定义// 定义LEDC通道、GPIO引脚和分辨率#define LEDC_CHANNEL1 0#define LEDC_CHANNEL2 1#define LEDC_CHANNEL3 2#define LEDC_GPIO1 5#define LEDC_GPIO2 18#define LEDC_GPIO3 19#define LEDC_RESOLUTION 10 // 设置分辨率为10位#define PWM_FREQ 15000 // 设置PWM频率为15000Hz// const char pwmA = 5;// const char pwmB = 18;// const char pwmC = 19;const float voltagePowerSupply = 12.0;float open_loop_timestamp = 0;float shaft_angle = 0; // 机械角度float zero_electric_angle = 0;float Ualpha, Ubeta = 0;float Ua = 0, Ub = 0, Uc = 0;float dc_a = 0, dc_b = 0, dc_c = 0;void setup(){ Serial.begin(57600); // PWM设置 pinMode(LEDC_GPIO1, OUTPUT); pinMode(LEDC_GPIO2, OUTPUT); pinMode(LEDC_GPIO3, OUTPUT); ledcSetup(LEDC_CHANNEL1, PWM_FREQ, LEDC_RESOLUTION); // pwm通道(1-16), 频率, 精度(0-14) ledcAttachPin(LEDC_GPIO1, 0); // 将GPIO引脚与LEDC通道关联,这样才能让LEDC信号输出到这个引脚 ledcSetup(LEDC_CHANNEL2, PWM_FREQ, LEDC_RESOLUTION); // pwm通道, 频率, 精度 ledcAttachPin(LEDC_GPIO2, 1); // 将GPIO引脚与LEDC通道关联 ledcSetup(LEDC_CHANNEL3, PWM_FREQ, LEDC_RESOLUTION); // pwm通道, 频率, 精度 ledcAttachPin(LEDC_GPIO3, 2); // 将GPIO引脚与LEDC通道关联 Serial.println("完成PWM初始化设置"); delay(3000);}/** 电角度 = 机械角度 * 极对数 * @brief 电角度计算函数 * @param shaft_angle 机械角度 * @param pole_pairs 电机的极对数*/float _electricalAngle(float shaft_angle, int pole_pairs){ return (shaft_angle * pole_pairs);}/**角度归一化到[0, 2pi],把输入的角度限制在[0, 2pi] * @brief 角度归一化函数 * @param angle 输入的角度 * @return 归一化后的角度 * 例如:_normalizeAngle(3.1415926) 返回 0*/float _normalizeAngle(float angle){ float a = fmod(angle, 2 * PI); // 取余,结果可能为负值 return a >= 0 ? a : (a + 2 * PI);}/**设置PWM输出 * @brief 设置PWM输出 * @param Ua 电机A的占空比 * @param Ub 电机B的占空比 * @param Uc 电机C的占空比*/void setPwm(float Ua, float Ub, float Uc){ // 计算占空比,并使用constrain()函数限制相电压的范围0到1 dc_a = constrain(Ua / voltagePowerSupply, 0.0f, 1.0f); dc_b = constrain(Ub / voltagePowerSupply, 0.0f, 1.0f); dc_c = constrain(Uc / voltagePowerSupply, 0.0f, 1.0f); // 写入PWM到PWM 0 1 2 通道 ledcWrite(0, static_cast(dc_a * 1023)); //使用10位分辨率计算占空比值 ledcWrite(1, static_cast(dc_b * 1023)); ledcWrite(2, static_cast(dc_c * 1023));}/** * @brief 设置相位电压 * @param Uq 电流值 * @param Ud 电压值 * @param angle_el 电机的电角度,单位 rad * 电角度 = 机械角度 * 极对数 * 机械角度 = 电角度 / 极对数*/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 + voltagePowerSupply / 2; Ub = (sqrt(3) * Ubeta - Ualpha) / 2 + voltagePowerSupply / 2; Uc = (-Ualpha - sqrt(3) * Ubeta) / 2 + voltagePowerSupply / 2; setPwm(Ua, Ub, Uc);}/** 开环速度函数,Uq和电角度生成器 * @brief 开环速度控制函数 * @param target_velocity 目标速度,单位 rad/s * @return 返回Uq值,用于控制电机转速 */float velocityOpenloop(float target_velocity){ // unsigned long now_us = micros(); // 获取从开启芯片以来的微秒数,它的精度是 4 微秒。 micros() 返回的是一个无符号长整型(unsigned long)的值 static float deltaT = 6.5e-5f; // 给定一个固定的开环运行时间间隔 // 计算当前每个Loop的运行时间间隔 // float Ts = (now_us - open_loop_timestamp) * 1e-6f; // 由于 micros() 函数返回的时间戳会在大约 70 分钟之后重新开始计数,在由70分钟跳变到0时,TS会出现异常,因此需要进行修正。如果时间间隔小于等于零或大于 0.5 秒,则将其设置为一个较小的默认值,即 1e-3f // if (Ts 0.5f) // Ts = 6.5e-5f; // 通过乘以时间间隔和目标速度来计算需要转动的机械角度,存储在 shaft_angle 变量中。在此之前,还需要对轴角度进行归一化,以确保其值在 0 到 2π 之间。 // shaft_angle = _normalizeAngle(shaft_angle + target_velocity * Ts); shaft_angle = _normalizeAngle(shaft_angle + target_velocity * deltaT); // 以目标速度为 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 = voltagePowerSupply / 24; setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, poles)); // 极对数可以设置为常量 // open_loop_timestamp = now_us; // 用于计算下一个时间间隔 return Uq;}/** * @brief 调试函数,用于输出PWM占空比 * @return 无 */void debug(){ Serial.printf("%f,%f,%f\n", dc_a, dc_b, dc_c);}void loop(){ velocityOpenloop(2.5f); // debug();}驱动测试代码二
✨代码中换算采用的是上面仿真中的算法,在开启VOFA+串口波形查看时,务必将波特率尽可能设置高一些,以减少打印信息执行的时间。
- 波形效果:
- 说明:
-
力矩大小影响因素: setTorque(0.3f, _electricalAngle(shaft_angle, poles));//Uq影响振幅,力矩大小
-
转速影响因素: velocityOpenloop(6.0f);//数值越大和变量deltaT
/* * 日期:2023.7.22 * 开环速度控制代码 * 进行串口调试,波特率需要设置为576000 * 电机参数 A2212/15T的极对数:7 * */#include #include #define VOFA_SERIAL // 使用vofa+串口调试器查看马鞍波波形const int poles = 7; // 电机的极对数// PWM输出引脚定义// 定义LEDC通道、GPIO引脚和分辨率#define LEDC_CHANNEL1 0#define LEDC_CHANNEL2 1#define LEDC_CHANNEL3 2#define LEDC_GPIO1 5#define LEDC_GPIO2 18#define LEDC_GPIO3 19#define LEDC_RESOLUTION 10 // 设置分辨率为10位#define PWM_FREQ 15000 // 设置PWM频率为10000Hz// const char pwmA = 5;// const char pwmB = 18;// const char pwmC = 19;const float voltagePowerSupply = 12.0;float open_loop_timestamp = 0;float shaft_angle = 0; // 机械角度float zero_electric_angle = 0;float Ualpha, Ubeta = 0;float Ua = 0, Ub = 0, Uc = 0;float dc_a = 0, dc_b = 0, dc_c = 0;//#define PI 3.14159265359#define PI_2 1.57079632679#define PI_3 1.0471975512#define _SQRT3 1.73205080757/** 电角度 = 机械角度 * 极对数 * @brief 电角度计算函数 * @param shaft_angle 机械角度 * @param pole_pairs 电机的极对数*/float _electricalAngle(float shaft_angle, int pole_pairs){ return (shaft_angle * pole_pairs);}/**角度归一化到[0, 2pi],把输入的角度限制在[0, 2pi] * @brief 角度归一化函数 * @param angle 输入的角度 * @return 归一化后的角度 * 例如:_normalizeAngle(3.1415926) 返回 0*/float _normalizeAngle(float angle){ float a = fmod(angle, 2 * PI); // 取余,结果可能为负值 return a >= 0 ? a : (a + 2 * PI);}/**设置PWM输出 * @brief 设置PWM输出 * @param Ua 电机A的占空比 * @param Ub 电机B的占空比 * @param Uc 电机C的占空比*/void setPwm(float Ua, float Ub, float Uc){ // 计算占空比,并使用constrain()函数限制相电压的范围0到1 dc_a = constrain(Ua / voltagePowerSupply, 0.0f, 1.0f); dc_b = constrain(Ub / voltagePowerSupply, 0.0f, 1.0f); dc_c = constrain(Uc / voltagePowerSupply, 0.0f, 1.0f); // 写入PWM到PWM 0 1 2 通道 ledcWrite(0, static_cast(dc_a * 1023)); //使用10位分辨率计算占空比值 ledcWrite(1, static_cast(dc_b * 1023)); ledcWrite(2, static_cast(dc_c * 1023));}/** * @brief 设置相位电压 * @param Uq 电流值 * @param Ud 电压值 * @param angle_el 电机的电角度,单位 rad * 电角度 = 机械角度 * 极对数 * 机械角度 = 电角度 / 极对数*/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 + voltagePowerSupply / 2; Ub = (sqrt(3) * Ubeta - Ualpha) / 2 + voltagePowerSupply / 2; Uc = (-Ualpha - sqrt(3) * Ubeta) / 2 + voltagePowerSupply / 2; setPwm(Ua, Ub, Uc);}void setTorque(float Uq, float angle_el) { if (Uq⛳解决上面程序中的痛点问题,引入双核心多线程任务运行方案
✨在Arduino平台,esp32程序默认运行在核心1上的,引入双核心多线程任务运行,将串口打印和SVPWM计算分别运行在核心0和核心1上,来保证任务执行的实时性。由于不同线程间的任务执行,任务的执行时间差异,需要及时给rtc看门狗,进行喂狗操作,否则,每执行一段时间,esp32就会产生看门狗复位的动作。
/* * 日期:2024.5.31更新 * 开环速度控制代码 * 进行串口调试,波特率需要设置为576000 * 电机参数 2204-1400KV-12N14P 的极对数:7 * */#include #include #include "soc/rtc_wdt.h" //设置看门狗用#include "freertos/FreeRTOS.h"#include "freertos/task.h"#include "freertos/queue.h"#include "driver/ledc.h"#include "esp_log.h"#include "esp_system.h"#include "esp_timer.h"#include "esp_attr.h"#include "esp_intr_alloc.h"#include "esp_err.h"#include "esp_task_wdt.h"#define VOFA_SERIAL // 使用vofa+串口调试器查看马鞍波波形const int poles = 7; // 电机的极对数// PWM输出引脚定义// 定义LEDC通道、GPIO引脚和分辨率#define LEDC_CHANNEL1 0#define LEDC_CHANNEL2 1#define LEDC_CHANNEL3 2#define LEDC_GPIO1 5#define LEDC_GPIO2 18#define LEDC_GPIO3 19#define LEDC_RESOLUTION 10 // 设置分辨率为10位#define PWM_FREQ 15000 // 设置PWM频率为10000Hzconst float voltagePowerSupply = 12.0;float open_loop_timestamp = 0;float shaft_angle = 0; // 机械角度float zero_electric_angle = 0;float Ualpha, Ubeta = 0;float Ua = 0, Ub = 0, Uc = 0;float dc_a = 0, dc_b = 0, dc_c = 0;//#define PI 3.14159265359#define PI_2 1.57079632679#define PI_3 1.0471975512#define _SQRT3 1.73205080757TaskHandle_t th_p[2];// 任务句柄,对xTaskCreate的调用返回。可用作参数到vTaskDelete以删除任务。/** 电角度 = 机械角度 * 极对数 * @brief 电角度计算函数 * @param shaft_angle 机械角度 * @param pole_pairs 电机的极对数*/float _electricalAngle(float shaft_angle, int pole_pairs){ return (shaft_angle * pole_pairs);}/**角度归一化到[0, 2pi],把输入的角度限制在[0, 2pi] * @brief 角度归一化函数 * @param angle 输入的角度 * @return 归一化后的角度 * 例如:_normalizeAngle(3.1415926) 返回 0*/float _normalizeAngle(float angle){ float a = fmod(angle, 2 * PI); // 取余,结果可能为负值 return a >= 0 ? a : (a + 2 * PI);}/**设置PWM输出 * @brief 设置PWM输出 * @param Ua 电机A的占空比 * @param Ub 电机B的占空比 * @param Uc 电机C的占空比*/void setPwm(float Ua, float Ub, float Uc){ // 计算占空比,并使用constrain()函数限制相电压的范围0到1 dc_a = constrain(Ua / voltagePowerSupply, 0.0f, 1.0f); dc_b = constrain(Ub / voltagePowerSupply, 0.0f, 1.0f); dc_c = constrain(Uc / voltagePowerSupply, 0.0f, 1.0f); // 写入PWM到PWM 0 1 2 通道 ledcWrite(0, static_cast(dc_a * 1023)); //使用10位分辨率计算占空比值 ledcWrite(1, static_cast(dc_b * 1023)); ledcWrite(2, static_cast(dc_c * 1023));}/** * @brief 设置相位电压 * @param Uq 电流值 * @param Ud 电压值 * @param angle_el 电机的电角度,单位 rad * 电角度 = 机械角度 * 极对数 * 机械角度 = 电角度 / 极对数*/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 + voltagePowerSupply / 2; Ub = (sqrt(3) * Ubeta - Ualpha) / 2 + voltagePowerSupply / 2; Uc = (-Ualpha - sqrt(3) * Ubeta) / 2 + voltagePowerSupply / 2; setPwm(Ua, Ub, Uc);}void setTorque(float Uq, float angle_el) { if (Uq调参测试过程,记录分析参考
- 3.2过度到6.0f波形变化
velocityOpenloop(6.0f);//3.2过度到6.0ffloat deltaT = 3.4e-3f;
- 修改deltaT :3.4e-3f;到 3.4e-4f波形变化。
velocityOpenloop(6.0f);float deltaT = 3.4e-4f;
- 修改形参6.0f 到12.0f变化:
velocityOpenloop(12.0f); float deltaT = 3.4e-4f;
- 修改deltaT ,3.4e-4f到6.5e-4f变化:
velocityOpenloop(12.0f); float deltaT = 6.5e-4f;
- 修改deltaT ,3.4e-4f到6.5e-4f变化:
- 修改形参6.0f 到12.0f变化:
- 修改deltaT :3.4e-3f;到 3.4e-4f波形变化。
- 3.2过度到6.0f波形变化
- 仿真代码
- ESP32在线SVPWM模拟仿真测试地址:https://wokwi.com/projects/396507548266030081