前言


循迹小车做了两次,准备这次做一个小的总结,这次是对铁丝的循迹,让我对循迹有了新的认识。

一、任务、要求及评分标准

1.1 任务

设计制作一个自动循迹小车。循迹传感器不限,在规定的平面跑道自动
按顺时针方向循迹前进。跑道的标识为一根直径 0.6~0.9mm 的细铁丝,按照图 1 的示意尺寸,用透明胶带将其贴在跑道上。图中所有圆弧的半径均为为20cm±2cm。
在这里插入图片描述

1.2 要求及评分标准

(1)在图 1 小车所在的直线区任意指定一起点(终点),小车依据跑道上设置的
铁丝标识,自动绕跑道跑完一圈。时间不得超过 10 分钟。小车运行时必须
保持轨迹铁丝位于小车垂直投影之下。如有越出,每次扣 2 分。 (30 分)
(2)实时显示小车行驶的距离和运行时间。(15 分)
(3)在任意直线段铁丝上放置 4 个直径约 19mm 的镀镍钢芯硬币(1 角硬币),
硬币边缘紧贴铁丝,如图 1 所示。小车路过硬币时能够发现并发出声音提
D-2
示。(15 分)
(4)尽量减少小车绕跑道跑完一圈运行时间。 (20 分)
(5)设计报告(20 分

1.3 说明

(1)自动循迹小车允许用玩具车改装。小车用自带电池供电运行,不能使用外
接电源。小车的尺寸为其在地面的投影不超过 A4 纸大小。小车自动运行后,
不得有任何人工干预小车运动的行为,如遥控等。
(2)跑道除指定的铁丝外,不得另外增加任何标记。跑道附近不应有其他额外
金属物体。

二、先上视频效果

投稿在b站的小车循迹
大体是20s完成
在这里插入图片描述
在这里插入图片描述

三、小车循迹方法

1.第一次做循迹时

之前寻黑工胶带的代码,黑工胶带大概是1cm宽,0 是检测到黑线,1 是没检测到,信号线接在P2口,当时用的是51单片机入门做的。
主要方法是遇到大转弯delay多一点,小转弯dalay小一点,delay的作用主要是为了让转弯的时间多一点,多调整几次速度,就能获得较好的巡线效果。
四路循迹 参考代码如下(示例):

void xunji()
{   
 switch(P2&0x0f)
				{
					case 0x0f:		// 1111全部没有压线,前进
						run();
						break;
					case 0x0e:		// 1110右压线,左转
						leftrun();
						delay(10);
						break;
					case 0x0c:		// 1100右压线,左转
					   leftrun();
					   delay(5);
						break;
					case 0x07:		// 0111左压线,右转
					rightrun();
						delay(5);	//转向延时
						break;
					case 0x03:		// 0011左压线,右转
					rightrun();					
						delay(10);	//转向延时
						break;
					case 0x00:
						 stop();
						 break;
					default:
					break;
                 }

}

2.第二次做循迹时

后来发现这样做,在寻细线(铁丝),或者如果要竞速(速度较快时),会出现两个问题:
1、那些比较直的转弯基本很难转过去,如果能转过去也需要调很久,换一个速度就需要重新调速,不适用于所有速度。
2、当走直线时,即使我们调的pwm值是一样的,却走不出来直线,会影响速度。

针对第一个问题:我们用状态机来解决。
针对第二个问题:我们用pid来解决。

状态机

问题引出
现在我们要解决的问题是,我们小车没办法在冲出转弯的时候意识到自己已经冲出转弯,而是转到一半,以为自己已经转到位了,便继续直行,这就是第一个出现的问题。
基本使用方法
于是我们现在需要让小车知道自己运行在什么状态,比方说我们小车一开始是直行,然后检测到左边有铁丝了,需要右转来控制使小车身压在铁丝正下方,此时,我们切换直行状态到右转状态,而转到什么来告诉小车现在是压在正下方呢,当我们发现小车在正下方时(我用的是四路传感器,即所有检测传感器都检测不到时),我们再次切换状态,从右转状态切回直行状态。
以上就是状态机的基本使用方法,我感觉像是给机器增加了一个上一步的记忆。
举个例子 按键的状态机
0是按下,1是未按下(相当于抬起来),就可以不需要delay函数消抖,提高了程序效率。

if (key == 0)
	a = 1;
 if (key == 1)
 	if (a ==1 )
 		{ 你的实现效果;
 		 a = 0;
 		}

我们小车现在有5种状态:直行,左转弯,急左转弯,右转弯,急右转弯,我们首先在头文件枚举。

enum State
{
	RapidLeft = -2,
	TurnLeft = -1,
	TurnRight = 1,
	RapidRight = 2,
	Straight = 0
} typedef State;

然后在.c原文件初始化最开始的状态

State st = Straight;

参考循迹程序如下

void xunji(void)
{
	switch (st)
	{
	case Straight:
	{
		Target_LV = 300;
		Target_RV = 300;
	}
	break;
	case TurnLeft:
	{
		Target_RV = 300;
		Target_LV = 100;
	}
	break;
	case TurnRight:
	{
		Target_RV = 100;
		Target_LV = 300;
	}
	break;
	case RapidLeft:
	{
		Target_RV = 300;
		Target_LV = 0;
	}
	break;
	case RapidRight:
	{
		Target_RV = 0;
		Target_LV = 300;
	}
	break;
	}
	if (Left1 == 1 && Left2 == 0 && Right2 == 1 && Right1 == 1)
	{
		st = TurnLeft;
	}
	else if (Left1 == 1 && Left2 == 1 && Right2 == 0 && Right1 == 1)
	{
		st = TurnRight;
	}
	else if (Left1 == 0 && Left2 == 1 && Right2 == 1 && Right1 == 1)
	{
		st = RapidLeft;
	}
	else if (Left1 == 1 && Left2 == 1 && Right2 == 1 && Right1 == 0)
	{
		st = RapidRight;
	}else if (Left1 == 1 && Left2 == 1 && Right2 == 1 && Right1 == 1)
	{
		if(st == TurnLeft || st == TurnRight) st = Straight;
	}
}

具体思路就是循环检测传感器,每次方向有变化zhuangtai就改变状态值,如果方向有到想要的变化时,才改变状态,否则就一直维持上一个状态,直到状态改变。这样就基本理论上可以实现完美循迹(只要传感器足够灵敏)。

调PID

调节pid的步骤:
1、调节pwm
2、限幅pwm
3、输出pwm

		Leftpwm  =   Incremental_PI(LeftEncoder, Target_L);//PID得到pwm
		Rightpwm =   Incremental_PI(RightEncoder, Target_R);	
		Limit_Pwm();//限幅函数 使pwm限幅 为0~100  //但有正反  所以 -100~100 
		SetCompare_Pwm(Leftpwm,Rightpwm);//pwm输出函数

1、调节pid,一般pid分为增量式pid和位置式pid,增量式pid一般是PI控制,然后位置式pid一般是PD控制,增量式pid的误差只会与前三次误差有关,所以一般来讲,一次错误的累积不会影响总体的误差判断,所以我选了增量式pid。

/**************************************************************************
函数功能:增量PI控制器
入口参数:编码器测量值,目标速度
返回  值:电机PWM
根据增量式离散PID公式 
pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)+Kd[e(k)-2e(k-1)+e(k-2)]
e(k)代表本次偏差 
e(k-1)代表上一次的偏差  以此类推 
pwm代表增量输出
在我们的速度控制闭环系统里面,只使用PI控制
pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)
**************************************************************************/
int Incremental_PI (int Encoder,int Target)
{ 	
	 static float error,Pwm,Last_error,Last_Last_error;
	 error=Target-Encoder;                                  //计算偏差
	 Pwm+=Kp*(error-Last_error)+Ki*error + Kd*(error - 2*Last_error + Last_Last_error);   //增量式PI控制器
	 Last_error=error;	                                   //保存上一次偏差
	 Last_Last_error = Last_error;                         //保存上上次误差
	 //OLED_ShowSignedNum(1,8,error,5);
	 return Pwm;                                           //增量输出
}

:输出的pwm就是编码器下降沿(跳变)的个数,理论上最后的Pwm与Target几乎一致,最后把调制出的pwm输出就是速度。
2、限幅pid,我pwm设的幅度为0-100,在实际测的时候发现在pwm在60以后左右电机偏差就会很大,于是限幅在了0-60。
在这里插入图片描述
3、输出pwm
我的编码器个数数的是AB相各一个脉冲,所以编码器的数字会出现负数,具体请看编码器测速。所以输出的会有正负。

//函数功能:输出给PWM寄存器
void SetCompare_Pwm(int Leftpwm,int Rightpwm)//将Leftpwm,Rightpwm输出出去,使舵机和小车电机受到相应控制
{
	if (Leftpwm >= 0)  //正数 代表正转
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_3);
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);
		PWM_SetCompareLeft(Leftpwm);
	}
	else
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_3);
		GPIO_SetBits(GPIOA, GPIO_Pin_2);
		PWM_SetCompareLeft(-Leftpwm);  //因为SetCompare要传的是正数 所以 speed加负号
	}
	
	
		 if (Rightpwm >= 0)  //正数 代表正转
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_4);
		GPIO_ResetBits(GPIOA, GPIO_Pin_5);
		PWM_SetCompareRight(Rightpwm);
	}
	else
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);
		GPIO_SetBits(GPIOA, GPIO_Pin_5);
		PWM_SetCompareRight(-Rightpwm);  //因为SetCompare要传的是正数 所以 speed加负号
	} 
		//PWM_SetCompareLeft( Leftpwm);
	  //PWM_SetCompareRight( Rightpwm);

}

调pid参数
一开始我定时器是500ms定时,调的时候至少20s才能调到想要的速度。
后来试着把定时器调到5ms定时,调好的时候几乎1s不到就能到想要的速度了。

注意:调节时,把速度指定为最大的速度的一半,这样调出的pid值对速度大速度小都有一个较好的调节。我最大的速度为700mm/s,由于偏向竞速,我选择速度为400ms为调节的速度。

1、PID,先调的P,此时其他置0,P是比例系数,影响最大,调节到有一个稳态误差时(一直与想要的速度有一个稳定的差值),P就差不多好了。
2、然后调I,我I的值是从P的值的1/00开始加的,直到达到想要的速度。
3、增量式一般D的影响不是很大,就置的0。

最后自己调的值为(5ms定时时)为
float Kp = 3.2,Ki = 0.1,Kd = 0.0000;
定时不同PID也要调节500ms定时时,调出来的pid值也是不同的,为
//0.68 0.01 //0.0025 0.0001

四、铁丝与硬币的判断

原理

硬币是1角钱硬币,铁丝是0.6mm粗的铁丝,虽然都含铁,但硬币的面积大,在切割磁感线的时候,变化更加明显,原理就是这样。如果用AD模拟量输出的话,会发现经过硬币时确实会有一个较大的变化。

:由于AD涡流传感器需要一个5v(或者3.3v)的稳定电压来供电,所以千万不能用给电机供电的稳压板上的5v给它供电,因为小车运转的同时,电压会变化,所以需要一个单独的电源模块给它供电。

为保证数字是稳定变化的,所以取了一个20次的平均值,且为了防止电压不稳会导致模拟量阈值不定,用了绝对值变化消除影响。
参考:

	float ad ,last_ad;
	last_ad = Get_adc_Average(ADC_Channel_0, 20);
	while(1)
	{
		ad =Get_adc_Average(ADC_Channel_0, 20);	
		OLED_ShowNum(3, 13, My_abs(ad-last_ad), 4);	
		if (My_abs(ad-last_ad) >80)
		{
		Buzzer_ON();
		delay_ms(300);
		Buzzer_OFF();				
		}
		last_ad = ad;
	}

还有一个注意事项:
1、我小车用的是铝合金的板子,所以在用模拟量时,要是底盘太低,靠近了涡流传感器上的线圈,也会产生较大的干扰,所以我把小车底盘架高(反过来装了)。
2、电池也不能靠近线圈,否则模拟量输出为0。
3、不能同时装有两个线圈,会分别干扰失去作用。
但是如果小车速度太快的话,也会导致蜂鸣器乱叫(就是模拟量乱跳),我怀疑是铝合金钢板太大了不可忽视,最好还是换成亚克力板

五、如何加快速度

硬件

1、编码器用的是光电编码器比较精确的测量的个数,或许对调节速度可能更精准(调pid参数比较好调)。
2、两片c8t6最小系统板,一块用来循迹,一块用来干其他事,这样循迹的主程序就受干扰。
3、差速的两轮小车比舵机的转向更加灵活,且没有最小转弯半径,支持原地自转。
4、牛眼轮比万向轮的摩擦更小。

软件

有一个不知道算方法的方法:
当你左右两个速度一样时,重心是在小车两轮中间,当左边速度是100,右边速度是300时,重心是在两轮右边偏1/4处,可以根据这个来调速度。

void xunji(void)
{
	switch (st)
	{
	case Straight:
	{
		Target_LV = 300;
		Target_RV = 300;
	}
	break;
	case TurnLeft:
	{
		Target_RV = 300;
		Target_LV = 100;
	}
	break;
	case TurnRight:
	{
		Target_RV = 100;
		Target_LV = 300;
	}
	break;
	case RapidLeft:
	{
		Target_RV = 300;
		Target_LV = 0;
	}
	break;
	case RapidRight:
	{
		Target_RV = 0;
		Target_LV = 300;
	}
	break;
	}

还有一个方法可以让主程序的速度变快

int n = 0;
int main(void)
{
while(1)
{
		n = (n + 1) % 50;
	if (n == 0)
	{}
	else if (n == 1)
	{}
	else if (n == 2)
	{}
	else if (n == 3)
	{}
}
}

理论上速度提高到了本来的50倍,本来是每次while都会执行里面四个函数,现在是很快的刷新while,第一次只执行n == 0里的内容,第二次只执行n == 1里的内容,50次循坏只执行4下,而原来每次循环执行4下,所以运行速度提高了本来的50倍。


总结

小车代码详见
链接:https://pan.baidu.com/s/1YpokxuVx42A8Ovzcs4vM8Q?pwd=ey8z
提取码:ey8z

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐