直流有刷电机PID控制及STM32配置
直流有刷电机PID控制及STM32配置
一、编码器读取
1. STM32编码器模式配置
想要实现电机的位置控制,首先要准确读出编码器的值,增量编码器有AB两相,AB相永远有90度的相位差,波形图如图所示:
以上升沿为一个周期的起点,如果B相比A相快90度对应的应该是逆时针旋转,此时编码器向下计数;如果A相比B相快90度对应的应该是顺时针旋转,此时编码器向上计数。
常规的方法,我们只测量 A 相(或 B 相)的上升 沿或者下降沿,也就是上图中对应的数字 1234 中的某一个,这样就只能计数 3次。而四倍频的方法是测量 A 相和 B 相编码器的上升沿和下降沿。这样在同样的 时间内,可以计数 12 次(3 个 1234 的循环)。这就是软件四倍频的原理。 STM32编码器配置如图所示:
Encoder Mode设置成TI1 and TI2对应的是四倍频模式。
生成的初始化代码如下:
// 初始化配置
void MX_TIM1_Init(void)
{
TIM_Encoder_InitTypeDef sConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim1.Instance = TIM1;
htim1.Init.Prescaler = 0;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htim1.Init.Period = 40000; // 重载值
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频系数
htim1.Init.RepetitionCounter = 0; // 初始的计数
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 自动重载
sConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 编码器模式(4倍频)
sConfig.IC1Polarity = TIM_ICPOLARITY_FALLING;
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 10; // 滤波
sConfig.IC2Polarity = TIM_ICPOLARITY_FALLING;
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sConfig.IC2Filter = 10; // 滤波
if (HAL_TIM_Encoder_Init(&htim1, &sConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
}
// 引脚配置
void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef* tim_encoderHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(tim_encoderHandle->Instance==TIM1)
{
__HAL_RCC_TIM1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
以上代码仅适用于stm32f103c8t6的TIM1中断,不同的MCU以及不同的中断通道配置方法与生成的代码均不同。
2. 速度与位置的计算
速度计算:
由于硬件编码器的回调函数返回的结果只能是无符号整型,重载值是40000就意味着编码器寄存器读数的范围是0 ~ 40000。在计数为0时向下计数,就会从40000开始计数。以下代码获取的是单位时间内编码器的读数,考虑了无符号整型不能为负值的情况:
int Read_Velocity()
{
int velocity;
uint32_t encoder = __HAL_TIM_GET_COUNTER(&htim1); // 读取编码器寄存器,无符号整型变量
if (encoder <= ENCODER_PERIOD/2) // 如果寄存器值 < 重载值的一半,说明编码器正在向上计数
{
velocity = encoder;
} else { // 如果寄存器值 > 重载值的一半,说明编码器正在向下计数
velocity = encoder - ENCODER_PERIOD;
}
__HAL_TIM_SET_COUNTER(&htim1, 0); // 编码器寄存器清零
return velocity / 4; // 因为是4倍频所以要 / 4
}
位置计算:
速度的积分就是位置,差分代替微分,累加代替积分,代码如下:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim4)
{
Encoder = Read_Velocity(); // 速度
Position+=Encoder; // 位置
}
/** PID及pwm输出 */
}
速度和位置计算是在执行PID算法的时刻执行的,而PID算法需要以固定频率执行,所以此时计算的速度和位置也是精确的。
二、PID控制原理与位置模式代码实现
1. 单级PID
信号框图:
PID的运行过程:
1. 为系统指定一个目标值
1. PID将目标值与被控对象当期的反馈量作差得到误差
1. PID将误差值分别经过三个环节计算得到输出分量,三个分量加起来得到PID的输出
1. 将PID的输出施加到被控对象上,使反馈量向目标值靠拢
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计算
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算法是以固定频率执行的,单位时间长度可以忽略不记,可以用差分代替微分,累加代替积分。
2. 串级PID
在进行电机位置控制时,经常发现一个问题,如果实际位置与目标位置之家差距太大,电机在转动时速度会很快,停止时会导致较大震荡,而且无论怎样修改参数都很难让系统表现更好一些,而串级PID可以改善这一点,信号框图如下:
位置环首先通过目标位置和位置反馈计算目标速度,速度环再根据目标速度和速度反馈计算输出扭矩。
代码实现很简单,调用两次单级PID就行,完整代码如下:
/**
* @brief
*
* @param pid
* @param outerRef 外环目标值(位置)
* @param outerFdb 外环反馈值 (位置)
* @param innerFdb 内环反馈值 (速度)
*/
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输出
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim4)
{
Encoder = Read_Velocity();
Position+=Encoder;
PID_CascadeCalc(&mypid, Target_Position, Position, Encoder); // 串级PID计算
if (mypid.output < 0) // 处理正反转
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_SET);
} else if (mypid.output >0)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);
} else
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);
}
set_pwm_param(htim3, myabs(mypid.output)); // 输出PWM波
}
}
三、速度模式与力矩模式
1. 速度模式
速度模式可以在位置模式的基础上去掉最外面的位置环,同时把速度环的输入值设定一个具体的目标速度,这样输出的扭矩就会永远使被控对象的实际速度永远趋向于目标速度,其信号框图如下:
代码如下:
/**
* @brief
*
* @param pid
* @param outerRef 外环目标值(位置)
* @param outerFdb 外环反馈值 (位置)
* @param innerFdb 内环反馈值 (速度)
*/
void PID_CascadeCalc(CascadePID *pid, float outerRef, float outerFdb, float innerFdb)
{
PID_Calc(&pid->inner, 100, innerFdb); // 100为目标速度
pid->output = 0;
}
由于没有使用位置环,pid参数只初始化速度环即可。
2. 力矩模式
力矩模式的要求是:先设定一个最大力矩,当外部施加作用力小于最大力矩时电机应保持静止;当外部施加作用力超过最大力矩时电机转动的阻力应该等于最大力矩。其状态转换如下:
代码如下:
/**
* @brief
*
* @param pid
* @param outerRef 外环目标值(位置)
* @param outerFdb 外环反馈值 (位置)
* @param innerFdb 内环反馈值 (速度)
*/
void PID_CascadeCalc(CascadePID *pid, float outerRef, float outerFdb, float innerFdb)
{
// 力矩模式
int foce = Foce;
if (myabs(pid->output) < foce) // 如果外部力没有达到最大扭矩就采用位置模式
{
PID_Calc(&pid->outer, outerRef, outerFdb); // 计算位置环
PID_Calc(&pid->inner, pid->outer.output, innerFdb); // 计算速度环
pid->output = pid->inner.output; // 内环输出就是串级PID输出
} else if (pid->output >= foce && Encoder <= 0) // 如果外部力达到最大力矩就直接输出最大力矩,区分正反转
{
pid->output = foce;
Target_Position = Position;
} else if (pid->output <= -foce && Encoder >= 0)
{
pid->output = -foce;
Target_Position = Position;
} else
{
pid->output = 0;
}
}
电机内部并没有力传感器,当编码器读数开始反向增加时(肉眼无法察觉)则说明外部力已经不足以抗衡当前力矩,此时应该泄力并转换成位置模式固定当前位置。