异步通信





5.00/5 (8投票s)
异步串口通信是如何工作的?有哪些Arduino库支持它?可能出现哪些问题?
串口异步通信是两个电子设备之间最常见的通信形式之一。让我们来看看有哪些Arduino库支持它,并检查一下它们的性能如何。
异步串口通信
使用异步串口通信,只需要两根线(加上地线)就可以连接两个电子系统。事实上,早期的电传打字机就是这样连接的。几年后,电传打字机被连接到计算机上,为操作员提供I/O设备。如今,这种通信方式常用于不同电子设备之间。一根线用于发送信号(TX),另一根用于接收信号(RX)。
通过这种设置,可以同时发送和接收(这种通信方式称为全双工)。也可以更简单,只使用一根线。但是,这样一次只能有一个设备发送,另一个必须监听(这称为半双工)。 debugwire协议,用于调试小型AVR MCU的硬件调试,就采用了这种机制。它使用RESET线在硬件调试器和MCU之间进行通信。
发送一个字节
异步通信的特征是,没有时钟信号指示线路上的数据何时有效(正如同步的I2C和SPI协议那样)。这意味着通信双方必须知道使用何种通信速度,并且在读写数据时必须保持该速度。
然而,不仅速度,格式也需要事先约定。如今,通常通过发送一个起始位(逻辑0),然后是数据字节(8位),不带奇偶校验位,最后是一个停止位(逻辑1)来传输一个所谓的帧。这被称为8N1格式。此外,通常的解释是传输从最低有效位开始(这是小端序)。如果用逻辑分析仪记录一个字节的传输,可能看起来像这样
空闲状态是线路处于高电平。起始位(大约在12 µs开始)始终是0位。然后,数据字节(在本例中为0x55)的位以反向方式传输,即最低有效位先。传输完8位后,以停止位结束传输,停止位始终是1位。之后,可以传输新字节,或者线路可以保持空闲状态。
在接收端,等待下降沿,它标志着起始位的开始。然后等待1.5个位时间,然后采样第一个(最低有效)位。之后,始终再等待一个位时间来采样位时间中间的位。
潜在的时序问题
然而,时序有时也可能出错。原因可能是系统时钟不准确,或者通用异步收发器(UART)设备无法从系统时钟生成正确的速率。例如,当以16 MHz运行AVR MCU时,以115200 bps的速度,你可能慢3.5%或快2.1%(参见WormFood的AVR波特率计算器),其中Arduino核心决定快2.1%。
那么系统时钟呢?幸运的是,Arduino UNO使用晶体振荡器,其精度应为100 ppm(=0.0001%)或更高。事实上,它确实是这样,如下面的图片所示。
然而,如果使用AVR MCU的内部RC振荡器,其精度保证仅为±10%。但在我见过的所有MCU中,都是±2%。通过用户校准,可以将精度降低到±1%。
那么,如果一方发送的比特速度比接收方预期的快或慢,对异步通信会产生什么后果?好消息是,我们只需要考虑一个帧,因为一个帧接收完毕后,时序就会从下一个起始位重新开始。这意味着错误不会在多个帧之间累积。
下图显示了以三种不同速度传输0x55。中间的是115200时的正确速度。上面的线显示了传输速度慢5%时的情况,下面的线显示了传输速度快5%时的情况。
可以看到,误差会随时间累积。由于位的数值在位时间中间确定,偏差5%时,仍然可以确定最后一个位的正确值(虚线所在处),假设中间的线反映了接收方的时序。然而,这显然不是最大的可能偏差。那么,多大的偏差会导致在第八个位中间,即8.5个位时间后,累积误差达到50%呢?
求解方程得到x = 5.88
。所以理论上,任何优于5.88%的都应该没问题。在Analog Devices关于UART通信协议的时钟精度要求的教程中,他们认为在大多数情况下,不能忽略信号的上升沿和下降沿时间。他们认为在“恶劣”环境中,只有位时间中间的50%可以被认为是稳定的,而在“正常”场景中,可能是75%。此外,他们假设您想验证停止位确实是逻辑1。基于这些假设,在“恶劣”环境中,可接受的相对误差降低到2.6%,在“正常”环境中降低到3.9%。
不幸的是,时序问题还有其他来源。一个是中断服务产生的延迟,例如,计算毫秒的定时器溢出中断。如引用的博客文章所示,这需要6.625 µs,对于我们在57600 bps下通信(位时间为17.36 µs)来说,这是一个相当大的时间。如果在软件中实现异步通信,那么就会依赖中断,在这种情况下,中断可能会延迟近7 µs!因此,在这些情况下,建议禁用定时器溢出中断。
最后一个问题是,在软件中实现异步通信时,接收一个字节需要在中断例程中完成。这意味着处理接收到的字节的时间非常短,只有停止位的位时间。这可能很快导致缓冲区溢出问题。
串口通信库
当您使用Arduino UNO时,异步通信的常用方法是使用Serial对象,它是HardwareSerial
类的一个实例。UART完成大部分工作,只有当一个字节被接收或一个字节可以被发送时,才会产生中断。接收数据的中断服务例程使用5 µs,发送下一个字节的中断例程在最坏情况下需要8.75 µs。
由于UNO只有一个硬件UART,通常需要使用软件UART。如果您使用没有硬件UART的小型ATtiny,那么除了使用软件UART别无选择。标准解决方案是SoftwareSerial库。但是,有三个主要问题。首先,需要尽可能准确地检测起始位的下降沿。这是通过接收线上的引脚变化中断来实现的。因此,其他中断是适得其反的。它们可能导致采样位太晚而误解接收到的比特流。例如,如果以57600的比特率接收数据,则位时间为17.4 µs。如果millis
中断在起始位下降沿之前触发,那么起始位的检测将延迟6.6 µs,这已经是位时间的1/3。再加一点其他变化,很容易误解数据流。
其次,发送和接收数据需要精确的时序,因此在此期间会禁用中断,而不能进行其他操作。由于接收例程会等待到停止位,因此只有半个位时间可用于处理接收到的字节。如果短时间内收到太多字节,接收缓冲区(64字节)可能会溢出。
第三,即使是慢速比特率也可能导致问题。例如,如果以1200 bps通信,则位时间为833 µs,因此中断将被阻塞至少9.5倍的位时间,即7.9 ms。这意味着每毫秒触发一次的millis
中断无法及时得到服务。
至少有两个替代SoftwareSerial的方案。一个是picoUART,一个非常极简的软件UART,我使用的是1.2.0版本。它只使用最少量的代码,但时序非常精确。然而,与SoftwareSerial不同,输入/输出引脚和通信速度必须在编译时固定。与SoftwareSerial类似,几乎整个帧时间都会被中断阻塞。接收数据可以通过轮询(即主动等待新数据)或中断来完成。在后一种情况下,只有一个字节的接收缓冲区,这很容易导致丢失数据字节。
我们的最后一个候选是AltSoftSerial,它使用与前两个库的位操作技术截然不同的方法。它利用ATmega328P上的Timer 1的输入捕获功能来捕获输入线信号边沿发生的时间。这是以中断驱动的方式完成的,这意味着该方法带来的中断延迟明显短于9.5倍的位时间。乐观估计为2-3 µs。然而,实际上,在最坏情况下可能达到16 µs。这仍然比其他方法好得多,但对于更高的比特率来说可能是不可行的。此外,据称该库可以容忍近一个位时间的延迟。加上其自身的16µs,这绝对是过于乐观的。要发送的字节是通过相同的定时器的输出比较功能以中断驱动的方式生成的。因此,与两种位操作方法相比,该库需要的MCU周期要少得多。当然,这也要付出代价:输入和输出引脚是固定的,并且不能使用与Timer 1相关的引脚的PWM功能。
那么,哪个是最好的替代品?SoftwareSerial是最灵活的。你可以使用任何引脚作为输入和输出。甚至可以设置多个SoftwareSerial实例,但一次只有一个可以处于活动状态。picoUART拥有最小的内存占用空间和令人印象深刻的时序精度。它似乎非常适合小型ATtiny。最后,AltSoftSerial依赖于定时器而不是延迟循环,时序非常准确,并且占用的CPU周期最少。
在下一节中,我们将看看这些库可以可靠地处理哪些通信速度,以及它们对比特率的偏差有多大的容忍度。
对不同串口库进行压力测试
发送时的时序有多准确?接收数据时库的容错能力如何?为了测量发送时的比特率准确性,我使用了我的Saleae逻辑分析仪来测量生成的比特率。为了对接收功能进行压力测试,我使用了一个由Python脚本驱动的FT232R板。
首先,我们来看看传输比特率。
比特率 | 硬件- 串口 | 软件- 串口 | pico- UART | AltSoft- 串口 |
1200 | 0.0% | 0.0% | 0.0% | 0.0% |
2400 | 0.0% | 0.0% | 0.0% | 0.0% |
4800 | 0.0% | 0.0% | 0.0% | -0.1% |
9600 | +0.2% | 0.0% | 0.0% | -0.1% |
19200 | +0.2% | -0.3% | +0.1% | -0.1% |
38400 | +0.2% | -0.7% | 0.0% | -0.1% |
57600 | +2.2% | -0.7% | 0.0% | -0.1% |
115200 | +2.2% | -0.7% | 0.0% | -0.1% |
230400 | -3.6% | -4.3% | +0.5% | ____ |
7812 | 0.0% | -0.2% | +0.1% | -0.1% |
15625 | 0.0% | -0.3% | 0.0% | -0.1% |
31250 | 0.0% | 0.0% | +0.1% | -0.1% |
62500 | 0.0% | -1.4% | +0.1% | -0.1% |
125000 | -0.1% | -2.8% | +0.1% | -0.1% |
250000 | -0.1% | -6.7% | -0.1% | ____ |
500000 | -0.1% | -12.4% | -0.2% | ____ |
1M | -0.1% | -22.0% | -0.1% | ____ |
传输时的通信速度偏差
这里有几点值得关注。首先,即使是硬件UART,也并不总是能生成接近标称值的比特率。对于57600和115200 bps,实际比特率快了2.2%。更糟糕的是,对于230400 bps,它慢了3.6%,这很成问题。造成这些偏差的原因已经提到:AVR波特率发生器无法生成所有速率。下一个值得关注的是,SoftwareSerial的比特率最好不要超过115200 bps。类似地,当请求的比特率高于125000 bps时,AltSoftSerial会拒绝工作。picoUART是明显的赢家。
那么,库在接收数据时表现如何?我使用了以下草图(略作简化)来测试SoftwareSerial的性能。对于其他库,看起来也差不多。请注意,我没有使用available()
方法,而是简单地读取并忽略结果(如果小于零)。这是读取流中字节的最快方式。
unsigned long baud=115200;
#include <SoftwareSerial.h>
SoftwareSerial UART = SoftwareSerial(8, 9);
const int RTS=12; // RTS line
void setup() {
// TIMSK0 = 0;
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
UART.begin(baud);
}
void loop() {
byte expect = 0;
int inp;
while (1) {
inp = UART.read();
if (inp >= 0) {
if (inp != expect++) {
digitalWrite(LED_BUILTIN, LOW);
pinMode(DTR, OUTPUT); // pull DTR low
while (1);
}
}
}
}
要接收的数据是由一个驱动FT232R板的Python脚本生成的。FT232R可以生成几乎任何你想要的比特率。它会选择一个最接近2400万除以比特率整数结果的比特率。这是(简化的)脚本。你可以用以下参数调用它
- <bps> – 基本比特率;
- <stopbits> – 停止位数,通常为1;如果通信应减慢,则可设置为2;
- <num> – 每个速度步长要发送的字节数;
- <dir> – 速度步长的变化方向,可以是#*#或‘-’
- <startstep> – 起始偏差,例如3.9,表示3.9%的偏差。
脚本以千分之几的步长系统地改变比特率,当Arduino草图因为读取到意外字节而将CTS线拉低时停止。
#!/usr/bin/env python3
import serial
import sys
import time
serialport = '/dev/cu.usbserial-XXXXX'
def usage():
print("serialgen.py <bps> <stopbits> <num> <dir> <startstep>")
exit()
if len(sys.argv) != 6: usage()
bps = int(sys.argv[1])
addstep = bps/100
stopbits = float(sys.argv[2])
maxwrite = int(sys.argv[3])
if sys.argv[4] == '+': direction = 1
else: direction = -1
step = float(sys.argv[5])*direction
outbyte = 0;
while (1):
dev = bps+(step*addstep)
print("bps:", bps, " deviation:", "%4.1f" % (step,),
" is:", int(dev))
ser = serial.Serial(serialport, int(dev), stopbits=stopbits)
time.sleep(0.05)
i = 0;
while i < maxwrite:
ser.write(outbyte.to_bytes(1,'big'))
i += 1
outbyte = (outbyte + 1) % 256
time.sleep((maxwrite+5)*10.1/(bps+(step*addstep)))
# otherwise bytes get dropped
if ser.cts:
print("Failure!")
exit()
ser.flush()
time.sleep(0.3) # otherwise bytes get dropped
ser.close()
step += 0.1*direction
对于某些比特率,当millis
中断激活时,库会报错,这并不奇怪,因为由计时中断例程引起的6.6 µs的中断延迟接近115200 bps时的1个位时间。为了获得有意义的结果,我禁用了该中断,并在下表中标注了“#”。有时,读取两个字节之间的空闲时间太短。我通过使用带有2个停止位的发送格式来允许一些额外时间。这在表中用星号“*”表示。
对于每个库,我报告了库容忍的最大负相对偏差和最大正相对偏差。我至少测试了10,000个字节。对于所有高于10,000 bps的比特率,我测试了100,000个字节。对于所有高于或等于100,000 bps的比特率,我使用了100万个字节。报告的百分比是可容忍的,而下一个更高(或更低)的比特率会导致错误。请注意,尤其是在较高比特率下,到下一个比特率的步长可能相当大(例如,在100,000 bps左右的比特率下为0.5%)。最后,应该注意的是,我使用了picoUART的阻塞版本,该版本在调用读取例程后会阻塞中断。
比特率 | HardwareSerial | SoftwareSerial | picoUART | AltSoftSerial | ||||||||
1200 | -5.9% | +2.2% | -5.6% | +5.6% | -5.7% | +5.5% | -5.8% | +5.5% | ||||
2400 | -5.8% | +2.2% | -5.6% | +5.7% | -5.7% | +5.4% | -5.8% | +5.4% | ||||
4800 | -5.9% | +2.1% | -5.4% | +5.6% | -5.7% | +5.4% | -5.8% | +5.4% | ||||
9600 | -5.7% | +2.3% | -5.2% | +5.7% | -5.7% | +5.3% | -5.8% | +5.3% | ||||
19200 | -5.6% | +2.2% | -4.8% | +5.2% | -5.6% | +5.4% | -5.7% | +5.5% | ||||
38400 | -5.6% | +2.2% | -4.1% | +4.3% | -5.6% | +5.3% | -5.5% | +5.2% | ||||
57600 | -3.6% | +5.4% | -2.5% | +4.1% | -5.6% | +5.3% | -2.6% | +4.2% | ||||
115200 | -3.5% | +4.3% | -0.6% | +6.4% | # | -5.1% | +5.2% | -1.8% | +5.1% | #* | ||
230400 | -8.5% | -1.9% | ____ | ____ | -4.3% | +5.3% | # | ____ | ____ | |||
7812 | -5.9% | +2.1% | -5.4% | +5.6% | -5.7% | +5.4% | -5.8% | +5.3% | ||||
15625 | -5.7% | +2.1% | -5.1% | +5.2% | -5.6% | +5.3% | -5.8% | +5.3% | ||||
31250 | -5.7% | +2.3% | -4.7% | +4.8% | -5.5% | +5.4% | -5.7% | +5.3% | ||||
62500 | -5.7% | +2.3% | -3.3% | +3.6% | -5.5% | +5.3% | -2.4% | +3.6% | ||||
125000 | -5.9% | +2.2% | -2.5% | +7.8% | #* | -5.6% | +5.2% | ____ | ____ | |||
250000 | -4.9% | +2.4% | ____ | ____ | -4.8% | +4.3% | # | ____ | ____ | |||
500000 | -4.0% | +2.1% | ____ | ____ | -3.9% | +4.4% | # | ____ | ____ | |||
1M | -4.1% | +1.7% | ____ | ____ | ____ | ____ | ____ | ____ |
接收数据时可能的速度偏差
(“#” = 无millis中断,“*” = 2个停止位)
这张表中有很多有趣的结果。首先,总的来说,显然硬件UART是最稳健的,这并不奇怪。但有两点需要注意。我完全不清楚为什么硬件UART的容差区间不是对称的。为什么在标称1200 bps时,它能容忍慢5.9%的比特率,但不能容忍快2.3%的比特率?我不知道!此外,在230400 bps时,硬件UART根本无法以标称速度接收字节!尽管如此,两个Arduino UNO之间仍然可以正常通信,因为它们具有相同的错误。
其次,SoftwareSerial在57600 bps及以下的速度下表现良好。但在112500 bps时,需要禁用millis中断,必须切换到两个停止位,而且生成的比特率(偏差-0.7%)超出了接收器的容差区间。因此,如果您让两个系统使用SoftwareSerial以115200 bps通信,很可能会遇到问题。
第三,picoUART似乎是最稳健的解决方案,即使在高比特率下也是如此。但需要注意的是,我使用的是阻塞版本,它会阻塞所有中断。对于中断驱动版本,我怀疑在高比特率下可能会出现问题,因为ISR需要更多的周期,而millis中断可能会干扰时序。最后,它也是最不灵活的解决方案,因为您必须在编译时固定比特率和引脚。
第四,AltSoftSerial不如我想象的那么稳健。在56700 bps及以下的比特率下,您比其他软件解决方案拥有更多可用的CPU周期,这是一个明显的优点。然而,在57600 bps时,可能应该禁用定时器中断(以及可能其他中断),因为组合起来的最坏情况中断延迟非常接近1个位时间。115200 bps只有在关闭所有中断并使用两个停止位的情况下才能维持。