CaptDam's Blog

Top of page

DIY一个ROV——通信系统——传输层

这里主要介绍ROV的通讯系统的传输层。在通讯系统模型中,传输层居于应用层与物理层之间。一方面,传输层将会对应用数据进行打包,再一个字节字节地发送出去;另一方面,传输层将会从物理层取得数据,重新封装为数据包后,提供数据给应用层做进一步的使用。 虽然物理层通过使用高压差分信号的方式提供全双工异步通信,但这套机制还是有很小的可能性会失效。这样,就需要传输层能够检测到错误并采取进一步行动。 物理层使用的是UART通信设备,一种以面向字的传输硬件。不同于在包的首尾有开始与结束信号的I2C与SPI,UART只在每个字的开头与结尾放置起始帧与结束帧。因此,传输层将需要能够判断包的开头与结尾。 以应用层软件的视角来看,物理层就是一个架构软件。使用传输层,应用层软件将全力处理应用层任务,而不需要面对繁杂的硬件系统。举一个例子,硬件只能一次发送一个字节的数据,但是应用层会一下子生成一个大的包。如果没有传输层,应用层软件将不得不一个字节一个字节地发送数据。而有了传输层的介入,应用层将可以直接将一整个包发送给传输层,并专心处理应用层任务。

简介:这里主要介绍ROV的通讯系统的传输层。在通讯系统模型中,传输层居于应用层与物理层之间。一方面,传输层将会对应用数据进行打包,再一个字节字节地发送出去;另一方面,传输层将会从物理层取得数据,重新封装为数据包后,提供数据给应用层做进一步的使用。 虽然物理层通过使用高压差分信号的方式提供全双工异步通信,但这套机制还是有很小的可能性会失效。这样,就需要传输层能够检测到错误并采取进一步行动。 物理层使用的是UART通信设备,一种以面向字的传输硬件。不同于在包的首尾有开始与结束信号的I2C与SPI,UART只在每个字的开头与结尾放置起始帧与结束帧。因此,传输层将需要能够判断包的开头与结尾。 以应用层软件的视角来看,物理层就是一个架构软件。使用传输层,应用层软件将全力处理应用层任务,而不需要面对繁杂的硬件系统。举一个例子,硬件只能一次发送一个字节的数据,但是应用层会一下子生成一个大的包。如果没有传输层,应用层软件将不得不一个字节一个字节地发送数据。而有了传输层的介入,应用层将可以直接将一整个包发送给传输层,并专心处理应用层任务。

修改:2019-06-09 20:32:47

发布:2019-06-09 17:04:47

🌍zh

施工中

此页面正在施工中,页面内容随时可能发生更改。

关于

这里主要介绍ROV的通讯系统的传输层。在通讯系统模型中,传输层居于应用层与物理层之间。一方面,传输层将会对应用数据进行打包,再一个字节字节地发送出去;另一方面,传输层将会从物理层取得数据,重新封装为数据包后,提供数据给应用层做进一步的使用。

虽然物理层通过使用高压差分信号的方式提供全双工异步通信,但这套机制还是有很小的可能性会失效。这样,就需要传输层能够检测到错误并采取进一步行动。

物理层使用的是UART通信设备,一种以面向字的传输硬件。不同于在包的首尾有开始与结束信号的I2C与SPI,UART只在每个字的开头与结尾放置起始帧与结束帧。因此,传输层将需要能够判断包的开头与结尾。

以应用层软件的视角来看,物理层就是一个架构软件。使用传输层,应用层软件将全力处理应用层任务,而不需要面对繁杂的硬件系统。举一个例子,硬件只能一次发送一个字节的数据,但是应用层会一下子生成一个大的包。如果没有传输层,应用层软件将不得不一个字节一个字节地发送数据。而有了传输层的介入,应用层将可以直接将一整个包发送给传输层,并专心处理应用层任务。

实际上,站在硬件的角度来讲,传输层并不能提高系统效率。实际上,因为层与层之间额外的数据传输与程序控制,传输层的介入反而会降低系统性能。但是,引入物理层的概念可以起到“重心分离”的作用,有助于开发者定义系统与编写软件。

在这篇文档里面,下列名词将会被使用:

- 控制包:操作台发送给ROV的一串数字信号,包含对ROV的直接控制与自动驾驶程序的配置。

- 数据包:ROV返回给控制台的一串数字信号,包括ROV的各种姿态参数。

- SYNCH:ROV返回给控制台的用于唤醒控制台的信号。因为这个信号会同步ROV与控制台,所以也叫做同步信号。

- Tx缓存/Rx缓存:内存块,传输层用其储存发送/接收的中间数据。

下面这两个词汇在下文中大量使用,但经常被人搞混,这里重新复习一遍。
字(word):字是硬件与软件直接操作的数据的长度。比如,ATmega328P的字是8比特长,因为ATmega328的通用寄存器是8比特宽的(其程序/闪存字是16比特长);UART的字是4到9比特长,因为UART包括了1个起始帧,4-9个数据帧,1-2个结束帧(在这个系统中,UART的字是8比特固定长度);Intel Core i7的字是64比特长,因为其主要累加器是64比特宽(实际上现在super-scaler out-of-order外加扩展指令集,所以宽度已经不止64了,但是我们任然认为其是64比特);一块FPGA的字可以是1234比特长,因为程序里可以写“std_logic_vector (1233 downto 0)”。
字节(byte):一个字节就是8比特。一个字可以是一个字节,2ge,或者4个,甚至半个字节都可以。

解决方案

TCP?

TCP是一个在计算机与高端嵌入式设备中使用的不错的协议。但是,TCP消耗了大量的内存与计算力。因此,对于低端嵌入式系统来说,TCP并不是一个好方案。

因此,需要定义一个自定义通讯协议。

Send and Retry?

1. 主机发送包给从机。

2. 如果从机接收到包,并且这个包是一个完整的,没有错误的包,从机返回ACK。否则,从机不返回信号。

3. 如果主机没有收到ACK,主机会重新发送包

问题1:这个方案需要额外的计时器,用于测量ACK超时。

问题2:消耗了大量的运算能力。

问题3:对全双工通信并不友好。

广播

1. ROV每1/4秒向控制台发数据包。

2. 控制台接收到第一个字后,发控制包给ROV。

优点1:避免了ROV与控制台之间时钟不同步的问题。

优点2:便于实现,只需要少量的计算能力。因为这个协议只需要设备按时无脑发包即可。_(:3 _| <)__

问题1:如何检测丢包与损坏?如何将错误信息返回发送方?丢包与错码的处理方案是什么?

问题2:如何区定义的开头与结尾?

广播方案

包结构总览

ROV与控制台每250毫秒进行一次数据交换。以应用层软件的视角来看,交换的数据以包(用C语言的话来说:struct结构)的形式存在。

一个包由一串字构成。在一个包中,有15个数据字,外加包的末尾的一个校验和。如果将包中所有的字加起来,不考虑进位,其和应该为0。换言之,校验和是包中其他字加起来之和的负数。如果校验和与数据不匹配,那就表示包损坏了。

对于数据包(ROV发送给控制台的包),这里还有额外一个SYNCH字位于包的首部。SYNCH用于唤醒控制台,并将控制台的状态同步到ROV的状态。SYNCH可以是任何值。SYNCH不计入包长度,也不参与校验和计算。

包的长度(16个字)是固定的并且写死在传输层架构程序中的。如果收到的包的长度比这个长度短,那么久说明这个包是不完整的,该包将会被丢弃。如果接收到的包比这个长度长(这种情况几乎不会发生),多余的字将会覆盖缓存中最后一个字。

对于应用层软件来说,应用层数据数据长度应该能被装入15字节长的包。如果实际数据没有15字节长,应用层需要使用随机数或0将其填充为15字节长,因为包的长度是固定的。

推荐是使用0进行填充,因为这样可以减少Checksum的运算。
假设目前有效部分的校验和为x。添加m个0后,其校验和为x+m*0=x。可见,校验和没有改变,因此只要计算有效部分校验和即可得到整个包的校验和。
实际上整个包才16字节长,多算几个校验和也不会用多少时间。难点倒是怎么得到填充用的随机数。

参考物理层,通信的速率(BAUD)被设定为2400。换言之,ROV与控制台每秒钟最多可以交换2400个比特。因为ROV与控制台没1/4秒进行一次数据交换,每个字包含1位起始帧,8位数据帧,1位结束帧(共10位),因此每250毫秒最大可以支持60字节数据交换。

因为ROV与控制台需要时间准备与执行数据,实际可交换数据将低于60字节。因此,这里决定定义包长度位固定16字节。选择16的好处是,16正好是2的倍数,16字节正好可以放进一个内存块,这样可以方便软件实现。虽然8字节与32字节也可以正好放进一个内存块,但是8比特对于应用层来说太小了,无法容纳应用层的数据;32比特又太大,消耗了太多的内存空间。因此,最终选择了16。

因为UART是面对字的通信设备,所以传输层将需要能定义包的开头与结尾。

在ROV侧,这个算法非常简单。发送SYNCH后接收到的第一个字就是包的开头,第16个字就是结尾。

在控制台侧,算法就会复杂不少。因为ROV的发包方式是,首先以波特率发送一连串字,然后等待,直到下一个250ms再发送下一个包。假设ROV会发送10个字,每个字需要发送10ms。在这种情况下,ROV发送:

- 包1字1 @ t = 0ms
- 包1字2 @ t = 10ms
- ......
- 包1字9 @ t = 80ms
- 包1字10@ t = 90ms
- 空闲
- 包2字1 @ t = 250ms
- 包2字2 @ t = 260ms

由此可见,在包1与包2之间,存在150ms的空间。因此控制台可以通过检测上一字接收时间与当前字接收时间只差的方式来判断包的开头。如果这个间断时间大于20ms(发送5个字节所需要的时间),那就说明当前字为新的包的包头。和ROV一样,第16个字为包尾。

没有ACK

这个方案没有ACK回包。因此,发送方将无从得接收方是否收到了正确的包。所以,所有的包都应该是无状态的(stateless)。虽然应用层可以建立基于无状态协议的状态通讯,但是那将使程序过于复杂。

另外,因为发送方将无从得接收方是否收到了正确的包,发送方将无法在通讯错误时重新发送包。实际上,重新发送斌不能给这个系统带来任何好处。因为ROV与控制台每250ms就会交换一次最新的数据,因此并没有理由消耗时间重新发送过时的数据。

实现

关于UART

UART (Universal Asynchronous Receiver/Transmitter)是MCU用于收发数据的专用设备。UART拥有自己的时钟与逻辑电路,因此UART在处理信号时(比如采样数据,检测帧错误)并不需要使用CPU。当数据接收或发送完成时,UART控制器将会提出一个中断请求,告知CPU其工作已完成。

对于发射器来说,当CPU将要发送的数据写入UART缓存后,UART控制器将会打开发射器。数据将以比特流的方式发射,首先是起始帧,然后是数据帧,最后是结束帧。发送结束后,UART控制器提出中断请求。CPU可以将下一个要发送的字写入UART缓存,或是关闭UART发射器(不写UART缓存)。

对于接收器来说,这里有两级缓存。第一级是直接连接至IO的工作缓存。当一个字的所有比特都位移进入工作缓存后,并且探测到结束帧,UART控制器将会把数据从工作缓存移动到第二级缓存——中间数据缓存。接下来,UART控制器将提出中断请求,同时一级缓存继续监听IO。CPU应尽快从中间数据缓存中读取接收到的数据;否则,当下一个字抵达时,该数据将会被覆盖。

虽然UART可以在CPU处理其他任务时处理信号,但UART只能处理以字为单位的数据。对于这个ROV系统中使用的STC89C52RC与ATmega328P来说,UART的字的大小是一个字节。当发送数据时,CPU需要提供一个字节的数据给UART。发送完成后,UART控制器提出中断请求,CPU再将下一个字节的数据提供给UART。接受时,UART控制器在收到一个字节是提出中断请求,CPU从缓存中读取该字节。当下一个字节抵达时,UART控制器再提出中断请求,CPU再从缓存中读取这个字节。

UART是面向字的设备,但是应用层软件是面向包的软件。为了使应用层程序与UART互动,传输层将需要能够将包数据与字节流数据相互转换。

传输层架构

为了实现这个功能,传输层将需要两个内存块,分别叫Tx缓存与Rx缓存,用于储存中间数据。因为UART是面向字的,所以还需要额外的两个内存空间,用于存放指针Tx pointer与Rx pointer。

要发包,应用层软件将需要首先将数据写入Tx缓存。接下来,传输层将会计算校验和并储存校验和到缓存的尾部。然后,传输层会开始通过UART发送数据。当发送完第一个字后,UART控制器提出中断请求。传输层便会发送缓存中的第二个字。当缓存中所有字都发送后,传输层中断发射程序。

要收包,当数据抵达后,UART控制器提出中断请求。传输层就会将数据从UART缓存复制到Rx缓存中。当缓存写满且还有更多的数据时,缓存末尾的字将会被覆盖。

对于控制台的传输层来说,还有一个附加功能。传输层将会测量当前字与上一字之间的时间。如果这个时间大于20毫秒的话,传输层将会终止当前程序。这样就可以将控制台与ROV同步。

下面的伪代码展示了ROV与控制台的传输层如何工作:

特殊情况

在通信过程中,两种错误可能发生:

一个或多个字丢失

在这种情况下,缓存中的数据量将会不足。因此,该包将不会被处理。当下个包抵达后,该包将会被舍弃。

有可能,有连续的多个字丢失。在这种情况下,可能会造成大于20毫秒的中断。一方面,控制台会认为一个新的包被发送了过来。但是,因为两个包的长度都不足,所以两个包都会被舍弃。当控制台接收到下一个包时,便可以得到最新的实时信息。另一方面,控制台会发送新的数据包给ROV。这会造成ROV接收缓存缓存溢出(但是缓存是有保护的)。当ROV读取缓存时,可以使用校验和发现这个问题。

一个或多个字损坏(乱码)

这种情况下,校验和与包数据将会不吻合。因此,包将会被舍弃。

有可能多个字顺坏最后造成了校验和吻合的家假象。这种情况下,ROV将会收到错误指令,控制台将会收到错误信息。这个问题将会持续250毫秒, 当下一个包抵达后,问题将会被解决。