第20章 步进电机闭环控制
本章我们将学习步进电机的闭环系统。经过我们前面的学习,我们知道步进电机它拥有非常优秀的开环能力,控制器不需要传感器反馈电机信号,只要在不超频且不超载的情况下,脉冲频率与转速成正比关系,而脉冲数与旋转位置成正比关系。那我们为什么还要引入闭环系统呢?因为在实际的应用中单独靠开环还是有很多不确定因素,比如步进电机的机械老化导致的丢步或堵转,还有一些外部因素比如:突然给电机加了一个比较大的负载或者快速启停都是有可能出现丢步、过冲甚至堵转现象,控制器是无法知晓并纠正偏差的,这些情况在一些要求高精度的场合是不允许的,否则有可能导致严重后果,所以我们引入闭环系统使控制器可以及时发现偏差并纠正偏差。本章将利用PID来实现步进电机的速度闭环控制以及位置闭环控制。
本章分为如下几个小节:
20.1 步进电机速度闭环控制
20.2 步进电机位置闭环控制
20.1 步进电机速度闭环控制
要实现步进电机的闭环控制,首先需要了解下步进电机的闭环系统有什么组成!
同直流有刷电机的闭环系统类似,步进电机的闭环系统的组成由:步进电机+编码器构成,编码器反馈步进电机的实际转速/位置给控制器,这样就可以控制器知晓是否有按要求到达指定目标速度/位置,当有偏差就可及时纠偏了。
接着来看总结的一些闭环系统与开环系统之间的优缺点:
表20.1.1 开/闭环系统优缺点对比
接着我们介绍下步进电机速度闭环系统。这里我们使用编码器进行速度反馈,PID算法进行控制,其原理如下图所示:

图20.1.1 步进电机速度闭环系统控制原理
图20.1.1中使用编码器作为反馈通道,以收集步进电机转子在单位采样时间内的实际执行步数。系统将实际步数转换为转速,并与目标转速进行比较,然后计算出偏差。偏差值被输入到 PID 控制器中,控制器输出经过修正的期望转速,最后交由步进电机执行。
20.1.1 硬件设计
1、例程功能
l 本实验使用步进电机接口1控制
l 当KEY0按下就增大目标速度值;当KEY1按下,就减小目标速度值。目标速度的绝对值大小决定电机的速度,它的正负决定电机的正反转方向。按下KEY2则马上停止电机。
l 可通过串口1即USB_TTL接口连接正点原子PID调试助手,查看PID波形;
l 注意驱动器使用8细分;
l LED0作为程序运行状态指示灯,闪烁周期200ms。
2、硬件资源
1)LED灯:LED0 – PE0
2)独立按键
KEY0 – PE12
KEY1 – PE13
KEY2 – PE14
3)定时器: TIM8_CH1:PC6,对应步进电机接口1
TIM3_CH1 : PA6(编码器接口1)
TIM3_CH2 : PA7(编码器接口2)
TIM6定时采样
方向引脚:DIR1: PF2
脱机引脚:EN1: PF9
4)步进电机
5)步进电机驱动器
6)12-50V的DC电源
7)编码器(本实验使用的是600线的光电编码器)
建议使用固定架将编码器与步进电机固定在一起,以避免编码器的抖动,从而导致编码器读数不准确。如下图所示:

图20.1.1.1 步进电机与编码器的连接图
整体连接图如下:

图20.1.1.2 整体连接图
3、原理图

图20.1.1.3 编码器接口
步进电机控制接口1已经介绍很多遍这里就不重复讲解,我们这里主要介绍下编码器接口,本实验我们除了用到基础驱动所需的引脚,还需要用到PM1_ENCA(PA6)、PM1_ENCB(PA7)这两个引脚,它们分别用于连接编码器的A、B两相。需要注意的是,编码器供电有2种方式,通过跳线帽P11选择,可以使用板载5V或者电源输入VIN,默认使用板载5V。所以当我们将编码器的接到CN15时,还需将P11处的VCC5与VCC_ENC使用跳线帽连接起来。
20.1.2 程序设计
20.2.2.1 步进电机速度闭环控制的配置步骤
1)初始化定时器
初始化定时器通道IO,设置ARR、PSC,计数方式以及脉冲输出模式等。
2)初始化编码器接口
初始化编码器接口模式定时器三的通道一和通道二,用来采集反馈信号。
3)PID算法实现
实现增量式 PID算法,调整好比例P,积分I,微分D的参数。
4)速度闭环运动控制
设置电机旋转方向,以及将PID控制器输出的期望值,作用于电机运动。
5)编写中断服务函数
在定时器6的更新中断回调函数里面进行速度环PID计算,计算后的结果用于定时控制电机,设置比较值。
20.1.2.2 程序流程图

图20.1.2.2.1 速度闭环控制流程图
20.1.2.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。首先先看下stepper_motor.h头文件:
#define PULSE_REV 1600 /* 每圈脉冲数 */
#define TIM_FREQ 170000000U /* 定时器主频 */
#define T1_FREQ (TIM_FREQ / 170) /* 频率ft值 */
/* 对定时器的频率做单位换算,避免数值太大溢出 */
#define FREQ_UINT (T1_FREQ / (SECOND / SAMPLING_PERIOD))
/* 编码器单圈线数*4倍频(根据编码器线数) */
#define ENCODER_SPR (float)(600 * 4)
/* 编码器和步进电机驱动器的比值(表示每个编码器输出线数对应的步进电机脉冲数) */
#define FEEDBACK_CONST (float)(PULSE_REV / ENCODER_SPR)
#define RUN (1) /* 电机运动标志 */
#define STOP (0) /* 电机停机标志 */
/* 电机参数结构体 */
typedef struct
{
uint8_t state; /* 电机状态 */
uint8_t dir; /* 电机方向 */
float speed; /* 电机实际速度 */
float setspeed; /* 电机目标速度 */
int32_t location; /* 电机位置 */
} Motor_TypeDef;
enum dir
{
CCW = 0, /* 逆时针旋转 */
CW , /* 顺时针旋转 */
};
/*************************************************************************/
/* 步进电机引脚定义*/
#define STEPPER_MOTOR_1 1 /* 步进电机接口序号 */
#define STEPPER_MOTOR_2 2
/* 步进电机方向引脚定义 */
#define STEPPER_DIR1_GPIO_PIN GPIO_PIN_2
#define STEPPER_DIR1_GPIO_PORT GPIOF
#define STEPPER_DIR1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0) /* PF口时钟使能 */
#define STEPPER_DIR2_GPIO_PIN GPIO_PIN_5
#define STEPPER_DIR2_GPIO_PORT GPIOE
#define STEPPER_DIR2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能 */
/* 步进电机脱机引脚定义 */
#define STEPPER_EN1_GPIO_PIN GPIO_PIN_9
#define STEPPER_EN1_GPIO_PORT GPIOF
#define STEPPER_EN1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0) /* PF口时钟使能 */
#define STEPPER_EN2_GPIO_PIN GPIO_PIN_6
#define STEPPER_EN2_GPIO_PORT GPIOE
#define STEPPER_EN2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能 */
上面主要包含两部分,第一部分的宏定义是步进电机本身的参数和闭环控制所需要用到的一些参数,包含步进电机单圈脉冲数,定时器频率,其中宏FEEDBACK_CONST 是细分后步进电机单圈脉冲数与编码器单圈脉冲数的比值,因为在闭环系统中,反馈的PID期望值为编码器的脉冲数,我们需要转换成与步进电机转速相关的参数;第二部分为2个步进电机接口的相关宏定义。接着看下PID头文件pid.h:
/*定义PID参数相关宏*/
#define S_KP 0.2f /* P参数 */
#define S_KI 0.05f /* I参数 */
#define S_KD 0.00f /* D参数 */
#define SMAPLSE_PID_SPEED 20 /* 采样率 单位ms */
/* PID结构体 */
typedef struct
{
__IO float SetPoint; /* 目标值 */
__IO float ActualValue; /* 期望输出值 */
__IO float SumError; /* 误差累计 */
__IO float Proportion; /* 比例常数 P */
__IO float Integral; /* 积分常数 I */
__IO float Derivative; /* 微分常数 D */
__IO float Error; /* Error[1] */
__IO float LastError; /* Error[-1] */
__IO float PrevError; /* Error[-2] */
__IO float IngMin; /* 积分限制 */
__IO float IngMax;
__IO float OutMin; /* 输出限制 */
__IO float OutMax;
} PID_TypeDef;
上述代码主要设置P、I、D参数以及采样时间的数值,PID_TypeDef 定义的是PID的结构体,主要参与PID算法计算相关。接着看下PID初始化以及PID的控制算法:
PID_TypeDef g_speed_pid; /* 速度环PID参数结构体 */
/**
* @brief pid初始化
* @param 无
* @retval 无
*/
void pid_init(void)
{
/*速度环初始化*/
g_speed_pid.SetPoint = 0; /* 设定目标Desired Value*/
g_speed_pid.ActualValue = 0.0; /* 期望值*/
g_speed_pid.SumError = 0.0; /* 积分值*/
g_speed_pid.Error = 0.0; /* Error[1]*/
g_speed_pid.LastError = 0.0; /* Error[-1]*/
g_speed_pid.PrevError = 0.0; /* Error[-2]*/
g_speed_pid.Proportion = S_KP; /* 比例常数 Proportional Const*/
g_speed_pid.Integral = S_KI; /* 积分常数 Integral Const*/
g_speed_pid.Derivative = S_KD; /* 微分常数 Derivative Const*/
g_speed_pid.IngMax = 20;
g_speed_pid.IngMin = -20;
g_speed_pid.OutMax = 600; /* 输出限制 */
g_speed_pid.OutMin = -600;
}
/**
* @brief pid闭环控制
* @param *PID:PID结构体变量地址
* @param Feedback_value:当前实际值
* @retval 期望输出值
*/
int32_t increment_pid_ctrl(PID_TypeDef *PID,float Feedback_value)
{
PID->Error = (float)(PID->SetPoint - Feedback_value); /* 计算偏差 */
/* 比例环节 */
PID->ActualValue += (PID->Proportion * (PID->Error - PID->LastError))
+ (PID->Integral * PID->Error) /* 积分环节 */
+ (PID->Derivative * (PID->Error - 2 * PID->LastError + PID->PrevError)); /* 微分环节 */
PID->PrevError = PID->LastError; /* 存储偏差,用于下次计算 */
PID->LastError = PID->Error;
/* 输出限制 */
if(PID->ActualValue > PID->OutMax)
{
PID->ActualValue = PID->OutMax;
}
else if(PID->ActualValue < PID->OutMin)
{
PID->ActualValue = PID->OutMin;
}
return ((int32_t)(PID->ActualValue)); /* 返回计算后输出的数值 */
}
首先初始化PID结构体成员,赋值初始值,其中S_KP、S_KI、S_KD需要不断调试,选择可以让电机最为稳定可靠的数据,increment_pid_ctrl函数为PID控制器,该控制器使用的是增量式算法,他的入口参数第一个为目标值,第二个为反馈回来的实际值,然后使用增量式公式计算输出期望值。接着我们来看下定时器的更新中断回调函数里面是如何处理的,看下stepper_tim.c:
/**
* @brief 定时器中断回调函数
* @param htim:定时器句柄指针
* @note 此函数会被定时器中断函数共同调用的
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t val = 0;
if(htim->Instance == GTIM_TIMX_ENCODER)
{
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle))
{
g_timx_encode_count--;
}
else
{
g_timx_encode_count++;
}
}
else if(htim->Instance == BTIM_TIMX_INT)
{
g_encode_now = gtim_get_encode(); /* 获取编码器值,用于计算速度 */
if(val == SMAPLSE_PID_SPEED) /* 每20ms进来一次 */
{
if(g_run_flag == RUN) /* 电机启动 */
{
/* 实际速度(使用编码器运算)速度 = 一分钟的脉冲数/一圈所需要的脉冲数 就可以求出RPM*/
g_speed = g_encode_now - g_encode_old;
g_encode_old = g_encode_now;
g_step_motor.speed = g_speed * ((1000 / SMAPLSE_PID_SPEED) *
60.0) / (uint16_t)(ENCODER_SPR);
/* PID控制器运算 */
g_motor_pwm = increment_pid_ctrl(&g_speed_pid,
g_step_motor.speed);
(g_motor_pwm > 0) ? (g_step_motor.dir = CW) :
(g_step_motor.dir = CCW);
g_motor_pwm = abs(g_motor_pwm);
stepper_motion_ctrl(g_step_motor.dir, g_motor_pwm);
g_send_flag = 1;
}else
{
g_speed = 0;
g_step_motor.speed = 0;
g_motor_pwm = 0;
g_step_angle = 0;
}
val = 0;
}
val ++;
}
}
上述代码主要可分为两部分。第一部分,编码器的溢出计数,当定时器溢出时,会进入该中断,第二部分,是在基本定时器TIM6中循环计算步进电机当前的转速,注意TIM6中是每20ms执行一次编码器的读数,并将在20ms采集到的编码器读数,转换成步进电机的实际转速,然后代入PID控制器,将输出的期望值代入闭环控制函数stepper_motion_ctrl中,实现步进电机的闭环控制。下面我们重点来介绍下闭环控制函数stepper_motion_ctrl,我们先来看下该函数的实现:
/**
* @brief: 步进电机运动控制
* @param: dir:步进电机运动方向 0:反转 1正转
location_m: PID输出的期望值(该期望值为编码器数值)
* @retval 无
*/
void stepper_motion_ctrl(uint8_t dir, uint16_t location_m)
{
float step_delay = 0.0; /* 步进延时 */
/* 当目标转速与实际转速一致时,可直接返回,使g_step_angle还是上一次的数值 */
if(location_m == 0) return;
ST1_DIR(dir); /* 设置旋转方向 */
/* 经过PID计算得到的结果是编码器的输出期望值,
将其转换为步进电机所需的脉冲数( 编码器期望值 * (步进电机一圈所需脉冲数 / 编码器一圈计数值))*/
step_delay = (float)(SMAPLSE_PID_SPEED / (location_m * FEEDBACK_CONST)); /* 单个脉冲的时间宽度,单位: ms */
/* ms转成s step_delay = step_delay/1000 */
/*
T : 单个脉冲所需的时间
c : 需要求解的比较值
f : 定时器计数频率
T = c *(1/f)
step_delay/1000 = c*1/1000000 (单位是s)
c = step_delay*1000 求出整一个脉冲的计数值,需要除以2 因为一个完整的脉冲需要翻转2次
所要的计数值:c/2 = Sstep_delay*1000/2 = step_delay*500
*/
/* 算出来的结果是周期,这里除以2,半周期翻转一次 */
g_step_angle = (uint16_t)(step_delay * 500);
HAL_TIM_OC_Start_IT(&g_atimx_handle, ATIM_TIMX_PWM_CH1);
}
上述代码是整个步进电机速度闭环控制的核心代码,首先函数stepper_motion_ctrl有两个入口参数,第一个为旋转方向,第二个为经过PID计算输出的期望值,也就是编码器的期望值,注意这个函数是每20ms在中断里边调用一次,即20ms执行一次该函数。函数内部操作如下:
① 判断期望值是否为0。如果为0,代表已到目标转速,则直接返回,不需要重新计算step_delay的值,使其保持之前的值。注意该值作用于TIM8的比较值直接影响脉冲频率,进而影响步进电机的实际转速;
② 不为0时,代表还未到达目标转速,所以首先设置旋转方向,然后将编码器的期望值转换成步进电机的脉冲数期望值,并求出单个脉冲需要的时间,知道了单个脉冲时间就可以通过T = c *(1/f),求出比较值c的值,求解步骤请看代码注释较为详细,然后使能定时器输出以及输出中断。接着将求得的比较值c作用到定时器上,在输出比较中断的回调函数中实现调用:
/**
* @brief 定时器比较中断
* @param htim:定时器句柄指针
* @note 无
* @retval 无
*/
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
__IO uint32_t tim_count = 0;
__IO uint32_t tmp = 0;
if(htim->Instance == ATIM_TIMX_PWM)
{
tim_count = __HAL_TIM_GET_COUNTER(&g_atimx_handle);
tmp = (tim_count + g_step_angle) & 0xFFFF;
__HAL_TIM_SetCompare(&g_atimx_handle, ATIM_TIMX_PWM_CH1, tmp);
}
}
这样就可以直接作用到步进电机的转轴上,并控制其转速了,接着在main函数里面编写如下代码:
int main(void)
{
uint8_t key, t;
char buf[32];
uint8_t debug_cmd = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 初始化串口 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
lcd_init(); /* 初始化LCD */
stepper_init(0xFFFF - 1, 170 - 1); /* 1Mhz的计数频率 */
btim_timx_int_init(1000 - 1, 170 - 1); /* 基本定时器初始化,1ms中断一次*/
gtim_timx_encoder_chy_init(0xFFFF - 1, 0); /* 编码器接口初始化 */
pid_init(); /* PID初始化 */
/* LCD显示提示信息 */
lcd_show_string(10, 10, 200, 24, 24,"Stepper Test", RED);
lcd_show_string(10, 40, 200, 16, 16,"KEY0:SPEED ++", RED);
lcd_show_string(10, 60, 200, 16, 16,"KEY1:SPEED --", RED);
lcd_show_string(10, 80, 200, 16, 16,"KEY2:STOP", RED);
lcd_show_string(10, 100, 200, 16, 16,"Stepper ID: 1", BLUE);
printf("KEY0 增加目标转速\r\n");
printf("KEY1 减少目标转速\r\n");
printf("KEY2 停机\r\n");
#if DEBUG_ENABLE /* 开启调试 */
debug_init(); /* PID调试初始化 */
debug_send_motorstate(IDLE_STATE); /* 空闲状态 */
debug_send_motorcode(STEP_MOTOR); /* 发送电机类型 */
/* 初始化同步数据(选择第x组PIDX,目标速度地址,P,I,D参数)到上位机 */
debug_send_initdata(TYPE_PID1, user_setpoint, S_KP, S_KI, S_KD);
#endif
while (1)
{
#if DEBUG_ENABLE /* Debug发送部分 */
if(g_send_flag)
{
g_send_flag = 0;
/* 选择通道1 发送实际速度 */
debug_send_wave_data(1, (int16_t)g_step_motor.speed);
/* 选择通道2 发送设置的目标速度 */
debug_send_wave_data(2, (int16_t)*user_setpoint);
}
#endif
t++;
if(t % 20 == 0)
{
sprintf(buf,"Set_Speed:%.2f",*user_setpoint);
lcd_show_string(10, 120, 239, 16, 16, buf, RED);
sprintf(buf,"Motor_Speed :%3d RPM ",(int16_t)g_step_motor.speed);
lcd_show_string(10, 140, 239, 16, 16, buf, RED);
LED0_TOGGLE(); /* LED0(红灯) 翻转 */
}
delay_ms(10);
/****************************** 按键部分 ****************************/
key = key_scan(0);
if(key == KEY0_PRES) /* 减速 */
{
g_run_flag = RUN;
stepper_star(STEPPER_MOTOR_1); /* 启动电机 */
debug_send_motorstate(RUN_STATE); /* 电机运行 */
*user_setpoint += 50; /* 增加目标速度 */
if (*user_setpoint >= 500) /* 限速 */
{
*user_setpoint = 500;
}
else if (*user_setpoint == 0)
{
g_run_flag = STOP;
stepper_stop(STEPPER_MOTOR_1); /* 关闭电机 */
debug_send_motorstate(BREAKED_STATE); /* 电机刹车 */
}
lcd_show_num(100, 120, *user_setpoint, 3, 16, RED);
}
else if(key == KEY1_PRES) /* 减速 */
{
g_run_flag = RUN;
stepper_star(STEPPER_MOTOR_1); /* 开启电机 */
debug_send_motorstate(RUN_STATE); /* 电机运行 */
*user_setpoint -= 50; /* 减少目标速度 */
if (*user_setpoint <= -500) /* 限速 */
{
*user_setpoint = -500;
}
else if (*user_setpoint == 0)
{
g_run_flag = STOP;
stepper_stop(STEPPER_MOTOR_1); /* 关闭电机 */
debug_send_motorstate(BREAKED_STATE); /* 电机刹车 */
}
lcd_show_num(100, 120, *user_setpoint, 3, 16, RED);
}
else if (key == KEY2_PRES) /* 停机 */
{
g_run_flag = STOP;
stepper_stop(STEPPER_MOTOR_1); /* 关闭电机 */
debug_send_motorstate(BREAKED_STATE); /* 电机刹车 */
pid_init();
*user_setpoint = 0;
lcd_show_num(100, 120, *user_setpoint, 3, 16, RED);
}
/************************* Debug接收部分 ********************************/
#if DEBUG_ENABLE
/* 查询接收PID助手的PID1参数*/
debug_receive_pid(TYPE_PID1, (float*)&g_speed_pid.Proportion,
(float*)&g_speed_pid.Integral, (float*)&g_speed_pid.Derivative);
debug_set_point_range(500, -500, 500); /* 设置目标调节范围 */
debug_cmd = debug_receive_ctrl_code(); /* 读取命令 */
if(debug_cmd == HALT_CODE) /* 停机 */
{
pid_init();
g_run_flag = STOP; /* 标记停机 */
stepper_stop(STEPPER_MOTOR_1);
}
else if (debug_cmd == RUN_CODE) /* 运行(解除刹车和停机) */
{
*user_setpoint = 50; /* 设置目标转速50RPM */
g_run_flag = RUN; /* 运行标记 */
stepper_star(STEPPER_MOTOR_1); /* 启动电机 */
debug_send_motorstate(RUN_STATE); /* 电机运行 */
}
else if (debug_cmd == BREAKED) /* 刹车(需要运行状态并且修改目标速度才可解除) */
{
pid_init();
g_run_flag = STOP; /* 标记停机 */
stepper_stop(STEPPER_MOTOR_1);
*user_setpoint = g_step_motor.speed; /* 将刹车后的实际速度赋值给目标值 */
debug_send_motorstate(BREAKED_STATE);/* 电机刹车 */
}
#endif
}
}
main.c的代码逻辑如下:
第一步,初始化相关的定时器、串口,本实验需要利用串口1和上位机进行通讯。
第二步,初始化PID参数、上位机调试,它们分别调用的是pid_init和debug_init函数。
第三步,上传电机的状态(空闲)、类型(直流有刷电机)、PID参数到上位机,它们分别调用的是debug_send_motorcode、debug_send_motorstate以及debug_send_initdata函数。
第四步,在while循环里面检测按键是否按下,如果key0按下,则目标速度增加50,上传电机状态(运行);如果key1按下,则目标速度减小50,上传电机状态(运行);如果key2按下,则停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机。
第五步,接收上位机下发的PID参数、设置目标速度的调节范围,它们分别调用的是debug_receive_pid以及debug_set_point_range函数。我们这里重点介绍一下后者,debug_set_point_range函数的第一、二个入口参数分别传入目标速度的最大值、最小值,它们的正负号代表电机的正反转方向,第三个入口参数传入的是电机速度的最大突变值。
第六步,调用debug_receive_ctrl_code函数,接收上位机下发的命令。如果上位机的命令为BREAKED(刹车),则停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机;如果上位机的命令是RUN_CODE,则开启电机,设置目标速度为50RPM,上传电机状态(运行)到上位机。
第七步,每隔200ms更新一次数据到屏幕,调用debug_send_speed函数发送实际速度值到上位机。
20.1.3 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,LCD上显示按键功能、占空比以及电机速度信息,当我们按下KEY0,目标速度将增大;按下KEY1,目标速度将减小;按下KEY2,电机将停止。目标速度为正数时,电机正转,反之电机反转。我们再打开PID调试助手,选择对应的串口端口,我这边是COM17,接着选择通道1和2,点击“开始”按钮,即可开始显示波形,如下图20.1.3.1所示:

图20.1.3.1 速度环PID控制波形
图20.1.3.1中,橙线代表目标速度,红线代表实际速度,当我们按下KEY0,目标速度增大,橙线先发生变化,而红线(实际速度)会逐渐靠近橙线(目标速度);按下KEY1,目标速度将减小,曲线的变化同理;按下KEY2,电机将停止,目标速度将为0。
注意:1、本实验需要使用USB数据线连接开发板的串口1到电脑,并启动电机之后才会有波形变化;2、如果发现波形不对,请检查编码器的A相和B相接线;3、PID系数并不是通用的,如果PID曲线不理想,大家需要根据自己的实际系统去调节。
20.2 步进电机位置闭环控制
位置环PID控制的原理非常简单,我们用编码器计数总值代表电机的位置,然后把PID控制流程中的控制对象换成电机位置即可。

图20.2.1 步进电机位置闭环系统控制原理
图20.2.1为步进电机闭环系统的整体控制流程,其中编码器作为反馈通道,反馈步进电机的实际位置,系统将实际位置与目标位置进行比较,计算差值,再把偏差值代入到PID控制器中,之后控制器输出期望值,最后作用于步进电机使其旋转至指定位置停止!这就是位置闭环控制。
20.2.1 硬件设计
1、例程功能
l 本实验使用步进电机接口1控制
l 按键KEY0用于启停电机,按键 KEY1增加目标位置,按键KEY2减少目标位置;
l 可通过串口1即USB_TTL接口连接正点原子PID调试助手,查看PID波形;
l 注意驱动器使用8细分,也就是旋转一圈所需要1600个脉冲数;
l LED0作为程序运行状态指示灯,闪烁周期200ms。
2、硬件资源
1)LED灯:LED0 – PE0
2)独立按键
KEY0 – PE12
KEY1 – PE13
KEY2 – PE14
3)定时器: TIM8_CH1:PC6,对应步进电机接口1
TIM3_CH1 : PA6(编码器接口1)
TIM3_CH2 : PA7(编码器接口2)
TIM6定时采样
方向引脚:DIR1: PF2
脱机引脚:EN1: PF9
4)步进电机
5)步进电机驱动器
6)12-50V的DC电源
7)编码器(本实验使用的是600线的光电编码器)
建议使用固定架将编码器与步进电机固定在一起,以避免编码器的抖动,从而导致编码器读数不准确。如下图所示:

图20.2.1.1 步进电机与编码器的连接图
3、原理图
跟速度闭环控制完全一致,这里就不重复讲解了。
20.2.2 程序设计
20.2.2.1 步进电机位置闭环控制的配置步骤
1)初始化定时器
初始化定时器通道IO,设置ARR、PSC,计数方式以及脉冲输出模式等。
2)初始化编码器接口
初始化编码器接口模式定时器三的通道一和通道二,用来采集反馈信号。
3)PID算法实现
实现增量式/位置式PID算法,调整好比例P,积分I,微分D的参数。
4)位置闭环运动控制
设置电机旋转方向,以及将PID控制器输出的期望值,作用于电机运动。
5)编写中断服务函数
编码器溢出计数,定时控制电机,设置比较值。
20.3.2.2 程序流程图

图20.3.2.2.1 闭环控制流程图
20.3.2.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。首先先看下stepper_motor.h头文件:
#define PULSE_REV 1600 /* 每圈脉冲数*/
#define TIM_FREQ 170000000U /* 定时器主频 */
#define T1_FREQ (TIM_FREQ/170) /* 频率ft值*/
/* 对定时器的频率做单位换算,避免数值太大溢出 */
#define FREQ_UINT (T1_FREQ/(SECOND/SAMPLING_PERIOD))
#define ENCODER_SPR (float)(600*4) /* 编码器单圈线数*4倍频 */
/* 步进电机旋转一圈,丝杠的距离;单位:mm/r */
#define MPR 5
/* 每mm内编码器的脉冲数;单位:Pules/mm */
#define PPM (ENCODER_SPR/MPR)
#define MPP ((float)(MPR)/ENCODER_SPR) /* 编码器单步步进距离 */
/* 编码器和步进电机驱动器的比值(表示每个编码器输出线数对应的步进电机脉冲数) */
#define FEEDBACK_CONST (float)(PULSE_REV/ENCODER_SPR)
/* 电机参数结构体 */
typedef struct
{
uint8_t state; /*电机状态*/
uint8_t dir; /*电机方向*/
float speed; /*电机实际速度*/
float setspeed; /*电机实际速度*/
int32_t location; /*电机位置*/
} Motor_TypeDef;
/************************************************************************/
/* 步进电机引脚定义*/
#define STEPPER_MOTOR_1 1 /* 步进电机接口序号 */
#define STEPPER_MOTOR_2 2
/* 步进电机方向引脚定义 */
#define STEPPER_DIR1_GPIO_PIN GPIO_PIN_2
#define STEPPER_DIR1_GPIO_PORT GPIOF
#define STEPPER_DIR1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0) /* PF口时钟使能 */
#define STEPPER_DIR2_GPIO_PIN GPIO_PIN_5
#define STEPPER_DIR2_GPIO_PORT GPIOE
#define STEPPER_DIR2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能 */
/* 步进电机脱机引脚定义 */
#define STEPPER_EN1_GPIO_PIN GPIO_PIN_9
#define STEPPER_EN1_GPIO_PORT GPIOF
#define STEPPER_EN1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0) /* PF口时钟使能 */
#define STEPPER_EN2_GPIO_PIN GPIO_PIN_6
#define STEPPER_EN2_GPIO_PORT GPIOE
#define STEPPER_EN2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能 */
上面主要包含两部分,第一部分的宏定义是步进电机本身的参数和闭环控制所需要用到的一些参数,包含步进电机单圈脉冲数,定时器频率,其中宏FEEDBACK_CONST 是细分后步进电机单圈脉冲数与编码器单圈脉冲数的比值,因为在闭环系统中,反馈的PID期望值为编码器的脉冲数,我们需要转换成与步进电机相关脉冲数;第二部分为2个步进电机接口的相关宏定义。接着看下PID头文件pid.h:
/* PID相关参数 */
#define INCR_LOCT_SELECT 1 /* 0:位置式,1:增量式 */
#if INCR_LOCT_SELECT
/*定义PID参数相关宏*/
#define L_KP 0.5f /* P参数 */
#define L_KI 0.05f /* I参数 */
#define L_KD 0.00f /* D参数 */
#define SMAPLSE_PID_SPEED 20 /* 采样率 单位ms */
#else
#define L_KP 0.5f /* P参数 */
#define L_KI 0.05f /* I参数 */
#define L_KD 0.00f /* D参数 */
#define SMAPLSE_PID_SPEED 20 /* 采样率 单位ms */
#endif
/* PID结构体 */
typedef struct
{
__IO float SetPoint; /* 目标值 */
__IO float ActualValue; /* 期望输出值 */
__IO float SumError; /* 误差累计 */
__IO float Proportion; /* 比例常数 P */
__IO float Integral; /* 积分常数 I */
__IO float Derivative; /* 微分常数 D */
__IO float Error; /* Error[1] */
__IO float LastError; /* Error[-1] */
__IO float PrevError; /* Error[-2] */
__IO float IngMin; /* 积分限制 */
__IO float IngMax;
__IO float OutMin; /* 输出限制 */
__IO float OutMax;
} PID_TypeDef;
首先通过宏INCR_LOCT_SELECT设置0或1选择位置式还是增量式PID算法,接着通过不同的宏定义选择不同的P、I、D参数以及采样时间,PID_TypeDef 定义的是PID的结构体,主要参与PID算法计算相关。接着看下PID初始化以及PID的控制算法:
PID_TypeDef g_location_pid; /* 位置环PID参数结构体 */
/**
* @brief pid初始化
* @param 无
* @retval 无
*/
void pid_init(void)
{
/*位置环初始化*/
g_location_pid.SetPoint = (float)(50 * PPM);/* 设定目标Desired Value*/
g_location_pid.ActualValue = 0.0; /* 期望值*/
g_location_pid.SumError = 0.0; /* 积分值*/
g_location_pid.Error = 0.0; /* Error[1]*/
g_location_pid.LastError = 0.0; /* Error[-1]*/
g_location_pid.PrevError = 0.0; /* Error[-2]*/
g_location_pid.Proportion = L_KP; /* 比例常数 ProportionalConst*/
g_location_pid.Integral = L_KI; /* 积分常数 Integral Const*/
g_location_pid.Derivative = L_KD; /* 微分常数 Derivative Const*/
g_location_pid.IngMax = 20;
g_location_pid.IngMin = -20;
g_location_pid.OutMax = 150; /* 输出限制 */
g_location_pid.OutMin = -150;
}
/**
* @brief pid闭环控制
* @param *PID:PID结构体变量地址
* @param Feedback_value:当前实际值
* @retval 期望输出值
*/
int32_t increment_pid_ctrl(PID_TypeDef *PID,float Feedback_value)
{
PID->Error = (float)(PID->SetPoint - Feedback_value); /* 计算偏差 */
#if INCR_LOCT_SELECT /* 增量式PID */
/* 比例环节 */
PID->ActualValue += (PID->Proportion * (PID->Error - PID->LastError))
+ (PID->Integral * PID->Error) /* 积分环节 */
+ (PID->Derivative * (PID->Error - 2 * PID->LastError
+ PID->PrevError)); /* 微分环节 */
PID->PrevError = PID->LastError; /* 存储偏差,用于下次计算 */
PID->LastError = PID->Error;
#else /* 位置式PID */
PID->SumError += PID->Error;
if (PID->SumError > PID->IngMax)
{
PID->SumError = PID->IngMax;
}
else if (PID->SumError < PID->IngMin)
{
PID->SumError = PID->IngMin;
}
PID->ActualValue = (PID->Proportion * PID->Error) /* 比例环节 */
+ (PID->Integral * PID->SumError) /* 积分环节 */
+ (PID->Derivative * (PID->Error - PID->LastError));
PID->LastError = PID->Error;
#endif
if(PID->ActualValue > PID->OutMax)
{
PID->ActualValue = PID->OutMax;
}
else if(PID->ActualValue < PID->OutMin)
{
PID->ActualValue = PID->OutMin;
}
return ((int32_t)(PID->ActualValue)); /* 返回计算后输出的数值 */
}
初始化PID结构体成员,赋值初始值,其中L_KP、L_KI、L_KD需要不断调试,选择可以让电机最为稳定可靠的数据,increment_pid_ctrl函数为PID控制器,他的入口参数第一个为目标值,第二个为反馈回来的实际值,然后通过宏INCR_LOCT_SELECT选择相应公式并计算输出期望值。接着我们来看下定时器的更新中断回调函数里面是如何处理的,看下stepper_tim.c:
/**
* @brief 定时器中断回调函数
* @param htim:定时器句柄指针
* @note 此函数会被定时器中断函数共同调用的
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t val = 0;
static uint16_t sum_pulse = 0;
if(htim->Instance == GTIM_TIMX_ENCODER)
{
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle))
{
g_timx_encode_count--;
}
else
{
g_timx_encode_count++;
}
}
else if(htim->Instance == BTIM_TIMX_INT)
{
g_encode_now = gtim_get_encode(); /* 获取编码器值,用于计算速度 */
if(val == SMAPLSE_PID_SPEED)
{
g_speed = g_encode_now - g_encode_old;
sum_pulse += abs(g_speed);
g_encode_old = g_encode_now;
/* 实际速度(使用编码器运算)速度 = 一分钟的脉冲数/一圈所需要的脉冲数 就可以求出RPM*/
g_step_motor.speed = g_speed * ((1000 / SMAPLSE_PID_SPEED) * 60.0)
/ (uint16_t)(ENCODER_SPR);
/* 通过求出的比较值c的大小,算出设定速度大小(脉冲运算转速) */
g_step_motor.setspeed = ((float)(1000000 / (uint16_t)(g_step_angle
* 2)) / PULSE_REV) * 60;
if(g_run_flag == RUN) /* 电机启动 */
{
g_step_motor.location = gtim_get_encode();
/* 限制最大20ms的时间内步进电机只能接收150*1600/2400=100个脉冲 限制速度
20ms最多输出100个脉冲,实际就是1ms输出5个脉冲也就是1s输出5000个脉冲
电机旋转一圈需要1600个脉冲也就是 5000/1600 = 3.125转每秒
换算成rpm的话:3.125*60 = 187.5rpm(转每分钟)*/
g_motor_pwm = increment_pid_ctrl(&g_location_pid,
g_step_motor.location);
(g_motor_pwm > 0) ? (g_step_motor.dir = CW) :
(g_step_motor.dir = CCW);
g_motor_pwm = abs(g_motor_pwm);
stepper_motion_ctrl(g_step_motor.dir, g_motor_pwm);
g_send_flag = 1;
}
val = 0;
}
val ++;
}
}
上述代码主要可分为两部分。第一部分,编码器的溢出计数,当定时器溢出时,会进入该中断,第二部分,是在基本定时器TIM6中循环计算步进电机当前的转速,注意TIM6中是每20ms执行一次编码器的读数,并将在20ms采集到的编码器读数,即编码器的实际位置,然后代入PID控制器,将输出的期望值代入闭环控制函数stepper_motion_ctrl中,实现步进电机的闭环控制。下面我们重点来介绍下闭环控制函数stepper_motion_ctrl,我们先来看下该函数的实现:
/**
* @brief: 步进电机位置运动控制
* @param: dir:步进电机运动方向 0:反转 1正转
location_m: PID输出的期望值(该期望值为编码器数值)
* @retval 无
*/
void stepper_motion_ctrl(uint8_t dir, uint16_t location_m)
{
float step_delay = 0.0; /* 步进延时 */
if(location_m == 0)
{
/* 当期望值与目标值一致时,代表已到指定位置,停止输出 */
HAL_TIM_OC_Stop_IT(&g_atimx_handle, ATIM_TIMX_PWM_CH1);
g_step_angle = 0; /* 清空数据 */
}
else
{
ST1_DIR(dir); /* 设置旋转方向 */
/* 经过PID计算得到的结果是编码器的输出期望值,
将其转换为步进电机所需的脉冲数( 编码器期望值 * (步进电机一圈所需脉冲数 / 编码
器一圈计数值))*/
step_delay = (float)(SMAPLSE_PID_SPEED / (location_m *
FEEDBACK_CONST)); /* 单个脉冲的时间宽度,单位: ms */
/* ms转成s step_delay = step_delay/1000 */
/*
T : 单个脉冲所需的时间
c : 需要求解的比较值
f : 定时器计数频率
T = c *(1/f)
step_delay/1000 = c*1/1000000 (单位是s)
c = step_delay*1000 求出整一个脉冲的计数值,需要除以2 因为一个完整的脉冲
需要翻转2次
所要的计数值:c/2 = Sstep_delay*1000/2 = step_delay*500
*/
/* 算出来的结果是周期,这里除以2,半周期翻转一次 */
g_step_angle = (uint16_t)(step_delay * 500);
HAL_TIM_OC_Start_IT(&g_atimx_handle, ATIM_TIMX_PWM_CH1);
}
}
上述代码是整个步进电机位置闭环控制的核心代码,首先函数stepper_motion_ctrl有两个入口参数,第一个为旋转方向,第二个为经过PID计算输出的期望值,也就是编码器的期望值,注意这个函数是每20ms在中断里边调用一次,即20ms执行一次该函数。函数内部操作如下:
① 判断期望值是否为0。如果为0,代表已到目标位置,关闭定时器脉冲输出;
② 不为0时,代表还未到达目标位置,需继续驱动步进电机旋转,所以首先设置旋转方向,然后将编码器的期望值转换成步进电机的脉冲数期望值,并求出单个脉冲需要的时间,知道了单个脉冲时间就可以通过T = c *(1/f),求出比较值c的值,然后能定时器输出以及输出中断。接着将求得的比较值c作用到定时器上,在输出比较中断回调函数中实现调用:
/**
* @brief 定时器比较中断
* @param htim:定时器句柄指针
* @note 无
* @retval 无
*/
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
__IO uint32_t tim_count=0;
__IO uint32_t tmp = 0;
if(htim->Instance == ATIM_TIMX_PWM)
{
tim_count=__HAL_TIM_GET_COUNTER(&g_atimx_handle);
tmp = (tim_count+step_angle)&0xFFFF;
__HAL_TIM_SetCompare(&g_atimx_handle,TIM_CHANNEL_3, tmp);
}
}
接着在main函数里面编写如下代码:
int main(void)
{
uint8_t key, t;
uint8_t flag_stop = 0;
char buf[32];
uint8_t debug_cmd = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 初始化串口 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
lcd_init(); /* 初始化LCD */
stepper_init(0xFFFF - 1, 170 - 1); /* 1Mhz的计数频率 */
/* 基本定时器初始化,中断周期1ms中断一次 */
btim_timx_int_init(1000 - 1, 170 - 1);
gtim_timx_encoder_chy_init(0xFFFF - 1, 0); /* 编码器接口初始化 */
pid_init(); /* PID初始化 */
/* LCD显示提示信息 */
lcd_show_string(10, 10, 200, 24, 24,"Stepper Test", RED);
lcd_show_string(10, 40, 200, 16, 16,"KEY0:Start/Stop", RED);
lcd_show_string(10, 60, 200, 16, 16,"KEY1:STEP ++", RED);
lcd_show_string(10, 80, 200, 16, 16,"KEY2:STEP --", RED);
lcd_show_string(10, 100, 200, 16, 16,"Stepper ID: 1", BLUE);
printf("KEY0 启停电机\r\n");
printf("KEY1 增加目标位置\r\n");
printf("KEY2 减少目标位置\r\n");
#if DEBUG_ENABLE /* 开启调试 */
debug_init(); /* PID调试初始化 */
debug_send_motorstate(IDLE_STATE); /* 初始电机为空闲状态 */
debug_send_motorcode(STEP_MOTOR); /* 发送电机类型 */
/* 初始化同步数据(选择第x组PIDX,目标位置地址,P,I,D参数)到上位机 */
debug_send_initdata(TYPE_PID1, user_setpoint, L_KP, L_KI, L_KD);
#endif
while (1)
{
#if DEBUG_ENABLE /* Debug发送部分 */
if(g_send_flag)
{
g_send_flag = 0; /* 选择通道1 发送实际位置 */
debug_send_wave_data(1, (int16_t)g_step_motor.location);
/* 选择通道2 发送设置的目标位置 */
debug_send_wave_data(2, (int16_t)*user_setpoint);
}
#endif
t++;
if(t % 20 == 0)
{
sprintf(buf,"Set_Locaion:%f",*user_setpoint);
lcd_show_string(10, 120, 239, 16, 16, buf, RED);
sprintf(buf,"Encode:%d ",gtim_get_encode());
lcd_show_string(10, 140, 239, 16, 16, buf, RED);
sprintf(buf,"Motor_Speed :%3d RPM ",(int16_t)g_step_motor.speed);
lcd_show_string(10, 160, 239, 16, 16, buf, RED);
sprintf(buf,"Set_Speed :%3d RPM ",(int16_t)g_step_motor.setspeed);
lcd_show_string(10, 180, 239, 16, 16, buf, RED);
LED0_TOGGLE(); /* LED0(红灯) 翻转 */
}
delay_ms(10);
/************************按键部分**********************************/
key = key_scan(0);
if(key == KEY0_PRES) /* 电机状态翻转 */
{
flag_stop = !flag_stop;
if(flag_stop)
{
g_run_flag = RUN;
stepper_star(STEPPER_MOTOR_1); /* 启动电机 */
}
else
{
g_run_flag = STOP;
stepper_stop(STEPPER_MOTOR_1); /* 关闭电机 */
}
}
else if(key == KEY1_PRES) /* 步数 ++ */
{
*user_setpoint += (20 * PPM);
if(*user_setpoint >= 60 * PPM)
{
*user_setpoint = 60 * PPM;
}
lcd_show_num(100, 120, *user_setpoint, 3, 16, RED);
}
else if(key == KEY2_PRES) /* 步数 -- */
{
*user_setpoint -= (20 * PPM);
if(*user_setpoint <= -60 * PPM)
{
*user_setpoint = -60 * PPM;
}
lcd_show_num(100, 120, *user_setpoint, 3, 16, RED);
}
/*************************** Debug接收部分 ****************************/
#if DEBUG_ENABLE
/* 查询接收PID助手的PID1参数*/
debug_receive_pid(TYPE_PID1, (float*)&g_location_pid.Proportion,
(float*)&g_location_pid.Integral,(float*)&g_location_pid.Derivative);
/* 设置目标调节范围 */
debug_set_point_range((float)(60 * PPM), (float)(-60 * PPM), 120 * PPM);
debug_cmd = debug_receive_ctrl_code(); /* 读取命令 */
if(debug_cmd == HALT_CODE) /* 停机 */
{
g_run_flag = STOP; /* 标记停机 */
stepper_stop(STEPPER_MOTOR_1);
}
else if (debug_cmd == RUN_CODE) /* 运行(解除刹车和停机) */
{
g_run_flag = RUN; /* 运行标记 */
stepper_star(STEPPER_MOTOR_1); /* 启动电机 */
debug_send_motorstate(RUN_STATE); /* 电机运行 */
}
else if (debug_cmd == BREAKED) /* 刹车(需要运行状态并且修改目标位置才可解除)*/
{
*user_setpoint = g_step_motor.location; /* 将刹车后的实际位置赋值给目标值*/
debug_send_motorstate(BREAKED_STATE); /* 电机刹车 */
}
#endif
}
}
main.c的代码逻辑如下:
第一步,初始化相关的定时器、串口,本实验需要利用串口1和上位机进行通讯。
第二步,初始化PID参数、上位机调试,它们分别调用的是pid_init和debug_init函数。
第三步,上传电机的状态(空闲)、类型(直流有刷电机)、PID参数到上位机,它们分别调用的是debug_send_motorcode、debug_send_motorstate以及debug_send_initdata函数。
第四步,在while循环里面检测按键是否按下,如果key0按下,则控制步进电机状态开启/关闭;如果key1按下,则增加目标位置;如果key2按下,则减少目标位置。
第五步,接收上位机下发的PID参数、设置目标速度的调节范围,它们分别调用的是debug_receive_pid以及debug_set_point_range函数。我们这里重点介绍一下后者,debug_set_point_range函数的第一、二个入口参数分别传入目标速度的最大值、最小值,它们的正负号代表电机的正反转方向,第三个入口参数传入的是电机速度的最大突变值。
第六步,调用debug_receive_ctrl_code函数,接收上位机下发的命令。如果上位机的命令为BREAKED(刹车),则停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机;如果上位机的命令是RUN_CODE,则开启电机,上传电机状态(运行)到上位机。
第七步,每隔200ms更新一次数据到屏幕,调用debug_send_speed函数发送实际位置值到上位机。
20.2.3 下载验证
硬件连接好之后下载程序到开发板,液晶屏和串口都会显示提示信息,打开正点原子PID调试助手(注意接上串口1的USB_TTL连接至电脑),当我们按下按键KEY0电机开始启动,通过调节按键KEY1和KEY2,可以看到PID调试助手位置变化曲线:

图20.2.3.1 PID调试助手波形图
图20.2.3.1中,橙线代表目标位置,红线代表实际位置,当我们按下按键KEY0电机开始启动,通过调节按键KEY1和KEY2即可调节目标位置的增大以及减小。
由图20.3.3.1我们发现该曲线更像是一条斜线,是因为我们对步进电机电机做了限速,所以PID输出的期望值大小被限制了,导致每次位置增量值一致,所以看起来类似条斜线。
注意:1、本实验需要使用USB数据线连接开发板的串口1到电脑,并启动电机之后才会有波形变化;2、如果发现波形不对,请检查编码器的A相和B相接线;3、PID系数并不是通用的,如果PID曲线不理想,大家需要根据自己的实际系统去调节。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3116764/ ,如需转载,请注明出处,否则将追究法律责任。