本个STM32平衡车项目采用STM32f103c8t6,电机驱动采用的是带稳压的TB6612电机驱动模块,电机采用的是直流霍尔编码的有刷电机。
首先是需要明确硬件接线原理,STM32f103c8t6的端口输出PWM-->驱动TB6612-->驱动电机转动。
12v电源-->降压模块和电机供电-->输出3.3v和5v,3.3v给STM32f103c8t6芯片供电,同时输出5v给TB6612板载供电。
首先我们需要知道TB6612的驱动方法,下图为他的
.目前就单一个电机驱动来说,要想让电机驱动起来,就需要满足以下条件:16引脚PWMA有PWM波形输入、AIN1和AIN2有控制电平的输入、STBY有5v的板载供电。其中AIN1=1 AIN2=0,电机正转/AIN1=0 AIN2=1,电机反转/AIN1=0 AIN2=0,电机不转。所以驱动电机A的接线图如下:
如果需要控制电机B的话也只需要和控制A一样输入PWMB和BIN1、BIN2即可,输出为BO1和BO2。但是板载供电STBY无论如何都要5v供电的哟!!!
接下来就的主要问题就是如何利用STM32f103c8t6输出PWM给电机驱动板啦!!
老样子我们还是利用STM32CubeMX对STM32f103c8t6进行开发。首先需要打开STM32CubeMX软件选择stm32f103c8tx:
然后配置常规的RCC和SYS
然后我们需要配置哪个GPIO来输出PWM,具体的GPIO需要看你工程上引脚的安排来配置,我们这就随意配置一个PA1引脚吧!!点开PA1引脚看看他的引脚是与哪个TIM的哪个通道相连的(具体也可以查看芯片手册!!!!)
可以看到PA1是与TIM2的通道2相连的,那我们就选中TIM_CH2,然后就去配置TIM2。
然后我们就需要去配置时钟树:
这里的原理是需要琢磨的,主要是控制芯片的工作频率以及其TIM2的工作频率:
其工作原理如下:定时器工作频率=外设总线频率/(PSC+1)【PSC:预分频器】
定时频率=定时器工作频率/(CNT+1)【CNT:自动重转载值】
PWM的工作模式等可以去官方资料的芯片手册中可以查到,这里就不多赘述啦!!!生成工程后我们需要手动的打开TIM2的通道2输出PWM。
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
uint16_t pwmVal=500; //定义PWM的占空比50%!!!
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2); //打开定时器2的通道2
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,pwmVal); //设置定时器2通道2PWM输出的占空比
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
然后烧录到板子上就可以实现PA1引脚的PWM输出了!!对其上诉的硬件接线原理就可以发现电机开始被驱动了。
在这采用PWM控制呼吸灯的原理对电机进行驱动,可以发现电机先启动,一会后又开始制动:代码如下:
#include "main.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
uint16_t pwmVal=0;
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_3);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
while(pwmVal<500)
{
pwmVal ++;
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,pwmVal);
HAL_Delay(50);
}
while(pwmVal)
{
pwmVal --;
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,pwmVal);
HAL_Delay(50);
}
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
就可以发现电机匀速启动然后匀速制动,如果需要调节启动和制动的时间则调节while循环里的HAL_Delay即可。
接下来我们简单的来说说电机的PID控制算法。
PID控制应该算是应用非常广泛的控制算法了。小到控制一个元件的温度,大到控制无人机的飞行姿态和飞行速度等等,都可以使用PID控制。这里我们从原理上来理解PID控制。PID(proportion integration differentiation)其实就是指比例,积分,微分控制。PID的公式和原理推导我就不多赘述了,大伙有兴趣的话可以去看看其他大佬们写的原理,这里只简单的说说给个参数的作用和如何调节三个参数!!!(重点)
P(比例系数):成比例的反应控制系统中输入与输出的偏差信号,只要偏差一旦产生,就立刻产生控制的作用来减少产生的误差。
I(Ti积分系数):在比例控制环节产生了静态误差,在积分环节中,主要作用就是消除静态误差来提高系统的稳定性。
D(Td微分系数):微分环节是反应系统偏差的一个变化趋势,可以在误差来临前提前引入一个有效的修正信号。
而常用的PID又分位置式PID和增量式PID:
位置式PID是一种非递推式算法,通过不断累加,可以直接控制执行机构(平衡小车),u(k)的值和执行机构的实际位置(如小车当前角度)是一一对应的,因此在执行机构中不带积分不见的对象中可以很好的应用。
增量式不需要不断累加,控制的增量△u(k)也只与最近3次的采样值有关。在速度闭环控制中有很好的实时性。
在进行PID控制时,位置式PID需要又积分限幅和输出限幅,而增量式PID只需要输出限幅。
重要的是PID参数的整定
这里我给大家推荐一个我常用的方法:一般调节法!!
一般调节法:在输出不振荡时,增大比例增益P。在输出不振荡时,减少积分常数Ti。
在输出不震荡时,增大微分时间常数Td。
1:确认比例增益P时,首先去掉PID中的积分Ti和微分Td,一般让Ti=0和Td=0,让其PID变为纯比例P调节。输入设定为系统允许的最大值的60%~70%,由0逐渐增大增益P,直到系统出现振荡;然后再反过来,逐渐将p减小,直至振荡消失,记录此时的比例增益P,设定为PID的比例增益P为当前的60%~70%,。
2:确定积分时间常数Ti:设定一个较大的积分时间常数Ti的初值,然后逐渐减小Ti,直至系统出现振荡,在翻过来逐渐加大Ti,直至系统振荡小时。记录此时的Ti,设定PID积分时间常数Ti为当前的150%~180%。
3:确认微分时间常数Td:积分时间常数Td一般不用设定,若要设定则与P和Ti方式相同,取不震荡的30%。
PID理想输出的时候的间两个波,前后高低4:1!!!
说到了PID,我们就需要知道他是一个闭环的控制系统,而我们需要经过采样才能知道电机的转速等信息,采样不可能是连续采样的(离散系统),这就要涉及到采样周期的选择了,
采样周期的选择:采样周期的选则是个矛盾的问题,采样周期越短控制效果越接近于连续,对于大多算法缩短采样周期可以使得控制回路的性能得到改善,但是采样周期的缩短也会增加和占用更多CPU的计算工作量,增加运算的负担,而且有些变化缓慢的受控对象也无需很高的采样频率,过高的采样周期会让CPU的资源浪费,变得没有意义,因此如何选着采样周期至关重要。从这就需要介绍一个著名的定理:香农采样定理(Nyquist——Shannon采样定理)。
在设计离散系统时,香农采样定理是必须严格遵守的一条准则,以为他指明了从采样信号中不失真的恢复原连续信号所需的理论上的最小采样周期T。香农定理指出:如果采样器的输入信号e(t)具有有限带宽,并且有直到ωn的频率分量,则使信号e(t)圆满地从采样信号e*(t)中恢复过来的采样周期T,要满足条件:,也就是
。
在控制工程实践中,一般总取,而不取恰好等于
。所以采样周期要小于整数周期的1/2,采样频率应该要大于原始频率的2倍。
数字PID控制代码的实现:通过板级支持包实现需要创建一个bsp_pid.c和bsp_pid.h来实现。
在bsp_pid.h中我们定义一下PID的结构体:
#ifndef __BSP_PID_H
#define __BSP_PID_H //预定义,如果没有定义__BSP_PID_H文件,那就定义__BSP_PID_H
typedef struct
{
float target_val; //目标值
float actual_val; //实际值
float err; //定义偏差值
float err_last; //定义上一个偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
float integral; //定义积分值
}_pid;
#endif //预定义结束(预定义标准格式!!!)
然后我们在bsp_pid.c实现一些PID算法:
//定义全局变量
_pid pid;
void PID_param_init() //初始化参数
{
/* 初始化参数 */
// printf("PID_init begin \n");
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 = 0.01; //这里PID的三个参数的值可以在这初始化!!
pid.Ki = 0.80;
pid.Kd = 0.04;
}
void set_pid_target(float temp_val)
{
pid.target_val = temp_val; // 设置当前的目标值
}
float get_pid_target(void)
{
return pid.target_val; // 设置当前的目标值
}
void set_p_i_d(float p, float i, float d)
{
pid.Kp = p; // 设置比例系数 P
pid.Ki = i; // 设置积分系数 I
pid.Kd = d; // 设置微分系数 D
}
float PID_realize(float temp_val)
{
/*计算目标值与实际值的误差*/
pid.err=pid.target_val-temp_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;
/*返回当前实际值*/
return pid.actual_val;
}
在主函数中我们需要初始化一下PID算法: /* PID算法参数初始化 */ PID_param_init();
这样我们一个简易的PID算法就构建完成了。再后面我们再来构建一下通过上位机来调试PID的3个参数,PID通常也需要和别的模块共同使用才能发挥其作用!!!
本片文章就到这里啦!!希望对大家有帮助,如果发现错误可以提出!!如果喜欢请点个赞。最后附上一张本次PWM和PID控制小实验的图吧!!!如果有兴趣的小伙伴可以私信我一起讨论和研究哟!!