知道单片机运行原理的撸友们都清楚,单片机是基于微控制器(下称MCU)搭建的电子系统。单片机的所有功能其实都是由板载的MCU提供的,Arduino开发板当然也不例外。Arduino(这里单指Uno)的板载MCU为ATmega328P。
在ATmega328P内部,实现串口的部件为USART。
是Universal Synchronous and Asynchronous serial Receiver andTransmitter头字母的缩写,
中文翻译为:通用同步/异步串行收发器。
看到这儿,是不是有点头晕,这到底与串口有什么关系呀?!
串口其实就是一种通讯方式的称呼,背后隐藏的实质是一种数据传输协议。数据一位一位地发送出去和接收进来。
就像做糖葫芦时,一个一个地串进去;吃糖葫芦时,一个一个地撸进嘴里去。名字起得还是很贴合实际的。
串口协议一方面定义了硬件方面的电气连接,另一方面又定义了要实现的协议。而USART就是实现协议的家伙,只是这个家伙是电子硬件系统,而不是我们传统理解的软件了。
而且硬件实现比软件实现的速度和稳定性都要高得多!
这样是不是清晰多了!
USART内部结构是十分复杂的,简而言之主要由三部分组成:波特率发生器、接收单元、发送单元。
每个单元的功能全部由硬件实现,同时以寄存器的形式对用户开放了配置接口(控制寄存器),又以寄存器的形式对用户开放了过程监控(状态寄存器)。
上图为波特率发生单元的内部结构示意图。
波特率发生器为串口的收发单元提供统一的时序,
保证了收发逻辑的准确性和稳定性。
这里要重点说说UBRRn(波特率设置寄存器)。
初始情况下,预分频计数器(Prescaling Down-Counter)自动装载用户写入的UBRRn值,并持续向下计数,计数到0时,重新装置UBRRn值,同时产生一个波特率时钟,由此便实现了波特率的产生。
UBRRn寄存器值与波特率的对应关系见下表所示。
除此之外,其它寄存器主要用于工作模式的配置。
因为正如名称所言,USART有同步和异步两种工作模式:
异步即收发双方各用各的时钟,
波特率产生关键部件见上图红色线框,
基于内部时钟、UBRRn和预分频计数器实现,直接供给本机的收发单元使用。
同步即收发双方共用一个波特率,由同步主机统一提供。主机通过内部时钟产生波特率,一方面供本机的收发单元使用,另一方面通过左下角的XCKn Pin输出给从机使用;从机则从左下角的XCKn Pin接收波特率,供从机内部的收发单元使用。
下图为发送单元结构示意图。
发送前,将待发送数据写入UDRn(数据发送寄存器),这一步是发送过程中用户唯一可参与的环节。
此后,硬件会在检测到移位寄存器(TRANSMIT SHIFT REGISTER)空时,
自动将待发送数据移入,并根据设置好的波特率,通过TXDn引脚将数据发送出去。
注意,虽然在发送过程中,用户是不可能介入的,
但USART设置了状态寄存器以供用户随时读取,以掌握发送的实时进度。
最常用的就是TXC(发送完成)标识和UDRE(发送寄存器空)标识。
TXC会在移位寄存器内数据全部被移出,且发送缓存内也没有数据时被置1,常用于判断数据是否全部发送。UDRE则会在发送数据缓存器为空时置1,以告诉用户可以写入新数据了!
下图为接收单元结构示意图。
工作中,时刻采集来自RxDn的输入信号,
一旦检测到有效的开始位,则在每个波特率周期向接收移位寄存器(RECEIVESHIFT REGISTER)内移入一位数据位,直到接收到第一个停止位。
上面的过程全由硬件自动实现,用户无法参与。直到接收的数据被转移到接收缓存,用户才可以通过UDRn寄存器读取它了。
同样的,虽然在接收过程中,用户是不可能介入的,但USART设置了状态寄存器以供用户随时读取,以掌握接收的实时进度。
最常用的就是RXC(接收完成)标识,只要接收缓存中有未被读取的数据,该位就会被置1,用户也就知道此时可以读数据了。Arduino串口的软件实现
Arduino实现了硬串口和软串口两种形式的串口通信,并且都以类的形式进行管理。
下面重点聊聊硬串口的实现机理。
Arduino以数组的形式管理着接收和发送缓存:
unsigned char _rx_buffer[SERIAL_RX_BUFFER_SIZE];
unsigned char _tx_buffer[SERIAL_TX_BUFFER_SIZE];
对Uno而言,两个数组的大小都是64字节。
为实现动态存储管理,又分别对接收缓存和发送缓存设计了头指针和尾指针:
volatile rx_buffer_index_t _rx_buffer_head;
volatile rx_buffer_index_t_rx_buffer_tail;
volatile tx_buffer_index_t_tx_buffer_head;
volatile tx_buffer_index_t _tx_buffer_tail;
初始时,这些指针都设置为0(指向数组的头部)。
接收过程的动态存储管理描述如下:
当接收到数据时,头指针+1;被接收的数据读取时,尾指针+1;当尾指针赶上头指针时,就表明接收缓存里没有数据可读取了。
接收到新数据时:
从缓存中读取数据时:
发送过程与此类似,不再展开。
上述机制,从软件上彻底屏蔽了USART硬件上的缓存概念。对用户而言,操作的缓存仅仅指的是接收和发送数组,而不是真正的USART的UDR寄存器。寄存器操作全部由Arduino封装了!
注意:由于缓存数组是队列式的,并不是首尾相连的环式,因此,操作过程中为防止指针超出数组边界,指针在操作时均设计了取模操作(%)。
Begin()-串口工作前的配置,包括波特率和数据格式。
Arduino共定义了两种形式的begin函数:
1)void begin(unsigned long baud) { begin(baud, SERIAL_8N1); }
2)void begin(unsigned long baud, byte config)
第1种形式只有一个参数波特率,其实内部调用了第2种形式,只是固化了数据格式。
第2种形式除了可以配置波特率外,还可以配置数据格式(数据位、校验位及停止位)。
设置过程中,为简化寄存器操作,
Arduino把常用的数据格式都以宏定义的形式封装好了。
其中,8N1即“8个数据位,无校验位,1个停止位”。
Available()-返回当前接收缓存(接收数组)内尚未读取的字节数。实现机制是取接收缓存头指针与尾指针的差值。实际使用中,具体的数值没有多大意义,主要用于判断接收缓存里是否还有未被读取的数据,以方便下一步的读取。
Read()-从接收缓存中读取一个字节的数据。
内部实现中,会首先判断是否还有数据可读(头指针是否赶上了尾指针),如不可读,则返回-1;如可读,则返回一个字节的数据,并更新尾指针。
Write()-向发送缓存里写入新字节。内部实现中,为提供发送效率。首先判断发送缓存数组以及底层的发送寄存器,如果都为空,则直接操作发送寄存器。如果不都为空,则将头指针+1,并将新字节加入到发送缓存中,具体什么时候再发送呢?
UDRE时刻:USART的接收缓存寄存器为空的时候。
实现方法包括两种:轮询或中断。
见下图红色线框内的部分。
基础例程解析
该例程从计算机中接收字符串,并打印到串口监视器上。
void setup(){
Serial.begin(9600);
}
String inputString="";
void loop(){
while(Serial.available()){
inputString=inputString+char(Serial.read());
delay(2);
}
if(inputString.length()>0){
Serial.println(inputString);
inputString="";
}
}
正常情况下的输出为:
上述例程在处理接收的字符串时,
用while(Serial.available()){…}形式,
同时为实现字符串的判断,使用delay(2)进行了适当的延时,
如果不延时,你会发现打印效果并不是自己想要的结果?!
为什么会这样呢?
因为不加延时时,USART内部的发送速度远大于接收速度,
从而导致每到一个新字节,进入一次available()发送出去后,就没有新的数据发送了,从而立即执行下面的打印命令!