一.硬件部分
必需:STM32F103C6T6(或者STM32F103C8T6),舵机(MG 996R),电机(TT马达 130电机),L298n驱动,电磁杆(可以自己制作),干簧管,两节18650电池,基础四轮车模。
辅助:OLED,HC-05蓝牙模块。
二.软件部分
必需:ADC多路采集的DMA配置,定时器PWM波输出,普通GPIO口,滤波,归一化,差比和,PID算法。
辅助:OLED驱动,串口打印。
1.舵机
三根线:VCC,GND,信号线。 我们给VCC接的6V。信号线接相应PWM波输出口。
舵机调中值:可以使用编码器调节占空比,看舵机一共能够转动多少占空比的范围(注意!舵机不是可以360度旋转的)。然后取最中间的占空比输出给舵机。小菜花当时是设置的20ms为周期,Counter Period 设置的20000-1。
2.电机
开环控制两个电机:两个PWM波输出,四个普通GPIO口控制高低电平。
3.L298n驱动
可以自己去了解如何接线,这里推荐一篇小菜花看到的文章http://t.csdn.cn/Anzwy
4.干簧管
用于在终点处停车。话不多说,上链接https://share.weiyun.com/2m5eUtRv
5.电磁杆的电感值采集
我们组开始准备使用两个水平电感,两个竖直电感;但是最后由于种种原因,我们使用了两个水平电感,两个内八字电感。(最后我们环岛没有进,所以就只使用了两个水平电感完成了最基础的寻迹)。
ADC给四个电感 开了四路DMA采集 ,分别是A1,A2,A3,A4。
如果有条件可以在电磁杆中间使用第五路电感,就是小菜花开的A5(虽然我最后没有用到)。
还有两路ADC 是采集 两个干簧管的IO口 高低电平。
6.一些算法
软件滤波
滤波(Wave filtering)是将信号中特定波段频率滤除的操作,很大程度上保证了采集到的数据的稳定与真实,是抑制和防止干扰的一项重要措施。
这是参考的别的博主的一种滤波算法: 中位算数平均滤波 。 即结合了中位值滤波和算数平均值滤波的一种算法。
这一篇文章非常详细的讲了滤波http://t.csdn.cn/a8DbF
void Get_ADC(void) //得到的ADC电压存储在ADC_Val中
{
int num = 0,count = 0;
for(num = 0; num < 10; num++)
{
HAL_ADC_Start_DMA (&hadc1 ,(uint32_t *)adci,7);//开启七路DMA
HAL_ADC_Start_DMA (&hadc1 ,(uint32_t *)adcj,7);
HAL_ADC_Start_DMA (&hadc1 ,(uint32_t *)adck,7);
for(count = 0; count < 7;count++)//取中值
{
if (adci[count] > adcj[count])
{
adctmp[count] = adci[count]; adci[count] = adcj[count]; adcj[count] = adctmp[count];
}
if (adck[count] > adcj[count])
adctmp[count] = adcj[count];
else if(adck[count] >adci[count])
adctmp[count] = adck[count];
else
adctmp[count] = adci[count];
sum1[count]+=adctmp[count];
}
}
for(count = 0; count < 7;count++)
{
AD_Val[count]=sum1[count]/10;
sum1[count]=0;
}
}
归一化
关于为什么要使用归一化:我看到的很多文章说使用归一化可以提高对不同赛道的适应性。我的理解是,在不同的赛道只用去测每一路电感的最大最小值就可以正常跑了,对于特殊元素的特征值不用再去测量。大大提高了对不同赛道的适应性。
归一化的定义:将数据映射到0-1范围之内处理,可以更快速便捷地观察数据。
归一化的公式:(X - Min) / (Max - Min).
其中 X为某一路电感 滤波后的ADC值;Min / Max为某一路电感 滤波后ADC采集到的最小 / 大值。
/*归一化后每一路ADC的值*/
uint16_t AD_left1;
uint16_t AD_left2;
uint16_t AD_right1;
uint16_t AD_right2;
/*归一化算法*/
void GuiYi_ADC(void)
{
AD_left1 = (uint16_t) (100 * (AD_Val[1] - AD_left1_min) / (AD_left1_max - AD_left1_min));
AD_left2 = (uint16_t) (100 * (AD_Val[2] - AD_left2_min) / (AD_left2_max - AD_left2_min));
AD_right1 = (uint16_t) (100 * (AD_Val[3] - AD_right1_min) / (AD_right1_max - AD_right1_min));
AD_right2 = (uint16_t) (100 * (AD_Val[4] - AD_right2_min) / (AD_right2_max - AD_right2_min));
}
可以进行适度放大(一般是放大100倍),使车能够更容易的根据电磁值判断路况。
差比和
电磁智能车 是根据电磁杆上电感 采集到的值判断路况,可以说电磁杆上的电感就是一辆电磁智能车的眼睛。差比和值能够让车更直观的判断路况。
差比和公式:(L-R)/(L+R)。差比和值范围0-1。
原理:当电感离中心磁感线越近,采集到的值就越大,反之越小。
所以当差比和值为负的时候,可以判断到车向左偏移了,为正则向右偏移了(公式里面的左右交换了则反之)。
int16_t ad_1_sum;
int16_t ad_1_diff;
double count_1;
double position_1;
/*差比和算法*/
void ChaBiHe_ADC(void)
{
ad_1_sum = (int16_t)AD_left2 + (int16_t)AD_right1+1;
ad_1_diff = (int16_t)AD_left2 - (int16_t)AD_right1;
count_1 = (double)ad_1_diff / ad_1_sum;
position_1 = count_1 * 100;
HAL_Delay(5);
}
小菜花给差比和值乘了100,这里的100可以自行修改,根据需要调整。
PID算法
小菜花采用的位置式PD控制舵机。
小菜花将差比和值乘100后(就是上段代码中的position)直接传给PD算法中作为误差error。
那么为什么可以这样呢,因为我把 差比和值为0作为目标,采集回来的差比和值作为实际值,那么误差error就是 实际值-目标值。 注意,我的目标值为0,所以error可以直接为采集回的差比和实际值。
typedef struct
{
double PID_P; /* 比例常数 */
double PID_D; /* 微分常数 */
double LastError; /* 前一项误差 */
double PrevError; /* 前第二项误差 */
}PID;
double PID_Vertical (PID *pp)
{
double dError1, Error1;
Error1 = position_1;//差比和的值作为error /*目标值为差比和值为0,所以可以直接将差比和实际所得值作为error*/
dError1 = pp->LastError - pp->PrevError;
pp->PrevError = pp->LastError;
pp->LastError = Error1;
PWMValue1 = pp->PID_P * Error1 + pp->PID_D * dError1;
return PWMValue1;
}
然后就开始调P和D,如果P D调得好,车就跑得很丝滑。小菜花建议可以使用分段PD来调车,亲测有效!车跑起来确实丝滑。
PD算法返回的值传给
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3,PWMValue1+745);
745是小菜花当时找到的舵机中值,PWMValue1可正可负,调节舵机可以左右转动。
7.特殊元素
十字路口
小菜花当时是用两路水平电感寻迹,到了十字路口也不会出现误判,能够正常行进。
三岔路口
当两路水平电感采集的归一化后的值 出现同时突然下降时,强制打角。
环岛检测
小菜花是设置了三个标志位,到赛道上去采集阈值。当三个标志位都满足时,进行了强制打角(可以在此时切换为内八字电感寻迹,由内八电感的PD算法输出占空比拐弯进环岛。不推荐强制打角,会降低对不同赛道的适应性)。
然后用水平电感跑环岛。出环的时候检测两条磁感线重合的位置,到赛道测阈值,设置标志位,满足条件则强制直行,延时控制出环岛(依旧不推荐强制控制,但小菜花的能力有限,只能想到这个办法)。
出库
小菜花当时的思路是:开机就强制打角,延时控制直到出库,随后正常水平巡线。(所以出库函数只运行一遍!)
void chuKu(void)
{
uint16_t i;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,7000);//先低速跑电机
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2,7000);
for(i=155;i<192;i++)
{
Value1=i+745;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3,Value1);
HAL_Delay(15);
}
}
用了一个for循环(也可以不用,直接输出占空比)。for循环 使舵机打角时不会突然一下就转到相应角度,而是更加丝滑地转过去。
入库
干簧管经过终点磁铁,会由高电平变为低电平。因此,检测到干簧管的IO口有一个电平变化,标志位加一。在while循环里面标志位变为一的时候进行强制打角,延时控制入库,舵机转回中值,电机停止转动。
/*干簧管标志位*/
static uint16_t ganflag = 0;
if ((AD_Val[0]<300||AD_Val[6]<300) )//干簧管检测出入库
{
ganflag++;
HAL_Delay(100);
if ((AD_Val[0]<300||AD_Val[6]<300) && ganflag == 1)
{
ruKu();
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);//电机GPIO口高低电平
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3,745);
}
}
void ruKu(void)
{
uint16_t i;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,7000);//先低速跑电机
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2,7000);
for(i=150;i<220;i++)
{
Value1=i+745;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3,Value1);
HAL_Delay(15);
}
}
干簧管标志位检测可以放在中断里面更好,检测到一次下降沿,触发一次中断,标志位加一。
小菜花放在while循环里面会存在一个问题:过一次磁铁,标志位不只加一(我猜测是干簧管检测到一次电平变化,但是代码已经刷过好几遍了,所以标志位加的次数不定)。所以,我加了一个延时,HAL_Delay(100),试了一下,可以过一次干簧管,标志位加一。
8. 一些建议
电磁杆
我们当时是自己做了电磁杆,但是由于其中有一路重要电感不能用,所以最终放弃,买了一个电磁杆(所以被迫使用水平+内八字电感)。 建议大家有条件的可以买一个电磁杆,不要把过多的时间都放在修电磁杆上面了!!!
OLED
可以用OLED来显示数据,观察起来很方便。 不幸的是,小菜花当时的OLED不知道为啥用不了,
插核心板上面一点反应都没有!!!
HC-05蓝牙模块
可以使用空闲中断,用蓝牙与手机通信,直接在手机上面调节PD值,十分方便。
Debug
如果你跟我一样,不幸地 OLED用不了,蓝牙串口不,打印那么就用Debug看变量的值吧!!!
文末
还有一个电脑端上位机VOFA+,推荐使用(由于小菜花能力和时间有限,没有深入了解这个VOFA+),读者可以自己去了解使用。