原理概述

步进电机是一种数字信号驱动的电机,其主要优点之一就是拥有很好的开环控制能力,控制系统不需要传感器和相应电路的反馈电机信息,在负载不超载和脉冲频率合适的情况下,步进电机接收到的脉冲数和转子的角位移就是严格成正比关系。虽然步进电机可以很好的开环控制,但实际在一些开环系统中,步进电机有可能由于自身性能及系统机械结构等因素的影响,在快速启停或负载突变时出现失步、过冲甚至堵转,控制器无法知晓和矫正,这些现象在某些对精度要求较高的系统中可能导致严重后果。而加入传感器反馈组成闭环系统后,可以检测是否有失步等现象发生并及时纠正偏差。

步进电机闭环控制方案有很多种,可以完全控制步进电机的转矩和位置,改善步进电机的转矩频率特性,降低发热和平均功耗,提高电机运行效率。平时听到的一些控制名词,比如速度环、位置环和电流环这些,就可以用于步进电机的闭环控制,当然这些名词同样适用于其他的电机闭环系统。

下面我们通过步进电机转速作为被控量(也就是速度环),使用旋转编码器作为反馈传感器,PID 算法进行控制的闭环控制系统。

速度闭环控制–增量式 PID

下面我们可以通过分别创建:

  • pid.h 和 pid.c 文件用来存放 PID 控制器相关程序。

  • stepper_ctrl.c、stepper_ctrl.h 文件用来存步进电机速度环控制程序及相关宏定义

操作步骤

  • 定时器 IO 配置
  • 步进电机、编码器相关外设初始化
  • 速度闭环控制实现
  • PID 参数整定

测试环境

STM32F103VE版本以上,外部高速晶振:8MHz,RTC晶振:32.768KHz。各总线运行时钟:系统时钟 = SYCCLK = AHB = 72MHz,APB2 = 72MHz ,APB1 = 36MHz

步进电机驱动器接口

编码器与步进电机的接线

代码解析

第一步查看stepper_ctrl.h文件,通过这个文件是初始化步进电机GPIO等

#ifndef __STEP_MOTOR_INIT_H
#define    __STEP_MOTOR_INIT_H

#include "stm32f1xx_hal.h"

//Motor 方向 
#define MOTOR_DIR_PIN                      GPIO_PIN_6  
#define MOTOR_DIR_GPIO_PORT                GPIOE               
#define MOTOR_DIR_GPIO_CLK_ENABLE()       __HAL_RCC_GPIOE_CLK_ENABLE()

//Motor 使能 
#define MOTOR_EN_PIN                      GPIO_PIN_5
#define MOTOR_EN_GPIO_PORT                GPIOE                       
#define MOTOR_EN_GPIO_CLK_ENABLE()        __HAL_RCC_GPIOE_CLK_ENABLE()

//Motor 脉冲
#define MOTOR_PUL_IRQn                  TIM8_CC_IRQn
#define MOTOR_PUL_IRQHandler            TIM8_CC_IRQHandler

#define MOTOR_PUL_TIM                   TIM8
#define MOTOR_PUL_CLK_ENABLE()              __HAL_RCC_TIM8_CLK_ENABLE()

#define MOTOR_PUL_PORT                         GPIOC
#define MOTOR_PUL_PIN                       GPIO_PIN_6
#define MOTOR_PUL_GPIO_CLK_ENABLE()          __HAL_RCC_GPIOC_CLK_ENABLE()
#define MOTOR_PUL_CHANNEL_x             TIM_CHANNEL_1

#define MOTOR_TIM_IT_CCx                TIM_IT_CC1
#define MOTOR_TIM_FLAG_CCx              TIM_FLAG_CC1

/*频率相关参数*/
//定时器实际时钟频率为:72MHz/TIM_PRESCALER

//具体需要的频率可以自己计算
#define TIM_PRESCALER                16  /*补充:对F103例程测试,提高分频利于位置环稳定状态*/
// 定义定时器周期,输出比较模式周期设置为0xFFFF
#define TIM_PERIOD                   0xFFFF

/************************************************************/
#define HIGH GPIO_PIN_SET       //高电平
#define LOW  GPIO_PIN_RESET     //低电平

#define ON  LOW                 //开
#define OFF HIGH                //关

#define CW  HIGH                //顺时针
#define CCW LOW                 //逆时针

//控制使能引脚
/* 带参宏,可以像内联函数一样使用 */
#define MOTOR_EN(x)                    HAL_GPIO_WritePin(MOTOR_EN_GPIO_PORT,MOTOR_EN_PIN,x)
#define MOTOR_PUL(x)                HAL_GPIO_WritePin(MOTOR_PUL_GPIO_PORT,MOTOR_PUL_PIN,x)
#define MOTOR_DIR(x)                HAL_GPIO_WritePin(MOTOR_DIR_GPIO_PORT,MOTOR_DIR_PIN,x)

#define MOTOR_EN_TOGGLE     HAL_GPIO_TogglePin(MOTOR_EN_GPIO_PORT,MOTOR_EN_PIN)
#define MOTOR_PUL_TOGGLE    HAL_GPIO_TogglePin(MOTOR_PUL_PORT,MOTOR_PUL_PIN)

extern TIM_HandleTypeDef TIM_StepperHandle;
extern void stepper_Init(void);
#endif /* __STEP_MOTOR_INIT_H */

stepper_ctrl.h 步进电机的步距角、驱动器细分数和 PID 控制用到的目标速度

步进电机本身的参数和闭环控制需要用到的参数,包括步进电机的步距角、驱动器细分数和 PID 控制用到的目标速度等。其中宏 PULSE_RATIO 是细分后的步进电机单圈脉冲数与编码器单圈脉冲数的比值,因为在整个速度闭环控制系统中,反馈和 PID 计算得出的都是编码器的脉冲数。

#ifndef __STEP_MOTOR_CTRL_H
#define    __STEP_MOTOR_CTRL_H

#include "stepper_init.h"
#include "encoder.h"

/*宏定义*/
/*******************************************************/
#define T1_FREQ           (SystemCoreClock/TIM_PRESCALER) // 频率ft值

/*电机单圈参数*/
#define STEP_ANGLE                1.8f                        //步进电机的步距角 单位:度
#define FSPR              ((float)(360.0f/STEP_ANGLE))//步进电机的一圈所需脉冲数

#define MICRO_STEP        32                                  //细分器细分数 
#define SPR               (FSPR*MICRO_STEP)           //细分后一圈所需脉冲数

#define PULSE_RATIO       ((float)(SPR/ENCODER_TOTAL_RESOLUTION))//步进电机单圈脉冲数与编码器单圈脉冲的比值
#define TARGET_DISP       2                    //步进电机运动时的目标圈数,单位:转
#define SPEED_LIMIT       10000                //最大启动速度限制

#define SAMPLING_PERIOD   50                          //PID采样频率,单位Hz

.......
........
........

void MSD_ENA(int NewState);
void Set_Stepper_Stop(void);
void Set_Stepper_Start(void);
void Stepper_Speed_Ctrl(void);

#endif /* __STEP_MOTOR_CTRL_H */

定义了一个结构体 __SYS_STATUS ,用来管理驱动器和电机的运行状态

typedef struct {
  unsigned char stepper_dir : 1;               //步进电机方向
  unsigned char stepper_running : 1;           //步进电机运行状态
  unsigned char MSD_ENA : 1;                   //驱动器使能状态
}__SYS_STATUS;

增量式 PID

PID 控制器的入口参数从原来的目标值更改为了反馈回来的实际值,而目标值在控制器外赋值,控制器的返回值变为 PID 计算得出的增量值,实际值的累加则放到了控制器外。整个增量式 PID 控制器的原理并没有变化,只是调整了部分代码的组织逻辑,这么做可以更方便的在程序的其他位置调用 PID 控制器。

步进电机闭环控制系统

/**
  * @brief  步进电机刹车
  * @param  无
  * @retval 无
  */
void Set_Stepper_Stop(void)
{
  /*失能比较通道*/
    TIM_CCxChannelCmd(MOTOR_PUL_TIM,MOTOR_PUL_CHANNEL_x,TIM_CCx_DISABLE);
  sys_status.stepper_running = 0;
}

/**
  * @brief  启动步进电机
  * @param  无
  * @retval 无
  */
void Set_Stepper_Start(void)
{
  /*使能驱动器*/
  MSD_ENA(0);
  /*使能比较通道输出*/
    TIM_CCxChannelCmd(MOTOR_PUL_TIM,MOTOR_PUL_CHANNEL_x,TIM_CCx_ENABLE);
  sys_status.MSD_ENA = 1;
  sys_status.stepper_running = 1;
}

/**
  * @brief  步进电机增量式PID控制
  * @retval 无
  * @note   基本定时器中断内调用
  */
void Stepper_Speed_Ctrl(void)
{
  /* 编码器相关变量 */
  static __IO int32_t last_count = 0;
  __IO int32_t capture_count = 0;
  __IO int32_t capture_per_unit = 0;
  /* 经过pid计算后的期望值 */
  static __IO float cont_val = 0.0f;

  __IO float timer_delay = 0.0f;

  /* 当电机运动时才启动pid计算 */
  if((sys_status.MSD_ENA == 1) && (sys_status.stepper_running == 1))
  {
    /* 计算单个采样时间内的编码器脉冲数 */
    capture_count =(int)__HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (encoder_overflow_count * ENCODER_TIM_PERIOD);

    /* 单位时间内的编码器脉冲数作为实际值传入pid控制器 */
    cont_val += PID_realize((float)capture_count);// 进行 PID 计算

    /* 判断速度方向 */
    cont_val > 0 ? (MOTOR_DIR(CW)) : (MOTOR_DIR(CCW));

    /* 计算得出的期望值取绝对值 */
    timer_delay = fabsf(cont_val);

         /* 限制最大启动速度 */
    timer_delay >= SPEED_LIMIT ? (timer_delay = SPEED_LIMIT) : timer_delay;

    /* 计算比较计数器的值 */
     OC_Pulse_num = ((uint16_t)(T1_FREQ / ((float)timer_delay * PULSE_RATIO))) >> 1;
     printf("实际值:%d,目标值:%.0f\r\n", capture_per_unit, pid.target_val);// 打印实际值和目标值 
  }
  else
  {
    /*停机状态所有参数清零*/
    last_count = 0;
    cont_val = 0;
    pid.actual_val = 0;
    pid.err = 0;
    pid.err_last = 0;
    pid.err_next = 0;
  }
}

定义了一些用于编码器测速和 PID 计算的中间变量

判断驱动器和电机运行状态,如果驱动器使能并且电机处于运动状态,才能执行闭环控制

if((sys_status.MSD_ENA == 1) && (sys_status.stepper_running == 1))

读取编码器计数值并计算在单个采样周期中的计数值 capture_per_unit ,单位是脉冲每毫秒,实际表示编码器脉冲的频率,这里为了后续计算方便并没有写成以转每秒为单位的速度;

 capture_count =(int)__HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (encoder_overflow_count * ENCODER_TIM_PERIOD);

在电机停止或由运行变为停止时,需要清零编码器读数的中间值和 PID 控制器中的累加数据,以免影响电机再次启动时的控制效果。

/*停机状态所有参数清零*/
last_count = 0;
cont_val = 0;
pid.actual_val = 0;
pid.err = 0;
pid.err_last = 0;
pid.err_next = 0;

执行过程

整个 Stepper_Speed_Ctrl 闭环控制函数中,传入 PID 和 PID 输出的参数都是编码器的数据,也就是编码器的脉冲频率,但是实际被控量是步进电机的转轴速度,需要做转换。将编码器的脉冲频率 capture_per_unit 乘上一个系数PULSE_RATIO 便可得到步进电机所需的脉冲频率,这个系数是由步进电机经过细分后转轴转一圈所需的脉冲数,与编码器转一圈发出的脉冲数之间的比值得出。不过此时的频率还是以 ms为单位的,为了后续计算方便,需要统一成以 s 为单位,因为本例程的采样周期是 20ms,所以单位转换只需要乘上 1s 内的采样次数 50 即可。

得到步进电机需要的脉冲频率后,把它转换成可以写入捕获比较寄存器的值。当定时器配置为输出比较模式时,通过修改捕获比较寄存器当中的值,可以改变步进电机脉冲的周期,从而改变电机转速。

定时器控制

基本定时器 TIM6 的定时中断中循环调用闭环控制程序,TIM6 配置为 20ms 中断一次,也就是说闭环控制的采样周期是 20ms。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* 判断触发中断的定时器 */
  if(htim->Instance == BASIC_TIM)
  {
    Stepper_Speed_Ctrl();
  }
  else if(htim->Instance == ENCODER_TIM)
  {  
    /* 判断当前计数方向 */
    if(__HAL_TIM_IS_TIM_COUNTING_DOWN(htim))
      /* 下溢 */
      encoder_overflow_count--;
    else
      /* 上溢 */
      encoder_overflow_count++;
  }
}

main

#include "main.h"
#include <stdio.h>
#include <stdlib.h>
#include "usart.h"
#include "stepper_init.h"
#include "key.h"
#include "led.h"
#include "pid.h"
#include "tim.h"
#include "stepper_ctrl.h"
#include "encoder.h"
#include "protocol.h"

extern _pid pid;
extern int pid_status;

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void) 
{
    /* 初始化系统时钟为72MHz */
    SystemClock_Config();
    /* 开启复用寄存器时钟 */
    __HAL_RCC_SYSCFG_CLK_ENABLE();

    /*补充:PID例程中 MOTOR_PUL_IRQn 优先级需要调为最高 */    
    /* Set Interrupt Group Priority */
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);    

    /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
    DEBUG_USART_Config();
    protocol_init();          /* 初始化串口通信协议 */
    HAL_InitTick(5);
    /*按键中断初始化*/
    Key_GPIO_Config();    
    /*led初始化*/
    LED_GPIO_Config();
    /* 初始化基本定时器定时,20ms产生一次中断 */
    TIMx_Configuration();
    /* 编码器接口初始化 */
    Encoder_Init();
    /*步进电机初始化*/
    stepper_Init();
    /* 上电默认停止电机 */
    Set_Stepper_Stop();
    /* PID算法参数初始化 */
    PID_param_init();

    /* 目标速度转换为编码器的脉冲数作为pid目标值 */
    pid.target_val = TARGET_DISP * ENCODER_TOTAL_RESOLUTION;
    while(1)
    {
    /* 接收数据处理 */
    receiving_process();

    /* 扫描KEY1,启动电机 */
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON  )
    {
        Set_Stepper_Start();
    }
    /* 扫描KEY2,停止电机 */
    if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON  )
    {
        Set_Stepper_Stop();     
    }
    /* 扫描KEY3,增大目标位置 */
    if( Key_Scan(KEY3_GPIO_PORT,KEY3_PIN) == KEY_ON  )
    {
        /* 位置增加2圈 */
        pid.target_val += 8000;
    }
    /* 扫描KEY4,减小目标位置 */
    if( Key_Scan(KEY4_GPIO_PORT,KEY4_PIN) == KEY_ON  )
    {
        /* 位置减小2圈 */
        pid.target_val -= 8000;
    }
}     

/**
  * @brief  定时器更新事件回调函数
  * @param  无
  * @retval 无
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* 判断触发中断的定时器 */
  if(htim->Instance == BASIC_TIM)
  {
    Stepper_Speed_Ctrl();
  }
  else if(htim->Instance == ENCODER_TIM)
  {  
    /* 判断当前计数方向 */
    if(__HAL_TIM_IS_TIM_COUNTING_DOWN(htim))
      /* 下溢 */
      encoder_overflow_count--;
    else
      /* 上溢 */
      encoder_overflow_count++;
  }
}

/**
  * @brief  System Clock Configuration
  *         The system Clock is configured as follow : 
  *            System Clock source            = PLL (HSE)
  *            SYSCLK(Hz)                     = 72000000
  *            HCLK(Hz)                       = 72000000
  *            AHB Prescaler                  = 1
  *            APB1 Prescaler                 = 2
  *            APB2 Prescaler                 = 1
  *            HSE Frequency(Hz)              = 8000000
  *            HSE PREDIV1                    = 2
  *            PLLMUL                         = 9
  *            Flash Latency(WS)              = 0
  * @param  None
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_ClkInitTypeDef clkinitstruct = {0};
  RCC_OscInitTypeDef oscinitstruct = {0};

  /* Enable HSE Oscillator and activate PLL with HSE as source */
  oscinitstruct.OscillatorType  = RCC_OSCILLATORTYPE_HSE;
  oscinitstruct.HSEState        = RCC_HSE_ON;
  oscinitstruct.HSEPredivValue  = RCC_HSE_PREDIV_DIV1;
  oscinitstruct.PLL.PLLState    = RCC_PLL_ON;
  oscinitstruct.PLL.PLLSource   = RCC_PLLSOURCE_HSE;
  oscinitstruct.PLL.PLLMUL      = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&oscinitstruct)!= HAL_OK)
  {
    /* Initialization Error */
    while(1); 
  }

  /* Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2 clocks dividers */
  clkinitstruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
  clkinitstruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  clkinitstruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  clkinitstruct.APB2CLKDivider = RCC_HCLK_DIV1;
  clkinitstruct.APB1CLKDivider = RCC_HCLK_DIV2;  
  if (HAL_RCC_ClockConfig(&clkinitstruct, FLASH_LATENCY_2)!= HAL_OK)
  {
    while(1); 
  }
}

encoder.h

#ifndef __ENCOEDER_H
#define    __ENCOEDER_H

#include "stm32f1xx.h"

/* 定时器选择 */
#define ENCODER_TIM                            TIM4
#define ENCODER_TIM_CLK_ENABLE()                       __HAL_RCC_TIM4_CLK_ENABLE()
#define ENCODER_TIM_AF_CLK_ENABLE()                   __HAL_AFIO_REMAP_TIM4_ENABLE()

/* 定时器溢出值 */        
#define ENCODER_TIM_PERIOD                     65535
/* 定时器预分频值 */
#define ENCODER_TIM_PRESCALER                  0      

/* 定时器中断 */
#define ENCODER_TIM_IRQn                       TIM4_IRQn
#define ENCODER_TIM_IRQHandler                 TIM4_IRQHandler

/* 编码器接口引脚 */
#define ENCODER_TIM_CH1_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOD_CLK_ENABLE()
#define ENCODER_TIM_CH1_GPIO_PORT              GPIOD
#define ENCODER_TIM_CH1_PIN                    GPIO_PIN_12

#define ENCODER_TIM_CH2_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOD_CLK_ENABLE()
#define ENCODER_TIM_CH2_GPIO_PORT              GPIOD
#define ENCODER_TIM_CH2_PIN                    GPIO_PIN_13

/* 编码器接口倍频数 */
#define ENCODER_MODE                           TIM_ENCODERMODE_TI12

/* 编码器接口输入捕获通道相位设置 */
#define ENCODER_IC1_POLARITY                   TIM_ICPOLARITY_FALLING
#define ENCODER_IC2_POLARITY                   TIM_ICPOLARITY_RISING

/* 编码器物理分辨率 */
#define ENCODER_RESOLUTION                     1000

/* 经过倍频之后的总分辨率 */
#if ((ENCODER_MODE == TIM_ENCODERMODE_TI1) || (ENCODER_MODE == TIM_ENCODERMODE_TI2))
  #define ENCODER_TOTAL_RESOLUTION             (ENCODER_RESOLUTION * 2)  /* 2倍频后的总分辨率 */
#else
  #define ENCODER_TOTAL_RESOLUTION             (ENCODER_RESOLUTION * 4)  /* 4倍频后的总分辨率 */
#endif


extern __IO int16_t encoder_overflow_count;
extern TIM_HandleTypeDef TIM_EncoderHandle;

void Encoder_Init(void);

#endif   /* __BSP_ENCODER_H */

encoder.c

#include "encoder.h"

/* 定时器溢出次数 */
__IO int16_t encoder_overflow_count = 0;
TIM_HandleTypeDef TIM_EncoderHandle;

/**
  * @brief  编码器接口引脚初始化
  * @param  无
  * @retval 无
  */
static void Encoder_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* 定时器通道引脚端口时钟使能 */
  ENCODER_TIM_CH1_GPIO_CLK_ENABLE();
  ENCODER_TIM_CH2_GPIO_CLK_ENABLE();
  /* 设置重映射 */
  ENCODER_TIM_AF_CLK_ENABLE();

  /* 设置输入类型 */
  GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT;
  /* 设置上拉 */
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  /* 设置引脚速率 */
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

  /* 选择要控制的GPIO引脚 */    
  GPIO_InitStruct.Pin = ENCODER_TIM_CH1_PIN;

  /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
  HAL_GPIO_Init(ENCODER_TIM_CH1_GPIO_PORT, &GPIO_InitStruct);

  /* 选择要控制的GPIO引脚 */    
  GPIO_InitStruct.Pin = ENCODER_TIM_CH2_PIN;
  /* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
  HAL_GPIO_Init(ENCODER_TIM_CH2_GPIO_PORT, &GPIO_InitStruct);
}

/**
  * @brief  配置TIMx编码器模式
  * @param  无
  * @retval 无
  */
static void TIM_Encoder_Init(void)
{ 
  TIM_Encoder_InitTypeDef Encoder_ConfigStructure;

  /* 使能编码器接口时钟 */
  ENCODER_TIM_CLK_ENABLE();

  /* 定时器初始化设置 */
  TIM_EncoderHandle.Instance = ENCODER_TIM;
  TIM_EncoderHandle.Init.Prescaler = ENCODER_TIM_PRESCALER;
  TIM_EncoderHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
  TIM_EncoderHandle.Init.Period = ENCODER_TIM_PERIOD;
  TIM_EncoderHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  TIM_EncoderHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

  /* 设置编码器倍频数 */
  Encoder_ConfigStructure.EncoderMode = ENCODER_MODE;
  /* 编码器接口通道1设置 */
  Encoder_ConfigStructure.IC1Polarity = ENCODER_IC1_POLARITY;
  Encoder_ConfigStructure.IC1Selection = TIM_ICSELECTION_DIRECTTI;
  Encoder_ConfigStructure.IC1Prescaler = TIM_ICPSC_DIV1;
  Encoder_ConfigStructure.IC1Filter = 0;
  /* 编码器接口通道2设置 */
  Encoder_ConfigStructure.IC2Polarity = ENCODER_IC2_POLARITY;
  Encoder_ConfigStructure.IC2Selection = TIM_ICSELECTION_DIRECTTI;
  Encoder_ConfigStructure.IC2Prescaler = TIM_ICPSC_DIV1;
  Encoder_ConfigStructure.IC2Filter = 0;
  /* 初始化编码器接口 */
  HAL_TIM_Encoder_Init(&TIM_EncoderHandle, &Encoder_ConfigStructure);

  /* 清零计数器 */
  __HAL_TIM_SET_COUNTER(&TIM_EncoderHandle, 0);

  /* 清零中断标志位 */
  __HAL_TIM_CLEAR_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
  /* 使能定时器的更新事件中断 */
  __HAL_TIM_ENABLE_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
  /* 设置更新事件请求源为:计数器溢出 */
  __HAL_TIM_URS_ENABLE(&TIM_EncoderHandle);

  /* 设置中断优先级 */
  HAL_NVIC_SetPriority(ENCODER_TIM_IRQn, 5, 1);
  /* 使能定时器中断 */
  HAL_NVIC_EnableIRQ(ENCODER_TIM_IRQn);

  /* 使能编码器接口 */
  HAL_TIM_Encoder_Start(&TIM_EncoderHandle, TIM_CHANNEL_ALL);
}

/**
  * @brief  编码器接口初始化
  * @param  无
  * @retval 无
  */
void Encoder_Init(void)
{
  Encoder_GPIO_Init();    /* 引脚初始化 */
  TIM_Encoder_Init();     /* 配置编码器接口 */
}

pid.c

/* Includes ------------------------------------------------------------------*/
#include "pid.h"
#include "math.h"
#include "stepper_ctrl.h"
#include "protocol.h"

/* 定义全局变量 */
_pid pid;
float set_point=0.0;
int pid_status=0;

/**
  * @brief  PID参数初始化
  *    @note     无
  * @retval 无
  */
void PID_param_init()
{
    /* 初始化参数 */
    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 = 1.2;
    pid.Ki = 0;
    pid.Kd = 0;
}

/**
  * @brief  设置目标值
  * @param  val        目标值
  *    @note     无
  * @retval 无
  */
void set_pid_actual(float temp_val)
{
  pid.target_val = temp_val;    // 设置当前的目标值
}

/**
  * @brief  获取目标值
  * @param  无
    *    @note     无
  * @retval 目标值
  */
float get_pid_actual(void)
{
  return pid.target_val;    // 设置当前的目标值
}

/**
  * @brief  设置比例、积分、微分系数
  * @param  p:比例系数 P
  * @param  i:积分系数 i
  * @param  d:微分系数 d
    *    @note     无
  * @retval 无
  */
void set_p_i_d(float p, float i, float d)
{
    pid.Kp = p;    // 设置比例系数 P
    pid.Ki = i;    // 设置积分系数 I
    pid.Kd = d;    // 设置微分系数 D
}

/**
  * @brief  增量式PID算法实现
  * @param  val:当前实际值
    *    @note     无
  * @retval 通过PID计算后的输出
  */
float PID_realize(float temp_val) 
{
    /*传入实际值*/
    pid.actual_val = temp_val;
    /*计算目标值与实际值的误差*/
    pid.err=pid.target_val-pid.actual_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.err_last = pid.err_next;
    pid.err_next = pid.err;
    /*返回增量值*/
    return increment_val;
}

pid.h

#ifndef __PID_H
#define    __PID_H
#include "stm32f1xx.h"
#include "usart.h"
#include <stdio.h>
#include <stdlib.h>

/*pid*/
typedef struct
{
  float target_val;     //目标值
    float actual_val;     //实际值
    float err;            //定义当前偏差值
    float err_next;       //定义下一个偏差值
    float err_last;       //定义上一个偏差值
    float Kp, Ki, Kd;     //定义比例、积分、微分系数
}_pid;


extern void PID_param_init(void);
extern void set_pid_actual(float temp_val);
extern float get_pid_actual(void);
extern void set_p_i_d(float p, float i, float d);
extern float PID_realize(float temp_val);
extern void time_period_fun(void);

#endif

位置式 PID

创建了 4 个文件:pid.h 和 pid.c 文件用来存放 PID 控制器相关程序,stepper_ctrl.c、stepper_ctrl.h 文件用来存步进电机速度环控制程序及相关宏定义。stepper_init.h 和stepper_ctrl.h 中的宏定义与增量式 PID中的宏定义完全相同。

pid.c

PID 控制器的入口参数从原来的目标值更改为了反馈回来的实际值,而目标值在控制器外赋值,控制器的返回值变为 PID 计算得出的位置值。整个位置式 PID 控制器的原理并没有变化,只是调整了部分代码的组织逻辑,这么做可以更方便的在程序的其他位置调用 PID 控制器。

#include "pid.h"
#include "math.h"
#include "stepper_ctrl.h"
#include "protocol.h"
/* 定义全局变量 */
_pid pid;
float set_point=0.0;
int pid_status=0;

/**
  * @brief  PID参数初始化
    *    @note     无
  * @retval 无
  */
void PID_param_init()
{
    /* 初始化参数 */
    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 = 1.2;
    pid.Ki = 0;
    pid.Kd = 0;
}

/**
  * @brief  设置目标值
  * @param  val        目标值
  *    @note     无
  * @retval 无
  */
void set_pid_actual(float temp_val)
{
    pid.target_val = temp_val;    // 设置当前的目标值
}

/**
  * @brief  获取目标值
  * @param  无
  *    @note     无
  * @retval 目标值
  */
float get_pid_actual(void)
{
  return pid.target_val;    // 设置当前的目标值
}

/**
  * @brief  设置比例、积分、微分系数
  * @param  p:比例系数 P
  * @param  i:积分系数 i
  * @param  d:微分系数 d
  *    @note     无
  * @retval 无
  */
void set_p_i_d(float p, float i, float d)
{
    pid.Kp = p;    // 设置比例系数 P
    pid.Ki = i;    // 设置积分系数 I
    pid.Kd = d;    // 设置微分系数 D
}

/**
  * @brief  位置式PID算法实现
  * @param  actual_val:当前实际值
  *    @note     无
  * @retval 通过PID计算后的输出
  */
float PID_realize(float actual_val) 
{
    /*传入实际值*/
    pid.actual_val = actual_val;
    /*计算目标值与实际值的误差*/
    pid.err = pid.target_val - pid.actual_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;
    /*PID算法实现,并返回计算值*/
    return pid.actual_val;
}

步进电机闭环控制系统

#include "math.h"
#include "tim.h"
#include "stepper_ctrl.h"
#include "pid.h"
#include "protocol.h"
extern _pid pid;
extern __IO uint16_t OC_Pulse_num;     //比较输出的计数值

__SYS_STATUS sys_status = {0};

/**
  * @brief  驱动器紧急停止
  * @param  NewState:使能或者禁止
  * @retval 无
  */
void MSD_ENA(int NewState)
{
    if(NewState)
    {
      //ENA失能,禁止驱动器输出,(脱机状态)此时电机为无保持力矩状态,可以手动旋转电机
      MOTOR_EN(OFF);
      sys_status.MSD_ENA = 0;            

    }
    else
    {
      //ENA使能,此时电机为保持力矩状态
      MOTOR_EN(ON);
      sys_status.MSD_ENA = 1;            
    }
}

/**
  * @brief  步进电机刹车
  * @param  无
  * @retval 无
  */
void Set_Stepper_Stop(void)
{
  /*失能比较通道*/
    TIM_CCxChannelCmd(MOTOR_PUL_TIM,MOTOR_PUL_CHANNEL_x,TIM_CCx_DISABLE);
    sys_status.stepper_running = 0;
}

/**
  * @brief  启动步进电机
  * @param  无
  * @retval 无
  */
void Set_Stepper_Start(void)
{
  /*使能驱动器*/
  MSD_ENA(0);
  /*使能比较通道输出*/
    TIM_CCxChannelCmd(MOTOR_PUL_TIM,MOTOR_PUL_CHANNEL_x,TIM_CCx_ENABLE);
  sys_status.MSD_ENA = 1;
  sys_status.stepper_running = 1;
}

/**
  * @brief  步进电机位置式PID控制
  * @retval 无
  * @note   基本定时器中断内调用
  */
void Stepper_Speed_Ctrl(void)
{
  /* 编码器相关变量 */
  __IO int32_t capture_per_unit = 0;
  __IO int32_t capture_count = 0;
  static __IO int32_t last_count = 0;
  /* 经过pid计算后的期望值 */
  __IO int32_t cont_val = 0;

  /* 当电机运动时才启动pid计算 */
  if((sys_status.MSD_ENA == 1) && (sys_status.stepper_running == 1))
  {
    /* 计算单个采样时间内的编码器脉冲数 */
    capture_count =(int)__HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (encoder_overflow_count * ENCODER_TIM_PERIOD);

    /* 单位时间内的编码器脉冲数作为实际值传入pid控制器 */
    cont_val = PID_realize((float)capture_count);// 进行 PID 计算

    /* 判断方向 */
    cont_val > 0 ? (MOTOR_DIR(CW)) : (MOTOR_DIR(CCW));

    /* 对计算得出的期望值取绝对值 */
    cont_val = abs(cont_val);

    /* 限制最大启动速度 */
    cont_val >= SPEED_LIMIT ? (cont_val = SPEED_LIMIT) : cont_val;

    /* 计算比较计数器的值 */
    OC_Pulse_num = ((uint16_t)(T1_FREQ / ((float)cont_val * PULSE_RATIO))) >> 1;
    printf("实际值:%d,目标值:%.0f\r\n", capture_count, pid.target_val);// 打印实际值和目标值
  }
  else
  {
    capture_per_unit = 0;
    cont_val = 0;
    pid.actual_val = 0;
    pid.err = 0;
    pid.err_last = 0;
    pid.integral = 0;
  }
}

main.c

/**
  ******************************************************************************
  * @file    main.c
  * @author  fire
  * @version V1.0
  * @date    2020-xx-xx
  * @brief   步进电机-位置环
  ******************************************************************************
  * @attention
  *
  * 实验平台:野火 F103-拂晓 STM32 开发板 
  * 论坛    :http://www.firebbs.cn
  * 淘宝    :https://fire-stm32.taobao.com
  *
  ******************************************************************************
  */ 

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include <stdio.h>
#include <stdlib.h>
#include "./usart/bsp_debug_usart.h"
#include "./stepper/bsp_stepper_init.h"
#include "./key/bsp_key.h"
#include "./led/bsp_led.h"
#include "./pid/bsp_pid.h"
#include "./tim/bsp_basic_tim.h"
#include "./stepper/bsp_stepper_ctrl.h"
#include "./Encoder/bsp_encoder.h"
#include "./protocol/protocol.h"

extern _pid pid;
extern int pid_status;

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void) 
{
    /* 初始化系统时钟为72MHz */
    SystemClock_Config();
    /* 开启复用寄存器时钟 */
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    /* Set Interrupt Group Priority */
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);    

    /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
    DEBUG_USART_Config();
    protocol_init();          /* 初始化串口通信协议 */
    HAL_InitTick(5);
    /*按键中断初始化*/
    Key_GPIO_Config();    
    /*led初始化*/
    LED_GPIO_Config();
    /* 初始化基本定时器定时,20ms产生一次中断 */
    TIMx_Configuration();
    /* 编码器接口初始化 */
    Encoder_Init();
    /*步进电机初始化*/
    stepper_Init();
    /* 上电默认停止电机 */
    Set_Stepper_Stop();
    /* PID算法参数初始化 */
    PID_param_init();

    /* 目标速度转换为编码器的脉冲数作为pid目标值 */
    pid.target_val = TARGET_DISP * ENCODER_TOTAL_RESOLUTION;
    while(1)
    {
        /* 接收数据处理 */
        receiving_process();

        /* 扫描KEY1,启动电机 */
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON  )
        {
            Set_Stepper_Start();
        }
        /* 扫描KEY2,停止电机 */
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON  )
        {
            Set_Stepper_Stop();     
        }
        /* 扫描KEY3,增大目标位置 */
        if( Key_Scan(KEY3_GPIO_PORT,KEY3_PIN) == KEY_ON  )
        {
            /* 位置增加2圈 */
            pid.target_val += 8000;
        }
        /* 扫描KEY4,减小目标位置 */
        if( Key_Scan(KEY4_GPIO_PORT,KEY4_PIN) == KEY_ON  )
        {
            /* 位置减小2圈 */
            pid.target_val -= 8000;
        }
    }
}     


/**
  * @brief  定时器更新事件回调函数
  * @param  无
  * @retval 无
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* 判断触发中断的定时器 */
  if(htim->Instance == BASIC_TIM)
  {
    Stepper_Speed_Ctrl();
  }
  else if(htim->Instance == ENCODER_TIM)
  {  
    /* 判断当前计数方向 */
    if(__HAL_TIM_IS_TIM_COUNTING_DOWN(htim))
      /* 下溢 */
      encoder_overflow_count--;
    else
      /* 上溢 */
      encoder_overflow_count++;
  }
}

void SystemClock_Config(void)
{
  RCC_ClkInitTypeDef clkinitstruct = {0};
  RCC_OscInitTypeDef oscinitstruct = {0};

  /* Enable HSE Oscillator and activate PLL with HSE as source */
  oscinitstruct.OscillatorType  = RCC_OSCILLATORTYPE_HSE;
  oscinitstruct.HSEState        = RCC_HSE_ON;
  oscinitstruct.HSEPredivValue  = RCC_HSE_PREDIV_DIV1;
  oscinitstruct.PLL.PLLState    = RCC_PLL_ON;
  oscinitstruct.PLL.PLLSource   = RCC_PLLSOURCE_HSE;
  oscinitstruct.PLL.PLLMUL      = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&oscinitstruct)!= HAL_OK)
  {
    /* Initialization Error */
    while(1); 
  }

  /* Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2 
     clocks dividers */
  clkinitstruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
  clkinitstruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  clkinitstruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  clkinitstruct.APB2CLKDivider = RCC_HCLK_DIV1;
  clkinitstruct.APB1CLKDivider = RCC_HCLK_DIV2;  
  if (HAL_RCC_ClockConfig(&clkinitstruct, FLASH_LATENCY_2)!= HAL_OK)
  {
    /* Initialization Error */
    while(1); 
  }
}