一、强拖概述
强拖(也称为开环拖动)是一种无刷电机(BLDC/PMSM)的无传感器启动方法或电机测试方法,主要用于电机启动或低速运行阶段,在没有位置传感器(如编码器、霍尔)的情况下,通过固定电压/频率比 (V/F) 或者计算一个模拟角度,同时进行电流闭环 (I/F) 强制驱动电机旋转。
1.1 V/F 启动简介
V/F 启动,是保持电压 (V) 与频率 (F) 的比值恒定。并不需要控制器采集包括电机自身任何的外部信息,它仅需要设置目标电频率 (可以去我之前文章搜搜电频率是啥,目前可粗浅的将电频率看作为转速) 以生成虚拟的电机位置角度,和目标调制的电压即可运行,这里所谓的调制电压可以看作母线电压的百分比。
- 优点:无需采集电流。
- 缺点:负载突变时易失步,恢复速度慢,转速会随负载波动。
1.2 I/F 启动简介
I/F 启动,保持电流(I)与频率(F)的比值恒定。通过调节电流实现转矩控制,简单来说就是根据配置生成虚拟的电机位置角度,和 FOC 的电流闭环同时控制。
- 优点:负载突变稳定,恒速。
- 缺点:需实时电流检测。
1.3 区别
简单来说,V/F和I/F最大的区别是,I/F 增加了电流闭环,根据电流闭环生成调制 PWM 输出。而 V/F 的调制比是固定的开环控制。
二、代码介绍
2.1 较简单的强拖与切换无感程序 (量产过)
2.1.1 参数和变量
#define FORCE_START 0 // 强拖启动 切换时会有噪音
#define FORCE_ACC (200) // 强拖加速度//todo:转成物理单位量
#define FORCE_RPM_MAX (500*23) // 强拖最大速度(电速度)
#define FORCE_SPEED_MIN (1000) // 强拖介入速度(x/32768*100%)
#define FORCE_SWITCH (50*23) // 强拖切换速度(电速度) 周期的角度增量(dpp)
stru_force_start force_start = {
// 加速步进的周期数
.acc_plus_periods = FORCE_ACC,
// 最大角速度(通过rpm2dpp宏将RPM转换为角度/周期)也就是每次调用自增的角度
.thetadpp_max = rpm2dpp(FORCE_RPM_MAX)
};
其中:
#define rpm2dpp(rpm) (((s32)rpm << 16) / (s16)PWM_FREQ / 60 ) // 将 RPM 转换为每 PWM 周期的角度增量(dpp)
1 rpm 为一转,也就是360°。对于这个代码框架来说 0~360° 映射到 0~65535,所以首先将 rpm 乘 65535,然后除 pwm 周期和 60。因为 rpm 是每分钟转速,我们需要算出每秒的转速。
最终得出 thetadpp_max 的值。
2.1.2 强拖主要程序
在以上的强拖逻辑中, force_start.thetadpp < force_start.thetadpp_max 时,需要先缓慢增加 force_start.thetadpp 的值,以模拟电机启动时转速放缓的情况。之后每个周期就按照 thetadpp_max 的值进行累加,以在强拖阶段达到我们的目标转速。
// 函数功能:处理强制启动阶段的电机控制
// 参数:s32_Angle - 霍尔传感器或编码器反馈的当前电机角度(单位:定点数或度)
void force_start_handle(m_s32 s32_Angle)
{
static u16 cnt = 0; // 静态计数器,用于控制加速周期
// 1. 检查MOS管状态(是否关闭)
if (MOS_CLOSED())
{
force_start.theta = 0; // 重置目标角度为0
cnt = 0; // 重置计数器为0
force_start.thetadpp = 0; // 重置角速度为0
}
// 2. 加速控制逻辑
if (++cnt >= force_start.acc_plus_periods) // 计数器递增并检查是否达到加速周期
{
cnt = 0; // 重置计数器
if (force_start.thetadpp < force_start.thetadpp_max)
{
force_start.thetadpp++; // 逐步增加角速度(直到达到最大值)
}
}
// 3. 更新目标角度
force_start.theta += force_start.thetadpp; // 累加角度值(模拟电机转动)
// 4. 计算角度偏差
force_start.angle_delta_flex = force_start.theta - flex.theta; // 目标角度与柔性启动角度的偏差
force_start.angle_delta_hall = force_start.theta - s32_Angle; // 目标角度与传感器反馈角度的偏差
}
这段代码中所生成的角度波形是这样的。在刚刚启动的时候,force_start.thetadpp 逐步自增,也就是下图中左侧部分一样,三角波形周期从长变短,模拟电机真实启动的过程。
因为使用的是 uint16_t 类型储存变量,所以生成的三角波会在 0-65535 区间。但是我们是按照有符号数读取的,那么就是 -32768 至 32767 了。

2.1.3 强拖角度补偿和强拖切换观测器
当 MOS 管处于关管状态时使 force_start.flag = 2,代表当前是强拖启动状态。
等 force_start.thetadpp > rpm2dpp(FORCE_SWITCH) 也就是强拖的转速达到指定值时对 force_start.iq_sub++ 也就是在此刻强加目标电流,以防止强拖失败。
if ((flex.theta - force_start.theta < 500) && (force_start.theta - flex.theta < 500)) 这个判断是强拖给出的角度和观测角度误差小于 500 就切换为观测角度。
void force_check_handle()
{
// 检测反电动势电压是否低于最低速度阈值
if (bemf.vol_pu < FORCE_SPEED_MIN)
{
// 检查MOS管是否关闭(电机未通电)
if (MOS_CLOSED())
{
force_start.flag = 2; // 进入强制启动状态2
force_start.iq_sub = 0; // 重置Q轴电流补偿值为0
}
}
// 检查当前是否处于强制启动状态2
if (2 == force_start.flag)
{
// 检查当前角速度是否超过切换阈值(FORCE_SWITCH转换为dpp单位)
if (force_start.thetadpp > rpm2dpp(FORCE_SWITCH))
{
// 计数器递增,每2次循环执行一次
if (++cnt > 1)
{
cnt = 0; // 重置计数器
// 增加Q轴电流补偿值(上限30000)
if (++force_start.iq_sub > 30000)
{
force_start.iq_sub = 30000; // 限制最大值
}
}
// 检查虚拟角度与实际角度偏差是否小于500(单位可能是Q16)
if ((flex.theta - force_start.theta < 500) &&
(force_start.theta - flex.theta < 500))
{
force_start.flag = 0; // 退出强制启动状态
}
}
}
}
2.1.4 强拖程序完整代码
#define rpm2dpp(rpm) (((s32)rpm << 16) / (s16)PWM_FREQ / 60) // 将 RPM 转换为每 PWM 周期的角度增量(dpp)
#define FORCE_START 0 // 强拖启动 切换时会有噪音
#define FORCE_ACC (200) // 强拖加速度//todo:转成物理单位量
#define FORCE_RPM_MAX (500 * 23) // 强拖最大速度(电速度)
#define FORCE_SPEED_MIN (1000) // 强拖介入速度(x/32768*100%)
#define FORCE_SWITCH (50 * 23) // 强拖切换速度(电速度) 周期的角度增量(dpp)
stru_force_start force_start = {
// 加速步进的周期数
.acc_plus_periods = FORCE_ACC,
// 最大角速度(通过rpm2dpp宏将RPM转换为角度/周期)也就是每次调用自增的角度
.thetadpp_max = rpm2dpp(FORCE_RPM_MAX)};
// 函数功能:处理强制启动阶段的电机控制
// 参数:s32_Angle - 霍尔传感器或编码器反馈的当前电机角度(单位:定点数或度)
void force_start_handle(m_s32 s32_Angle)
{
static u16 cnt = 0; // 静态计数器,用于控制加速周期
// 1. 检查MOS管状态(是否关闭)
if (MOS_CLOSED())
{
force_start.theta = 0; // 重置目标角度为0
cnt = 0; // 重置计数器为0
force_start.thetadpp = 0; // 重置角速度为0
}
// 2. 加速控制逻辑
if (++cnt >= force_start.acc_plus_periods) // 计数器递增并检查是否达到加速周期
{
cnt = 0; // 重置计数器
if (force_start.thetadpp < force_start.thetadpp_max)
{
force_start.thetadpp++; // 逐步增加角速度(直到达到最大值)
}
}
// 3. 更新目标角度
force_start.theta += force_start.thetadpp; // 累加角度值(模拟电机转动)
// 4. 计算角度偏差
force_start.angle_delta_flex = force_start.theta - flex.theta; // 目标角度与柔性启动角度的偏差
force_start.angle_delta_hall = force_start.theta - s32_Angle; // 目标角度与霍尔传感器反馈角度的偏差
}
void force_check_handle()
{
// 检测反电动势电压是否低于最低速度阈值
if (bemf.vol_pu < FORCE_SPEED_MIN)
{
// 检查MOS管是否关闭(电机未通电)
if (MOS_CLOSED())
{
force_start.flag = 2; // 进入强制启动状态2
force_start.iq_sub = 0; // 重置Q轴电流补偿值为0
}
}
// 检查当前是否处于强制启动状态2
if (2 == force_start.flag)
{
// 检查当前角速度是否超过切换阈值(FORCE_SWITCH转换为dpp单位)
if (force_start.thetadpp > rpm2dpp(FORCE_SWITCH))
{
// 计数器递增,每2次循环执行一次
if (++cnt > 1)
{
cnt = 0; // 重置计数器
// 增加Q轴电流补偿值(上限30000)
if (++force_start.iq_sub > 30000)
{
force_start.iq_sub = 30000; // 限制最大值
}
}
// 检查虚拟角度与实际角度偏差是否小于500(单位可能是Q16)
if ((flex.theta - force_start.theta < 500) &&
(force_start.theta - flex.theta < 500))
{
force_start.flag = 0; // 退出强制启动状态
}
}
}
}
2.2 某简易电机驱动的强拖程序(带预定位)
2.1.1 预定位
这段代码预定位的逻辑非常简单,仅靠延迟做实现。简单来说转子预定为就是给一个固定的角度输出给 FOC 控制一段时间,使转子拖动到这个角度。
对于控制逻辑相当于是:
- 施加一个固定的D轴电流(
Id),产生定向磁场(如指向0°电角度)。 - 保持Q轴电流(
Iq)为零(不产生转矩)。 - 持续
LOCK_COUNTER个控制周期,确保转子被拉至目标位置。
LOCK_COUNTER 取值:
通常为几十到几百个PWM周期(例如20ms,若PWM频率=10kHz,则 LOCK_COUNTER=200)。
/* 初始锁定序列(用于磁场对齐) */
if (pmsm_mc_param.startup_lock_count < LOCK_COUNTER)
{
pmsm_mc_param.startup_lock_count++; // 锁定计数器递增
}
2.1.2 加速度和切无感条件
同样,这段代码也有启动的加速度:
pmsm_mc_param.startup_ramp += OPENLOOP_RAMPSPEED_INCREASERATE; // 增加开环转速
这段代码的切换判断条件也很有意思:
开环生成的角度 pmsm_foc_param.angle < π(180°)实际转子角度 smc1.theta < π(180°)
这两个条件同时满足才会切换:
if ((pmsm_foc_param.angle < M_PI) && (smc1.theta < M_PI))
全部代码如下:
/* 加速阶段 */
else if (pmsm_mc_param.startup_ramp < END_SPEED_RADS_ELEC_COUNTER)
{
pmsm_mc_param.startup_ramp += OPENLOOP_RAMPSPEED_INCREASERATE; // 增加开环转速
}
/* 准备切换到闭环 */
else
{
theta_error = pmsm_foc_param.angle - smc1.theta; // 计算角度误差
#ifndef OPEN_LOOP_MODE
// 检查角度是否在合理范围内(避免突变)
if ((pmsm_foc_param.angle < M_PI) && (smc1.theta < M_PI))
{
pmsm_mc_param.change_mode = 1; // 标记模式切换
pmsm_mc_param.openloop = 0; // 退出开环模式
}
#endif
2.1.3 完整代码
static void calculate_park_angle(void)
{
if (pmsm_mc_param.openloop)
{
/* 初始锁定序列(用于磁场对齐) */
if (pmsm_mc_param.startup_lock_count < LOCK_COUNTER)
{
pmsm_mc_param.startup_lock_count++; // 锁定计数器递增
}
/* 加速阶段 */
else if (pmsm_mc_param.startup_ramp < END_SPEED_RADS_ELEC_COUNTER)
{
pmsm_mc_param.startup_ramp += OPENLOOP_RAMPSPEED_INCREASERATE; // 增加开环转速
}
/* 准备切换到闭环 */
else
{
theta_error = pmsm_foc_param.angle - smc1.theta; // 计算角度误差
#ifndef OPEN_LOOP_MODE
// 检查角度是否在合理范围内(避免突变)
if ((pmsm_foc_param.angle < M_PI) && (smc1.theta < M_PI))
{
pmsm_mc_param.change_mode = 1; // 标记模式切换
pmsm_mc_param.openloop = 0; // 退出开环模式
}
#endif
}
// 更新Park角度(累加开环转速)
pmsm_foc_param.angle += pmsm_mc_param.startup_ramp;
// 角度归一化(0~2π)
if (pmsm_foc_param.angle > ANGLE_2PI)
pmsm_foc_param.angle = pmsm_foc_param.angle - ANGLE_2PI;
}
2.3 南京凌欧官方代码框架强拖程序
2.3.1 强拖角度生成
预先设置目标转速和每周期增加的步长 (角度)
// 根据电机运行方向设置强拖频率方向
if (struFOC_CtrProc.bMotorDirtionCtrl == CW) // 顺时针方向
{
// 设置正向强拖目标频率(最小服务频率)
mOpenForceRun.wOpen2CloseFreq = struAppCommData.wSvcMinFreq;
}
else // 逆时针方向
{
// 设置负向强拖目标频率
mOpenForceRun.wOpen2CloseFreq = -struAppCommData.wSvcMinFreq;
}
// 设置强拖频率的加速步长(将用户单位转换为应用单位)
mOpenForceRun.nFreqAccStep = User2AppFreqTrans(FREQ_ACC);
// 设置强拖频率的减速步长(将用户单位转换为应用单位)
mOpenForceRun.nFreqDecStep = User2AppFreqTrans(FREQ_DEC);
// 执行频率爬坡生成(生成线性变化的频率参考值)
SpeedReferenceGen(&mOpenForceRun);
其中 SpeedReferenceGen() 函数实现如下:
只做了简单的累加,并没有设置强拖刚开始的缓启动。
/*****************************************************************************
* 函数名 : void SpeedReferenceGen(stru_OpenForceRunDef *this)
* 说明 : 速度爬坡
* 设计思路 :1.速度爬坡
* 参数 :无
* 返回值 :无
* 修改时间 :2020.08.17
*****************************************************************************/
void SpeedReferenceGen(stru_OpenForceRunDef *this)
{
s32 ax;
ax = this->wOpen2CloseFreq << 7;
if(this->wRampFreqRef < (ax))
{
this->wRampFreqRef += this->nFreqAccStep;
if(this->wRampFreqRef >= ax)
{
this->wRampFreqRef = ax;
}
}
if(this->wRampFreqRef > ax)
{
this->wRampFreqRef -= this->nFreqDecStep;
if(this->wRampFreqRef <= ax)
{
this->wRampFreqRef = ax;
}
}
}
2.3.1 强拖切无感逻辑
根据强拖角度和和观测角度误差在指定范围内累加切换。
// 计算观测器估算角度与开环强拖角度的误差
struAppCommData.wThetaErr = struFluxOB_Param.wElectAngleEst - struFluxOB_Param.wElectAngleOpen;
// 取误差绝对值并右移16位(Q16格式转换为实际值)
struAppCommData.wThetaErr = ABS(struAppCommData.wThetaErr) >> 16;
// 检查角度误差是否在合理范围内(2000~18000对应10.98°~98.87°)
if ((struAppCommData.wThetaErr > 2000) && (struAppCommData.wThetaErr < 18000))
{
// 误差在范围内,增加匹配计数器
struAppCommData.nMatchCnt++;
}
else
{
// 误差超出范围,重置匹配计数器
struAppCommData.nMatchCnt = 0;
}
2.3.2 完整代码
南京的凌欧的代码框架在强拖前有预定位的配置,但是并没有在改函数内实现。
/*****************************************************************************
* 函数名 : void StateOpen(void)
* 说明 : 电机强拖程序
* 设计思路 :1.给定电机强拖频率、强拖电流、强拖加速度,使电机从静止开始拖动到设定的强拖频率。 \
2.当电机达到设定频率,且估算角度和强拖角度误差在(10.98°~ 98.87°)之内则判断为估算 \
已经跟随到强拖角度,连续5次则从开环切换到闭环。 \
3.由于强拖给定的是Id,所以在程序留了从Id-->Iq电流的切换时间,切换时间的长短依照实际电机负载\
来调整。(总的原则,负载轻则切换时间短;负载重则切换时间长,对于灯扇类应用则可以适当延长,\
等扇叶完全展开后再切入速度闭环。 \
4.开环状态用来测试电机极对数,计算公式:极对数N = 60f/Speed ,其中f为电机电频率,Speed为电机\
机械转速。
* 参数 :无
* 返回值 :无
* 修改时间 :2020.08.17
*****************************************************************************/
void StateOpen(void)
{
// 检查是否处于开环强拖状态(bOpenRunFlag=0表示未启动强拖)
if (struAppCommData.bOpenRunFlag == 0)
{
// 根据电机运行方向设置强拖频率方向
if (struFOC_CtrProc.bMotorDirtionCtrl == CW) // 顺时针方向
{
// 设置正向强拖目标频率(最小服务频率)
mOpenForceRun.wOpen2CloseFreq = struAppCommData.wSvcMinFreq;
}
else // 逆时针方向
{
// 设置负向强拖目标频率
mOpenForceRun.wOpen2CloseFreq = -struAppCommData.wSvcMinFreq;
}
// 设置强拖频率的加速步长(将用户单位转换为应用单位)
mOpenForceRun.nFreqAccStep = User2AppFreqTrans(FREQ_ACC);
// 设置强拖频率的减速步长(将用户单位转换为应用单位)
mOpenForceRun.nFreqDecStep = User2AppFreqTrans(FREQ_DEC);
// 执行频率爬坡生成(生成线性变化的频率参考值)
SpeedReferenceGen(&mOpenForceRun);
// 获取当前频率设定值(右移7位进行缩放)
struMotorSpeed.wSpeedSet = mOpenForceRun.wRampFreqRef >> 7;
// 将频率参考值转换为核心控制层单位
struMotorSpeed.wSpeedRef = App2CoreFreqTrans(struMotorSpeed.wSpeedSet);
// 检查是否达到设定的强拖目标频率
if (struMotorSpeed.wSpeedSet == mOpenForceRun.wOpen2CloseFreq)
{
// 计算观测器估算角度与开环强拖角度的误差
struAppCommData.wThetaErr = struFluxOB_Param.wElectAngleEst - struFluxOB_Param.wElectAngleOpen;
// 取误差绝对值并右移16位(Q16格式转换为实际值)
struAppCommData.wThetaErr = ABS(struAppCommData.wThetaErr) >> 16;
// 检查角度误差是否在合理范围内(2000~18000对应10.98°~98.87°)
if ((struAppCommData.wThetaErr > 2000) && (struAppCommData.wThetaErr < 18000))
{
// 误差在范围内,增加匹配计数器
struAppCommData.nMatchCnt++;
}
else
{
// 误差超出范围,重置匹配计数器
struAppCommData.nMatchCnt = 0;
}
// 检查是否连续匹配次数超过阈值(默认5次)
if (struAppCommData.nMatchCnt > MATCH_TIME)
{
// 根据系统配置决定是否切换到闭环
#if (OPEN_RUN_STATUS == TRUE)
{
// 配置为仅开环运行,不执行任何操作
}
#elif (OPEN_RUN_STATUS == FALSE)
{
// 切换到闭环运行模式
struFOC_CtrProc.eSysState = RUN; // 设置系统状态为运行模式
struAppCommData.bOpenRunFlag = 1; // 标记开环强拖已完成
// 根据电机方向设置Q轴电流(转矩电流)
if (struFOC_CtrProc.bMotorDirtionCtrl == CW) // 顺时针方向
{
// 设置正向Q轴电流(经过两级单位转换)
struFOC_CurrLoop.nQCurrentSet = App2CoreCurTrans(User2AppCurTrans(U_START_CUR_SET_S));
}
else if (struFOC_CtrProc.bMotorDirtionCtrl == CCW) // 逆时针方向
{
// 设置负向Q轴电流(经过两级单位转换)
struFOC_CurrLoop.nQCurrentSet = -App2CoreCurTrans(User2AppCurTrans(U_START_CUR_SET_S));
}
// 设置D轴电流(励磁电流)
// 注:开环阶段使用Id强拖,闭环后Id逐渐降为0,Iq逐渐增加
struFOC_CurrLoop.nDCurrentSet = App2CoreCurTrans(User2AppCurTrans(U_START_CUR_SET_F));
// 设置电流环的加速斜率(经过两级单位转换)
struAppCommData.nCurrentACC = App2CoreCurTrans(User2AppCurTrans(OPEN2CLOSE_RUN_CURRENT_RAMP));
// 设置电流环的减速斜率(经过两级单位转换)
struAppCommData.nCurrentDEC = App2CoreCurTrans(User2AppCurTrans(OPEN2CLOSE_RUN_CURRENT_RAMP));
// 更新D轴电流环参数
CurrentLoopAxisD_Set();
// 更新Q轴电流环参数
CurrentLoopAxisQ_Set();
}
#endif
}
}
}
}

1395

被折叠的 条评论
为什么被折叠?



