• 编码器数值的获取及其数值实际意义:电机转速=编码器读数*当前频率/电机减速比/编码器精度/倍频数 (r/s)。
  • 闭环控制的意义:有反馈的控制。速度闭环控制的过程:根据当前速度反馈,调整 PWM 值。

速度环 PID 控制原理

速度环 PID 控制的原理比较简单,只需要把 PID 控制流程中的控制对象换成电机速度就可以了。

我们先设置目标转速,系统会计算出偏差 e,然后将偏差输入到 PID 控制的三个环节中,PID 计算后的输出值用于控制 PWM 的占空比,进而控制电机的速度。

电机接口连接

  • 其中 2、3、4、5 号接口为电机编码器接口,用于读取电机转速,2、5 号引脚给编码器供电,3、4 号引脚接单片机PB6(TIM4CH1)、PB7(TIM4CH2)引脚。速度闭环控制即有速度反馈的控制,由编码器提供速度反馈。
  • 将反馈速度(当前速度_V_Now )与目标速度VT_arget 进行比较,_VTarget 大于VNow 则加大速度,VTarget 与_V_Now 之差越大加速度则越大。即设定VTarget 后,速度闭环控制使电机根据VTarget 与_V_Now 的关系自动加速使电机达到VT_arget ,同时达到_VTarget 后,如果由于外界原因使VNow 变化偏离VTarget ,电机也会自动调整回到VTarget 。

编码器

编码器是将信号(如比特流)或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。编码器把角位移或直线位移转换成电信号,前者称为码盘,后者称为码尺。按照读出方式编码器可以分为接触式和非接触式两种;按照工作原理编码器可分为增量式和绝对式两类。

增量式编码器是将位移转换成周期性的电信号,再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。绝对式编码器的每一个位置对应一个确定的数字码,因此它的示值只与测量的起始和终止位置有关,而与测量的中间过程无关。

按照原理一般可分为:

  • 光电编码器(光学式)
  • 触点电刷式
  • 霍尔编码器(磁式)

编码器的工作原理(正交式)

编码器能够将电机的机械几何位移转化为脉冲信号或数字量。本实验采用的编码器为增量式编码器,增量式编码器通常有两个输出信号,分别为A相和B相。电机带动霍尔码盘转动,在码盘的结构位上将电机在转动时会产生A、B两相的脉冲信号,且这两路脉冲信号的相位差为90度(即正交)配置定时器进行捕获计数,测得脉冲频率,再根据脉冲序列的频率确定电机的转速。同时,在此过程中,A,B相位触发的先后顺序可以确定转动的正反方向。

编码器电机的配置

  • M1与M2,高/低电平决定电机转动的方向(测试电机:直接向其接入12V以下电源,另一端接地,反之反转)

  • GND——接地 VCC——接电源3.3V

  • C1——霍尔编码器A相位 C2——霍尔编码器B相位

  • 霍尔传感器编码器的测速模块,配有 11 线强磁码盘,A B 双相输出 共同利用下,通过计算可得出车轮转一圈时,脉冲数可达30_11_2=660个,单相也可以达到 330 个.(减速比为30:1)

    关于编码器的工作模式

  • 模式1,即计数器仅在TI1的边沿处计数;

  • 模式2,即计数器仅在TI2的边沿处计数;

  • 模式3,即定时器在TI1双边沿处计数(具体请参考stm32f1系列中文参考手册)

实验电机编译器计算

速度闭环控制需要速度反馈,所以我们速度开环控制的程序基础上添加编码器库函数。同时添加一个定时 10ms 读取编码器数值的函数(使用定时器 TIM2)。

编码器读数与电机转速的关系:

10ms 读取一次编码器(即 100HZ)。我们提供的电机减速比为 20,电机自带的霍尔编码器精度为 13,AB 双相组合得到 4 倍频,则转 1 圈编码器读数为

20 _ 13 _ 4 = 1040。

电机转速=编码器读数 * 100 / 1040 (r/s)。由于电机本身误差,转 1圈编码器读数可能不等于 1040,这时可以添加误差系数。

代码解析

使用外部变量,用于定时读取编码器读数

//外部变量 extern 说明改变量已在其它文件定义
extern int Encoder; //当前速度
extern int TargetVelocity, Encoder,PWM; //目标速度、编码器读数、PWM 控制变量
extern float Velcity_Kp, Velcity_Ki, Velcity_Kd; //相关速度 PID 参数
extern int MortorRun; //允许电机控制标志位

定义相关结构体

GPIO_InitTypeDef GPIO_InitStructure; //定义一个引脚初始化的结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//定义一个定时器初始化的结构体
TIM_ICInitTypeDef TIM_ICInitStructure; //定义一个定时器编码器模式初始化的结构体

使能相关时钟

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //使能 TIM4 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能 CPIOB 时钟

初始化相关 GPIO

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //TIM4_CH1、TIM4_CH2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据 GPIO_InitStructure 的参数初始化 GPIO

初始化定时器 TIM4

TIM_TimeBaseStructure.TIM_Period = 0xffff; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //选择时钟分频:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据
TIM_TimeBaseInitStruct 的参数初始化定时器 TIM4

初始化编码器

ICPolarity_Rising, TIM_ICPolarity_Rising); //使用编码器模式 3:CH1、CH2 同时计数,为四分频
TIM_ICStructInit(&TIM_ICInitStructure); //把 TIM_ICInitStruct 中的每一个参数按缺省值填入
TIM_ICInitStructure.TIM_ICFilter = 10; //设置滤波器长度
TIM_ICInit(TIM4, &TIM_ICInitStructure); //根 TIM_ICInitStructure 参数初始化定时器 TIM4 编码器模式

清除相关标志位、数值与相关使能

TIM_ClearFlag(TIM4, TIM_FLAG_Update);//清除 TIM 的更新标志位
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //更新中断使能
TIM_SetCounter(TIM4,0); //初始化清空编码器数值
TIM_Cmd(TIM4, ENABLE); //使能定时器 4

实验操作

在第 ⑦节速度开环控制程序的基础上添加一下程序,同时把电机控制程序转移到 TIM2 中断服务函数

  • 编码器初始化函数使能 GPIO、TIM 时钟→初始化 GPIO 引脚→初始化定时器→初始化编码器→清除相关标志位、数值→使能相关功能、定时器。
  • 读取编码器计数值函数读取编码器计数并判断方向正反,读取完后将计数值清零。
  • 定时读取编码器数值初始化函数使能TIM2时钟→初始化定时器→使能中断→初始化中断分组→使能定时器。
  • TIM2 中断服务函数调用读取编码器计数值函数并进行速度闭环控制。
  • 主函数(主函数与第 1 节不同)调用相关初始化函数,在 while 循环中进行检测按键,以切换电机使能/失能状态。实验效果为:接通电源打开电源开关后,连接电机和控制器,按下用户按键,电机将进行速度闭环控制,再次按下按键电机停止。同时 OLED 显示屏会显示目标速度值(Target_V)、读取速度值(Current_V)和当前 PWM 值。

PID调参

  • 速度环p、i、d各值先赋为0,先调p值(比例系数),整个过程只调p、i的值即可。将编码器获取到的脉冲数encoder_counter(实际值)和目标值Target_Velocity打印到vofa中去,观察encoder_counter值靠近Target_Velocity的趋势,响应太慢则加大p值,比例系数是构建输入与输出的线性关系的。
  • 当encoder_counter值靠近但不超过Target_Velocity时,p值就差不多调好了,此时通过调大i值来使encoder_counter值更加靠近Target_Velocity,i值一般以0.001为单位来加。

Code

tb6612.c与tb6612.h 第⑦课一样

encoder.c

#include "motorencoder.h" 

//外部变量 extern说明改变量已在其它文件定义
extern int   Encoder; //当前速度
extern int   TargetVelocity, Encoder,PWM; //目标速度、编码器读数、PWM控制变量
extern float Velcity_Kp,  Velcity_Ki,  Velcity_Kd; //相关速度PID参数
extern int   MortorRun;  //允许电机控制标志位

/**************************************************************************
函数功能:编码器初始化函数
入口参数:无
返回  值:无
**************************************************************************/
void MotorEncoder_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure; //定义一个引脚初始化的结构体  
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//定义一个定时器初始化的结构体
  TIM_ICInitTypeDef TIM_ICInitStructure; //定义一个定时器编码器模式初始化的结构体

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //使能TIM4时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能CPIOB时钟

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;    //TIM4_CH1、TIM4_CH2
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
    GPIO_Init(GPIOB, &GPIO_InitStructure);    //根据GPIO_InitStructure的参数初始化GPIO

    TIM_TimeBaseStructure.TIM_Period = 0xffff; //设定计数器自动重装值
    TIM_TimeBaseStructure.TIM_Prescaler = 0; // 预分频器 
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //选择时钟分频:不分频
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct的参数初始化定时器TIM4

  TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); //使用编码器模式3:CH1、CH2同时计数,为四分频
  TIM_ICStructInit(&TIM_ICInitStructure); //把TIM_ICInitStruct 中的每一个参数按缺省值填入
    TIM_ICInitStructure.TIM_ICFilter = 10;  //设置滤波器长度
  TIM_ICInit(TIM4, &TIM_ICInitStructure); //根TIM_ICInitStructure参数初始化定时器TIM4编码器模式

    TIM_ClearFlag(TIM4, TIM_FLAG_Update);//清除TIM的更新标志位
  TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //更新中断使能
  TIM_SetCounter(TIM4,0); //初始化清空编码器数值

    TIM_Cmd(TIM4, ENABLE); //使能定时器4
}

/**************************************************************************
函数功能:读取TIM4编码器数值
入口参数:无
返回  值:无
**************************************************************************/
int Read_Encoder(void)
{
    int Encoder_TIM;
    Encoder_TIM=TIM4->CNT; //读取计数
    if(Encoder_TIM>0xefff)Encoder_TIM=Encoder_TIM-0xffff; //转化计数值为有方向的值,大于0正转,小于0反转。
                                                          //TIM4->CNT范围为0-0xffff,初值为0。
    TIM4->CNT=0; //读取完后计数清零
    return Encoder_TIM; //返回值
}

/**************************************************************************
函数功能:TIM4中断服务函数
入口参数:无
返回  值:无
**************************************************************************/
void TIM4_IRQHandler(void)
{                                       
    if(TIM4->SR&0X0001)//溢出中断
    {                                                        
    }                   
    TIM4->SR&=~(1<<0);//清除中断标志位         
}

/**************************************************************************
函数功能:通用定时器2初始化函数,
入口参数:自动重装载值 预分频系数 默认定时时钟为72MHZ时,两者共同决定定时中断时间
返回  值:无
**************************************************************************/
void EncoderRead_TIM2(u16 arr, u16 psc)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrue; //定义一个定时中断的结构体
    NVIC_InitTypeDef NVIC_InitStrue; //定义一个中断优先级初始化的结构体

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能通用定时器2时钟

    TIM_TimeBaseInitStrue.TIM_Period=arr; //计数模式为向上计数时,定时器从0开始计数,计数超过到arr时触发定时中断服务函数
    TIM_TimeBaseInitStrue.TIM_Prescaler=psc; //预分频系数,决定每一个计数的时长
    TIM_TimeBaseInitStrue.TIM_CounterMode=TIM_CounterMode_Up; //计数模式:向上计数
    TIM_TimeBaseInitStrue.TIM_ClockDivision=TIM_CKD_DIV1; //一般不使用,默认TIM_CKD_DIV1
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStrue); //根据TIM_TimeBaseInitStrue的参数初始化定时器TIM2

    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //使能TIM2中断,中断模式为更新中断:TIM_IT_Update

    NVIC_InitStrue.NVIC_IRQChannel=TIM2_IRQn; //属于TIM2中断
    NVIC_InitStrue.NVIC_IRQChannelCmd=ENABLE; //中断使能
    NVIC_InitStrue.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级为1级,值越小优先级越高,0级优先级最高
    NVIC_InitStrue.NVIC_IRQChannelSubPriority=1; //响应优先级为1级,值越小优先级越高,0级优先级最高
    NVIC_Init(&NVIC_InitStrue); //根据NVIC_InitStrue的参数初始化VIC寄存器,设置TIM2中断

    TIM_Cmd(TIM2, ENABLE); //使能定时器TIM2
}

/**************************************************************************
函数功能:TIM2中断服务函数 定时读取编码器数值并进行速度闭环控制 10ms进入一次
入口参数:无
返回  值:无
**************************************************************************/
void TIM2_IRQHandler()
{
  if(TIM_GetITStatus(TIM2, TIM_IT_Update)==1) //当发生中断时状态寄存器(TIMx_SR)的bit0会被硬件置1
    {
      Encoder=Read_Encoder();   //读取当前编码器读数,即速度

        if(MortorRun) //如果按键按下,运行电机控制程序
            {
                PWM=Velocity_FeedbackControl(TargetVelocity, Encoder); //速度环闭环控制

                SetPWM(PWM); //设置PWM
            }
          else PWM=0,SetPWM(PWM); //如果按键再次按下,电机停止

        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //状态寄存器(TIMx_SR)的bit0置0
    }
}

    return ControlVelocity; //返回速度控制值
}

根据增量式离散PID公式

ControlVelocity+=Kp[e(k)-e(k-1)]+Ki*e(k)+Kd[e(k)-2e(k-1)+e(k-2)]

  • e(k)代表本次偏差

  • e(k-1)代表上一次的偏差 以此类推

  • ControlVelocity代表增量输出

  • 在我们的速度控制闭环系统里面,只使用PI控制

    ControlVelocity+=Kp[e(k)-e(k-1)]+Ki*e(k)

/**************************************************************************
函数功能:速度闭环PID控制(实际为PI控制)
入口参数:目标速度 当前速度
返回  值:速度控制值
**************************************************************************/
int Velocity_FeedbackControl(int TargetVelocity, int CurrentVelocity)
{
        int Bias;  //定义相关变量
        static int ControlVelocity, Last_bias; //静态变量,函数调用结束后其值依然存在

        Bias=TargetVelocity-CurrentVelocity; //求速度偏差

        ControlVelocity+=Velcity_Kp*(Bias-Last_bias)+Velcity_Ki*Bias;  //增量式PI控制器
       //Velcity_Kp*(Bias-Last_bias) 作用为限制加速度
        //Velcity_Ki*Bias             速度控制值由Bias不断积分得到 偏差越大加速度越大
        Last_bias=Bias;    
        return ControlVelocity; //返回速度控制值
}

encoder.h

#ifndef __ENCODER_H 
#define __ENCODER_H 
#include "sys.h"
#include "TB6612.h"

void MotorEncoder_Init(void); 
int Read_Encoder(void);
void EncoderRead_TIM2(u16 arr, u16 psc);
int Velocity_FeedbackControl(int TargetVelocity, int CurrentVelocity);

#endif

main.c

#include "delay.h"
#include "usart.h"            
#include "TB6612.h"
#include "motorencoder.h"
#include "usart.h"
#include "led.h"
#include "oled.h"
#include "key.h"

int   TargetVelocity=50, Encoder, PWM;  //目标速度、编码器读数、PWM控制变量
float Velcity_Kp=20,  Velcity_Ki=5,  Velcity_Kd; //相关速度PID参数
int   MortorRun;  //允许电机控制标志位

/**************************************************************************
函数功能:OLED显示屏显示内容
入口参数:无
返回  值:无
**************************************************************************/
void Oled_Show(void)
{  
        OLED_Refresh_Gram(); //刷新显示屏
        OLED_ShowString(00,00,"VelocityFeedback"); //速度闭环控制

        //显示目标速度,分正负
        OLED_ShowString(00,10,"Target_V :");         
        if(TargetVelocity>=0)
        {
            OLED_ShowString(80,10,"+");
            OLED_ShowNumber(90,10,TargetVelocity,5,12);
        }
        else
        {
            OLED_ShowString(80,10,"-");
            OLED_ShowNumber(90,10,-TargetVelocity,5,12);
        }

        //显示当前速度,即编码器读数,分正负
        OLED_ShowString(00,20,"Current_V:");         
        if(Encoder>=0)
        {
            OLED_ShowString(80,20,"+");
            OLED_ShowNumber(90,20,Encoder,5,12);
        }
        else
        {
            OLED_ShowString(80,20,"-");
            OLED_ShowNumber(90,20,-Encoder,5,12);
        }

        //显示速度控制值,即PWM,分正负
        OLED_ShowString(00,30,"PWM      :");         
        if(PWM>=0)
        {
            OLED_ShowString(80,30,"+");
            OLED_ShowNumber(90,30,PWM,5,12);
        }
        else
        {
            OLED_ShowString(80,30,"-");
            OLED_ShowNumber(90,30,-PWM,5,12);
        }
}

/**************************************************************************
函数功能:主函数
入口参数:无
返回  值:无
**************************************************************************/
int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组
    delay_init();                //延迟函数初始化
    JTAG_Set(JTAG_SWD_DISABLE);  //关闭JTAG才能开启OLED显示屏 
    LED_Init();                  //LED灯初始化
    OLED_Init();                 //OLED显示屏初始化
    uart_init(9600);           //串口初始化     
    MotorEncoder_Init();            //编码器初始化 使用定时器4
    TB6612_Init(7199, 0);        //电机驱动外设初始化 使用定时器3 
    EncoderRead_TIM2(7199, 99);  //10ms读取一次编码器(即100HZ),电机减速比为20,霍尔编码器精度13,AB双相组合得到4倍频,
                                  //则转1圈编码器读数为20*13*4=1040,电机转速=Encoder*100/1040r/s 使用定时器2
    delay_ms(2000);              //延迟等待初始化完成
    while(1)
    {        
        delay_ms(5);
        LED=0;    //LED灯闪烁
        if(KEY_Scan())MortorRun=!MortorRun; //按下按键MortorRun取反

        Oled_Show(); //OLED显示屏显示内容

        //串口打印目标速度、当前速度、转速            
        printf("TargetVelocity:%d\r\n",TargetVelocity);
        printf("Encoder:%d\r\n",Encoder);
        printf("转速:%.3fr/s\r\n", Encoder/1.04);
    }
}