目录
目录
摘要
基于stm32的智能小车设计,通过超声波模块检测障碍物,光电管辅助避障,采用红外对管进行循迹,小车每次转向都会触发LED转向指示灯和蜂鸣器报警,按键用于切换小车模式,蓝牙模块用于无线遥控,OLED显示超声波测量距离和实时时钟。
一、设计主要解决的问题
1、实现2m的自动直线行驶,要求左右偏移不大于10cm。
2、车载显示屏显示车辆当前行驶速度或车辆运行时间,当前方20cm内出现障碍物时显示与前方障碍物的距离,车每次转弯蜂鸣器报警,同时通过左右指示灯指示转弯方向。
3、依次驶过障碍物①②③④,且不触碰障碍物。
4、成功进行调头转弯,行驶过程中车轮不碾压黑实线。
5、在车道指定位置停车且不越界。
6、通过红外、蓝牙、WiFi等任意一种方式实现对小车的控制,使其前进、后退、左转、右转。
二、赛道图
1、避障赛道
2、循迹赛道
三、系统框图
选用STM32F103C8T6作为主控处理器,通过超声波模块检测障碍物,光电管辅助避障,采用红外对管进行循迹,小车每次转向都会触发LED转向指示灯和蜂鸣器报警,按键用于切换小车模式,蓝牙模块用于无线遥控,OLED显示超声波测量距离和实时时钟。
四、硬件选型
1、主控处理器
stm32f103c8t6最小系统板:
原理图:
引脚定义图:
2、超声波模块
HC-SR04超声波模块
供电:5V
原理图:
测距原理:
(1)采用IO触发测距,给至少10us的高电平信号,实际40-50uS效果好。
(2)模块自动发送8个40khz的方波。
(3)有信号返回,通过IO输出一高电平,高电平持续的时间就是超声波从发射到返回的时间。
(4)测试距离=(高电平时间*声速(340M/S))/2。
(5)有2厘米测距盲区。
3、光电管
E18-D80NK
接线: 棕:正 蓝:负 黑:信号 供电:5V 感应距离:3-80cm
特性:无遮挡时输出高电平,灯灭;有遮挡时输出低电平,灯亮。
4、循迹模块
TCRT5000红外对管
供电:3.3-5V 检测距离:1mm-25mm
特性:检测到黑色(红外被吸收)或未接收到反射为高电平,灯灭;正常反射为低电平,灯亮。
DO:数字输出 AO:模拟输出
5、显示模块
0.96寸OLED四针脚IIC协议
供电:3.3V
6、蜂鸣器模块
有源蜂鸣器
供电:3.3-5V
注意事项:有源蜂鸣器通电给触发电平即可响,无源蜂鸣器无法直接通过高低电平控制,需要引脚输出PWM波,蜂鸣器才可以发声,并且发声频率可以由PWM频率去改变。另外,经本人测试,采用下图三极管结构的有源蜂鸣器控制发声时,控制电平电压需要大于等于供电VCC电压,例如VCC为5V,控制电平为3.3V,控制就会失效,这可能与三极管导通原理有关。因为STM32标准电平为3.3V,所以蜂鸣器采用3.3V供电。
7、蓝牙模块
HC-05蓝牙模块,USB转TTL(用于蓝牙AT指令操作)
供电:3.3-5V 传输距离:10m
原理:串口。蓝牙相当于中转站,把从单片机串口接收到的信息发送出去或者把接受到的信息发送到单片机串口,用户只需要会使用串口就行。
引脚功能与接法:
STATE | 状态指示。未连接时输出低电平,连接时输出高电平。 |
RXD | 接收引脚 |
TXD | 发射引脚 |
GND | 地 |
VCC | 接电源,可以用+5V |
EN | 使能。接地时禁用此模块,悬空或接3.3V使能。 |
蓝牙具有两种工作模式:命令响应工作模式(AT模式)和自动连接工作模式。命令响应模式用于AT指令进行蓝牙配置,自动连接模式用于数据传输。
进入AT模式:将蓝牙VCC,GND,RX,TX与USB转串口模块连接(TX,RX交叉连接),按住蓝牙上的黑色按键给模块上电,蓝牙上的led以两秒的间隔闪烁,表示连接成功,进入AT命令模式,电脑端打开串口助手,波特率为38400,可以进行AT指令操作。通过AT指令可以进行蓝牙的波特率、名称、密码等配置。
AT指令:发送AT指令时最后要加上回车键(勾选发送新行)!
配置好后给模块重新上电即可正常使用。也可以不进行AT指令配置,使用默认配置,蓝牙名称为元件型号HC-05,波特率9600,密码1234。
将模块与单片机连接,手机连接蓝牙,进入蓝牙调试器,就可以进行无线数据传输,实现遥控功能了。
8、电机驱动模块
tb6612电机驱动模块
供电:VM脚12V
TB6612是双驱动,可同时驱动两个电机
引脚定义:
STBY | 接单片机的IO口清零电机全部停止,置1通过AIN1 AIN2,BIN1,BIN2 来控制正反转 |
VM | 建议接10V以内电源( 瞬间上电12V可能会有尖峰电压击穿器件 ) |
VCC | 接5V电源 |
GND | 接电源负极 |
PWMA | 接单片机的PWM口 ,控制转速 |
PWMB | 接单片机的PWM口 ,控制转速 |
AO1、AO2 | 接电机1的两个脚 |
BO1、BO2 | 接电机2的两个脚 |
IN1/IN2对应电机状态:
IN1 | 0 | 0 | 1 |
IN2 | 0 | 1 | 0 |
电机运行状态 | 停止 | 正转 | 反转 |
9、电机选型
尺寸小,适合小型玩具,入门级智能小车,便宜,稳定。
10、其他辅助器件
LED灯珠,按键,限流电阻。
LED接法: ADC接法:
五、供电方案
12V:电机驱动VM脚,ADC的R1输入端。
5V:超声波模块、光电管、蓝牙模块、STM32最小系统板5V输入脚。
3.3V:OLED、红外对管、LED。
六、PCB设计与小车组装
采用b站up:好家伙VCC 的智能循迹小车V1.5版本PCB,光电管和LED通过拓展排针和面包板外接,具体引脚可参考代码部分,注意,stm32有的引脚不能承受5V电压,所以5V供电的外设尽量连接到可以承受5V的引脚上(引脚定义带FT标识)。
1、PCB原理图
2、PCB走线图
3、PCB实物图
4、小车实物图
七、软件底层设计
1、LED初始化
用于转向灯与蜂鸣器,二者总是同时进行的,所以封装到一个函数里。
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3);
}
void BUZZER_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_14);
}
void LED_LEFT_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
GPIO_ResetBits(GPIOB, GPIO_Pin_14);
}
void LED_LEFT_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_2);
GPIO_SetBits(GPIOB, GPIO_Pin_14);
}
void LED_RIGHT_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_3);
GPIO_ResetBits(GPIOB, GPIO_Pin_14);
}
void LED_RIGHT_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOB, GPIO_Pin_14);
}
void LED_ALL_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_2);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOB, GPIO_Pin_14);
}
2、按键初始化
采用外部中断法,能够及时响应,切换模式。
uint16_t mode;
void NVIC_Group_Init(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //NVIC分组
}
void key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能PORTA,PORTE时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化PA12
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化PA7
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //使能复用功能时钟
//GPIOA.12 中断线以及中断初始化配置 下降沿触发 KEY2
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource12);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line12;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOA.7 中断线以及中断初始化配置 上升沿触发 KEY1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource7);
EXTI_InitStructure.EXTI_Line=EXTI_Line7;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //使能按键KEY1所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //使能按键KEY2所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
/**
* @brief 按键KEY1 PA7 中断处理函数
* @param
* @return
*/
void EXTI9_5_IRQHandler(void)
{
Delay_ms(10);//消抖
if( GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_7)==1) //按键KEY1 PA7
{
mode = (mode+1)%7;
}
EXTI_ClearITPendingBit(EXTI_Line7); //清除LINE4上的中断标志位
}
/**
* @brief 按键KEY2 PA12 中断处理函数
* @param
* @return
*/
void EXTI15_10_IRQHandler(void)
{
Delay_ms(10);//消抖
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_12)==0) //按键KEY2
{
mode = 0;
}
EXTI_ClearITPendingBit(EXTI_Line12); //清除LINE4上的中断标志位
}
3、OLED初始化
用于显示内容,采用软件IIC。
/*引脚配置*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOC, GPIO_Pin_14, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOC, GPIO_Pin_15, (BitAction)(x))
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_Init(GPIOC, &GPIO_InitStructure);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
4、超声波模块初始化
超声波有定时器中断法、外部中断法、输入捕获法等多种方法,这里使用输入捕获法。
int time = 0;
void SR04_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能TIM2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
//trig
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0端口配置, 推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //推挽输出 ,IO口速度为50MHz
GPIO_SetBits(GPIOA,GPIO_Pin_0);
//echo
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //PA1 清除之前设置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA1 输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_1); //PA1 下拉
//初始化定时器2
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 65536-1; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_Prescaler =72-1; //预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
//初始化TIM5输入捕获参数
TIM_ICInitTypeDef TIM2_ICInitStructure;
TIM2_ICInitStructure.TIM_Channel = TIM_Channel_2; // 选择输入端 IC1映射到TI2上
TIM2_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获
TIM2_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI2上
TIM2_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM2_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波
TIM_ICInit(TIM2, &TIM2_ICInitStructure);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级2级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC2,ENABLE);//允许更新中断 ,允许CC2IE捕获中断
TIM_Cmd(TIM2,ENABLE ); //使能定时器2
}
u8 TIM2CH1_CAPTURE_STA=0; // 输入捕获状态
u16 TIM2CH1_CAPTURE_VAL; // 输入捕获值
void TIM2_IRQHandler(void)
{
if((TIM2CH1_CAPTURE_STA&0X80)==0)//还未成功捕获
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
if(TIM2CH1_CAPTURE_STA&0X40)//已经捕获到高电平了
{
if((TIM2CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
{
TIM2CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次
TIM2CH1_CAPTURE_VAL=0XFFFF;
}else TIM2CH1_CAPTURE_STA++;
}
}
if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)//捕获1发生捕获事件
{
if(TIM2CH1_CAPTURE_STA&0X40) //捕获到一个下降沿
{
TIM2CH1_CAPTURE_STA|=0X80; //标记成功捕获
TIM2CH1_CAPTURE_VAL=TIM_GetCapture2(TIM2);
TIM_OC2PolarityConfig(TIM2,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获
}else //还未开始,第一次捕获上升沿
{
TIM2CH1_CAPTURE_STA=0; //清空
TIM2CH1_CAPTURE_VAL=0;
TIM_SetCounter(TIM2,0);
TIM2CH1_CAPTURE_STA|=0X40; //标记捕获到了上升沿
TIM_OC2PolarityConfig(TIM2,TIM_ICPolarity_Falling); //CC1P=1 设置为下降沿捕获
}
}
}
TIM_ClearITPendingBit(TIM2, TIM_IT_CC2|TIM_IT_Update); //清除中断标志位
}
int Get_Distance(void)
{
int distance ;
GPIO_SetBits(GPIOA,GPIO_Pin_0);
Delay_us(13);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);;
if(TIM2CH1_CAPTURE_STA&0X80)//成功捕获到了一次上升沿
{
time=TIM2CH1_CAPTURE_STA&0X3F;
time*=65536;//溢出时间总和
time+=TIM2CH1_CAPTURE_VAL;//得到总的高电平时间
distance = time*0.033/2;
TIM2CH1_CAPTURE_STA=0;//开启下一次捕获
}
return distance;
}
5、红外对管与光电管初始化
用于读取红外对管与光电管电平,光电管是5V供电,要把他连接到32能承受5V的引脚(引脚定义中带FT标识)。
void E18_D80NK_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
void TCRT5000_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE);//使能PORTA,PORTE时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3| GPIO_Pin_4| GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //默认下拉
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
#define GD_1 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_6)//读取光电管
#define GD_2 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7)
#define GD_3 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_9)
#define GD_4 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_10)
#define HW_1 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_5)//读取红外对管
#define HW_2 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_4)//读取红外对管
#define HW_3 GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_3)//读取红外对管
#define HW_4 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取红外对管
6、PWM初始化
采用高级定时器1输出普通pwm,用于调节电机转速。
void TB6612_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PB,PE端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13|GPIO_Pin_12|GPIO_Pin_1|GPIO_Pin_0; //LED1-->PE.5 端口配置, 推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //推挽输出 ,IO口速度为50MHz
GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_12|GPIO_Pin_1|GPIO_Pin_0); //PE.5 输出高
}
/**
* @brief 定时器1
* @param PA8-通道1 PA11-通道4
* @retval 用于输出PWM
* @attention TIM_CtrlPWMOutputs(TIM1,ENABLE);高级定时器专属--MOE主输出使能
*/
void pwm_Init(void)
{
/******************************开启时钟*****************************/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //开启定时器1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIO时钟
/*****************************定义结构体变量******************************/
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
/****************************配置引脚的模式*******************************/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/****************************配置时基单元*******************************/
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //1/2/4分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100-1; //ARR:自动重装载寄存器的值 Freq = CK_PSC/(PSC+1)/(ARR+1)
TIM_TimeBaseInitStructure.TIM_Prescaler = 36-1; //PSC:预分频系数,72MHz,那么可以给PSC=0,36MHz计数时,那么就给PSC=1
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//TIM_RepetitionCounter:重复计数,也就是重复多少次才发生计数溢出中断。官方注释里面这个数只与TIM1和TIM8有关
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure);
TIM_CtrlPWMOutputs(TIM1,ENABLE);//高级定时器专属--MOE主输出使能
/******************************输出比较单元配置*****************************/
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//比较输出模式选择
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//比较输出极性,可选为高电平有效或低电平有效
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//比较输出使能
TIM_OCInitStructure.TIM_Pulse = 0; //比较输出脉冲宽度,实际设定比较寄存器 CCR 的值,决定脉冲宽度。可设置范围为 0 至 65535。PWM占空比:Duty = CCR / (ARR + 1) = CCR / 100
TIM_OC1Init(TIM1, &TIM_OCInitStructure);//结构体初始化
TIM_OC4Init(TIM1, &TIM_OCInitStructure);//结构体初始化
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);//使能或者失能 TIMx 在 CCRx 上的预装载寄存器
TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable);//使能或者失能 TIMx 在 CCRx 上的预装载寄存器
TIM_Cmd(TIM1, ENABLE);//开启定时器
}
void Motor_RightSpeed(int8_t Speed)
{
//设置方向速度
if(Speed >= 0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_1);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
TIM_SetCompare1(TIM1,Speed);
}
else
{
GPIO_ResetBits(GPIOB,GPIO_Pin_1);
GPIO_SetBits(GPIOB,GPIO_Pin_0);
TIM_SetCompare1(TIM1,-Speed);//Speed为负数
}
}
void Motor_LeftSpeed(int8_t Speed)
{
//设置方向速度
if(Speed >= 0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
GPIO_ResetBits(GPIOB,GPIO_Pin_13);
TIM_SetCompare4(TIM1,Speed);
}
else
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
GPIO_SetBits(GPIOB,GPIO_Pin_13);
TIM_SetCompare4(TIM1,-Speed);//Speed为负数
}
}
7、小车动作初始化
用于封装小车各种动作函数,方便调用,其他动作读者可自行设置,修改speed里的数值可改变速度。
//前进
void Car_Ahead(void)
{
LED_ALL_OFF();
Motor_RightSpeed(18);
Motor_LeftSpeed(-18);
}
//直行
void Car_Line(void)
{
LED_ALL_OFF();
Motor_RightSpeed(22);
Motor_LeftSpeed(-20);
}
//后退
void Car_Back(void)
{
LED_ALL_OFF();
Motor_RightSpeed(-18);
Motor_LeftSpeed(18);
}
//原地右转
void Self_Right(void)
{
LED_RIGHT_ON();
Motor_RightSpeed(-30);
Motor_LeftSpeed(-30);
}
//原地左转
void Self_Left(void)
{
LED_LEFT_ON();
Motor_RightSpeed(30);
Motor_LeftSpeed(30);
}
//右转,右轮不转
void Turn_Right(void)
{
LED_RIGHT_ON();
Motor_RightSpeed(0);
Motor_LeftSpeed(-20);
}
//左转,左轮不转
void Turn_Left(void)
{
LED_LEFT_ON();
Motor_RightSpeed(20);
Motor_LeftSpeed(0);
}
//停止
void Car_Stop(void)
{
LED_ALL_OFF();
Motor_RightSpeed(0);
Motor_LeftSpeed(0);
}
//前进并右转
void Forwar_Right(void)
{
LED_RIGHT_ON();
Motor_RightSpeed(10);
Motor_LeftSpeed(-20);
}
//前进并左转
void Forward_Left(void)
{
LED_LEFT_ON();
Motor_RightSpeed(20);
Motor_LeftSpeed(-10);
}
//加速
void Accelerate(uint8_t acc)
{
LED_ALL_OFF();
Motor_RightSpeed(acc);
Motor_LeftSpeed(-acc);
}
//快速原地左转
void Self_LeftFast(void)
{
LED_LEFT_ON();
Motor_RightSpeed(40);
Motor_LeftSpeed(40);
}
//快速原地右转
void Self_RightFast(void)
{
LED_RIGHT_ON();
Motor_RightSpeed(-40);
Motor_LeftSpeed(-40);
}
8、串口初始化
用于蓝牙遥控。
uint8_t Serial_RxData;//接收数据
uint8_t Serial_RxFlag;//接收标志位
uint8_t UART3_FLAG = 0;
uint8_t acc=0;
void Uart3_Init(void)
{
//开启USART和GPIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//GPIO初始化,把TX配置为复用输出,RX配置为输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//配置USART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;//波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控,不使用
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//串口模式,发送/接受
USART_InitStructure.USART_Parity = USART_Parity_No;//校验位
USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长
USART_Init(USART3, &USART_InitStructure);
//开启中断
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
//配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;//中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&NVIC_InitStructure);
//开启开关
USART_Cmd(USART3, ENABLE);
}
//发送字节数据函数
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART3, Byte);//发送到移位寄存器
while (USART_GetFlagStatus(USART3, USART_FLAG_TXE) == RESET);//等待,防止数据覆盖
}
//发送数组函数
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
//发送字符串函数
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
//计算X的Y次方
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
//发送数字,以字符串方式显示
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
//以十进制拆开,转换成字符数据发送出去
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
//重定向fputc到串口
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
//多个串口使用printf,发送可变参数
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;//定义参数列表变量
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);//释放参数表
Serial_SendString(String);
}
//读取接收标志位后自动清零
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
//读取接收到的数据
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
9、ADC初始化
用于测量电池电量。
//初始化ADC
//这里我们仅以规则通道为例
//我们默认将开启通道0~3
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//PA4 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
// ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
}
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
u16 Get_Adc_Average(u8 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
Delay_ms(5);
}
return temp_val/times;
}
10、RTC初始化
用时间戳获取实时时间。
uint16_t MyRTC_Time[] = {2024, 6, 23, 9, 0, 0}; //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒
void MyRTC_SetTime(void); //函数声明
/**
* 函 数:RTC初始化
* 参 数:无
* 返 回 值:无
*/
//void MyRTC_Init(void)
//{
// /*开启时钟*/
// RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
// RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
//
// /*备份寄存器访问使能*/
// PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问
//
// if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
// //只有第一次配置有效,主电源断电或复位后时间继续走
// //if成立则执行第一次的RTC配置
// {
// RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟
// while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪
//
// RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSE
// RCC_RTCCLKCmd(ENABLE); //RTCCLK使能
//
// RTC_WaitForSynchro(); //等待同步
// RTC_WaitForLastTask(); //等待上一次操作完成
//
// RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1Hz
// RTC_WaitForLastTask(); //等待上一次操作完成
//
// MyRTC_SetTime(); //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
//
// BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
// }
// else //RTC不是第一次配置
// {
// RTC_WaitForSynchro(); //等待同步
// RTC_WaitForLastTask(); //等待上一次操作完成
// }
//}
//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码,使用LSI当作RTCCLK
//LSI无法由备用电源供电,故主电源掉电时,RTC走时会暂停
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(40000 - 1);
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RCC_LSICmd(ENABLE); //即使不是第一次配置,也需要再次开启LSI时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}
/**
* 函 数:RTC设置时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
*/
void MyRTC_SetTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60; //调用mktime函数,将日期时间转换为秒计数器格式
//- 8 * 60 * 60为东八区的时区调整
RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中
RTC_WaitForLastTask(); //等待上一次操作完成
}
/**
* 函 数:RTC读取时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
*/
void MyRTC_ReadTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_cnt = RTC_GetCounter() + 8 * 60 * 60; //读取RTC的CNT,获取当前的秒计数器
//+ 8 * 60 * 60为东八区的时区调整
time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式
MyRTC_Time[0] = time_date.tm_year + 1900; //将日期时间结构体赋值给数组的时间
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
}
OLED显示:
八、程序设计
1、避障思路
避障程序有多种设计方法,这里提供一种思路:
2、循迹程序设计
循迹主要有两个难点,1、2两个急转弯和3入库,利用红外对管3、4同时检测到时进行快速左转,速度要比普通转弯快,实际使用时可能要把4扳到离3近一些,检测到的可能性会有所提高。由于四个光电管同时检测到的概率较小,所以利用2、3同时检测到时进行入库停车操作。
if(mode == 4)//红外对管循迹
{
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 0 && HW_4 == 0)
{
Car_Ahead();
Delay_ms(20);
}
if(HW_1 == 0 && HW_2 == 1 && HW_3 == 0 && HW_4 == 0)
{
Self_Right();
Delay_ms(20);
}
if(HW_1 == 1 && HW_2 == 0 && HW_3 == 0 && HW_4 == 0)
{
Self_Right();
Delay_ms(50);
}
if(HW_1 == 1 && HW_2 == 1 && HW_3 == 0 && HW_4 == 0)
{
Self_Right();
Delay_ms(300);
}
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 1 && HW_4 == 0)
{
Self_Left();
Delay_ms(20);
}
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 0 && HW_4 == 1)
{
Self_Left();
Delay_ms(50);
}
if(HW_1 == 0 && HW_2 == 0 && HW_3 == 1 && HW_4 == 1)
{
Self_LeftFast();
Delay_ms(400);
}
if(HW_2 == 1 && HW_3 == 1)
{
Car_Ahead();
Delay_ms(1500);
Car_Stop();
mode = 0;
}
}
3、跟随程序设计
主要实现一个定距离跟随功能,离得太近小车后退,离得太远小车前进,这里只写了直线跟随,可以利用光电管实现非直线跟随。
if(mode == 5)//跟随
{
distance = Get_Distance();
OLED_ShowNum(3,5,distance,3);
if(distance > 20)
{
Car_Ahead();
Delay_ms(50);
}
if(distance < 15)
{
Car_Back();
Delay_ms(50);
}
Car_Stop();
}
4、蓝牙遥控程序设计
实现蓝牙发送数据,小车实现对应功能。
串口中断服务函数:
//中断服务函数
void USART3_IRQHandler(void)
{
if (USART_GetITStatus(USART3, USART_IT_RXNE) == SET)//判断标志位
{
Serial_RxData = USART_ReceiveData(USART3);//接收数据
if(Serial_RxData == 'A') UART3_FLAG = 1;//前进
if(Serial_RxData == 'B') UART3_FLAG = 2;//后退
if(Serial_RxData == 'C') UART3_FLAG = 3;//原地左转
if(Serial_RxData == 'D') UART3_FLAG = 4;//原地右转
if(Serial_RxData == 'E') //停止
{
UART3_FLAG = 5;
acc = 0;
}
if(Serial_RxData == 'F') //跟随
{
mode = 5;
UART3_FLAG = 6;
}
if(Serial_RxData == 'G') //寻迹
{
mode = 4;
UART3_FLAG = 7;
}
if(Serial_RxData == 'H') //加速
{
UART3_FLAG =8;
acc +=10;
if(acc > 50) acc = 0;
}
if(Serial_RxData == 'I') //直行
{
mode = 3;
UART3_FLAG = 9;
}
if(Serial_RxData == 'J') //停车
{
mode = 0;
UART3_FLAG = 10;
}
if(Serial_RxData == 'K') //蓝牙模式
{
mode = 2;
UART3_FLAG = 11;
}
if(Serial_RxData == 'L') //避障
{
mode = 1;
UART3_FLAG = 12;
}
Serial_RxFlag = 1;//接收标志位
//Serial_SendNumber(time_ms,6);
USART_ClearITPendingBit(USART3, USART_IT_RXNE);//清除标志位
}
}
主循环蓝牙模式:
if(mode == 2)//蓝牙
{
Car_Stop();
switch(UART3_FLAG)
{
case 1:Car_Ahead();
break;
case 2:Car_Back();
break;
case 3:Self_Left();
break;
case 4:Self_Right();
break;
case 5:Car_Stop();
break;
case 6:
break;
case 7:
break;
case 8:Accelerate(acc);
break;
case 9:
break;
case 10:
break;
default:Car_Stop();
break;
}
}
九、程序源码
链接:https://pan.baidu.com/s/1-IG5TbmaLH36aXVDPOdxHQ?pwd=s2rn
提取码:s2rn