Arduino UNO R3主处理器ATMega328P的串行通信子系统可以用于与计算机、外设或其他微控制器进行通信,它支持3种串行通信方式:通用同步/异步收发器,串行外设接口和两线串行接口。
1. 通用同步/异步收发器
在串行通信中,波特率用来衡量传输速率的快慢,同步和异步的对象是波特率的时钟信号;同步通信的设备之间需要一条额外的时钟线,也因此同步方式可以提供更高的波特率;这里将以异步为例。
下面的示例可以使通过串口发送给Arduino的数据回显到串口监视器上:
1 // SerialEcho.ino
2 char data;
3
4 void setup() {
5 Serial.begin(9600);
6 }
7
8 void loop() {
9 if (Serial.available() > 0) {
10 data = Serial.read();
11 Serial.print(data);
12 }
13 }
与通用同步/异步收发器相关的Arduino库函数有:
Serial.begin(speed):打开串口0并设置它的波特率
speed:串口0的波特率
Serial.available():判断串口0的缓冲区内是否有数据
函数返回串口0缓冲区内数据的字节数
Serial.read():读取串口0输入数据
函数返回串口0输入数据的一个字节
Serial.print(val):向串口0打印数据
val:打印的数据
Serial.println(val):向串口0打印数据并换行
val:打印的数据
ATMega328P的串口0由5个相关寄存器控制,串口0状态和控制寄存器A(UCSR0A)的结构如下图所示:
RXC0
|
TXC0
|
UDRE0
|
FE0
|
DOR0
|
PE0
|
U2X0
|
MPCM0
|
接收完成标志位RXD和数据寄存器空标志位UDRE分别在完成一帧数据接收和发送缓冲区为空时被置为1,可以通过向它们写1清0。
串口状态和控制寄存器B(UCSR0B)的结构如下图所示:
RXCIE0
|
TXCIE0
|
UDRIE0
|
RXEN0
|
TXEN0
|
UCSZ02
|
RXB08
|
TXB08
|
向接收使能位RXNE或发送使能位TXNE写入1可以分别使能串口0的接收或发送功能。
串口0控制寄存器C(UCSR0C)的结构如下图所示:
URSEL01
|
UMSEL00
|
UPM01
|
UPM00
|
USBS0
|
UCSZ01
|
UCSZ00
|
UCPOL0
|
向终止位选择控制位USBS0位写入0,则只有1个停止位,写入1则有2个停止位。数据帧长度控制位UCSZ0[2:0]同时存在UCSR0B寄存器和UCSR0C寄存器中,它和奇偶校验模式控制位UPM0[1:0]位的设置如下表所示:
UCSZ0[2:0]
|
数据帧长度
|
|
UPM0[1:0]
|
奇偶校验模式
|
000
|
5位
|
|
00
|
无奇偶校验
|
001
|
6位
|
|
010
|
7位
|
|
01
|
(保留)
|
011
|
8位
|
|
100
|
(保留)
|
|
10
|
偶校验
|
101
|
(保留)
|
|
110
|
(保留)
|
|
11
|
奇校验
|
111
|
9位
|
|
串口0波特率寄存器(UBRR0H和UBRR0L)的计算公式是:

Arduino UNO R3开发板使用8位数据帧长度,1个停止位,无奇偶校验,通过直接访问寄存器改写以上程序为:
1 // SerialEcho_reg.ino
2 unsigned char USART0_Receive();
3 void USART0_Transmit(unsigned char val);
4
5 void setup() {
6 UCSR0A = 0x20;
7 UCSR0B = 0x18;
8 UCSR0C = 0x06;
9
10 UBRR0H = 0x00;
11 UBRR0L = 0x67;
12 }
13
14 void loop() {
15 USART0_Transmit(USART0_Receive());
16 }
17
18 unsigned char USART0_Receive() {
19 while (!(UCSR0A & (1 << RXC0)));
20 return UDR0;
21 }
22
23 void USART0_Transmit(unsigned char val) {
24 while (!(UCSR0A & (1 << UDRE0)));
25 UDR0 = val;
26 }
2. 串行外设接口
串行外设接口是一种同步的串行通信方式,因此它需要比通用同步/异步收发器多一条时钟线。此外,串行外设接口还引入了主机和从机的概念,通信中使用的时钟信号由主机产生,从机只有在被主机选中时才能与其进行通信;因此,一个串行外设接口设备一般需要连接4条信号线:SPI时钟SCK,主入从出MISO,主出从入MOSI和SPI选中SS。
74HC595是一种8位的存储器,它的结构如下图所示:

当11(SH_CP)引脚有上升沿产生时,14(DS)引脚上的电平信号会被采样,并移入8位移位寄存器中,多余的位将从9(Q7’)引脚移出;当12(ST_CP)引脚上有上升沿产生,并且13(OE)引脚为低电平时,移位寄存器中的内容会被复制到存储寄存器中并输出。
74HC595芯片不是标准的串行外设接口设备,但可以使用串行外设接口向它输入数据,如图所示连接电路,Arduino开发板11(PB3/MOSI)引脚连接到74HC595芯片14(DS)引脚,13(PB5/SCK)引脚连接到11(SH_CP)引脚;74HC595芯片12(ST_CP)引脚可以连接到任一Arduino数字引脚,这里是A0(PC0)引脚:

下面的示例代码可以使74HC595芯片连接的LED呈现明暗交替的图案:
1 // ShiftOutLed.ino
2 const int DS = 11;
3 const int SH_CP = 13;
4 const int ST_CP = A0;
5
6 void setup() {
7 pinMode(DS, OUTPUT);
8 pinMode(SH_CP, OUTPUT);
9 pinMode(ST_CP, OUTPUT);
10
11 digitalWrite(ST_CP, LOW);
12 shiftOut(DS, SH_CP, MSBFIRST, B10101010);
13 digitalWrite(ST_CP, HIGH);
14 }
15
16 void loop() {
17 }
与串行外设接口相关的Arduino库函数有:
shiftOut(dataPin, clockPin, bitOrder, value):作为主机移位输出
dataPin:指定移位输出的引脚
clockPin:指定同步时钟信号的引脚
bitOrder:从高位开始发送数据(MSBFIRST)或从低位开始发送数据(LSBFIRST)
val:移位输出的数据
ATMega328P的串行外设接口由2个相关寄存器控制,SPI控制寄存器SPCR的结构如下图所示:
SPIE
|
SPE
|
DORD
|
MSTR
|
CPOL
|
CPHA
|
SPR1
|
SPR0
|
SPI使能位SPE写入1则启用串行外设接口,写入0则禁用;数据序列位DORD位写入1则从SPI数据寄存器SPDR的高位开始发送,写入0则从低位开始发送;时钟相位位CPHA写入1则数据在上升沿采样,写入0则在下降沿采样。此外,Arduino作为主机,则主/从选择位MSTR需写入1。
SPI状态寄存器SPSR的结构如下图所示:
SPI2X位与SPCR寄存器中的SPR[1:0]位共同设定SPI的分频系数,如下表所示:
SPI2X
|
SPR[1:0]
|
时钟源
|
0
|
00
|
系统时钟4分频
|
0
|
01
|
系统时钟16分频
|
0
|
10
|
系统时钟64分频
|
0
|
11
|
系统时钟128分频
|
1
|
00
|
系统时钟2分频
|
1
|
01
|
系统时钟8分频
|
1
|
10
|
系统时钟32分频
|
1
|
11
|
系统时钟64分频
|
通过直接访问寄存器改写以上程序为:
1 // ShiftOutLed_reg.ino
2 void setup() {
3 DDRB |= (1 << PB3) | (1 << PB5);
4 DDRC |= (1 << PC0);
5
6 PORTC &= ~(1 << PC0);
7 SPCR = 0x77;
8 SPDR = 0xaa;
9 PORTC |= (1 << PC0);
10 }
11
12 void loop() {
13 }
3. 两线串行接口
两线串行接口同样也是一种同步的串行通信方式,它的读和写时序如下图所示:


由于主机先发送从机地址,从机应答后再发送其他数据,因此两线串行接口不需要类似于串行外设接口的选择信号线;又因为采用半双工的通信方式,两线串行接口只需要一条数据线,所以一个两线串行接口设备一般只需要2条信号线,即时钟信号线SCL和数据信号线SDA。
两线串行接口可以工作在主机发送模式,主机接收模式,从机发送模式或从机接收模式,Arduino IDE的Wire库提供了这四种模式的示例,我们主要关注主机发送模式和主机接收模式,下面是这两个示例:
1 // master_writer.ino
2 #include <Wire.h>
3
4 void setup() {
5 Wire.begin();
6 }
7
8 byte x = 0;
9
10 void loop() {
11 Wire.beginTransmission(8);
12 Wire.write("x is ");
13 Wire.write(x);
14 Wire.endTransmission();
15
16 x++;
17 delay(500);
18 }
19
20 // master_reader.ino
21 #include <Wire.h>
22
23 void setup() {
24 Wire.begin();
25 Serial.begin(9600);
26 }
27
28 void loop() {
29 Wire.requestFrom(8, 6);
30
31 while (Wire.available()) {
32 char c = Wire.read();
33 Serial.print(c);
34 }
35
36 delay(500);
37 }
与两线串行接口主机相关的Arduino库函数有:
Wire.begin():作为主机打开两线串行接口
Wire.beginTransmission(address):开始向指定地址从机传输数据
address:指定从机的地址
Wire.write(val):向从机发送数据
val:发送的数据
Wire.endTransmission():结束向从机发送数据
Wire.requestFrom(address, quantity):向指定地址从机请求指定字节数的数据
address:指定从机的地址
quantity:指定请求的字节数
Wire.available():判断两线串行接口的缓冲区内是否有数据
函数返回两线串行接口缓冲区内数据的字节数
Wire.read():读取两线串行接口输入的数据
函数返回两线串行接口输入数据的一个字节
ATMega328P的两线串行接口的主机模式由3个相关寄存器控制。两线串行接口波特率寄存器TWBR的计算公式是:

其中,TWPS是预分频系数,它由两线串行接口状态寄存器TWSR中的TWPS[1:0]位设置,寄存器的结构如下图所示:
TWS7
|
TWS6
|
TWS5
|
TWS4
|
TWS3
|
|
TWPS1
|
TWPS0
|
两线串行接口控制寄存器TWCR的结构如下图所示:
TWINT
|
TWEA
|
TWSTA
|
TWSTO
|
TWWC
|
TWEN
|
|
TWIE
|
向TWI使能位TWEN写入1则启用两线串行接口,写入0则禁用;向起始信号使能位TWSTA或停止信号使能位TWSTO写入1,则会产生起始信号或停止信号,停止条件产生后,TWSTO位会自动清零。
TWI中断标志位被置1表示产生了相关事件的中断,通过判断TWSR寄存器高5位的值可以判断中断事件,如下表所示:
主机发送模式(TWPS[1:0] = 00)
|
主机接收模式(TWPS[1:0] = 00)
|
状态码
|
状态
|
状态码
|
状态
|
0x08
|
START已发送
|
0x08
|
SATRT已发送
|
0x10
|
重复START已发送
|
0x10
|
重复START已发送
|
0x18
|
SLA+W已发送,接收到ACK
|
0x38
|
SLA+R或NOT ACK仲裁失败
|
0x20
|
SLA+W已发送,接收到NOT ACK
|
0x40
|
SLA+R已发送,接收到ACK
|
0x28
|
数据已发送,接收到ACK
|
0x48
|
SLA+R已发送,接收到NOT ACK
|
0x30
|
数据已发送,接收到NOT ACK
|
0x50
|
接收到数据,已产生ACK
|
0x38
|
SLA+W或数据的仲裁失败
|
0x58
|
接收到数据,已产生NOT ACK
|
通过直接访问寄存器改写以上程序为:
1 // master_writer_reg.ino
2 #define READ 0x01
3 #define WRITE 0x00
4 void twi_write(byte address, byte val);
5
6 void setup() {
7 TWSR &= ~(1 << TWPS1) & ~(1 << TWPS0);
8 TWBR = 0x48;
9 }
10
11 byte x = 0;
12
13 void loop() {
14 twi_write(8, x);
15
16 x++;
17 delay(500);
18 }
19
20 void twi_write(byte address, byte val) {
21 // 发送开始
22 TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
23 while (!(TWCR & (1 << TWINT)));
24 if ((TWSR & 0xf8) != 0x08) {
25 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
26 return;
27 }
28
29 // 发送从机地址
30 TWDR = (address << 1) | WRITE;
31 TWCR = (1 << TWINT) | (1 << TWEN);
32 while (!(TWCR & (1 << TWINT)));
33 if ((TWSR & 0xf8) != 0x18) {
34 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
35 return;
36 }
37
38 // 发送数据
39 TWDR = val;
40 TWCR = (1 << TWINT) | (1 << TWEN);
41 while (!(TWCR & (1 << TWINT)));
42 if ((TWSR & 0xf8) != 0x28) {
43 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
44 return;
45 }
46
47 // 发送停止
48 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
49 }
50
51 // master_reader_reg.ino
52 #define READ 0x01
53 #define WRITE 0x00
54
55 byte data;
56 void twi_read(byte address);
57
58 void setup() {
59 TWSR &= ~(1 << TWPS1) & ~(1 << TWPS0);
60 TWBR = 0x48;
61
62 Serial.begin(9600);
63 }
64
65 void loop() {
66 twi_read(8);
67 Serial.print(data);
68
69 delay(500);
70 }
71
72 void twi_read(byte address) {
73 // 发送开始
74 TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
75 while (!(TWCR & (1 << TWINT)));
76 if ((TWSR & 0xf8) != 0x08) {
77 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
78 return;
79 }
80
81 // 发送从机地址(伪写)
82 TWDR = (address << 1) | WRITE;
83 TWCR = (1 << TWINT) | (1 << TWEN);
84 while (!(TWCR & (1 << TWINT)));
85 if ((TWSR & 0xf8) != 0x18) {
86 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
87 return;
88 }
89
90 // 发送重复开始
91 TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
92 while (!(TWCR & (1 << TWINT)));
93 if ((TWSR & 0xf8) != 0x10) {
94 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
95 return;
96 }
97
98 // 发送从机地址(读)
99 TWDR = (address << 1) | READ;
100 TWCR = (1 << TWINT) | (1 << TWEN);
101 while (!(TWCR & (1 << TWINT)));
102 if ((TWSR & 0xf8) != 0x40) {
103 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
104 return;
105 }
106
107 // 读取数据
108 TWCR = (1 << TWINT) | (1 << TWEN);
109 while (!(TWCR & (1 << TWINT)));
110 if ((TWSR & 0xf8) != 0x58) {
111 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
112 return;
113 }
114 data = TWDR;
115
116 // 发送停止
117 TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
118 }