很多时候需要Arduino和其他设备需要通信,用来接受控制命令,返回监测到的数据等等。比如:和电脑主机相连,和手机相连。

相连的方式不外乎以下几种,

  1. USB连接,通过一根数据线把智能硬件和设备连接起来;
  2. 蓝牙连接,智能硬件和设备通过蓝牙相连。采用这种方式需要一个蓝牙模块;
  3. WIFI连接,智能硬件和设备都连接到同一个WIFI路由器上,让彼此通过WIFI网络传输数据。采用这种方式需要一个WIFI模块;
  4. 红外遥控连接,智能硬件接收遥控器发出的红外信号,实现单方面的数据接收。采用这种方式需要一个红外模块;

7.1 串口通信

在介绍前面列举出的各种通信方案之前,我们先来了解下串口通信,串口通信是Arduino最基本的数据通信方式,Arduino开发板会提供一个至多个用于通信用的串口接口。

其实我们之前的学习过程中,已经不知不觉使用过了串口通信,比如调试信息的输出、程序部署到开发板。虽然你看到的是我们使用的USB接口,但实际上USB接口是和串口绑定在一起的。所以实际上,使用的还是串口通信。

不同开发板,串口引脚的位置有些不同。不过在电路板上都标注着TXRX标志。TX表示发送数据的引脚,RX表示接受数据的引脚。

  1. UNO的串口硬件位置
    第7节 数据通信 - 图1
    USB接口和0、1引脚都是对应着串口-Serial。
  2. MEGA的串口硬件位置
    第7节 数据通信 - 图2
    MEGA有多个串口,
    USB接口和0、1引脚对应着串口-Serial
    18、19引脚对应着串口1-Serial1
    16、17引脚对应着串口2-Serial2
    14、15引脚对应着串口3-Serial3

每一个串口都由一个输入引脚(RX)和输出引脚(TX)组成。硬件接收和发送数据的功能,就是通过对应的引脚实现的。

串口通信是最基础的通信方式,Arduino开发板的USB接口就是使用了串口通信,相当于USB接口直接和串口做了连接。

如果要使用外接的wifi和蓝牙模块,也需要将它们的通信引脚与Ardiono上的串口引脚进行连接。它们都是利用了串口通信来实现数据的交换。

可以说串口是所有通信的桥梁,不管用什么模块实现数据的交换,数据最后都要汇聚到串口这里来。

红外遥控接口没有使用串口,我们最后来介绍。

7.1.1 串口数据的读写

虽然连接串口的设备不同(USB口,WIFI,蓝牙),但是读写串口数据的方式都是相同的。

在进行读写之前,要判断一下串口是否可用,

  1. if(Serial.available())
  2. {
  3. //这里面对串口操作才会有意义
  4. }

当我们想在代码中使用这些串口读取数据的时候,可以,

  1. 读一个字节,```cpp //通过UNO或MEGA的Serial读数据, //读一个字节 int data = Serial.read();

//通过MEGA的Serial2读数据, //读一个字节 int data = Serial2.read();

  1. 2. 读到数组缓冲区-`buffer`中,```cpp
  2. //通过UNO或MEGA的Serial读数据,
  3. //读到的数据放在buffer的空间当中,
  4. //返回值nRead表示成功读取到的字节数
  5. int length = 128;
  6. char * buffer = new char[length];
  7. int nRead = Serial.readBytes(buffer, length)
  8. //通过MEGA的Serial2读数据,
  9. //读到的数据放在buffer的空间当中,
  10. //返回值nRead表示成功读取到的字节数
  11. int length = 128;
  12. char * buffer = new char(length);
  13. int nRead = Serial2.readBytes(buffer, length)

当我们想在代码中使用这些串口发送数据的时候,可以,

  1. 发送一个byte,```cpp //通过UNO或MEGA的Serial发数据 Serial.write(‘A’);

//通过MEGA的Serial2发数据 Serial2.write(‘A’);

  1. 2. 发送一个缓冲区的内容,```cpp
  2. //通过UNO或MEGA的Serial发数据
  3. //result表示发送成功的byte数
  4. uint8_t data[] = "hello arduino";
  5. int len = sizeof(data);
  6. int result = Serial.write(data, len);
  7. //通过MEGA的Serial2发数据
  8. //result表示发送成功的byte数
  9. uint8_t data[] = "hello arduino";
  10. int len = sizeof(data);
  11. int result = Serial2.write(data, len);


以上内容是通过串口发送和接受数据的共同方式,请一定要记住,不管最终是通过USB还是蓝牙,还是WIFI模块,都是这样操作的。
Serial的使用方法,可以参考Arduino对应的官方文档章节

7.2 USB通信

通过USB口进行数据的读写我们已经非常熟悉了。

其实我们之前的学习过程中,已经不知不觉使用过了串口通信,比如调试信息的输出、程序部署到开发板。虽然你看到的是我们使用的USB接口,但实际上USB端口已经在开发板上完成了串口的连接,我们只需要使用就好,不要单独去连线了。所以,这种方案使用的还是串口通信。

7.3 蓝牙通信

蓝牙连接是采用较多的一个方案,主要原因是,这是一个无线通信的方式,没有数据线的牵绊,而且Arduino蓝牙模块的价格相对便宜,实现的成本低,而且编程很简单。

下面展示了HC-05蓝牙模块的引脚布局,详细各位从引脚的标注也能猜到各个引脚的作用。其中,使用4个引脚(输出TX、输入RX、电源VC、接地GND)连接开发板就能完成组装了。(其他型号的蓝牙模块,也和它差不多)

第7节 数据通信 - 图3

7.3.1 蓝牙连接

我们经常见到的蓝牙模块有HC-05 HC-06。还有一种形式的蓝牙模块,它就远不止4个引脚了。

第7节 数据通信 - 图4

我们上面采用的蓝牙模块,就是功能最为简单的HC-06蓝牙模块,它只能够被动的接收其它蓝牙模块发起的连接请求(不能主动发起连接),连接时的配对密码默认是1234

第7节 数据通信 - 图5

这里使用了MEGA开发板来进行说明。如果使用UNO开发板原理也是一样的。

根据之前串口连接的原则,我们将蓝牙模块与Arduino开发板用插线连接起来。我们选择将蓝牙模块的RX TX连接到MEGA开发板(或者UNO)的0和1引脚--使用Serial这个串口。

第7节 数据通信 - 图6

或者使用扩展板来连接蓝牙模块,这里我们使用HC-06举例,将HC-06连接到UNO或MEGA扩展板的Serial接口上,

扩展板引脚 连接 蓝牙模块引脚
0组V(电源VC) <—-> 电源VC
0组G(接地GND) <—-> 接地GND
1组S(TX) <—-> 输入RX
0组S(RX) <—-> 输出TX

第7节 数据通信 - 图7

当开发板通电(USB数据线连接上电脑)以后,蓝牙模块的指示灯就亮了。此时它就进入了工作状态:其他蓝牙设备就可以查找并连接这个外接的蓝牙模块了。

*注意开发板和外接通信模块相连的时候后,是TX连着RX,一个发送,另一个当然就是接收了;并且VC/GND和TX/RX不能接反了,不然可能会烧毁蓝牙模块。
电源的两根线不能接到数据发送的两个的引脚上,否则会烧毁蓝牙模块
电源的两根线不能接到数据发送的两个的引脚上,否则会烧毁蓝牙模块
电源的两根线不能接到数据发送的两个的引脚上,否则会烧毁蓝牙模块

不过你一定会遇到一个大坑:当你的蓝牙设备连接到串口引脚后,你会发现程序无法通过串口从电脑端部署到Arduino开发板上了。原因很简单,USB传输数据要使用串口,蓝牙模块传世数据也要使用串口,这个串口太受欢迎了,被两个模块同时占用着。

解决办法有下面几种:

  • 部署代码的时候,把蓝牙模块拔掉。部署完成后再把蓝牙模块接回去。
  • 更换开发板,把UNO换成MEGA,毕竟MEGA上的串口要多出来好几个,大家各用各的就可以了。
  • 使用软串口,这个方法我们后面来讲。

7.3.2 蓝牙数据的读写

接下来,我们做个简单的程序:让蓝牙模块接受其他蓝牙设备发送过来的数据,每次收到该数据后,又把数据原封不动的回传给发送设备,

第7节 数据通信 - 图8

  1. setup()中,初始化好蓝牙设备的波特率,为9600,因为我们硬件连接的是Serial,所以就要初始化Serialcpp void setup() { //初始化蓝牙使用的串口波特率 Serial.begin(9600); }

  2. loop()中,不停的尝试读取串口(蓝牙设备)收到的消息,一旦收到,就通过串口(蓝牙设备)把收到的内容原封不动的发送出去,```cpp void loop() { //如果可以读取数据 if (Serial.available()) {

    1. //创建读取数据用的缓存
    2. int length = 128;
    3. char * buffer = new char[length];
    4. //读取数据到buffer中
    5. int nReadSize = Serial.readBytes(buffer, length);
    6. //如果成功读取到数据,就原封不动的发回去
    7. if(nReadSize > 0)
    8. {
    9. Serial.write(buffer, nReadSize);
    10. }
    11. //删除分配的空间,避免内存泄漏
    12. delete buffer;

    } } ```
    这个用法其实和使用普通串口没有区别,我们的代码操作的都是串口,至于串口连接的是具体什么设备(蓝牙、WIFI),我们都不用去关注了。

7.3.3 使用软串口

刚才的方案中,蓝牙模块数据传输占用了UNO板唯一的串口,会导致无法通过串口进行调试。为此,我们可以使用软串口技术,也就是使用其他引脚来接收蓝牙模块的数据,但读写数据的方式又和读写串口的方式保持一致。就好像通过软件的方式创造了一个新的串口来模拟之前的硬件串口。

软件代码模拟的串口的细节不需要我们去理解和深究,直接使用就可以了。对于我们使用者来说,不用去区分它到底是真的从硬件获取的数据,还是从虚拟的软件上获取到的数据。

为此,开发板引脚的连接,就需要做下调整:硬件串口(0、1引脚)保持不被占用,选用其他引脚(比如5和6)作为接收和发送引脚。

扩展板引脚 连接 蓝牙模块引脚
6组V(电源VC) <—-> 电源VC
6组G(接地GND) <—-> 接地GND
6组S(TX) <—-> 输入RX
5组S(RX) <—-> 输出TX

注意,蓝牙模块的输入输出对应着开发板的输出输入。

在代码端做如下修改,

  1. #include <SoftwareSerial.h>
  2. //初始化蓝牙使用的软串口引脚
  3. //第一个参数:数据接收的引脚
  4. //第二个参数:数据发送的引脚
  5. SoftwareSerial softSerial(5, 6);
  6. void setup()
  7. {
  8. //初始化蓝牙使用的串口波特率
  9. Serial.begin(9600);
  10. //初始化蓝牙使用的软串口波特率
  11. softSerial.begin(9600);
  12. }
  13. void loop()
  14. {
  15. //如果可以读取蓝牙数据
  16. if (softSerial.available())
  17. {
  18. //创建读取数据用的缓存
  19. int length = 128;
  20. char * buffer = new char[length];
  21. //读取数据到buffer中
  22. int nReadSize = softSerial.readBytes(buffer, length);
  23. //如果成功读取到数据,就原封不动的发回去
  24. if(nReadSize > 0)
  25. {
  26. softSerial.write(buffer, nReadSize);
  27. //使用真正的串口打印调试数据
  28. Serial.println("feedback data");
  29. }
  30. //删除分配的空间,避免内存泄漏
  31. delete buffer;
  32. }
  33. }

这里面使用了#include <SoftwareSerial.h>来使用软串口,这是Arduino自身就提供的一个软件功能,我们拿来用就可以了。

另外,大家也能注意到,软串口的用法和真正硬件串口的用法一致,不需要我们重新去学习,这大大简化了开发者的学习成本。

7.4 WIFI通信

使用WIFI模块来连接,需要进行的操作要稍微复杂些,不过复杂的主要在于WIFI网络的接入,后续的读取和发送数据,与蓝牙模块通过Serial读写数据是完全一样的。

我们选用的WIFI模块是ESP8266_WIFI模块。我们这里暂时不介绍了以后专门章节介绍。

7.5 IRDA红外遥控

红外遥控与之前几种数据通信不太一样:

  1. 不使用串口,而是通过数字接口获取数据;
  2. 通信是单向的,只能从遥控器发送给接收器;

下图是我们将要使用的红外遥控发送(遥控器)和接收端模块:

第7节 数据通信 - 图9

当按下遥控器上的一个按钮后,接收器就会收到一段二进制代码,比如:

  1. 150CB559BCBBCBCBCBCBCBBCB21B21C20C21B21B21B21B21C20CBC20CBCBCBB21CBCBB21CBB21C20C21BCB21B
  2. DCABB559BCBCBCBCBCBCBCBCB21C20C21B22A21B21C20C21BCB21B21BCBCBCB21BCB21BCBCB21C20C20CBC21B
  3. E86AB358CCBBCBCBCBCBCBBCB21C20C21B21B21B22B20C20CBB22BCB21BCBCB21CBB21CBB21BCB21C20CCB21B
  4. ...

将这段代码解码后,我们能得到发送的数据包含了:

  • 遥控器的类型(decode type):比如NEC、JVC等,对照表如下:| 遥控器类型 | 对应代码 | 遥控器类型 | 对应代码 | 遥控器类型 | 对应代码 | | —- | —- | —- | —- | —- | —- | | NEC | 1 | SONY | 2 | RC5 | 3 | | RC6 | 4 | DISH | 5 | SHARP | 6 | | PANASONIC | 7 | JVC | 8 | SANYO | 9 | | MITSUBISHI | 10 | UNKNOWN | -1 | | |
  • 按钮可能对应的数值(value),这个值对不同的遥控器类型可能是不同的,以下只是列出来供大家感性认识下。具体的值,需要大家在开发的时候查阅手册,或者根据后面的测试代码自己验证。| 按钮名称 | 对应数值 | 按钮名称 | 对应数值 | 按钮名称 | 对应数值 | | —- | —- | —- | —- | —- | —- | | CH- | FFA25D | CH | FF629D | CH+ | | | << | FF22DD | >> | FF02FD | || | FFC23D | | - | FC8FEB4F | + | FFA857 | EQ | FF906F | | 0 | FF6897 | 100+ | FF9867 | 200+ | FFB04F | | 1 | FF30CF | 2 | FF18E7 | 3 | FF7A85 | | 4 | FF10EF | 5 | FF38C7 | 6 | FF5AA5 | | 7 | FF42BD | 8 | FF4AB5 | 9 | FF52AD |

7.5.1 硬件连接

IRDA模块的连接非常简单,接收端可以直接连接在开发板上面。

IRDA模块引脚 开发板引脚
IN 6组SIG
VCC 6组5V
GND 6组GND

如下图所示,

第7节 数据通信 - 图10

7.5.2 红外数据的读写

红外遥控模块,对数据的读取是通过数据引脚获取的,而且获取的这些数据是原始的二进制码(前面我们已经看到过它的样子了),因此需要做进一步的解析。我们将采用第三方提供的库文件进行这个操作(实际上是源代码)。
第7节 数据通信 - 图11

前面的章节介绍了在Arduino IDE中使用第三方库的方式:将库文件放到Arduino IDE认识的Library路径下。比如下图位置,
第7节 数据通信 - 图12

使用的时候在.ino文件顶部,用<>引用头文件;

  1. #include <IRremote.h>

这里采用一种新的用法:将库文件放到本项目代码所在的目录下,
第7节 数据通信 - 图13
使用的时候在.ino文件顶部,用""引用头文件;

  1. #include "IRremote.h"

本例会将库文件放到项目目录下。然后,修改源码如下:

  1. // 库源码放在本项目所在目录
  2. #include "IRremote.h"
  3. // 设置接收数据的引脚
  4. int RECV_PIN = 6;
  5. // 初始化IRDA模块
  6. IRrecv irrecv(RECV_PIN);
  7. // 准备读取数据的数据结构
  8. decode_results results;
  9. void setup() {
  10. // 初始化引脚
  11. pinMode(RECV_PIN, INPUT);
  12. // 初始化串口
  13. Serial.begin(9600);
  14. // 启动IRDA接收模块,随时接收数据
  15. irrecv.enableIRIn();
  16. }
  17. void loop() {
  18. // 解码成功
  19. if (irrecv.decode(&results)) {
  20. // 解码后的遥控器类型代码
  21. Serial.print("decode type:");
  22. Serial.print(results.decode_type, HEX);
  23. // 解码后的遥控器按钮代码
  24. Serial.print(" value: ");
  25. Serial.print(results.value, HEX);
  26. // 接收到的原始数据内容(没有解码)
  27. Serial.print(" raw data: ");
  28. for(int i=0; i<results.rawlen; i++) {
  29. Serial.print(results.rawbuf[i], HEX);
  30. }
  31. Serial.println("");
  32. // 清空IRDA当前缓存的数据,可以处理下一条接收到的数据
  33. irrecv.resume();
  34. }
  35. }

特别需要注意的是:每处理完一个指令后,一定要使用irrecv.resume()进行缓存的清空,否则该数据还在,下一条待处理数据仍然是它。

假如你希望更加深入的了解如何读取Arduino开发板引脚的数据,并如何进行解码的,以及想知道decode_results数据结构中,到底有哪些数据,请参考库文件源码,里面都有很详细的实现和解释。这里附上一段源码中对decode_results的定义,供大家感性认识:

  1. // Results returned from the decoder
  2. class decode_results {
  3. public:
  4. int decode_type; // NEC, SONY, RC5, UNKNOWN
  5. unsigned int panasonicAddress; // This is only used for decoding Panasonic data
  6. unsigned long value; // Decoded value
  7. int bits; // Number of bits in decoded value
  8. volatile unsigned int *rawbuf; // Raw intervals in .5 us ticks
  9. int rawlen; // Number of records in rawbuf.
  10. };

7.5.3 观察

将程序部署到开发板上,按动遥控器按钮,可以看到串口调试窗口中,会出现对应按钮的数据。

第7节 数据通信 - 图14

请注意valueFFFFFFFF这样的数据,它们是按住某个按钮不放,IRDA接收端收到的数据,表示和之前接收到的数据一样。


至此,Arduino通过外接外接无线模块接收和发送数据的功能都能完整的实现了。