Arduino串口通信(Serial)

Arduino串口的硬件结构


知道单片机运行原理的撸友们都清楚,单片机是基于微控制器(下称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实现了硬串口和软串口两种形式的串口通信,并且都以类的形式进行管理。

  • 硬串口的操作类为HardwareSerial,定义于HardwareSerial.h源文件中,并对用户公开声明了Serial对象,用户在Arduino程序中直接调用Serial,就可实现串口通讯。
  • 软串口的操作类为SoftwareSerial,定义于SoftwareSerial.h源文件中,但不像硬串口那样,源文件中并没有事先声明软串口对象,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()发送出去后,就没有新的数据发送了,从而立即执行下面的打印命令!

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页