ITPub博客

首页 > 嵌入式/内核开发 > 嵌入式/内核开发 > 《DMG474电机控制专题教程V1.1 》第10章 直流有刷电机速度环控制实现

《DMG474电机控制专题教程V1.1 》第10章 直流有刷电机速度环控制实现

原创 嵌入式/内核开发 作者:正点原子 时间:2026-04-13 10:20:27 0 删除 编辑

第10章 直流有刷电机速度环控制实现


       本章我们主要来学习直流有刷电机速度环PID控制的原理,并实现速度环PID控制的实验。 

       本章分为如下几个小节:

       10.1 速度环PID控制原理

       10.2 硬件设计

       10.3 程序设计

       10.4 下载验证


        10.1 速度环PID控制原理

       速度环PID控制的原理非常简单,我们只需要把PID控制流程中的控制对象换成电机速度即可,如图10.1.1所示:


图10.1.1速度环PID控制流程


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


        10.2 硬件设计


       1. 例程功能


       1、本实验以电机开发板的直流有刷电机驱动接口1为例,基于编码器测速实验,加入速度环PID控制算法,对电机的速度进行闭环控制。

       2、当按键0按下,就增大目标速度值;当按键1按下,就减小目标速度值。目标速度的绝对值大小决定电机的速度,它的正负决定电机的正反转方向。按下按键2则马上停止电机。

       3、屏幕显示按键功能、占空比、目标转速以及实际转速。

       4、串口1和上位机进行数据通信。

       5、LED0闪烁指示程序运行。


       2. 硬件资源


       1)LED灯

              LED0 – PE0

       2)独立按键

               KEY0 – PE12

               KEY1 – PE13

               KEY2 – PE14

       3)定时器1、3、6

              TIM1正常输出通道 PA8

              TIM1互补输出通道 PB13

              TIM3 编码器A相输入通道 PA6

              TIM3 编码器B相输入通道 PA7

       4)SD(刹车)信号输出 PC13

       5)串口1

              USART1_TX  PB6(发送)

              USART1_RX  PB7(接收)


       3. 原理图


图10.2.1 直流有刷电机接口1原理图


       图10.2.1就是我们DMG474电机开发板的直流有刷电机接口1原理图,本实验我们除了用到基础驱动所需的引脚,还需要用到PM1_ENCA(PA6)、PM1_ENCB(PA7)这两个引脚,它们分别用于连接编码器的A、B两相。

       本实验的硬件接线部分和编码器测速实验一模一样,这里就不再赘述,大家可以回顾编码器测速实验的内容。


        10.3 程序设计

       本实验所用到的基础驱动、编码器测速的代码在前面实验都有介绍过了。我们在程序解析中只讲解速度环PID控制相关的函数,下面介绍一下速度环PID控制的配置步骤。


       速度环PID控制的配置步骤 

       1)配置相关定时器

       配置基础驱动、编码器测速相关的定时器,此部分内容和编码器测速实验一致。

       2)初始化串口1

       初始化串口1,开启串口接收中断,串口1在PID控制代码中用于上位机通信。

       注意:在PID控制的代码中,串口1仅用于PID数据上传,尽量不要输出其他信息,否则有可能影响PID数据。

       3)定义PID参数结构体变量

       为了方便管理PID相关的控制量,我们需要定义一个PID参数结构体变量,方法如下:

PID_TypeDef  g_speed_pid;           /* 速度环PID参数结构体 */

       4)初始化PID参数

       把PID控制系统的目标速度值、期望输出值、累计偏差等清零,然后配置PID系数。

       5)初始化上位机调试

       调用debug_init函数初始化所需内存,为上位机的调试做准备。

       6)编写中断服务函数

       在定时器6的更新中断回调函数里面进行速度环PID计算,计算后的结果用于控制PWM的占空比。


       10.3.1 程序流程图


图10.3.1.1 速度环PID控制程序流程图


       10.3.2 程序解析

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。关于基础驱动、编码器测速以及上位机协议的代码,这里不再赘述,大家可以回顾相应的章节。首先我们看串口1的代码,该串口在PID控制代码中用于上位机通信。我们只看串口1中断服务函数,其在usart.c文件中,具体定义如下:

/**
 * @brief       串口1中断服务函数
 * @param       无
 * @retval      无
 */
void USART_UX_IRQHandler(void)
{ 
    uint8_t res;
 
/* 接收到数据 */
    if ((__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_RXNE) != RESET)) 
    {
        HAL_UART_Receive(&g_uart1_handle, &res, 1, 1000);
        debug_handle(&res);
    } 
}

       该函数非常简单,当上位机有数据下发时,我们直接调用debug_handle函数来解析上位机下发的数据。

       下面我们看速度环PID控制的相关源码,其包括两个文件:pid.c和pid.h。

       首先看pid.h头文件的几个宏定义:

/* PID相关参数 */
 
#define  INCR_LOCT_SELECT  0          /* 0:位置式 ,1:增量式 */
 
#if INCR_LOCT_SELECT
 
/* 增量式PID参数相关宏 */
#define  KP      8.50f                 /* P参数*/
#define  KI      5.00f                 /* I参数*/
#define  KD      0.10f                 /* D参数*/
#define  SMAPLSE_PID_SPEED  50   /* 采样周期 单位ms*/
 
#else
 
/* 位置式PID参数相关宏 */
#define  KP      10.0f                 /* P参数*/
#define  KI      6.00f                 /* I参数*/
#define  KD      0.5f                  /* D参数*/
#define  SMAPLSE_PID_SPEED  50      /* 采样周期 单位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] */
} PID_TypeDef;
 
extern PID_TypeDef  g_speed_pid;   /* 速度环PID参数结构体 */

       可以把上面的宏定义分成两部分,第一部分是PID计算方式以及PID系数的宏定义,我们可以通过改变INCR_LOCT_SELECT这个宏的值来选择相应的PID计算方式,第二部分则是PID参数相关的结构体,这个结构体用于管理PID控制所需要的控制量,本实验中我们定义了速度环PID参数的结构体变量g_speed_pid。

       下面看pid.c的程序,这里我们只介绍PID初始化函数,关于PID闭环控制的函数介绍请回顾8.3章节,PID初始化函数定义如下:

/**
 * @brief       pid初始化
 * @param       无
 * @retval      无
 */
void pid_init(void)
{
    g_speed_pid.SetPoint = 0;  /* 设定目标值 */
    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 = KP; /* 比例常数 Proportional Const */
    g_speed_pid.Integral = KI;  /* 积分常数 Integral Const */
    g_speed_pid.Derivative = KD; /* 微分常数 Derivative Const */ 
}

       该函数主要是将PID控制系统的目标速度值、期望输出值、累计偏差等清零,然后配置PID系数。

       最后要介绍的是定时器的更新中断回调函数,它在dcmotor_time.c中实现,具体的定义如下:

/**
 * @brief       定时器更新中断回调函数
 * @param        htim:定时器句柄指针
 * @note        此函数会被定时器中断函数共同调用的
 * @retval      无
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
     static uint8_t val = 0;
 
/* 定时器3相关程序 */
     if (htim->Instance == TIM3)
     {
    /* 判断CR1的DIR位 */
        if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle))   
        {
            g_timx_encode_count--;             /* DIR位为1,也就是递减计数 */
        }
        else
        {
            g_timx_encode_count++;             /* DIR位为0,也就是递增计数 */
        }
     }
 
/* 定时器6相关程序 */
     else if (htim->Instance == TIM6)
     {
        int Encode_now = gtim_get_encode();      /* 获取编码器值,用于计算速度 */
 
        speed_computer(Encode_now, 5);           /* 5ms计算一次速度 */
         
        if (val % SMAPLSE_PID_SPEED == 0)        /* 50ms进行一次pid计算 */
        {
            if (g_run_flag)                      /* 判断电机是否启动了*/
            { 
                /* PID计算,输出比较值(占空比) */
                g_motor_data.motor_pwm = increment_pid_ctrl(&g_speed_pid, 
g_motor_data.speed);
    /* 限制输出 */
                if (g_motor_data.motor_pwm >= 8200)         
                {
                    g_motor_data.motor_pwm = 8200;
                }
                else if (g_motor_data.motor_pwm <= -8200)
                {
                    g_motor_data.motor_pwm = -8200;
                }
 
#if DEBUG_ENABLE  /* 发送基本参数*/
 
    /* 选择通道1,发送实际速度(波形显示)*/
                debug_send_wave_data( 1 ,g_motor_data.speed); 
    /* 选择通道2,发送目标速度(波形显示)*/
                debug_send_wave_data( 2 ,g_speed_pid.SetPoint);  
    /* 选择通道3,发送占空比(波形显示)*/
                debug_send_wave_data( 3 ,g_motor_data.motor_pwm * 100 / 8400); 
#endif
                motor_pwm_set(g_motor_data.motor_pwm);    /* 设置电机转速 */
            }
            val = 0;
        }
        val ++;
     }
}

       该函数我们只介绍定时器6相关的程序,进入更新中断回调函数后,所执行的代码逻辑如下:

       第一步,判断是不是定时器6的寄存器基地址,如果是则获取编码器的计数总值,然后每隔5ms计算一次电机速度。

       第二步,每隔50ms进行一次PID计算,在计算PID之前,需要判断g_run_flag是否为1,如果是则说明电机已经启动,可以开始PID计算。

       第三步,进行速度环PID计算,返回的输出存放在g_motor_data.motor_pwm这个成员中,该成员用于设置PWM的占空比,除此之外,我们还需要对返回的输出进行限制,这一点非常重要。

       第四步,发送实际速度、目标速度以及占空比的波形数据到上位机,最后设置PWM的占空比,进而控制电机的转速。

       在main.c里面编写如下代码:

int main(void)
{
    uint8_t key;
    uint16_t t;
    uint8_t debug_cmd = 0;
    
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(85, 2, 2, 4, 8);       /* 设置时钟,170Mhz */
    delay_init(170);                            /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    led_init();                                 /* 初始化LED */
    lcd_init();                                 /* 初始化LCD */
    key_init();                                 /* 初始化按键 */
pid_init();                                 /* 初始化PID参数 */
/* 170 000 000 / 1 = 170 000 000 170Mhz的计数频率,计数8500次为50us */
    atim_timx_cplm_pwm_init(8500 - 1, 0);       
    dcmotor_init();                             /* 初始化电机 */
    gtim_timx_encoder_chy_init(0XFFFF - 1, 0);  /* 编码器定时器初始化,不分频 */
    btim_timx_int_init(1000 - 1 , 170 - 1);     /* 基本定时器初始化,1ms计数周期*/
 
#if DEBUG_ENABLE                                /* 开启调试 */
    
    debug_init();                               /* 初始化调试 */
    debug_send_motorcode(DC_MOTOR);             /* 上传电机类型(直流有刷电机) */
    debug_send_motorstate(IDLE_STATE);          /* 上传电机状态(空闲) */
    
    /* 同步数据(选择第1组PID,目标速度地址,P,I,D参数)到上位机 */
    debug_send_initdata(TYPE_PID1, (float*)(&g_speed_pid.SetPoint),KP,KI,KD);
    
#endif
 
    lcd_show_string(10, 10, 200, 24, 24, "DcMotor Test", RED);
    lcd_show_string(10, 40, 200, 24, 24, "KEY0:Forward", RED);
    lcd_show_string(10, 70, 200, 24, 24, "KEY1:Backward", RED);
    lcd_show_string(10, 100, 200, 24, 24, "KEY2:Stop", RED);
    
    while (1)
    {
        key = key_scan(0);                                  /* 按键扫描 */
        if(key == KEY0_PRES)                                /* 当key0按下 */
        {
            g_run_flag = 1;                                 /* 标记电机启动 */
            g_speed_pid.SetPoint += 30;
            if (g_speed_pid.SetPoint == 0)
            {
                dcmotor_stop();                             /* 停止则立刻响应 */
                g_motor_data.motor_pwm = 0;
                motor_pwm_set(g_motor_data.motor_pwm);      /* 设置电机转速 */
            }
            else
            {
                dcmotor_start();                            /* 开启电机 */
                if (g_speed_pid.SetPoint >= 300)            /* 限速 */
                {
                    g_speed_pid.SetPoint = 300;
                }
#if DEBUG_ENABLE
                debug_send_motorstate(RUN_STATE);           /* 上传电机状态 */
#endif
            }
        }
 
        else if(key == KEY1_PRES)                           /* 当key1按下 */
        {
            g_run_flag = 1;                                 /* 标记电机启动 */
            g_speed_pid.SetPoint -= 30;
            if (g_speed_pid.SetPoint == 0)
            {
                dcmotor_stop();                             /* 停止则立刻响应 */
                g_motor_data.motor_pwm = 0;
                motor_pwm_set(g_motor_data.motor_pwm);      /* 设置电机转速*/
            }
            else
            {
                dcmotor_start();                            /* 开启电机 */
                if (g_speed_pid.SetPoint <= -300)           /* 限速 */
                {
                    g_speed_pid.SetPoint = -300;
                }
#if DEBUG_ENABLE
                debug_send_motorstate(RUN_STATE);           /* 上传电机状态 */
#endif
            }
        }
 
        else if(key == KEY2_PRES)                           /* 当key2按下 */
        {
            dcmotor_stop();                                 /* 停止电机 */
            pid_init();                                     /* 重置pid参数 */
            g_run_flag = 0;                                 /* 标记电机停止 */
            g_motor_data.motor_pwm = 0;
            motor_pwm_set(g_motor_data.motor_pwm);          /* 设置电机速度*/
            
#if DEBUG_ENABLE
            debug_send_motorstate(BREAKED_STATE);           /* 上传电机状态 */
            debug_send_initdata(TYPE_PID1, (float *)(&g_speed_pid.SetPoint), 
KP, KI, KD);             /* 同步数据 */
#endif
        }
 
#if DEBUG_ENABLE
         /* 查询接收PID助手的PID参数 */
        debug_receive_pid(TYPE_PID1, (float*)&g_speed_pid.Proportion,
(float*)&g_speed_pid.Integral, (float*)&g_speed_pid.Derivative);
        
        debug_set_point_range(300, -300, 300);              /* 设置目标速度范围*/
        debug_cmd = debug_receive_ctrl_code();              /* 读取上位机指令 */
 
        if (debug_cmd == BREAKED)                           /* 电机刹车 */
        {
            dcmotor_stop();                                 /* 停止电机 */
            pid_init();                                     /* 重置pid参数 */
            g_run_flag = 0;                                 /* 标记电机停止 */
            g_motor_data.motor_pwm = 0;
            motor_pwm_set(g_motor_data.motor_pwm);          /* 设置电机转向速度*/
            debug_send_motorstate(BREAKED_STATE);           /* 上传电机状态 */
            debug_send_initdata(TYPE_PID1, (float*)(&g_speed_pid.SetPoint), 
KP, KI, KD);
        } 
        else if (debug_cmd == RUN_CODE)                     /* 电机运行 */
        {  
            dcmotor_start();                                /* 开启电机 */
            g_speed_pid.SetPoint = 30;                      /* 设置速度30RPM */
            g_run_flag = 1;                                 /* 标记电机启动 */
            debug_send_motorstate(RUN_STATE);               /* 上传电机状态 */
        }
#endif
        t++;
        if(t % 20 == 0)
        {
            lcd_dis();                                      /* 显示数据 */
            LED0_TOGGLE();                                  /* LED0(红灯)翻转*/
#if DEBUG_ENABLE
            debug_send_speed(g_motor_data.speed);           /* 发送速度 */
#endif
        }
        delay_ms(10);
    }
}

       main.c的代码逻辑如下:

       第一步,初始化相关的定时器、串口,本实验需要利用串口1和上位机进行通讯。

       第二步,初始化PID参数、上位机调试,它们分别调用的是pid_init和debug_init函数。

       第三步,上传电机的状态(空闲)、类型(直流有刷电机)、PID参数到上位机,它们分别调用的是debug_send_motorcode、debug_send_motorstate以及debug_send_initdata函数。

       第四步,在while循环里面检测按键是否按下,如果key0按下,则目标速度增加30,上传电机状态(运行);如果key1按下,则目标速度减小30,上传电机状态(运行);如果key2按下,则停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机。

       第五步,接收上位机下发的PID参数、设置目标速度的调节范围,它们分别调用的是debug_receive_pid以及debug_set_point_range函数。我们这里重点介绍一下后者,debug_set_point_range函数的第一、二个入口参数分别传入目标速度的最大值、最小值,它们的正负号代表电机的正反转方向,第三个入口参数传入的是电机速度的最大突变值。

       第六步,调用debug_receive_ctrl_code函数,接收上位机下发的命令。如果上位机的命令为BREAKED(刹车),则停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机;如果上位机的命令是RUN_CODE,则开启电机,设置目标速度为30RPM,上传电机状态(运行)到上位机。

       第七步,每隔200ms更新一次数据到屏幕,调用debug_send_speed函数发送实际速度值到上位机。


        10.4 下载验证

       下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,LCD上显示按键功能、占空比以及电机速度信息,当我们按下KEY0,目标速度将增大;按下KEY1,目标速度将减小;按下KEY2,电机将停止。目标速度为正数时,电机正转,反之电机反转。我们再打开PID调试助手,选择对应的串口端口,我这边是COM7,接着选择通道1和2,点击“开始”按钮,即可开始显示波形,如下图10.4.1所示: 


图10.4.1 速度环PID控制波形


       图10.4.1中,橙线代表目标速度,红线代表实际速度,当我们按下KEY0,目标速度增大,橙线先发生变化,而红线(实际速度)会逐渐靠近橙线(目标速度);按下KEY1,目标速度将减小,曲线的变化同理;按下KEY2,电机将停止,目标速度将为0。

       注意:1、本实验需要使用USB数据线连接开发板的串口1到电脑,并启动电机之后才会有波形变化;2、如果发现波形不对,请检查电机的M+和M-接线;3、PID系数并不是通用的,如果PID曲线不理想,大家需要根据自己的实际系统去调节。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3115815/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2024-08-30

  • 博文量
    588
  • 访问量
    763978