1. 主要内容

结合阿克曼运动需求,本团队设计了阿克曼运动系统框图如图 1,主要包括阿克曼小车仿真设计和阿克曼小车实物实现。
在这里插入图片描述
图 1 阿克曼运动系统框图

1.1 阿克曼小车仿真设计

本节先介绍阿克曼小车模型,小车仿真设计包括SolidWorks和gazebo三维建模。

1.1.1 阿克曼小车

阿克曼小车是一款经典的车模。小车模型后轮是通过电机直驱,前轮通过舵机控制前轮转向角,前轮部分则是在模型中添加一个竖直的关节,使前轮能围绕此关节转动。

1.1.2 SolidWorks三维模型

采用SolidWorks(2016版)设计软件搭建阿克曼小车三维模型如图 2图 3图 4,主体分为底板、电池、控制器、带编码电机的后轮、激光雷达、深度摄像头、阿克曼前轮转向7个部分如图 5。各部分零件都定义了材料属性,选择取小车底盘中心为模型的原点,配置了小车URDF文件见附件3。目前可以导入到gazebo环境中,可以实现小车的前进与后退,但转向无法实现,原因分析由于阿克曼转向结构属于空间四连杆结构(并联结构不支持)如图 6。
在这里插入图片描述

图 2 正等测
在这里插入图片描述

图 3 前视图
在这里插入图片描述

图 4 俯视图
在这里插入图片描述

图 5 爆炸视图

在这里插入图片描述

图 6 模型导入gazebo效果

1.1.3 gazebo三维建模

为了简化小车的运动,直接调入用阿克曼模型,简化小车的前后轮运动关系,搭配了里程计和摄像头,可以实现小车的转向、直行和后退。小车分别导入到gazebo和rviz效果分别如图 7图 8建模过程见附件1文档。
在这里插入图片描述

图 7 加载到gazebo
在这里插入图片描述

图 8 加载到rvzi

1.2 阿克曼小车实物实现

小车底层搭载STM32F103系列单片机,运动控制算法采用阿克曼算法,解析后分别驱动舵机和编码电机,可以通过串口通讯实现上下位机的人机交互,方便调试我们设计了PS2手柄控制模式。实现小车的实物制作如图 9。
在这里插入图片描述

图 9 小车实物

1.2.1控制器

小车控制器采用意法半导体STM32F103C6,是一款 ARM 32 位 Cortex-M3 微控制器,72MHz,32kB 闪存,10kB SRAM,PLL,嵌入式内部 RC 8MHz 和 32kHz,实时时钟,嵌套中断控制器,省电模式,JTAG 和 SWD,2 同步. 具有输入捕捉、输出比较和 PWM 的 16 位定时器、16 位 6 通道高级定时器、2 个 16 位看门狗定时器、SysTick 定时器、SPI、I2C、2 个 USART、USB 2.0 全速接口、CAN 2.0B 激活、 2 个 12 位 10 通道 A/D 转换器,快速 I/O 端口如图 10。整体资源满足小车需求,IO使用情况详细说明, STM32核心板和底板原理图见附件1。
在这里插入图片描述

图 10 STM32F103引脚定义图

1.2.2 阿克曼运动算法

阿克曼转向是一种现代汽车的转向方式,在汽车转弯的时候,内外轮转过的角度不一样,内侧轮胎转弯半径小于外侧轮胎。理想的阿克曼转向如图 11,而本车模型采用反向的阿克曼模型。
在这里插入图片描述

图 11 理想的阿克曼转向
根据阿克曼转向几何设计转向机构,在车辆沿着弯道转弯时,利用四连杆的相等曲柄,可以使内侧轮的转向角比外侧轮大大约 2~4度,使四个轮子路径的圆心大致上交会于后轴的延长线上瞬时转向中心,从而让车辆可以顺畅的转弯。阿克曼核心公式如下:





cot





β





cot





α


=



K


T






1






\cot{\beta}-\cot{\alpha}=\frac{K}{T} (1)


cotβcotα=TK1

式中:β一汽车前外轮转角,a —汽车前内轮转角,K一两主销中心距,L一轴距如图 12。具体实现见附件2中control.c中Kinematic_Analysis函数。
在这里插入图片描述

图 12 阿克曼数学模型
control.c

#include “control.h”    

//#define T 0.245f
//#define L 0.29f
//#define K 14.00f
#define T 0.156f
#define L 0.1445f
#define K 622.8f
u8 Flag_Target,Flag_Change; //相关标志位
//float Voltage_Temp,Voltage_Count,Voltage_All; //电压采样相关变量
int j,sum;
/**
函数功能:小车运动数学模型
入口参数:速度和转角
返回 值:无
**/

void Kinematic_Analysis(float velocity,float angle)
{
Target_A=velocity_(1+T_tan(angle)/2/L);
Target_B=velocity_(1-T_tan(angle)/2/L); //后轮差速
Servo=SERVO_INIT+angle_K; //舵机转向
}
/**
函数功能:所有的控制代码都在这里面
定时中断触发
严格保证采样和数据处理的时间同步
**/

void Control(void)
{
oled_show(); //显示屏打开
Encoder_Left=Read_Encoder(2);
Encoder_Right=-Read_Encoder(3); //读取左右编码器
delay_ms(50); //=====延时等待稳定
if(Turn_Off(Voltage)==0&&Flag_Way==0)
{
jiexi();
Kinematic_Analysis(Velocity,Angle); //小车运动学分析
Motor_A=Target_A_20; //===计算电机A最终PWM
Motor_B=Target_B_20; //===计算电机B最终PWM
Xianfu_Pwm(); //===PWM限幅
Set_Pwm(Motor_A,Motor_B,Servo); //===赋值给PWM寄存器

}
else if(Turn_Off(Voltage)==0&&Flag_Way==1) //===如果不存在异常
{
Get_RC();
Kinematic_Analysis(Velocity,Angle); //小车运动学分析
Motor_A=Incremental_PI_Left(Encoder_Left,Target_A); //===速度闭环控制计算电机A最终PWM
Motor_B=Incremental_PI_Right(Encoder_Right,Target_B); //===速度闭环控制计算电机B最终PWM
Xianfu_Pwm(); //===PWM限幅
Set_Pwm(Motor_A,Motor_B,Servo); //===赋值给PWM寄存器
}
else Set_Pwm(0,0,SERVO_INIT); //===赋值给PWM寄存器
Voltage_Temp=Get_battery_volt(); //=====读取电池电压
Voltage_Count++; //=====平均值计数器
Voltage_All+=Voltage_Temp; //=====多次采样累积
if(Voltage_Count==10) Voltage=Voltage_All/10,Voltage_All=0,Voltage_Count=0;//=====求平均值
if(Flag_Show==0) Led_Flash(100);
else if(Flag_Show==1) Led_Flash(0); //led闪烁
Key(); //===扫描按键状态 单击双击可以改变小车运行状态

}

/**
函数功能:赋值给PWM寄存器
入口参数:PWM
返回 值:无
**/

void Set_Pwm(int motor_a,int motor_b,int servo)
{
if(motor_a<0) PWMA2=7200,PWMA1=7200+motor_a;
else PWMA1=7200,PWMA2=7200-motor_a;

if(motor_b<0) PWMB1=7200,PWMB2=7200+motor_b;
else PWMB2=7200,PWMB1=7200-motor_b;
SERVO=servo;
}
/**
函数功能:限制PWM赋值
入口参数:幅值
返回 值:无
**/

void Xianfu_Pwm(void)
{
int Amplitude=6900; //===PWM满幅是7200 限制在6900
if(Motor_A<-Amplitude) Motor_A=-Amplitude;
if(Motor_A>Amplitude) Motor_A=Amplitude;
if(Motor_B<-Amplitude) Motor_B=-Amplitude;
if(Motor_B>Amplitude) Motor_B=Amplitude;
if(Servo<(SERVO_INIT-500)) Servo=SERVO_INIT-500; //舵机限幅
if(Servo>(SERVO_INIT+500)) Servo=SERVO_INIT+500; //舵机限幅
}
/_**_
函数功能:按键修改小车运行状态
入口参数:无
返回 值:无
**/

void Key(void)
{
u8 tmp,tmp2;
tmp=click();
// tmp=click_N_Double(50); //双击,双击等待时间500ms
if(tmp==1)Flag_Stop=!Flag_Stop;//单击控制小车的启停
//if(tmp==2)Flag_Show=!Flag_Show;//双击控制小车的显示状态
tmp2=Long_Press(); //长按
if(tmp2==1)Flag_Show=!Flag_Show;//控制小车的显示状态
}
/**
函数功能:异常关闭电机
入口参数:电压
返回 值:1:异常 0:正常
**/

u8 Turn_Off( int voltage)
{
u8 temp;
if(voltage<740||Flag_Stop==1)//电池电压低于11.1V关闭电机
{
temp=1;
PWMA1=0; //电机控制位清零
PWMB1=0; //电机控制位清零
PWMA2=0; //电机控制位清零
PWMB2=0; //电机控制位清零
}
else
temp=0;
return temp;
}

/**
函数功能:绝对值函数
入口参数:int
返回 值:unsigned int
**/

int myabs(int a)
{
int temp;
if(a<0) temp=-a;
else temp=a;
return temp;
}
/**
函数功能:增量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_Left (int Encoder,int Target)
{
static int Bias,Pwm,Last_bias;
Bias=Target-Encoder; //计算偏差
Pwm+=Velocity_KP_(Bias-Last_bias)+Velocity_KI_Bias; //增量式PI控制器
Last_bias=Bias; //保存上一次偏差
return Pwm; //增量输出
}
int Incremental_PI_Right (int Encoder,int Target)
{
static int Bias,Pwm,Last_bias;
Bias=Target-Encoder; //计算偏差
Pwm+=Velocity_KP_(Bias-Last_bias)+Velocity_KI_Bias; //增量式PI控制器
Last_bias=Bias; //保存上一次偏差
return Pwm; //增量输出
}
/**
函数功能:通过指令对小车进行遥控
入口参数:PS2指令
返回 值:无
**/

void Get_RC(void)//PS2控制
{
int Yuzhi=2;
float LY,RX;
LY=PS2_LY-128; //计算偏差
RX=PS2_RX-128;
if(LY>-Yuzhi&&LY<Yuzhi)LY=0; //小角度设为死区 防止抖动出现异常
if(RX>-Yuzhi&&RX<Yuzhi)RX=0;
Velocity=-LY/2; //速度和摇杆的力度相关。
Angle=RX/200;

}



1.2.3 舵机驱动

舵机的控制由一个脉冲宽度调制信号(PWM)来实现,该信号由 stm32发出。通常来说,舵机的控制信号周期为 20ms的脉宽调制信号,其中脉冲宽度从 0.5-2.5对应舵盘位置的 0-180度如图 13,呈线性变化,也就是说给它提供一定的脉宽,它的输出轴就会保持在一定对应角度上,无论外界力矩怎么改变,直到给它提供另外宽度的脉冲信号,它才会改变输出角度到新的位置上。
在这里插入图片描述

图 13 舵机输出转角与输入信号脉冲宽度的关系
采用DSServo达盛金属舵机如图 14,型号为 DS3230,适用于航模、车模、船模及机器人的小型舵机,额定扭矩 3N.m,转动角度 270度。具体定义见附件2中motor.c,功能实现见control.c中Kinematic_Analysis函数。
在这里插入图片描述

图 14 达盛金属舵机
motor.c

#include “motor.h”

void Motor_PWM_Init(u16 arr,u16 psc)
{

RCC->APB1ENR|=1<<2; //TIM4时钟使能
RCC->APB2ENR|=1<<3; //PORTB时钟使能
GPIOB->CRL&=0X00FFFFFF; //PORTB6 7 8 9推挽输出
GPIOB->CRL|=0XBB000000; //PORTB6 7 8 9推挽输出
GPIOB->CRH&=0XFFFFFF00; //PORTB6 7 8 9推挽输出
GPIOB->CRH|=0X000000BB; //PORTB6 7 8 9推挽输出
TIM4->ARR=arr;//设定计数器自动重装值
TIM4->PSC=psc;//预分频器不分频
TIM4->CCMR1|=6<<4;//CH1 PWM1模式
TIM4->CCMR1|=6<<12; //CH2 PWM1模式
TIM4->CCMR2|=6<<4;//CH3 PWM1模式
TIM4->CCMR2|=6<<12; //CH4 PWM1模式

TIM4->CCMR1|=1<<3; //CH1预装载使能
TIM4->CCMR1|=1<<11;//CH2预装载使能
TIM4->CCMR2|=1<<3; //CH3预装载使能
TIM4->CCMR2|=1<<11;//CH4预装载使能
TIM4->CCER|=1<<0; //CH1输出使能
TIM4->CCER|=1<<4; //CH2输出使能
TIM4->CCER|=1<<8; //CH3输出使能
TIM4->CCER|=1<<12; //CH4输出使能
TIM4->CR1=0x80; //ARPE使能
TIM4->CR1|=0x01; //使能定时器

}

/_**__ *
函数功能:舵机PWM以及定时中断初始化
入口参数:入口参数:arr:自动重装值 psc:时钟预分频数
返回 值:无
**/

void Servo_PWM_Init(u16 arr,u16 psc)
{

RCC->APB2ENR|=1<<11; //使能TIM1时钟
RCC->APB2ENR|=1<<2; //PORTA时钟使能
GPIOA->CRH&=0XFFFF0FFF; //PORTA11复用输出
GPIOA->CRH|=0X0000B000; //PORTA11复用输出
TIM1->ARR=arr; //设定计数器自动重装值
TIM1->PSC=psc; //预分频器不分频
TIM1->CCMR2|=6<<12; //CH4 PWM1模式
TIM1->CCMR2|=1<<11; //CH4预装载使能
TIM1->CCER|=1<<12; //CH4输出使能
TIM1->BDTR |= 1<<15; //TIM1必须要这句话才能输出PWM
TIM1->CR1 = 0x80; //ARPE使能
TIM1->DIER|=1<<0; //允许更新中断
TIM1->CR1|=0x01; //使能定时器1
TIM1->CCR4=1500;
// MY_NVIC_Init(1,1,TIM1_UP_IRQn,2);
}

1.2.4 编码电机驱动

直流减速电机接口方式如图 15,是一个带编码器的直流减速电机,编码器的作用是测速。一般包括六个接线端子,电机电源输入 M1和电机电源输入 M2是直流电机引脚,电机的旋转和速度调节只需这两个引脚即可。剩下中间的四个引脚是编码器,。
在这里插入图片描述

图 15 编码电机
STM32F103的高级控制定时器 TIM1和 TIM8在基本定时器的基础上引入了外部引脚,可以输入捕获和输出比较功能。高级定时器能够完成输入捕获和输出比较的功能,输出比较包括翻转、强制为有效电平、PWM1和 PWM2等模式,其中 PWM模式时最常用的。电机采用定时器TIM8,两路编码器分别采用定时器TIM2和TIM3。 电机具体定义见附件2中motor.c,编码器具体定义见附件2中encoder.c,功能实现见control.c中Kinematic_Analysis函数。
encoder.c

#include “encoder.h”
#include “stm32f10x_gpio.h”

/**
函数功能:把TIM2初始化为编码器接口模式
入口参数:无
返回 值:无
**/

void Encoder_Init_TIM2(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);// 需要使能AFIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能定时器2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PA端口时钟
//RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能PB端口时钟

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOA

TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器
TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM向上计数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_ICFilter = 10;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);//清除TIM的更新标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//Reset counter
TIM_SetCounter(TIM2,0);
TIM_Cmd(TIM2, ENABLE);

}

/**
函数功能:把TIM3初始化为编码器接口模式
入口参数:无
返回 值:无
**/


void Encoder_Init_TIM3(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//使能定时器3的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PB端口时钟

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOA

TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器
TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM向上计数
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_ICFilter = 10;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ClearFlag(TIM3, TIM_FLAG_Update);//清除TIM的更新标志位
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
//Reset counter
TIM_SetCounter(TIM3,0);
TIM_Cmd(TIM3, ENABLE);

}

/**
函数功能:单位时间读取编码器计数
入口参数:定时器
返回 值:速度值
**/

int Read_Encoder(u8 TIMX)
{
int Encoder_TIM;
switch(TIMX)
{
case 2: Encoder_TIM= (short)TIM2 -> CNT; TIM2 -> CNT=0;break;
case 3: Encoder_TIM= (short)TIM3 -> CNT; TIM3 -> CNT=0;break;
default: Encoder_TIM=0;
}
return Encoder_TIM;
}
/**
函数功能:TIM3中断服务函数
入口参数:无
返回 值:无
**/

void TIM3_IRQHandler(void)
{
if(TIM3->SR&0X0001)//溢出中断
{
}
TIM3->SR&=~(1<<0);//清除中断标志位
}
/**
函数功能:TIM2中断服务函数
入口参数:无
返回 值:无
**/

void TIM2_IRQHandler(void)
{
if(TIM2->SR&0X0001)//溢出中断
{
}
TIM2->SR&=~(1<<0);//清除中断标志位
}


1.2.5 串口通讯

小车采用有线的串口与电脑通讯来模拟驱动板通过串口与工控机 NANO板完成信息交互,使用STM32F103串口 1实现数据的传输,对应输出引脚为 TXD—PA10,RXD–PA9,编写中驱动板通过串口与工控机 NANO板完成信息交互,通过轮询的方式将驱动板采集到的传感器数据等发送给工控机,通过中断的方式接收工控机发来的串口控制量从而完成小车的运动控制。通过CH340G芯片完成电平转换,CH340是一个 USB总线的转换芯片,实现 USB转串口,CH340G芯片中的 TXD和 RXD轻舟机器人学习教程与 STM32F103的 USART1_RXD和 USART1_TXD相连接。编程要点为:首先配置串口参数及中断优先级,然后编写发送函数,最后使能接收中断。具体定义见附件2中usart.c,功能实现见DataScope_DP.c中USART_TX和jiexi函数。
usart.c

#include “usart.h”      
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(use_no_semihosting)
//标准库需要的支持函数
struct
FILE
{
int handle;
/_ Whatever you require here. If the only file you are using is _/
/_ standard output using printf() for debugging, no file handling _/
/_ is required. _/
};
/_ FILE is typedef’ d in stdio.h. _/
FILE stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE _f)
{

while((USART3->SR&0X40)==0);
USART3->DR = (u8) ch;
return ch;
}
#endif
u8 Usart3_Receive=0X5A;
void usart1_init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟

//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9

//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//USART 初始化设置
//UsartNVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器

USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式

USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1

}

/**
函数功能:串口1接收中断
入口参数:无
返回 值:无
**/

int USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收到数据
{
u8 temp;
static u8 count,last_data,last_last_data,Usart_ON_Count;
if(Usart_ON_Flag==0)
{
if(++Usart_ON_Count>10)Usart_ON_Flag=1;
}
temp=USART1->DR;
if(Usart_Flag==0)
{
if(last_data==0x5a&&last_last_data==0xa5)
Usart_Flag=1,count=0;
}
if(Usart_Flag==1)
{
Urxbuf[count]=temp;
count++;
if(count==8)Usart_Flag=0;
}
last_last_data=last_data;
last_data=temp;
}
return 0;
}
//
/**实现函数** _功 能: usart3发送一个字节
*/

void usart3_send(u8 data)
{
USART3->DR = data;
while((USART3->SR&0x40)==0);
}
/**
函数功能:串口3初始化
入口参数: bound:波特率
返回 值:无
**/

void usart3_init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);// 需要使能AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); //使能GPIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); //使能USART时钟
GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE);//引脚重映射
//USART_TX
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //C10
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOC, &GPIO_InitStructure);
//USART_RX
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//PC11
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOC, &GPIO_InitStructure);
//UsartNVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART3, &USART_InitStructure); //初始化串口3
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART3, ENABLE); //使能串口3
}

/**
函数功能:串口3接收中断
入口参数:无
返回 值:无
**/

void USART3_IRQHandler(void)
{
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) //接收到数据
{
static u8 Flag_PID,i,j,Receive[50];
static float Data;
Usart3_Receive=USART3->DR;
if(Usart3_Receive>=0x41&&Usart3_Receive<=0x48)
Flag_Direction=Usart3_Receive-0x40;
else
Flag_Direction=0;

//以下是与APP调试界面通讯
if(Usart3_Receive==0x7B) Flag_PID=1; //APP参数指令起始位
if(Usart3_Receive==0x7D) Flag_PID=2; //APP参数指令停止位

if(Flag_PID==1) //采集数据
{
Receive[i]=Usart3_Receive;
i++;
}
if(Flag_PID==2) //分析数据
{
if(Receive[3]==0x50) PID_Send=1;
else if(Receive[1]!=0x23)
{
for(j=i;j>=4;j)
{
Data+=(Receive[j-1]-48)_pow(10,i-j);
}
switch(Receive[1])
{
case 0x30: RC_Velocity=Data;break;
case 0x31: Velocity_KP=Data;break;
case 0x32: Velocity_KI=Data;break;
case 0x33: break;
case 0x34: break;
case 0x35: break;
case 0x36: break;
case 0x37: break; //预留
case 0x38: break; //预留
}
}
Flag_PID=0;//相关标志位清零
i=0;
j=0;
Data=0;
memset(Receive, 0, sizeof(u8)_50);//数组清零
}
}
}



DataScope_DP.c



#include “DataScope_DP.h”

unsigned char DataScope_OutPut_Buffer[42] = {0}; //串口发送缓冲区

int send_cnt = 0;
static u8 Send_rasberry[60];
int re_Encoder_Left,re_Encoder_Right;
int Distance_A,Distance_B,Distance_C,Distance_D;
u8 Urxbuf[8],Usart_Flag,x=0;
short accelX,accelY,accelZ,gyroX,gyroY,gyroZ,magX,magY,magZ;
void USART_TX(void)
{
Send_rasberry[0] = 0xA5; // 数据头,固定值
Send_rasberry[1] = 0x5A; // 数据头,固定值
Send_rasberry[2] = 0x33; // 发送数据的长度
re_Encoder_Left = -Encoder_Left;
re_Encoder_Right = -Encoder_Right;
for(send_cnt=0; send_cnt<4; send_cnt++) //左编码器增量值
{
Send_rasberry[3+send_cnt] = ((unsigned char _)&re_Encoder_Left)[send_cnt];
}
for(send_cnt=0; send_cnt<4; send_cnt++) //右编码器增量值
{
Send_rasberry[7+send_cnt] = ((unsigned char _)&re_Encoder_Right)[send_cnt];
}
for(send_cnt=0; send_cnt<4; send_cnt++) //电池电压采样
{
Send_rasberry[11+send_cnt] = ((unsigned char _)&Voltage)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // X 轴加速度计值
{
Send_rasberry[15+send_cnt] = ((unsigned char _)&accelX)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // Y 轴加速度计值
{
Send_rasberry[17+send_cnt] = ((unsigned char _)&accelY)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // Y 轴加速度计值
{
Send_rasberry[19+send_cnt] = ((unsigned char _)&accelZ)[send_cnt];
}
//send gyro X Y Z
for(send_cnt=0; send_cnt<2; send_cnt++) // X 轴角速度值
{
Send_rasberry[21+send_cnt] = ((unsigned char _)&gyroX)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // Y 轴角速度值
{
Send_rasberry[23+send_cnt] = ((unsigned char _)&gyroY)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // Z 轴角速度值
{
Send_rasberry[25+send_cnt] = ((unsigned char _)&gyroZ)[send_cnt];
}
//send MAG X Y Z
for(send_cnt=0; send_cnt<2; send_cnt++) // X 轴磁力计值
{
Send_rasberry[27+send_cnt] = ((unsigned char _)&magX)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // Y 轴磁力计值
{
Send_rasberry[29+send_cnt] = ((unsigned char _)&magY)[send_cnt];
}
for(send_cnt=0; send_cnt<2; send_cnt++) // Z 轴磁力计值
{
Send_rasberry[31+send_cnt] = ((unsigned char _)&magZ)[send_cnt];
}
//send ultrasonic A B C D
for(send_cnt=0; send_cnt<4; send_cnt++) // 超测量距离值 A
{
Send_rasberry[33+send_cnt] = ((unsigned char _)&Distance_A)[send_cnt];
}
for(send_cnt=0; send_cnt<4; send_cnt++) // 超测量距离值 B
{
Send_rasberry[37+send_cnt] = ((unsigned char _)&Distance_B)[send_cnt];
}
for(send_cnt=0; send_cnt<4; send_cnt++) // 超测量距离值 C
{
Send_rasberry[41+send_cnt] = ((unsigned char _)&Distance_C)[send_cnt];
}
for(send_cnt=0; send_cnt<4; send_cnt++) // 超测量距离值 D
{
Send_rasberry[45+send_cnt] = ((unsigned char _)&Distance_D)[send_cnt];
}
Send_rasberry[50]=x;
x++;
//send Send_rasberry
UartASendStr(Send_rasberry,51);
memset(Send_rasberry, 0, sizeof(u8)_51); //数组清零
}

void UartASendStr (u8 _pucStr, u8 ulNum)
{
u8 i;
for(i = 0;i<ulNum;i++)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
USART_SendData(USART1,_pucStr++);
}
}

void ACK(void)
{
u8 data[6];
data[0]=0x0c;
data[1]=0xAA;
data[2]=0x00;
data[3]=0xFF;
data[4]=0x0F;
data[5]=x;
x++;
UartASendStr(data,6);
}

void jiexi(void)
{
float StrAngle,Speed;
TargetAngleDirection=Urxbuf[1];
TargetStrAngle=Urxbuf[2];//0~255
TargetSpeed=Urxbuf[3];//0~255
TargetModeSelect=Urxbuf[4];
TargetShiftPosition=Urxbuf[5];//控制参数
Sum=Urxbuf[7];
if(Sum==0x92)
{
if(TargetModeSelect==0x00)
{
StrAngle=TargetStrAngle_1-128; //计算偏差
Speed=TargetSpeed_1-128;
}
else if(TargetModeSelect==0x01)
{
switch(TargetAngleDirection)
{
case 0x00: StrAngle=0,Speed=80;break;
case 0x10: StrAngle=60,Speed=90;break;
case 0x20: StrAngle=-60,Speed=90;break;
default: StrAngle=0,Speed=0;break;
}
}
Velocity=Speed/2; //速度和角度。
Angle=StrAngle/200;
Sum=0x00;
UartASendStr(Urxbuf,8);
memset(Urxbuf, 0, sizeof(u8)_8); //数组清零
}
// printf(“respond:”);
// UartASendStr(Urxbuf,8);

}



1.2.6 PS2通讯

为了方便调试,设计了PS2手柄控制。PS2采用的是SPI通信协议如图 16,SPI是串行外设接口的缩写,是一种高速的、全双工、同步的通信总线,并且在芯片的管脚上只占用四根线(DI、DO、CS、CLK),节约了芯片的管脚,同时为PCB的布局上节省空间。具体定义见附件2中pstwo.c,功能实现见control.c中Get_RC函数。
在这里插入图片描述

图 16 PS2无线手柄

pstwo.c

#include “pstwo.h”

#define DELAY_TIME delay_us(5);
u16 Handkey; // 按键值读取,零时存储。
u8 Comd[2]={0x01,0x42}; //开始命令。请求数据
u8 Data[9]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; //数据存储数组
u16 MASK[]={
PSB_SELECT,
PSB_L3,
PSB_R3 ,
PSB_START,
PSB_PAD_UP,
PSB_PAD_RIGHT,
PSB_PAD_DOWN,
PSB_PAD_LEFT,
PSB_L2,
PSB_R2,
PSB_L1,
PSB_R1 ,
PSB_GREEN,
PSB_RED,
PSB_BLUE,
PSB_PINK
}; //按键值与按键明
//c2改成b15 c1改成b14 c3改成c13 a4改成a12

void PS2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC, ENABLE); //使能端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_Init(GPIOC, &GPIO_InitStructure); //根据设定参数初始化GPIO

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14|GPIO_Pin_15; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //50M
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB

GPIO_InitStructure.GPIO_Pin =GPIO_Pin_12; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //50M
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOA


}

//向手柄发送命令
void PS2_Cmd(u8 CMD)
{
volatile u16 ref=0x01;
Data[1] = 0;
for(ref=0x01;ref<0x0100;ref<<=1)
{
if(ref&CMD)
{
DO_H; //输出一位控制位
}
else DO_L;

CLK_H; //时钟拉高
DELAY_TIME;
CLK_L;
DELAY_TIME;
CLK_H;
if(DI)
Data[1] = ref|Data[1];
}
delay_us(16);
}
//判断是否为红灯模式,0x41=模拟绿灯,0x73=模拟红灯
//返回值;0,红灯模式
// 其他,其他模式
u8 PS2_RedLight(void)
{
CS_L;
PS2_Cmd(Comd[0]); //开始命令
PS2_Cmd(Comd[1]); //请求数据
CS_H;
if( Data[1] == 0X73) return 0 ;
else return 1;

}
//读取手柄数据
void PS2_ReadData(void)
{
volatile u8 byte=0;
volatile u16 ref=0x01;
CS_L;
PS2_Cmd(Comd[0]); //开始命令
PS2_Cmd(Comd[1]); //请求数据
for(byte=2;byte<9;byte++) //开始接受数据
{
for(ref=0x01;ref<0x100;ref<<=1)
{
CLK_H;
DELAY_TIME;
CLK_L;
DELAY_TIME;
CLK_H;
if(DI)
Data[byte] = ref|Data[byte];
}
delay_us(16);
}
CS_H;
}

//对读出来的PS2的数据进行处理,只处理按键部分
//只有一个按键按下时按下为0, 未按下为1
u8 PS2_DataKey()
{
u8 index;

PS2_ClearData();
PS2_ReadData();

Handkey=(Data[4]<<8)|Data[3]; //这是16个按键 按下为0, 未按下为1
for(index=0;index<16;index++)
{
if((Handkey&(1<<(MASK[index]-1)))==0)
return index+1;
}
return 0; //没有任何按键按下
}

//得到一个摇杆的模拟量 范围0~256
u8 PS2_AnologData(u8 button)
{
return Data[button];
}

//清除数据缓冲区
void PS2_ClearData()
{
u8 a;
for(a=0;a<9;a++)
Data[a]=0x00;
}
/**
Function: void PS2_Vibration(u8 motor1, u8 motor2)
Description: 手柄震动函数,
Calls: void PS2_Cmd(u8 CMD);
Input: motor1:右侧小震动电机 0x00关,其他开
motor2:左侧大震动电机 0x40~0xFF 电机开,值越大 震动越大
**/

void PS2_Vibration(u8 motor1, u8 motor2)
{
CS_L;
delay_us(16);
PS2_Cmd(0x01); //开始命令
PS2_Cmd(0x42); //请求数据
PS2_Cmd(0X00);
PS2_Cmd(motor1);
PS2_Cmd(motor2);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
CS_H;
delay_us(16);
}
//short poll
void PS2_ShortPoll(void)
{
CS_L;
delay_us(16);
PS2_Cmd(0x01);
PS2_Cmd(0x42);
PS2_Cmd(0X00);
PS2_Cmd(0x00);
PS2_Cmd(0x00);
CS_H;
delay_us(16);
}
//进入配置
void PS2_EnterConfing(void)
{
CS_L;
delay_us(16);
PS2_Cmd(0x01);
PS2_Cmd(0x43);
PS2_Cmd(0X00);
PS2_Cmd(0x01);
PS2_Cmd(0x00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
CS_H;
delay_us(16);
}
//发送模式设置
void PS2_TurnOnAnalogMode(void)
{
CS_L;
PS2_Cmd(0x01);
PS2_Cmd(0x44);
PS2_Cmd(0X00);
PS2_Cmd(0x01); //analog=0x01;digital=0x00 软件设置发送模式
PS2_Cmd(0x03); //Ox03锁存设置,即不可通过按键“MODE”设置模式。
//0xEE不锁存软件设置,可通过按键“MODE”设置模式。
PS2_Cmd(0X00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
PS2_Cmd(0X00);
CS_H;
delay_us(16);
}
//振动设置
void PS2_VibrationMode(void)
{
CS_L;
delay_us(16);
PS2_Cmd(0x01);
PS2_Cmd(0x4D);
PS2_Cmd(0X00);
PS2_Cmd(0x00);
PS2_Cmd(0X01);
CS_H;
delay_us(16);
}
//完成并保存配置
void PS2_ExitConfing(void)
{
CS_L;
delay_us(16);
PS2_Cmd(0x01);
PS2_Cmd(0x43);
PS2_Cmd(0X00);
PS2_Cmd(0x00);
PS2_Cmd(0x5A);
PS2_Cmd(0x5A);
PS2_Cmd(0x5A);
PS2_Cmd(0x5A);
PS2_Cmd(0x5A);
CS_H;
delay_us(16);
}
//手柄配置初始化
void PS2_SetInit(void)
{
PS2_ShortPoll();
PS2_ShortPoll();
PS2_ShortPoll();
PS2_EnterConfing(); //进入配置模式
PS2_TurnOnAnalogMode(); //“红绿灯”配置模式,并选择是否保存
//PS2_VibrationMode(); //开启震动模式
PS2_ExitConfing(); //完成并保存配置
}

<a id=”21087”>2 设计思路和流程

小车通过前期的仿真设计,结合实验室材料情况,搭建的小车实物。为了方便演示串口通讯和PS二手柄控制,对此设计了控制程序流程图如图 17,来引导小车控制程序的设计。
在这里插入图片描述

图 17 控制程序流程图
控制程序流程图理解如下,首先初始化小车,把电机舵机进行归零处理,定义对应的传感器,初始化OLED 屏幕。显示屏会提示模式选择,模式一是串口控制模式,点击确认后,小车会自动上传小车状态信息数据给上位机,上位机可以发送指令小车接收后会执行对应的运动控制,会进行一次判断小车是否异常,如果正常会进行二次判断小车是否需要结束运动,那么小车与上位机进行正常的人机交互,通过串口助手和单片机通讯并下发电机运动指令;片机接收串口助手的指令并驱动电机的正反转和差速控制;单片机接收串口助手的指令控制舵机和电机基于阿克曼运动算法的转动。如果是最后会初始化小车归零结束程序。模式二是PS2控制模式,会通过显示屏显示小车的运动数据,用户可以通过PS二手柄进行遥控小车,同样会进行二次判断,当用户发出结束小车运动指令时,小车会归零,最终结束程序。小车完整程序见附件2中轻舟驱动板demo-OLED8.0

2.1 串口控制模式

串口控制和PS2控制如图 18。在串口控制模式中,首先调用函数,接下来显示屏打开,读取左右编码器,延时消除不稳定。接下来进行数据解析、卡尔曼算法、限制pwm幅、输出对应的电机和舵机。最后电机电压显示和单机单双击可以改变小车的运行状态,以此循环往复,实现人机交互。具体串口协议见附件3中阿克曼运动串口协议。
在这里插入图片描述

图 18 串口和PS2控制

2.2 PS2控制模式

PS2控制模式中与串口控制模式有不同的在于PS2数据解析,其他与串口通讯一致。

3 设计效果与呈现

本节综合前面阿克曼小车的仿真设计,实物设计和程序设计。最后到了实验设计效果展示的环节,本环节由串口控制演示和PS2演示控制两部分组成。具体的演示见附件4的演示视频。

3.1 串口控制演示

通过安卓数据线连接小车串口1,。本次串口控制演示采用的是友善串口助手如图 19,串口设置端口连接com8,波特率115200,数据为八位,无校验位,一位停止位,无流控,接收设置采用hex自动换行显示发送和显示时间,接收采用hex。串口收发控制如图 20图 21。
在这里插入图片描述

图 19串口设置
在这里插入图片描述

图 20 左转控制
在这里插入图片描述

图 21 停止控制

3.2 PS2控制演示

在PS2控制中,首先打开手柄的开关电源,然后按一下start开始控制小车。通过摇动左右摇杆实现一个左右前进控制,其中左摇杆前进是油门儿,右摇杆是控制舵机转向。PS2控制操作如图 22。
在这里插入图片描述

图 22 PS2控制操作

演示视频:



阿克曼视频演示

4 创新与优化

在完成本技术要点的三个方面前提下,还在两方面提出了创新,对小车分析了不足,并对小车的研究进行展望。

4.1 创新点

设计了两种阿克曼小车模型,SolidWorks模型特征齐全,可以实现前进和后退;gazebo模型可以正常参与后面的仿真实验。
设计了PS2手柄控制模式,能有效验证实验方案,方便串口调试,增加用户的体验感。

    4.2 不足

    本次串口通讯采用的是有线串口,对测试条件有点不便,应该采用蓝牙或者WiFi串口通讯更加的方便和直观。

      4.3 研究展望

      串口接收函数实现,通过串口中断接收工控机发来的控制命令。
      在ROS环境下完成相关阿克曼运动控制。

        对应文档