Arduino菜鸟通俗版解读系列(4)串口通信---USART


四轴PID测试平台实验

        这一篇来讲一个重要的知识点---串口通信。上面的视频是一个用到串口通信的例子,视频中的MPU6050惯性测量单元和Arduino之间就是通过串口通信进行数据传输的,另外,电脑对于Arduino的监控也是通过串口通信进行监控的。(请大家注意区别:串口通信,串行通信,串行通信指的是一个大类,区别于并行通信;本系列中提到的串口通信一般指Arduino上面的USART通信模式,USART也是串行通信的一种,可以有硬串口,软串口两种实现方式,并且USART是一种异步串行通信,关于串行通信后面会有专门的一篇总结文章,以区分各种概念)

        由于串口通信个人感觉内容比较抽象,不知道如何讲起,所以打算从以下3方面来讲解:

1)串口通信是干什么的?为什么很重要?串口通信和串行通信的简单区别介绍?

2)Arduino上的串口通信是怎么工作的?

3)如何实现遥控?

        其中1)和2)部分会在这一篇讲解,3)遥控由于涉及到专门的NRF24L01模块,所以会在下一讲进行。

1)串口通信是干什么的?为什么很重要?串口通信的种类?

        串口通信是用来在不同电子设备之间交换数据用的技术,其实就是要实现不同电子设备之间的“通讯对话”。试想在第2篇中讲到的LED小灯,如果我要通过电脑,实时传送一个亮度控制参数给Arduino,怎么实现呢?在之前几篇的讲解中,亮度控制参数都是直接写到程序中,然后随着程序烧入Arduino实现的。这就意味着我没有办法随时随地地去改变LED灯的亮度参数,因为如果我想改变这个亮度参数,我就需要在程序中进行修改并重新烧录程序进入Arduino。

        那怎么才能够通过Arduino“实时”地去控制LED小灯的亮度呢?想实现“实时”功能当然就得建立一个和Arduino的通信通道,这样才能随时随地的将我们的意图传达给Arduino,并让Arduino来执行。在此,我们通过电脑作为我们的指令发送装置,所以我们要在电脑和Arduino之间建立起这样一个通信通道,这样的通信通道就是串口通信。

        有了上面一段所讲的基本想法,那么具体到程序里面的形式又是怎样的呢?基本逻辑是我们在程序里预留一个变量,例如val,这个变量val用来存储LED灯的亮度值,在第一次烧录程序的时候我们并不给val赋上具体的值,而是通过后续的串口通信,把这个值传送给val,然后让Arduino直接用val来控制LED灯,随着我们通过电脑串口通信不断改变 val的值,从而起到实时控制Arduino的效果。具体程序可见图1:

Arduino菜鸟通俗版解读系列(4)串口通信---USART_第1张图片
图1

        图1中的程序用于点亮一个LED小灯,其中的变量val用来控制LED灯的亮度,可以看到在程序开头并没有对val赋值;那么什么时候赋的值呢?就是步骤1和步骤2。步骤1的作用是读取串口寄存器中的信息(这个串口寄存器就是串口通信数据的暂存地,我们通过计算机输出的信息将会先到达串口寄存器,然后我们再从串口寄存器中转移信息到Arduino的内存),并将这个信息存储入A数组;然后在步骤2中,将A数组中的信息转换成10进制的阿拉伯数字,并赋值给val。通过步骤1 和步骤2我们就完成了将信息从电脑传送到Arduino内存这样一个过程。好像明白了一些对吗?别急,这里面有两个重要点需要说一下:第一,在前面我们直接就开始读取串口寄存器中的数据了,但是计算机的信息到底是怎么通过USB线传送到串口寄存器的?数据在串口寄存器里又是怎么分布的?这是个很重要的问题,明白了这个问题也就明白了为什么我们需要在图1中的程序里加上delay(100)这条命令,如果不加这条命令我们将得到完全错误的结果;第二,为什么我们要加上val=strtol(A,NULL,10)这条命令?这条命令也非常重要。  OK,对于第一个问题,我们将在讲解“Arduino上USB端的串口通信是怎么工作的?”时给出答案,现在先讲讲为什么要加上val=strtol(A,NULL,10)这条命令?先说一下这条命令的功能:即将A信息转换成10进制的阿拉伯数字,然后赋值给val 。好,继续讲,还记得第一篇中我们讲到Arduino的编程环境中有一个按钮,叫做“串口通信监视窗口”吗?,见图2。

Arduino菜鸟通俗版解读系列(4)串口通信---USART_第2张图片
图2

        图2中,点击右上角的按钮可打开如左侧的对话框,这个对话框中可以输入你想传送给Arduino的信息(注意:一定要将对话框下面的波特率与右侧程序中设置的波特率保持一致),在这里我们想传送100这个数值给到Arduino。但是!在串口对话框中输入的任何信息都是字符型的信息,包括数字信息在内,都是以字符型存在的。所以在图2对话框中的100其实是三个字符,即‘1’,‘0’,‘0’。只不过它们看上去像阿拉伯数字100,但是它们绝对不能够直接用于数学运算。所以在程序中,我们需要增加一条命令来将字符信息转换成数字信息,然后才能传给val用以控制LED灯亮度。所以我们需要val=strtol(A,NULL,10)这条命令,来完成字符向数字的转变。讲到这里我们应该大致了解到串口通信是干什么的了,简单说就是用来让Arduino和电脑或者其他芯片进行沟通的,所以串口通信很重要,如果没有串口通信,Arduino就成了哑巴和聋子,只能自己和自己玩(比如前面讲的LED灯项目,没有用到串口通信,所以其实只是一个封闭的项目),无法通过和外界沟通信息来完成更复杂的功能了。

        接着简单说说串口通信和串行通信的区别。是这样,串行通行是一个大类,下面包括好多种类型:SPI,I2C,USART等等。而串口通信在Arduino上指的是USART,也叫同步/异步收发器。SPI,I2C,USART它们都属于串行通信方式,区别在于SPI,I2C为同步串行通信,它们通过时钟线来协调发送端和接收端的动作;USART(串口通信)属于异步串行通信,它需要通过设定波特率来协调发送端和接收端的动作。

2)Arduino上串口通信是怎么工作的?

Arduino菜鸟通俗版解读系列(4)串口通信---USART_第3张图片
图3

        这是本讲的核心内容。首先看一下图3。图3中表示的场景是一台电脑正在通过USB串口通信传送信息给Arduino。在Arduino的USB接口连接一个串口寄存器,它是用来暂时存放电脑传过来的信息的(之后Arduino会根据开发者的程序,从串口寄存器中提取数据,保存到Arduino内存中),在和电脑通过USB串口通信时,默认分配给Arduino UNO的串口寄存器空间可存放63帧的信息(注意是Arduino UNO,如果是Arduino其他类型的板子可能这个空间大小会不一样)。注意图中电脑连接线上的一个个小点,每一个小点代表一帧信息,电脑就是这样像图3中所示一帧一帧地传送信息的。由于串口寄存器空间默认为63帧,所以如果数据很多,我又没能及时读取并清空寄存器中的信息的话,后来的信息会覆盖之前的信息,造成信息丢失。这个该怎么理解呢?首先讲一下什么是一帧信息?一帧你可以理解为一个数据包,这个数据包包含若干字节的内容,如图4所示。

Arduino菜鸟通俗版解读系列(4)串口通信---USART_第4张图片
图4

        一帧信息中第一个字节代表帧头,最后一个字节代表帧尾。帧头和帧尾是用来分辨一个完整帧信息的,假如没有帧头,Arduino将无法知道这一帧信息从何处开始,没有帧尾的话Arduino将无法知道这一帧信息在何处结束,在帧头和帧尾之间就是我们真正传送的信息,每一帧信息的大小在不同的串口设备上是不同的,例如在Arduino和电脑通信的USB串口上,一帧等于3个字节(帧头,信息,帧尾),而在MPU6050这款惯性测量单元芯片上,一帧等于10个字节,所以具体一帧多大要看你买的芯片的设置,这些信息在购买时卖家提供的网上资料里都会有说明。上面讲了帧头和帧尾以及一帧信息的构成,为什么要这样构成呢?为什么要加上所谓的帧头和帧尾呢?这其实也可以通过一个比喻来形象地说明:如图4中绘制的一群小人。这一群小人排队通过一个关卡,关卡有个管理员,这个管理员就相当于Arduino的处理器,而这一群小人就相当于从电脑端发送过来的信息。假设这些小人是从很远的一个营地跑过来的,那么营地就相当于一台电脑,他们在管理员面前暂时等待的区域就相当于串口通信寄存器。现在管理员要指挥这一排小人有秩序地通过关卡,这就相当于Arduino处理器要依次读取寄存器中的信息。好,我们可以看到每一组小人有一个排头兵和一个排尾兵,那么这个管理员会通过识别排头兵和排尾兵从而有秩序地让小人一队队地通过,每一队小人就相当于一帧信息,而管理员的通过规则是:每次看到排头兵就开始放行,一直到发现排尾兵为止则停止通行;然后隔一小会儿再让下一队通过。那么按照我们的常识就会知道,如果管理员放行的速率太慢,那么由于不断有后续的小人从营地跑过来,这个暂时等待的地方就会装不下这么多小人,这时候后面赶来的小人就会挤兑前面的小人,也就相当于刚才我们说的:后来的信息会覆盖前面的信息,造成信息丢失。

        那么这种由于放行速度过慢而导致信息被覆盖丢失的情况会出现在串口通信中吗?一般不会出现的,因为Arduino的主频有16M,也就是每秒运算160万次;而串口通信的速度(也就是前面讲到的波特率)最高一般用到115200,也就是每秒115200个比特,约为14400个字节。这样看来当后序的信息传送过来的时候,前面的信息早就被Arduino读取完毕了,所以不会出现挤兑的情况。在此我给出一个乌龟赛跑的概念:假如你把前面例子中的管理员换做你自己,一队小人换作一只乌龟,那么串口传输信息的速度和Arduino运算的速度相比,相当于一排乌龟排队冲刺,然后你拿着一个钳子捉乌龟,每当一只乌龟到达你面前你就抓走一只;就算乌龟再努力地奔跑,对于你而言都是很慢的,所以不会出现乌龟来的太快导致你来不及抓而产生拥堵

        OK,上面讲了Arduino和电脑之间进行串口通信时,信息是以一帧一帧的形式来传送的,下面来看看这些信息在抵达Arduino的USB端口后是怎么进入到Arduino内部的。在图1中的程序里我们只是直接告诉你用Serial.read()命令就可以读取,但是其中的物理过程是怎么样的呢?先看一下图5,图5显示了电脑和Arduino之间串口通信时的接线方式,当然这些接线都集成在USB线内部,我们看到的只是一根完整的USB线。

Arduino菜鸟通俗版解读系列(4)串口通信---USART_第5张图片
图5

        图5中可见,电脑和Arduino之间的串口通信都需要一个专门的串口通信芯片(USB两端是否分别集成一个串口通信芯片不确定,但是可以这样理解,假定就是通过两端的串口芯片进行的串口通信),这两块串口芯片各自都有4个接口:5V,TX,RX,GND。两块芯片的5V口和GND口要接到一起,5V和GND这两个接口的作用是给芯片供电。TX接口是发送口,就是信息从这个口发送出去,RX接口是接受口,就是信息从这个口接收进来。所以对于电脑而言,电脑的TX口应该接Arduino的RX口,电脑的RX口应该接Arduino的TX口。另外提一句:SPI通信模式不使用TX和RX这种写法,而是采用MOSI和MISO这样的写法,MOSI的意思就是master out slave in,MISO的意思就是master in slave out,从字面上理解MOSI就是主机输出从机接收,MISO就是主机接收从机输出。所以我们看到TX,RX那么基本上说明这个模块用的是USART,如果是MOSI和MISO,那么这个模块用的是SPI。

      了解完接线形式后,现在假设我们从电脑上发送了三个帧的数据给到Arduino,这三个帧的内容分别是'a','b','c'。那么这三个字母是怎么进入到Arduino内部的呢?看图6。

Arduino菜鸟通俗版解读系列(4)串口通信---USART_第6张图片
图6

        图6中左侧给出的是对应的程序,右侧给出的是Arduino内部的物理过程。

        首先,右侧中步骤1至步骤4这段时间内,信息一帧一帧地进入串口寄存器中,假如我们想要一次性读取所有的信息,那么在步骤1到步骤4的这段时间内我们不能够对信息进行读取,因为这个时候信息还没有完全传送完毕,也就是说所有信息没有全部进入寄存器。假如我们在图6中的步骤2就开始读取寄存器中的信息,那么我们将只能获得'a'这个信息。这就是前面讲到的“乌龟理论”,对于Arduino来讲,串口信息的传送是很慢的,虽然串口通信1秒能传送上万个字节,对于我们人类来说是一眨眼的功夫,但是对于Arduino这个1秒钟运算160万次的芯片来说,这就是乌龟爬行的速度,所以我们必须在开头给Arduino设置一个等待时间,这个等待时间一般就设定100ms即可,这就是为什么要加上delay(100)这条命令的原因。

        然后,当等待时间结束,这时候我们认为信息已经完全传送完毕。当然,说实话这都是靠猜,我们估计100ms信息传送完毕了,可能信息早在10ms 的时候就传送完毕了,不过没关系,为了保险我们宁可多等待一会儿,反正是毫秒级的时间损失,对于我们来说多等几十毫秒根本感觉不出来,当然你觉得有把握的话也可以把等待时间缩短一些,比如50ms甚至20ms都可以,你可以尝试着用,假如没有出现信息丢失那你完全可以用更短的等待时间。信息完全传送完毕后,我们才开始要把信息从串口寄存器中都读取出来了,也即是步骤5到步骤7的过程。注意,图6中的黑线指向的那个“大箭头”是Arduino内部的一个指针,这个指针时刻监测着串口寄存器,我们随时可以使用命令Serial.available()来调动这个指针,获取传入信息的数量,当然如我们所说,要等信息全部传送完毕后再开始调用,假如我们在步骤2就调用Serial.available()的话,我们只能探测到一个数据。而在图6中的步骤4我们可以看到,总共有三个信息传入了寄存器,所以这个时候调用Serial.available()获得的值是3,然后我们把这个值赋给变量j,于是变量j就代表了寄存器中信息的数量。

        最后,还是这个指针,我们可以使用命令Serial.read()来调动这个指针来抓取数据,注意Serial.read()每次可以抓取一帧的信息,每次抓取走信息后,寄存器中原本存放这个信息的空间就空了出来,我的意思是:Serial.read()命令不是复制信息而是剪切信息。好,回过头来说,3个信息每次抓取1个,那么为了抓取寄存器中的所有信息,我们需要抓取3次,于是我们采用了一个 for循环,循环数设定为j,这样就可以自动抓取所有寄存器中的信息了。每次抓取的数据我们将会存储到数组A中,A是我们在程序开头就定义好的一个字符型数组。关于A数组要注意两点:1)必须定义为字符型数组,因为我们讲过电脑串口输入的所有信息都是字符形式传输的,就算是阿拉伯数字也只是一个字符,而不是真的阿拉伯数字;2)A数组的空间大小在定义的时候要给足,假如我们打算每次处理1帧的数据,那么A的空间就要给到1帧或更多;假如我们每次打算处理3帧的数据,那么A的空间就要大于等于3帧。然后我们把“帧”换算成字节来定义A的空间。关于“帧”和字节的关系本篇前半部分有讲。

        以上就是串口通信的的原理和内部传输过程。通常我们购买一些涉及到串口通信的芯片时,买家都会提供一些资料,这些资料里往往会有适用于这款芯片的“串口通信库”或者现成的串口通信程序,所以我们也不需要完全自己去编写串口通信的程序,但是本篇讲的通信原理是一定要了解的。

你可能感兴趣的:(Arduino菜鸟通俗版解读系列(4)串口通信---USART)