电磁循迹小车赛后总结

电磁循迹小车赛后总结

MCU选用:STM32F103C8T6

编程语言:C语言

开发工具:MDK Keil,CubeMX

所用开发库:HAL库

适用读者: 初步接触STM32,没有制作智能车的实战经验等嵌入式入门读者.

一.小车架构

1.硬件部分

在这里插入图片描述

在硬件部分我只列举了做一个简易电磁车最基本的部件,蓝色字体是我们所选用的部件。

在上面所列出的小车底板、控制板、和电磁杆中,控制板和电磁杆是由我的队员用EDA软件画板,开板再焊接制成的,其余一些模块都是通过购买所得。

2.软件部分

在这里插入图片描述

3.控制逻辑

在这里插入图片描述

二.关键模块说明

1.电磁杆(电磁传感器)

下面有关电磁传感器的内容摘自王盼宝《智能车制作-从元器件、机电系统、控制算法到完整的智能车设计》清华大学出版社出版,如有侵权请联系我删除。

Ⅰ.电感传感器原理

根据电磁学相关知识,我们知道在导线中通人变化的电流(如按止弦规律变化的电流).则导线周围会产生变化的磁场,且磁场与电流的变化规律具有一致性,如果在此磁场中置一个电感,则该电感上会产生感应电动势,且该感应电动势的大小和通过线圈回路的磁通量的变化率成正比。由于在导线周围不同位置,磁感应强度的大小和方向不同,所以不同位置上的电感产生的感应电动势也应该不同。据此,则可以确定电感的大致位置。

Ⅱ.磁传感器信号处理电路

确定使用电感作为检测导线的传感器,但是其感应信号较微弱,且混有杂波,所以要进行信号处理。要进行以下三个步骤才能得到较为理想的信号:信号的滤波,信号的放大,信号的检波。

1)信号的滤波

比赛选择20kHz的交变电磁场作为路径导航信号,在频谱上可以有效地避开周围其他磁场的干扰,因此信号放大需要进行选频放大,使得20kHz的信号能够有效地放大,并且去除其他干扰信号的影响。使用LC串联谐振电路实现选频电路(带通电路),具体电路如下图所示。

LC谐振电路

LC谐振电路

其中, E E E是感应线圈中的感应电动势, L L L是感应线圈的电感值, R 0 R_{0} R0主要是电感的内阻, C C C是谐振电容。电路谐振频率为:
f = 1 2 π L C f =\frac{1}{2π\sqrt{LC}} f=2πLC 1
已知感应电动势的频率 f = 20 k H z f=20kHz f=20kHz,感应线圈电感为 L = 10 m H L=10mH L=10mH,可以计算出谐振电容的容量为 C = 6.33 × 1 0 − 9 F C=6.33×10^{-9}F C=6.33×109F。通常在市场上可以购买到的标称电容与上述容值最为接近的电容为 6.8 n F 6.8nF 6.8nF,所以在实际电路中选用 6.8 n F 6.8nF 6.8nF的电容作为谐振电容。

2)信号的放大

第一步处理后的电压波形已经是较为规整的20kHz正弦波,但是幅值较小,随着距离衰减很快,不利于电压采样,所以要进行放大,官方给出了如下图所示的参考方案,即用三极管进行放大,但是用三极管放大有一个不可避免的缺点就是温漂较大,而且在实际应用中静电现象严重。

在这里插入图片描述

共射三极管放大电路

因此我们放弃三级管放大的方案,而是采用集成运放进行信号的放大处理,集成运放较三极管优势是准确、受温度影响很小、可靠性高。集成运放放大电路可构成同相比例运算电路和反相比例运算电路,在实际中使用反相比例运算电路。由于运放使用单电源供电,因此在同相端加 V c c / 2 V_{cc}/2 Vcc/2(典型值)的基准电位,基准电位由两个阻值相等的电阻分压得到。

3)信号的检波

测量放大后的感应电动势的幅值 E E E 可以有多种方法。最简单的方法是使用二极管检 波电路将交变的电压信号检波形成直流信号,然后再通过单片机的AD采集获得正比于感 应电压幅值的数值。

我们采用的是竞赛组委会给出的第一种方案,即使用两个二极管进行倍压检波。倍压检波电路可以获得正比于交流电压信号峰-峰值的直流信号。为了能够获得更大的动态范围,倍压检波电路中的二极管推荐使用肖特基二极管或者锗二极管。由于这类二极管的开启电压一般在0.1~0.3V,小于普通的硅二极管(0.5~0.7V),可以增加输出信号的动态范 围和增加整体电路的灵敏度。这里选用常见的肖特基二极管1N5817。

最终确定下来的电路方案如下图所示:

在这里插入图片描述

最终方案电路

Ⅲ.磁传感器的布局原理及改进

对于直导线,当装有小车的中轴线对称的两个线圈的小车沿其直线行驶,即两个线圈的位置关于导线对称时,则两个线圈中感应出来的电动势大小应相同且方向亦相同。若小车偏离直导线,即两个线圈关于导线不对称时,则通过两个线圈的磁通量是不一样的。这时,距离导线较近的线圈中感应出的电动势应大于距离导线较远的那个线圈中的。根据这两个不对称的信号的差值,即可调整小车的方向,引导其沿直线行驶。

对于弧形导线,即路径的转弯处,由于弧线两侧的磁力线密度不同,则当载有线圈的小车行驶至此处时,两边的线圈感应出的电动势是不同的。具体的情况是,弧线内侧线圈的感应电动势大于弧线外侧线圈的,据此信号可以引导小车拐弯。

另外,当小车驶离导线偏远致使两个线圈处于导线的一侧时,两个线圈中感应电动势也是不平衡的。距离导线较近的线圈中感应出的电动势大于距离导线较远的线圈。由此,可以引导小车重新回到导线上。

由于磁感线的闭合性和方向性,通过两线圈的磁通量的变化方向具有一致性,即产生的感应电动势方向相同,所以由以上分析,比较两个线圈中产生的感应电动势大小即可判断小车相对于导线的位置,进而做出调整,引导小车大致循线行驶。

采用双水平线圈检测方案,在边缘情况下,其单调性发生变化,这样就存在一个定位不清的区域(如下图箭头所指)。同一个差值,会对应多个位置,不利于定位。另外,受单个线圈感应电动势的最大距离限制,两个线圈的检测广度很有限。

在这里插入图片描述

双线圈差值法有定位不清区域

现提出一种优化方案,5个垂直放置的电感按一字排布,每个电感相距约为5cm(见下图),这样覆盖赛道范围约为25cm。三个一字排布的电感可以大大提高检测密度和广度,向前有两个电感,可以提高前瞻,改善小车入弯状态和路径,两个45°的电感,可以改善入弯和出弯的姿态。

在这里插入图片描述

电感排布检测方案

Ⅳ.谐振电路的改进

按照组委会推荐的10mH+6.8nF组成的谐振电路,其谐振的峰值频率为19.6kHz,并不是信号发生器的20kHz。当谐振频率与外界激励频率不匹配时,将不会输出最大的谐振电压。另外,一般电感和电容均有±20%的误差,谐振频率将会随机分布在16kHz~24kHz之间,这会对传感器的对称性造成极大的影响。因此需要对组成谐振电路的电感电容进行匹配,使得其谐振频率恰好为20kHz,同时挑选电感电容对,使得对称位置的输出电压一致。

电磁杆的具体制作可以参考我队友的博客:

2.L298N电机驱动模块

L298N模块的介绍可以参考以下博主的博客

3.HC-05蓝牙模块

在此次比赛中我们主要使用蓝牙模块来返回小车调试过程中的赛道信息,以及通过蓝牙模块给小车发送相关指令以控制小车电机的启动与停止和更改小车控制算法的一些参数。

相关资料可以参考以下博主的博客:

4.直流减速电机

可以参考以下博主的博客:

5.模拟舵机

可以参考以下博主的博客:

6.干簧管

相关原理可以参考如下链接:

在此次比赛中我们需要用的干簧管来检测车库外所防止的磁铁以实现入库操作。

干簧管的使用较为简单,可以把他看作一个开关使用,从MUC上分配处一个GPIO口给干黄管的一端,另一端接地,通过捕捉该GPIO口的下降沿触发外部中断即可。

7.OLED屏幕

OLED的屏幕主要用于显示ADC采集到的赛道值用以确定特征位置如环岛、Y叉等处的特征标志值方便对算法的相关判断阈值进行更改。同时我们也通过OLED屏幕显示当前小车的PID参数以方便记录。

OLED的相关使用方法可参考以下博主的博客:

8.Wifi模块-ESP8266

该模块可以完成同蓝牙模块的相同操作,同时搭配Vofa+软件的话可以实现可视化调参。

相关使用方法可参考以下博主的博客:

三.CubeMX配置

CubeMX如何配置的总体介绍可以参考以下博主的博客:

1.引脚预览

在这里插入图片描述

2.ADC

ADC我们配置了6路通道,ADC1五路,ADC2一路。最初方案我们配置了七个通道,ADC1五路,ADC2两路,我们最初也只是使用了五个电感,所以只用了ADC1的五路通道,ADC2多出来的两路当时是备用。但是在调车过程中我们发现原先中间的电感旋转角度无法兼顾环岛与Y叉标志值的检测(可能是我们的设计存在问题),因此我们在中间多加了一路电感和原先的中路电感垂直,用于标志值的检测,并关闭了ADC2的一路通道方便代码编写。

ADC1:

在这里插入图片描述

在这里插入图片描述

ADC2:

在这里插入图片描述

ADC的相关配置可参考以下博主的博客:

3.TIM

TIM1用于输出控制电机两轮转速的PWM波

在这里插入图片描述

在这里插入图片描述

TIM2用于输出控制舵机的PWM波

在这里插入图片描述

TIM3中断用于控制算法的执行

在这里插入图片描述

TIM4中断用于一些需要计时函数的执行

在这里插入图片描述

TIM的相关配置可以参考以下博主的博客:

4.I2C

在这里插入图片描述

5.USART

我们配置了两个USART,比赛中只用到一个USART1

在这里插入图片描述

在这里插入图片描述

6.NVIC

在NVIC的配置中要根据自己的控制逻辑来配置他们的中断优先级

在这里插入图片描述

四.小车代码编写

1.基础框架的搭建

在写代码之前可以像我上面一样画出整个代码的架构和控制逻辑的思维导图(所用软件:uTools插件-知犀思维导图|uTools官网下载:uTools官网 - 新一代效率工具平台).然后根据代码的整体架构编写相关独立函数先初步搭建出基础框架,如相关变量的结构体,外围功能的函数等:

/*---------------------- 相关结构体定义 -------------------------- */
/**
	*@name		MotorDriver
	*@type		struct array
	*@about 	电机控制
	*@param
	*		- MotorDriver[0]		电机1
	*		- MotorDriver[1]		电机2
	*		--
	*		- Onoff			电机启动与停止标志位
	*		- pwmrate		传给Tim -> CRR的值,可改变PWM占空比
	*		- IN1			L298n IN1逻辑控制引脚
	*		- IN2			L298n IN2逻辑控制引脚
	*/
typedef struct{
	
	_Bool	OnOff;
	_Bool	IN1;
	_Bool 	IN2;
	float	pwmrate;
	
}MotorDriver;

/**
	*@name		MotDiff
	*@type		struct
	*@about 	电机差速控制
	*@param		
	*		--
	*		-	Param			增幅系数
	*		-	pwmSwitch		电机差速跟随转向控制开关
	*/
struct{
	
	_Bool		pwmSwitch;
	uint16_t	basepwmvalue;
	float 		Param;
	
}MotDiff;

/**
	*@name		SteMotDriver
	*@type		struct
	*@about 	舵机控制
	*@param
	*		- pwmrateTemp		pwm控制过渡值
	*		- pwmrateFnal		pwm控制最终值
	*		- Min				舵机中值
	*		- angle				舵机目前角度
	*/
typedef struct{
	
	uint8_t		angle;
	float		pwmrateTemp;	
	float   	pwmrateFnal;
	float		Min;
    
}SteMotDriver;


/**
	*@name		ADCData
	*@type		struct
	*@about 	ADC数据
	*@param
	*		- origanlData[]		ADC采集到的原始数据
	*		- filterData[]		滤波处理后的数据
	*		- IDUC_L			左电感
	*		- IDUC_R			右电感
	*		- IDUC_M			中电感
	*		- IDUC_LM			左中电感
	*		- IDUC_RM			右中电感
	*		- Error				差值
	*/
typedef struct{
	
	__IO	uint16_t		orignalData[5];
	__IO	float			filterData[5];
	__IO	float			IDUC_L;
	__IO	float			IDUC_R;
	__IO	float			IDUC_M;
	__IO	float			IDUC_LM;
	__IO	float			IDUC_RM;
	__IO	float			IDUC_Ex;
	__IO	float			Error;
	
}ADCData;


/**
	*@name	PoorCmpAnd
	*@type	struct
	*@about 	差比和加权算法系数
	*@param
	*		--
	*		- paramA			控制系数A
	*		- paramB			控制系数B
	*		- paramC			控制系数C
	*		- paramP			比例系数P
 	*/
typedef struct{
	
	uint8_t	flag;			//控制算法转换
	float paramA;
	float paramB;
	float paramC;
	float paramP;
	float paramL;
	
}PoorCmpAnd;


/**
	*@name		Switch
	*@type		struct
	*@about 	函数开关
	*@param
	*		- 		
	*		- 		
	*/
struct{
	
	_Bool ONOF1;		//环岛
	_Bool ONOF2;		//十字
	_Bool ONOF3;		//Y形
	_Bool ONOF4;		//标志值捕获
	uint8_t ONOF5;		//滤波
	
}Switch = {1, 0, 1, 0, 1};

/**
	*@name	Kalmam
	*@type	struct
	*@about Kalman Filter 
	*@param
	*	- symStateNow			系统实时状态			   	  X(k)
	* 	- symStatePostFore		系统上次预测状态			 X(k|k-1)
	* 	- symStatePostBest		系统上次最优状态			 X(k-1|k-1)
	* 	- covNow				本次系统状态协方差			P(k|k)
	* 	- covPostFore			上次预测状态协方差			P(k|k-1)
	*	- covPostBest			上次最优状态协方差			P(k-1|k-1)
	*	- symControl			系统控制量				   U(k)
	*	- symParmA				系统参数A					A
	*	- symParmB				系统参数B					B
	* 	- errorMes				k时刻测量值				   Z(k)
	* 	- mesParm				测量系统的参数				  H
	* 	- pcesNoise				过程噪声					W(k)
	* 	- mesNoise				测量噪声					V(k)
	*	- transposeA			A的转置矩阵					A'
	*	- transposeQ			W(k)的转置矩阵				Q	
	*	- transposeR			V(k)的转置矩阵				R
	*	- transposeH			H的转置矩阵					H'
	*	- gain					卡尔曼增益					Kg
	*/
typedef struct{
	__IO	float symStateNow[5];
	__IO	float symStatePostFore[5];
	__IO	float symStatePostBest[5];
	__IO	float covNow[5];
	__IO	float covPostFore[5];
	__IO	float covPostBest[5];
			float symControl;
			float symParmA;
			float symParmB;
	__IO	float errorMes;
			float mesParm;
			float pcesNoise;
			float mesNoise;	
			float transposeA;	
			float transposeQ;
			float transposeR;
			float transposeH;
	__IO	float gain[5];
}Kalman;


/**
	*@name		TyPID
	*@type		struct
	*@about 	PID控制系数
	*@param
	*		--
	*		- Err					自变量
	*		- ErrLastValue[]		前几次Err值
	*		- ErrLastSum			Err累加值
	*		- Proportion			比例
	*		- Integral				积分
	*		- Differential			微分
	*		- Integra_Max			积分限幅值
	*/
typedef struct{
	
	int 	Proportion;
	int 	Integral;
	int 	Differential;
	float   Err;
	float	ErrLastValue[3];
	float	ErrLastSum;
	float	Integral_Max;
	float 	k;
	float 	b;
	
}TyPID;

/**
	*@breif	全局标志位
	*/
struct Flag{
	uint16_t 	A;		//环岛
	uint16_t 	B;		//环形
	uint16_t	C;		//Y形
	uint16_t	D;		//捕获sign
	uint16_t	G;		//干簧管
	uint16_t	S;		//出库与入库
	uint16_t	T;		//出库
	uint16_t	K;		//Y形消抖
	uint16_t	W;		//入库消抖
}Flag = {0, 0, 0, 0, 0, 0, 0, 0};

/**
	*@breif	中断读秒
	*/
struct	ITReadTimes{
	int	Tim1;		//环岛
	int	Tim2;		//入库
	int Tim3;		//Y形
	int	Tim4;
	int Tim5;
	int	Tim6;
	int	Tim7;
	int Tim8;
}ITRT = {270, 45, 100, 35, 30, 30, 30, 30};

/**
	*@breif	赛道特征判断值
	*/
struct JudgeValue{
	uint16_t	jm1;		//环岛
	uint16_t	jm2;		//环岛
	uint16_t	jm3;		//十字
	uint16_t	jm4;		//十字
	uint16_t	jm5;		//十字
	uint16_t	jm6;		//Y形
	uint16_t	jm7;		//Y形
	uint16_t	jm8;		//捕获
}JDVL = {3800, 500, 2000, 100, 100, 10, 100, 2000};

/**
	*@funcname		bsp_LED_FeedBack()
	*@brief 		LED程序测试闪烁反馈函数
	*/
void bsp_LED_FeedBack(void)
{
	HAL_GPIO_TogglePin(GPIOC, LED1_Pin);
	HAL_Delay(100);
	HAL_GPIO_TogglePin(GPIOC, LED1_Pin);
}
/**
	*@funcname		fputc()
	*@brief 		串口输出重定向
	*/
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

/**
	*@funcname		bsp_Usart_Receive()
	*@brief 		串口接收数据函数
	*/
void bsp_Usart_Receive(void)
{
	if(recv_end_flag ==1)			
		{	
			bsp_Usart_Operate(rx_buffer);
			for(uint8_t i=0;i<rx_len;i++)
			{
				rx_buffer[i]=0;
			}
			rx_len=0;
			recv_end_flag=0;
		}
		HAL_UART_Receive_DMA(&huart1,rx_buffer,200);	
}

STM32串口的应用可以参考该博主博客:

/**
	*@funcname		bsp_OLED_Display()
	*@brief 			OLED屏显函数
	*@count
	*		--
	*		-	PID数值
	*		-	ADC原始采集值
	*		-	Err
	*/
void bsp_OLED_Display(void)
{
	bsp_PID_Control();
	bsp_SteMot_PwmSet(SteMot.pwmrateTemp);
	
	OLED_ShowString(0, 0, (uint8_t *)"P:", 12);
	OLED_ShowNum(15, 0, PID.Proportion, 3, 12);
	OLED_ShowString(40, 0, (uint8_t *)"I:", 12);
	OLED_ShowNum(55, 0, PID.Integral, 3, 12);
	OLED_ShowString(80, 0, (uint8_t *)"D:", 12);
	OLED_ShowNum(95, 0, PID.Differential, 3, 12);	
	
	OLED_ShowString(0, 2, (uint8_t *)"ADC OrValue:", 12);
	OLED_ShowNum(0, 3, adcData.orignalData[0], 4, 12);
	OLED_ShowNum(50, 3, adcData.orignalData[1], 4, 12);
	OLED_ShowNum(100, 3, adcData.orignalData[2], 4, 12);
	OLED_ShowNum(0, 4, adcData.orignalData[3], 4, 12);
	OLED_ShowNum(50, 4, adcData.orignalData[4], 4, 12);
	OLED_ShowNum(100, 4, adcData.IDUC_Ex, 4, 12);
	
	OLED_ShowString(0, 5, (uint8_t *)"PwmRate:", 12);
	OLED_ShowUnFloat(60, 5, SteMot.pwmrateFnal, 7, 2, 12);
	
	OLED_ShowString(0, 6, (uint8_t *)"A:", 12);
	OLED_ShowNum(30, 6, Flag.A, 4, 12);
	OLED_ShowString(60, 6, (uint8_t *)"B:", 12);
	OLED_ShowNum(100, 6, Flag.B, 4, 12);
	OLED_ShowString(0, 7, (uint8_t *)"C:", 12);
	OLED_ShowNum(30, 7, Flag.C, 4, 12);
	OLED_ShowString(60, 7, (uint8_t *)"D:", 12);
	OLED_ShowNum(100, 7, Flag.D, 4, 12);
	
}

/**
	*@funcname		bsp_Usart_CallBack()
	*@brief 		串口参数返回
	*/
void bsp_Usart_CallBack(void)
{
	printf("\n");
	printf("\nPID - P: %d", PID.Proportion);
	printf("\nPID - I: %d", PID.Integral);
	printf("\nPID - D: %d", PID.Differential);
	printf("\nPID - Pb: %.2f", PID.b);
	printf("\nPID - Pk: %.2f", PID.k);
	printf("\n");
	printf("\nPCA - A: %.2f", PCA.paramA);
	printf("\nPCA - B: %.2f", PCA.paramB);
	printf("\nPCA - C: %.2f", PCA.paramC);
	printf("\nPCA - P: %.2f", PCA.paramP);
	printf("\nPCA - L: %.2f", PCA.paramL);
	printf("\n");
	printf("\nSpeed: %.2f%%", MotDiff.basepwmvalue/100.0);
	if (MotDiff.pwmSwitch == 1)
	{
		printf("\nMotDiff: ON");
		printf("\nMotDiff Param: %.2f", MotDiff.Param);
	}
	else
		printf("\nMotDiff: OFF");
	if (Switch.ONOF1 == 1)
	{
		printf("\nhuandaoK; %d", huandaoK);
		printf("\nJ1: %d", JDVL.jm1);
		printf("\nJ2: %d", JDVL.jm2);
	}
	if (Switch.ONOF2 == 1)
	{
		printf("\nJ3: %d", JDVL.jm3);
		printf("\nJ4: %d", JDVL.jm4);
		printf("\nJ5: %d", JDVL.jm5);
	}
	if (Switch.ONOF3 == 1)
	{
		printf("\nJ6: %d", JDVL.jm6);
		printf("\nJ7: %d", JDVL.jm7);
	}
	if (Switch.ONOF4 == 1)
		printf("\nJ8: %d", JDVL.jm8);
	
	printf("\nFlag G:%d", Flag.G);
	printf("\nITRT Tim2: %d", ITRT.Tim2);
	printf("\nITRT Tim1: %d", ITRT.Tim1);
	printf("\nITRT Tim3: %d", ITRT.Tim3);
	printf("\nErr: %.3f", adcData.Error);
	
	printf("\n");
}

在这一步,你也可以只定义出结构体与函数的名称,留到后面再完善也可,只需初步搭建出代码框架.如:

/* 舵机控制 */
typedef struct{
    
}SteMotDriver;

/**
	*@funcname		bsp_LED_FeedBack()
	*@brief 		LED程序测试闪烁反馈函数
	*/
void bsp_LED_FeedBack(void)
{

}

2.滤波算法

滤波算法的使用是为了在一定程度上滤除部分噪声,使得最终得到的值无限逼近实际值,在电磁循迹车上是为了使得电磁杆采集值经过滤波处理后可以比较真实的反应赛道实际情况。

刚开始我们选择的滤波算法是卡尔曼滤波,他的结构体在上面可以看到。但是由于对卡尔曼滤波的不甚了解,我们无法对其参数进行调整,所以在最后改用了一阶αβ滤波算法。由于赛道较为简单,速度较慢,在比赛中我们也遇到了不用滤波算法直接对电感采集值归一化后传给PID控制的小组。事实也证明,并非越复杂越高级的算法就越好,在实际运用中,只有最适合算法的没有最好的算法,所谓大道至简

在开头的架构我也列出了一些常见的滤波算法,具体算法的思想可以参考以下博主的博客:

/**
	*@funcname		bsp_ArBi_Filter()
	*@brief 			一阶(αβ)滤波
	*/
float bsp_ArBi_Filter(uint16_t value, uint8_t i)
{
	static uint16_t ArBi_lastValue[5] = {0, 0, 0, 0, 0};
	float result;
	
	result = 0.80 * value + (1 - 0.80) * ArBi_lastValue[i];
	ArBi_lastValue[i] = result;
	
	return result;
}

3.归一化算法

我们采用的是南通大学原创的差比和差加权算法,其数学模型如下:
E r r = A ⋅ ( L − R ) + B ⋅ ( L M − R M ) A ⋅ ( L + R ) + C ⋅ ∣ L M − R M ∣ ⋅ P Err=\frac{A·(L-R)+B·(LM-RM)}{A·(L+R)+C·|LM-RM|}·P Err=A(L+R)+CLMRMA(LR)+B(LMRM)P
详细介绍可参考以下链接:

该算法有四个参数要调,具体如何调参可以参考我队友的博客:

/**
	*@funcname		bsp_PCA_Init()
	*@brief 		PCA(差比和加权算法系数)初始化
	*@param
	*			--	未调参值
	*				- A			1
	*				- B			1
	*				- C			1
	*				- P			0.5
	*/
void bsp_PCA_Init(void)
{
	PCA.paramA = 1.90;
	PCA.paramB = 6.75;
	PCA.paramC = 9.65;
	PCA.paramP = 1.18;
	PCA.paramL = 1;
}
/**
	*@funcname		bsp_ADCValue_PoorCmpAnd()
	*@brief 		差比和差加权算法
	*/
float bsp_ADCValue_PoorCmpAnd(ADCData value)
{
	float Err;
	
	/* 差比和差加权 */
	if(PCA.flag == 0)
	{
		Err = (
						(	PCA.paramA * (value.IDUC_L - value.IDUC_R) +
							PCA.paramB * (value.IDUC_LM - value.IDUC_RM)) /
						(
							(	PCA.paramA * (value.IDUC_L + value.IDUC_R)) +
							(	PCA.paramC * (fabs((double)(value.IDUC_LM - value.IDUC_RM))))
						)
					) * PCA.paramP;
						
	}
	
	return Err;
}

4.PID控制算法

PID的具体原理可以参考以下博主的博客:

在实际调车中,我们一般只用PD进行控制,具体调参表现为:

P增大,小车对差值响应幅度更大,过大会导致小车行驶过程中扭动。

D增大,可以一定程度上减小小车扭动,D过大后对P的抑制作用逐渐减弱。

附PID调参口诀:

在这里插入图片描述

关于PID我们对P通过一个关于差值Err的线性函数对其实现动态改变,以适应直道与弯道的不同需求,满足公式:
P = k ⋅ ∣ E r r ∣ + b P = k·|Err|+b P=kErr+b
因此在实际调参中是对 k k k b b b值进行调整。

/**
	*@funcname		bsp_PID_Init()
	*@brief 		PID初始化
	*@param
	*			--(未调参值)
	*			- P			1
	*			- I			1
	*			- D			1
	*/
void bsp_PID_Init(void)
{
	PID.Proportion = 30;
	PID.Integral = 0;
	PID.Differential = 80;
	PID.Integral_Max = 0;
	PID.b = 55;
	PID.k = 178;
	
	PID.ErrLastSum = 0;
	PID.ErrLastValue[0] = 0.0;
	PID.ErrLastValue[1] = 0.0;
	PID.ErrLastValue[2] = 0.0;
}

/**
	*@funcname		bsp_PID_Core()
	*@brief 		PID核心算法(位置式)
	*/
float bsp_PID_Core(float error)
{
	PID.Err = error;
	float PwmRate;		
	
	PID.ErrLastValue[2] = PID.ErrLastValue[1];
	PID.ErrLastValue[1] = PID.ErrLastValue[0];
	PID.ErrLastValue[0] = error;
	
	/* 积分限幅 */
	if (
			((PID.ErrLastSum + error) < PID.Integral_Max) &&
			((PID.ErrLastSum + error) > -PID.Integral_Max)
		 )
	{
		/* err值累加 */
		PID.ErrLastSum += error;			
	}
	else if (PID.ErrLastSum > 0)		
	{
		/* 正向限幅 */
		PID.ErrLastSum = PID.Integral_Max;
	}
	else if (PID.ErrLastSum < 0)		
	{
		/* 反向限幅 */
		PID.ErrLastSum = -PID.Integral_Max;
	}
	
	PID.Proportion = PID.k * fabs(adcData.Error)+ PID.b;
	/* Core */
	PwmRate = PID.Proportion * (error) + PID.Integral * PID.ErrLastSum + 
						PID.Differential * ((PID.ErrLastValue[0] - PID.ErrLastValue[1]) -
					(	PID.ErrLastValue[1] - PID.ErrLastValue[2]));
	
	return PwmRate;
}


/**
	*@funcname		bsp_PID_Control()
	*@brief 		PID控制函数
	*/
void bsp_PID_Control(void)
{
	bsp_ADC_Operate();
	SteMot.pwmrateTemp = bsp_PID_Core(adcData.Error);
	bsp_SteMot_PwmSet(SteMot.pwmrateTemp);

}

5.赛道特征值响应函数

该函数主要是要在实际调参过程中在赛道的环岛,Y叉等特征处通过采集确定可以识别出该处的特征值,在函数中通过条件判断,一旦小车电磁杆识别到特征值直接通过对舵机进行固定转角实现过弯。

/**
	*@funcname		bsp_CycleIn()
	*@brief 		环岛判断
	*/
float bsp_CycleIn(float value)
{
	float result = value;
	
	if (Switch.ONOF1 == 1)
	{
		if((adcData.IDUC_LM + adcData.IDUC_RM) > 5000 && adcData.IDUC_M > 3000)
		{
					Flag.A = 1;
		}
	}
	
	if (Flag.A == 1)
	{
		result -= adcData.IDUC_Ex/15;
	}
		return result;
}



/**
	*@funcname		bsp_Cross()
	*@brief 		十字,环形
	*/
float bsp_Cross(float	value)
{	
	float result = value;
	
	if (Switch.ONOF2 == 1)
	{
		if (adcData.IDUC_M > JDVL.jm3)
		{
			if (adcData.IDUC_L - adcData.IDUC_R < JDVL.jm4)
			{
				if (adcData.IDUC_LM - adcData.IDUC_RM < JDVL.jm5)
				{
					Flag.B++;
				}
			}
		}
	}
	return result;
}

/**
	*@funcname		bsp_Yshape()
	*@brief 		Y形
	*/
float bsp_Yshape(float value)
{
	float result = value;
	
	if (Switch.ONOF3 == 1)
	{
		if (adcData.IDUC_M < 10)
		{
			if (adcData.IDUC_Ex < 120 && adcData.IDUC_M < 120 && ITRT.Tim3 == 100)
			{				
					Flag.C++;
					Flag.K = 1;			
			}
		}
	}
	
	if(Flag.C == 1)
	{
		result -= 120;
	}
	if(Flag.C == 3)
	{
		result += 170;
	}
	return result;
}

基本的控制类算法就如上所示了。

6.如何方便调参

蓝牙模块与Wifi模块的运用可以很大程度上提高调参的效率。

可以通过编写相关的蓝牙指令实现在小车上电过程中直接对小车参数进行调整,从而避免反复烧录程序的麻烦。

/**
	*@funcname		bsp_Usart_Operate()
	*@brief 		串口指令
	*@oder
	*		--
	*		-		1			P:value			PID_P = value
	*		-		2			I:value			PID_I = value
	*		-		3			D:value			PID_D = value
	*		-		4			A:value			PCA_A = value
	*		-		5			B:value			PCA_B = value
	*		-		6			C:value			PCA_C = value
	*		-		7			p:value			PCA_P = value
	*		-		24			L:value			PCA_L = value
	*		-		8			M:value			SteMot.Min = value
	*		-		9			--	
	*							-	G:1			电机启动
	*							-	G:0			电机关闭
	*		-		10			--
	*							-	S:1			电机差速跟随控制启动
	*							-	S:0			电机差速跟随控制关闭
	*		-		11			M:value			电机占空比(速度)	
	*		-		15			Pb:value		PID_Pb = value
	*		-		16			Pk:value		PID_Pk = value
	*		-		19			s1:value		开断环岛判断
	*		-		20			s2:value		开断十字判断
	*		-		21			s3:value		开断Y形判断
	*		-		22			s4:value		开断标识值捕获
	*		-		23			s5:value		切换滤波算法
	*		-		17			W:value			切换归一化算法
	*		-		18			h:value			环岛中值电感增益值削减度
	*		-		25			U:Any			串口返回参数值
	*		--
	*/
void bsp_Usart_Operate(uint8_t *str)
{
	float value;
	uint8_t oder = 0;
	
	if			((*str == 'P') && (sscanf((const char *)str, "P:%f", &value) == 1))		oder = 1;
	else if ((*str == 'I') && (sscanf((const char *)str, "I:%f", &value) == 1))		oder = 2;
	else if ((*str == 'D') && (sscanf((const char *)str, "D:%f", &value) == 1))		oder = 3;
	else if ((*str == 'A') && (sscanf((const char *)str, "A:%f", &value) == 1))		oder = 4;
	else if ((*str == 'B') && (sscanf((const char *)str, "B:%f", &value) == 1))		oder = 5;
	else if ((*str == 'C') && (sscanf((const char *)str, "C:%f", &value) == 1))		oder = 6;
	else if ((*str == 'L') && (sscanf((const char *)str, "L:%f", &value) == 1))		oder = 24;
	else if ((*str == 'p') && (sscanf((const char *)str, "p:%f", &value) == 1))		oder = 7;
	else if ((*str == 'F') && (sscanf((const char *)str, "F:%f", &value) == 1))		oder = 8;
	else if ((*str == 'G') && (sscanf((const char *)str, "G:%f", &value) == 1))		oder = 9;
	else if ((*str == 'S') && (sscanf((const char *)str, "S:%f", &value) == 1))		oder = 10;
	else if ((*str == 'M') && (sscanf((const char *)str, "M:%f", &value) == 1))		oder = 11;
	
	else if ((*str == 'P') && (*(str+1) == 'k') && (sscanf((const char *)str, "Pk:%f", &value) == 1))		oder = 15;
	else if ((*str == 'P') && (*(str+1) == 'b') && (sscanf((const char *)str, "Pb:%f", &value) == 1))		oder = 16;
	else if ((*str == 's') && (*(str+1) == '1') && (sscanf((const char *)str, "s1:%f", &value) == 1))		oder = 19;
	else if ((*str == 's') && (*(str+1) == '2') && (sscanf((const char *)str, "s2:%f", &value) == 1))		oder = 20;
	else if ((*str == 's') && (*(str+1) == '3') && (sscanf((const char *)str, "s3:%f", &value) == 1))		oder = 21;
	else if ((*str == 's') && (*(str+1) == '4') && (sscanf((const char *)str, "s4:%f", &value) == 1))		oder = 22;
	else if ((*str == 's') && (*(str+1) == '5') && (sscanf((const char *)str, "s5:%f", &value) == 1))		oder = 23;
		
	else if ((*str == 'W') && (sscanf((const char *)str, "W:%f", &value) == 1))		oder = 17;
	else if ((*str == 'h') && (sscanf((const char *)str, "h:%f", &value) == 1))		oder = 18;
	else if ((*str == 'U') && (sscanf((const char *)str, "U:%f", &value) == 1))		oder = 25;
	
	else if ((*str == 'J') && (*(str+1) == '1') && (sscanf((const char *)str, "J1:%f", &value) == 1))		oder = 26;
	else if ((*str == 'J') && (*(str+1) == '2') && (sscanf((const char *)str, "J2:%f", &value) == 1))		oder = 27;
	else if ((*str == 'J') && (*(str+1) == '3') && (sscanf((const char *)str, "J3:%f", &value) == 1))		oder = 28;
	else if ((*str == 'J') && (*(str+1) == '4') && (sscanf((const char *)str, "J4:%f", &value) == 1))		oder = 29;
	else if ((*str == 'J') && (*(str+1) == '5') && (sscanf((const char *)str, "J5:%f", &value) == 1))		oder = 30;
	else if ((*str == 'J') && (*(str+1) == '6') && (sscanf((const char *)str, "J6:%f", &value) == 1))		oder = 31;
	else if ((*str == 'J') && (*(str+1) == '7') && (sscanf((const char *)str, "J7:%f", &value) == 1))		oder = 32;
	else if ((*str == 'J') && (*(str+1) == '8') && (sscanf((const char *)str, "J8:%f", &value) == 1))		oder = 33;
	
	else		printf("\nvalue set fail!");
	
	switch(oder)
	{
		case 1: 
						PID.Proportion = (int)value;	
						printf("\nSuccessful set the value of P: %d", PID.Proportion);
						break;
		case 2: 
						PID.Integral = (int)value;	
						printf("\nSuccessful set the value of I: %d", PID.Integral); 
						break;
		case 3:
						PID.Differential = (int)value;	
						printf("\nSuccessful set the value of D: %d", PID.Differential); 
						break;
		case 4:
						PCA.paramA = value;
						printf("\nSuccessful set the value of PCA-A: %f", PCA.paramA); 
						break;
		case 5:
						PCA.paramB = value;
						printf("\nSuccessful set the value of PCA-B: %f", PCA.paramB); 
						break;
		case 6:
						PCA.paramC = value;
						printf("\nSuccessful set the value of PCA-C: %f", PCA.paramC); 
						break;
		case 7:
						PCA.paramP = value;
						printf("\nSuccessful set the value of PCA-P: %f", PCA.paramP); 
						break;
		case 24:
						PCA.paramL = value;
						printf("\nSuccessful set the value of PCA-L: %f", PCA.paramL); 
						break;
		case 8:
						MotDiff.Param = value;
						printf("\nSuccessful set the value of SteMin: %f", MotDiff.Param); 
						break;
		case 9:
						if(value == 1)
						{
							Motor[0].OnOff = 1;
							printf("\nCar Motor ON."); 
						}
						else if (value == 0)
						{
							Motor[0].OnOff = 0;
							printf("\nCar Motor OFF.");
						}
						else
							printf("Err!!");
						break;
		case 10:
						if(value == 1)
						{
								MotDiff.pwmSwitch = 1;
								printf("\nCar Motor Diff Foller ON."); 
						}
						else if (value == 0)
						{
								MotDiff.pwmSwitch = 0;
								printf("\nCar Motor Diff Follor OFF.");
						}
						else
							printf("Err!!");
						break;

		case 11:
						MotDiff.basepwmvalue = (uint16_t)value;
						printf("\nMotor Speed: %.2f%%", (MotDiff.basepwmvalue/100.0)); 
						break;
		case 15:
						PID.k = value;
						printf("\nPID Pk: %.2f", PID.k); 
						break;
		case 16:
						PID.b = value;
						printf("\nPID Pb: %.2f", PID.b); 
						break;
		case 17:
						PCA.flag = value;
						printf("\nSuccessful set the value of pac flag: %f", (float)PCA.flag); 
						break;
		case 18:
						huandaoK = (uint8_t)value;
						printf("\nSuccess! %d", huandaoK);
						break;
		case 19:
						Switch.ONOF1 = (_Bool)value;
						printf("\nSuccess! ONOF1: %d", Switch.ONOF1);
						break;
		case 20:
						Switch.ONOF2 = (_Bool)value;
						printf("\nSuccess! ONOF2: %d", Switch.ONOF2);
						break;
		case 21:
						Switch.ONOF3 = (_Bool)value;
						printf("\nSuccess! ONOF3: %d", Switch.ONOF3);
						break;
		case 22:
						Switch.ONOF4 = (_Bool)value;
						printf("\nSuccess! ONOF4: %d", Switch.ONOF4);
						break;
		case 23:
						Switch.ONOF5 = (uint8_t)value;
						printf("\nSuccess! ONOF5: %d", Switch.ONOF5);
						break;
		case 25:
						bsp_Usart_CallBack();
						break;
		
		case 26:
						JDVL.jm1 = (uint16_t)value;
						printf("\nJ1: %d", JDVL.jm1);
						break;
		case 27:
						JDVL.jm2 = (uint16_t)value;
						printf("\nJ2: %d", JDVL.jm2);
						break;
		case 28:
						JDVL.jm3 = (uint16_t)value;
						printf("\nJ3: %d", JDVL.jm3);
						break;
		case 29:
						JDVL.jm4 = (uint16_t)value;
						printf("\nJ4: %d", JDVL.jm4);
						break;
		case 30:
						JDVL.jm5 = (uint16_t)value;
						printf("\nJ5: %d", JDVL.jm5);
						break;
		case 31:
						JDVL.jm6 = (uint16_t)value;
						printf("\nJ6: %d", JDVL.jm6);
						break;
		case 32:
						JDVL.jm7 = (uint16_t)value;
						printf("\nJ7: %d", JDVL.jm7);
						break;
		case 33:
						JDVL.jm8 = (uint16_t)value;
						printf("\nJ8: %d", JDVL.jm8);
						break;
		default:	break;
	}

}

7.其他代码

/**
	*@funcname		bsp_Motor_PwmSet()
	*@brief 		电机PWM控制函数(调速)
	*/
void bsp_Motor_PwmSet(MotorDriver *MD)
{
	char K = 0;
	
	if(MotDiff.pwmSwitch==1)
	{
		K = adcData.Error > 0 ? 1 : -1;
		MD[0].pwmrate += K * (MotDiff.Param * (SteMot.pwmrateFnal - SteMot.Min) /
																					(STEPWMMAX - SteMot.Min));
		MD[1].pwmrate -= K * (MotDiff.Param * (SteMot.pwmrateFnal - SteMot.Min) /
																					(STEPWMMAX - SteMot.Min));
	}
	
	Motor[0].pwmrate = MotDiff.basepwmvalue;
	Motor[1].pwmrate = MotDiff.basepwmvalue;
	__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_1, (uint16_t)MD[0].pwmrate);
	__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_2, (uint16_t)MD[1].pwmrate);
}

/**
	*@funcname		bsp_SteMot_PwmSet()
	*@brief 		舵机PWM设置函数(转向)
	*/
void bsp_SteMot_PwmSet(float value)
{
	float pwmrate = value;
	
	/* 中值增减 */
	pwmrate += SteMot.Min;
	
	/* 环岛判断 */
	pwmrate = bsp_CycleIn(pwmrate);
	
	/* 十字判断 */
	pwmrate = bsp_Cross(pwmrate);
	
	/* Y形判断 */
	pwmrate = bsp_Yshape(pwmrate);
	
	/* 出库判断 */
	if (Flag.T == 0)
	{
		pwmrate += 220;
	}
	/* 入库 */
		if (Flag.S == 1 && ITRT.Tim2 != 0)
	{
		pwmrate += 230;
	}
	
	/* 标志值捕获 */
	bsp_SignJudge();
	
	if (Flag.S == 1 && ITRT.Tim2 > 0)
		pwmrate += 150;
	if (Flag.S == 1 && ITRT.Tim2 <= 0)
		Motor->OnOff = 0;
	
	/* 限幅 */
	if(pwmrate < STEPWMMIN)
		pwmrate = STEPWMMIN;
	else if (pwmrate > STEPWMMAX)
		pwmrate = STEPWMMAX;
	
	/* 终值更新 */
	SteMot.pwmrateFnal=pwmrate;
	
	__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, (uint16_t)SteMot.pwmrateFnal);
}
/**
	*@brief 	定时器中断回调函数
	*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if (htim == &htim3)
	{
		HAL_ADC_Start(&hadc2);
		adcData.IDUC_Ex = HAL_ADC_GetValue(&hadc2);
		bsp_PID_Control();
		bsp_Motor_PwmSet(Motor);
	}
	if(htim == &htim4)
	{
		if (Flag.S == 1)
		{
			ITRT.Tim2--;
		}
		if (Flag.K == 1)
		{
			ITRT.Tim3--;
			if(ITRT.Tim3 <= 0)
			{
				Flag.K = 0;
				ITRT.Tim3 = 100;
			}
		}
		if (Flag.A == 1)
		{
			ITRT.Tim4--;
			if (ITRT.Tim4 <= 0)
			{
				Flag.A = 0;
				ITRT.Tim4 = 35;
			}
		}
		if (Flag.T == 0)
		{
			ITRT.Tim5--;
			if (ITRT.Tim5 <= 0)
			{
				Flag.T = 1;
			}
		}
		if (Flag.C == 1)
		{
			ITRT.Tim6--;
			if (ITRT.Tim6 <= 0)
			{
				Flag.C = 2;
				ITRT.Tim6= 30;
			}
		}		
		if(Flag.C == 3)
		{
			ITRT.Tim7--;
			if(ITRT.Tim7<=0)
			{
				Flag.C = 0;
				ITRT.Tim7 = 30;
			}
		}
		if (Flag.W == 1)
		{
			ITRT.Tim8--;
			if (ITRT.Tim8 <= 0)
			{
				Flag.W = 0;
				ITRT.Tim8 = 30;
			}
		}
	}
}
/**
  * @brief  外部中断回调函数
  */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	
	if (GPIO_Pin == GPIO_PIN_1 && Flag.W == 0)
	{
		Flag.G++;
		Flag.W = 1;
		bsp_OutAndInbound();
		while(!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1));
	}
	else if (GPIO_Pin == Key1_Pin)
	{
		Motor->OnOff = !(Motor->OnOff);
		while(!HAL_GPIO_ReadPin(GPIOA, Key1_Pin));
	}
}

五.总结

1.在开始做车之前,不要急着动手,先理清楚思路,确定好方案,在开始着手。
2.大道至简,并非最复杂的算法就是最好的算法,只有最适合的才是最好的。
3.在调参时,不要随机乱调参,要先搞清楚每个参数的作用,在一点一点逐渐调参。

源码链接:ei-tracking-car

博主也是第一次做车,第一次参赛,所以在代码编写和算法理解上还有些生疏,如果那里有错误,欢迎大家指正。

  • 46
    点赞
  • 380
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值