stm32通过pid闭环控制小车速度

一: 概要

本章节主要为通过编码器,pid闭环控制电机转速,来实现所需的固定速度。需要学习pid原理的可以自行上网搜索,还有闭环控制的流程。
(注:免费源码链接,请点赞关注私信)
主控芯片为stm32f407系列,编码器规格为电机每转11个脉冲,轮子每转528个脉冲,定时器每20ms中断计算当前转速。

二:效果演示

lcd第一列显示为,当前速度。本程序设置的速度为120 r/min。

pid速度控制

三:程序源码

1:pid闭环控制

(1):pid.c

#include "./BSP/PID/pid.h"
#include "./SYSTEM/sys/sys.h"
#include "./BSP/LCD/lcd.h"
#include "./SYSTEM/usart/usart.h"

extern TIM_HandleTypeDef g_timx_pwm_chy_handle;     /* 定时器x句柄 */
extern TIM_HandleTypeDef g_timx_cnt_chy_handle;        /* 定时器x句柄 */

TIM_HandleTypeDef g_timx_handle;             /* 定时器x句柄 */

uint8_t Point=120;                    /* 设定目标转速  min/r  */
//#define SetPointL Point*528/3000      /* 转速转化为 20ms 编码器脉冲数量 */
uint16_t Moto;

PID zspeed,zpos;
Error value,vpos;

void PID_init(void)    /* 初始化 P I D */
{
    /* 速度PID  */
    zspeed.P=20;
    zspeed.I=2.5;
    zspeed.D=0;
    
    value.Current_Error=0;//当前误差
    value.Last_Error=0;//上一次误差
    value.Previous_Error=0;//上上次误差
    
    /* 位置PID */
    zpos.P=20;
    zpos.I=2.5;
    zpos.D=0;
    
    vpos.Current_Error=0;//当前误差
    vpos.Last_Error=0;//上一次误差
    vpos.Previous_Error=0;//上上次误差
    
}

/*! 
 *  @brief      增量式PID
 *  @since      v1.0
 *  *sptr :误差参数
 *  *pid:  PID参数
 *  NowPlace:实际值
 *  Point:   期望值
 */
// 增量式PID电机控制


uint32_t PID_Increase(Error *sptr, PID *pid, uint32_t NowPlace, uint32_t Point)
{
 
	uint32_t iError,	//当前误差
		Increase;	//最后得出的实际增量
 
	iError = Point - NowPlace;	// 计算当前误差
 
	Increase =  pid->P * (iError - sptr->Last_Error)   //比例P
			  + pid->I * iError      //积分I
			  + pid->D * (iError - 2 * sptr->Last_Error + sptr->Previous_Error);  //微分D
	
	sptr->Previous_Error = sptr->Last_Error;	// 更新前次误差
	sptr->Last_Error = iError;		  	// 更新上次误差
	
	return Increase;	// 返回增量
}

/*! 
 *  @brief      位置式PID
 *  @since      v1.0
 *  *sptr :误差参数
 *  *pid:  PID参数
 *  NowPlace:当前位置
 *  Point:   预期位置  
 */
 
// 位置式PID控制
float PID_Realize(Error *sptr,PID *pid, uint32_t NowPlace, float Point)
{
 
	uint32_t iError,	// 当前误差
		 Realize;   //实际输出	
 
	iError = Point - NowPlace;	// 计算当前误差
	sptr->Current_Error += pid->I * iError;	// 误差积分
      
    sptr->Current_Error = sptr->Current_Error > pid->limit?pid->limit:sptr->Current_Error;//积分限幅
      sptr->Current_Error = sptr->Current_Error <-pid->limit?-pid->limit:sptr->Current_Error;
	
    Realize = pid->P * iError       //比例P
            + sptr->Current_Error   //积分I
			+ pid->D * (iError - sptr->Last_Error);  //微分D
	sptr->Last_Error = iError;		  	// 更新上次误差
	return Realize;	// 返回实际值
}

//速度PID
void Speed_PID(uint8_t Point)
{
    uint32_t SetPointL=Point*528/3000;                   /* 转速转化为 20ms 编码器脉冲数量 */
    uint32_t count = 0,speed=0,Increase=0;
   
    count=__HAL_TIM_GET_COUNTER(&g_timx_cnt_chy_handle); /* 获取当前编码器脉冲计数值 */

    speed=(count)*3000/528;                              /* 计算当前的转速 */
    
    __HAL_TIM_SET_COUNTER(&g_timx_cnt_chy_handle, 0);    /* 计数器清零 */      
    lcd_show_num(50,20,speed,3,16,RED);

    Increase=PID_Increase( &value , &zspeed , count , SetPointL );  /* 计数得到增量式PID的增量数值 */
    lcd_show_num(50,60,Increase,3,16,RED);
    Moto+=Increase;

    if(Moto>=950) Moto=950;                                         /* 限制PWM占空比防止损坏电机 */
    lcd_show_num(50,40,Moto,3,16,RED);
    __HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, TIM_CHANNEL_1 ,Moto );/* 调节PWM使速度达到目标值 */
    
    printf("%d\r\n",speed);                                             /* 串口打印当前速度 */


}
/**
 * @brief       通用定时器TIMX定时中断初始化函数

 * @param       arr: 自动重装值
 * @param       psc: 预分频系数
 * @retval      无
 */
void gtim_timx_int_init(uint16_t arr, uint16_t psc)
{
    GTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */

    g_timx_handle.Instance = GTIM_TIMX_INT;                 /* 通用定时器x */
    g_timx_handle.Init.Prescaler = psc;                     /* 预分频系数 */
    g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;    /* 递增计数模式 */
    g_timx_handle.Init.Period = arr;                        /* 自动装载值 */
    HAL_TIM_Base_Init(&g_timx_handle);
    
    HAL_NVIC_SetPriority(GTIM_TIMX_INT_IRQn, 1, 2);         /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_EnableIRQ(GTIM_TIMX_INT_IRQn);                 /* 开启ITMx中断 */

    HAL_TIM_Base_Start_IT(&g_timx_handle);                  /* 使能定时器x和定时器x更新中断 */
}

/**
 * @brief       定时器中断服务函数 计算转速
 * @param       无
 * @retval      无
 */
void GTIM_TIMX_INT_IRQHandler(void)
{   
    /* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式         20ms  */
    if(__HAL_TIM_GET_FLAG(&g_timx_handle, TIM_FLAG_UPDATE) != RESET)
    { 
         __HAL_TIM_CLEAR_IT(&g_timx_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */
        Speed_PID(120);                                       /* 目标速度 */
                      
    }
}

(2):pid.h

#ifndef __PID_H
#define	__PID_H
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/sys/sys.h"


/* TIMX 中断定义 
 * 默认是针对TIM2~TIM5
 * 注意: 通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器.
 */
 
#define GTIM_TIMX_INT                       TIM3
#define GTIM_TIMX_INT_IRQn                  TIM3_IRQn
#define GTIM_TIMX_INT_IRQHandler            TIM3_IRQHandler
#define GTIM_TIMX_INT_CLK_ENABLE()          do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0)   /* TIM3 时钟使能 */

typedef struct 
{ 
  float P,I,D,limit;
}PID;

typedef struct
{
  float Current_Error;//当前误差
  float Last_Error;//上一次误差
  float Previous_Error;//上上次误差
}Error;


void gtim_timx_int_init(uint16_t arr, uint16_t psc);

uint32_t PID_Increase(Error *sptr, PID *pid, uint32_t NowPlace, uint32_t Point);
float PID_Realize(Error *sptr,PID *pid, uint32_t NowPlace, float Point);

void PID_init(void);
void Speed_PID(uint8_t Point);

#endif

2:定时器pwm ,脉冲计数

(1): gtim.c

#include "./BSP/TIMER/gtim.h"
#include "./BSP/LED/led.h"
#include "./SYSTEM/usart/usart.h"
TIM_HandleTypeDef g_timx_cnt_chy_handle;        /* 定时器x句柄 */

/*********************************以下是通用定时器PWM输出实验程序*************************************/

TIM_HandleTypeDef g_timx_pwm_chy_handle;     /* 定时器x句柄 */

/**
 * @brief       通用定时器TIMX 通道Y PWM输出 初始化函数(使用PWM模式1)
 * @note
 *              通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
 *              通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 * @param       arr: 自动重装值。
 * @param       psc: 预分频系数
 * @retval      无
 */
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy = {0};                       /* 定时器输出句柄 */
    
    g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM;                 /* 定时器x */
    g_timx_pwm_chy_handle.Init.Prescaler = psc;                     /* 预分频系数 */
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;    /* 递增计数模式 */
    g_timx_pwm_chy_handle.Init.Period = arr;                        /* 自动重装载值 */
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);                       /* 初始化PWM */

    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;                       /* 模式选择PWM1 */
    timx_oc_pwm_chy.Pulse = 500 ;                                /* 设置比较值,此值用来确定占空比 */

    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH;                                        /* 输出比较极性为高 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);                           /* 开启对应PWM通道 */
}

/**
 * @brief       定时器底层驱动,时钟使能,引脚配置
 *              此函数会被HAL_TIM_PWM_Init()调用
 * @param       htim:定时器句柄
 * @retval      无
 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_PWM)
    {
        GPIO_InitTypeDef gpio_init_struct;
        GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE();                            /* 开启通道y的CPIO时钟 */
        GTIM_TIMX_PWM_CHY_CLK_ENABLE();                                 /* 使能定时器时钟 */

        gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN;              /* 通道y的CPIO口 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                        /* 复用推完输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                            /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;                  /* 高速 */
        gpio_init_struct.Alternate = GTIM_TIMX_PWM_CHY_GPIO_AF;         /* IO口REMAP设置, 是否必要查看头文件配置的说明! */
        HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
    }
}


/*********************************以下是通用定时器脉冲计数实验程序*************************************/



/* 记录定时器计数器的溢出次数, 方便计算总脉冲个数 */
uint32_t g_timxchy_cnt_ofcnt = 0 ;              /* 计数溢出次数 */

/**
 * @brief       通用定时器TIMX 通道Y 脉冲计数 初始化函数
 * @note
 *              本函数选择通用定时器的时钟选择: 外部时钟源模式1(SMS[2:0] = 111)
 *              这样CNT的计数时钟源就来自 TIMX_CH1/CH2, 可以实现外部脉冲计数(脉冲接入CH1/CH2)
 *
 *              时钟分频数 = psc, 一般设置为0, 表示每一个时钟都会计数一次, 以提高精度.
 *              通过读取CNT和溢出次数, 经过简单计算, 可以得到当前的计数值, 从而实现脉冲计数
 *
 * @param       arr: 自动重装值 
 * @retval      无
 */
void gtim_timx_cnt_chy_init(uint16_t psc)
{
    GPIO_InitTypeDef gpio_init_struct;
    TIM_SlaveConfigTypeDef tim_slave_config = {0};
    GTIM_TIMX_CNT_CHY_CLK_ENABLE();                                        /* 使能TIMx时钟 */
    GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE();                                   /* 开启GPIOA时钟 */
    
    g_timx_cnt_chy_handle.Instance = GTIM_TIMX_CNT;                        /* 定时器x */
    g_timx_cnt_chy_handle.Init.Prescaler = psc;                            /* 预分频系数 */
    g_timx_cnt_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;           /* 递增计数模式 */
    g_timx_cnt_chy_handle.Init.Period = 65535;                             /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_cnt_chy_handle);

    gpio_init_struct.Pin = GTIM_TIMX_CNT_CHY_GPIO_PIN;                     /* 输入捕获的GPIO口 */
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                               /* 复用推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                                 /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;                    /* 高速 */
    gpio_init_struct.Alternate = GTIM_TIMX_CNT_CHY_GPIO_AF;                /* 复用为捕获TIMx的通道 */
    HAL_GPIO_Init(GTIM_TIMX_CNT_CHY_GPIO_PORT, &gpio_init_struct);

    /* 从模式:外部触发模式1 */
    tim_slave_config.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;                  /* 从模式:外部触发模式1 */
    tim_slave_config.InputTrigger = TIM_TS_TI1FP1;                         /* 输入触发:选择 TI1FP1(TIMX_CH1) 作为输入源 */
    tim_slave_config.TriggerPolarity = TIM_TRIGGERPOLARITY_FALLING;         /* 触发极性:上升沿 */
    tim_slave_config.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;         /* 触发预分频:无 */
    tim_slave_config.TriggerFilter = 0x0;                                  /* 滤波:本例中不需要任何滤波 */
    HAL_TIM_SlaveConfigSynchronization(&g_timx_cnt_chy_handle, &tim_slave_config);

    HAL_NVIC_SetPriority(GTIM_TIMX_CNT_IRQn, 1, 3);                        /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_EnableIRQ(GTIM_TIMX_CNT_IRQn);                                /* 开启ITMx中断 */

    __HAL_TIM_ENABLE_IT(&g_timx_cnt_chy_handle, TIM_IT_UPDATE);            /* 使能更新中断 */
    HAL_TIM_IC_Start(&g_timx_cnt_chy_handle, GTIM_TIMX_CNT_CHY1);           /* 开始捕获TIMx的通道y */
}

/**
 * @brief       通用定时器TIMX 通道Y 获取当前计数值 
 * @param       无
 * @retval      当前计数值
 */
uint32_t gtim_timx_cnt_chy_get_count(void)
{
    uint32_t count = 0;
    //count = g_timxchy_cnt_ofcnt * 65535;                    /* 计算溢出次数对应的计数值 */
    count += __HAL_TIM_GET_COUNTER(&g_timx_cnt_chy_handle); /* 加上当前CNT的值 */
//    printf("gtim_timx count %d \r\n", count);
    return count;
}

/**
 * @brief       通用定时器TIMX 通道Y 重启计数器
 * @param       无
 * @retval      当前计数值
 */
void gtim_timx_cnt_chy_restart(void)
{
    __HAL_TIM_DISABLE(&g_timx_cnt_chy_handle);          /* 关闭定时器TIMX */
    g_timxchy_cnt_ofcnt = 0;                            /* 累加器清零 */
    __HAL_TIM_SET_COUNTER(&g_timx_cnt_chy_handle, 0);   /* 计数器清零 */
    __HAL_TIM_ENABLE(&g_timx_cnt_chy_handle);           /* 使能定时器TIMX */
}

/**
 * @brief       通用定时器TIMX 脉冲计数 更新中断服务函数
 * @param       无
 * @retval      无
 */
void GTIM_TIMX_CNT_IRQHandler(void)
{
    /* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式 */
    if(__HAL_TIM_GET_FLAG(&g_timx_cnt_chy_handle, TIM_FLAG_UPDATE) != RESET)
    {
        g_timxchy_cnt_ofcnt++;          /* 累计溢出次数 */
    }

    __HAL_TIM_CLEAR_IT(&g_timx_cnt_chy_handle, TIM_IT_UPDATE);
}

(2):gitm.h

#ifndef __GTIM_H
#define __GTIM_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* 通用定时器 定义 */


/* TIMX PWM输出定义 
 * 这里输出的PWM控制LED0(RED)的亮度
 * 默认是针对TIM2~TIM5
 * 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,任意一个IO口输出PWM
 */
#define GTIM_TIMX_PWM_CHY_GPIO_PORT         GPIOF
#define GTIM_TIMX_PWM_CHY_GPIO_PIN          GPIO_PIN_9
#define GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)  /* PF口时钟使能 */
#define GTIM_TIMX_PWM_CHY_GPIO_AF           GPIO_AF9_TIM14                               /* 端口复用到TIM14 */

/* TIMX REMAP设置
 * 因为我们LED0接在PF9上, 必须通过开启TIM14的部分重映射功能, 才能将TIM14_CH1输出到PF9上
 */

#define GTIM_TIMX_PWM                       TIM14                                        /* TIM14 */
#define GTIM_TIMX_PWM_CHY                   TIM_CHANNEL_1                                /* 通道Y,  1<= Y <=4 */
#define GTIM_TIMX_PWM_CHY_CCRX              TIM14->CCR1                                  /* 通道Y的输出比较寄存器 */
#define GTIM_TIMX_PWM_CHY_CLK_ENABLE()      do{ __HAL_RCC_TIM14_CLK_ENABLE(); }while(0)  /* TIM14 时钟使能 */


/* TIM2 速度 输入计数定义  
* 这里的输入计数使用定时器TIM2_CH1,捕获WK_UP按键的输入
* 默认是针对TIM2~TIM5, 只有CH1和CH2通道可以用做输入计数, CH3/CH4不支持!
* 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,CH1/CH2对应IO口做输入计数
*       特别要注意:默认用的PA0,设置的是下拉输入!如果改其他IO,对应的上下拉方式也得改!
*/
#define GTIM_TIMX_CNT_CHY_GPIO_PORT            GPIOA
#define GTIM_TIMX_CNT_CHY_GPIO_PIN             GPIO_PIN_0
#define GTIM_TIMX_CNT_CHY_GPIO_AF              GPIO_AF1_TIM2                                /* AF功能选择 */
#define GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE()    do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)  /* PA口时钟使能 */

#define GTIM_TIMX_CNT                          TIM2
#define GTIM_TIMX_CNT_IRQn                     TIM2_IRQn
#define GTIM_TIMX_CNT_IRQHandler               TIM2_IRQHandler
#define GTIM_TIMX_CNT_CHY1                      TIM_CHANNEL_1                                /* 通道Y,  1<= Y <=2 */

#define GTIM_TIMX_CNT_CHY_CLK_ENABLE()         do{ __HAL_RCC_TIM2_CLK_ENABLE(); }while(0)   /* TIM2 时钟使能 */

/******************************************************************************************/


/******************************************************************************************/

void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc);    /* 通用定时器 PWM初始化函数 */
void gtim_timx_cnt_chy_init(uint16_t psc);                  /* 通用定时器 脉冲计数初始化函数 */
uint32_t gtim_timx_cnt_chy_get_count(void);                 /* 通用定时器 获取脉冲计数 */
void gtim_timx_cnt_chy_restart(void);                       /* 通用定时器 重启计数器 */

void gtim_tim5_cnt_chy_init(uint16_t psc);                  /* 通用定时器 脉冲计数初始化函数 */

#endif

3:主函数


extern TIM_HandleTypeDef g_timx_pwm_chy_handle;     /* 定时器x PWM 句柄 */
extern TIM_HandleTypeDef g_timx_cnt_chy_handle;     /* 定时器x 脉冲计数句柄 */


int main(void)
{
   
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7);         /* 设置时钟,168Mhz */
    delay_init(168);                            /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    led_init();                                 /* 初始化LED */
    PID_init();
    lcd_init();

    gtim_timx_cnt_chy_init(0);                  /* 定时器计数初始化, 不分频 */
    

    gtim_timx_pwm_chy_init(1000 - 1, 84 - 1);
    gtim_timx_int_init(200-1,8400-1);           /*   20 ms 中断*/
    lcd_clear(WHITE);
    lcd_show_string(0,20,200,16,16,"SPEED:",RED);

    lcd_show_string(0,40,200,16,16,"Moto:",RED);

    lcd_show_string(0,60,200,16,16,"Reali:",RED);
    
    lcd_show_string(0,80,200,16,16,"Desti:",RED);
    
    lcd_show_string(0,100,200,16,16,"Count:",RED);


    __HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, TIM_CHANNEL_1 ,0);

    while (1)
    {

       


    }
}

四:小结

本章内容就到此为止了,如果想要速度稳定效果更好就需要自己对P,I,D的参数进行修改调试。程序有些瑕疵仅供参考相互学习,想要下载源文件点击这里stm32+pid闭环控制小车速度 博主后期还会分享相关学习,想要一块学习进步的小伙伴可以关注一下博主。

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜·き

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值