直流有刷电机多环控制
提高部分-第8讲 直流有刷电机多环控制实现(1)_哔哩哔哩_bilibili
PID模型
外环的输出作为内环的输入,外环是最主要控制的效果,主要控制电机的位置。改变位置可以改变速度,改变速度是受电流控制。
实验环境
【 !】功能简介:
按下KEY1使能电机,按下KEY2禁用电机,按下KEY3\KEY4可以调整目标值,以到达控制的效果.
可以通过上位机<野火多功能调试助手>----PID调试助手,查看现象或进行调试.
在PID调试助手中,打开开发板对应的串口,单击下方启动即可.
注意,部分例程中,上位机设置PID目标值时,未做幅值限制,若出现积分饱和为正常现象.
在电机未停止时重新开启电机,可能出现PID调整不准确的问题,电机会因为惯性保持运行,定时器会捕获不该捕获的脉冲.
F103性能相对有限,程序中处理的任务较多,使用上位机调试时可能出现指令不执行的情况,多发几次即可.
【 !】实验操作:
接线:
电机驱动板 5V\GND <----> 开发板 5V\GND
电机驱动板 PWM1\PWM2 <----> 开发板 PE9\PE11
电机驱动板 SD <----> 开发板 PD15
电机驱动板 编码器输出A\B <----> 开发板 PD12\PD13
电机驱动板 电压\电流检测 <----> 开发板 PC1\PC0
注意:必须使用跳冒连接 J33 和 J34
下载本程序,复位开发板即可。
/********************************************************************************/
【*】 引脚分配
定时器TIM1输出PWM信号:
两路PWM输出到PE9\PE11上连接到驱动板上的PWM信号输入.
电机驱动板 PWM1\PWM2 <----> 开发板 PE9\PE11
电机使能引脚:
电机驱动板 SD <----> 开发板 PD15
编码器信号:
用于对电机进行测速
电机驱动板 编码器输出A\B <----> 开发板 PC7\PC6
电压电流采集:
电机驱动板 信号检测 电压\电流 <----> 开发板 PD12\PD13
/************************************************************************************/
【*】 时钟
A.晶振:
-外部高速晶振:25MHz
-RTC晶振:32.768KHz
B.各总线运行时钟:
-系统时钟 = SYCCLK = AHB1 = 72MHz
-APB2 = 72MHz
-APB1 = 36MHz
C.浮点运算单元:
不使用
定时器定时执行PID电机控制
定时器6,时钟72MHz,预分频因子1440,重装载值2500
定时器周期:
72000000 / 1440 / 2500 = 20
1 / 20 = 50ms 进入一次中断,计算一次PID
#ifndef __BASIC_TIM_H
#define __BASIC_TIM_H
#include "stm32f1xx.h"
#define BASIC_TIM TIM6
#define BASIC_TIM_CLK_ENABLE() __HAL_RCC_TIM6_CLK_ENABLE()
#define BASIC_TIM_IRQn TIM6_DAC_IRQn
#define BASIC_TIM_IRQHandler TIM6_DAC_IRQHandler
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到BASIC_PERIOD_COUNT-1,即为BASIC_PERIOD_COUNT次,为一个定时周期
#define BASIC_PERIOD_COUNT (50*50)
//定时器时钟源TIMxCLK = 2 * APB1_CLK
// 如果APB1_CLK = 72MHz = TIMxCLK
// 否则TIMxCLK=APB1_CLK * 2
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)
#define BASIC_PRESCALER_COUNT (1440)
/* 获取定时器的周期,单位ms */
//#define __HAL_TIM_GET_PRESCALER(__HANDLE__) ((__HANDLE__)->Instance->PSC) // Get TIM Prescaler.
//#define GET_BASIC_TIM_PERIOD(__HANDLE__) (1.0/(HAL_RCC_GetPCLK2Freq()/(__HAL_TIM_GET_PRESCALER(__HANDLE__)+1)/(__HAL_TIM_GET_AUTORELOAD(__HANDLE__)+1))*1000)
/* 以下两宏仅适用于定时器时钟源TIMxCLK=72MHz,预分频器为:1440-1 的情况 */
#define SET_BASIC_TIM_PERIOD(T) __HAL_TIM_SET_AUTORELOAD(&TIM_TimeBaseStructure, (T)*50 - 1) // 设置定时器的周期(1~1000ms)
#define GET_BASIC_TIM_PERIOD() ((__HAL_TIM_GET_AUTORELOAD(&TIM_TimeBaseStructure)+1)/50.0f) // 获取定时器的周期,单位ms
extern TIM_HandleTypeDef TIM_TimeBaseStructure;
void TIMx_Configuration(void);
#endif /* __BASIC_TIM_H */
/**
******************************************************************************
* @file bsp_basic_tim.c
* @author STMicroelectronics
* @version V1.0
* @date 2015-xx-xx
* @brief 基本定时器定时范例
******************************************************************************
* @attention
*
* 实验平台:野火 STM32 F103 开发板
* 论坛 :http://www.firebbs.cn
* 淘宝 :http://firestm32.taobao.com
*
******************************************************************************
*/
#include "./tim/bsp_basic_tim.h"
#include "./usart/bsp_debug_usart.h"
#include "./protocol/protocol.h"
TIM_HandleTypeDef TIM_TimeBaseStructure;
/**
* @brief 基本定时器 TIMx,x[6,7]中断优先级配置
* @param 无
* @retval 无
*/
static void TIMx_NVIC_Configuration(void)
{
//设置抢占优先级,子优先级
HAL_NVIC_SetPriority(BASIC_TIM_IRQn, 2,0);
// 设置中断来源
HAL_NVIC_EnableIRQ(BASIC_TIM_IRQn);
}
/*
* 注意:TIM_TimeBaseInitTypeDef结构体里面有5个成员,TIM6和TIM7的寄存器里面只有
* TIM_Prescaler和TIM_Period,所以使用TIM6和TIM7的时候只需初始化这两个成员即可,
* 另外三个成员是通用定时器和高级定时器才有.
*-----------------------------------------------------------------------------
* TIM_Prescaler 都有
* TIM_CounterMode TIMx,x[6,7]没有,其他都有(基本定时器)
* TIM_Period 都有
* TIM_ClockDivision TIMx,x[6,7]没有,其他都有(基本定时器)
* TIM_RepetitionCounter TIMx,x[1,8]才有(高级定时器)
*-----------------------------------------------------------------------------
*/
static void TIM_Mode_Config(void)
{
// 开启TIMx_CLK,x[6,7]
BASIC_TIM_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = BASIC_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到BASIC_PERIOD_COUNT - 1,即为BASIC_PERIOD_COUNT次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = BASIC_PERIOD_COUNT - 1;
//定时器时钟源TIMxCLK = 2 * PCLK1
// PCLK1 = HCLK / 2
// => TIMxCLK=HCLK/2=SystemCoreClock/2*2=72MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)
TIM_TimeBaseStructure.Init.Prescaler = BASIC_PRESCALER_COUNT - 1;
TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
TIM_TimeBaseStructure.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频
// 初始化定时器TIMx, x[2,3,4,5]
HAL_TIM_Base_Init(&TIM_TimeBaseStructure);
// 开启定时器更新中断
HAL_TIM_Base_Start_IT(&TIM_TimeBaseStructure);
}
/**
* @brief 初始化基本定时器定时,默认50ms产生一次中断
* @param 无
* @retval 无
*/
void TIMx_Configuration(void)
{
TIMx_NVIC_Configuration();
TIM_Mode_Config();
#if PID_ASSISTANT_EN
uint32_t temp = GET_BASIC_TIM_PERIOD(); // 计算周期,单位ms
set_computer_value(SEND_PERIOD_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送目标值
#endif
}
/*********************************************END OF FILE**********************/
电机PID控制
外环环周期(电流环计算周期为定时器周期T,速度环为2T,位置环为3T)
采样周期要小于PID计算周期,这里采样周期应该是ADC电流采样、编码器等数据。
使得调节连续,不联系需要改变周期值。
位置环的输出设置为速度环的目标值
速度环的输出设置为电流环的目标值
/**
******************************************************************************
* @file bsp_motor_control.c
* @author fire
* @version V1.0
* @date 2019-xx-xx
* @brief 电机控制接口
******************************************************************************
* @attention
*
* 实验平台:野火 STM32 F103 开发板
* 论坛 :http://www.firebbs.cn
* 淘宝 :http://firestm32.taobao.com
*
******************************************************************************
*/
#include ".\motor_control\bsp_motor_control.h"
#include "./usart/bsp_debug_usart.h"
#include <math.h>
#include <stdlib.h>
#include "./Encoder/bsp_encoder.h"
#include "./adc/bsp_adc.h"
#include "./tim/bsp_basic_tim.h"
static motor_dir_t direction = MOTOR_FWD; // 记录方向
static uint16_t dutyfactor = 0; // 记录占空比
static uint8_t is_motor_en = 0; // 电机使能
#define TARGET_CURRENT_MAX 200 // 目标电流的最大值 mA
#define TARGET_SPEED_MAX 200 // 目标速度的最大值 r/m
static void sd_gpio_config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/* 定时器通道功能引脚端口时钟使能 */
SHUTDOWN_GPIO_CLK_ENABLE();
/* 引脚IO初始化 */
/*设置输出类型*/
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
/*设置引脚速率 */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/*选择要控制的GPIO引脚*/
GPIO_InitStruct.Pin = SHUTDOWN_PIN;
/*调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO*/
HAL_GPIO_Init(SHUTDOWN_GPIO_PORT, &GPIO_InitStruct);
}
/**
* @brief 电机初始化
* @param 无
* @retval 无
*/
void motor_init(void)
{
Motor_TIMx_Configuration(); // 初始化电机 1
sd_gpio_config();
}
/**
* @brief 设置电机速度
* @param v: 速度(占空比)
* @retval 无
*/
void set_motor_speed(uint16_t v)
{
v = (v > PWM_PERIOD_COUNT) ? PWM_PERIOD_COUNT : v; // 上限处理
dutyfactor = v;
if (direction == MOTOR_FWD)
{
SET_FWD_COMPAER(dutyfactor); // 设置速度
}
else
{
SET_REV_COMPAER(dutyfactor); // 设置速度
}
}
/**
* @brief 设置电机方向
* @param 无
* @retval 无
*/
void set_motor_direction(motor_dir_t dir)
{
direction = dir;
if (direction == MOTOR_FWD)
{
SET_FWD_COMPAER(dutyfactor); // 设置速度
SET_REV_COMPAER(0); // 设置速度
}
else
{
SET_FWD_COMPAER(0); // 设置速度
SET_REV_COMPAER(dutyfactor); // 设置速度
}
}
/**
* @brief 使能电机
* @param 无
* @retval 无
*/
void set_motor_enable(void)
{
is_motor_en = 1;
MOTOR_ENABLE_SD();
MOTOR_FWD_ENABLE();
MOTOR_REV_ENABLE();
}
/**
* @brief 禁用电机
* @param 无
* @retval 无
*/
void set_motor_disable(void)
{
is_motor_en = 0;
MOTOR_DISABLE_SD();
MOTOR_FWD_DISABLE();
MOTOR_REV_DISABLE();
}
/**
* @brief 电机位置式 PID 控制实现(定时调用)
* @param 无
* @retval 无
*/
void motor_pid_control(void)
{
static uint32_t louter_ring_timer = 0; // 外环环周期(电流环计算周期为定时器周期T,速度环为2T,位置环为3T)
int32_t actual_current = get_curr_val(); // 读取当前电流值
if(actual_current > TARGET_CURRENT_MAX)
{
actual_current = TARGET_CURRENT_MAX;
}
if (is_motor_en == 1) // 电机在使能状态下才进行控制处理
{
static int32_t Capture_Count = 0; // 当前时刻总计数值
static int32_t Last_Count = 0; // 上一时刻总计数值
float cont_val = 0; // 当前控制值
/* 当前时刻总计数值 = 计数器值 + 计数溢出次数 * ENCODER_TIM_PERIOD */
Capture_Count = __HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (Encoder_Overflow_Count * ENCODER_TIM_PERIOD);
/* 位置环计算 */
if (louter_ring_timer++ % 3 == 0)
{
cont_val = location_pid_realize(&pid_location, Capture_Count); // 进行 PID 计算
/* 目标速度上限处理 */
if (cont_val > TARGET_SPEED_MAX)
{
cont_val = TARGET_SPEED_MAX;
}
else if (cont_val < -TARGET_SPEED_MAX)
{
cont_val = -TARGET_SPEED_MAX;
}
set_pid_target(&pid_speed, cont_val); // 设定速度的目标值
#if defined(PID_ASSISTANT_EN)
int32_t temp = cont_val;
set_computer_value(SEND_TARGET_CMD, CURVES_CH2, &temp, 1); // 给通道 2 发送目标值
#endif
}
/* 速度环计算 */
static int32_t actual_speed = 0; // 实际测得速度
if (louter_ring_timer % 2 == 0)
{
/* 转轴转速 = 单位时间内的计数值 / 编码器总分辨率 * 时间系数 */
actual_speed = ((float)(Capture_Count - Last_Count) / ENCODER_TOTAL_RESOLUTION / REDUCTION_RATIO) / (GET_BASIC_TIM_PERIOD()*2/1000.0/60.0);
/* 记录当前总计数值,供下一时刻计算使用 */
Last_Count = Capture_Count;
cont_val = speed_pid_realize(&pid_speed, actual_speed); // 进行 PID 计算
if (pid_speed.target_val >= 0) // 判断电机方向
{
set_motor_direction(MOTOR_FWD);
}
else
{
cont_val = -cont_val;
set_motor_direction(MOTOR_REV);
}
cont_val = (cont_val > TARGET_CURRENT_MAX) ? TARGET_CURRENT_MAX : cont_val; // 电流上限处理
set_pid_target(&pid_curr, cont_val); // 设定电流的目标值
#if defined(PID_ASSISTANT_EN)
int32_t temp = cont_val;
set_computer_value(SEND_TARGET_CMD, CURVES_CH3, &temp, 1); // 给通道 3 发送目标值
#endif
}
/* 电流环计算 */
cont_val = curr_pid_realize(&pid_curr, actual_current); // 进行 PID 计算
if (cont_val < 0)
{
cont_val = 0; // 下限处理
}
else if (cont_val > PWM_MAX_PERIOD_COUNT)
{
cont_val = PWM_MAX_PERIOD_COUNT; // 速度上限处理
}
set_motor_speed(cont_val); // 设置 PWM 占空比
#if defined(PID_ASSISTANT_EN)
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &Capture_Count, 1); // 给通道 1 发送实际值
// set_computer_value(SEND_FACT_CMD, CURVES_CH2, &actual_speed, 1); // 给通道 2 发送实际值
// set_computer_value(SEND_FACT_CMD, CURVES_CH3, &actual_current, 1); // 给通道 3 发送实际值
#else
printf("1.电流:实际值:%d. 目标值:%.0f.\n", Capture_Count, get_pid_target(&pid_location)); // 打印实际值和目标值
#endif
}
}
/*********************************************END OF FILE**********************/
PID设置和整定
- 先整定内环,再整定外环。
- 先注释位置环和速度环,只保留电流和PID,进行整定。
- 然后,再解开速度环,整定速度环PID,最后解开位置环,整定位置环PID。
手撕PID(带死区、积分分离、不完全微分)_pid死区程序-CSDN博客
闭环死区
带死区的PID控制算法就是检测偏差值,若是偏差值达到一定程度,就进行调节。若是偏差值较小,就认为没有偏差。用公式表示如下:
其中的死区值得选择需要根据具体对象认真考虑,因为该值太小就起不到作用,该值选取过大则可能造成大滞后。
带死区的PID算法,对无论位置型还是增量型的表达式没有影响,不过它是一个非线性系统。
除以上描述之外还有一个问题,在零点附近时,若偏差很小,进入死去后,偏差置0会造成积分消失,如是系统存在静差将不能消除,所以需要人为处理这一点。
一个水龙头,选择角度设置为了3.5度,但是它只能一度一度的旋转,导致系统再3度和4度之间波动,使得系统不稳定。设置闭环死区,把这个误差可忽略即可。
积分分离
在过程的启动、结束或大幅度增减设定值时,短时间内系统输出有很大偏差,会造成PID运算的积分累积,引起超调或者振荡。为了解决这一干扰,人们引入了积分分离的思想。其思路是偏差值较大时,取消积分作用,以免于超调量增大;而偏差值较小时,引入积分作用,以便消除静差,提高控制精度。
具体的实现步骤是:根据实际情况,设定一个阈值;当偏差大于阈值时,消除积分仅用PD控制;当偏差小于等于阈值时,引入积分采用PID控制。则控制算法可表示为:
偏差很大的差值,直接在积分时忽略,防止积分饱和。
不完全微分PID
微分项有引入高频干扰的风险,但若在控制算法中加入低通滤波器,则可使系统性能得到改善。方法之一就是在PID算法中加入一个一阶低通滤波器。这就是所谓的不完全微分,其结构图如下:
微分部分表达式为:
其中 其中α的取值在0和1之间,有滤波常数和采样周期确定