嵌入式项目之51智能车
第 1 章 项目需求
1.1项目概述
本项目旨在设计和实现一个基于51单片机的自动化小车。该小车不仅具备基本的移动功能,还能实现循迹、蓝牙控制和超声避障等智能化功能。
1.2功能概述
1)车有三种运动模式可调
小车有遥控、避障、巡线三种运行模式
三种模式可以通过按键调节
2)车速度可调节
可根据情况对小车车速进行自动化调节。
调节速度范围为40档,
可以正向可以反向,取决于程序0或者遥控需要
3)迹行驶
小车能够识别预先设定的轨迹(例如黑线)并沿着轨迹行进。
可以按照弧线或者直角进行转弯
可以识别交叉线
可以识别弯度较大的锐角
范例地图如下:

4)牙控制
能够通过蓝牙与外部设备(如智能手机)进行通信。
可以通过小程序控制小车
可以遥控小车前进后退
可以遥控小车左转和右转
5)声避障
通过超声模块,可检测前方障碍物,进行自动避障
超声波模块检测范围100mm-3000mm
与障碍物距离小于300mm后小车开始顺时针旋转避障
1.3硬件设计概述
1.3.1项目整体结构

1.3.2动力系统
我们想让小车跑起来,首先要考虑马达、驱动芯片和供电模块选型。这三者中,马达是最核心的部分,只有确定了马达,其他平台才能确定。
1)达选型
51单片机小车马达选型一般就是两种:TT马达和N20马达。前者主要是用在各种儿童玩具上,后者的应用就比较广泛,在电子锁,电动牙刷上都有使用。两者之间,主要是在成本和性能之间做权衡。由于我们不仅需要小车动起来,还要精准的操控它,所以这里我们选用精度较高的N20马达。
2)电模块选型
当马达型号确定以后,我们去查找N20参数,就可以发现N20有3v、6v、12v三个平台。这里我们选用6v平台,其输入电压是5-9v。我们可以选择两节锂电池串联供电:锂电池的放电电压是4.2v-2.5v,刚好在范围之内。
电压
DCV 空载转速
rpm/min 负载转速
rpm/min 额定力矩
kg.cm 额定电流
ma 堵转力矩
kg.cm 堵转电流
ma 减速比
1:00
6 300 240 0.2 160 1.6 100 50
6 150 120 0.3 160 2.4 200 100
6 100 80 0.4 160 3.2 200 150
从图中我们可以看到,N20额定电流是160ma,我们采用两个电机,同时满载驱动需要320ma,只要电池放电速度高于这个数,就能够供应电机。为了稳妥起见,这里我们采用具有800ma放电速度的电池。
3)动模块选型
驱动模块就比较随意了,只要能够满足电机的额定电压和电流,任意驱动模块都可以。这里我们选用睿智微的TA6586芯片驱动电机。驱动两个电机我们共需要两片芯片。
1.3.3避障系统
避障系统方案我们采用51智能小车上最常见的超声模块SR-04避障。我们只需要在设计小车时留好排针插座即可。
1.3.4巡线系统
巡线方案我们采用光电感应模块,共有5组广电感应模块负责巡黑线,同时采用模拟比较器完成模数转换。比较器的型号就选择非常经典的两路比较器XD393。由于我们有5组光电模块,共需要3片比较器芯片。
1.3.5遥控系统
为了方便通过手机操作小车,遥控方面我们选择JDY-23蓝牙模块。这个模块使用串口和单片机通信我们同样留好排针插座即可
1.3.6其他交互模块
除了上述主要模块,为了方便和小车交互,我们还可以预留以下模块:
(1)LCD1602模块方便输出信息
(2)独立按键方便在蓝牙无连接时操作小车
(3)LED灯和蜂鸣器增加小车其他输出
第 2 章小车原理图


第 3 章项目架构设计
对于比较复杂的项目,生产环境上都会对项目架构设计成多层,各层如下:
驱动层:和单片机直接接触的层级,一般包含单片机片内外设驱动代码;
接口层:单片机外部设备的驱动代码,一般通过标准接口(调用驱动层代码)和单片机通信;
中间层:单片机内部硬件和外部硬件共同组成的一个个功能模块,一般是对驱动和接口两层进行更贴近业务的二次封装;
应用层:整个应用的主控制模块都放在这一层,一般由核心控制代码组成,通过调用中间层代码实现控制。
通用层:这其实不是标准架构中的一层,而是我们将一些通用的定义和常量声明放在这一层,供其他层代码调用。
原则上只能上层代码调用下层代码,同层不能互相调用。
我们的项目架构如图所示:

第 4 章避障功能
4.1功能架构
避障功能整体架构如下所示:

4.2通用层
4.2.1简介
我们首先要在通用层实现一个公共方法和声明,用来声明一些常数、公共的类型别名和方法。例如:
声明单片机的晶振频率
声明单片机的机器周期长度
声明常用的8位、16位无符号数的定义别名,简化编程过程
声明常用的延时函数和一切其他公共函数
4.2.2代码实现
新建Com文件夹,并实现下面的代码:
(代码可找我领取)
4.3驱动层
驱动层我们主要实现单片机的内部外设驱动:GPIO,定时器中断以及串口。新建Dri文件夹,并实现以下代码。(代码可找我领)
4.3.1GPIO驱动
GPIO驱动主要是声明各个硬件对应的GPIO引脚,方便集中管理硬件更改带来的引脚变更。一般来说如果是更复杂的STM32开发板,这里需要实现GPIO的基本引脚操作,但51开发板支持位寻址,引脚的拉高拉低非常方便,就不做二次包装类。
1)ri_GPIO.h
4.3.2定时器中断
4.3.2.1简介
我们使用一个定时器中断集中执行所有需要定时执行的代码,例如电机驱动的方波信号,蜂鸣器的方波等等。但这里存在一个问题:定时器中断中需要执行的函数全部都是上层逻辑,但是按照我们的分层理论,只能由上层模块调用下层模块,那这一部分代码该如何实现呢?这里我们介绍一种通用的解决思想:回调函数。
我们在实现定时器中断驱动时,可以不关心具体的业务逻辑实现,只在驱动层的函数中实现调用过程,而具体的业务逻辑,在上层代码中实现。上层代码实现的业务逻辑封装在函数中,这些函数在驱动层被调用,它们就是回调函数。
打个比方,将底层的定时器中断看作一个快递公司。快递公司需要收发快递,快递箱可以交给顾客自行打包,快递公司不知道每件快递的具体内容,而只负责运输过程。
要实现这一过程,我们需要用到一个特殊的技巧:函数指针。来看下面的例子:
#include <stdio.h>
// 定义函数指针类型(无参无返回值函数指针)
typedef void (*Func)(void);
// 定义一个无参无返回值的函数
void say_hello()
{
printf(“Hello World\n”);
}
// 定义一个能够 调用无参无返回值的函数 的高阶函数
void call_func(Func func) {
func();
}
int main()
{
// 通过高阶函数 调用say_hello函数。这里say_hello是函数指针
call_func(say_hello);
}
这个例子中我们将say_hello函数的函数指针作为参数传递给了call_func函数,从而在call_func函数中完成了say_hello函数的调用。say_hello就是回调函数。
在定时器中断驱动中,我们可以声名一个全局数组用来保存所有将来需要调用的函数指针,并声名一个注册方法供上层函数将函数指针保存到数组即可。之后我们就可以在中断函数中调用这些函数指针。
4.3.2.2流程图解

4.3.2.3代码实现
1)ri_Timer0.h
#ifndef DRI_TIMER0_H
#define DRI_TIMER0_H
#include <STC89C5xRC.H>
#include “Util.h”
typedef void (*Timer0_Callback)(void);
#define MAX_CALLBACK_COUNT 4
/**
- @brief 定时器初始化
*/
void Dri_Timer0_Init();
/**
- @brief 提供注册入口,用这个函数注册完成的函数,会以1000Hz的频率被调用
- @return 成功返回1,失败返回0
*/
bit Dri_Timer0_RegisterCallback(Timer0_Callback);
/**
- @brief 反注册回调函数,反注册的函数不会再被周期调用
- @return bit 反注册的结果,成功位1,失败为0
*/
bit Dri_Timer0_DeregisterCallback(Timer0_Callback);
#endif // !DRI_TIMER0_H
2)ri_Timer0.c
#include “Dri_Timer0.h”
#include <STDIO.H>
#define T1MS (65536 - FOSC / NT / 1000)
static u16 s_timer0_counter = 0;
static Timer0_Callback s_timer0_callbacks[MAX_CALLBACK_COUNT];
void Dri_Timer0_Init()
{
u8 i;
// 总中断开关
EA = 1;
// 定时器中断开关
ET0 = 1;
// 设置定时器0的工作模式:16位定时器
TMOD &= 0xF0;
TMOD |= 0x01;
// 设置定时器的初始值
TL0 = T1MS;
TH0 = T1MS >> 8;
// 定时器0的开关
TR0 = 1;
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
s_timer0_callbacks[i] = NULL;
}
}
bit Dri_Timer0_RegisterCallback(Timer0_Callback callback)
{
// 判断这个函数有没有被注册过
u8 i;
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i] == callback)
{
// 如果该函数被注册过,直接返回
return 1;
}
}
// 注册该函数
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i] == NULL)
{
s_timer0_callbacks[i] = callback;
return 1;
}
}
return 0;
}
bit Dri_Timer0_DeregisterCallback(Timer0_Callback callback)
{
u8 i;
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i] == callback)
{
s_timer0_callbacks[i] = NULL;
return 1;
}
}
return 0;
}
/**
- @brief 1ms调用一次这个函数
*/
void Dri_Timer0_Func() interrupt 1
{
u8 i;
// 定义下次进入时钟中断的时间
TL0 = T1MS;
TH0 = T1MS >> 8;
// 调用所有的回调函数
for (i = 0; i < MAX_CALLBACK_COUNT; i++)
{
if (s_timer0_callbacks[i])
{
s_timer0_callbacks[i]();
}
}
}
4.4接口层
接口层我们主要实现片外设备驱动代码。新建Int文件夹,并实现下面的代码。
4.4.1蜂鸣器模块
我们使用的无源蜂鸣器,需要单片机给它输入一定频率的方波,这里我们采用定时器对输入引脚做反相,就可以输入500Hz的方波,蜂鸣器就会发出500Hz的声音。
4.4.1.1代码实现
1)nt_Buzzer.h
#ifndef INT_BUZZER_H
#define INT_BUZZER_H
#include “Dri_GPIO.h”
#include “Dri_Timer0.h”
/**
- @brief 蜂鸣器初始化
*/
void Int_Buzzer_Init();
/**
- @brief 让蜂鸣器发声
- @param ms 希望蜂鸣器响多少ms
*/
void Int_Buzzer_Buzz(u16 ms);
#endif
2)nt_Buzzer.c
#include “Int_Buzzer.h”
static u16 s_buzzer_counter;
void Int_Buzzer_TimerCallback()
{
if (s_buzzer_counter)
{
s_buzzer_counter–;
BUZZER_PIN = ~BUZZER_PIN;
}
else
{
BUZZER_PIN = 0;
}
}
void Int_Buzzer_Init()
{
s_buzzer_counter = 0;
Dri_Timer0_RegisterCallback(Int_Buzzer_TimerCallback);
}
void Int_Buzzer_Buzz(u16 ms)
{
s_buzzer_counter = ms;
}
4.4.1.2测试
1)Main.c中写入以下内容。
#include “Int_Buzzer.h”
#include “Dri_Timer0.h”
void main() {
// 初始化定时器
Dri_Timer0_Init();
// 初始化蜂鸣器
Int_Buzzer_Init();
// 设置蜂鸣器 BUZZER_PIN 反相次数,从而更改蜂鸣器鸣响时长,1000次反相鸣响1s
Int_Buzzer_Buzz(3000);
// 打开中断总开关
EA = 1;
// 阻塞程序防止跑飞
while (1);
}
2)译并烧录
编译烧录后蜂鸣器响起,持续3s,调整蜂鸣器BUZZER_PIN反相次数,重新编译烧录后,鸣响时长发生相应变化则测试通过。
4.4.2LCD1602模块
LCD模块我们直接采用之前的代码,引脚名称改变一下即可。
4.4.2.1代码实现
1)nt_LCD1602.h
#ifndef INT_LCD1602_H
#define INT_LCD1602_H
#include <STC89C5xRC.H>
#include “Util.h”
/**
- @brief 初始化方法
*/
void Int_LCD1602_Init();
/**
- @brief 清屏
*/
void Int_LCD1602_Clear();
/**
- @brief 向LCD1602写一串字符串
- @param pos_y 行[0-1]
- @param pos_x 列[0-15]
- @param str 要写的字符串,不要超过16个字符
*/
void Int_LCD1602_ShowStr(u8 pos_y, u8 pos_x, u8 *str);
/**
- @brief 向LCD1602写一串数字
- @param pos_y 行[0-1]
- @param pos_x 列[0-15]
- @param num 要写的数字 [-32768-32767]
*/
void Int_LCD1602_ShowNum(u8 pos_y, u8 pos_x, int num);
#endif
2)nt_LCD1602.c
#include “Int_LCD1602.h”
#include “Dri_GPIO.h”
#include <STDIO.H>
/**
- @brief 如果上一条指令没有执行完,会阻塞进程等待执行结束
*/
static void Int_LCD1602_Wait()
{
// 释放busy_flag引脚
LCD1602_BUSY_FLAG = 1;
LCD1602_RS = 0;
LCD1602_RW = 1;
LCD1602_EN = 1;
// 等待忙状态
while (LCD1602_BUSY_FLAG)
{
}
LCD1602_EN = 0;
}
/**
- @brief 向LCD1602写一条命令
- @param cmd 要写的命令
*/
static void Int_LCD1602_WriteCmd(u8 cmd)
{
// 先检查忙状态
Int_LCD1602_Wait();
LCD1602_RS = 0;
LCD1602_RW = 0;
// 准备命令
LCD1602_DB = cmd;
// 下降沿
LCD1602_EN = 1;
LCD1602_EN = 0;
}
/**
- @brief 向LCD1602写一条数据
- @param dat 要写的数据
*/
static void Int_LCD1602_WriteData(u8 dat)
{
// 先检查忙状态
Int_LCD1602_Wait();
LCD1602_RS = 1;
LCD1602_RW = 0;
// 准备命令
LCD1602_DB = dat;
// 下降沿
LCD1602_EN = 1;
LCD1602_EN = 0;
}
void Int_LCD1602_Init()
{
LCD1602_RW = 0;
LCD1602_RS = 0;
LCD1602_EN = 0;
// 设置显示模式(2行 5x8)
Int_LCD1602_WriteCmd(0x38);
// 设置光标显示(文字显示,光标不显示)
Int_LCD1602_WriteCmd(0x0C);
// 设置屏幕不移动
Int_LCD1602_WriteCmd(0x06);
// 清屏
Int_LCD1602_WriteCmd(0x01);
}
void Int_LCD1602_Clear()
{
// 清屏
Int_LCD1602_WriteCmd(0x01);
}
void Int_LCD1602_ShowStr(u8 pos_y, u8 pos_x, u8 *str)
{
pos_x &= 0x0F;
pos_y &= 0x01;
// 设置光标位置
Int_LCD1602_WriteCmd(0x80 | pos_y << 6 | pos_x);
// 一个字节一个字节输入
while (*str)
{
Int_LCD1602_WriteData(*str++);
}
}
void Int_LCD1602_ShowNum(u8 pos_y, u8 pos_x, int num)
{
u8 buff[7];
char len;
// 将数字转成字符串
len = sprintf(buff, "%d ", num);
if (len <= 0)
{
return;
}
// 调用显示字符串方法
Int_LCD1602_ShowStr(pos_y, pos_x, buff);
}
4.4.2.2测试
1)改Main.c,写入以下内容
#include “Int_LCD1602.h”
void main() {
// 初始化LCD1602
Int_LCD1602_Init();
// 在LCD1602的液晶屏显示 "hello world!"
Int_LCD1602_ShowStr(0,0,"hello world!");
// 阻塞程序防止跑飞
while (1);
}
2)译并烧录
如果可以在LCD1602的液晶屏上看到“hello world!”字样,则测试通过。
4.4.3电机驱动模块
4.4.3.1PWM波电机驱动方式
TA6586手册如下:
TA6586共有两个GPIO输入,两个输出,如下图所示:
其中BI和FI代表PWM波输入,而out3和out4连接N20电机,我们通过调节BI,FI输入的PWM波的占空比,完成电机两端电压控制,从而实现控制转速。整个过程如下PPT所示:

4.4.3.2代码实现
1)nt_Motor.h
#ifndef INT_MOTOR_H
#define INT_MOTOR_H
#include “Dri_GPIO.h”
#include “Util.h”
#include “Dri_Timer0.h”
/**
- @brief 初始化方法
*/
void Int_Motor_Init();
/**
- @brief 设置左轮速度
- @param speed 左轮速度 [-40, 40]
*/
void Int_Motor_SetLeft(char speed);
/**
- @brief 设置右轮速度
- @param speed 右轮速度 [-40, 40]
*/
void Int_Motor_SetRight(char speed);
/**
- @brief 更新轮子状态的方法,循环调用这个方法,会让轮子的当前速度趋向目标速度
*/
void Int_Motor_UpdateSpeed();
#endif
2)nt_Motor.c
#include “Int_Motor.h”
#include <STDLIB.H>
#define MAX_SPEED 40
#define SHARP 1
static u8 s_speed_counter;
static u8 s_wheel_status_counter;
typedef struct
{
// 轮子方向,0朝前,1朝后
u8 direction;
// 轮子速度绝对值
u8 absolute_speed;
// 轮子的当前转速
char current_speed;
// 轮子的目标速度
char target_speed;
} Struct_WheelStatus;
// 0是左轮,1是右轮
static Struct_WheelStatus s_st_wheel[2];
/**
- @brief 电机驱动的时钟中断方法,负责发方波
*/
static void Int_Motor_TimerCallback()
{
s_speed_counter++;
if (s_speed_counter >= MAX_SPEED)
{
s_speed_counter = 0;
}
// 如果我们不希望轮子转,两个都为0
if (s_speed_counter < s_st_wheel[0].absolute_speed)
{
// 如果我们希望轮子转,拉高其中一个引脚即可
// 如果前进,direction是0,后退direction是1
// 前进的时候 MOTOR_L_PIN1 = 1, MOTOR_L_PIN2 = 0
// 后退时候 MOTOR_L_PIN1 = 0, MOTOR_L_PIN2 = 1
MOTOR_L_PIN1 = !s_st_wheel[0].direction;
MOTOR_L_PIN2 = s_st_wheel[0].direction;
}
else
{
// 如果我们不希望轮子转,两个都为0
MOTOR_L_PIN1 = 0;
MOTOR_L_PIN2 = 0;
}
if (s_speed_counter < s_st_wheel[1].absolute_speed)
{
MOTOR_R_PIN1 = !s_st_wheel[1].direction;
MOTOR_R_PIN2 = s_st_wheel[1].direction;
}
else
{
// 如果我们不希望轮子转,两个都为0
MOTOR_R_PIN1 = 0;
MOTOR_R_PIN2 = 0;
}
}
/**
- @brief 内部方法,给某一个轮子设置目标速度
- @param p_st_wheel 轮子指针
- @param target_speed 目标速度
*/
static void Int_Motor_SetTargetSpeed(Struct_WheelStatus *p_st_wheel, char target_speed)
{
if (target_speed < -MAX_SPEED)
{
p_st_wheel->target_speed = -MAX_SPEED;
}
else if (target_speed > MAX_SPEED)
{
p_st_wheel->target_speed = MAX_SPEED;
}
else
{
p_st_wheel->target_speed = target_speed;
}
}
/**
-
@brief 内部方法,更新一个轮子的状态,让其当前速度趋近于目标速度
-
@param p_st_wheel
*/
static void Int_Motor_UpdateWheel(Struct_WheelStatus *p_st_wheel)
{
// 让当前速度趋近于一点点目标速度
if (p_st_wheel->target_speed == p_st_wheel->current_speed)
{
return;
}if (p_st_wheel->target_speed > p_st_wheel->current_speed)
{
p_st_wheel->current_speed++;
}
else
{
p_st_wheel->current_speed–;
}// 根据更新的当前速度,更新方向和速度绝对值
p_st_wheel->direction = (p_st_wheel->current_speed < 0);
p_st_wheel->absolute_speed = abs(p_st_wheel->current_speed);
}
void Int_Motor_Init()
{
u8 i;
s_speed_counter = 0;
for (i = 0; i < 2; i++)
{
s_st_wheel[i].target_speed = 0;
s_st_wheel[i].current_speed = 0;
s_st_wheel[i].absolute_speed = 0;
s_st_wheel[i].direction = 0;
}
Dri_Timer0_RegisterCallback(Int_Motor_TimerCallback);
}
void Int_Motor_SetLeft(char speed)
{
Int_Motor_SetTargetSpeed(s_st_wheel, speed);
}
void Int_Motor_SetRight(char speed)
{
Int_Motor_SetTargetSpeed(s_st_wheel + 1, speed);
}
void Int_Motor_UpdateSpeed()
{
s_wheel_status_counter++;
if (s_wheel_status_counter == SHARP)
{
Int_Motor_UpdateWheel(s_st_wheel);
}
else if (s_wheel_status_counter == 2 * SHARP)
{
s_wheel_status_counter = 0;
Int_Motor_UpdateWheel(s_st_wheel + 1);
}
}
4.4.3.3测试
1)改Main.c,写入以下内容
#include “Int_Motor.h”
#include “Dri_Timer0.h”
void main() {
// 初始化定时器
Dri_Timer0_Init();
// 打开中断总开关
EA = 1;
// 初始化电机
Int_Motor_Init();
// 设置电机速度
Int_Motor_SetLeft(10);
Int_Motor_SetRight(10);
while(1) {
// 更新电机速度
Int_Motor_UpdateSpeed();
}
}
2)新编译并烧录
可以看到电机转动,更改电机速度,可以看到变化,则测试通过。
4.4.4超声波测距模块
4.4.4.1简介
SR04超声模块手册如下:
超声波测距模块共有两个GPIO引脚:TRIG和ECHO,它们的具体工作模式如下:

4.4.4.2代码实现
1)nt_Range.h
#ifndef RANGE_H
#define RANGE_H
#include <INTRINS.H>
#include “Dri_GPIO.h”
#include “Dri_Timer0.h”
#include “Util.h”
#define RANGE_MS 100 // 超声波探测周期
/**
- @brief 初始化超声波模块
*/
void Int_Range_Init();
/**
- @brief 获取超声波测距距离
- @return 距离,单位为mm
*/
u16 Int_Range_GetRange();
#endif
2)nt_Range.c
#include “Int_Range.h”
#define RANGE_INTERVAL 100
// 标志位:该标志位为距离上次测距的ms时间间隔
static u8 s_get_range_flag;
static u16 s_range;
static void Int_Range_TimerCallback()
{
if (s_get_range_flag < RANGE_INTERVAL)
{
s_get_range_flag++;
}
}
void Int_Range_Init()
{
s_get_range_flag = RANGE_INTERVAL;
TRIG = 0;
ECHO = 1;
Dri_Timer0_RegisterCallback(Int_Range_TimerCallback);
}
u16 Int_Range_GetRange()
{
u8 count;
if (s_get_range_flag == RANGE_INTERVAL)
{
s_get_range_flag = 0;
// 测距
// TRIG引脚发一个10us的脉冲
TRIG = 1;
Delay10us();
TRIG = 0;
count = 0;
while (!ECHO)
{
count++;
Delay10us();
if (count >= 25)
{
return s_range;
}
}
EA = 0;
s_range = 0;
// 说明ECHO引脚拉高了
while (s_range < 1000 && ECHO)
{
_nop_();
_nop_();
P15 = ~P15;
s_range++;
}
EA = 1;
// 返回最终结果
s_range *= 2;
}
return s_range;
// 返回上一次结果
}
4.4.4.3测试
1)改Main.c文件,写入以下内容
#include “Int_LCD1602.h”
#include “Int_Range.h”
#include “Dri_Timer0.h”
#include <STDIO.H>
void main()
{
// 定义变量,用于接收测距结果
u16 distance;
// 定义字符串,用于在LCD展示测距结果
u8 str[17];
// 初始化定时器
Dri_Timer0_Init();
// 打开中断总开关
EA = 1;
// 初始化LCD1602
Int_LCD1602_Init();
// 初始化超声波测距模块
Int_Range_Init();
while (1)
{
// 调用方法测距并记录结果
distance = Int_Range_GetRange();
// 为所有distance末尾添加空格,覆盖之前的测距结果
sprintf(str, "%d ", distance);
// 通过LCD1602的液晶屏显示测距结果
Int_LCD1602_ShowStr(0, 0, str);
}
}
2)译并烧录

我们选用的单片机引脚图如下。

ALE/P4.5引脚为复用引脚,既可作为GPIO引脚使用,又可作为控制信号引脚使用(ALE即Address Latch Enable,用于使能或禁用地址锁存器)。此处我们将其作为GPIO引脚使用,因此,在烧录时要勾选“ALE脚用作P4.5口”。
3)看效果

超声模块插入小车前方的母座,在超声模块正前方放置物体,观察LCD1602液晶屏的内容,调整物体的位置,重新观察液晶屏,示数会立即变化。可以观察到上述现象则测试通过。
4.5应用层
新建App文件夹,并实现下面的代码:
4.5.1简介
调用超声波测距,当距离不足300mm时,蜂鸣器鸣响并原地顺时针旋转。
4.5.2设置多编译目标
1)置多编译目标(target)
target可以理解为一种标签,可以在其中做特定配置,从而控制编译行为,target之间不会相互影响。我们可以通过target的切换方便地更改编译模式。默认情况下,每个新建的EIDE项目所处的target名称均为Debug。
(1)切换目标

(2)新建target

(3)输入新的target名称

(4)回车,自动切换至新建的target

现在我们有两个target:Release和Debug,Release用于最终小车整体运行情况的测试,Debug用于单个模块或功能的测试。
2)target添加宏定义变量
由于DEBUG目标用来测试,那么这个目标的代码就不用刻意考虑性能,而应该为了方便调试。我们可以在代码中通过条件编译的方式添加调试代码,并给目标绑定特定的宏定义,实现不同目标代码不同。例如:
// 遇到障碍 蜂鸣器响 开始向右旋转
Int_Buzzer_Buzz(100);
Int_Motor_SetLeft(40);
Int_Motor_SetRight(-40);
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 1, "Turn Right! ");
#endif
上面的代码中,最后一句在LCD显示信息就是条件编译,我们只需要预先定义宏“DEBUG”,就会编译这句代码。以下为如何添加宏定义的示例:
(1)切换至Debug target


(2)添加预处理宏定义变量


4.5.3代码实现
1)pp_Avoidance.h
#ifndef APP_AVOIDANCE_H
#define APP_AVOIDANCE_H
#include “Int_Buzzer.h”
#include “Int_Motor.h”
#include “Int_Range.h”
/**
- @brief 避障逻辑,获取超声探测距离,并根据距离决定直行还是旋转
*/
void App_Avoidance_Control();
#endif // !APP_AVOIDANCE_H
2)pp_Avoidance.c
#include “App_Avoidance.h”
#include “Int_LCD1602.h”
void App_Avoidance_Control()
{
u16 ran;
// 获取超声探测距离
ran = Int_Range_GetRange();
#ifdef DEBUG
Int_LCD1602_ShowNum(0, 0, ran);
#endif
if (ran < 300)
{
// 遇到障碍 蜂鸣器响 开始向右旋转
Int_Buzzer_Buzz(100);
Int_Motor_SetLeft(40);
Int_Motor_SetRight(-40);
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 1, "Turn Right! ");
#endif
}
else
{
// 没有障碍直行
Int_Motor_SetLeft(40);
Int_Motor_SetRight(40);
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 1, “Go Straight!”);
#endif
}
}
4.5.4测试
1)改Main.c文件,写入以下内容
#include “App_Avoidance.h”
#include “Int_Range.h”
#include “Int_Motor.h”
#include “Int_LCD1602.h”
#include “Int_Buzzer.h”
#include “Dri_Timer0.h”
void main() {
// 打开中断总开关
EA = 1;
// 初始化定时器
Dri_Timer0_Init();
// 初始化测距模块
Int_Range_Init();
// 初始化电机模块
Int_Motor_Init();
// 初始化蜂鸣器
Int_Buzzer_Init();
// 初始化液晶屏
Int_LCD1602_Init();
while(1) {
// 当计数变量为0时执行避障逻辑,并将i重置为1000
App_Avoidance_Control();
Int_Motor_UpdateSpeed();
}
}
2)新编译并烧录
涉及到超声模块,需要勾选“ALE脚用作P4.5口”。
3)察现象
(1)液晶屏的第一行会显示小车与前方障碍物的距离,单位为mm。示数随距离变化,有微小延迟。
(2)距离大于等于300时,液晶屏第二行显示“Go Straight!”,同时左右两个电机向前转动,蜂鸣器不响。
(3)距离小于300时,液晶屏第二行显示“Turn Right!”,同时左侧电机向前转动,右侧电机向后转动,蜂鸣器响起。
(4)如果可以观察到上述全部现象,则测试通过。
第 5 章遥控功能
5.1功能架构
遥控功能整体架构如下所示:

5.2驱动层
5.2.1串口驱动
5.2.1.1简介
串口驱动主要有两个作用:
和PC通信打印调试信息。
和蓝牙通信实现遥控。
由于蓝牙模块默认波特率9600,这里我们将串口通信波特率初始化为9600。
5.2.1.2代码实现
1)ri_UART.h
5.2.1.3测试
将单片机的TypeC口与PC的USB口相连,确保蓝牙模块没有插入,或者核心板未与小车相连,蓝牙模块如下图所示。

1)code目录下创建Main.c文件,写入以下内容
2)开程序文件

3)录

要注意,小车配备了内置电源,通过TypeC接口连接PC和小车单片机时,要确保内置电源已被切断,否则PC无法识别USB串口,烧录失败。
开关拨动至图示位置则内置电源已被切断。

此外,单片机只有一个串口,单片机烧录和蓝牙模块通信都要用到串口,烧录时,务必确保蓝牙模块未被插入。

如图所示,蓝牙模块母座空置。
4)开串口助手,选择串口,设置波特率

5)开串口并发送数据

6)看结果

看到以上内容则串口驱动测试通过。
5.3应用层
5.3.1遥控模式
5.3.1.1简介
通过微信小程序的蓝牙遥控连接小车蓝牙,并发送遥控信号。小车根据信号行进或者转向。
1)入蓝牙模块
开始之前要将蓝牙模块插入小车左后方的六座排母,如下图所示。

2)牙模块介绍
JDY23蓝牙模块手册如下:

此处的蓝牙模块仅用于透传,所谓透传,是指直接传输数据,不做任何处理。因此,我们可以通过串口驱动直接接收蓝牙模块接收到的数据,作相应处理。
5.3.1.2更改蓝牙发射名称
目前我们的蓝牙发射名称全部是JDY-23,大家在连接蓝牙的时候是无法分辨自己的蓝牙信号的。我们在进行遥控适配前,首先要改变蓝牙模块默认的发射名称。
查看蓝牙手册,如果想改名字,需要通过串口向蓝牙模块发送“AT+NAME名称\r\n”。例如,如果我们想将蓝牙改名为“Atguigu01”,那么我们需要向蓝牙发送“AT+NAMEAtguigu01\r\n”,蓝牙模块会回复“+OK”。改名范例代码如下:
1)ain.c
#include “App_Remote.h”
#include “Int_LCD1602.h”
#include “Dri_UART.h”
#include “Dri_Timer0.h”
#include “Util.h”
void main()
{
u8 len, str[17];
// 打开中断总开关
EA = 1;
// 初始化定时器0
Dri_Timer0_Init();
// 初始化串口
Dri_UART_Init();
// 初始化LCD1602
Int_LCD1602_Init();
while (1)
{
Dri_UART_SendStr("AT+NAMEAtguigu01\r\n");
// 接收串口的数据,存储到字符串变量str中
len = Dri_UART_ReadStr(str);
// 清屏
if (len)
{
Int_LCD1602_Clear();
// 如果接收到的字符串长度不为0,则显示到液晶屏
Int_LCD1602_ShowStr(0, 0, str);
}
Delay1ms(1000);
}
}
当看到LCD上显示“OK”即代表改名成功。
5.3.1.3代码实现及遥控适配
将main.c中改名字和延时的代码删掉,如下所示:
#include “App_Remote.h”
#include “Int_LCD1602.h”
#include “Dri_UART.h”
#include “Dri_Timer0.h”
#include “Util.h”
void main()
{
u8 len, str[17];
// 打开中断总开关
EA = 1;
// 初始化定时器0
Dri_Timer0_Init();
// 初始化串口
Dri_UART_Init();
// 初始化LCD1602
Int_LCD1602_Init();
while (1)
{
// 接收串口的数据,存储到字符串变量str中
len = Dri_UART_ReadStr(str);
// 清屏
if (len)
{
Int_LCD1602_Clear();
// 如果接收到的字符串长度不为0,则显示到液晶屏
Int_LCD1602_ShowStr(0, 0, str);
}
}
}
重新编译并烧录
(1)烧录时,将蓝牙模块取下,并断开内置电源。
(2)烧录完成后插入蓝牙模块,串口会自动切换至蓝牙模块,观察LCD1602的液晶屏,可以看到“+Ready”字样。

(3)蓝牙模块插入后,不能再烧录程序,如果需要重新烧录,将蓝牙模块取下,并点击一次单片机的复位键,或点击两次电源键重启单片机,PC就可以重新连接串口,继续烧录。

2)控器连接小车
(1)微信小程序搜索“乐进智能小车遥控蓝牙ble wifi”,选择下图所示第一个小程序。

(2)进入小程序后,在“蓝牙列表”中选择名为“JDY-23”的设备,单击右侧的“Go Connect”。

(3)片刻后,JDY-23的状态变更为“Go Connect”,同时,小车的液晶屏显示变更为“CONNECTED”。


(4)同时,小程序会自动跳转至“连接详情”菜单,勾选“characteristics”即完成连接。下方五个键为控制菜单,如下图所示。

(5)依次点击“Up”、“Down”、“Left”、“Right”、“Stop”五个按键,液晶屏会出现不同的内容,可以得出如下结论。
① 按下“Up”发送“U”,按下“Down”发送“D”,按下“Left”发送“L”,按下“Right”发送“R”,按下“Stop”发送“S”。
② 按键没有自锁功能,所有按键松开都会立即发送“S”。
基于上述结论,我们对代码做出调整,通过接收到的数据控制电机的行为,从而实现操控小车移动的目的。需要注意的是,我们在接收到指令时为了方便调试,将指令输出到了LCD1602,显示在液晶屏上。而第九章测试时,液晶屏是用来显示小车运行模式信息的,二者会发生冲突。因此,将涉及到液晶显示的语句设置为条件编译,只有存在宏定义变量DEBUG时可用。
3)pp_Remote.h
#ifndef APP__BTCONTROL_H
#define APP__BTCONTROL_H
#include “Dri_UART.h”
#include “Int_Motor.h”
#include “Util.h”
/**
- @brief 遥控逻辑,根据收到的蓝牙信号决定行进方向
*/
void App_Remote_Control();
#endif // !APP__BTCONTROL_H
4)pp_Remote.c
#include “App_Remote.h”
#include “Int_LCD1602.h”
#include “Int_Motor.h”
void App_Remote_Control()
{
u8 len, str[17];
static u8 lastChar = 0;
// 如果收到蓝牙数据,改变运动状态
if (Dri_UART_IsAvailable())
{
len = Dri_UART_ReadStr(str);
Int_LCD1602_Clear();
if (*str == 'S') {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "stop ");
#endif
Int_Motor_SetLeft(0);
Int_Motor_SetRight(0);
} else if (*str == ‘U’) {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "Go Forward! ");
#endif
Int_Motor_SetLeft(40);
Int_Motor_SetRight(40);
lastChar = ‘U’;
} else if (*str == ‘D’) {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "Go Back! ");
#endif
Int_Motor_SetLeft(-40);
Int_Motor_SetRight(-40);
lastChar = ‘D’;
} else if (*str == ‘R’) {
if (lastChar == ‘D’) {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "Back Right! ");
#endif
Int_Motor_SetLeft(-20);
Int_Motor_SetRight(-10);
} else {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "Forward Right! ");
#endif
Int_Motor_SetLeft(20);
Int_Motor_SetRight(10);
}
} else if (*str == ‘L’) {
if (lastChar == ‘D’) {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "Back Left! ");
#endif
Int_Motor_SetLeft(-10);
Int_Motor_SetRight(-20);
} else {
#ifdef DEBUG
Int_LCD1602_ShowStr(0, 0, "Forward Left! ");
#endif
Int_Motor_SetLeft(10);
Int_Motor_SetRight(20);
}
}
#ifdef DEBUG
else {
Int_LCD1602_Clear();
Int_LCD1602_ShowStr(0, 0, str);
}
#endif
}
}
5)改Main.c,写入以下内容
#include “App_Remote.h”
#include “Int_LCD1602.h”
#include “Dri_UART.h”
#include “Dri_Timer0.h”
#include “Int_Motor.h”
void main()
{
// 打开中断总开关
EA = 1;
// 初始化定时器0
Dri_Timer0_Init();
// 初始化串口
Dri_UART_Init();
// 初始化LCD1602
Int_LCD1602_Init();
// 初始化电机
Int_Motor_Init();
while (1)
{
App_Remote_Control();
Int_Motor_UpdateSpeed();
}
}
6)新编译并烧录
7)试
拔下USB-TypeC连接线,插入蓝牙模块,打开内置电源开关,重新通过小程序连接小车。如果可以观察到如下现象,则测试通过。
(1)按下“Up”键,小车前进。
(2)按下“Down”键,小车后退。
(3)先按下“Up”键,再按下“Left”键,小车前进的同时左转。
(4)先按下“Up”键,再按下“Right”键,小车前进的同时右转。
(5)先按下“Down”键,再按下“Left”键,小车后退的同时左转。
(6)先按下“Down”键,再按下“Right”键,小车后退的同时右转。
(7)按下“S”键或松开任意按键,小车逐渐停止。
第 6 章巡线功能
6.1功能架构
巡线功能整体架构如下所示:

6.2接口层
6.2.1光电传感器模块
6.2.1.1简介
1)计思路
光电传感器模块以弧形排列在车头,从而侦测出小车行进方向和轨道的偏离程度,从而进行纠正。当传感器正下方有黑线时,传感器返回信号1;否则返回信号0。
2)D393简介
XD393手册如下所示:

其封装如下所示:

每个XD393提供两组比较器,1IN+、1IN-和1OUT为一组,2IN+、2IN-和2OUT为一组,Vcc接电源,GND接地。当1IN+电势高于1IN-时,1OUT输出高电平,否则输出低电平。当2IN+电势高于2IN-时,2OUT输出高电平,否则输出低电平。1OUT和2OUT的电平只有两个取值,高电平(VCC)或低电平(0V,GND)。
6.2.1.2原理图分析

(1)DY-ITR9909是光电传感器,左半部分为红外发射器,引脚2接高电平,引脚1接低电平。右半部分可以理解为一个光敏电阻,引脚4接高电平,引脚3接地。
(2)当接收不到红外光时,光敏电阻阻值无穷大VB途径光敏电阻至VE为断路,VA和VB电势均与电源保持一致,为5V。
(3)当接收到红外光时,光敏电阻阻值减小,VB至VE导通,光敏电阻阻值与R11串联,二者根据阻值大小分压,从而改变VB的电势。
(4)R12为滑动变阻器(电位器),通过移动滑片可以改变U11.1引脚2的电势,在上图中,滑片位置越靠上,U11.1引脚2电势越高。
(5)U11.1为XD393的其中一组比较器,假设R12的滑片位置已固定。当VB电势为5V时,引脚3电势为5V,大于引脚2的电势,此时引脚1输出高电平5V,VF电势为5V,R9和LED6串联,此时二者两侧电势均为5V,LED6不会亮起。
(6)光敏电阻接收到的红外光越强,R11分压越高,VB电势越低,U11.1引脚3电势越低,当其电势小于引脚2电势时,引脚1输出低电平,即0V,VF电势为0V,LED6亮起。
综上,光电传感器的发射端向物体表面发射红外光,物体将部分红外光反射回去被光电传感器的接收端接收,后者接收的红外光未达到一定值(与器件生产厂家、电位器滑片的位置等有关)时,LED6不亮。反之,当接收端接收的红外光足够多时,LED6亮起。
6.2.1.3代码实现
1)nt_Sensor.h
#ifndef SENSOR_H
#define SENSOR_H
#include “Dri_GPIO.h”
#include “Util.h”
/**
- @brief 获取当前误差,正数偏左,负数偏右
- @return 误差
*/
char Int_Sensor_GetError();
#endif
2)nt_Sensor.c
#include “Int_Sensor.h”
#define LL_VALUE -4
#define LM_VALUE -2
#define MM_VALUE 0
#define RM_VALUE 2
#define RR_VALUE 4
// 最近一次测量的误差
static char s_error;
char Int_Sensor_GetError()
{
// 侦测到黑线的传感器个数
u8 count;
if (LL || LM || MM || RM || RR)
{
count = 0;
s_error = 0;
if (MM)
{
s_error += MM_VALUE;
count++;
}
if (LM)
{
s_error += LM_VALUE;
count++;
}
if (LL)
{
s_error += LL_VALUE;
count++;
}
if (RM)
{
s_error += RM_VALUE;
count++;
}
if (RR)
{
s_error += RR_VALUE;
count++;
}
s_error /= count;
}
return s_error;
}
6.2.1.4测试
1)改Main.c文件,写入以下内容
#include “Int_Sensor.h”
#include “Int_LCD1602.h”
void main()
{
// 初始化LCD1602
Int_LCD1602_Init();
while(1)
{
// 当计数变量减为0时,接收传感器结果,并将i重置为1000
Int_Sensor_GetError();
}
}
2)新编译并烧录
第九章最终测试之前,所有测试程序的编译均在Debug target下完成。
3)试
(1)将小车放置在白色平面,可以看到车头前方的五个指示灯全部亮起,液晶屏显示“Digression!”。

(2)遮挡最左侧的光电传感器,使得仅有P04熄灭,液晶屏显示右偏,temp变量值为4。

(3)遮挡次左侧的光电传感器,使得仅有P00熄灭,液晶屏显示右偏,temp变量值为2。

(4)遮挡最中间的光电传感器,使得仅有P01熄灭,液晶屏无显示。

(5)遮挡次右侧的光电传感器,使得仅有P02熄灭,液晶屏显示左偏,temp变量值为2。

(6)遮挡最右侧的光电传感器,使得仅有P03熄灭,液晶屏显示左偏,temp变量值为4。

出现上述现象,则光电传感器模块测试通过。
6.3应用层
6.3.1循迹模式
6.3.1.1简介
通过获取光电传感器信号确认小车和黑线的偏离角度,通过PID算法算出左右车轮速度,控制小车前进或者后退
6.3.1.2PID算法

6.3.1.3代码实现
1)pp_Patrol.h
#ifndef APP_PATROL_H
#define APP_PATROL_H
#include “Int_Sensor.h”
#include “Int_Motor.h”
/**
- @brief 循迹逻辑
*/
void App_Patrol_Control();
#endif // !APP_PATROL_H
2)pp_Patrol.c
#include “App_Patrol.h”
// 定义PiD参数
#define KP 1000
#define KI 1
#define KD 600
#define MAX_INTEGRAL 1000
static char last_error = 0;
static int integral = 0;
static char App_Patrol_GetPID()
{
int result;
char error;
// 获取当前误差
error = Int_Sensor_GetError();
// 累计当前误差
integral += error;
if (integral > MAX_INTEGRAL)
{
integral = MAX_INTEGRAL;
}
else if (integral < -MAX_INTEGRAL)
{
integral = -MAX_INTEGRAL;
}
// 计算PID
result = KP * error + KI * integral + KD * (error - last_error);
last_error = error;
// 除对应系数,并返回最终结果
result /= 50;
if (result > 80)
{
result = 80;
}
else if (result < -80)
{
result = -80;
}
return result;
}
void App_Patrol_Control()
{
char error;
// 获取PID结果
error = App_Patrol_GetPID();
// 根据PID结果调节左右轮速
Int_Motor_SetLeft(40 + error);
Int_Motor_SetRight(40 - error);
}
第 7 章模式切换
7.1功能架构

7.2接口层
7.2.1独立按键模块
7.2.1.1定时器中断消抖
独立按键模块我们在之前的51单片机中给大家介绍过,这个模块最重要的部分就是按键的消抖。之前我们通过延时完成消抖,但这种消抖过程效率很低,实际生产中用的非常少。在生产环境中,我们一般在定时器中断中调用如下代码完成消抖:
// 每毫秒调用一次
s_key1_status <<= 1;
s_key1_status |= KEY1;
其中key1_status是一个8bit数字,KEY1则是我们的按键传感器信号。key1_status共存在3种情况:
如果KEY1稳定按下,所有KEY1信号都是0,8ms后key1_status值为0;
如果KEY1稳定抬起,所有KEY1信号都是1,8ms后key1_status值为255;
其他所有情况都是抖动。
7.2.1.2代码实现
1)nt_Button.h
#ifndef INT_BUTTON_H
#define INT_BUTTON_H
#include <STC89C5xRC.h>
#include “Util.h”
/**
- @brief 按键模块初始化,负责注册回调函数
*/
void Int_Button_Init();
/**
- @brief sw1是否触发上升沿或下降沿
- @return u8 0代表状态不变 1代表触发了下降沿 2代表出发了上升沿
*/
u8 Int_Button_GetKey1Status();
/**
- @brief sw2是否触发上升沿或下降沿
- @return u8 0代表状态不变 1代表触发了下降沿 2代表出发了上升沿
*/
u8 Int_Button_GetKey2Status();
#endif // !INT_BUTTON_H
2)nt_Button.c
#include “Int_Button.h”
#include “Dri_Timer0.h”
#include “Dri_GPIO.h”
static u8 s_key1_status;
static u8 s_key2_status;
// 标记按键之前的状态:1表示抬起 0表示按下
static bit s_is_sw1_released;
static bit s_is_sw2_released;
/**
- @brief 内部方法,注册到定时器中断中,每ms调用一次,更新按键的状态
*/
static void Int_Button_UpdateStatus()
{
s_key1_status <<= 1;
s_key1_status |= KEY1;
s_key2_status <<= 1;
s_key2_status |= KEY2;
}
void Int_Button_Init()
{
s_key1_status = 0xFF;
s_key2_status = 0xFF;
s_is_sw1_released = 1;
s_is_sw2_released = 1;
Dri_Timer0_RegisterCallback(Int_Button_UpdateStatus);
}
u8 Int_Button_GetKey1Status()
{
// 按键之前的状态为抬起,但此时status为0
if (s_is_sw1_released && s_key1_status == 0)
{
s_is_sw1_released = 0;
return 1;
}
// 按键之前的状态为按下,但此时status为0xFF
if (!s_is_sw1_released && s_key1_status == 0xFF)
{
s_is_sw1_released = 1;
return 2;
}
// 什么情况返回0
return 0;
}
u8 Int_Button_GetKey2Status()
{
// 按键之前的状态为抬起,但此时status为0
if (s_is_sw2_released && s_key2_status == 0)
{
s_is_sw2_released = 0;
return 1;
}
// 按键之前的状态为按下,但此时status为0xFF
if (!s_is_sw2_released && s_key2_status == 0xFF)
{
s_is_sw2_released = 1;
return 2;
}
// 什么情况返回0
return 0;
}
7.2.1.3测试
1)Main.c中写入以下内容。
#include “Dri_UART.h”
#include “Dri_UART.h”
#include “Int_Button.h”
#include “Dri_Timer0.h”
void main()
{
u8 key1_status;
u8 key2_status;
// 初始化串口通信
Dri_UART_Init();
// 初始化定时器
Dri_Timer0_Init();
// 初始化按键
Int_Button_Init();
// 获取 key1 和 key2 的状态,根据状态发送相应数据
while (1)
{
key1_status = Int_Button_GetKey1Status();
key2_status = Int_Button_GetKey2Status();
// 根据 key1 的状态返回数据
if (key1_status == 1)
{
Dri_UART_SendStr("key1 pressed!\n");
}
else if (key1_status == 2)
{
Dri_UART_SendStr("key1 released!\n");
}
// 根据 key2 的状态返回数据
if (key2_status == 1)
{
Dri_UART_SendStr("key2 pressed!\n");
}
else if (key2_status == 2)
{
Dri_UART_SendStr("key2 released!\n");
}
}
}
2)译并烧录
3)口设置同上
4)开串口
5)下按键

在按下或抬起按键时,接收区可以看到以上内容。
7.3应用层
7.3.1模式切换逻辑
7.3.1.1简介
定义小车的运行模式:遥控,循迹,避障。
实现通过外部按键切换模式的方法。
7.3.1.2代码实现
1)pp_ModeSwitch.h
#ifndef APP_MODESWITCH_H
#define APP_MODESWITCH_H
#include “Int_Button.h”
#include “Int_LCD1602.h”
#include “Int_Motor.h”
#define MODE_PATROL 1
#define MODE_REMOTE 2
#define MODE_AVOIDANCE 3
/**
- @brief 模式切换模块初始化方法
*/
void App_ModeSwitch_Init();
/**
- @brief 获取当前的运行模式
- @return 运行模式:1循迹 2遥控 3壁障
*/
u8 App_ModeSwitch_GetMode();
#endif
2)pp_ModeSwitch.c
#include “App_ModeSwitch.h”
#include <STDIO.H>
// 声明模式临时变量和倒计时变量
static u8 s_mode, s_count_down;
// 标记位如果为1, 刷新LCD, 为0不刷新
static bit refresh_flag;
void App_ModeSwitch_Init()
{
s_mode = MODE_REMOTE;
refresh_flag = 1;
s_count_down = 200;
}
u8 App_ModeSwitch_GetMode()
{
// 根据按键切换模式
if (Int_Button_GetKey2Status() == 1)
{
s_mode++;
refresh_flag = 1;
s_count_down = 200;
if (s_mode > MODE_AVOIDANCE)
{
s_mode = MODE_PATROL;
}
Int_Motor_Init();
Int_LCD1602_Clear();
}
// 将模式打印在LCD上
if (refresh_flag)
{
// 倒计时开始时候打印抬头
if (s_count_down == 200)
{
switch (s_mode)
{
case MODE_PATROL:
Int_LCD1602_ShowStr(0, 0, "Mode: Patrol");
break;
case MODE_REMOTE:
Int_LCD1602_ShowStr(0, 0, "Mode: Remote");
break;
case MODE_AVOIDANCE:
Int_LCD1602_ShowStr(0, 0, "Mode: Avoidance");
break;
default:
break;
}
}
// 打印倒计时
if (s_count_down)
{
Delay1ms(10);
if (s_count_down % 50 == 0)
{
Int_LCD1602_ShowNum(1, 0, s_count_down / 50);
}
s_count_down--;
return 0;
}
else
{
// 倒计时结束打印“Go”
Int_LCD1602_ShowStr(1, 0, "Go");
refresh_flag = 0;
}
}
return s_mode;
}
7.3.1.3测试
1)改Main.c文件,写入以下内容
#include “App_Mode.h”
#include “Int_LCD1602.h”
#include “Int_Button.h”
#include “Dri_Timer0.h”
void main()
{
// 打开中断总开关
EA = 1;
// 初始化定时器
Dri_Timer0_Init();
// 初始化按键模块
Int_Button_Init();
// 初始化 LCD1602
Int_LCD1602_Init();
while (1) {
App_Mode_GetStatus();
}
}
}
2)新编译并烧录
3)下按键并观察现象
(1)烧录完成后,直至按下Key2之前液晶屏均为清屏状态。
(2)按下,液晶屏第一行立即变为“Mode:模式名称”,第二行仍未清屏状态。
(3)直至松开Key2,第二行开始出现倒计时,倒计时从3开始,至1结束,最后变为Go。
(4)操作记录如下。

如果出现上述现象,则测试通过。
第 8 章主控函数
8.1代码实现
主函数完成所有模块的初始化,主循环中获取小车当前模式,并按照模式执行相应逻辑。
在Main.c文件中写入以下内容。
#include <STC89C5xRC.H> //包含STC89C52的头文件
#include “Util.h”
#include “Dri_Timer0.h”
#include “Dri_UART.h”
#include “Int_Button.h”
#include “Int_Buzzer.h”
#include “Int_LCD1602.h”
#include “Int_Motor.h”
#include “Int_Range.h”
#include “App_Avoidance.h”
#include “App_ModeSwitch.h”
#include “App_Patrol.h”
#include “App_Remote.h”
void init()
{
Dri_Timer0_Init();
Dri_UART_Init();
Int_Motor_Init();
Int_Buzzer_Init();
Int_Range_Init();
Int_LCD1602_Init();
Int_Button_Init();
App_ModeSwitch_Init();
}
void main()
{
u8 mode;
init();
while (1)
{
mode = App_ModeSwitch_GetMode();
switch (mode)
{
case MODE_PATROL:
App_Patrol_Control();
break;
case MODE_REMOTE:
App_Remote_Control();
break;
case MODE_AVOIDANCE:
App_Avoidance_Control();
break;
default:
continue;
break;
}
Int_Motor_UpdateSpeed();
}
}
8.2编译并烧录
1)注意,编译前要将项目的target切换为Release。
2)开程序文件时,要选择Release下的hex文件,如下图所示。

3)录时要勾选“ALE脚用作P4.5口”,确保超声模块正常工作。
4)录时要将内置电源关闭,蓝牙模块拔下。
5)录完成后,断开PC和单片机的连接,插入蓝牙模块,并打开内置电源。
第 9 章测试
9.1遥控功能测试
多次按下按键2,直至液晶屏出现如下提示,则切换为遥控模式。

按照7.4节的测试方式,小车可以正确响应则测试通过。
9.2巡线模式测试
多次按下按键2,直至液晶屏出现如下提示,则切换为巡线模式。

将小车放到巡线地图上,如图所示:

若小车可以按照线路前进,则测试通过。
需要注意的是,小车如果无法按照线路行进,可能是受到反光的影响,可以从以下几个方向做出调整。
(1)调整传感器的电位器,使传感器更灵敏。
(2)降低小车的巡航速度,给它更多的反应时间。
(3)调整PID算法的相关参数。
9.3避障模式测试
多次按下按键2,直至液晶屏出现如下提示,则切换为避障模式。

在超声模块前方放置障碍物,小车在距离约300mm时会有转向避障,出现上述现象则测试通过。
如果你觉得该项目对你有帮助,全套代码,资料,视频教学文档加绿app:xel4572
如果你觉得该项目对你有帮助,全套代码,资料,视频教学文档加绿app:xel4572
如果你觉得该项目对你有帮助,全套代码,资料,视频教学文档加绿app:xel4572
&spm=1001.2101.3001.5002&articleId=156085257&d=1&t=3&u=145e0ece26c6484c83b5b201f338b2d3)

被折叠的 条评论
为什么被折叠?



