星空·综合体育官网入口 智能车—电磁循迹
电磁跟踪车
本文是第一届智能汽车学校大赛的回忆整理。主要从软件部分讲解了如何从零开始实现一个电磁追踪小车。它不涉及复杂的算法和特殊轨道元素的识别。
一、系统总体架构 1、硬件架构
2、软件架构
二、基础知识准备 1. PWM(脉宽调制)
pwm中两个比较重要的参数是频率和占空比
频率:指1秒内(一个周期)信号从高电平到低电平再回到高电平的次数
占空比:一个脉冲周期内,高电平时间与整个周期时间的比值
简单来说,PWM就是单片机的IO口输出的一系列高低电平信号。当频率固定时,通过调节占空比可以输出不同的模拟电压。占空比越大,输出电压就越大。当占空比为100%时,输出电压一般为单片机电源电压(3.3V或5V)。
2、舵机控制原理
伺服电机是带有输出轴的位置伺服电机。当我们向它发送控制信号时,输出轴就可以转动到特定的位置。只要控制信号保持不变,伺服机构就会保持其相对角位置。如果控制信号发生变化,输出轴的位置也会随之变化。大多数舵机都是通过 PWM 信号控制的。
舵机的PWM信号控制原理主要是通过控制脉宽时间来确定的。对应关系如下:
占空比 = t / T 相关参数如下:
t = 0.5ms —————— 舵机会转到 -90°
t = 1.0ms —————— 舵机会转到 -45°
t = 1.5ms —————— 舵机会转到 0°
t = 2.0ms —————— 舵机会转到 45°
t = 2.5ms —————— 舵机会转到 90°
若设置PWM信号的频率为50HZ,则一个脉冲周期为2 ms,调节占空比在2.5%-12.5%即可控制舵机从-90°—90°。
PS:不是所有的舵机都是180度舵机,但脉冲宽度为1.5ms所对应的位置一般都为舵机的中间位置(即全范围的一半)
3、电机控制原理
智能汽车所使用的电机主要分为有刷电机和无刷电机两种。一般来说,有刷电机比较常用,不过近两年无刷电机也开始在越野、骑行团体中使用。由于笔者没有接触过无刷电机,所以本文主要关注有刷电机。
对于有刷电机,当电机两端施加电压时,电机即可旋转。电压越大,电机转动越快。电机的正反转取决于电源的方向,因此电机没有严格意义上的正负极之分。
由于电机的正反转是由供电方向决定的,实际电路中的电源接口一般是固定的(即供电电路的正负极是固定的),所以有一个H-需要桥式驱动电路来切换电机的供电方向。 H桥驱动电路如下图所示。四个开关均由MOS管组成。当对角线开关闭合时,电机转动。如图所示,S1、S4闭合时电机正转,S2、S3闭合时电机反转。
通过以上分析,电机控制主要控制电机转速和电机方向。前面提到的PWM信号正好可以满足电压转换,所以很自然地想到用PWM信号来控制电机。但问题是单片机IO口输出的PWM信号是否可以直接给电机供电。答案是否定的。 。原因是单片机IO口的负载能力有限(通俗点就是电流太小,只有几十毫安),驱动电机需要较大的电流。所以就需要一个所谓的电机驱动(这也是很多新手不明白的一点)。我在制作智能车时使用过的驱动程序有DRV8701和HIP4082。本文主要是算法分析,不涉及具体的硬件电路分析,所以不做更多介绍。
4、电磁跟踪原理
轨道中央铺设电磁引导线,流过20khz、100mA的交流电。因此,在电磁导丝附近会产生一个按照电流规律变化的磁场。当导体置于该磁场中时,就会产生感应电动势。一般采用工字形电感作为导体,产生的感应电压经过运算放大器(运算放大器)放大后输入到单片机进行处理。
显然,单片机获得的电压值越大,电感器距离轨道中心越近。两个对称的电感器通常用于线路跟踪。当两个电感之差为0时,汽车位于轨道中心。
5、PID算法
PID算法是闭环控制系统常用的算法。简单来说就是控制输出的物理量达到我们期望的值,比如控制电机的转速到某个固定的转速。 PID算法又分为位置式PID和增量式PID。该算法的具体内容网上有很多介绍,本文不再赘述。
由于本文针对的是从零开始的初学者,不涉及更多传感器,因此电机控制采用开环控制星空·体育中国官方网,舵机控制采用位置PD算法。
3、模块调试
1. 舵机居中
从商家购买车型时,四轮C车型的转向器是没有安装的,需要自己组装。组装分为以下两步:
机械对中:如上所述,当给舵机一个1.5ms的高电平pwm信号时,舵机就会转到中间位置。此时,与轮子连接的旋转轴将与舵机本体平行安装。机械对准完成。
软件调整:需要软件调整的主要原因是机械调整好的舵机安装在车模上后,由于一些人为原因以及车体机械结构的原因,会出现一些偏差。这时就需要软件进行调整。一般的方法是用代码测试中间位置(一般是机械调节的中间位置附近),直到轻轻推车就能直线前进,则软件调节完成。
2.运放调试
前面提到,电磁传感器获得的电磁值需要经过运算放大器放大星空体育app下载入口,然后输入到单片机进行处理。原因是,一般情况下,电磁传感器获得的值较小,需要放大以进行后续处理。调试运放的步骤如下:
4. 数据处理 1. 归一化
归一化是指将数据映射到0~1的范围以供后续处理。之所以需要对电磁数据进行归一化,是因为电感的性能差异以及不同轨道的电磁线的差异会导致电感数据波动较大,导致不同情况下小车的稳定性下降。
通过标准化可以更好地消除这些影响。在不同轨道上调试时,只需测量电感可以读取的最大值和最小值,就可以很好地纠正误差。
归一化的公式如下:
value = (value-value_Min)/(value_Max-value_Min)
对于电磁数据,我们会在归一化的同时将其扩大100倍,以便后续进行元素识别和舵机控制。
value = 100*(value-value_Min)/(value_Max-value_Min)
有的新手可能会问为什么归一化后要放大100倍呢?这不是失去了标准化效果吗?其实这个问题很容易解释。据了解,我们可以将数据归一化到0~100的范围,这样就消除了影响,也方便后续的元素识别(本文不会涉及这部分内容,可以参考网上专家的博客)。
2. 差值比与总和
差比和算法的公式如下:
Err = (A(L−R) + B(LM−RM)) / (A(L+R) + C∣LM−RM∣)
上面的公式是卓庆老师推文中提到的公式(应该是某学校改进的差比和算法),但本文是为了入门电磁巡线车,所以对公式进行了简化作为:
Err = (L - R) / (L + R)
这个公式也是最简单的差比和公式星空体育平台官网入口,可以实现正常的循线,但是因为丢弃了中间两个八位数电感的数据,所以对曲线不会那么敏感。
式中,L代表左侧的电感值,R代表右侧的电感值。可以看出,当小车移动到赛道左侧时,L变小,R变大,Err为负数。否则,Err为正数,可以确定。汽车在赛道上的位置。一般情况下,这个功能只用L - R就可以实现。为什么要用差比和算法。原因是差比和和可以让得到的Err数据更加平滑,也就是说Err在某个位置不容易变异。
3. 数据过滤
数据过滤的主要目的是过滤掉一些收集到的不合理的数据。这些数据可能是由于电感本身或轨道本身的问题而导致的异常波动数据。
常用的滤波方法有中值滤波、均值滤波、中值平均滤波、卡尔曼滤波等,本文给出了中值平均滤波的代码:
for(j=0;j<9;j++) // 冒泡排序
{
for(i=0;i<9-j;i++)
{
if(ad_value[i]>ad_value[i+1])
{
temp = ad_value[i];
ad_value[i] = ad_value[i+1];
ad_value[i+1] = temp;
}
}
}
// 去掉最大值和最小值
for(i=1;i<9;i++)
{
sum_value += ad_value[i];
}
// 求平均值
temp = sum_value/8;
思路是将得到的10个连续数据从小到大排列,去掉最大值和最小值,然后对剩下的数据进行平均。
5、车辆控制的实现
完整的车辆流程图如下:
数据采集和处理部分在之前的文章中已经提到过,所以这一部分主要讲述如何实现舵机和电机控制。
1、伺服PD控制
舵机的控制原理前面已经讲解过。对于智能车寻线来说,舵机的作用是控制车体的位置始终处于赛道的中心。那么如何将电感器采集到的误差值与舵机的转向角进行拟合呢?关系是我们需要考虑的一切。 PID算法是一种能够很好实现闭环控制的算法。它可以在误差和方向盘转角之间建立联系,使方向盘能够快速、准确地转动。由于这部分需要长期的参数调整,需要读者自行探索(建议读者在调整参数之前对PID算法有一定的了解,不宜盲目调整)。
代码实现如下:
error_last = error;
error = get_adc(); //获取处理后的电感值
error_angle = kp*error + kd*(error-error_last); //舵机pd算法确定转角偏值
2. 电机开环控制
由于作者在第一届学校比赛中未能实现速度闭环控制,因此本文采用电机开环控制。原理其实很简单。给定控制电机pwm的占空比,电机可以保持以一定速度旋转。
当然,开环控制有一个很大的问题,那就是速度不稳定。当 PWM 的占空比固定时,电机转速会因电池电压下降以及其他因素的影响而产生波动。这个问题在低速时不明显,但在高速时(如车速达到2m/s)就会出现严重波动,导致汽车失控。因此,电机速度闭环非常重要。闭环控制采用递增速度。定量PID算法本文不再详细描述。
3. 其他细节
在控制舵机时,需要限制舵机可以转动的最大角度。舵机安装在车模上后,可以转动的角度范围并不大。如果不加以限制,舵机可能会被卡住。转动并最终烧毁舵机。
当感应器无法感应到电磁信号时,将小车的速度设置为0,以避免小车冲出赛道后损坏车模。
六、基于诸飞底层的代码实现 1、引脚定义
//引脚定义区
#define POWER_ADC1_MOD ADC_1 //定义通道一 ADC模块号
#define POWER_ADC1_PIN ADC1_CH3_B14 //定义通道一 ADC引脚
#define POWER_ADC2_MOD ADC_1 //定义通道二 ADC模块号
#define POWER_ADC2_PIN ADC1_CH4_B15 //定义通道二 ADC引脚
#define POWER_ADC3_MOD ADC_1 //定义通道三 ADC模块号
#define POWER_ADC3_PIN ADC1_CH10_B21 //定义通道三 ADC引脚
#define POWER_ADC4_MOD ADC_1 //定义通道四 ADC模块号
#define POWER_ADC4_PIN ADC1_CH12_B23 //定义通道四 ADC引脚
#define S_MOTOR1_PIN PWM4_MODULE2_CHA_C30 //定义舵机引脚
#define PWM_0 PWM1_MODULE3_CHA_D0 //定义电机pwm信号引脚
#define PWM_1 PWM1_MODULE3_CHB_D1
#define PWM_2 PWM2_MODULE3_CHA_D2
#define PWM_3 PWM2_MODULE3_CHB_D3
2. 外设初始化
void Init_Proc(void)
{
pit_init(); //初始化pit外设
pit_interrupt_ms(PIT_CH0,5); //初始化pit通道0,中断时间为5ms
pwm_init(S_MOTOR1_PIN,50,3600); //舵机pwm引脚初始化
//电机pwm引脚初始化
pwm_init(PWM_0, 17000, 0); //单片机端口D0 初始化PWM_1周期10K 占空比0
pwm_init(PWM_1, 17000, 0); //单片机端口D1 初始化PWM_2周期10K 占空比0
pwm_init(PWM_2, 17000, 0); //单片机端口D2 初始化PWM_1周期10K 占空比0
pwm_init(PWM_3, 17000, 0); //单片机端口D3 初始化PWM_2周期10K 占空比0
ips200_init(); //初始化IPS屏幕
//同一个ADC模块分辨率应该设置为一样的,如果设置不一样,则最后一个初始化时设置的分辨率生效
adc_init(POWER_ADC1_MOD,POWER_ADC1_PIN,ADC_8BIT);
adc_init(POWER_ADC2_MOD,POWER_ADC2_PIN,ADC_8BIT);
adc_init(POWER_ADC3_MOD,POWER_ADC3_PIN,ADC_8BIT);
adc_init(POWER_ADC4_MOD,POWER_ADC4_PIN,ADC_8BIT);
}
3. 电感数据采集与处理
//电感数据采集处理函数
int get_adc(void)
{
int i,j;
int ad_value[10], sum_value = 0;
int temp;
for(i=0;i<10;i++)
{
//采集电感值,巡线只使用了两侧电感,若需要进行元素识别则需要使用中间电感
ad_value1 = adc_mean_filter(POWER_ADC1_MOD,POWER_ADC1_PIN,10);
ad_value2 = adc_mean_filter(POWER_ADC2_MOD,POWER_ADC2_PIN,10);
ad_value3 = adc_mean_filter(POWER_ADC3_MOD,POWER_ADC3_PIN,10);
ad_value4 = adc_mean_filter(POWER_ADC4_MOD,POWER_ADC4_PIN,10);
//差比和算法
ad_value[i] = 100*(ad_value4 - ad_value1)/(ad_value4 + ad_value1);
}
for(j=0;j<9;j++) //冒泡排序
{
for(i=0;i<9-j;i++)
{
if(ad_value[i]>ad_value[i+1])
{
temp = ad_value[i];
ad_value[i] = ad_value[i+1];
ad_value[i+1] = temp;
}
}
}
for(i=1;i<9;i++)
{
sum_value += ad_value[i];
}
temp = sum_value/8;
return temp;
}
4. HIP电机驱动功能
//HIP电机驱动函数
void motor_ctr(int32 motor1, int32 motor2)
{
if(motor1 > 0)
{
pwm_duty(PWM_0, motor1);
pwm_duty(PWM_2, 0);
}
else
{
pwm_duty(PWM_0, 0);
pwm_duty(PWM_2, -motor1);
}
if(motor2 > 0)
{
pwm_duty(PWM_1, motor2);
pwm_duty(PWM_3, 0);
}
else
{
pwm_duty(PWM_1, 0);
pwm_duty(PWM_3, -motor2);
}
}
5、主要功能
int main(void)
{
DisableGlobalIRQ();
board_init(); //务必保留,本函数用于初始化MPU 时钟 调试串口
systick_delay_ms(300); //延时300ms,等待主板其他外设上电成功
Init_Proc(); //外设初始化
EnableGlobalIRQ(0); //使能
ips200_clear(WHITE); //显示屏清屏
//变量初始化
error = 0;
error_last = 0;
kp = 5; //舵机pd算法的pd值
kd = 30;
while(1)
{
ips_show();
duty = 3600; //中值3600,3180-4020(车轮从右到左) //3180,4020都没转到极限,不过3170 4030就会转到极限了
error_last = error;
error = get_adc(); //获取处理后的电感值
error_angle = kp*error + kd*(error-error_last); //舵机pd算法确定转角偏值
if(abs(error)<20)
{
error_angle=0;
}
duty = 3600 + error_angle;
if(duty > 4000) //舵机限幅,防止舵机打死
duty = 4000;
else if(duty < 3200)
duty = 3200;
pwm_duty(S_MOTOR1_PIN, duty); //舵机占空比设置
motor1 = 10000; //前两行电机配速,最后一行调用电机函数,发动电机
motor2 = 10000;
if((abs(ad_value1)<2) && (abs(ad_value4)<2)) //感应不到电磁后电机停止转动
{
motor1 = 0;
motor2 = 0;
}
motor_ctr(motor1, motor2);
}
}
附录:程序源码
#include "SEEKFREE_FONT.h"
#include "headfile.h"
//引脚定义区
#define POWER_ADC1_MOD ADC_1 //定义通道一 ADC模块号
#define POWER_ADC1_PIN ADC1_CH3_B14 //定义通道一 ADC引脚
#define POWER_ADC2_MOD ADC_1 //定义通道二 ADC模块号
#define POWER_ADC2_PIN ADC1_CH4_B15 //定义通道二 ADC引脚
#define POWER_ADC3_MOD ADC_1 //定义通道三 ADC模块号
#define POWER_ADC3_PIN ADC1_CH10_B21 //定义通道三 ADC引脚
#define POWER_ADC4_MOD ADC_1 //定义通道四 ADC模块号
#define POWER_ADC4_PIN ADC1_CH12_B23 //定义通道四 ADC引脚
#define S_MOTOR1_PIN PWM4_MODULE2_CHA_C30 //定义舵机引脚
#define PWM_0 PWM1_MODULE3_CHA_D0 //定义电机pwm信号引脚
#define PWM_1 PWM1_MODULE3_CHB_D1
#define PWM_2 PWM2_MODULE3_CHA_D2
#define PWM_3 PWM2_MODULE3_CHB_D3
//函数声明区
void Init_Proc(void); //初始化
int get_adc(void); //电感采集
void ips_show(void); //屏幕显示
void motor_ctr(int32 motor1, int32 motor2); //电机控制
//变量定义区
int32 duty; //舵机占空比
int32 motor1, motor2; //设置电机转速变量
float kp, kd; //舵机pid的p d参数
int level;
// 误差值
int16 error;
int16 error_last;
int16 error_angle; //舵机转角pd算法偏值
//四个电感值
int16 ad_value1;
int16 ad_value2;
int16 ad_value3;
int16 ad_value4;
int main(void)
{
DisableGlobalIRQ();
board_init(); //务必保留,本函数用于初始化MPU 时钟 调试串口
systick_delay_ms(300); //延时300ms,等待主板其他外设上电成功
Init_Proc(); //外设初始化
EnableGlobalIRQ(0); //使能
ips200_clear(WHITE); //显示屏清屏
//变量初始化
error = 0;
error_last = 0;
kp = 5; //舵机pd算法的pd值
kd = 30;
while(1)
{
ips_show();
duty = 3600; //中值3600,3180-4020(车轮从右到左) //3180,4020都没转到极限,不过3170 4030就会转到极限了
error_last = error;
error = get_adc(); //获取处理后的电感值
error_angle = kp*error + kd*(error-error_last); //舵机pd算法确定转角偏值
if(abs(error)<20)
{
error_angle=0;
}
duty = 3600 + error_angle;
if(duty > 4000) //舵机限幅,防止舵机打死
duty = 4000;
else if(duty < 3200)
duty = 3200;
pwm_duty(S_MOTOR1_PIN, duty); //舵机占空比设置
motor1 = 10000; //前两行电机配速,最后一行调用电机函数,发动电机
motor2 = 10000;
if((abs(ad_value1)<2) && (abs(ad_value4)<2)) //感应不到电磁后电机停止转动
{
motor1 = 0;
motor2 = 0;
}
motor_ctr(motor1, motor2);
}
}
//初始化函数
void Init_Proc(void)
{
pit_init(); //初始化pit外设
pit_interrupt_ms(PIT_CH0,5); //初始化pit通道0,中断时间为5ms
pwm_init(S_MOTOR1_PIN,50,3600); //舵机pwm引脚初始化
pwm_init(PWM_0, 17000, 0); //单片机端口D0 初始化PWM_1周期10K 占空比0 //电机pwm引脚初始化
pwm_init(PWM_1, 17000, 0); //单片机端口D1 初始化PWM_2周期10K 占空比0
pwm_init(PWM_2, 17000, 0); //单片机端口D2 初始化PWM_1周期10K 占空比0
pwm_init(PWM_3, 17000, 0); //单片机端口D3 初始化PWM_2周期10K 占空比0
ips200_init(); //初始化IPS屏幕
adc_init(POWER_ADC1_MOD,POWER_ADC1_PIN,ADC_8BIT); //同一个ADC模块分辨率应该设置为一样的,如果设置不一样,则最后一个初始化时设置的分辨率生效
adc_init(POWER_ADC2_MOD,POWER_ADC2_PIN,ADC_8BIT);
adc_init(POWER_ADC3_MOD,POWER_ADC3_PIN,ADC_8BIT);
adc_init(POWER_ADC4_MOD,POWER_ADC4_PIN,ADC_8BIT);
}
//电感数据采集处理函数
int get_adc(void)
{
int i,j;
int ad_value[10], sum_value = 0;
int temp;
for(i=0;i<10;i++)
{
//采集电感值,巡线只使用了两侧电感,若需要进行元素识别则需要使用中间电感
ad_value1 = adc_mean_filter(POWER_ADC1_MOD,POWER_ADC1_PIN,10);
ad_value2 = adc_mean_filter(POWER_ADC2_MOD,POWER_ADC2_PIN,10);
ad_value3 = adc_mean_filter(POWER_ADC3_MOD,POWER_ADC3_PIN,10);
ad_value4 = adc_mean_filter(POWER_ADC4_MOD,POWER_ADC4_PIN,10);
//差比和算法
ad_value[i] = 100*(ad_value4 - ad_value1)/(ad_value4 + ad_value1);
}
for(j=0;j<9;j++) //冒泡排序
{
for(i=0;i<9-j;i++)
{
if(ad_value[i]>ad_value[i+1])
{
temp = ad_value[i];
ad_value[i] = ad_value[i+1];
ad_value[i+1] = temp;
}
}
}
for(i=1;i<9;i++)
{
sum_value += ad_value[i];
}
temp = sum_value/8;
return temp;
}
//ips屏幕显示函数
void ips_show(void)
{
ips200_showstr(0,1,"ad_value1="); //显示屏显示电感值
ips200_showint16(80,1,ad_value1);
ips200_showstr(0,2,"ad_value2=");
ips200_showint16(80,2,ad_value2);
ips200_showstr(0,3,"ad_value3=");
ips200_showint16(80,3,ad_value3);
ips200_showstr(0,4,"ad_value4=");
ips200_showint16(80,4,ad_value4);
ips200_showstr(0,5,"error=");
ips200_showint16(80,5,error);
ips200_showstr(0,6,"error_angle=");
ips200_showint16(100,6,error_angle);
ips200_showstr(0,7,"duty="); //显示舵机转角
ips200_showint16(80,7,duty);
ips200_showstr(0,8,"motor1");
ips200_showint16(80,8,motor1);
ips200_showstr(0,9,"motor2");
ips200_showint16(80,9,motor2);
}
//HIP电机驱动函数
void motor_ctr(int32 motor1, int32 motor2)
{
if(motor1 > 0)
{
pwm_duty(PWM_0, motor1);
pwm_duty(PWM_2, 0);
}
else
{
pwm_duty(PWM_0, 0);
pwm_duty(PWM_2, -motor1);
}
if(motor2 > 0)
{
pwm_duty(PWM_1, motor2);
pwm_duty(PWM_3, 0);
}
else
{
pwm_duty(PWM_1, 0);
pwm_duty(PWM_3, -motor2);
}
}
写在最后:这篇文章是作者第一次做智能汽车的回顾。也是他第一次造车时遇到的一些难以理解的问题的整理。希望可以帮助第一次造车的同学入门。只要每个人都实现了从0到1的跨越,那么从1到100就是水到渠成的事情。
如果有错误或者疑问,欢迎私信与作者沟通更正。
参考博客:
【嵌入式·单片机】本文带你了解电机驱动模块
【电磁追踪】从0到1
PWM原理、PWM频率和占空比详解
智能汽车电感差值比及差值加权算法研究
我要评论