# 5. 项目课程

好了，所有的东西都已经准备完毕，我们正式开始4WD蓝牙多功能小车的编程项目。前面我们从简单的传感器和模块开始，循序渐进完成模块传感器的测试项目，现在再来完成几个不同类型的机器人，最后我们把所有学到的知识结合到一起，完成一个综合的项目：多功能桌面小车。

注意：本项目中的各传感器/模块上标有（G）表示负极，是连接到控制板或传感器扩展板上的G或-或GND；标有（V）表示正极，是连接到控制板或扩展板上的V或VCC或+或5V。

## 第1课 LED灯项目 

### 1.1 项目介绍

![](media/ae80c4ac2d601bf0e96ccb25ed872dcb.jpg)前面我们安装了keyes UNO R3开发板的驱动。接下来的项目我们就要由简单到复杂，一步一步探索Arduino的世界了。首先我们要来完成经典的“Arduino点亮LED”，也就是Blink项目。Blink对于学习Arduino的爱好者而言，是最基础的项目是新手必须经历的一个练习。

LED，发光二极管的简称。由含镓（Ga）、砷（As）、磷（P）、氮（N）等的化合物制成。当电子与空穴复合时能辐射出可见光，因而可以用来制成发光二极管。在电路及仪器中作为指示灯，或者组成文字或数字显示。

为了实验的方便，我们将LED发光二极管做成了一个模块，在第一个项目中，我们用一个最基本的测试代码来控制LED，亮一秒钟，灭一秒钟，来实现闪烁的效果。你可以改变代码中LED灯亮灭的时间，实现不同的闪烁效果。LED模块信号端S为高电平时LED亮起，S为低电平时LED熄灭。

### 1.2 原理图及参数

![](media/85b2076096371fd9ca03671602b1e5ce.png)
![](media/31734302247e9a18efc688056a9c1019.png)

LED模块参数：

控制接口: 数字口

工作电压: DC 3.3-5V

排针间距: 2.54mm

LED显示颜色：红色

### 1.3 项目组件


|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|keyes 草帽LED白发红模块*1|
|-|-|-|
|![](media/fa9659fdadb816de22363e180b01cc51.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/31fb938502d3d519813c391569d6a3f3.png)|
|USB线*1|3Pin 双母头杜邦线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/a86a5db2b0af8b35a94356bc47796b03.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 1.4 接线图

![](media/404ab1579bef4c98dfdc2aa1530c7787.png)

由上图我们可以看到，扩展板是堆叠在开发板上的，LED模块的-接到了扩展板的G,LED模块的+接到了扩展板的5V，LED模块的S已经接到了扩展板上的D9接口，接好线之后我们开始编写代码：

### 1.5 项目代码


```
/*
4WD 蓝牙多功能车
lesson 1.1
Blink
http://www.keyes-robot.com
*/
void setup()
{
  pinMode(9, OUTPUT);// 将数字引脚9初始化为输出
}
void loop() // 循环功能
{ 
  digitalWrite(9, HIGH); // 点亮LED
  delay(1000); // 等待一秒钟
  digitalWrite(9, LOW); // 熄灭LED
  delay(1000); // 等待一秒钟
}
//*******************************************************************
```

### 1.6 项目结果

点击上传程序，你应该看到D9脚接着的LED打开和关闭，而且间隔的时间是一秒钟。

1.7 代码说明:

```
pinMode(9，OUTPUT) 在使用Arduino的引脚之前，你需要告诉开发板它是INPUT还是OUTPUT。我们使用一个内置的“函数”pinMode()来做到这一点。
```

```
digitalWrite(9，HIGH) 当使用引脚作为OUTPUT时，可以将其命令为HIGH（输出5伏）或LOW（输出0伏）。
```

### 1.7 项目拓展

前面我们控制了LED模块亮1秒钟,灭一秒钟，现在我们来拓展一下思路，通过改变delay的时间来改变LED 灯闪烁的频率。

代码如下:


```
/*
4WD 蓝牙多功能车
lesson 1.2
delay
http://www.keyes-robot.com
*/
void setup()
{
  pinMode(9, OUTPUT);// 将数字引脚9初始化为输出
}
void loop() // 循环功能
{
  digitalWrite(9, HIGH); // 点亮LED
  delay(100); // 等待0.1秒钟
  digitalWrite(9, LOW); // 熄灭LED
  delay(100); // 等待0.1秒钟
}
```

怎么样是不是很好理解，就是通过改变delay这个代码的时间，来改变3脚LED亮和灭的频率，不多说，我们上传代码。看看这个LED灯闪烁的频率是不是比之前快了？

## 第2课 LED 亮度的调节 

### 2.1 项目介绍

前面课程中，我们详细的介绍了通过代码控制LED亮灭，实现闪烁的效果。这节课我们使用PWM来控制LED亮度不断地变化，模拟我们呼吸的效果。

PWM是使用数字手段来控制模拟输出的一种手段。使用数字控制产生占空比不同的方波（一个不停在高电平与低电平之间切换的信号)来控制模拟输出。一般来说端口的输入电压只有两个0V与5V。如果想要改变灯的亮度怎么办呢个？有同学说串联电阻，对，这个方法是正确的。但是，如果想要得到不同的亮度，且在不同亮度之间来回变动怎么办呢？不可能不停地切换电阻吧。这种情况下就需要使用PWM了，那它是怎么控制的呢？

![](media/a6dbf9a1d18d8849437f013dcaf2d4a6.png)

对于Arduino的数字端口输出只有LOW与HIGH两个，对应的就是0V与5V的电压输出，可以把LOW定义为0，HIGH定义为1，1秒内让Arduino输出500个0或者1的信号。如果这500个全部为1，那就是完整的5V，如果全部为0，那就是0V。如果010101010101这样输出，刚好一半，端口输出的平均电压就为2.5V了。这个和放映电影是一个道理，咱们所看的电影并不是完全连续的，它其实是每秒输出25张图片。在这种情况下，人的肉眼是分辨不出来的，看上去就是连续的了。PWM也是同样的道理，如果想要不同的电压，就控制0与1的输出比例控制就可以了。当然这和真实的连续输出还是有差别的，单位时间内输出的0,1信号越多，控制的就越精确。

### 2.2 项目组件


|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|keyes 草帽LED白发红模块*1|
|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/31fb938502d3d519813c391569d6a3f3.png)|
|USB线*1|3Pin 双母头杜邦线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/a86a5db2b0af8b35a94356bc47796b03.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 2.3 接线图

Arduino的PWM引脚在3，5，6，9，10，11,上一小节的接线刚刚好在9脚，所以我们这个接线不用变

![](media/404ab1579bef4c98dfdc2aa1530c7787.png)

### 2.4 项目代码

```
/*
4WD 蓝牙多功能车  
lesson 2.1
PWM
http://www.keyes-robot.com
*/
int ledPin = 9; // 定义LED引脚在D9
int value;
void setup () {
  pinMode (ledPin, OUTPUT); // 将ledpin初始化为输出
}
void loop () {
  for (value = 0; value < 255; value = value + 1) {
    analogWrite (ledPin, value); // LED灯逐渐点亮
    delay (5); // 延迟5毫秒
  }
  for (value = 255; value > 0; value = value - 1) {
    analogWrite (ledPin, value); // LED逐渐熄灭
    delay (5); // 延迟5毫秒
  }
}
```

### 2.5 项目结果

代码下载完成后，我们可以看到LED会有个逐渐由亮到灭的一个缓慢过程，而不是直接的亮灭，如同呼吸一般，均匀变化。

### 2.6 代码说明

当我们需要重复执行某句话时，我们可以使用for语句。

for语句格式如下：

![](media/8d32c66be794f5c422e946ea3500aa81.png)

```
for循环顺序如下：

第一轮：1 → 2 → 3 → 4

第二轮：2 → 3 → 4

…

直到2不成立，for循环结束。

知道了这么个顺序之后，回到代码中：

for (int value = 0; value \< 255; value=value+1){

...}

for (int value = 255; value \>0; value=value-1){

...}
```



这两个for语句实现了value的值不断由0增加到255，随之在从255减到0，在增加到255……,无限循环下去。

再看下for里面，涉及一个新函数analogWrite()。

我们知道数字口只有0和1两个状态，那如何发送一个模拟值到一个数字引脚呢？就要用到该函数。观察一下Arduino板，查看数字引脚，你会发现其中6个引脚旁标有“~”，这些引脚不同于其他引脚，它们可以输出PWM信号。

函数格式如下：

```
analogWrite(pin,value)
```

**analogWrite()**函数用于给PWM口写入一个0~255的模拟值。所以，value是在0~255之间的值。特别注意的是，**analogWrite()**函数只能写入具有**PWM**功能的数字引脚，也就是**D3，D5，D6，D9，D10，D11**引脚。

PWM是一项通过数字方法来获得模拟量的技术。数字控制来形成一个方波，方波信号只有开关两种状态（也就是我们数字引脚的高低）。通过控制开与关所持续时间的比值就能模拟到一个0到5V之间变化的电压。开（学术上称为高电平）所占用的时间就叫做脉冲宽度，所以PWM也叫做脉冲宽度调制。

通过下面五个方波来更形象的了解一下PWM。

![](media/553f3d1b6ca04e1aa0479841dd075fa2.png)

PWM示意图

上图绿色竖线代表方波的一个周期。每个analogWrite(value)中写入的value都能对应一个百分比，这个百分比也称为占空比(Duty Cycle)，指的是高电平在周期内占的时间比值，也就是：占空比=高电平时间 /
周期时间。图中，从上往下，第一个方波，占空比为0%，对应的value为0。LED亮度最低，也就是灭的状态。高电平持续时间越长，也就越亮。所以，最后一个占空比为100%的对应value是255，LED最亮。50%就是最亮的一半了，25%则相对更暗。

PWM比较多的用于调节LED灯的亮度。或者是电机的转动速度，电机带动的车轮速度也就能很容易控制了，在玩一些Arduino机器人时，更能体现PWM的好处。

### 2.7 项目拓展

我们不改变灯的脚位，只是改变程序里面delay的值，看看它如何改变渐变效果。

```
/*
4WD 蓝牙多功能车 
lesson 2.2
PWM
http://www.keyes-robot.com
*/
int ledPin = 9; // 定义LED引脚在D9
int value;
void setup () {
  pinMode (ledPin, OUTPUT); // 将ledpin初始化为输出
}
void loop () {
  for (value = 0; value < 255; value = value + 1) {
    analogWrite (ledPin, value); // LED灯逐渐点亮
    delay (30); // 延迟30毫秒
  }
  for (value = 255; value > 0; value = value - 1) {
    analogWrite (ledPin, value); // LED逐渐熄灭
    delay (30); // 延迟30毫秒
  }
}
//**********************************************************
```

上传代码到开发板，看LED渐变的效果是不是慢了一些。

## 第3课 巡线传感器 

### 3.1 项目介绍

![](media/6780dd9ed614a63a3862f503e39b3b7e.png)

循迹传感器实际上是红外传感器。 此处使用的组件是TCRT5000红外管。其工作原理是利用红外光对颜色的不同反射率，然后将反射信号的强度转换为电流信号。在检测过程中，黑色在高电平时处于活动状态，而白色在低电平时处于活动状态。
检测高度为0-3厘米。

KEYES三路循迹模块在一块板上集成了三个TCRT5000红外管，接线和控制更加方便。通过旋转传感器上的可调电位器，可以调节传感器的检测灵敏度。

### 3.2 模块参数

工作电压：3.3-5V（DC）

接口：5PIN

输出信号：数字信号

检测高度：0-3厘米

**特别说明：在测试之前，请旋转传感器上的电位器以调整检测灵敏度。**当将LED调整在ON和OFF之间的阈值时，灵敏度是最好的。

![](media/07f91c262808ec0b48146173080d227e.png)

### 3.3 项目组件

|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|keyes 草帽LED白发红模块*1|Keyes connectors 循迹传感器*1|
|-|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/31fb938502d3d519813c391569d6a3f3.png)|![](media/4f3f0b0638fcb1a64af48afdc3740309.png)|
|XH2.54转PH2.0 5P 连接线*1|3Pin 双母头杜邦线*1|USB线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|![](media/f5dc1ad0f10e043c326aefa252a82575.png)|![](media/a86a5db2b0af8b35a94356bc47796b03.jpg)|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 3.4 接线图

![](media/656e36875efe90bd1ea216206c2407dd.png)

循迹传感器接扩展板的D11、D7、D8引脚（左11，中7，右8）。

### 3.5 项目代码

```
/*
4WD 蓝牙多功能车 
lesson 3.1
Line Track sensor
http://www.keyes-robot.com
*/
int L_pin = 11;  //定义左边循迹传感器的引脚
int M_pin = 7;  //定义中间循迹传感器的引脚
int R_pin = 8;  //定义右边循迹传感器的引脚
int val_L, val_R, val_M; // 定义三个传感器的变量值
void setup()
{
  Serial.begin(9600); // 设置波特率为9600
  pinMode(L_pin, INPUT); //将L_pin设置为输入模式
  pinMode(M_pin, INPUT); //将m_pin设置为输入模式
  pinMode(R_pin, INPUT); //将R_pin设置为输入模式
}
void loop()
{ 
  val_L = digitalRead(L_pin);//读取L_pin的值:
  val_R = digitalRead(R_pin);//读取M_pin的值
  val_M = digitalRead(M_pin);//读取R_pin的值
  Serial.print("left:");//串口打印left
  Serial.print(val_L);//串口打印L_pin的值
  Serial.print(" middle:");//串口打印middle
  Serial.print(val_M);//串口打印M_pin的值
  Serial.print(" right:");//串口打印right
  Serial.println(val_R);//串口打印R_pin的值
  delay(500);//延时0.5秒
}
//****************************************************************************
```

### 3.6 项目结果

上传代码带开发板，打开串口监视，可以看到左中右三个循迹传感器的状态，如果我们用白纸去遮挡传感器，传感器的状态都是0。在没有接收到信号的时候，三个传感器都是高电平状态，显示的数值是1。

![](media/440330dd5cfe3998b1db73a60d4e77e3.png)

### 3.7 代码说明

```
Serial.begin(9600)-初始化串口,串口通信波特率为9600.

pinMode- 定义单片机PIN脚模式是输入还是输出，input是输入，output是输出.

digitalRead-读取引脚电平状态，一般有两种状态，HIGH或者LOW.
```

### 3.8 项目拓展

上面我们了解了循迹传感器的工作原理，接下来我们在第9脚接上一个LED
灯，然后通过读取循迹传感器的状态，来控制LED的亮和灭。如下图接线：

![](media/703cea80f32f06c0d2c23fb215e3a358.png)

我们开始来编写代码：


```
/*
4WD 蓝牙多功能车  
lesson 3.2
Line Track sensor
http://www.keyes-robot.com
*/
int L_pin = 11;  //定义左边循迹传感器的引脚
int M_pin = 7;  //定义中间循迹传感器的引脚
int R_pin = 8;  //定义右边循迹传感器的引脚
int val_L, val_R, val_M; // 定义三个传感器的变量值
void setup()
{
  Serial.begin(9600); // 设置波特率为9600
  pinMode(L_pin, INPUT); //将L_pin设置为输入模式
  pinMode(M_pin, INPUT); //将m_pin设置为输入模式
  pinMode(R_pin, INPUT); //将R_pin设置为输入模式
  pinMode(9, OUTPUT);//将第9脚上接的LED灯设置为输入
}
void loop()
{ 
  val_L = digitalRead(L_pin);//读取L_pin的值:
  val_R = digitalRead(R_pin);//读取M_pin的值
  val_M = digitalRead(M_pin);//读取R_pin的值
  Serial.print("left:");//串口打印left
  Serial.print(val_L);//串口打印L_pin的值
  Serial.print(" middle:");//串口打印middle
  Serial.print(val_M);//串口打印M_pin的值
  Serial.print(" right:");//串口打印right
  Serial.println(val_R);//串口打印R_pin的值
  delay(500);//延时0.5秒
 if (val_L == LOW||val_M ==LOW||val_R == LOW)//if line tracking sensor 检测到信号
  {
    digitalWrite(9, HIGH);//LED 灯灭
  }
  else//如果left line tracking sensor 没有检测到信号
  {
    digitalWrite(9, LOW);//LED 灯亮
  }
}
```

上传代码到开发板，用我们的手去一个个的靠近传感器，我们看看LED灯的状态发生了改变没有？当我们用手去遮挡循迹传感器的时候，我们可以看到LED灯亮起来了。

## 第4课 舵机控制

![](media/415a1937ec707da81173cc3da9ab2ea0.png)

### 4.1 项目介绍

舵机是一种位置伺服的驱动器，主要是由外壳、电路板、无核心马达、齿轮与位置检测器所构成。其工作原理是由接收机或者单片机发出信号给舵机，其内部有一个基准电路，产生周期为20ms，宽度为1.5ms
的基准信号，将获得的直流偏置电压与电位器的电压比较，获得电压差输出。

![](media/69be958142b773acdae33eeef12afed7.png)舵机有很多规格，但所有的舵机都有外接三根线，分别用棕、红、橙三种颜色进行区分，由于舵机品牌不同，颜色也会有所差异，棕色为接地线，红色为电源正极线，橙色为信号线。

舵机的转动的角度是通过调节PWM（脉冲宽度调制）信号的占空比来实现的，标准PWM（脉冲宽度调制）信号的周期固定为20ms（50Hz），理论上脉宽分布应在1ms到2ms
之间，但是，事实上脉宽可由0.5ms 到2.5ms
之间，脉宽和舵机的转角0°～180°相对应。

![](media/c9173ab0ea434f3359aff23a2469be4f.png)

对应的舵机角度值如下:

![](media/586b7664671a400f37588edb56d6cec9.png)

### 4.2 舵机参数

工作电压：DC 4.8V〜6V

可操作角度范围：大约 About 180°(在 500→2500 μsec)

脉波宽度范围：500→2500 μsec

空载转速：0.12±0.01 sec/60（DC 4.8V） 0.1±0.01 sec/60（DC 6V）

空载电流：200±20mA（DC 4.8V） 220±20mA（DC 6V）

停止扭力：1.3±0.01kg·cm（DC 4.8V） 1.5±0.1kg·cm（DC 6V）

停止电流：≦850mA（DC 4.8V） ≦1000mA（DC 6V）

待机电流：3±1mA（DC 4.8V） 4±1mA（DC 6V）

### 4.3 项目组件


|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|Keyes SG90 9G 舵机*1|Keyes SG90 9G 舵机*1|
|-|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/0fd7b4148500d7edd1becf5acfeac337.png)|
|USB线*1|USB线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 4.4 接线图

![](media/f9c8f6d733ac2cc54dbfb5030f04afb8.png)

接线注意：舵机连接到G（GND）、V（VCC）、10，舵机的棕色线是与Gnd(G)相连，红色线与5v(V)相连，橙色线是与数字10相连的。接舵机的时候必须要外接供电，因为驱动舵机的电流要求比较大，一般峰值的情况下接近1A，开发板的电流远远不够。如果不接外接电源，很有可能烧坏开发板。

### 4.5 项目代码1

```
/*
4WD 蓝牙多功能车  
lesson 4.1
Servo
http://www.keyes-robot.com
*/
#define servoPin 10  //servo Pin
int pos; //舵机的角度变量
int pulsewidth; //舵机的脉宽变量
void setup() 
{
  pinMode(servoPin, OUTPUT);  //舵机引脚设置为输出
  procedure(0); //设置舵机的角度为0度
}
void loop() {
  for (pos = 0; pos <= 180; pos += 1) 
  { 
    procedure(pos);              
    delay(15);   //控制舵机转动的速度
  }
  for (pos = 180; pos >= 0; pos -= 1) 
  {
    procedure(pos);              
    delay(15);
  }
}
//控制舵机的函数
void procedure(int myangle) 
{
  pulsewidth = myangle * 11 + 500;  //计算出脉宽值
  digitalWrite(servoPin, HIGH);
  delayMicroseconds(pulsewidth);   //高电平持续的时间，就是脉宽
  digitalWrite(servoPin, LOW);
  delay((20 - pulsewidth / 1000));  //周期是20ms，所以低电平持续剩下的时间
}
//**********************************************************************************
```

### 4.6 项目结果

在上传代码成功，我们可以看到舵机在0°到180°角度范围来回摆动。

其实我们还可以有一种更简单的方法控制舵机，就是使用Arduino的舵机库文件，可以参考Arduino
官方的使用说明：<https://www.arduino.cc/en/Reference/Servo>，

### 4.7 项目代码2

以下是使用了舵机库文件的程序,接线图不变

![](media/247e252bab51cf65a1cacae0db0869ec.png)

项目代码2:

```
/*
4WD 蓝牙多功能车
lesson 4.2 
Servo
http://www.keyes-robot.com
*/
#include <Servo.h>

Servo myservo;  // 创建舵机控制对象
                // 多数控制板最多支持12个舵机对象

int pos = 0;    // 存储舵机位置的变量

void setup()
{
  myservo.attach(10);  // 将舵机连接至10号引脚
}

void loop() 
{
  // 0°→180°正转
  for (pos = 0; pos <= 180; pos += 1) { 
    myservo.write(pos);              // 设定舵机角度
    delay(15);                       // 等待舵机转动到位
  }
  
  // 180°→0°反转
  for (pos = 180; pos >= 0; pos -= 1)
  { 
    myservo.write(pos);              // 设定舵机角度
    delay(15);                       // 等待舵机转动到位
  }
}
```

### 4.8 项目结果2

上传代码成功，上电后，舵机也是在0°到180°角度范围来回摆动。这两个项目的效果是一样的，通常我们使用库文件来控制的比较多。

### 4.9 代码说明

```
#include<Servo.h>是Arduino自带的Servo函数及其语句，下面是舵机函数的几个常用语句：  
1、attach（接口）——设定舵机的接口，只有9或10接口可用。  
2、write（角度）——用于设定舵机旋转角度的语句，可设定的角度范围是0°到180°。  
3、read（）——用于读取舵机角度的语句，可理解为读取最后一条write()命令中的值。  
4、attached（）——判断舵机参数是否已发送到舵机所在接口。  
注：以上语句的书写格式均为“舵机变量名.具体语句（）”例如：myservo.attach(9)。
```



## 第5课 超声波模块项目 

### 5.1 项目介绍

![](media/2a0b40b983f1aead31d43e1662c4257e.png)

HC-SR04超声波传感器像蝙蝠一样使用声纳来确定到物体的距离。它提供出色的非接触范围检测，具有高精度和稳定的读数。它带有超声波发射和接收模块。HC-SR04或超声波传感器被广泛用于创建障碍物检测和距离测量应用以及其他各种应用的电子项目中。在这里，我们介绍使用arduino和超声传感器测量距离的简单方法.

### 5.2 超声波参数

电源：+ 5V DC

静态电流：\<2mA

工作电流：15mA

有效角度：\<15°

测距范围：2cm – 400 cm

分辨率：0.3厘米

测量角度：30度

触发输入脉冲宽度：10uS

### 5.3 项目组件

|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|keyes 草帽LED白发红模块*1|HC-SR04超声波传感器*1|
|-|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/31fb938502d3d519813c391569d6a3f3.png)|![](media/7fcf3f572a14fd99b8439d3c1604ea62.png)|
|HX-2.54 4P 双头 连接线*1|3Pin 双母头杜邦线*1|USB线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|![](media/146f0fc8a39a7d767675ff130236d967.png)|![](media/a86a5db2b0af8b35a94356bc47796b03.jpg)|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 5.4 超声波模块知识

原理：看超声波的图可知，像是有两个眼睛，其一边是发射超声的，一边是接收超声波的，然后检测从发射遇到障碍物返回被接收到所需的时间t，再根据声音在空气中的传播速度大概是343m/s, **距离 = 速度 \* 时间** ，由于超声波发射返回是两段路程了，所以需要除以2，故超声波测到的**距离=（速度 \* 时间）/2**

```
超声波模块的使用方法及时序图：

1、使用GPIO引脚给SR04的Trig引脚至少10μs的高电平信号，触发SR04模块测距功能；

2、触发后，模块会自动发送8个40KHz的超声波脉冲，并自动检测是否有信号返回。这步会由模块内部自动完成。

3、如有信号返回，Echo引脚会输出高电平，高电平持续的时间就是超声波从发射到返回的时间。
```



![](media/7d2dccb20acaf9b17c53063421a5faa0.png)

### 5.5 超声波模块的电路图

![](media/6637578e3d2c6f902ee32931532a83ca.png)

### 5.6 接线图

**接线注意**：超声波传感器模块的VCC引脚连接至keyestudio V5
传感器扩展板的5v(V)，Trig引脚至数字12(S)，Echo引脚至数字13(S)，Gnd引脚至Gnd(G)。

![](media/d659ce5f14906b5e8744fca2f359986d.png)

### 5.7 项目代码

```
/*
4WD 蓝牙多功能车  
lesson 5.1
Ultrasonic sensor
http://www.keyes-robot.com
*/
int trigPin = 12;    // 定义超声波发射脚位
int echoPin = 13;    // 定义超声波接收脚位
long duration, cm, inches;
void setup() {
  Serial.begin (9600);  //开始串口打印
  pinMode(trigPin, OUTPUT); //定义超声波发射为输出
  pinMode(echoPin, INPUT); //定义超声波接收为输入
}
void loop() {
 //传感器由10毫秒或更长时间的高电平脉冲触发
 //预先给短的LOW脉冲以确保稳定高电平脉冲
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
   //读取来自传感器的信号：一个高脉冲
  //持续时间是指从发送到发送的时间（以微秒为单位）
  // ping到接收对象的回声。
  duration = pulseIn(echoPin, HIGH);
//将时间转换为距离
  cm = (duration/2) / 29.1;     // 除以29.1或乘以0.0343
  inches = (duration/2) / 74;   // 除以74或乘以0.0135
  Serial.print(inches);
  Serial.print("in, ");
  Serial.print(cm);
  Serial.print("cm");
  Serial.println();
  delay(200);
}
//**************************************************************************
```

### 5.8 项目结果

上传好测试代码到开发板，打开串口监视器，设置波特率为9600，我们可以看到超声波模块显示的距离，单位是厘米和英寸。用手阻挡超声波模块，我们看到显示距离的数值变小了。

![](media/ff0cd233b65f57153a09458fd5c3c258.png)

### 5.9 代码说明

```
int trigPin- 这个是定义发射超声波的脚位，通常是输出

int echoPin - 这个是定义接收超声波的脚位，通常是输入

cm = (duration/2) / 29.1

inches = (duration/2) / 74
```

我们可以使用以下公式计算距离：

```
距离=（行驶时间/ 2）x声速
```

**声音速度为：343m / s = 0.0343 cm / uS = 1 / 29.1 cm / uS**

或英寸：13503.9in / s = 0.0135in / uS = 1 / 74in / uS

我们需要将传播时间除以2，因为我们必须考虑到波浪已发送，撞击物体然后返回到传感器。

### 5.10 项目拓展

我们刚刚测出了超声波显示的距离，那我们动动脑筋，能不能用测出的距离来做一些控制呢，如果控制一个LED灯的亮和灭。我们来试一下，在D9脚接上一个LED灯模块。

![](media/5c65aebcb35dd1c128148fa651b8b6a4.png)


```
/*
4WD 蓝牙多功能车  
lesson 5.2
Ultrasonic sensor
http://www.keyes-robot.com
*/
int trigPin = 12;    // 定义超声波发射脚位
int echoPin = 13;    // 定义超声波接收脚位
long duration, cm, inches;
void setup() {
  Serial.begin (9600);  //Serial Port begin开始串口打印
  pinMode(trigPin, OUTPUT); //定义超声波发射为输出
  pinMode(echoPin, INPUT); //定义超声波接收为输入
  pinMode(9, OUTPUT);//将第9脚上接的LED灯设置为输入
}
void loop() {
 //传感器由10毫秒或更长时间的高电平脉冲触发
 //预先给短的LOW脉冲以确保稳定高电平脉冲
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
   //读取来自传感器的信号：一个高脉冲
  //持续时间是指从发送到发送的时间（以微秒为单位）
  // ping到接收对象的回声。
  duration = pulseIn(echoPin, HIGH);
//将时间转换为距离
  cm = (duration/2) / 29.1;     // 除以29.1或乘以0.0343
  inches = (duration/2) / 74;   // 除以74或乘以0.0135
  Serial.print(inches);
  Serial.print("in, ");
  Serial.print(cm);
  Serial.print("cm");
  Serial.println();
  delay(200);
 if (cm >= 2 && cm <= 10)//如果超声波测到的距离大于2小于10CM
{
    digitalWrite(9, HIGH);//点亮LED
  }
  else//如果不是
 {
    digitalWrite(9, LOW);熄灭LED
  }
}
//****************************************************************
```

上传好测试代码到开发板，我们用手去靠近超声波传感器，看LED
灯亮起来了没有。

## 第6课 红外接收原理及应用 

### 6.1 项目介绍

![](media/817764a6b04fc6fff7ceb939f94224d5.jpeg)

红外遥控在日常生活中随处可见，它被用来控制各种家电，如电视、音响、录影机和卫星信号接收器。红外遥控是由红外发射和红外接收系统组成的，也就是一个红外遥控器和红外接收模块和一个能解码的单片机组成的。 

![](media/05e43825a247a1d90a5d6d8509a6d14f.png)

红外发射的遥控器发射的38K红外载波信号是由遥控器里的编码芯片对其进行编码。它是以一段引导码，用户码，数据码，数据反码组成，利用脉冲的时间间隔来区别是0还是1信号(高电平低电平之比约为1:1时被认为是信号0)，而编码就是由这些0、1信号组成。同一个遥控器的用户码是不变的，用数据吗不同来分辨遥控器按的键不同。当按下遥控器按键时，遥控器发送出红外载波信号，红外接收器接收到信号时程序对载波信号进行解码，通过数据码的不同来判断按下的是哪个键。单片机由接收到的01信号进行解码，由此判断遥控器按下的是什么键。

红外接收我们用的是一个红外接收模块，主要由红外接收头组成，它是集接收、放大、解调一体的器件，它内部IC就已经完成了解调，能够完成从红外线接收到输出与TTL电平信号兼容的所有工作，输出的就是数字信号。他适用于红外线遥控和红外线数据传输。接收器做成的红外接收模块只有三个引脚，信号线，VCC，GND。与arduino和其他单片机连接通信非常方便。

### 6.2 红外接收的参数

![](media/9dcb924eb37101aa05b6f21d317fbc3b.png)![](media/17b787d350e8882a03f0c31eeee3c9f2.png)工作电压：3.3-5V（DC）

接口：3PIN接口

输出信号：数字信号

接收角度：90度

频率：38khz  
接收距离：10米

右图为红外接收模块的实物图和电路图

项目组件：

|        keyes UNO R3 for arduino 开发板*1        |      Keyes brick L298P 电机驱动扩展板 V1*1      |            keyes 草帽LED白发红模块*1            |
| :---------------------------------------------: | :---------------------------------------------: | :---------------------------------------------: |
| ![](media/67417bd98f12bffd0352f76063e5abbd.png) | ![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png) | ![](media/31fb938502d3d519813c391569d6a3f3.png) |
|          keyes brick 红外接收传感器*1           |              JMP-1 17键红外遥控*1               |                     USB线*1                     |
| ![](media/a6b01539a553d69acfd8b9c5a5e1d500.png) | ![](media/2a93c4291fa6f2a2c8d2cf50babbcc55.png) | ![](media/b54b3d7da383ff2147f8a15a658d6102.jpg) |
|               3Pin 双母头杜邦线*1               |             XH2.54-3Pin+杜邦母双*1              |           18650双节电池盒*1(电池自备)           |
| ![](media/a86a5db2b0af8b35a94356bc47796b03.jpg) | ![](media/c704647f0d48171a099121537d437794.png) | ![](media/c5bf59a8e5cdded95c02334369ab6fdd.png) |

### 6.3 接线图

**接线注意：**由于红外接收传感器输入的数字信号，将红外接收传感器模块的“-”、“+”和S引脚分别用导线连接到keyestudio传感器扩展板G（GND）、V（VCC）、A1，模拟口在数字口不够的情况下，模拟口也可以当数字口使用，模拟口A0相当于数字口14，A1相当于数字口15，以此类推。

![](media/463e4394ec9e12cd1ae22d69007843ce.png)

### 6.4 项目代码

在编写代码之前，要先导入红外的库文件，具体步骤请参考，（3.4 添加库）这个文档。


```
/*
4WD 蓝牙多功能车  
lesson 6.1
IRremote
http://www.keyes-robot.com
*/
#include <IRremote.h>     // IRremote库声明  
int RECV_PIN = A1;        //定义红外接收器的引脚为A1
IRrecv irrecv(RECV_PIN);
decode_results results;   //解码结果放在 decode results结构的 result中
void setup()
{
  Serial.begin(9600);
  irrecv.enableIRIn(); // 启动接收器
}
void loop() {
  if (irrecv.decode(&results))//解码成功，收到一组红外讯号
  {
    Serial.println(results.value, HEX);//以16进制换行输出接收代码
    irrecv.resume(); // 接收下一个值
  }
  delay(100);
}
//*******************************************************
```

### 6.5 项目结果

上传好测试代码，打开串口监视器，设置波特率为9600，拿出遥控器，对准红外接收传感器发送信号，即可看相应按键的键值，如果按键时间过长，容易出现乱码。

![](media/56ad2d8c8eb870420d507cd5d70efc4f.png)

我们通过测试得出的数值，做了一个遥控器按键值表，方便以后使用。

![](media/e7946515345fdf65e1b3d58893f9a207.png)

### 6.6 代码说明

```
irrecv.enableIRIn()-启动红外解码后，这时候IRrecv对象会在后台接收红外线信号。

decode()-接着就可以利用decode()函数持续检查，看看有没有解码成功。

irrecv.decode(&results)  解码成功，这个函数会返回true，并把结果放在results里面，在解码一个红外线信号之后，要运行resume()函数，这样才会持续接收下一组信号。

```

### 6.7 项目拓展

我们刚刚解码了红外遥控器的按键值，那我们能不能用测出的按键值来做一些控制呢，如果控制一个LED灯的亮和灭。我们来试一下，在9脚接上一个LED灯模块。红外接收器的脚位不变,当有遥控器的按键按下时,接在数字引脚9上的发光LED就会点亮，再按一下按键，led熄灭，接线图如下：

![](media/82ed4865d569b677e3185e25e76d39ca.png)


```
/*
4WD 蓝牙多功能车  
lesson 6.2
IRremote
http://www.keyes-robot.com
*/
#include <IRremote.h>
int RECV_PIN = A1;//定义红外接收器的引脚为A1
int LED_PIN = 9; //定义发光LED引脚数字9
int a = 0;
IRrecv irrecv(RECV_PIN);
decode_results results;
void setup()
{
  Serial.begin(9600);
  irrecv.enableIRIn(); // 初始化红外接收器
  pinMode(LED_PIN, OUTPUT); //设置发光LED引脚数字9为输出模式
}
void loop() {
  if (irrecv.decode(&results)) {
    Serial.println(results.value, HEX);
    if (results.value == 0xFF02FD & a == 0) //由上面的键值码，我们用的遥控器上的OK键，如果按下OK键
    {
      digitalWrite(LED_PIN, HIGH); //LED点亮
      a = 1;
    }
    else if (results.value == 0xFF02FD & a == 1) //再按一下
    {
      digitalWrite(LED_PIN, LOW); //LED熄灭
      a = 0;
    }
    irrecv.resume(); // 接收下一个值
  }
}
//**********************************************************************************
```

上传代码带开发板,当遥控器按下OK按键时,LED就会亮，再按一下LED就会灭,同时电脑的串口会出现按键的命令编码.

## 第7课 蓝牙遥控的原理及应用 

### 7.1 项目介绍

蓝牙是近几十年来最流行的一种简单的无线通信模块，易于使用，已在大多数电池供电的设备中使用。蓝牙标准进行了许多升级，以不断满足客户和技术的需求。几年来，发生了许多变化，包括数据传输速率，可穿戴设备和IoT设备以及安全系统的功耗。

在这里，我们将学习HM-10 BLE 4.0。 HM-10是一种随时可用的蓝牙4.0模块。
该模块用于建立无线数据通信。
该模块是使用德州仪器（TI）的CC2540或CC2541蓝牙低功耗（BLE）片上系统（SoC）设计的。

### 7.2 蓝牙参

蓝牙协议：蓝牙V4.0 BLE串口收发无字节限制

工作距离：在开放环境中，实现50-100m超远距离通讯

工作频率：2.4GHz ISM频段

调制方式：GFSK（高斯频移键控）

传输功率：-23dbm，-6dbm，0dbm，6dbm，可通过AT命令修改。

灵敏度：0.1％BER时≤-84dBm

传输速率：异步：6K字节； 同步：6k字节

安全功能：身份验证和加密

支持服务：中央和外围UUID FFE0，FFE1

功耗：自动休眠模式，待机电流400uA〜800uA，传输期间为8.5mA。

电源：5V DC

工作温度：–5至+65摄氏度

### 7.3 项目组件


|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|keyes 草帽LED白发红模块*1|Keyes Bluetooth-4.0 蓝牙4.0 V2*1|
|-|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/31fb938502d3d519813c391569d6a3f3.png)| ![image-20250427110808986](media/image-20250427110808986.png) |
|3Pin 双母头杜邦线*1|USB线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|![](media/a86a5db2b0af8b35a94356bc47796b03.jpg)|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 7.4 接线图

![image-20250427110836747](media/image-20250427110836747.png)

**注意：在上传代码之前不要将蓝牙插在扩展板或UNO板上。烧录完成程序后。烧录完成后，再将蓝牙按上图VCC与扩展板的5V对齐，正确插在扩展板上的串口位置。**

### 7.5 项目代码


```
/*
4WD 蓝牙多功能车  
lesson 7.1
bluetooth
http://www.keyes-robot.com
*/
char ble_val; //字符变量，用于存放蓝牙接收到的值
void setup() {
  Serial.begin(9600);
}
void loop() {
  if (Serial.available() > 0) //判断串口缓存区是否有数据
  {
    ble_val = Serial.read();  //读取串口缓存区的数据
    Serial.println(ble_val);  //打印出来
  }
}
//**********************************************************************
```

**上传代码之前不要连接蓝牙模块，因为代码的上传也是用的串口通信，跟蓝牙的串口通信会有冲突，导致代码上传不成功**

上传代码到开发板，然后再插上蓝牙模块，等待手机发出的指令。

### 7.6 下载蓝牙测试APP

#### 7.6.1 安卓系统手机APP

1. 扫码下载![](media/f5f1708bf257c88dbcfc1782491f073f.png)

   或者进入APP下载链接：http://8.210.52.206/keyes_4WD_Car.apk

**注意：当我们扫码下载的时候需要使用浏览器打开，使用微信扫可能无效**。

2.  下载后安装，安装成功，显示图标如下。

![](media/b2727ac1d2ad70764616d61b5b7b1fb0.png)

3. 点击上图图标，进入APP，显示如下图。

![](media/8051e12c71828866255ca9a92662b2ae.jpg)

4. REV4板上传代码成功后，连接蓝牙，上电后，蓝牙模块上LED闪烁。点击APP![image-20250427111054631](media/image-20250427111054631.png)图标，搜索到蓝牙，显示如下图。

![](media/0fcbbb8c87d427a4bfa12285c0eccde7.jpg)

5. 点击连接，蓝牙连接成功，显示如下图，蓝牙模块上LED变为常亮。

![](media/2e2791e116b1c5b0d2933a197e6e97ff.jpg)

#### 7.6.2 苹果系统手机APP

1.打开App Store。

![](media/29c5a8fc3e39ca5259f31baf812ef041.png)

2.点击搜索，搜索keyestudio，下载搜索到的keyes BT car。

![](media/04e623ed0a9df1c1b7e55cab2a945589.png)

3.打开keyes BT car。

![](media/75e98db6a123422655b70d481c0c0e32.png)

4.开启手机蓝牙，点击左上角的connect按钮，进行蓝牙搜索和连接。

![](media/5e92657dcaeaebeccbe0ef212daf04ec.png)

5.点击桌面小车的![](media/1ec777d4324b73a9faa0973f0beb90f1.png)图片按钮，进入控制桌面小车的界面

![](media/8051e12c71828866255ca9a92662b2ae.jpg)

### 7.7 代码说明

```
Serial.available()的意思是：返回串口缓冲区中当前剩余的字符个数。一般用这个函数来判断串口的缓冲区有无数据，当Serial.available()\>0时，说明串口接收到了数据，可以读取。
```

```
Serial.read()指从串口的缓冲区取出并读取一个Byte的数据，比如有设备通过串口向Arduino发送数据了，我们就可以用Serial.read()来读取发送的数据。
```

### 7.8 项目拓展

上面的项目，我们讲解了蓝牙接收到手机发送的信号并且在开发板的串口显示出来，比如我们按下![](media/4294a850df600cd901f59b2a02fb3237.png)，然后我们就会接收到‘B’，当我们松开的时候又接收到‘S’。那接下来我们就要想一下了，我们可以利用接收到的信号去做一些事情吗，答案是肯定的，我们这里就利用手机发送的命令去打开或者关闭一个LED灯。看接线图，在D9脚接了一个LED。

![image-20250427111413435](media/image-20250427111413435.png)


```
/*
4WD 蓝牙多功能车  
lesson 7.2
bluetooth
http://www.keyes-robot.com
*/
int ledpin = 9;//定义LED灯的脚位在D9
void setup()
{
  Serial.begin(9600);开始串口打印
  pinMode(ledpin, OUTPUT);
}
void loop()
{
  int i;
  if (Serial.available())//判断串口缓存区是否有数据
  {
    i = Serial.read();//读取串口缓存区的数据
    Serial.println("DATA RECEIVED:");
    if (i == 1)//如果串口数据是1
    {
      digitalWrite(ledpin, HIGH);//点亮LED
      Serial.println("led on");//串口打印led on
    }
    if (i == 0)//如果串口数据是0
    {
      digitalWrite(ledpin, LOW);//熄灭LED
      Serial.println("led off");//串口打印led off
    }
  }
}
//*************************************************************************
```

### 7.9  代码说明

上传代码完成后，点击手机APP上![](media/2c775d76241235f2d35df8a104b4ea41.png)以控制LED。当您按下发送\`\`B''时LED将打开，而当您松开发送\`\`S''时，LED将关闭。

![](media/26cb7b1cfaa0d52caa58cb12cc782562.png)

## 第8课 电机的驱动和调速 

### 8.1 项目介绍

驱动电机的方法有很多，我们这个智能车用到的是最常用的L298P这个方案，
L298P是ST意法半导体公司出品的优秀大功率电机专用驱动芯片，可直接驱动直流电机、二相、四相步进电机，驱动电流达2A，电机输出端采用8只高速肖特基二极管作为保护。

我们根据L298P的电路设计了一款扩展板，叠层的设计可直接插接到开发板上使用，降低了用户使用和驱动电机的技术难度。

### 8.2 电路图和示意图

![](media/991bfe4ef7264a6b83455ac668ecc10b.png)

![](media/df0709b0a0ce857a0a5a7a441d3708e6.jpg)

![](media/7fc06405e02b8a7a50bce7750e12f55e.png)

为了调节小车上的4个电机，使得电机电机的驱动方向与后续的课程代码描述一致。驱动板上自带8个跳线帽，也可用于控制电机转向，例如当MA电机接口前方2个跳线帽由横向连接改为纵向连接时，MA电机的转动方向就和原来的转动方向相反。

#### 8.2.1 规格参数

逻辑部分输入电压：DC 5V

驱动部分输入电压：DC 7-12V

逻辑部分工作电流：\<36mA

驱动部分工作电流：\<2A

最大耗散功率：25W（T=75℃）

控制信号输入电平：高电平2.3V\<Vin\<5V  ，低电平-0.3V\<Vin\<1.5V  
工作温度：-25＋130℃

#### 8.2.2 驱动小车运行原理

根据上面电机驱动板的电路图和示意图，我们让MA电机的方向引脚在D2，调速引脚在D6，MB电机的方向引脚在D4，调速引脚在D5，按照以下表格的运动逻辑，我们就可以知道如何通过控制数字口，PWM口控制2个电机转动，从而实现智能小车的行走。其中PWM值范围为0-255，设置数值越大，电机转动越快。（A1接M1电机、A2接M2电机、B1接M3电机、B2接M4电机）

||D2|D6（PWM）|电机组MA（M1、M2）|D4|D5（PWM）|电机组MB （M3、M4）|
|-|-|-|-|-|-|-|
|前进|LOW|200|正转|HIGH|200|正转|
|后退|HIGH|200|反转|LOW|200|反转|
|右旋转|LOW|200|正转|LOW|200|反转|
|左旋转|HIGH|200|反转|HIGH|200|正转|
|停止|/|0|停止|/|0|停止|

### 8.3 项目组件


|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|4.5V 200转/分 单轴减速箱+双头轴马达+250MM PH2.0mm-2P线材*4|
|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/b5951bc181e1ac91cfb214789dfa709c.png)![](media/b5951bc181e1ac91cfb214789dfa709c.png)![](media/b5951bc181e1ac91cfb214789dfa709c.png)![](media/b5951bc181e1ac91cfb214789dfa709c.png)|
||USB线|18650双节电池盒*1|18650电池*2 （电池自配）|
||![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|

### 8.4 接线图

![](media/54fdab2761f7430cbedade518fbf9c55.png)

### 8.5 项目代码

```
/*
4WD 蓝牙多功能车  
lesson 8.1
motor driver shield
http://www.keyes-robot.com
*/
int MA = 2; //定义电机M1,M2方向控制引脚为D2
int PWMA = 6; //定义电机M1,M2速度控制引脚为D6
int MB = 4; //定义电机M3,M4方向控制引脚为D4
int PWMB = 5; //定义电机M3,M4速度控制引脚为D5
void setup() {
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
}
void loop() {
  //前进1秒
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
  delay(1000);

  //后退1秒
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
  delay(1000);

  //左转1秒
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
  delay(1000);

  //右转1秒
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
  delay(1000);

  //停止1秒
  analogWrite(PWMA, 0);
  analogWrite(PWMB, 0);
  delay(1000);
}
//***********************************************************************
```

### 8.6 项目结果

上传代码成功，上电后，智能车前进1秒，后退1秒，左转1秒，右转1秒，停止1秒，循环。

### 8.7 代码说明

```
digitalWrite(MB,LOW);电机的正反转是靠高低电平的转换来实现的，控制电机正反转的脚位用一般的数字脚位就可以了。
```

```
analogWrite(PWMB,200);电机的速度调节是靠PWM来实现的，控制电机调速的脚位必须是Arduino的PWM 脚位。
```

### 8.8 项目拓展

我们来通过调整PWM控制电机的速度，为后面我们控制车速做一个铺垫，接线不变


```
/*
4WD 蓝牙多功能车  
lesson 8.2
motor driver shield
http://www.keyes-robot.com
*/
int MA = 2; //定义电机M1,M2方向控制引脚为D2
int PWMA = 6; //定义电机M1,M2速度控制引脚为D6
int MB = 4; //定义电机M3,M4方向控制引脚为D4
int PWMB = 5; //定义电机M3,M4速度控制引脚为D5
void setup() {
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);

}
void loop() {
  //前进1秒
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 100); //电机B速度为100
  delay(1000);

  //后退1秒
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 100); //电机B速度为100
  delay(1000);

  //左转1秒
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 100); //电机B速度为100
  delay(1000);

  //右转1秒
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 100); //电机B速度为100
  delay(1000);

  //停止1秒
  analogWrite(PWMA, 0);
  analogWrite(PWMB, 0);
  delay(1000);
}
//***********************************************************************
```


上传代码成功，怎么样，电机转动的速度是不是慢了很多？

## 第9课 LED表情灯板 

### 9.1 项目介绍

如果在我们的机器人上加一块表情面板，这将是多么好玩的一件事情，keyes的8\*16点阵就可以满足你的要求。你可以自己创建面部表情，动画，图案或者是其他有趣的显示。8\*16 LED灯板自带128个LED。微处理器（arduino）的数据通过两线总线接口与AiP1640通讯，从而控制模块上128个LED的亮灭，从而让模块上点阵显示你需要的图案。为方便接线，我们还配送一根HX-2.54 4Pin接线。

### 9.2 规格参数

![](media/426284f651896c8c6dfc5f4d2974b3ab.jpg)


工作电压: DC 3.3-5V

功率损耗：400mW

震荡频率：450KHz

驱动电流：200mA

工作温度：-40~80℃

通信方式：I2C通信

### 9.3 项目组件

|keyes UNO R3 for arduino 开发板*1|Keyes brick L298P 电机驱动扩展板 V1*1|USB线*1|18650双节电池盒*1|18650电池*2 （电池自配）|
|-|-|-|-|-|
|![](media/67417bd98f12bffd0352f76063e5abbd.png)|![](media/3dca1bdd1d1420c1d12b16cbf52fee00.png)|![](media/b54b3d7da383ff2147f8a15a658d6102.jpg)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|![](media/c5bf59a8e5cdded95c02334369ab6fdd.png)|
|keye8x16 LED灯板*1|HX-2.54 4P 转杜邦线母单 26AWG*1|
|![](media/4b3b276b9242a386ec26a38cc48b6401.png)|![](media/b289bdda58092c91ab7bbcff4eac9aa1.png)|

### 9.4 8\*16点阵模块详细介绍

### 9.4.1 原理图

### ![](media/8acfd52a64b8ebce33fad4fe207d5ea2.png)9.4.2 控制8\*16点阵的原理

是怎么控制8\*16点阵的每个led灯的呢？要知道一个字节有8位，每一位是0或1，0时关闭led，1时打开led灯，那么一个字节就可以控制点阵一列的led灯开关了，自然16个字节就可以控制16列led灯，即控制了8\*16点阵。

### 9.4.3 接口说明及通讯协议

微处理器（arduino）的数据通过两线总线接口与AiP1640通讯。

通讯协议图如下(SCLK)就是SCL，(DIN)就是SDA ：

![](media/2f63c317b84c809c96550cc1e204664e.png)

①数据输入的开始条件是，SCL为高电平，SDA由高变低。

②数据命令设置，有下图所示方法可选

我们的示例程序中选择 地址自动加1的方式，其二进制是0100 0000对应的十六进制为0x40

![](media/94c238ce64374fdababbfd3dc1738eec.png)

③地址命令设置，有如下图地址可以选

我们示例程序中选了第一个00H，其二进制1100 0000对应的十六进制是0xc0

![](media/9acaaab02c25385cf3dda86947424f15.png)

④数据输入的要求是，在输入数据时当SCL是高电平时，SDA上的信号必须保持不变，只有SCL上的时钟信号为低电平时，SDA上的信号才可以改变。数据的输入是
低位在前，高位在后 传输。

⑤数据传输结束的条件是，SCL为低时，SDA为低，SCL为高时，SDA电平也变为高电平。

⑥显示控制，设置不同脉宽，脉宽有如下图可选

我们示例中选了脉宽为4/16，1000 1010对应的十六进制是0x8A

![](media/dcd8dbbdfbb5ce9c25e3269a7eb89364.png)

对应我们的示例程序来学习会理解的更好。

### 9.4.4 取模工具的使用说明

设置时，我们需要把一个图案转换成1组16个的16位数据，这里就需要用到一个取模软件,这个软件已放入资料文件夹中。使用时打开![](media/c7e1f4b0440706c0cdb7e745ac29844a.png)图标，显示如下图。

![](media/c8281eddce26ce57970d5f152aabe2a4.png)

点击![](media/692d9479a8801057b02c0b3f26861a15.png)这个图标新建图案，根据显示屏规格，设置宽度为16，高度为8，如下图。

![](media/a1c97576e9772caf16f3947709a62a37.png)

初始时发现格点不大，不方便设置，我们可以通过设置模拟动画，设置格点大小，点击如下图。

![](media/655611122bc3f5fd2ee8e5176c3847ca.png)

一直鼠标左键点击![](media/87dbff178a12ce43c6c6f40877483de5.png)，就可以一直放大格点了。

放大后，我们就可以通过用鼠标点击白色区域，设置显示图案了。

![](media/07834404e360fb5a561cab18d039e04d.png)

设置时，鼠标点击（左右键都可以）白色格点，变为黑色；再点击黑色格点，变为白色。黑色代表该格点显示亮起，白色代表格点不显示。显示屏最多能设置16\*8个点显示。设置笑脸显示如下图。

![](media/fa3de1661e05dccdbbd105a9a183b85d.png)

设置参数设置，选择其他选项，设置如下图。设置完成点击![](media/b8373eb149df42e9b559824a8f1ccb82.png)。

![](media/d24b1b5674a1ac003349621db4224f81.png)

![](media/05779d4455696252a12c0df7e184465b.png)

设置取模方式，选择C51格式选择如下图。

![](media/a18329dc5ddfc4d9099b80f62f59d2ae.png)

设置成功后，在以下区域就可以看到对应的16个数据了，只需要将数据复制粘贴在数组中，就可以用直接调用了。（0x00,0x00,0x1C,0x02,0x02,0x02,0x5C,0x40,0x40,0x5C,0x02,0x02,0x02,0x1C,0x00,0x00）

![](media/d3aa71456797375083d34feeffe11d66.png)

### 9.5 接线图

![](media/678879eee681ffef1e455c9c9b8461f3.png)

接线注意： 8x16 LED灯板的GND、VCC、SDA、SCL分别对应的接到keyestudio传感器扩展板-（GND）、+（VCC）、A4、A5进行两线串行通信。（注意：这里是接了arduino IIC的引脚，但是这个模块并不是IIC通讯的，是可以接任意两个引脚的。）

### 9.6 项目代码

点阵显示上面画的微笑表情的代码

```
/*
4WD 蓝牙多功能车  
lesson 9.1
matrix
http://www.keyes-robot.com
*/
//从取摸工具中得到的微笑图案的数据
unsigned char smile[] = {0x00, 0x00, 0x1c, 0x02, 0x02, 0x02, 0x5c, 0x40, 0x40, 0x5c, 0x02, 0x02, 0x02, 0x1c, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
void setup() {
  //设置引脚为输出
  pinMode(SCL_Pin, OUTPUT);
  pinMode(SDA_Pin, OUTPUT);
  //清屏
  //matrix_display(clear);
}
void loop() {
  matrix_display(smile);  //显示微笑表情图案
}
//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址

  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
}
//************************************************************************************

```

### 9.7 项目结果

在keyestudio V4.0开发板上传代码成功，按照接线图接线，拨码开关拨打到右端上电后，看一下，我们的显示屏上是不是显示了一个笑脸。

![](media/d6e69a3fd6ba16c6dc0e34d9cbafdf48.png)

### 9.8 项目拓展

我们利用刚刚学到的取模工具，[http://dotmatrixtool.com/#](http://dotmatrixtool.com/),让点阵循环显示开始图案，前进图案，停止图案，然后清除图案，时间间隔为2000毫秒。

![](media/bba60b6abb9d6a964e9213222448d3a2.png)![](media/609a8b068bb8f95a328718bac3fba986.png)![](media/e7fbd5f40b576712264670f1da25e73a.png)
![](media/aae33c656475a9d989dddf5fc2f6b967.png)

利用取模工具得到的我们要显示的图形代码

开始的代码：

```
0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01
```

前进的代码：

```
0x00,0x00,0x00,0x00,0x00,0x24,0x12,0x09,0x12,0x24,0x00,0x00,0x00,0x00,0x00,0x00
```

后退的代码：

```
0x00,0x00,0x00,0x00,0x00,0x24,0x48,0x90,0x48,0x24,0x00,0x00,0x00,0x00,0x00,0x00
```

左转的代码：

```
0x00,0x00,0x00,0x00,0x00,0x00,0x44,0x28,0x10,0x44,0x28,0x10,0x44,0x28,0x10,0x00
```

右转的代码：

```
0x00,0x10,0x28,0x44,0x10,0x28,0x44,0x10,0x28,0x44,0x00,0x00,0x00,0x00,0x00,0x00
```

停止的代码：

```
0x2E,0x2A,0x3A,0x00,0x02,0x3E,0x02,0x00,0x3E,0x22,0x3E,0x00,0x3E,0x0A,0x0E,0x00
```

清屏的代码：

```
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
```

接线图不变：

![](media/678879eee681ffef1e455c9c9b8461f3.png)

下面就是多个图案切换显示的代码：


```
/*
4WD 蓝牙多功能车  
lesson 9.2
matrix
http://www.keyes-robot.com
*/
//数组，用于储存图案的数据，可以自己算也可以从取摸工具中得到
unsigned char start01[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
unsigned char front[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x12, 0x09, 0x12, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char back[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x48, 0x90, 0x48, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char left[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x00};
unsigned char right[] = {0x00, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char STOP01[] = {0x2E, 0x2A, 0x3A, 0x00, 0x02, 0x3E, 0x02, 0x00, 0x3E, 0x22, 0x3E, 0x00, 0x3E, 0x0A, 0x0E, 0x00};
unsigned char clear[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
void setup() {
  //设置引脚为输出
  pinMode(SCL_Pin, OUTPUT);
  pinMode(SDA_Pin, OUTPUT);
  //清屏
  matrix_display(clear);
}
void loop() {
  matrix_display(start01);  //显示开始图案
  delay(2000);
  matrix_display(front);    //前进图案
  delay(2000);
  matrix_display(STOP01);   //停止图案
  delay(2000);
  matrix_display(clear);    //清屏
  delay(2000);
}

//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址
  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
} 
//*************************************************************************************
```

上传代码到开发板，我们看到表情面板（8\*16点阵显示开始前进停止然后清屏的图案，循环反复）。


![](media/62934fafbccedab49b5e8e62ab52d595.png)

![](media/b676867d8d9b4f1973244a255f46f1fc.png)

![](media/71b8d2089f39a655481e98faceb06136.png)




## 第10课 画地为牢智能车

### 10.1 项目介绍

前面我们详细的介绍了智能车上各个传感器、模块、扩展板的使用方法。在这里我们可以结合前面课程中知识制作一个画地为牢智能车。实验中，我们通过循迹传感器检测智能车底部是否存在黑线，然后根据检测结果控制两个电机的转动，从而把智能车关在黑线圈中即画地为牢。

### 10.2 流程图

画地为牢智能车具体逻辑如下表格。


|检测|中循迹传感器|检测到黑线：高电平|
|-|-|-|
|检测|中循迹传感器|检测到白线：低电平|
|检测|左循迹传感器|检测到黑线：高电平|
|检测|左循迹传感器|检测到白线：低电平|
|检测|右循迹传感器|检测到黑线：高电平|
|检测|右循迹传感器|检测到白线：低电平|

|条件|状态|
|-|-|
|左循迹传感器没检测到黑线且中循迹传感器没检测到黑线且右循迹传感器没检测到黑线|前进（PWM设为200）|
|左循迹传感器检测到黑线 或者中循迹传感器检测到黑线或者右循迹传感器检测到黑线|后退（PWM设为200） 然后左旋转（PWM设为200）|

按照前面思路设计好智能车后，我们就需要按照设计思路开始制作智能车。我们需要设计对应的接线，测试代码，然后接线上传代码，运行，确保智能车能够实现理想中的功能。

### 10.3 接线图

循迹模块+电机

![](media/5e69157a55bb98d4e8eb53e0372367ac.png)

### 10.4 测试代码

```
/*
4WD 蓝牙多功能车  
lesson 10
Stuck in place
http://www.keyes-robot.com
*/
int L_pin = 11; //定义左边传感器引脚为D11
int M_pin = 7; //定义中间传感器引脚为D7
int R_pin = 8; //定义右边传感器引脚为D8
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5
int L_val, M_val, R_val;

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
}

void turnL() { //小车左转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
}

void turnR() { //小车右转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

void setup() {
  Serial.begin(9600); //设置波特率为9600
  pinMode(L_pin, INPUT); //循迹传感器引脚都配置为输入模式
  pinMode(M_pin, INPUT);
  pinMode(R_pin, INPUT);
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
}
void loop() {
  L_val = digitalRead(L_pin); //读取左边传感器的值
  M_val = digitalRead(M_pin); //读中间传感器的值
  R_val = digitalRead(R_pin); //读取右边传感器的值
  if ( L_val == 0 && M_val == 0 && R_val == 0 ) { //当都没有检测到黑线时前进
    advance();
  }
  else { //否则任一巡线传感器检测到黑线就后退再左转
    back();
    delay(500);
    turnL();
    delay(800);
  }
}
```

### 10.5 测试结果

当小车行驶过程中检测到黑线立即撤退，然后左转继续行驶。

## 第11课 循线智能车 

### 11.1 项目介绍

前面我们详细的介绍了画地为牢智能车的实现方法。在这里我们可以结合前面课程中知识制作一个循迹智能车。实验中，我们还是通过循迹传感器检测智能车底部是否存在黑线，然后根据检测结果控制两个电机的转动，从而控制智能车沿着黑线行走。

### 11.2流程图

循迹智能车具体逻辑如下表格。

|检测|中循迹传感器|检测到黑线：高电平|
|-|-|-|
|检测|中循迹传感器|检测到白线：低电平|
|检测|左循迹传感器|检测到黑线：高电平|
|检测|左循迹传感器|检测到白线：低电平|
|检测|右循迹传感器|检测到黑线：高电平|
|检测|右循迹传感器|检测到白线：低电平|

|条件|条件|状态|
|-|-|-|
|中循迹传感器检测到黑线|左循迹传感器检测到黑线并且 右循迹传感器检测到白线|左旋转（PWM设为200）|
|中循迹传感器检测到黑线|左循迹传感器检测到白线并且 右循迹传感器检测到黑线|右旋转（PWM设为200）|
|中循迹传感器检测到黑线|左循迹传感器检测到白线并且 右循迹传感器检测到白线|前进|
|中循迹传感器检测到黑线|左循迹传感器检测到黑线并且 右循迹传感器检测到黑线|前进|
|中循迹传感器检测到白线|左循迹传感器检测到黑线并且 右循迹传感器检测到白线|左旋转（PWM设为200）|
|中循迹传感器检测到白线|左循迹传感器检测到白线并且 右循迹传感器检测到黑线|右旋转（PWM设为200）|
|中循迹传感器检测到白线|左循迹传感器检测到白线并且 右循迹传感器检测到白线|停止|
|中循迹传感器检测到白线|左循迹传感器检测到黑线并且 右循迹传感器检测到黑线|停止|

按照前面思路设计好智能车后，我们就需要按照设计思路开始制作智能车。我们需要设计对应的接线，测试代码，然后接线上传代码，运行，确保智能车能够实现理想中的功能。

### 11.3 接线图

巡线模块+电机

![](media/5e69157a55bb98d4e8eb53e0372367ac.png)

接线注意：用导线把循迹模块连接到电机驱动扩展板上P1接口的G、V、D11、D7、D8；(M1、M2)和(M3、M4)两对时电机分别对应的连接到电机驱动扩展板上的接口A和接口B，电源接到BAT接口。

### 11.4 测试代码

```
/*
4WD 蓝牙多功能车  
lesson 11
Line Tracking Robot
http://www.keyes-robot.com
*/
int L_pin = 11; //定义左边传感器引脚为D11
int M_pin = 7; //定义中间传感器引脚为D7
int R_pin = 8; //定义右边传感器引脚为D8
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5
int L_val, M_val, R_val;

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 150); //电机B速度为150
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 150); //电机B速度为150
}

void turnL() { //小车左转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 150); //电机B速度为150
}

void turnR() { //小车右转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 150); //电机B速度为150
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}
void setup() {
  Serial.begin(9600); //设置波特率为9600
  pinMode(L_pin, INPUT); //循迹传感器引脚都配置为输入模式
  pinMode(M_pin, INPUT);
  pinMode(R_pin, INPUT);
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
}
void loop() {
  L_val = digitalRead(L_pin); //读取左边传感器的值
  M_val = digitalRead(M_pin); //读中间传感器的值
  R_val = digitalRead(R_pin); //读取右边传感器的值
  if (M_val == 1) { //中间检测到黑线
    if (L_val == 1 && R_val == 0) { //如果左边检测到黑线，右边没有，左转
      turnL();
    }
    else if (L_val == 0 && R_val == 1) { //否则如果右边检测到黑线，左边没有，右转
      turnR();
    }
    else { //否则前进
      advance();
    }
  }
  else { //中间没检测到黑线
    if (L_val == 1 && R_val == 0) { //如果左边检测到黑线，右边没有，左转
      turnL();
    }
    else if (L_val == 0 && R_val == 1) { //否则如果右边检测到黑线，左边没有，右转
      turnR();
    }
    else { //否则停止
      stopp();
    }
  }
}
```

### 11.5 测试结果

将驱动扩展板堆叠在UNO R3板上，上传好代码，按照接线图接线，将拨码开关拨至ON端后，智能车能够沿着黑线行走。

## 第12课 超声波跟随智能车 

### 12.1 项目介绍

我们结合硬件知识-各种传感器，模块，电机驱动器，来制造超声波跟随机器人车！实验中，我们通过避障传感器检测智能车左右两方是否存在障碍物，检测智能车和前方障碍物的距离，然后根据这三个数据控制两个电机的转动，从而控制智能车的运动状态。

### 12.2 流程图

跟随智能车具体逻辑如下表格。

|检测|超声波测试前方物体距离|distance（单位：cm）|
|-|-|-|
|条件|distance<8||
|状态|后退（PWM设为100）||
|条件|8＜distance≤13||
|状态|停止||
|条件|13≤distance≤35并且l_val=1并且r_val=1||
|状态|前进（PWM设为100）||
|条件|distance＞35||
|状态|停止||

按照前面思路设计好智能车后，我们就需要按照设计思路开始制作智能车。我们需要设计对应的接线，测试代码，然后接线上传代码，运行，确保智能车能够实现理想中的功能。

### 12.3 接线图

超声波模块+电机+红外避障传感器

![](media/ef5ded98940f8f89b05df5a616a5602c.png)

**接线注意：**A、B两电机分别对应的连接电机驱动扩展板上的接口A和接口B；超声波传感器模块的V引脚至V，T（Trig）引脚至数字12(S)，E（Echo）引脚至数字13(S)，G引脚至G；电源接到BAT接口。

### 12.4 测试代码

```
/*
4WD 蓝牙多功能车  
lesson 12
Ultrasonic Follow Robot
http://www.keyes-robot.com
*/
#include <Servo.h>
Servo myservo;  // create servo object to control a servo
int trigPin = 12; //定义TRIG引脚接D12
int echoPin = 13; //定义ECHO引脚接D13
int distance;
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5

int get_distance() { //超声波测距函数
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH); //给TRIG引脚至少10us的时间触发
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  distance = pulseIn(echoPin, HIGH) / 58; //检测脉冲宽度，并计算出距离
  delay(20);  //延时20ms
  Serial.print("distance:");  //串口打印出距离
  Serial.print(distance);
  Serial.println("cm");
}

void setup() {
  Serial.begin(9600);  //设置波特率为9600
  myservo.attach(10);  // attaches the servo on pin 10 to the servo object
  pinMode(trigPin, OUTPUT); //定义TRIG为输出模式
  pinMode(echoPin, INPUT); //定义ECHO为输入模式
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);

}

void loop() {
  get_distance();  //调用测距函数

  if (distance < 8 ) {//如果距离小于8
    back();//后退
  }
  else if (distance >= 8 && distance < 13) { //如果距离大于等于8，小于13
    stopp();//停止
  }
  else if (distance >= 13 && distance <= 35 ) { //如果距离大于等于13，小于35
    advance();//跟随
  }
  else {//如果以上都不是
    stopp();//停止
  }
}

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 100); //电机B速度为100
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 100); //电机B速度为100
}

void turnL() { //小车左转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 100); //电机B速度为100
}

void turnR() { //小车右转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 100); //电机A速度为100
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 100); //电机B速度为100
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

```


好了，桌面迷你蓝牙智能车跟随功能效果的代码全部编写好了，上传程序，看看精彩的效果！（在上传程序代码前，需要把蓝牙模块取下，否则代码会上传失败。需要上传代码成功后，再连接蓝牙模块。）

### 12.5 测试结果

将驱动扩展板堆叠在UNO R3板上，上传好代码，按照接线图接线，将拨码开关拨至ON端后，智能车能够随着前方障碍物的移动而移动。

## 第13课 自动避障智能车 

### 13.1 项目介绍

在上课程中，我们制作了一个跟随智能车。实际上，利用同样的电子元件，同样的接线方法，我们只需要更改一个测试代码就可以将跟随智能车变为避障智能车。

### 13.2 流程图

避障智能车具体逻辑如下表格。


|检测|左边障碍物距离|distance_l（单位：cm）|
|-|-|-|
|检测|右边障碍物距离|distance_r（单位：cm）|
|检测|中间障碍物距离|distance（单位：cm）|
|条件|条件|状态|
|-|-|-|
|0<distance<20|distance_l > distance_r 如果左边大于右边|向左转|
|0<distance<20|distance_l<=distance_r 如果左边不大于右边|向右转|
|distance>=20|前进|前进|

使用的电子元件，接线方法和课程四一样，更换测试代码，运行，确保智能车能够实现理想中的功能。

### 13.3 接线图

超声波模块+电机+舵机

![](media/adacc5a207fc3bc8cbf8722346dd4d71.png)

**接线注意：**A、B两电机分别对应的连接电机驱动扩展板上的接口A和接口B；超声波传感器模块的V引脚至V，T（Trig）引脚至数字12(S)，E（Echo）引脚至数字13(S)，G引脚至G；电源接到BAT接口，舵机S接D10。

测试代码

```
/*
4WD 蓝牙多功能车  
lesson 13
Ultrasonic Avoiding Robot
http://www.keyes-robot.com
*/
#include <Servo.h>
Servo myservo;  // create servo object to control a servo
//数组，用于储存图案的数据，可以自己算也可以从取摸工具中得到
unsigned char front[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x12, 0x09, 0x12, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char left[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x00};
unsigned char right[] = {0x00, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char STOP01[] = {0x2E, 0x2A, 0x3A, 0x00, 0x02, 0x3E, 0x02, 0x00, 0x3E, 0x22, 0x3E, 0x00, 0x3E, 0x0A, 0x0E, 0x00};
unsigned char clear[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
int trigPin = 12;  //定义TRIG引脚接D12
int echoPin = 13;   //定义ECHO引脚接D13
int distance, distance_l, distance_r;
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5

void setup ()
{
  Serial.begin(9600);     //测量结果将通过此串口输出至 PC 上的串口监视器
  myservo.attach(10);  // attaches the servo on pin 10 to the servo object
  pinMode(echoPin, INPUT);      //设置EchoPin 为输入模式
  pinMode(trigPin, OUTPUT);     //设置超声波数字IO脚模式，OUTPUT为输出
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
  pinMode(SCL_Pin, OUTPUT); //  设置时钟引脚为输出
  pinMode(SDA_Pin, OUTPUT); //设置数据引脚为输出
  matrix_display(clear);// 清屏
  myservo.write(90);  //舵机角度为90
  delay(500);
}

void loop()
{
  distance = get_distance(); //调用测距函数

  if (distance > 0 && distance < 20) { //如果距离小于20且大于0
    stopp();//停止
    matrix_display(STOP01);   //点阵显示停止图案
    delay(100);
    myservo.write(180); //舵机转到180度
    delay(500);
    distance_l = get_distance(); //获取左边的距离
    delay(100);
    myservo.write(0); //舵机转到0度
    delay(500);
    distance_r = get_distance(); //获取右边的距离
    delay(100);
    if (distance_l > distance_r) { //比较距离，如果左边大于右边
      turnL();  //向左转
      matrix_display(left);   //点阵显示向左图案
      delay(1000);
      myservo.write(90);//舵机回到90度
      matrix_display(front);   //点阵显示前进图案

    }
    else { //否则如果右边大于左边
      turnR();//向右转
      matrix_display(right);   //显示右转图案
      delay(1000);
      myservo.write(90);//舵机回到90度
      matrix_display(front);   //显示前进图案
    }
  }

  else { //前方距离小于等于10cm时
    advance();//前进
    matrix_display(front);   //显示前进图案
  }

}

int get_distance() {
  int distance = 0;
  digitalWrite(trigPin, LOW);     // 通过Trig/Pin 发送脉冲，触发 HC-SR04 测距，使发出发出超声波信号接口低电平2μs
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);    // 使发出发出超声波信号接口高电平10μs，这里是至少10μs
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);     // 保持发出超声波信号接口低电平
  distance = pulseIn(echoPin, HIGH) / 58; // 读出脉冲时间,将脉冲时间转化为距离（单位：厘米）
  Serial.println(distance);        //输出距离值
  return distance;
}

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 150); //电机B速度为150
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 150); //电机B速度为150
}

void turnL() { //小车左转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 150); //电机B速度为150
}

void turnR() { //小车右转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 150); //电机A速度为150
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 150); //电机B速度为150
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址
  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
}
```

### 13.5 测试结果

将驱动扩展板堆叠在UNO R3板上，上传好代码，按照课程三接线图接线，将拨码开关拨至ON端后，智能车能够自动避开障碍物行走。

## 第14课 红外遥控智能车 

### 14.1 项目介绍

前面的学习中我们详细的介绍了智能车上各个传感器、模块、扩展板的使用方法。在这里我们可以再结合前面课程中知识制作一个红外控制智能车。在传感器项目第四课中，我们已经测试出红外遥控器各个按键对应的键值。实验中，我们可以通过代码设置（键值），让对应的按键控制智能车对应的运动状态，且相应的状态模式显示在8X16 LED矩阵上。

### 14.2 流程图

循迹智能车具体逻辑如下表格：


|按键：![](media/b11dc5ffa6cccebc6088e5d557d76daf.png)|键值：FF629D|状态：前进|
|-|-|-|
|按键：![](media/ae8110034aacb083151cfd882ee599ba.png)|键值：FFA857|状态：后退|
|按键：![](media/bce9cba2c6d2465fbcce570ad4210eba.png)|键值：FF22DD|状态：左转|
|按键：![](media/ad907a618af86f30d52986bbbd57ba76.png)|键值：FFC23D|状态：右转|
|按键：![](media/9716a4ed61a4064d2f47a7b73eccaf87.png)|键值：FF02FD|状态：停止|

按照前面思路设计好智能车后，我们就需要按照设计思路开始制作智能车。我们需要设计对应的接线，测试代码，然后接线上传代码，运行，确保智能车能够实现理想中的功能。

### 14.3 接线图

电机+红外接收模块

**接线注意：**由于红外接收传感器输入的数字信号，将红外接收传感器模块用导线连接到电机驱动扩展板上的G、V、A1，A、B两组电机分别对应的连接到堆叠在UNO R3板上的电机驱动扩展板上的接口A和接口B，电源接到BAT接口。

![](media/b474b130a3a68000c8ebcf57cf501c01.png)

### 14.4 测试代码

```
/*
4WD 蓝牙多功能车  
lesson 14
Remote Control Robot
http://www.keyes-robot.com
*/
#include <IRremote.h>
int RECV_PIN = A1; //定义红外IO口A1
IRrecv irrecv(RECV_PIN);
decode_results results;//声明一个IRremote库函数独有的变量类型
int IR_val;
//数组，用于储存图案的数据，可以自己算也可以从取摸工具中得到
unsigned char start01[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
unsigned char front[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x12, 0x09, 0x12, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char back01[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x48, 0x90, 0x48, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char left[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x00};
unsigned char right[] = {0x00, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char STOP01[] = {0x2E, 0x2A, 0x3A, 0x00, 0x02, 0x3E, 0x02, 0x00, 0x3E, 0x22, 0x3E, 0x00, 0x3E, 0x0A, 0x0E, 0x00};
unsigned char clear[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机B方向控制引脚为D4
int PWMB = 5; //定义电机B速度控制引脚为D5

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
}

void turnL() { //小车左转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
}

void turnR() { //小车右转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

void setup() {
  Serial.begin(9600);  //设置波特率为9600
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
  pinMode(SCL_Pin, OUTPUT);
  pinMode(SDA_Pin, OUTPUT);
  irrecv.enableIRIn();// 使能红外接收
  matrix_display(clear);//清屏
  matrix_display(start01);
}

void loop() {
  if (irrecv.decode(&results)) { //是否接收到红外遥控信号
    IR_val = results.value;
    Serial.println(IR_val, HEX); //串口打印数据
    switch (IR_val) {
      case 0xFF629D:  advance();  matrix_display(front);  break;
      case 0xFFA857:  back();     matrix_display(back01); break;
      case 0xFF22DD:  turnL();    matrix_display(left);   break;
      case 0xFFC23D:  turnR();    matrix_display(right);  break;
      case 0xFF02FD:  stopp();    matrix_display(STOP01); break;
    }
    irrecv.resume();// 接收下个数据
  }
}



//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址
  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
}
```


好了，上传程序，红外遥控器对准红外接收器，按下红外遥控器对应按键，看看效果吧！**（注意：在上传测试代码前，需要把蓝牙模块取下，否则测试代码会上传失败。需要上传代码成功后，再连接蓝牙模块。）**

### 14.5 测试结果

将驱动扩展板堆叠在UNO R3板上，上传好代码，按照接线图接线，将拨码开关拨至ON端后，我们就能用红外遥控控制智能车运动了。

## 第15课 蓝牙遥控智能车 

### 15.1 项目介绍

前面课程中，我们利用红外控制智能车运动，在这课程中我们可以做一个蓝牙控制智能车。既然是控制智能车，那就有一个控制端和被控制端。课程中我们把手机当做控制端（主机），HM-10蓝牙模块（从机）连接的智能车当做被控制端。使用时，我们需要在手机上安装一个APP，然后连接HM-10蓝牙模块，然后我们利用蓝牙APP上各个按钮，控制智能车实现各种运动状态。

### 15.2 流程图

先取下蓝牙模块，程序代码上传后，再连接蓝牙模块和打开串口监视器，设置波特率为9600。对准蓝牙模块按下手机APP按钮，我们可以看到APP按钮对应的控制字符，如下图。

![](media/790d75eeecb57da117e2649d4a087eac.png)

经过测试，我们得出了手机APP上各个按钮对应的控制字符和各个按钮对应的功能，这里我们整理了一个表格如下：

|按钮：![image-20250427114520268](media/image-20250427114520268.png)|功能：配对连接HM-10蓝牙模块||
|-|-|-|
|按钮:![image-20250427114606056](media/image-20250427114606056.png)|功能：进入蓝牙控制界面||
|按钮:![image-20250427114624930](media/image-20250427114624930.png)|功能：断开蓝牙连接||
|按钮:![](media/6223550c3d79e8e0f594ca15c5238cc8.png)|控制字符：按下：F；松开：S|功能：按下，小车前进；松开就停止|
|按钮:![](media/fb1772180f8aa36921d330c3e34a2edb.png)|控制字符：按下：B；松开：S|功能：按下，小车后退；松开就停止|
|按钮:![](media/5caffc69e999fb670bafdaf2b5cccd6b.png)|控制字符：按下：L；松开：S|功能：按下，小车左旋转；松开就停止|
|按钮:![](media/1ceac7609237f50a37c963a342475c38.png)|控制字符：按下：R；松开：S|功能：按下，小车右旋转；松开就停止|
|按钮:![](media/049343f587e0e7cf19fe8b665d735321.png)|控制字符：按下：a；松开：S|功能：点击，加速，最大加到255|
|按钮:![](media/264f77cce6018584b54f46676fee4247.png)|控制字符：按下：d；松开：S|功能：点击，减速，最小减到0|
|按钮:![](media/b2f9ad09c7912a461fff0af86298346e.png)|控制字符：|功能：点击一下开启手机方向感应控制，再点击一下退出方向感应控制|
|按钮:![](media/5aac2a167009fec900868ee75374b11a.png)|控制字符：点击发送：Y,再次电机发送S|功能：开启避障功能，再次点击退出|
|按钮:![](media/094b1761810aaed0e4ee1f466ea5f00b.png)|控制字符：点击发送：X,再次电机发送S|功能：开启循线功能，再次点击退出|
|按钮:![](media/8eaf85628b37873183a2940fe00d7b93.png)|控制字符：点击发送：U,再次电机发送S|功能：开启超声波跟随功能，再次点击退出|
|按钮:![](media/67ed8e9457f9b71d3ce93163dc3ce1e8.png)|控制字符：点击发送：G,再次电机发送S|功能：开启画地为牢功能，再次点击退出|

### 15.3 接线图

蓝牙+电机

![image-20250427114938053](media/image-20250427114938053.png)

**接线注意：**
蓝牙模块的RXD、TXD、GND、VCC分别对应的接到电机驱动扩展板上的TX、RX、-（GND）、+（VCC），而蓝牙模块的STATE和BRK两引脚不需要接，电源接到BAT接口。

A、B两电机分别对应的连接到电机驱动扩展板上的接口A和接口B；蓝牙模块的RXD、TXD、GND、VCC分别对应的接到电机驱动扩展板上的TX、RX、-（GND）、+（VCC），而蓝牙模块的STATE和BRK两引脚不需要接，电源接到BAT接口。

### 15.4 测试代码

```
/*
  keyes 4WD Multifunctional Smart Car
  lesson 15
  Blluetooth Control Robot
  http://www.keyes-robot.com
*/
//数组，用于储存图案的数据，可以自己算也可以从取摸工具中得到
unsigned char start01[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
unsigned char front[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x12, 0x09, 0x12, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char back01[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x48, 0x90, 0x48, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char left[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x00};
unsigned char right[] = {0x00, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char STOP01[] = {0x2E, 0x2A, 0x3A, 0x00, 0x02, 0x3E, 0x02, 0x00, 0x3E, 0x22, 0x3E, 0x00, 0x3E, 0x0A, 0x0E, 0x00};
unsigned char clear[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5
char blue_val;

void setup() {
  Serial.begin(9600);  //设置波特率为9600
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
  //设置引脚为输出
  pinMode(SCL_Pin, OUTPUT);
  pinMode(SDA_Pin, OUTPUT);
  //清屏
  matrix_display(clear);
  matrix_display(start01);
}
void loop() {
  if (Serial.available() > 0) { //接收到蓝牙信号
    blue_val = Serial.read(); //接收到的信号赋给blue_val
    Serial.println(blue_val);  //串口监视器显示蓝牙信号
    switch (blue_val) {
      case  'F':  advance();  matrix_display(front);  break;  //接收到‘F’前进
      case  'B':  back();     matrix_display(back01); break;  //接收到‘B’后退
      case  'L':  turnL();    matrix_display(left);   break;  //接收到‘L’左旋转
      case  'R':  turnR();    matrix_display(right);  break;  //接收到‘R’右旋转
      case  'S':  stopp();    matrix_display(STOP01); break;  //接收到‘S’停止
    }
  }
}

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
}

void turnL() { //小车左旋转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, 200); //电机B速度为200
}

void turnR() { //小车右旋转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, 200); //电机A速度为200
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, 200); //电机B速度为200
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址
  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
}
```


好了，按住蓝牙APP的前进、后退、左转弯、右转弯、停止、左旋转、右旋转的按钮控制桌面迷你蓝牙智能车分别前进、后退、左转弯、右转弯、停止、左旋转、右旋转的程序代码全编写完了。上传程序，看看效果。（在上传测试代码前，需要把蓝牙模块取下，否则代码会上传失败。需要上传代码成功后，再连接蓝牙模块。）

### 15.5 测试结果

将驱动扩展板堆叠在UNO R3板上，上传好代码，按照接线图接线，将拨码开关拨至ON端后，手机APP连接蓝牙成功后，我们就能用手机APP控制智能车运动并在LED灯板上显示对应的图案了。

按下![](media/7116d72a22a699991156feac4746ae2a.png)按钮，小车前进；按下![](media/0e99537a6cf32884ec107963e5107298.png)按钮，小车后退；按下![](media/e2dba3bd895f7d39fac137d74ff1b834.png)按钮，小车左旋转；按下![](media/3149880e1a7d4b1d61a43d586caade5a.png)按钮，小车右旋转；点击一下![](media/aa34fbcb6fb84503689c22e567e15881.png)按钮，开启手机方向感应控制，再点击一下![](media/aa34fbcb6fb84503689c22e567e15881.png)按钮，退出方向感应控制。

## 第16课 蓝牙调速智能车 

### 16.1 项目介绍

前面课程中，我们利用蓝牙控制智能车，在这课程中我们做一个蓝牙可以控制速度的智能车。既然要控制智能车速度，我们可以将速度定义一个变量speeds来表示。项目中我们只要改变这是变量speeds就可以改变智能车的速度啦。下面让我们通过代码来实现。

流程图：

按照前面思路设计好智能车后，我们就需要按照设计思路开始制作智能车。我们需要设计对应的接线，测试代码，然后接线上传代码，运行，确保智能车能够实现理想中的功能。

### 16.2 接线图

蓝牙+电机

![image-20250427115140534](media/image-20250427115140534.png)

接线跟上一课一样

### 16.4 测试代码

```
/*
4WD 蓝牙多功能车  
lesson 16
Bluetooth control speed
http://www.keyes-robot.com
*/
//数组，用于储存图案的数据，可以自己算也可以从取摸工具中得到
unsigned char start01[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
unsigned char front[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x12, 0x09, 0x12, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char back01[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x48, 0x90, 0x48, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char left[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x00};
unsigned char right[] = {0x00, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char STOP01[] = {0x2E, 0x2A, 0x3A, 0x00, 0x02, 0x3E, 0x02, 0x00, 0x3E, 0x22, 0x3E, 0x00, 0x3E, 0x0A, 0x0E, 0x00};
unsigned char speed_a[] = {0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0xff, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00};
unsigned char speed_d[] = {0x00, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0xff, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00};
unsigned char clear[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5
int speeds = 150; //初始化速度为150
char blue_val;

void setup() {
  Serial.begin(9600);  //设置波特率为9600
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
  //设置引脚为输出
  pinMode(SCL_Pin, OUTPUT);
  pinMode(SDA_Pin, OUTPUT);
  //清屏
  matrix_display(clear);
  matrix_display(start01);
}
void loop() {
  if (Serial.available() > 0) { //接收到蓝牙信号
    blue_val = Serial.read(); //接收到的信号赋给blue_val
    Serial.println(blue_val);  //串口监视器显示蓝牙信号
    switch (blue_val) {
      case  'F':  advance();  matrix_display(front);  break;  //接收到‘F’前进
      case  'B':  back();     matrix_display(back01); break;  //接收到‘B’后退
      case  'L':  turnL();    matrix_display(left);   break;  //接收到‘L’左旋转
      case  'R':  turnR();    matrix_display(right);  break;  //接收到‘R’右旋转
      case  'S':  stopp();    matrix_display(STOP01); break;  //接收到‘S’停止
      case  'a':  speeds_a(); matrix_display(speed_a); break;  //接收到‘a’加速
      case  'd':  speeds_d(); matrix_display(speed_d); break; //接收到‘d’减速
    }
  }
}

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void turnL() { //小车左旋转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void turnR() { //小车右旋转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

void speeds_a() { //增速函数
  while (1) {
    Serial.println(speeds);  //显示速度
    if (speeds < 255) { //最大增到255
      speeds++;
      delay(10);  //调节增速的速度
    }
    blue_val = Serial.read();
    if (blue_val == 'S')break; //接收到‘S’停止加速
  }
}

void speeds_d() { //减速函数
  while (1) {
    Serial.println(speeds);  //显示速度
    if (speeds > 0) { //最小减到0
      speeds--;
      delay(10);    //调节减速的速度
    }
    blue_val = Serial.read();
    if (blue_val == 'S')break; //接收到‘S’停止减速
  }
}

//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址
  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
}
```

### 16.5 测试结果

按下![](media/b2d6cb9c7c86786d8a5067ed7536dd31.png)按钮，小车加速；按下![](media/f70a0831084cf8578de1c3d256453480.png)按钮，小车减速.

## 第17课 多功能智能小车 

### 17.1 项目介绍

在前面课程中，我们只是让智能车实现单个功能，那我们能不能把所有功能合在一起呢？能，在这一课程中，我们利用一个代码测试智能车，智能车包含前面课程中讲到的所有功能，我们利用手机蓝牙APP上按钮自动切换各种功能,简单方便。

### 17.2 流程图

按照前面思路设计好智能车后，我们就需要按照设计思路开始制作智能车。我们需要设计对应的接线，测试代码，然后接线上传代码，运行，确保智能车能够实现理想中的功能。

### 17.3 接线图

**接线注意：**

循迹模块连接到电机驱动扩展板上P1接口的G、V、D11、D7、D8；超声波传感器模块的VCC引脚连接至连接到电机驱动扩展板上，V引脚至V，T（Trig）引脚至数字12(S)，E（Echo）引脚至数字13(S)，G引脚至G；红外接收传感器模块用导线连接到电机驱动扩展板上的G、V、A1；（M1、M2），（M3、M4）B两组电机分别对应的连接到电机驱动扩展板上的接口A和接口B；舵机接数字口10；LED点阵屏接A4、A5管脚（不一定要接IIC引脚）；蓝牙模块的RXD、TXD、GND、VCC分别对应的接到电机驱动扩展板上的TX、RX、-（GND）、+（VCC），而蓝牙模块的STATE和BRK两引脚不需要接，电源接到BAT接口。

![image-20250427115441286](media/image-20250427115441286.png)

### 17.4 测试代码


```
/*
  keyes 4WD Multifunctional Smart Car
  lesson 17
  Bluetooth control multifunctional 4WD robot
  http://www.keyes-robot.com
*/
#include <IRremote.h>  //导入红外的库
int RECV_PIN = A1; //定义IO口A1
IRrecv irrecv(RECV_PIN);
decode_results results;//声明一个IRremote库函数独有的变量类型
#include <Servo.h>
Servo myservo;  // create servo object to control a servo
//数组，用于储存图案的数据，可以自己算也可以从取摸工具中得到
unsigned char start01[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
unsigned char front[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x12, 0x09, 0x12, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char back01[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x48, 0x90, 0x48, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char left[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x44, 0x28, 0x10, 0x00};
unsigned char right[] = {0x00, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
unsigned char STOP01[] = {0x2E, 0x2A, 0x3A, 0x00, 0x02, 0x3E, 0x02, 0x00, 0x3E, 0x22, 0x3E, 0x00, 0x3E, 0x0A, 0x0E, 0x00};
unsigned char speed_a[] = {0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0xff, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00};
unsigned char speed_d[] = {0x00, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0xff, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00};
unsigned char clear[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define SCL_Pin  A5  //设置时钟引脚为 A5
#define SDA_Pin  A4  //设置数据引脚为 A4
int IR_val;
char blue_val;

int L_pin = 11; //定义左边传感器引脚为D11
int M_pin = 7; //定义中间传感器引脚为D7
int R_pin = 8; //定义右边传感器引脚为D8
int L_val, M_val, R_val;

int MA = 2; //定义电机A方向控制引脚为D2
int PWMA = 6; //定义电机A速度控制引脚为D6
int MB = 4; //定义电机A方向控制引脚为D4
int PWMB = 5; //定义电机A速度控制引脚为D5
int speeds = 150; //初始化速度为150

int trigPin = 12; //TRIG引脚接D12
int echoPin = 13; //ECHO引脚接D13
int distance, distance_l, distance_r;

void setup() {
  Serial.begin(9600); //设置波特率为9600
  myservo.attach(10);  // attaches the servo on pin 10 to the servo object
  myservo.write(90);  //舵机角度为90
  delay(500);
  pinMode(L_pin, INPUT); //循迹传感器引脚都配置为输入模式
  pinMode(M_pin, INPUT);
  pinMode(R_pin, INPUT);
  //设置引脚为输出
  pinMode(SCL_Pin, OUTPUT);
  pinMode(SDA_Pin, OUTPUT);
  pinMode(trigPin, OUTPUT); //定义TRIG为输出模式
  pinMode(echoPin, INPUT); //定义ECHO为输入模式
  pinMode(MA, OUTPUT); //配置电机引脚为输出模式
  pinMode(PWMA, OUTPUT);
  pinMode(MB, OUTPUT);
  pinMode(PWMB, OUTPUT);
  irrecv.enableIRIn();// 使能红外接收
  //清屏
  matrix_display(clear);
  matrix_display(start01);
}

void loop() {
  if (Serial.available() > 0) { //接收到蓝牙信号
    blue_val = Serial.read(); //接收到的信号赋给blue_val
    Serial.println(blue_val);  //串口监视器显示蓝牙信号
    switch (blue_val) {
      case  'F':  advance();  matrix_display(front);   break;  //接收到‘F’前进
      case  'B':  back();     matrix_display(back01);  break;  //接收到‘B’后退
      case  'L':  turnL();    matrix_display(left);    break;  //接收到‘L’左旋
      case  'R':  turnR();    matrix_display(right);   break;  //接收到‘R’右旋
      case  'S':  stopp();    matrix_display(STOP01);  break;  //接收到‘S’电机停止转动，功放停止
      case  'a':  speeds_a(); matrix_display(speed_a); break;  //接收到‘a’加速
      case  'd':  speeds_d(); matrix_display(speed_d); break; //接收到‘d’减速
      case  'U':  follow();  break; //接收到‘U’，进入跟随模式
      case  'Y':  avoid();   break;  //接收到‘Y’，进入避障模式
      case  'G':  prison();  break;  //接收到‘G’，画地为牢模式
      case  'X':  track();   break;  //接收到‘X’，巡黑线模式
    }
  }
  if (irrecv.decode(&results)) { //是否接收到红外遥控信号
    IR_val = results.value;
    Serial.println(IR_val, HEX); //串口打印数据
    switch (IR_val) {
      case 0xFF629D:  advance();  matrix_display(front);  break;  //前进
      case 0xFFA857:  back();     matrix_display(back01); break;  //后退
      case 0xFF22DD:  turnL();    matrix_display(left);   break;  //左转
      case 0xFFC23D:  turnR();    matrix_display(right);  break;  //右转
      case 0xFF02FD:  stopp();    matrix_display(STOP01); break;  //停止
    }
    irrecv.resume();// 接收下个数据
  }
}

void advance() { //小车前进
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void back() { //小车后退
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void turnL() { //小车左旋转
  digitalWrite(MA, HIGH); //电机A反转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, HIGH); //电机B正转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void turnR() { //小车右旋转
  digitalWrite(MA, LOW); //电机A正转
  analogWrite(PWMA, speeds); //电机A速度为speeds
  digitalWrite(MB, LOW); //电机B反转
  analogWrite(PWMB, speeds); //电机B速度为speeds
}

void stopp() { //小车停止
  analogWrite(PWMA, 0); //电机A速度为0
  analogWrite(PWMB, 0); //电机B速度为0
}

void speeds_a() { //增速函数
  while (1) {
    Serial.println(speeds);  //显示速度
    if (speeds < 255) { //最大增到255
      speeds++;
      delay(10);  //调节增速的速度
    }
    blue_val = Serial.read();
    if (blue_val == 'S')break; //接收到‘S’停止加速
  }
}

void speeds_d() { //减速函数
  while (1) {
    Serial.println(speeds);  //显示速度
    if (speeds > 0) { //最小减到0
      speeds--;
      delay(10);    //调节减速的速度
    }
    blue_val = Serial.read();
    if (blue_val == 'S')break; //接收到‘S’停止减速
  }
}

int get_distance() {
  int distance = 0;
  digitalWrite(trigPin, LOW);     // 通过Trig/Pin 发送脉冲，触发 HC-SR04 测距，使发出发出超声波信号接口低电平2μs
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);    // 使发出发出超声波信号接口高电平10μs，这里是至少10μs
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);     // 保持发出超声波信号接口低电平
  distance = pulseIn(echoPin, HIGH) / 58; // 读出脉冲时间,将脉冲时间转化为距离（单位：厘米）
  Serial.println(distance);        //输出距离值
  return distance;
}

void follow() {
  int follow_flag = 1;
  while (follow_flag) {
    distance = get_distance(); //调用测距函数
    if (distance < 8 ) {//如果距离小于8
      back();//后退
    }
    else if (distance >= 8 && distance < 13) { //如果距离大于等于8，小于13
      stopp();//停止
    }
    else if (distance >= 13 && distance <= 35 ) { //如果距离大于等于13，小于35
      advance();//跟随
    }
    else {//如果以上都不是
      stopp();//停止
    }
    blue_val = Serial.read();
    if (blue_val == 'S') { //接收到‘S’退出循环，小车停止
      follow_flag = 0;
      stopp();
    }
  }
}

void avoid() {
  int avoid_flag = 1;
  while (avoid_flag) {
    distance = get_distance(); //调用测距函数

    if (distance > 0 && distance < 20) { //如果距离小于20且大于0
      stopp();//停止
      matrix_display(STOP01);   //点阵显示停止图案
      delay(100);
      myservo.write(180); //舵机转到180度
      delay(500);
      distance_l = get_distance(); //获取左边的距离
      delay(100);
      myservo.write(0); //舵机转到0度
      delay(500);
      distance_r = get_distance(); //获取右边的距离
      delay(100);
      if (distance_l > distance_r) { //比较距离，如果左边大于右边
        turnL();  //向左转
        matrix_display(left);   //点阵显示向左图案
        delay(1000);
        myservo.write(90);//舵机回到90度
        matrix_display(front);   //点阵显示前进图案

      }
      else { //否则如果右边大于左边
        turnR();//向右转
        matrix_display(right);   //显示右转图案
        delay(1000);
        myservo.write(90);//舵机回到90度
        matrix_display(front);   //显示前进图案
      }

    }
    else { //前方距离小于等于10cm时
      advance();//前进
      matrix_display(front);   //显示前进图案

    }
    blue_val = Serial.read();
    if (blue_val == 'S') { //接收到‘S’退出循环，小车停止
      avoid_flag = 0;
      stopp();
    }
  }
}

void prison() {
  int prison_flag = 1;
  while (prison_flag) {
    L_val = digitalRead(L_pin); //读取左边传感器的值
    M_val = digitalRead(M_pin); //读中间传感器的值
    R_val = digitalRead(R_pin); //读取右边传感器的值
    if ( L_val == 0 && M_val == 0 && R_val == 0 ) { //当没有检测到黑线时前进
      advance();
    }
    else { //否则任一巡线传感器检测到黑线就后退再左转
      back();
      delay(500);
      turnL();
      delay(800);
    }
    blue_val = Serial.read();
    if (blue_val == 'S') { //接收到‘S’退出循环，小车停止
      prison_flag = 0;
      stopp();
    }
  }
}

void track() {
  int track_flag = 1;
  while (track_flag) {
    L_val = digitalRead(L_pin); //读取左边传感器的值
    M_val = digitalRead(M_pin); //读中间传感器的值
    R_val = digitalRead(R_pin); //读取右边传感器的值
    if (M_val == 1) { //中间检测到黑线
      if (L_val == 1 && R_val == 0) { //如果左边检测到黑线，右边没有，左转
        turnL();
      }
      else if (L_val == 0 && R_val == 1) { //否则如果右边检测到黑线，左边没有，右转
        turnR();
      }
      else { //否则前进
        advance();
      }
    }
    else { //中间没检测到黑线
      if (L_val == 1 && R_val == 0) { //如果左边检测到黑线，右边没有，左转
        turnL();
      }
      else if (L_val == 0 && R_val == 1) { //否则如果右边检测到黑线，左边没有，右转
        turnR();
      }
      else { //否则停止
        stopp();
      }
    }
    blue_val = Serial.read();
    if (blue_val == 'S') { //接收到‘S’退出循环，小车停止
      track_flag = 0;
      stopp();
    }
  }
}

//这个函数用于点阵屏显示
void matrix_display(unsigned char matrix_value[])
{
  IIC_start();  //调用数据传输开始条件的函数
  IIC_send(0xc0);  //选择地址
  for (int i = 0; i < 16; i++) //图案数据有16个字节
  {
    IIC_send(matrix_value[i]); //传输图案的数据
  }
  IIC_end();   //结束图案数据传输
  IIC_start();
  IIC_send(0x8A);  //显示控制，选择脉宽为4/16
  IIC_end();
}
//传输数据开始的条件
void IIC_start()
{
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
}
//传输数据
void IIC_send(unsigned char send_data)
{
  for (char i = 0; i < 8; i++) //每个字节有8位
  {
    digitalWrite(SCL_Pin, LOW); //将时钟引脚SCL_Pin拉低，才可以改变SDA的信号
    delayMicroseconds(3);
    if (send_data & 0x01) //根据字节的每一位是1还是0来设置SDA_Pin的高低电平
    {
      digitalWrite(SDA_Pin, HIGH);
    }
    else
    {
      digitalWrite(SDA_Pin, LOW);
    }
    delayMicroseconds(3);
    digitalWrite(SCL_Pin, HIGH); //将时钟引脚SCL_Pin拉高，停止数据的传输
    delayMicroseconds(3);
    send_data = send_data >> 1;  //一位一位的检测，所以将数据右移一位
  }
}
//数据传输结束的标志
void IIC_end()
{
  digitalWrite(SCL_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, LOW);
  delayMicroseconds(3);
  digitalWrite(SCL_Pin, HIGH);
  delayMicroseconds(3);
  digitalWrite(SDA_Pin, HIGH);
  delayMicroseconds(3);
}
```


好了，蓝牙多功能控制智能车的程序都已经编写好了，上传程序，实际操作下看看效果。（在上传程序代码前，需要把蓝牙模块取下，否则代码会上传失败。需要上传代码成功后，再连接蓝牙模块。）

### 17.5 测试结果

将驱动扩展板堆叠在UNO R3板上，上传好代码，按照接线图接线，将拨码开关拨至ON端后，手机APP连接蓝牙成功后，我们就能用手机APP控制智能车运动了。我们可以通过按下对应按钮实现对应功能，通过停止钮来停止功能。

注意：利用安卓系统手机APP点击![](media/07774c50588977f9ab9398976b2be309.png)，测试语音控制时，不能实现语音控制功能。
