这篇文章来源于DevicePlus.com英语网站的翻译稿。
今天,我们将讨论arduino通信协议的有关内容。设备往往需要相互通信以中继所处环境相关信息,显示其状态变化,或请求执行辅助操作。在进行任何电子设备爱好相关研究时,您将需要使用多个不同的传感器或多个不同的模块(如ESP8266),届时必然会遇到一个或多个主流的通信协议。在本教程中,我们将会介绍电子设备使用的标准通信协议,并使用Arduino Uno对其进行详细说明。
二进制数字系统
设备间的通信通过数字信号进行。在讨论通信协议之前,我们将首先讨论如何传输这些信号。
在数字信号中,数据由一系列的高电平到低电平或低电平到高电平的脉冲进行传输,并且切换非常迅速。数字信号中的高电平和低电平分别代表1和0,当按照顺序连在一起时,就携带了可由微控制器编译的信息。听说过位和字节吗?这些1和0是位,当它们以8个为一组时,就称之为位,当它们以8个为一组时,就称之为字节!
一个字节看起来大概是这样:10111001
事实证明,这个由8位组成的序列代表了一个数字,就像597这个数字代表了597一样。每个数字占据了一个位置值,该位置值中的1或者0表示该位置值被计数了多少次。
在597的示例中,5表示有5个100,9表示有9个10,7表示有7个1。加在一起,就表示5个100+9个10+7个1(500 + 90 + 7)……或597。因为1是100 ,10是101,100是102等等,因此这叫做基于10的系统!在基于10的系统中,每个数字的取值范围为0到9(0到10-1)。
现在我们再来看10111001。我们可以看出这是基于2的系统,因为每个数字的取值范围为0到1。我们可以说每个数字都是2的幂,这意味着10111001实际为1 * 27 + 0 * 26 + 1 * 25 + 1 * 24 + 1 * 23 + 0 * 22 + 0 * 21 + 1 * 20,或者说为基于10的系统中的185。
可以想象,您可以拥有基于任何数字的数字系统!常见的一些数字为2、8、10和16。为了简单起见,数学家为这些常见的数字系统命了名——基于2的为二进制,基于8的为八进制,基于10的为十进制,基于16的为十六进制。每个数字系统都遵循相同的原理:每个数字代表该基数的幂被计数的次数,并且每个数字的值都只能在0到(基数-1)之间。
了解不同的基数系统很有用,因为字节和数据通常以不同的方式进行表达。可以看出,写B9(十六进制)比写10111001(二进制)容易。在软件中,二进制数以0b作为前缀,八进制数以0作为前缀,十六进制数以0x作为前缀。十进制数没有前缀。
知道如何在基数之间进行转换也很有用,因为一些很酷的数学技巧通常用二进制数进行表达。但是,出于本教程的目的,我们仅讲述到这里。请查看以下本教程的附录来获取有关这些技巧的文章!
3 种协议:UART,SPI和I2C
电气工程行业使用三种通信协议对电子设备进行标准化,以确保设备之间的兼容性。将设备以几种协议为中心进行标准化,意味着设计者可以通过掌握每个通信协议的一些基本概念实现任何设备之间的交互。UART,SPI和I2C这三种协议的实现方式不同,但都会达到相同的目的:将数据高速传输到任何兼容的设备上。
1. UART
我们将要介绍的第一个通信协议是通用异步收发器(UART)。UART是一种串行通信,因为数据是一位一位依次进行传输的(我们将在稍后介绍)。设置UART通信的接线非常简单:一根用于传输数据(TX)的线,另一根用于接收数据(RX)的线。如您所料,TX线用于发送数据,RX线用于接收数据。使用串行通信的设备的TX线和RX线将一起形成一个串行端口,通过该端口可以进行通信。
图1:UART的硬件连接图
UART一词实际上是指管理串行数据打包和转换的板载硬件。如果希望设备能够通过UART协议进行通信,就必须具有该硬件!在 Arduino Uno上,有一个专用于与Arduino所连接计算机之间进行通信的串行端口。对!通用串行总线USB正是一个串行端口!在Arduino Uno上,USB的连接通过板载硬件配置为成两个数字引脚GPIO 0和GPIO 1,可用于涉及与计算机以外的电子设备进行串行通信的项目。
图2:GPIO 0 和GPIO 1是串口RX和TX。任何GPIO引脚都可以通过SoftwareSerial库用作串口RX或TX。
您还可以使用SoftwareSerial Arduino 库(SoftwareSerial.h)将其他GPIO引脚用作串口RX和TX线。
UART之所以成为异步,是因为不使用试图相互通信的两个设备之间的同步时钟信号进行通信。由于通信速率不是通过这种稳定信号定义的,“发送方”设备无法确定“接收方”设备是否获取了正确的数据。因此,设备将数据分成了固定大小的块,以确保接收到的数据与发送的数据相同。
UART数据包如下所示:
图3:UART数据包视图/ ©electric imp
通过UART进行通信的设备会发送预定义大小的数据包,其中包含有关消息的开始与结束,以及确认消息是否接收的附加信息。例如,为了开始通信,发送设备将发送线拉低,指示数据包开始发送。然而,目前遇到的问题是,与同步通信方式相比,UART速度较慢,因为传输的数据只有一部分用于设备的应用程序(其余部分用于通信本身!)。
在大多数嵌入式平台(例如Arduino)上实现UART串行通信时,用户不用在位的数据级别上进行通信,平台通常提供更高级别的软件库,用户只需在这些软件库中处理通信内容即可。在Arduino平台上,用户可以使用Serial和SoftwareSerial库为自己的项目实现UART通信。
以下是有关Arduino Serial和SoftwareSerial的初始化和使用的C++简要参考。
Serial 和 SoftwareSerial 方法(method) | 目的 | 代码 | 释义 |
Constructor (仅SoftwareSerial) | 定义GPIO引脚为UART RX线和TX线。 | SoftwareSerial comms (2 , 3); | 定义GPIO 2 上的RX线与GPIO 3上的TX线为串行连接 |
begin | 定义串行连接的波特率(传输速度)在范围4800~115200之间 | comms.begin(9600); | “comms”串行端口上的通信将以9600波特率的速度进行 |
通过串行连接将字节数据转换为可阅读的字符 | comms.println(“Hello World”); | 写入等效于Hello World (可读字符)的字节 | |
write | 通过串行连接写入原始字节数据 | comms.write(45); | 写入值为45的字节 |
available | 当数据可通过串行连接获取时评估为真(true) | if (comms.available()) | 如果可获取通过串行连接读取的数据,执行if语句 |
read | 读取从串行连接获得的数据 | comms.read(); | 从串行连接读取数据 |
有关UART通信,有一个重要的点需要注意,该通信协议下一次只能实现两个设备之间的通信。因为该协议仅发送指示消息的开始、消息内容和消息的结束的位,所以没有办法区分同一条线路上的多个发送和接收设备。如果有多个设备尝试在同一条线路上传输数据,则会发生总线争用,并且接收设备很可能会接收无法使用的垃圾数据!
此外,UART是半双工的,这意味着即使可以在两个方向上进行通信,两个设备也无法同一时间相互传输数据。例如,在一个项目中,两个Arduino通过串行连接相互通信,这意味着在给定的时刻,只有其中一个Arduino可以与另一个“交流”。对于大多数应用来说,这一特点相对来说并不重要,并且不会产生不利影响。
2. SPI
我们将介绍的下一个通信协议是串行外围设备接口(SPI)。SPI与UART主要有以下不同点:
• 同步
• 遵循主从模式,包含一个主设备和多个从设备
• 应用中需要两条以上的线
SPI的硬件连接图稍微复杂一些,看起来像这样:
图4:SPI的硬件连接图
MOSI (“Master Out Slave In”): 从主设备到从设备的数据传输线
SCK (“Clock”): 定义了传输速率和传输开始/结束特性的时钟线
SS (“Slave Select”): 用于主设备选择进行通信的从设备的线
MISO (“Master In Slave Out”):从设备到主设备的数据传输线
SPI的第一个特点是遵循主从模型。这意味着通信中将会有一个设备为主设备,而其他设备为从设备。在该模式下,会在设备之间创建层次结构,从而显示出哪个设备有效地“控制”了其他设备。我们会在阐述一个主从设备之间的通信示例时简单地讨论一下此主从模型。
前面我们提到,多个从设备可以连接到一个主设备。这种系统的硬件图如下所示:
图5:连接到一个主设备的多个从设备
SPI不需要为连接到主设备的每个从设备提供单独的发送线和接收线。在所有从设备和主设备之间连接了一条公共接收线(MISO)和一条公共发送线(MOSI),以及一条公共时钟线(SCK)。主设备通过每个从设备分别配置的SS线来决定将与哪个从设备进行通信。这意味着每增加一个与主设备通信的从设备,都需要在主设备一侧再使用一个GPIO引脚。
SPI是同步的,也就是说主设备和从设备之间的通信与主设备定义的时钟信号(固定频率的方波)紧密相关。从这里我们可以看出主从模型的直接影响之一,即主设备通过时钟信号指定通信速率来驱动通信,而从设备在该速率下进行通信来响应主设备。所定义的速率适用于主设备所主导的任何通信过程(在从设备可以承受的最大速率范围内)。
在SPI中,时钟信号的两个特征决定了数据传输的开始和结束:时钟极性(CPOL)和时钟相位(CPHA)。CPOL是指时钟信号的空闲状态(低电平或高电平)。为了节省功耗,设备在不与任何从设备通信时会将时钟线置于空闲状态,并且在该空闲状态下可用的两个选项为低电平或高电平。CPHA是指时钟信号的跳变沿,决定何时对数据进行采样。方波有两种跳变沿(上升沿和下降沿),并且根据CPHA设置,可以对上升沿或下降沿进行采样。
CPOL和CPHA有四种不同的组合方式,如下表所示:
CPHA = 0 (时钟信号的“第一种跳变沿”) |
CPHA = 1 (时钟信号的“第二种跳变沿”) |
|
CPOL = 0 (空闲状态为0) |
“SPI 模式 0” • 时钟第一次从0(空闲)变为1时开始采样。 • 数据采样发生在时钟信号的上升沿(0→1) |
“SPI 模式 1” • 时钟第一次从0(空闲)变为1时开始采样。 • 数据采样发生在时钟信号的下降沿 (1→0) |
CPOL = 1 (空闲状态为 1) |
“SPI 模式 2” • 时钟第一次从1(空闲)变为0时开始采样。 • 数据采样发生在时钟信号的上升沿(0→1) |
“SPI 模式 3” • 时钟第一次从1(空闲)变为0时开始采样。 • 数据采样发生在时钟信号的下降沿(1→0) |
维基百科以图形方式很好地表示出了CPOL和CPHA的设置与其所传输数据的关系!
图6:CPOL和CPHA的设置视图/ ©Wikipedia
现在,我们将重点介绍使用Arduino作为主设备(SPI.h)在Arduino上实现SPI的方法。SCK、MOSI和MISO的SPI数字引脚连接要在Arduino开发板上进行预定义。对于Arduino Uno,连接如下:
SCK: GPIO 13 或 ICSP 3
MOSI: GPIO 11 或 ICSP 4
MISO: GPIO 12 或 ICSP 1
SS: GPIO 10
任何数字引脚都可以作为SS引脚。为了选择设备,该数字引脚必须被驱动为低电平。
图7:MOSP、MISO和SCK引脚分配在ICSP接头以及GPIO 11、GPIO 12 和GPIO 13上
以下是有关Arduino SPI初始化和使用的C++简要参考。
SPI 方法(Method) | 目的 | 代码 | 释义 |
Constructor | 定义时钟速率、数据位顺序(最高有效位在前或最低有效位在前)以及SPI模式 |
SPI.beginTransaction (SPISettings(14000000, MSBFIRST, SPI_MODE0)); |
在SPI模式0定义14MHz下的SPI连接,并以最高有效位在前的方式进行数据传输 |
digitalWrite | 选择连接到该GPIO引脚的从设备 | digitalWrite(10, LOW); | 将GPIO引脚驱动为低电平以选择从设备。然后将引脚驱动为高电平以取消选择从设备。 |
transfer | 将字节传送到所选择的从设备 | SPI.transfer(0x00); | 发送值为0的字节 |
endTransaction | 结束SPI程序(应在SS线上调用digitalWrite(high)之后调用) | SPI.endTransaction(); | 结束SPI程序 |
SPI是全双工的,这意味着即使在应用中只要求在一个方向上进行数据传输,通信也始种是双向进行的。SPI全双工数据传输通常通过移位寄存器来实现。(请参阅附录中有关移位寄存器的教程!)。这表明在读取一个位时,前面的位将被移动一位,随后,最前边的位将会被触发,并通过SPI连接发送到另一台设备上!
3. I2C
内部集成电路总线(I2C)的发音为“I方C”,是我们在本教程中介绍的最后一个通信协议。虽然该协议的实现是三种协议中最复杂的,但是I2C解决了其他通信协议中存在的一些问题,使其在某些应用程序中比其他通信协议更具有优势。这些优势包括:
能够实现多个主设备与多个从设备之间的连接
同步(同SPI),具有更高的通信速率
简易性:仅需要两根线和一些电阻即可实现
从硬件层面来说,I2C是一种两线接口—I2C连接中仅需要的两根线是数据线(成为SDA)和时钟线(称为SCL)。数据线和时钟线在空闲状态下被拉高,而在需要通过连接发送数据时,这些线会通过一些MOSFET电路被拉低。在本教程中,我们将不讨论I2C电路内部的MOSFET操作,这里需要说明的一个重要点是该系统是漏极开路的,即线路只能由设备驱动为低电平。因此,在项目中使用I2C时,上拉电阻(通常为4.7kΩ)这一步至关重要,以确保在空闲状态下线路确实被拉高。
图8:I2C硬件连接图
I2C具有其独特性,因为它通过寻址解决了与多个从设备之间的接口问题。与SPI通信一样,I2C利用主从模型建立了通信的“层次结构”。但是,主设备不是通过单独的数字线路选择从设备,而是通过主设备中具有唯一性的字节地址来选择从设备,这种字节地址大概类似于这样:0x1B。这意味着将从设备连接到主设备不再需要添加数字线路。只要每个从设备都有唯一性的地址,应用程序就可以区分这些地址所对应的从设备。您可以将这些地址视为名称。要调用从设备的功能,主设备仅调用其名称即可,而只有具有该名称的从设备会发生响应。
I2C通信线路中的地址和对应数据看起来像下图这样。
图9:通信中的地址与对应数据样图/ ©tessel.io
请注意通信线上的ACK 和 NACK请注意通信线上的ACK和NACK位。这些位表示被寻址的从设备是否响应通信—这是一种定期检查通信是否按照预期进行的方法。这些位当然与发送的地址位或数据位无关,但是在复杂的通信体系中,与包含许多开始和结束位并会发生停顿的类似UART这样的协议相比,它们只增加了非常少的额外时间而已。
I2C 使从设备可以自由决定通信请求的方式。向不同的从设备写入或发出请求需要在SDA线以不同的顺序写入不同的字节。例如,在某些加速计模块中,在读取请求发送之前,需要写入指示主设备所要读取的硬件寄存器的字节。对于这些规格,用户需要参考从设备数据手册中的设备地址、寄存器地址和设备设置。
在Arduino上,I2C通过Wire 库(Wire.h)实现应用。Arduino可以配置为一个I2C主设备或从设备。在Arduino Uno上的连接如下所示:
SDA: 模拟引脚 4
SCL: 模拟引脚 5
图9:I2C(Wire)SDA为模拟引脚4(A4),SCL为模拟引脚5(A5)
以下是有关Arduino I2C初始化和使用的C++简要参考。
I2C (线) Method | 目的 | 代码 | 释义 |
begin | 启动库,并以主设备或从设备的身份加入I2C 总线。 | Wire.begin(); | 以主设备的身份加入I2C 总线。如果将地址指定为方法的参数,则Arduino将以该地址作为从设备加入总线。 |
beginTransmission | 对于配置为I2C 主设备的Arduino:启动与具有给定地址的从设备之间的传输。 | Wire.beginTransmission(0x68); | 开始对具有十六进制地址Ox68的从设备进行传输。 |
write | 通过I2C总线写入字节数据。 | Wire.write(0x6B); | 通过I2C总线写入值Ox68的字节数据。 |
requestFrom |
向具有给定地址的从设备请求指定值的字节;可以选择释放I2C线或将其保留以进行进一步的通信。 所请求的字节被放入缓冲区,随后通过Wire.read()调用读取。 |
Wire.requestFrom(0x68, 6, true); |
向地址为Ox68的从设备请求6个字节。请求完成后释放I2C线。 如果最后一个参数为false,则Arduino将保留I2C线以进行进一步的通信,而不允许其他设备通过该线进行通信。 |
read |
从设备发送数据后,读取放入缓冲区的字节。 该方法将在调用 Wire.requestFrom之后调用。 |
Wire.read(); |
从缓冲区读取一个字节(在调用requestFrom之后)。要从缓冲区读取两个字节,必须两次调用该方法。 例: Wire.requestFrom(0x68, 2, true); Wire.read(); Wire.read(); |
endTransmission | 结束当前传输;可以选择释放I2C线或将其保留以进行进一步的通信。 | Wire.endTransmission(true); | 结束传输并释放I2C线。 |
只需将SDA和SCL线连接到总线上,就可以通过I2C总线连接多个主设备。但是,一次只能有一个主设备与从设备通信,因为让多个设备进行相互通信会导致总线争用。同样,不能同时进行主设备到从设备和从设备到主设备之间的双向通信,因为这也会导致总线争用。这使得I2C变成了半双工,就像UART那样!
总之,多个主设备无法通过同一个I2C总线实现相互通信。在将多个主设备连接到从设备的应用中,主设备可以通过单独的总线或单独的通信协议实现相互之间的通信。
如果您学完了本教程,那么应该已经拥有所有使用UART、SPI和I2C通信协议所需的工具了!查看我们的一些Arduino项目,可以获取有关使用这些协议的更多示例!
审核编辑:汤梓红
-
通信协议
+关注
关注
28文章
850浏览量
40236 -
I2C
+关注
关注
28文章
1479浏览量
123194 -
uart
+关注
关注
22文章
1223浏览量
101150 -
Arduino
+关注
关注
187文章
6461浏览量
186575
发布评论请先 登录
相关推荐
评论