Cubemx平衡车开发过程(STM32F103C8T6)

写在前面:

正在备战今年的电赛的控制赛道,为了学习一些常用模块以及一些算法的使用,就去买了一个平衡车的架子,电机驱动模块TB6612以及MPU6050等模块都是从实验室的同学还有实验室的收纳箱里面掏出来的........(使用的代码并不全是原创,大多都有借鉴,感谢无私奉献的前辈们)

问就是能白嫖的肯定不会买+_+......

首先做一个框架,制作一个平衡车需要了解哪方面的知识 

一.平衡车的平衡原理(这个是为了在编写有关平衡车的PID代码时能够比较清晰的认识到各个环的作用)

二.STM32上一些简单外设的使用:

比如基本定时器,实现简单的定时的功能(为了能够通过编码器定时计算小车的速度),使用定时器实现PWM的输出,定时器的编码器模式(输入捕获模式).

还有NVIC,也就是中断的使用,定时器的计数溢出中断,定时器编码器的计数溢出中断.

以及GPIO的使用,通过它来控制电机的正反转.

UART,也就是串口,通过上位机来对平衡车进行调参(这是很重要的一部分,之前有一些比赛就是因为不会用上位机进行调参,导致效率低下,很容易被调参折磨疯,强烈建议通过上位机进行调参)

三.总结部分(显的高大上一点)

--------------------------------------------------------第一部分-------------------------------------------------------------

                                                                平衡车的原理

先来一个最直观的感受:

如果你用一个手去立一个木棍,很明显,如果你什么都不做,木棍会往一个方向倾斜,直到倒下,但是实际上,如果木棍往一个方向去倒,那么如果你想要它平衡,那么你就要往它倾斜的方向去运动,这样它才会平衡。

(图并不是原创的,借鉴其他博主的,博主的这个例子很容易理解,如果侵权请联系删除)

在这个过程中,我们的眼睛作为传感器,看到了木棍向某一个方向倒,我们的身体作为一个电机,在大脑的支配下往木棍倒下的方向运动,因此,我们可以了解到,如果想要平衡车平衡,至少需要有三个部件:控制器,传感器,以及电机。

再来理性的认识:

对于平衡车,我们可以把他简化成一个单摆模型

 把静止的单摆拉起一个角度,如果没有任何的阻力,那么单摆将会一直来回的摆动,永不停息也就是回复力,但是实际上,会有各种各样的阻力来阻止它平衡,也就是阻尼力比如绳子结点的摩擦,空气阻力等等,最终单摆会重新回到平衡位置,不再进行摆动.

 (挺抽象)

平衡车的简化模型实际上就是把上面的单摆模型给倒过来,单摆的下端固定在一个带轮子的车上,上端固定一个合适质量的小球,当小球有一定的倾斜时,也就是小球会向某一个方向运动,根据动量守恒原理,轮子必然会有一个相反方向的速度来和这个速度相抵消.(下端铰链连接)

 也就是说,如果你想要让它平衡,那么就必须出现一个额外的速度,也就是一个外界的力来打破这个状态,这个速度的方向和V{_{1}}^{}相反,与V_{2}相同,这样才能保证平衡。

但是又有一个问题,也就是这个力应该怎么施加上去?如果一直施加一个跟直到小球回到最高点,会出现小小球有又往另一个方向倾斜的现象,也就是有一个逆时针的角速度,这个角速度导致小球在回到最高点时不能平衡,这个力应该和小车的状态有一定的联系,我们可以选用小车的倾角作为一个和这个力有关系的量.

通过上面的分析我们可以知道,如果要让小车直立起来,我们是需要让这个倾角等于0°,并且没有角加速度,不让小车在到达最高点时又冲过去,因此,施加的这个力应该和这两个量有直接的联系,通过这两个量来让小车直立。

但是仅通过直立的作用的远远不够的,我们可以这样想,如果想要给小车一个速度,比如速度为0,如果按简单的PID控制来想,如果小车速度增大,那么按理说应该要减小小车的速度才能让它为0,但是由于小车需要平衡,小车的速度改变是由于小车在重力的作用下往某一个方向进行了运动,导致小车发生了倾斜,这时候小车的速度应该要增大才能让倾角变小,但是由于速度环的作用,想要小车速度为0,小车速度应该要减小,因此,就出现了一个矛盾的状态。

但是在这个过程中,无论如何,小车都应该在直立的状态下,如果小车不是在直立的状态(机械平衡)下进行这种控制,那么小车将难以平衡,倾角将会一直变大(电机的作用太小,不足以和重力相提并论),直立和速度环的作用相互抑制(在两个环的参数调的不好的情况下),所以,在调节过程中,其主要作用的必须是直立环,速度环的控制必须要在已经平衡的条件下进行!!


到此已经1800字了,先写到这些,剩下的之后再补,至于最后一个环,转向环是对直立作用最弱的一个环,其实对它进行调节与否都没有太大的左右,我会在最后进行补充,在代码的调节过程中,先不进行转向环的调节。                                                                                                           2023年6月9日


                                                                     第二部分

                                                             STM32外设的使用

使用Cubemx利用HAL库进行开发,首先选择需要配置的资源,利用Cubemx的图形化配置界面进行配置。

 由上往下进行配置,先配置GPIO

 选择PA4,PA5,PA6,PA7为GPIO的输出端口,用于输出高低电平来驱动电机。

 RCC选择外部时钟

 Debug选择单总线

 定时器选择

 定时器1使用编码器模式,参数配置如下:

 选择计数周期为20000,不分频,编码器模式选择上升沿下降沿都计数。

定时器二同理。

 定时器三作为基本定时器,预分频系数设置为71,计数模式为向上计数,计数周期设置为999,由于之后的时钟树配置APB1线上到定时器3的时钟频率为72MHZ,因此计数的周期应该为,1/(72MHZ/72)*1000,为1ms,也就是说,每过1ms计数器溢出一次。

打开TIM3中断。

定时器四作为PWM的输出定时器,选择通道1和通道2作为PWM的输出通道,预分频系数设置为99,定时器的周期设置为7199。

至此配置完了有关定时器计数、编码器、PWM输出的配置,下面进行I2C的配置。

选择I2C模式,I2C设置为快速模式,时钟频率设置为400000HZ,其他的不变。 

再进行串口的配置,选择异步模式,波特率设置为115200bit/s,数据长度为8bit,也就是1一个字节的大小,不进行校验,停止位一位,打开串口中断。

 时钟树直接配置为72MHZ

 随便写一个名字,工具链选择MDK-ARM。

如上,之后点击生成代码。

先进行MPU6050的代码移植并进行测试。

 把MPU6050文件包中的.h头文件直接放进Inc中。

 .c文件放入Src中

 把.c文件包含进工程中,进行一次编译,没有错误。

 再main.h的头文件中包含

#include "stdio.h"
#include "mpu6050.h"
#include "inv_mpu.h"

回到main.c文件中

/**
   *@brief 重定向串口2,以便可以直接使用printf函数把数据打印到电脑上。
   *@param ch 输入字符
   *@param 无
   *
   *@retval 无
   */
int fputc(int ch, FILE *f)
{
uint8_t temp[1] = {ch};
HAL_UART_Transmit(&huart2, temp, 1, 2);//huart2需要根据你的配置修改
return ch;
}

重定向串口2,并定义变量

再在main函数中的while循环前添加:

while(mpu_dmp_init()){    ;   }

来对MPU6050进行初始化,之后就可以调用API函数来获取pitch,roll,yaw,以及对应的角加速度。

		if(mpu_dmp_get_data(&pitch,&roll,&yaw)){
        MPU_Get_Accelerometer(&gx,&gy,&gz);
		printf("%.2f,%.2f,%.2f\n",pitch,roll,yaw);
        }

在while循环中加入如上函数,就可以在串口打印出当前陀螺仪的状态。

MPU6050的VCC接3.3V,GND接GND,SCL接PB10,SDA接PB11。

PA2接USB转串口的RX脚,PA3接TX脚,3V3接C8T6的3V3脚,GND连接C8T6的GND。硬件部分连接完成.

注意,在使用串口重定向时必须打开微库,否则串口重定向没有作用!!

结果如下:

 

效果可以接受。

至此,MPU6050的配置已经完成,下面进行编码器,定时器,以及PWM输出的配置。 

然后创建一个Handware文件夹,用来存放有关编码器的文件

 在Handware文件夹下创建一个encoder的.c文件和.h文件,并把这个文件夹包含进工程当中。

 在encoder.h文件下添加如下的代码:

#ifndef _ENCODER_H_
#define _ENCODER_H_

#include "stm32f1xx.h"
#include "tim.h"
#include "usart.h"
#include "pid.h"
#include "stdio.h"
//电机1的编码器输入引脚
#define MOTO1_ENCODER1_PORT GPIOA
#define MOTO1_ENCODER1_PIN  GPIO_PIN_0
#define MOTO1_ENCODER2_PORT GPIOA
#define MOTO1_ENCODER2_PIN  GPIO_PIN_1

//定时器号
#define ENCODER_TIM1 htim1
#define ENCODER_TIM2 htim2
#define PWM_TIM     htim4
#define GAP_TIM     htim3

#define MOTOR_SPEED_RERATIO 48u    //电机减速比
#define PULSE_PRE_ROUND 11 //一圈多少个脉冲
#define RADIUS_OF_TYRE 34 //轮胎半径,单位毫米
#define LINE_SPEED_C RADIUS_OF_TYRE * 2 * 3.14
#define RELOADVALUE1 __HAL_TIM_GetAutoreload(&ENCODER_TIM1)    //获取自动装载值,本例中为20000
#define COUNTERNUM1 ((short)__HAL_TIM_GetCounter(&ENCODER_TIM1))        //获取编码器定时器中的计数值
#define RELOADVALUE2 __HAL_TIM_GetAutoreload(&ENCODER_TIM2)    //获取自动装载值,本例中为20000
#define COUNTERNUM2 ((short)__HAL_TIM_GetCounter(&ENCODER_TIM2))        //获取编码器定时器中的计数值
#define Z_Static_Deviation (-16)                                //Z轴静态误差
typedef struct _Motor
{
    int32_t totalCount;  //总计数值
    float speed;         //电机转速
    uint8_t direct;      //旋转方向
}Motor;

#endif
void Motor_Init(void);

电机的减速比可以在购买电机的特性参数中找到,一圈多少个脉冲也可以找到,轮胎的半径可以通过多次简单的测量取平均的方法获得,COUNTERNUM1 ((short)__HAL_TIM_GetCounter(&ENCODER_TIM1)),这一部分代码用一个short强制类型转换将CNT寄存器中的计数值从0~65535变到从-32768~32768,正转时从0计时32768,反转时从0计数到-32768。

然后再encoder.c文件中添加如下代码:

#include "encorder.h"

Motor motor1,motor2;
uint8_t counter = 0;
int out_PWM_R;               //经过速度环、位置环、转向环输出的PWM
int out_PWM_L;               //经过速度环、位置环、转向环输出的PWM

void Motor_Init(void)
{
    HAL_TIM_Encoder_Start(&ENCODER_TIM2, TIM_CHANNEL_ALL);      //开启编码器定时器2
    __HAL_TIM_ENABLE_IT(&ENCODER_TIM2,TIM_IT_UPDATE);           //开启编码器定时器更新中断,防溢出处理
    HAL_TIM_Encoder_Start(&ENCODER_TIM1, TIM_CHANNEL_ALL);      //开启编码器定时器1
    __HAL_TIM_ENABLE_IT(&ENCODER_TIM1,TIM_IT_UPDATE);           //开启编码器定时器更新中断,防溢出处理
    HAL_TIM_Base_Start_IT(&GAP_TIM);                       //开启定时器中断                                          
    motor1.speed = 0;
    motor1.direct = 0;
    motor2.totalCount = 0;                                
    motor2.speed = 0;
    motor2.direct = 0;
}

这个函数的作用主要是开启两个编码器定时器,并开启基本定时中断,以及设计编码器的定时器初始值.

再添加定时器的定时中断回调函数

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时器回调函数,用于计算速度
{	
    if(htim->Instance==GAP_TIM.Instance)//间隔定时器中断,是时候计算速度了
    {
			counter++;
			if(counter ==50)
				{
				counter = 0;
        motor1.direct = __HAL_TIM_IS_TIM_COUNTING_DOWN(&ENCODER_TIM1);//如果向上计数(正转),返回值为0,否则返回值为1
        motor1.totalCount = COUNTERNUM1;//一个周期内的总计数值等于目前计数值加上溢出的计数值
        __HAL_TIM_SET_COUNTER(&ENCODER_TIM1, 0);
		motor1.speed = (float)(motor1.totalCount) / (4 * MOTOR_SPEED_RERATIO * PULSE_PRE_ROUND) * 3000;//算得每秒多少转,除以4是因为4倍频
        motor2.direct = __HAL_TIM_IS_TIM_COUNTING_DOWN(&ENCODER_TIM2);//如果向上计数(正转),返回值为0,否则返回值为1
        motor2.totalCount = COUNTERNUM2;//一个周期内的总计数值等于目前计数值加上溢出的计数值
		__HAL_TIM_SET_COUNTER(&ENCODER_TIM2, 0); 
        motor2.speed = (float)(motor2.totalCount) / (4 * MOTOR_SPEED_RERATIO * PULSE_PRE_ROUND) * 3000;//算得每秒多少转,除以4是因为4倍频
//			if(Flag){printf("%.2f,%.2f,%.2f\n",pitch,roll,yaw);}
//				printf("samples:%d,%d\n",out_PWM_R,out_PWM_L);
				printf("samples:%.2f,%.2f\n",motor1.speed,motor2.speed);
			}
}
}

下面来解释各行代码的作用,首先是电机方向的获得:

__HAL_TIM_IS_TIM_COUNTING_DOWN(&ENCODER_TIM1);

 在如果电机向前转动(也就是正转),那么在Tl1处于上升沿的过程中,Tl2处于低电平,当Tl1处于下降沿时,Tl2处于高电平,如果电机向后转动(也就是反转),那么在Tl1处于上升沿的过程中,Tl2就处于高电平,这和向前转动是相反的,因此可以通过这个特性来区分电机的正反转。

之后是编码器计数值的获得,再获得编码器的计数值后,就把编码器的CNT寄存器置0,从而进行下一次速度的计算。

通过串口打印数据到电脑,结果如下:

之后就是有关控制的算法引入了,今天就到这里                                                         2023年6月10日


算法部分:

经过上面的分析,如果想要比较好的平衡状态,一般需要引入两个环的作用,一个是直立环,一个是速度环(转向环可以根据需求添加,并不是必要的,在直立环和速度环的作用下平衡车就能够平衡),并且速度环的应该是具有正反馈的作用,这个环必须加快直立环的作用,在直立环使小车达到机械平衡之后,即便小车出现沿某个方向的速度(即出现倾倒),小车也能够很快在直立环+速度环的共同作用下回到平衡状态。

代码部分:

在Handware文件夹下新建pid.c和pid.h文件,在pid.h文件下添加如下代码:

#ifndef _PID_H__
#define _PID_H__

#include "stm32f1xx.h"
#include "tim.h"
#include "usart.h"
#include "string.h"
#include "encorder.h"
#include "main.h"

typedef struct
{
   	float kp,ki,kd;//三个系数
		uint16_t Target_Speed;
    float error,lastError;//误差、上次误差
    float integral,maxIntegral;//积分、积分限幅
    float output,maxOutput;//输出、输出限幅
}PID;

extern PID pid;
extern float PID_Upright_Kp; //直立环Kp
extern float PID_Upright_Kd ; //直立环Kd
extern float PID_Steering_Kp; //转向环Kp
extern float PID_Steering_Kd; //转向环Kd

#endif
void PID_Init(void);
int Velocity_PI(int Speed_measure);
int Ldire_Control(int PWM);
int PID_Steering(int16_t expect, float angle, float gyro);
int PID_Upright(float expect, float angle, float gyro);
int Rdire_Control(int PWM);

在pid.c文件下添加如下代码:

#include "pid.h"

float PID_Steering_Kp = 0; //转向环Kp
float PID_Steering_Kd = 0; //转向环Kd
float PID_Upright_Kp = 0; //直立环Kp
float PID_Upright_Kd = 0; //直立环Kd
PID pid;
void PID_Init()
{
	pid.kp = 0;
	pid.ki = 0;
	pid.maxOutput = 0;
	pid.maxIntegral = 7200;
}

/**
 * @brief 直立环(位置式PD)
 * @param expect:期望角度
 * @param angle:真实角度
 * @param gyro:真实角速度
 * @return PWM
 */
int PID_Upright(float expect, float angle, float gyro)
{
    int PWM_out;
    PWM_out = PID_Upright_Kp * (angle - expect) + PID_Upright_Kd * gyro;
    return PWM_out;
}


/**
 * @brief 转向环(位置式PD)
 * @param expect:期望转向的角度
 * @param angle:真实的角度(一般是yaw)
 * @param gyro:真实角加速度(一般是gyro_z)
 * @return PWM
 */
int PID_Steering(int16_t expect, float angle, float gyro)
{
    int PWM_out;
    float a = 0.7;
    static float gyro_Last;
    gyro = (1 - a) * gyro + a * gyro_Last; //低通滤波
    PWM_out = PID_Steering_Kp * (angle - expect) + PID_Steering_Kd * gyro;
    return PWM_out;
}

/**
 * @brief 车轮转向控制
 * @param PWM 经过并联控制后的占空比输出
 * @return 无 PA4 & PA5 L
 */
int Ldire_Control(int PWM)
{
	if(PWM>7200){PWM = 7200;}
	if(PWM<-7200){PWM = -7200;}
	if(PWM>0)
	{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);//控制轮子正转
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
	PWM = PWM;}
	else
    {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4 ,GPIO_PIN_SET);//控制轮子反转
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5 ,GPIO_PIN_RESET);
	PWM = -PWM;}
	
	return PWM;
	
}

int Rdire_Control(int PWM)
{
	if(PWM>7200){PWM = 7200;}
	if(PWM<-7200){PWM = -7200;}
	if(PWM>0)
	{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET);//控制轮子正转
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);
	PWM = PWM;}
	else
    {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6 ,GPIO_PIN_SET);//控制轮子反转
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7 ,GPIO_PIN_RESET);
	PWM = -PWM;}
	
	return PWM;
	
}
/**
 * @brief 速度环(增量式式PD)
 * @param reference:期望速度
 * @return PWM
 */

int Velocity_PI(int Speed_measure)
{
	static int Encoder_err, Encoder_err_low, Encoder_err_low_last, Encoder_sum, Movement;
	static int PWM_out;
	
	Encoder_err = 0 - Speed_measure;
	Encoder_err_low = 0.7 * Encoder_err + 0.3 * Encoder_err_low_last;
	Encoder_err_low_last = Encoder_err_low;
	Encoder_sum += Encoder_err_low;
	Encoder_sum = Encoder_sum + Movement;
	if(Encoder_sum > 7200) Encoder_sum = 7200;
	if(Encoder_sum < -7200) Encoder_sum = -7200;
	
	PWM_out = pid.kp*Encoder_err + pid.ki*Encoder_sum;
		
	return PWM_out;
}

在代码中添加了低通滤波,滤除一些低频的噪声,使输出平滑一些。

再在定时器的中断溢出中添加如下代码:

		out_PWM_R = PID_Upright(0.8, pitch, gy+Y_Static_Deviation) + Velocity_PI((motor2.speed+motor1.speed)) + PID_Steering(0, yaw, gz+Z_Static_Deviation);//
		out_PWM_L = PID_Upright(0.8, pitch, gy+Y_Static_Deviation) + Velocity_PI((motor2.speed+motor1.speed)) - PID_Steering(0, yaw, gz+Z_Static_Deviation);//
		out_PWM_R = Rdire_Control(out_PWM_R);
		out_PWM_L = Ldire_Control(out_PWM_L);
		__HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_1,out_PWM_R);
		__HAL_TIM_SET_COMPARE(&htim4,TIM_CHANNEL_2,out_PWM_L);

0.8为机械中值,可以通过将MPU6050获取的pitch值输出到串口,用手扶小车,在小车刚好往另一个方向倒时记下此时的pitch值,多次测量取平均值即可得到。

最后有关参数的调整,这一部分需要使用蓝牙进行调参,今天就写到这里,之后再进行添加。


蓝牙调参部分:(模块选择)

首先是一块JDY-31的蓝牙模块:

 然后是一块USB转串口模块。手头上有啥用啥,用野火或者正点原子开发板上的串口模块也可以。

然后对蓝牙模块进行配置,配置成与之前在Cubemx中配置的一样。

这是有关于测试的指令集。

 

有关配置的指令集,可以通过指令 AT+BAUD8将波特率设置为115200。

通过AT+NAME????    更改蓝牙的名字。

比如 AT+NAME1234就可以将名字改为1234。

 具体的可以参见JDY-31蓝牙串口透传的模块使用手册,我会在最后的总结部分整合为一个压缩包发送。

然后就是代码部分:

老样子,在Handware文件夹下创建:

 

 hc05.c文件和hc05.h文件,并将这两个文件添加进工程中:

并在.c文件下包含.h文件。

在.h文件下添加如下代码:

#ifndef _HC05_H__
#define _HC05_H__

#include "stm32f1xx.h"
#include "main.h"
#include "string.h"
#include "usart.h"
typedef struct
{
   	float kp,ki,kd;//三个系数
		uint16_t Target_Speed;
    float error,lastError;//误差、上次误差
    float integral,maxIntegral;//积分、积分限幅
    float output,maxOutput;//输出、输出限幅
}PID;


#endif

 在.c文件下添加如下代码:
 

#include "hc05.h"

uint16_t RxLine = 0;//指令长度
uint8_t DataBuff[200];//指令内容
float PID_Steering_Kp = 0; //转向环Kp
float PID_Steering_Kd = 0; //转向环Kd
float PID_Upright_Kp = 150; //直立环Kp
float PID_Upright_Kd = 0.78; //直立环Kd
PID pid;
void PID_Init()
{
	pid.kp = -9.5;
	pid.ki = -0.0475;
	pid.maxOutput = 0;
	pid.maxIntegral = 7200;
}
/*
 * 解析出DataBuff中的数据
 * 返回解析得到的数据
 */
float Get_Data(void)
{
    uint8_t data_Start_Num = 0; // 记录数据位开始的地方
    uint8_t data_End_Num = 0; // 记录数据位结束的地方
    uint8_t data_Num = 0; // 记录数据位数
    uint8_t minus_Flag = 0; // 判断是不是负数
    float data_return = 0; // 解析得到的数据
    for(uint8_t i=0;i<200;i++) // 查找等号和感叹号的位置
    {
        if(DataBuff[i] == '=') data_Start_Num = i + 1; // +1是直接定位到数据起始位
        if(DataBuff[i] == '!')
        {
            data_End_Num = i - 1;
            break;
        }
    }
    if(DataBuff[data_Start_Num] == '-') // 如果是负数
    {
        data_Start_Num += 1; // 后移一位到数据位
        minus_Flag = 1; // 负数flag
    }
    data_Num = data_End_Num - data_Start_Num + 1;
    if(data_Num == 4) // 数据共4位
    {
        data_return = (DataBuff[data_Start_Num]-48)  + (DataBuff[data_Start_Num+2]-48)*0.1f +
                (DataBuff[data_Start_Num+3]-48)*0.01f;
    }
    else if(data_Num == 5) // 数据共5位
    {
        data_return = (DataBuff[data_Start_Num]-48)*10 + (DataBuff[data_Start_Num+1]-48) + (DataBuff[data_Start_Num+3]-48)*0.1f +
                (DataBuff[data_Start_Num+4]-48)*0.01f;
    }
    else if(data_Num == 6) // 数据共6位
    {
        data_return = (DataBuff[data_Start_Num]-48)*100 + (DataBuff[data_Start_Num+1]-48)*10 + (DataBuff[data_Start_Num+2]-48) +
                (DataBuff[data_Start_Num+4]-48)*0.1f + (DataBuff[data_Start_Num+5]-48)*0.01f;
    }
    else if(data_Num == 7) // 数据共7位
    {
        data_return = (DataBuff[data_Start_Num]-48)*1000 + (DataBuff[data_Start_Num]-48)*100 + (DataBuff[data_Start_Num+1]-48)*10 + (DataBuff[data_Start_Num+2]-48) +
                (DataBuff[data_Start_Num+4]-48)*0.1f + (DataBuff[data_Start_Num+5]-48)*0.01f;
    }
    else if(data_Num == 8) // 数据共8位
    {
        data_return = (DataBuff[data_Start_Num]-48)*10000 + (DataBuff[data_Start_Num]-48)*1000 + (DataBuff[data_Start_Num]-48)*100 + (DataBuff[data_Start_Num+1]-48)*10 + (DataBuff[data_Start_Num+2]-48) +
                (DataBuff[data_Start_Num+4]-48)*0.1f + (DataBuff[data_Start_Num+5]-48)*0.01f;
    }
    if(minus_Flag == 1)  data_return = -data_return;
//    printf("data=%.2f\r\n",data_return);
    return data_return;
}

/*
 * 根据串口信息进行PID调参
 */
void USART_PID_Adjust(uint8_t Motor_n)
{
    float data_Get = Get_Data(); // 存放接收到的数据
    printf("data=%.2f\r\n",data_Get);
				if(DataBuff[0]=='P' && DataBuff[1]=='2'){ // 速度环P
            pid.kp = data_Get;
//						printf("%.2f",pid.kp);
					}
        else if(DataBuff[0]=='I' && DataBuff[1]=='2'){ // 速度环I
            pid.ki = data_Get;
//					printf("%.2f",pid.ki);
					}
        else if(DataBuff[0]=='D' && DataBuff[1]=='2'){ // 速度环D
            pid.kd = data_Get;
//				printf("%.2f",pid.kd);
				}
        else if((DataBuff[0]=='S' && DataBuff[1]=='p') && DataBuff[2]=='e'){ //目标速度
            pid.Target_Speed = data_Get;
//				printf("%d",pid.Target_Speed);
				}
				else if((DataBuff[0]=='M' && DataBuff[1]=='t') && DataBuff[2]=='e'){ //积分限幅
						pid.maxIntegral = data_Get;
//				printf("%.2f",pid.maxIntegral);
				}
				else if((DataBuff[0]=='m' && DataBuff[1]=='O') && DataBuff[2]=='t'){ //输出限幅
						pid.maxOutput = data_Get;
//				printf("%.2f",pid.maxOutput);
				}else if(DataBuff[0]=='u' && DataBuff[1]=='p')     //直立环p
				{
					PID_Upright_Kp = data_Get;
				}else if(DataBuff[0]=='u' && DataBuff[1]=='d')  //直立环d
				{
					PID_Upright_Kd = data_Get;
				}else if(DataBuff[0]=='s' && DataBuff[1]=='p')    //转向环p
				{
					PID_Steering_Kp = data_Get;
				}else if(DataBuff[0]=='s' && DataBuff[1]=='d')   //转向环d
				{
					PID_Steering_Kd = data_Get;
				}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{
    if(UartHandle->Instance==USART2)//如果是串口2
    {
        RxLine++;                      //每接收到一个数据,进入回调数据长度加1
        DataBuff[RxLine-1]=RxBuffer[0];  //把每次接收到的数据保存到缓存数组
        if(RxBuffer[0]==0x21)           //接收结束标志位,这个数据可以自定义,根据实际需求,这里只做示例使用,不一定是0x21
        {
//            printf("RXLen=%d\r\n",RxLine);
//            for(int i=0;i<RxLine;i++)
//            printf("UART DataBuff[%d] = %c\r\n",i,DataBuff[i]);
            USART_PID_Adjust(1);//数据解析和参数赋值函数
            memset(DataBuff,0,sizeof(DataBuff));  //清空缓存数组
            RxLine=0;  //清空接收长度
        }
        RxBuffer[0]=0;
        HAL_UART_Receive_IT(&huart2, (uint8_t *)RxBuffer, 1); //每接收一个数据,就打开一次串口中断接收,否则只会接收一个数据就停止接收
    }
}

注意串口中断RxBuffer这个数组需要在main.h中进行全局变量的声明,并在main.c的主函数的while循环前添加:

HAL_UART_Receive_IT(&huart2, (uint8_t *)RxBuffer, 1);

用以提前开启串口中断接受。

注释写的很详细,不清楚的部分可以参看注释理解。

再进行上位机的配置:

再vofa+中配置命令,也就是发送的内容,其中%.2f为发送的数据,P2作为用于对数据的识别,=用于标明数据的起始位置,感叹号!用于标识数据的结束位置。

然后在控件中拖入一个控件 :

 

 然后打开电脑端的蓝牙:

 与蓝牙模块进行连接

 配对完成后回到vofa+中:

 会出现如图所示的标准串行,此时蓝牙上的指示灯不再闪烁,表明配对成功。

点击拖入的模块:,选择绑定命令:

 然后就可以进行数据发送了,可以通过上面的方式添加相应的命令,并在代码中进行识别获取数据。

滑动滑块,出现如图所示的现象(蓝色为发送的数据,绿色是单片机接受后返回的数据)

 

 由此就可以通过上位机对平衡车进行调参了,妈妈再也不用担心调参效率低下啦=-=。

最后就到了调整pid参数的环节,也是平衡车的最后一公里。

先调整直立环的参数--->

首先是关于Kp的极性确定,首先取Kp为正值大小为100,拿起小车向一个方向倾倒,如果小车向着相同的方向加速,则说明极性正确,反之则极性错误,然后不断增大Kp,直到小车出现低频抖动的情况,这时候说明小车的Kp已经足够大了,然后调整Kd,从0.5开始步进(根据实际情况调整),增加到小车出现一个高频抖动的情况后说明Kd的已经能够迅速对角加速度的变化做出响应,再将Kp和Kd的值同时✖0.6作为最终的值(0.6是一个经验取值,乘以一个小于1的数的原因是速度环能够对辅助直立环的作用,也就是增强直立环,剩下的0.4的作用让速度环来弥补可以让小车更加平滑)。

然后是速度环参数Kp的极性确定,先将直立环的Kp,Kd置0,然后Kp取5,Ki取Kp/200(经验值),即0.025,转动小车的轮子,如果小车的另一个轮子同向加速到最大,则说明极性正确,反之则错误。然后不断的增大Kp,直至小车能够出现在某一个范围内左右摆动,并且幅度不是太大,就基本调整完成,小车能够维持较长时间的直立了。

下面是文档中使用的有关代码:

链接:https://pan.baidu.com/s/1_umXEdMO0pf93ykBzcQVAQ?pwd=7qpe 
提取码:7qpe


          总结部分

一:平衡车的平衡原理

二:有关STM32外设以及外部测量模块的使用

串口、定时器、GPIO、中断、I2C...

以及MPU6050、编码器模块和蓝牙模块的使用....

写这篇文章花了我四天的时间,所有文字均通过手打,作为我进入大学后写的第一篇能够切实帮到其他人的博客,之后我会不断在这个账号上更新内容,一是作为备忘录,二是能够帮助和我一样的同学,三是留下一点大学生活的记录.......

关于平衡车的效果视频可以在B站:

【简陋平衡车】 https://www.bilibili.com/video/BV17L411q79z/?share_source=copy_web&vd_source=ad3a1dc87d4fd4b220ba63adb9f50dc1

这个视频中找到,这个视频中的参数调整还没有达到最佳状态,之后在增大速度环参数后能够很快在一个很小的摆动幅度能平衡,平衡车的效果很大程度也取决于参数调整的好坏,愿意再花时间调整的话是能够调整的很好的效果的,不愿意的话按上面的过程进行调整后效果应该和视频里面差不多。

                                                                                                     ----长春理工大学302实验室某某某

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值