学习记录:
参考野火的电机开发,最近尝试了一下直流电机的驱动。电机的型号是https://item.taobao.com/item.htm?spm=a1z2k.11010449.931864.2.362e509dj90qpJ&scm=1007.13982.82927.0&id=45347924687&last_time=1616899617
驱动电路是最常见的L298n,单路最大2A输出。
学习内容:
PID调试准备
1、编码器使用(获取转速)
2、 PWM电机驱动
3、 PID移植
4、 上位机调试
编码器使用:
这里使用的是霍尔编码器,1:30减速比,编码器线数13ppr。相当于电机一圈有390个信号出现。
编码器原理比较简单,就是比较常见的正交解码。通过两个方波信号进行判断正反转和速度。正反通过相位差判断(90度还是270度),速度通过频率判断,这里我们使用STM32的正交解码功能。每一次经过编码器就会有四个边沿变化。
可以看到定时器上面的TF1和TF2的编码器接口,然后我们要进行四分频,否则得到的计数值为四倍。当然也可以发现他会使用CNT计数器进行计数,这也代表我们的PWM就不能输出。意思是为了一个编码,这个定时器基本就没有其他可以用的了。所以不要希望还能有PWM输出了。
首先cube配置只展示关键部分
主要是四分频,选择两个通道,上升沿触发,分频4-1,滤波器1-15随便选,现在没看到。
之后进行启动旧可以了。
HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL);//开始编码器器
__HAL_TIM_GET_COUNTER(&htim4)//获取计数值
如果每有一次信号,CNT就会加一或者减一,当然有人会发现CNT16位,意味着溢出是很有可能的。因此处理溢出很重要。这里有两种方法可以实现。
首先是野火的处理方式,这个是最完善的,可以处理多圈溢出。
//使用定时器溢出来获取,这种是最完善的
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* 判断当前计数器计数方向 */
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle))
/* 下溢 */
Encoder_Overflow_Count--;
else
/* 上溢 */
Encoder_Overflow_Count++;
}
这是我的处理方式,比较简单粗暴,通过计数值来判断正反转,考虑到我的电机转速上限不高,并且我获取速度还挺快的,所以使用了这个。
//这个是我实现的方法,缺点是转速不能过快,因为我这个算法溢出不能多次,当然经我实测,一般不会出现,所以我采用了。
int32_t get_speed(void)
{
int32_t temp;
static uint16_t last_cnt;
uint16_t now_cnt=__HAL_TIM_GET_COUNTER(&htim4);
//判断计数变多还是变少
if(now_cnt>last_cnt)
{
//判断是溢出导致还是正常情况
if(last_cnt<10000 && now_cnt>55535)
{//溢出了导致的
temp=0xffff-now_cnt+last_cnt;
last_cnt=now_cnt;
return temp;
}
else
{
temp=last_cnt-now_cnt;
last_cnt=now_cnt;
return temp;
}
}
else
{
//判断是溢出导致还是正常情况
if(now_cnt<10000 && last_cnt>55535)
{//溢出了导致的
temp= 0xffff-now_cnt+last_cnt;
last_cnt=now_cnt;
return temp;
}
else
{
temp= last_cnt-now_cnt;
last_cnt=now_cnt;
return temp;
}
}
}
到此为止,计数就完成了。
PWM电机驱动:
电机驱动是H桥电路,原理不细讲。这里是L298N的驱动方法。比较简单,我们只要对两个信号进行PWM输出就可以了。
void set_pwm(float period)
{
//我的周期的999,所以上限制也是999
if(period>0)
{
uint16_t temp=period;
if(temp>999)
temp=999;
TIM3->CCR1=temp;
TIM3->CCR2=0;
}
else
{
uint16_t temp=-1*period;
if(temp>999)
temp=999;
TIM3->CCR1=0;
TIM3->CCR2=temp;
}
/**TIM3 GPIO Configuration
PA6 ------> TIM3_CH1
PA7 ------> TIM3_CH2
PB0 ------> TIM3_CH3
PB1 ------> TIM3_CH4
*/
}
PID移植:
移植野火的PID算法。
typedef struct
{
float target_val; //目标值
float actual_val; //实际值
float err; //定义偏差值
float err_last; //定义上一个偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
float integral; //定义积分值
}_pid1;
/*pid*/
typedef struct
{
float target_val; //目标值
float actual_val; //实际值
float err; //定义当前偏差值
float err_next; //定义下一个偏差值
float err_last; //定义最后一个偏差值
float Kp, Ki, Kd; //定义比例、积分、微分系数
}_pid2;
//位置PID
void PID1_param_init(_pid1* pid)
{
/* 初始化参数 */
pid->target_val=0.0;
pid->actual_val=0.0;
pid->err=0.0;
pid->err_last=0.0;
pid->integral=0.0;
pid->Kp = 0.31;
pid->Ki = 0.070;
pid->Kd = 0.3;
}
float PID1_realize(float temp_val,_pid1* pid)
{
/* 计算目标值与实际值的误差 */
pid->err=pid->target_val-temp_val;
/* 误差累积 */
pid->integral+=pid->err;
/*PID 算法实现 */
pid->actual_val=pid->Kp*pid->err+pid->Ki*pid->integral+pid->Kd*(pid->err-pid->err_last);
/* 误差传递 */
pid->err_last=pid->err;
/* 返回当前实际值 */
return pid->actual_val;
}
//增量PID
void PID2_param_init(_pid2* pid)
{
/* 初始化参数 */
pid->target_val=0.0;
pid->actual_val=0.0;
pid->err = 0.0;
pid->err_last = 0.0;
pid->err_next = 0.0;
pid->Kp = 0.21;
pid->Ki = 0.80;
pid->Kd = 0.01;
}
float PID2_realize(float temp_val,_pid2* pid)
{
/* 计算目标值与实际值的误差 */
pid->err=pid->target_val-temp_val;
/*PID 算法实现 */
float increment_val = pid->Kp*(pid->err - pid->err_next) + pid->Ki*pid->err+ pid->Kd*(pid->err - 2 * pid->err_next + pid->err_last);
/* 累加 */
pid->actual_val += increment_val;
/* 传递误差 */
pid->err_last = pid->err_next;
pid->err_next = pid->err;
/* 返回当前实际值 */
return pid->actual_val;
}
放到定时器进行循环计算输出。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{//50ms的定时器中断
if (htim->Instance==TIM6)
{
float speed=1.0*get_speed();
set_pwm(10*PID2_realize(speed,&pid2[0]));
set_computer_channel(0,speed);
set_computer_channel(1,pid2[0].target_val);
set_computer_channel(2,pid2[0].actual_val*10.0);
}
}
上位机调试:
这里使用现成的PID调试软件,有三个。野火的PID软件上位机莫名其妙丢帧,不知道谁可以帮我解决。无名的协议太复杂,移植费劲。这里选择的VOFA+软件,可以自己编写命令,非常好用啊!
首先发送P I D直接发送字母,然后的第二个字节代表PID通道,后面四个字节浮点数代表实际的数据。然后接入滑动条。
这样调参就完成了。
//
void set_computer_channel(uint8_t pid_channel,float data)
{
float temp=data;
if(s_tx_buf[4*channel_num+3] !=0x7f)
{
s_tx_buf[4*channel_num]=0x00;
s_tx_buf[4*channel_num+1]=0x00;
s_tx_buf[4*channel_num+2]=0x80;
s_tx_buf[4*channel_num+3]=0x7f;
}
memcpy(s_tx_buf+4*pid_channel,&temp,4);
HAL_UART_Transmit_DMA(&huart1,s_tx_buf,sizeof(s_tx_buf));
}
void set_pid1(_pid1* pid,uint8_t data_type,float data)
{
switch (data_type)
{
case 'P':pid->Kp=data;break;
case 'I':pid->Ki=data;break;
case 'D':pid->Kd=data;break;
case 'T':pid->target_val=data;break;
}
}
void set_pid2(_pid2* pid,uint8_t data_type,float data)
{
switch (data_type)
{
case 'P':pid->Kp=data;break;
case 'I':pid->Ki=data;break;
case 'D':pid->Kd=data;break;
case 'T':pid->target_val=data;break;
}
}
void get_computer_channel(uint8_t* data,uint8_t len)
{
pid_channel_data* p= (pid_channel_data*)data;
switch ((*p).header)
{
case 'P':set_pid2(&pid2[(*p).channel],'P',(*p).data);break;
case 'I':set_pid2(&pid2[(*p).channel],'I',(*p).data);break;
case 'D':set_pid2(&pid2[(*p).channel],'D',(*p).data);break;
case 'T':set_pid2(&pid2[(*p).channel],'T',(*p).data);break;
}
}
效果如图,因为空载,所以PID很容易调个差不多。