10. 步进电机位置环控制实现¶
关于步进电机闭环控制的原理在上一章 步进电机速度环控制实现 已经详细说明, 我们也在上一章中学习了步进电机速度环的控制方法,接下来,我们继续学习步进电机位置环的控制方法。 在本章中,控制方法的实现与上一章基本相同,最大的区别就是将速度量的控制变为了位置量。
本章通过我们前面学习的位置式PID和增量式PID两种控制方式分别来实现步进电机位置环的控制, 如果还不知道什么是位置式PID和增量式PID,请务必先学习前面PID算法的通俗解说这一章节。
10.1. 硬件设计¶
本实验的硬件设计与编码器的使用章节中的硬件设计完全相同,在此不再赘述。
10.2. 步进电机位置环控制–位置式PID实现¶
本实验会结合之前章节的PID控制和步进电机编码器测速,来讲解如何使用增量式PID对步进电机进行位置闭环控制。 学习本小节内容时,请打开配套的“步进电机位置环控制——增量式PID”工程配合阅读。
10.2.1. 编程要点¶
配置定时器可以输出PWM控制电机
配置定时器可以读取编码器的计数值
配置基本定时器可以产生定时中断来执行PID运算
编写位置式PID算法
编写速度控制函数
增加上位机曲线观察相关代码
10.2.2. 软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本章配套的工程。 关于编码器的部分主要查看前面的编码器章节。
10.2.2.1. 新建工程¶
我们直接在基础部分的直流有刷章节的 “302_Motor_Stepper_PID_Speed_Positional” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 302_Motor_Stepper_PID_Speed_Positional, 然后将工程文件夹重命名为 304_Motor_Stepper_PID_Location_Positional,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 302_Motor_Stepper_PID_Speed_Positional, 然后将工程文件夹重命名为 304_Motor_Stepper_PID_Location_Positional,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
工程新建好之后,在工程根目录的 “src” 文件夹下面,将野火提供的 “pid” 、 “protocol” 文件夹添加进去。 其中主要是PID代码和上位机通信协议代码,关于其中的原理已经在提高部分第三章节讲过,本章不再赘述,只展示部分更改的代码。 工程文件结构如下。
10.2.2.2. FSP配置¶
本例程需要使用按键来调节位置,这样更有利于观察实验现象,所以需要创建按键中断函数, 关于按键中断的内容可以查看《瑞萨RA系列FSP库开发实战指南》的 “ICU——外部中断” 章节, 也可以参考 “002_GPIO_KEY_IRQ” 例程,这里不再赘述。
10.2.2.3. 编码器定时器中断函数¶
在编程要点1在前章节已经讲解过,这里就不在讲解,如果不明白请先学习前面相关章节的内容。 这里需要对读取编码器的计数值部分进行更改。
这段代码的改动主要目的是在位置环控制中实现对电机方向的准确控制,通过让 pulse_period 支持正负数值, 可以直接用来区分正转(计数增加)和反转(计数减少),从而避免额外的方向处理逻辑。 在正转事件 TIMER_EVENT_CAPTURE_A 中,pulse_period 自增,表示编码器正向移动; 而在反转事件 TIMER_EVENT_CAPTURE_B 中,pulse_period 自减,表示编码器反向移动。
/* 全局变量 */
volatile int32_t pulse_period = 0; // 脉冲数
volatile _Bool flag = true; // 方向标志,true 为正转,false 为反转
/* 编码器中断回调函数 */
void encoder_callback(timer_callback_args_t *p_args)
{
switch (p_args->event)
{
/* 捕获事件:正转计数(捕获A事件) */
case TIMER_EVENT_CAPTURE_A:
flag = true; // 设置为正转方向
//更新计数值
pulse_period++;
break;
/* 捕获事件:反转计数(捕获B事件) */
case TIMER_EVENT_CAPTURE_B:
flag = false; // 设置为反转方向
//更新计数值
pulse_period--;
break;
/* 定时器溢出事件 */
case TIMER_EVENT_CYCLE_END:
overflow_count++; // 增加溢出计数
break;
default:
break;
}
}
10.2.2.4. 定时器中断函数¶
这段代码的作用是通过 GPT 定时器周期结束事件触发位置环控制逻辑。在定时器中断回调函数 gpt0_timing_callback 中, 利用 pulse_period 的值调用 motor_pid_control 函数,调整电机的运行状态。 这里将 pulse_period 显式转换为浮点数 (float)pulse_period, 以确保与 motor_pid_control 函数的输入类型匹配,同时避免整型溢出或精度不足的问题。
这种实现方式通过 pulse_period 的正负值和大小直接反映当前位置的偏移量, 结合 PID 控制算法能够有效地实现电机的精准定位。 需要注意的是,pulse_period 的更新和计算准确性在很大程度上决定了位置环的控制精度。 因此,建议在编码器的事件回调中严格确保 pulse_period 的正确性和实时性。
/* GPT定时器中断回调函数 */
void gpt0_timing_callback(timer_callback_args_t *p_args)
{
if (TIMER_EVENT_CYCLE_END == p_args->event) // 检查定时器周期结束事件
{
// 调用PID控制函数调整电机状态
motor_pid_control((float)pulse_period);
}
}
10.2.2.5. PID算法初始化¶
这里开始编写位置式PID算法, 该代码段实现了 PID 控制参数的初始化,目标值 (target_val) 设为 ENCODER_TOTAL_RESOLUTION(电机转一圈所捕获的脉冲值), 实际值、误差 (err) 和误差积累值 (integral) 都初始化为 0.0, 其中pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 接下来,通过 set_computer_value 函数将这三个参数(Kp、Ki、Kd)发送到上位机的通道 1,用于后续的调整和监控。
/* 编码器物理分辨率 */
#define ENCODER_RESOLUTION 1000.0f
/* 经过倍频之后的总分辨率 (编码器一圈可以捕获的脉冲)*/
#if (ENCODER_MODE == TIM_ENCODERMODE_TI12)
#define ENCODER_TOTAL_RESOLUTION (ENCODER_RESOLUTION * 4) /* 4倍频后的总分辨率 */
#else
#define ENCODER_TOTAL_RESOLUTION (ENCODER_RESOLUTION * 2) /* 2倍频后的总分辨率 */
#endif
/*
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化参数 */
pid.target_val=ENCODER_TOTAL_RESOLUTION;
pid.actual_val=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.integral=0.0;
pid.Kp = 1.8f;
pid.Ki = 0;
pid.Kd = 0;
float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}
10.2.2.6. PID算法实现¶
该函数主要实现了位置式PID算法,用传入的目标值减去实际值得到误差值得到比例项,在对误差值进行累加得到积分项, 用本次误差减去上次的误差得到微分项,然后通过前面章节介绍的位置式PID公式实现PID算法,并返回实际控制值。
/**
* @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;
}
10.2.2.7. 速度控制函数¶
该代码实现了电机位置式 PID 控制的核心逻辑,根据当前电机的实际速度,通过 PID 算法计算出控制值, 并设置电机的运行方向和 PWM 占空比以控制电机的运动状态。 该函数在定时器的中断里定时调用默认是25毫秒调用一次,如果改变了周期那么PID三个参数也需要做相应的调整, PID的控制周期与控制效果是息息相关的。以下是代码的主要说明:
PID 计算:调用 PID_realize(actual_location) 函数计算出控制值 cont_val,该值决定电机运行方向和占空比。
方向设置:通过判断 cont_val 的正负值来设置电机方向。若 cont_val 为负,则需取其绝对值并设置为反方向。
数据传输:通过 set_computer_value(SEND_FACT_CMD, CURVES_CH1, &location, 1) 函数将当前实际速度发送到上位机,方便监测和记录。
后续可以结合目标位置、当前位置的偏差进一步完善位置环控制逻辑。
/**
* @brief 步进电机位置式PID控制
* @retval 无
* @note 基本定时器中断内调用
*/
void motor_pid_control(float actual_location)
{
int32_t location = (int32_t)actual_location; // 将实际位置转换为整数类型
/* 经过pid计算后的期望值 */
volatile float cont_val = 0;
/* 当电机运动时才启动pid计算 */
if(motor_state == true )
{
/* 将实际位置作为输入传入PID控制器进行计算 */
cont_val = PID_realize(actual_location); // 进行 PID 计算
// 根据计算结果控制步进电机的旋转方向
if (cont_val > 0)
{
STEP_CW; // 顺时针旋转
}
else
{
STEP_CCW; // 逆时针旋转
cont_val = -cont_val; // 取绝对值
}
// 控制值上限处理
cont_val >= PWM_MAX_FREQUENCY ? (cont_val = PWM_MAX_FREQUENCY) : cont_val;
// 设置步进电机速度
Motor_Control_SetSpeed(cont_val);
// 给通道 1 发送实际位置值
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &location, 1);
}
}
10.2.2.8. 按键中断函数¶
这段代码通过按键中断回调函数实现目标位置的动态调整,适用于位置环控制。 按键2的回调函数会将目标位置 target_location 增加一圈( ENCODER_TOTAL_RESOLUTION ), 按键3的回调函数则减少一圈。每次调整后,通过 set_pid_target 将目标值传递给 PID 控制器, 并调用 set_computer_value 将目标位置发送至上位机,方便实时监测位置目标的变化。
/* 目标值 */
int32_t target_location = (int32_t)ENCODER_TOTAL_RESOLUTION;
// 按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 增加目标位置
target_location += (int32_t)ENCODER_TOTAL_RESOLUTION;
set_pid_target((float)target_location); // 设置PID目标值
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_location, 1); // 给通道 1 发送目标值
}
// 按键3中断回调函数
void sw3_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 减少目标位置
target_location -= (int32_t)ENCODER_TOTAL_RESOLUTION;
set_pid_target((float)target_location); // 设置PID目标值
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_location, 1); // 给通道 1 发送目标值
}
10.2.2.9. hal_entry函数¶
在主函数里面首先做了一些外设的初始化,然后通过上位机来观察、控制电机。
void hal_entry(void)
{
/* TODO: add your own code here */
LED_Init(); // LED 初始化
Debug_UART9_Init();//调试串口初始化
IRQ_Init();//按键中断初始化
Motor_Control_Init(); //初始化电机定时器
/*使能定时器*/
GPT_Timing_Init();
//初始化编码器
initEncoder();
Motor_Control_Stop(); // 调用电机停止函数
/* 协议初始化 */
protocol_init();
/*PID参数初始化*/
PID_param_init();
set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_location, 1); // 给通道 1 发送目标值
while(1)
{
/* 接收数据处理 */
receiving_process();
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
10.2.3. 下载验证¶
我们按前面介绍的硬件连接好电机和驱动板, 将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,按下Key2增加一圈,按下Key3减少一圈,下图是电机运行效果图。

10.3. 步进电机位置环控制-增量式PID实现¶
10.3.1. 软件设计¶
同过前面位置式PID控制的学习,大家应该对速度环PID控制有了更深刻的理解, 这里将只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本节配套的工程。
10.3.1.1. 新建工程¶
我们直接在基础部分的直流有刷章节的 “304_Motor_Stepper_PID_Location_Positional” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “304_Motor_Stepper_PID_Location_Positional”, 然后将工程文件夹重命名为 “305_Motor_Stepper_PID_Location_Incremental”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “304_Motor_Stepper_PID_Location_Positional”, 然后将工程文件夹重命名为 “305_Motor_Stepper_PID_Location_Incremental”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
10.3.1.2. PID算法初始化¶
增量式PID实现的速度环控制和位置式PID现实的速度环控制其控制代码大部分都是一样的, 在上面的编程要点中只有第4项是不同的,其他代码均相同,所以这里将只讲解不一样的部分代码, 完整代码请参考本节配套工程。
/**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化参数 */
pid.target_val=ENCODER_TOTAL_RESOLUTION;
pid.actual_val=0.0;
pid.err = 0.0;
pid.err_last = 0.0;
pid.err_next = 0.0;
pid.Kp = 1.8f;
pid.Ki = 1;
pid.Kd = 0.05f;
float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}
10.3.1.3. PID算法实现¶
这个函数主要实现了增量式PID算法,用传入的目标值减去实际值得到误差值得到当前偏差值。 然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。
/**
* @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;
}
10.3.2. 下载验证¶
我们按前面介绍的硬件连接好电机和驱动板, 将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,按下Key2增加一圈,按下Key3减少一圈,下图是电机运行效果图。
