STM32双轮平衡小车(基于STM32F103C8T6HAL库)

STM32双轮平衡小车参考教程

这个项目是跟做以上UP的STM32双轮平衡小车,主要是为了学习电机驱动和PID控制。这篇我就不提供源码了,我也是跟学的,原作者也提供了源码,我记录一下自己的理解。

1 PID原理

1.1 PID简介

1.2 PID演示 

KP:比例环节:向目标靠近,但是在目标附近振荡

KI:积分环节:消除稳态误差

KD:微分环节:减小振荡

(1)无任何控制

上图中,可以看到中间是一个小球和绿色的板子,我们点击修改目标后波动板子位置。如下图所示:

然后我们切换到推动小球模式下,鼠标波动一下小球,可以看到下图所示结果:

在没有任何控制的情况的下,小球会跑飞。 

(2)施加KP控制

首先要给一个输出限幅,这个输出限幅是一定要加的。在实际应用中,比如说要控制电机,电机转动是有一个速度上限的,我们先给个10,然后KP给个0.1。会产生在这样的情况,小球会以目标为中心,在目标周围振荡,并且振荡幅度会越变越大,并且KP值越大,振荡幅度越大。

所以KP的作用就是能够让小球接近目标,但是会发生震荡,并且振荡幅度会越来越大。 

(3)施加KD控制

为了减小KP产生的振荡,我们要引入微分环节KD,就是求导,这里对位置求导实际上就是速度,相当于引入了一个阻尼力,减小振荡。

引入KD以后,我们会发现振荡幅度远远减小,并且KD越大,幅度越小。具体表现为KD小的情况下作用弱,要振荡多次才能停下来,KD大的时候阻尼力变大,振荡次数减少。但是也不是越大越好,因为在实际系统中,可能受到其它因素的干扰,KD过大会将这些干扰放大,造成小球在本该静止大的位置发生抽搐现象。存在的问题就是目标位置和当前位置有一定差距,就是使用KD控制没办法减小系统平衡时和目的之间的差距,即稳态误差。

(4)引入KI控制

为了消除稳态误差,需要引入KI环节。因为PD控制器是和时间无关的,因此在整个系统达到稳定的过程中,没办法消除累计误差,因此需要引入KI环节。

1.3 平衡小车PID控制原理 

1.3.1 直立控制

我们先看一下结论:

我们为什么要得到这样的结论? 如何得到这样的结论?

中立位:这个中立位是个相对的概念,我们可能会认为中立位是小车直立(θ=0)的时候,实际上只要小车能够保持平衡不请到都是中立位。比如说我的小车要前进,那必然不可能保持完全直立,必须要有一定的θ才能前进。

a=k_{1}\theta +k_{2}\theta ^{'}:这是不是就是KP+KD(比例+微分)控制,KP让小车能够趋于平衡,KD减小振荡、缩短小车达到平衡的时间。

现在结论就来了:可以这么理解,上面标红色的那个公式是小车中立位的状态,而这个结论就是小车从非中立位靠近中立位的过程。 

2 双轮平衡小车各模块驱动

草履虫都能学会的STM32平衡小车教程(基础篇)_哔哩哔哩_bilibili

此项目的重点是PID控制,因此关于各外设不详细展开,这位UP提供的源码里面也有,所以重点记录PID的使用。

2.1 硬件原理图

2.2 OLED屏幕

因为这个外设江科大的讲的更详细,就不单独记录了。

参考:

[模块教程] 第1期 0.96寸OLED显示屏_哔哩哔哩_bilibili

7.OLED显示_哔哩哔哩_bilibili

2.3 MPU6050

这个模块江科大讲的更详细,就不单独记录了。

参考:

 江协科技STM32学习笔记(第09章 I2C通信)_xcl、xda-CSDN博客

8.MPU6050读取角度_哔哩哔哩_bilibili

2.4 超声波测距模块

超声波测距模块HC-SR04(基于STM32F103C8T6HAL库)-CSDN博客

2.5 电机驱动及编码器测速

电机驱动及编码器测速(基于STM32F103C8T6HAL库)-CSDN博客

2.6 蓝牙串口模块JDY-31

蓝牙串口模块JDY-31(基于STM32F103C8T6HAL库)-CSDN博客

3 PID控制

在实现以上驱动部分基础上实现PID控制。

3.1 PID程序编写

创建pid.c和pid.h

3.1.1 直立环PD控制器

/*直立环PD控制器
输入:期望角度、真实角度、角速度
期望角度:通过外环速度环控制器获得,
真实角度:通过MPU6050陀螺仪获得,
角速度:通过MPU6050陀螺仪获得
*/
int Vertical(float Med,float Angle,float gyro_Y)
{
	int temp;
	temp=Vertical_Kp*(Angle-Med)+Vertical_Kd*gyro_Y;
	return temp;                             //直立环的输出会反馈给速度环,所以要保持跟速度环输入一样,速度环输入是电机的PWM
}

3.1.2 速度环PI控制器

/*速度环PI控制器
输入:期望速度、左编码器测速、右编码器测速
*/
int Velocity(int Target,int encoder_L,int encoder_R)
{
	static int Err_LowOut_last,Encoder_S;
	static float a=0.7;
	int Err,Err_LowOut,temp;
	Velocity_Ki=Velocity_Kp/200;
	//1、计算偏差值
	Err=(encoder_L+encoder_R)-Target;           
	//这里应该是为了方便这样写的,因为在平衡位置,我们的小车左右速度应该是一样的,可以单独计算,为了方便,直接相加计算了
	//2、低通滤波
	Err_LowOut=(1-a)*Err+a*Err_LowOut_last;
	Err_LowOut_last=Err_LowOut;
	//3、积分
	Encoder_S+=Err_LowOut;
	//4、积分限幅(-20000~20000),防止积分值过大影响P环节
	Encoder_S=Encoder_S>20000?20000:(Encoder_S<(-20000)?(-20000):Encoder_S); 
	/*因为无法直接获得积分值,所以以上几步就是用来计算积分值的*/
	if(stop==1)Encoder_S=0,stop=0;       
	/*蓝牙下发停止指令后,让积分值清0,状态清0,给下一次使用做准备,因为下发指令后;如果有积分影响,会导致按下停止按钮后,
	不会立刻停下来,会先往前,然后再退回*/
	//5、速度环计算
	temp=Velocity_Kp*Err_LowOut+Velocity_Ki*Encoder_S;
	return temp;
}

3.1.3 转向环PD控制器 

/*转向环PD控制器
输入:角速度、目标角度值
角速度:MPU6050测得
目标角度值:在这个值是相对的,给90°就表示小车你要给我转90°
输出:电机PWM改变值,使用差速转向,转向时两个轮子转速一个高一个低
*/
int Turn(float gyro_Z,int Target_turn)
{
	int temp;
	temp=Turn_Kp*Target_turn+Turn_Kd*gyro_Z;
	return temp;
}

3.2 核心闭环控制函数

闭环控制函数就是将以上几个控制器结合到一起。

先motor.c在里添加电机速度限幅函数和电机停止函数。

#define PWM_MAX 7200
#define PWM_MIN -7200
extern uint8_t stop;
/*电机限幅函数*/
void Limit(int *motoA,int *motoB)
{
	if(*motoA>PWM_MAX)*motoA=PWM_MAX;
	if(*motoA<PWM_MIN)*motoA=PWM_MIN;
	if(*motoB>PWM_MAX)*motoB=PWM_MAX;
	if(*motoB<PWM_MIN)*motoB=PWM_MIN;
}
/*电机停止函数*/
void Stop(float *Med_Jiaodu,float *Jiaodu)
{
	if(abs((int)(*Jiaodu-*Med_Jiaodu))>60)
	{
		Load(0,0);
		stop=1;
	}
}

pid.c: 

/*闭环控制函数:将上面几个控制器整合到一块*/
void Control(void)	//每隔10ms调用一次
{
	int PWM_out;
	//1、读取编码器和陀螺仪的数据
	Encoder_Left=Read_Speed(&htim2);           //读取编码器测速  
	Encoder_Right=-Read_Speed(&htim4);        
	mpu_dmp_get_data(&pitch,&roll,&yaw);       //读取姿态角
	MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz);   //读取角速度值
	MPU_Get_Accelerometer(&aacx,&aacy,&aacz);  //读取加速度值
	
	//2、将数据传入PID控制器,计算输出结果,即左右电机转速值
	Velocity_out=Velocity(Target_Speed,Encoder_Left,Encoder_Right);   //速度环输出
	Vertical_out=Vertical(Velocity_out+Med_Angle,roll,gyrox);         //后面两个参数与MPU6050安装方向有关
	Turn_out=Turn(gyroz,Target_turn);                                 //转向换输出
	PWM_out=Vertical_out;                //直立环输出后的参数就是电机的PWM控制
	//差速转向控制
	MOTO1=PWM_out-Turn_out;
	MOTO2=PWM_out+Turn_out;              
	Limit(&MOTO1,&MOTO2);                //PWM的最大转速是`7200和7200
	Load(MOTO1,MOTO2);
	Stop(&Med_Angle,&roll);//安全检测
}

3.3 利用MPU6050产生中断 

现在需要完成的工作就是每隔一段时间进行一次采样和PID控制了,因此需要比较精准的定时器,这里采用MPU6050的INT引脚来进行,每当MPU6050采样好,就会输出一个低电平,告诉单片机采样好了。

每隔10ms采样一次的话,就设置MPU6050的采样率:

MPU_Set_Rate(100);						//设置采样率100Hz

这里需要配置PB5引脚为外部中断模式,并配置为上拉模式,下降沿触发方式,每隔10ms拉低一次电平上报采样数据给单片机。

先初始化陀螺仪再开启中断是因为我们读取数据的前提是陀螺仪初始化好,不然读不出来,所以要先初始化陀螺仪再开中断。

更改中断回调函数:

我们之前在超声波测距模块里调用了中断回调函数,使用HAL库配置的项目中断回调函数是共用的,所以要在这个函数里区别一下是哪个引脚产生的中断。

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin==GPIO_PIN_2)
	{
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_2)==GPIO_PIN_SET)
		{
			__HAL_TIM_SetCounter(&htim3,0);
			HAL_TIM_Base_Start(&htim3);
		}
		else
		{
			HAL_TIM_Base_Stop(&htim3);
			count=__HAL_TIM_GetCounter(&htim3);
			distance=count*0.017;
		}
	}
	if(GPIO_Pin==GPIO_PIN_5)
		Control();
}

4 PID调参

需要调以下几个参数

4.1 机械中值的确定

小车组装好后,将代码下载到单片机里,让小车分别向前向后倾倒,观察倾倒时候的角度值。

Med_Angle =(\theta _{1}+\theta _{2})/2

这个值可能每次开机都不一样,大概就行。

4.2 直立环数量级的确定

(1)Kp数量级

我们的输出是电机的PWM,电机的PWM在-7200~7200之间,角度大概是在0~60°,我们取个中间值30。

7200/30240
7200/10720

所以Kp大概在0~1000之间。

(2)Kd数量级

Kd就是角度的微分,即角速度,所以我们可以直接用OLED显示进行观察一下,这里为什么是gyrox呢,因为我们的小车前倾和后倾是绕x轴转动的。

经过观察,这个gyrox大概在2000~3000的话,7200/2000=3.6,所以Kd的数量级大概在0~10之间。

4.3 速度环参数数量级的确定 

速度环的输出是一个角度值,输入是7200的PWM,但是我们的输入是编码器测得的速度,所以我们可以直接加载一个满量程转速测一下。

可以测得PWM满量程的时候,编码器测得的转速大概为70左右。所以编码器测得的值会在0~70之间。

\frac{0~70}{70+70}=0.5

所以Kp大概在0~1之间,Ki根据工程经验得出,约为Kp/200。

4.4 直立环参数极性的确定

我们可以单独给要判断赋一个初值,小车向前倾车轮向前转就是正确极性,极性反了就会导致小车向前倾,轮子向后转。

4.5 速度环参数极性的确定

先给直立环赋一个比较小的值,然后速度环给一个较大值:

极性正确的情况下往前倾会车轮转速会变快,极性反了车轮会转得比较慢。 

4.6 直立环调参

(1)Kp调参

我们在其它参数没有得时候先给一个Kp。

观察现象:下载后朝某一个方向压一下小车,如果现象是小车移动时候倾角很明显,那就是Kp太小了,如果现象是小车振荡幅度很大,那就是超调了,Kp给的太大。反复测试得到一个合适的值,先调到一个明显临界超调的状态。

(2)Kd调参

Kd得目的是为了消除小车平衡时候的振荡,先调到一个明显临界超调的状态。

以上两个值是我们是使用明显临界超调的时候调出来的参数,所以最终我们还需要乘以一个系数0.6。

4.7 速度环调参 

在上面参数的基础上,先给速度环Kp一个参数0.4,Ki为Kp/200,然后反复修改,直至没有显著振荡。

4.8 转向环调参

这个转向换并不是一个严格的PD控制器, 不是那种目标角度值减去实际角度值的格式,因为陀螺仪测量转向角(偏航角)这个角度的时候,测的不是很准,误差非常大,因为陀螺仪在做DMP的时候,是累计误差的,是把时间上的误差都累积起来,所以算出来的偏航角误差非常大,所以没办法直接用这个角度。这里Targe_turn直接是目标的转向速度,Kp环节主要负责遥控转向,Kp值起到放大作用,Kp越大转向越快,这个值不是特别重要;Kd主要是为了保证转向时候走直线,Kd的数量值与速度环Ki量级是一样的,一般取一个经验值0.6。

5 蓝牙遥控

这一节就是为了实现手机蓝牙串口助手下发指令控制小车前后左右、停止运行。

初始化的时候要先打开串口才能接收数据

void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */

  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */
	Bluetooth_data=rx_buf[0];
	if(Bluetooth_data==0x00)		 Fore=0,Back=0,Left=0,Right=0;//刹
	else if(Bluetooth_data==0x01)Fore=1,Back=0,Left=0,Right=0;//前
	else if(Bluetooth_data==0x05)Fore=0,Back=1,Left=0,Right=0;//后
	else if(Bluetooth_data==0x03)Fore=0,Back=0,Left=0,Right=1;//右
	else if(Bluetooth_data==0x07)Fore=0,Back=0,Left=1,Right=0;//左
	else												 Fore=0,Back=0,Left=0,Right=0;//刹
	HAL_UART_Receive_IT(&huart3,rx_buf,1);
  /* USER CODE END USART3_IRQn 1 */
}

 这里的值要跟手机蓝牙串口设置的一样,下发指令后才会正常响应。

/*闭环控制函数:将上面几个控制器整合到一块*/
void Control(void)	//每隔10ms调用一次
{
	int PWM_out;
	//1、读取编码器和陀螺仪的数据
	Encoder_Left=Read_Speed(&htim2);           //读取编码器测速  
	Encoder_Right=-Read_Speed(&htim4);        
	mpu_dmp_get_data(&pitch,&roll,&yaw);       //读取姿态角
	MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz);   //读取角速度值
	MPU_Get_Accelerometer(&aacx,&aacy,&aacz);  //读取加速度值
	//遥控
	if((Fore==0)&&(Back==0))Target_Speed=0;//未接受到前进后退指令-->速度清零,稳在原地
	if(Fore==1)
	{
		if(distance<50)
			Target_Speed++;                 //这个先+还是先-要根据极性做出相应改变
		else
			Target_Speed--;
	}
	if(Back==1){Target_Speed++;}//
	Target_Speed=Target_Speed>SPEED_Y?SPEED_Y:(Target_Speed<-SPEED_Y?(-SPEED_Y):Target_Speed);//限幅
	
	/*左右*/
	if((Left==0)&&(Right==0))Target_turn=0;
	if(Left==1)Target_turn+=30;	//左转
	if(Right==1)Target_turn-=30;	//右转
	Target_turn=Target_turn>SPEED_Z?SPEED_Z:(Target_turn<-SPEED_Z?(-SPEED_Z):Target_turn);//限幅( (20*100) * 100   )
	
	/*转向约束*/
	if((Left==0)&&(Right==0))Turn_Kd=0.6;//若无左右转向指令,则开启转向约束
	else if((Left==1)||(Right==1))Turn_Kd=0;//若左右转向指令接收到,则去掉转向约束

	
	//2、将数据传入PID控制器,计算输出结果,即左右电机转速值
	Velocity_out=Velocity(Target_Speed,Encoder_Left,Encoder_Right);   //速度环输出
	Vertical_out=Vertical(Velocity_out+Med_Angle,roll,gyroy);         //后面两个参数与MPU6050安装方向有关
	Turn_out=Turn(gyroz,Target_turn);                                 //转向换输出
	PWM_out=Vertical_out;                //直立环输出后的参数就是电机的PWM控制
	//差速转向控制
	MOTO1=PWM_out-Turn_out;
	MOTO2=PWM_out+Turn_out;              
	Limit(&MOTO1,&MOTO2);                //PWM的最大转速是`7200和7200
	Load(MOTO1,MOTO2);
	Stop(&Med_Angle,&roll);//安全检测
}

6 最终代码

(1)main.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "oled.h"
#include "IIC.h"
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h"
#include "mpu6050.h"
#include "stdio.h"
#include "sr04.h"
#include "motor.h"
#include "encoder.h"
/* 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 */
extern float roll;
extern int Encoder_Left,Encoder_Right;
uint8_t display_buf[20];
uint32_t sys_tick;
extern float distance;
extern uint8_t rx_buf[2];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void Read(void);
/* 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_I2C1_Init();
  MX_TIM3_Init();
  MX_TIM1_Init();
  MX_TIM2_Init();
  MX_TIM4_Init();
  MX_USART3_UART_Init();
  /* USER CODE BEGIN 2 */
	OLED_Init();
	OLED_Clear();
	MPU_Init();
	mpu_dmp_init();
	OLED_ShowString(0,00,"Init Sucess",16);
	HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
	HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);
	HAL_TIM_Encoder_Start(&htim2,TIM_CHANNEL_ALL);
	HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL);
	HAL_UART_Receive_IT(&huart3,rx_buf,1);
	Load(0,0);
	OLED_Clear();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		sprintf((char *)display_buf,"Encoder_L:%d   ",Encoder_Left);
		OLED_ShowString(0,0,display_buf,16);
		sprintf((char *)display_buf,"Encoder_R:%d   ",Encoder_Right);
		OLED_ShowString(0,2,display_buf,16);		
		sprintf((char *)display_buf,"roll:%.1f   ",roll); 
		OLED_ShowString(0,4,display_buf,16);
		GET_Distance();
		sprintf((char *)display_buf,"distance:%.1f  ",distance);
		OLED_ShowString(0,6,display_buf,12);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */


/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

 (2)motor.c

#include "motor.h"

#define PWM_MAX 7200
#define PWM_MIN -7200
extern TIM_HandleTypeDef htim1;
extern uint8_t stop;
int abs(int p)
{
	if(p>0)
		return p;
	else
		return -p;
}

void Load(int moto1,int moto2)			//-7200~7200
{
	if(moto1<0)
	{
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_SET);
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET);
	}
	else
	{
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_RESET);
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
	}
	__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_4,abs(moto1));
	if(moto2<0)
	{
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_SET);
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
	}
	else
	{
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_RESET);
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
	}
	__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_1,abs(moto2));
}

void Limit(int *motoA,int *motoB)
{
	if(*motoA>PWM_MAX)*motoA=PWM_MAX;
	if(*motoA<PWM_MIN)*motoA=PWM_MIN;
	if(*motoB>PWM_MAX)*motoB=PWM_MAX;
	if(*motoB<PWM_MIN)*motoB=PWM_MIN;
}
void Stop(float *Med_Jiaodu,float *Jiaodu)
{
	if(abs((int)(*Jiaodu-*Med_Jiaodu))>60)
	{
		Load(0,0);
		stop=1;
	}
}

(3)motor.h

#ifndef _MOTOR_H
#define _MOTOR_H

#include "stm32f1xx_hal.h"
void Load(int moto1,int moto2);
void Limit(int *motoA,int *motoB);
void Stop(float *Med_Jiaodu,float *Jiaodu);
#endif

(4)pid.c

#include "pid.h"
#include "encoder.h"
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h"
#include "mpu6050.h"
#include "motor.h"

//传感器数据变量
int Encoder_Left,Encoder_Right;
float pitch,roll,yaw;
short gyrox,gyroy,gyroz;
short	aacx,aacy,aacz;

//闭环控制中间变量
int Vertical_out,Velocity_out,Turn_out,Target_Speed,Target_turn,MOTO1,MOTO2;
float Med_Angle=3;//平衡时角度值偏移量(机械中值)
//参数
float Vertical_Kp=-120,Vertical_Kd=-0.8;			//直立环 数量级(Kp:0~1000、Kd:0~10)
float Velocity_Kp=-1.2,Velocity_Ki=-0.006;		//速度环 数量级(Kp:0~1)
float Turn_Kp=10,Turn_Kd=0.1;											//转向环

uint8_t stop;

extern TIM_HandleTypeDef htim2,htim4;
extern float distance;
extern uint8_t Fore,Back,Left,Right;
#define SPEED_Y 12 //俯仰(前后)最大设定速度
#define SPEED_Z 150//偏航(左右)最大设定速度 

//直立环PD控制器
//输入:期望角度、真实角度、角速度
int Vertical(float Med,float Angle,float gyro_Y)
{
	int temp;
	temp=Vertical_Kp*(Angle-Med)+Vertical_Kd*gyro_Y;
	return temp;
}

//速度环PI控制器
//输入:期望速度、左编码器、右编码器
int Velocity(int Target,int encoder_L,int encoder_R)
{
	static int Err_LowOut_last,Encoder_S;
	static float a=0.7;
	int Err,Err_LowOut,temp;
	Velocity_Ki=Velocity_Kp/200;
	//1、计算偏差值
	Err=(encoder_L+encoder_R)-Target;
	//2、低通滤波
	Err_LowOut=(1-a)*Err+a*Err_LowOut_last;
	Err_LowOut_last=Err_LowOut;
	//3、积分
	Encoder_S+=Err_LowOut;
	//4、积分限幅(-20000~20000)
	Encoder_S=Encoder_S>20000?20000:(Encoder_S<(-20000)?(-20000):Encoder_S);
	if(stop==1)Encoder_S=0,stop=0;
	//5、速度环计算
	temp=Velocity_Kp*Err_LowOut+Velocity_Ki*Encoder_S;
	return temp;
}


//转向环PD控制器
//输入:角速度、角度值
int Turn(float gyro_Z,int Target_turn)
{
	int temp;
	temp=Turn_Kp*Target_turn+Turn_Kd*gyro_Z;
	return temp;
}

void Control(void)	//每隔10ms调用一次
{
	int PWM_out;
	//1、读取编码器和陀螺仪的数据
	Encoder_Left=Read_Speed(&htim2);
	Encoder_Right=-Read_Speed(&htim4);
	mpu_dmp_get_data(&pitch,&roll,&yaw);
	MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz);
	MPU_Get_Accelerometer(&aacx,&aacy,&aacz);
	//遥控
	if((Fore==0)&&(Back==0))Target_Speed=0;//未接受到前进后退指令-->速度清零,稳在原地
	if(Fore==1)
	{
		if(distance<50)
			Target_Speed++;
		else
			Target_Speed--;
	}
	if(Back==1){Target_Speed++;}//
	Target_Speed=Target_Speed>SPEED_Y?SPEED_Y:(Target_Speed<-SPEED_Y?(-SPEED_Y):Target_Speed);//限幅
	
	/*左右*/
	if((Left==0)&&(Right==0))Target_turn=0;
	if(Left==1)Target_turn+=30;	//左转
	if(Right==1)Target_turn-=30;	//右转
	Target_turn=Target_turn>SPEED_Z?SPEED_Z:(Target_turn<-SPEED_Z?(-SPEED_Z):Target_turn);//限幅( (20*100) * 100   )
	
	/*转向约束*/
	if((Left==0)&&(Right==0))Turn_Kd=0.6;//若无左右转向指令,则开启转向约束
	else if((Left==1)||(Right==1))Turn_Kd=0;//若左右转向指令接收到,则去掉转向约束

	
	//2、将数据传入PID控制器,计算输出结果,即左右电机转速值
	Velocity_out=Velocity(Target_Speed,Encoder_Left,Encoder_Right);
	Vertical_out=Vertical(Velocity_out+Med_Angle,roll,gyrox);
	Turn_out=Turn(gyroz,Target_turn);
	PWM_out=Vertical_out;
	MOTO1=PWM_out-Turn_out;
	MOTO2=PWM_out+Turn_out;
	Limit(&MOTO1,&MOTO2);
	Load(MOTO1,MOTO2);
	Stop(&Med_Angle,&roll);//安全检测
}

(5)pid.h

#ifndef __PID_H__
#define __PID_H__

#include "stm32f1xx_hal.h"

void Control(void);	//每隔10ms调用一次

#endif

(6)stm32f1xx_it.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    stm32f1xx_it.c
  * @brief   Interrupt Service Routines.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f1xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN TD */

/* USER CODE END TD */

/* 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 */
uint8_t rx_buf[2],Bluetooth_data;
uint8_t Fore,Back,Left,Right;
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/* External variables --------------------------------------------------------*/
extern UART_HandleTypeDef huart3;
/* USER CODE BEGIN EV */

/* USER CODE END EV */

/******************************************************************************/
/*           Cortex-M3 Processor Interruption and Exception Handlers          */
/******************************************************************************/
/**
  * @brief This function handles Non maskable interrupt.
  */
void NMI_Handler(void)
{
  /* USER CODE BEGIN NonMaskableInt_IRQn 0 */

  /* USER CODE END NonMaskableInt_IRQn 0 */
  /* USER CODE BEGIN NonMaskableInt_IRQn 1 */
  while (1)
  {
  }
  /* USER CODE END NonMaskableInt_IRQn 1 */
}

/**
  * @brief This function handles Hard fault interrupt.
  */
void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */

  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Memory management fault.
  */
void MemManage_Handler(void)
{
  /* USER CODE BEGIN MemoryManagement_IRQn 0 */

  /* USER CODE END MemoryManagement_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
    /* USER CODE END W1_MemoryManagement_IRQn 0 */
  }
}

/**
  * @brief This function handles Prefetch fault, memory access fault.
  */
void BusFault_Handler(void)
{
  /* USER CODE BEGIN BusFault_IRQn 0 */

  /* USER CODE END BusFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_BusFault_IRQn 0 */
    /* USER CODE END W1_BusFault_IRQn 0 */
  }
}

/**
  * @brief This function handles Undefined instruction or illegal state.
  */
void UsageFault_Handler(void)
{
  /* USER CODE BEGIN UsageFault_IRQn 0 */

  /* USER CODE END UsageFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_UsageFault_IRQn 0 */
    /* USER CODE END W1_UsageFault_IRQn 0 */
  }
}

/**
  * @brief This function handles System service call via SWI instruction.
  */
void SVC_Handler(void)
{
  /* USER CODE BEGIN SVCall_IRQn 0 */

  /* USER CODE END SVCall_IRQn 0 */
  /* USER CODE BEGIN SVCall_IRQn 1 */

  /* USER CODE END SVCall_IRQn 1 */
}

/**
  * @brief This function handles Debug monitor.
  */
void DebugMon_Handler(void)
{
  /* USER CODE BEGIN DebugMonitor_IRQn 0 */

  /* USER CODE END DebugMonitor_IRQn 0 */
  /* USER CODE BEGIN DebugMonitor_IRQn 1 */

  /* USER CODE END DebugMonitor_IRQn 1 */
}

/**
  * @brief This function handles Pendable request for system service.
  */
void PendSV_Handler(void)
{
  /* USER CODE BEGIN PendSV_IRQn 0 */

  /* USER CODE END PendSV_IRQn 0 */
  /* USER CODE BEGIN PendSV_IRQn 1 */

  /* USER CODE END PendSV_IRQn 1 */
}

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

/******************************************************************************/
/* STM32F1xx Peripheral Interrupt Handlers                                    */
/* Add here the Interrupt Handlers for the used peripherals.                  */
/* For the available peripheral interrupt handler names,                      */
/* please refer to the startup file (startup_stm32f1xx.s).                    */
/******************************************************************************/

/**
  * @brief This function handles EXTI line2 interrupt.
  */
void EXTI2_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI2_IRQn 0 */

  /* USER CODE END EXTI2_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
  /* USER CODE BEGIN EXTI2_IRQn 1 */

  /* USER CODE END EXTI2_IRQn 1 */
}

/**
  * @brief This function handles EXTI line[9:5] interrupts.
  */
void EXTI9_5_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI9_5_IRQn 0 */

  /* USER CODE END EXTI9_5_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_5);
  /* USER CODE BEGIN EXTI9_5_IRQn 1 */

  /* USER CODE END EXTI9_5_IRQn 1 */
}

/**
  * @brief This function handles USART3 global interrupt.
  */
void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */

  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */
	Bluetooth_data=rx_buf[0];
	if(Bluetooth_data==0x00)		 Fore=0,Back=0,Left=0,Right=0;//刹
	else if(Bluetooth_data==0x01)Fore=1,Back=0,Left=0,Right=0;//前
	else if(Bluetooth_data==0x05)Fore=0,Back=1,Left=0,Right=0;//后
	else if(Bluetooth_data==0x03)Fore=0,Back=0,Left=0,Right=1;//右
	else if(Bluetooth_data==0x07)Fore=0,Back=0,Left=1,Right=0;//左
	else												 Fore=0,Back=0,Left=0,Right=0;//刹
	HAL_UART_Receive_IT(&huart3,rx_buf,1);
  /* USER CODE END USART3_IRQn 1 */
}

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

其余部分不需要进行更改,保持原来各模块的驱动原来状态即可。

7 手机蓝牙串口助手的设置

连接蓝牙模块,连接的时候如果搜不到的话,开启手机的定位。

控制参数设置

UP:0x01

DOWN:0x05

LEFT:0x07

RIGHT:0x03

STOP: 0x00

上面几个参数和串口中断里对应上即可。

我又不小心玩坏了一个蓝牙模块,这个UP设计的板子上是按照引脚对引脚接的:

蓝牙VCC——板子VCC;

蓝牙GND——板子GND;

蓝牙RX———板子RX;

蓝牙TX———板子TX。

我没注意看,按照常规思维接的RX-TX,TX-RX,直接给整冒烟了。后面再接上去就马上蓝牙模块马上就烫手了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/875453.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot+Vue+MySQL的笔记记录分享网站

系统展示 用户前台界面 管理员后台界面 系统背景 在当今数字化时代&#xff0c;笔记记录与分享已成为学习、工作与生活中不可或缺的一部分。为了满足用户高效整理思绪、便捷分享知识的需求&#xff0c;我们设计了一款基于SpringBoot后端框架、Vue前端框架及MySQL数据库的笔记记…

存储课程学习笔记1_访问scsi磁盘读写测试(struct sg_io_hdr,ioctl,mmap)

创建虚拟机时&#xff0c;可以选择SCSI,STAT,NVME不同类型的磁盘。 0&#xff1a;总结 》了解内核提供的访问scsi的结构和方法 &#xff08;主要是sg_io_hdr_t 结构体和ioctl函数&#xff09;。 》需要读scsi协议文档&#xff0c;了解相关指令&#xff0c;只演示了16字节固定…

Leetcode第414周赛第二题:3281. 范围内整数的最大得分

一&#xff1a;题目&#xff1a; 给你一个整数数组 start 和一个整数 d&#xff0c;代表 n 个区间 [start[i], start[i] d]。 你需要选择 n 个整数&#xff0c;其中第 i 个整数必须属于第 i 个区间。所选整数的 得分 定义为所选整数两两之间的 最小 绝对差。 返回所选整数的…

利他决策的神经机制:脑电功能连接网络分析

摘要 利他主义是一种复杂的亲社会行为&#xff0c;具有多样性的动机和显著的个体差异。研究利他主义的神经机制对于识别行为中的亲社会和反社会倾向的客观标志至关重要。本研究旨在通过网络方法分析基于EEG的功能连接模式来深入探讨利他主义的机制。为了实验性地引发利他决策情…

eclipse配置maven

eclipse配置maven 启动 Eclipse&#xff0c;转到 Window > Preferences 在左侧导航栏中&#xff0c;展开 Maven 节点。 在 User Settings 下&#xff0c;单击 Add。 浏览到 Maven 安装目录中 conf/settings.xml 文件。 在 Global Settings 下&#xff0c;单击 Add。 浏览到…

动态规划算法---04.斐波那契数列模型_解码方法_C++

题目链接&#xff1a;91. 解码方法 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/decode-ways/description/ 一、题目解析 题目&#xff1a; 题目大意&#xff1a;从题目中我们可以知道&#xff0c;解码就是在字符串s中由‘1’到‘26’的字符可以转化…

架构模式:MVC

引言 MVC&#xff0c;即 Model&#xff08;模型&#xff09;-View&#xff08;视图&#xff09;-Controller&#xff08;控制器&#xff09;&#xff0c;是广泛应用于交互式系统中的典型架构模式&#xff0c;尤其在 GUI 和 Web 应用中。 MVC 的概念源自 GOF&#xff08;Gang …

Web安全之GroovyShell讲解:错误与正确示范,安全问题与解决方案

第一章、引言 Groovy 是一门基于 Java 虚拟机&#xff08;JVM&#xff09;的动态语言&#xff0c;而 GroovyShell 是 Groovy 提供的一个灵活强大的脚本执行工具。通过 GroovyShell&#xff0c;开发者可以在运行时动态执行 Groovy 脚本&#xff0c;它的灵活性非常适合那些需要动…

多层建筑能源参数化模型和城市冠层模型的区别

多层建筑能源参数化&#xff08;Multi-layer Building Energy Parameterization, BEP&#xff09;模型和城市冠层模型&#xff08;Urban Canopy Model, UCM&#xff09;都是用于模拟城市环境中能量交换和微气候的数值模型&#xff0c;但它们的侧重点和应用场景有所不同。以下是…

MongoDB事务机制

事务机制 1.事务概念 在对数据的操作的过程中&#xff0c;涉及到一连串的操作&#xff0c;这些操作如果失败&#xff0c;会导致我们的数据部分变化了&#xff0c;部分没变化。这个过程就好比如你去吃早餐&#xff0c;你点完餐了&#xff0c;并且吃完早餐了&#xff0c;没付钱你…

【文件包含】——日志文件注入

改变的确很难&#xff0c;但结果值得冒险 本文主要根据做题内容的总结&#xff0c;如有错误之处&#xff0c;还请各位师傅指正 一.伪协议的失效 当我们做到关于文件包含的题目时&#xff0c;常用思路其实就是使用伪协议&#xff08;php:filter,data,inpput等等&#xff09;执行…

职业技能大赛背景下的移动互联网应用软件开发(Android)实训室建设方案

一、建设背景 随着科技的持续进步&#xff0c;移动设备已成为人们日常生活中不可或缺的一部分。据相关数据&#xff0c;移动互联网的使用率在近年来显著上升。在这样的背景下&#xff0c;移动互联技术不仅推动了科技的发展&#xff0c;也渗透到了智能家居、车联网、工业自动化…

TESSY创建需要手写桩的测试用例

如果需要让桩函数有额外的功能&#xff0c;如&#xff1a;传参检测、局部数据处理、多传参检测、函数实现变更等&#xff0c;可以进行手写桩。 我们以tessy5.1 IDE为例&#xff0c;给大家展示编写一个需要手写桩的测试用例过程。 1、前期的准备工作 可以参考以下文章&#xff1…

Qt常用控件——QTextEdit

文章目录 QTextEdit核心属性和信号同步显示示例信号示例 QTextEdit核心属性和信号 QTextEdit表示多行输入框&#xff0c;是一个富文本和markdown编辑器&#xff0c;并且能在内存超出编辑框范围时自动提供滚动条。 QPlainTexEdit是纯文本&#xff0c;QTextEdit不仅表示纯文本&a…

记得忘记密码情况下如何退出苹果Apple ID

在日常使用苹果手机时&#xff0c;我们可能会遇到需要退出Apple ID的情况&#xff0c;比如更换手机、不再使用某些服务或出于安全考虑等。下面&#xff0c;我们就来详细介绍一下苹果手机如何退出Apple ID。 情况一&#xff1a;记得Apple ID密码 若是记得Apple ID密码&#xff…

Tomcat服务详解

一、部署Tomcat服务器 JDK安装官方网址&#xff1a;https://www.oracle.com/cn/java Tomcat安装官方网址&#xff1a;Apache Tomcat - Welcome! 安装JDK 1.获取安装包 wget https://download.oracle.com/otn/java/jdk/8u411-b09/43d62d619be4e416215729597d70b8ac/jdk-8u41…

透明任务栏怎么设置?Windows电脑任务栏透明效果(详尽指南)

电脑任务栏可以自定义调整透明度&#xff0c;在Windows10和Windows11系统中&#xff0c;任务栏透明是默认不透明的。如果想要设置任务栏透明度&#xff0c;那么推荐以下方法实现&#xff0c;简单三个步骤即可实现&#xff0c;一起来看看吧&#xff01; 第一步、选择合适的任务栏…

基于鸿蒙API10的RTSP播放器(五:拖动底部视频滑轨实现跳转)

拖动前播放位置&#xff1a; 拖动后播放位置&#xff1a; 在Slider组件中&#xff0c;添加onChange方法进行监听&#xff0c;当视频轨道拖放结束时&#xff0c;触发this.seekTo()函数&#xff0c;其中seekTo函数需要传递一个视频已播放时长作为参数 Slider({ value: this.p…

【iOS】push和present的区别

【iOS】push和present的区别 文章目录 【iOS】push和present的区别前言pushpop presentdismiss简单小demo来展示dismiss和presentdismiss多级 push和present的区别区别相同点 前言 在iOS开发中&#xff0c;我们经常性的会用到界面的一个切换的问题&#xff0c;这里我们需要理清…

【专题】2024年8月医药行业报告合集汇总PDF分享(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p37621 在科技飞速发展的当今时代&#xff0c;医药行业作为关乎人类生命健康的重要领域&#xff0c;正处于前所未有的变革浪潮之中。数智医疗服务的崛起&#xff0c;为医疗模式带来了全新的转变&#xff0c;开启了医疗服务的新时代。…