UART、I2C和SPI是嵌入式世界里最常见的三种通信协议。在阅读教程,或者购买模块时常常看见“这个模块是用I2C协议驱动的”, “两个设备之间数据走SPI”诸如此类的描述,很多新手觉得是什么高阶的知识就一知半解得略过去略了。今天小编简略地为大家介绍一下这三种协议,以及它们各自的优缺点。
什么是通信协议
白话点说,通信协议就是通信时候双方约定的规则。打个类比,就像你跟另外一个人交流,你们之间需要一种交流的约定,就是语言。要么都说中文,要么都说英文。通信协议就类似于硬件之间交流的语言。
串行通信和并行通信
在硬件通信里,一般有两种通信方式:串行通信(Serial Communication)和并行通信(Parallel Communication)。这两种通信方式的区别从字面理解也很简单:串行就是数据一个接一个的发送;并行就是所有数据都同时发出。
但由于并行通信在线路成本和同步困难方面的问题使其在实际应用上面远不如串口通信来得广泛。本文介绍的UART, I2C和SPI三种通信协议都属于串口通信。
单工,半双工,双工
根据串行数据的传输方向,我们可以将通信分为单工,半双工,双工。
同步通信和异步通信
根据串行数据传输方式,我们又可以分为:同步通信和异步通信。
同步通信是指发送端和接收端必须使用同一时钟,是一种连续传送数据的通信方式。由于发送和接收的双方都采用同一时钟,这样接收方就可以通过时钟信号来确定每个信息位。
异步通信不需要双方使用同一时钟,并且它是一种不连续的传输通信方式,一次通信只能传输一个字符数据(字符帧)。而字符帧之间的间隙可以是任意的。但由于不知道对方什么时候发送数据,所以发送方要在数据的开始后结束都要加一些辅助的信息,比如开始要有起始位,结束要有结束位等。
同步通信的通信效率比异步通信高,可以传输较大的数据量但对于时钟同步要求比较高,而异步通信不需要同步时钟,通信实现简单,设备相对简单,但是传输速率不高,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。
了解了上面的几个基本概念,我们再来按个看一看UART、I2C和SPI三种协议。
UART协议
UART的全称叫 Universal Asynchronous Reception and Transmission通用异步收发。从名字我们就可以知道,UART是异步串行通信的。利用UART协议传输数据时,需要两个数据引脚,一个用于传输数据的TX,另一个用于接收的RX。
Arduino板上默认的RX和TX引脚分别是Pin0和Pin1。UART硬件层和软件层的功能都是Arduino集成的,一般我们不需要从底层操作数据。一个UART口(一组RX和TX)只能连接一个UART设备进行通信,我们需要这样就接线:
注意,一个设备的TX接的是另一设备的RX,反之RX接TX,最后需要将两个设备共地,共地是让它们的参考电压一致。
下面,我们用两块Arduino实际实践一下。如图连接两个Arduino:
我们在Arduino A里烧录以下代码:
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
}
void loop() {
// put your main code here, to run repeatedly:
char s[]="I am Arduino A";//每隔1s发送一次字符数组s
Serial.print(s);
delay(1000);
}
再在Arduino B里烧录以下代码:
String a=""; //定义字符串,接收数据
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
}
void loop() {
while(Serial.available())//当发现缓存中有数据时,将数据送至字符数组a中
{
a+=char(Serial.read());
delay(3);
}
if (a.length() > 0){
Serial.println(a)
a = ""
}
}
这样就A就通过UART向B发送了“I am Arduino A”消息,B接收到以后输出。当然你可以修改A的代码实现双向数据传输,这里我们就点到即止了。
UART是最常见的通信协议之一,它可以实现全双工传输,但它的传输速度比较慢,而且只能支持一对一的设备。
I2C协议
I2C是Philips公司在1980年代发明的一种协议,全称是Inter-Integrated Circuit。I2C最常被应用于模块或者传感器之间的通信,因为I2C基于二根传输线,同步通信的半双工协议,而且协议也非常简单可靠。I2C使用两根传输线实现一个主设备与多个从设备,甚至是多个主设备与对应从设备之间的通信。 这两根通讯线一根为控制时钟线,称之为SCL,用于同步设备间的数据传输时钟; 另一根为数据线,称之为SDA,用于携带数据。理论上,一条I2C总线上能支持挂载128台设备。
Arduino UNO的I2C引脚是A4(SDA), A5(SDL)。Arduino对I2C协议也进行了库封装:
https://www.arduino.cc/en/Reference/Wire
下面我们还是用两块Arduino来实践一下如何利用I2C协议来传输数据。如图连接好两块Arduino:
一台我们作为主设备(Master),烧录以下代码:
#include <Wire.h>
void setup() {
Serial.begin(9600); /* begin serial comm. */
Wire.begin(); /* join i2c bus as master */
Serial.println("I am I2C Master");
}
void loop() {
Wire.beginTransmission(8); /* begin with device address 8 */
Wire.write("Hello Slave"); /* sends hello string */
Wire.endTransmission(); /* stop transmitting */
Wire.requestFrom(8, 9); /* request & read data of size 9 from slave */
while(Wire.available()){
char c = Wire.read();/* read data received from slave */
Serial.print(c);
}
Serial.println();
delay(1000);
}
另一块作为从设备(Slave),烧录以下代码:
#include <Wire.h>
void setup() {
Wire.begin(8); /* join i2c bus with address 8 */
Wire.onReceive(receiveEvent); /* register receive event */
Wire.onRequest(requestEvent); /* register request event */
Serial.begin(9600); /* start serial comm. */
Serial.println("I am I2C Slave");
}
void loop() {
delay(100);
}
// function that executes whenever data is received from master
void receiveEvent(int howMany) {
while (0 <Wire.available()) {
char c = Wire.read(); /* receive byte as a character */
Serial.print(c); /* print the character */
}
Serial.println(); /* to newline */
}
// function that executes whenever data is requested from master
void requestEvent() {
Wire.write("Hi Master"); /*send string on request */
}
这样,我们就实现了主从设备的双向传输。打开主机Arduino的串口监视器我们可以看见如下的输出:
从机Arduino的串口输出:
I2C虽然只需要两根线,就能支持多主机多从机的数据传输,但由于只有一根用于数据传输,它通过在“接收”和“传输”两种状态之间但切换实现了双向传输,但牺牲了不少传输速率。I2C还有典型的开漏问题,总线需要加上拉电阻。
SPI协议
最后,我们来看一下SPI协议。SPI全称Serial Peripheral Interface(串行外设接口),由摩托罗拉公司提出的一种同步串行数据传输协议。SPI类似I2C也是同步通信的协议,但是全双工,支持数据的同时输出和输入。这两个特征使SPI的传输速率比UART和I2C都高,这对于像SD卡、或者屏幕等数据型模块来说,是非常具有优势的。
SPI支持一主多从的模式,但SPI也是三种协议中需要线最多的协议,一共需要4条信号线:
但Arduino UNO默认的SPI引脚分别为D13(SCK), D12(MISO), D11(MOSI), D10(SS),其中SS是从机选择引脚,没有强制要求,你也可以选其他的引脚。
同样,我们来实践一下用SPI实现数据传输。
如图连接好两块Arduino UNO。还是一块作为主机(Master), 另一块作为从机(Slave)。Arduino对SPI协议也做了类封装:
https://www.arduino.cc/en/reference/SPI
主机烧录以下代码:
#include <SPI.h>
void setup (void)
{
Serial.begin(115200);
digitalWrite(SS, HIGH);
SPI.begin ();
SPI.setClockDivider(SPI_CLOCK_DIV8);
}
void loop (void)
{
char c;
// enable Slave Select
digitalWrite(SS, LOW); // SS is pin 10
// send test string
for (const char * p = "Hello, world!\n" ; c = *p; p++) {
SPI.transfer (c);
Serial.print(c);
}
// disable Slave Select
digitalWrite(SS, HIGH);
delay (1000);
}
从机烧录:
#include <SPI.h>
char buf [100];
volatile byte pos;
volatile boolean process_it;
void setup (void)
{
Serial.begin (115200); // debugging
// turn on SPI in slave mode
SPCR |= bit (SPE);
// have to send on master in, *slave out*
pinMode(MISO, OUTPUT);
// get ready for an interrupt
pos = 0; // buffer empty
process_it = false;
// now turn on interrupts
SPI.attachInterrupt();
} // end of setup
// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR; // grab byte from SPI Data Register
// add to buffer if room
if (pos < sizeof buf)
{
buf [pos++] = c;
// example: newline means time to process buffer
if (c == '\n')
process_it = true;
} // end of room available
} // end of interrupt routine SPI_STC_vect
// main loop - wait for flag set in interrupt routine
void loop (void)
{
if (process_it)
{
buf [pos] = 0;
Serial.println(buf);
pos = 0;
process_it = false;
} // end of flag set
} // end of loop
这样从机就能接受到主机发过来的消息了。
总结
今天,我们粗略地介绍了一下Arduino数据通信中最常用的三种协议:UART、I2C和SPI。
协议 | 通信方式 | 通信方向 | 信号线 | 传输速率 | 主从模式 |
---|---|---|---|---|---|
UART | 异步 | 全双工 |
2线 RX、TX |
最低 | 一对一 |
I2C | 同步 | 半双工 |
2线 SDA、SCL,以地址选择从机 |
低 |
多主机 多从机 |
SPI | 同步 | 全双工 |
4线 MOSI、MISO、SCLK、CS(或SS),以CS选择从机 |
高 | 一主多从 |
它们各自都有自己的优缺点和适用的场景,并没有绝对的好坏,这也是这三种协议经久不衰的原因。只有了解并掌握它们,我们才能在具体的应用场景里选择最合适的协议。当然在嵌入式世界里,还有其他很多协议,小编以后再介绍吧。如果对这三种协议的底层感兴趣的朋友,也可以自己再去深入了解。