前面章节已经学习过了USB的引脚定义了,但是对于其中的USB 2.0的两根数据线D+和D-所对应的数据传输,却没有详细介绍。此处就是介绍,在此串行数据线中,数据是如何被编码和传送的。
USB所传输的数据,用的数据编码方式是NRZI(Non-Return-to-Zero Inverted),其具体的含义解释,看到有位网友已经非常清晰的分析过了,我就不重复造轮子了。
USB 的 NRZI 编码
为了防止这位网友的服务器出问题,下面我再复制一份。
这两天继续看 USB 相关的内容,准备用纯软件实现一下 USB 设备传输,为将来的项目打好基础。
首先碰到的就是这个 NRZI 编码的问题了,基础太薄弱,看了一上午总算明白了大概。
首先,USB 的数据是串行发送的,就像 UART、I2C、SPI 等等,连续的01 信号只通过一根数据线发送给接受者。
但是因为发送者和接收者运行的频率不一样,信号的同步就是个问题,比如,接受者接收到了一个持续一段时间的低电平,无法得知这究竟是代表了5个0 还是1000个0。
一个解决办法,就是在传输数据信号的同时,附加一个时钟信号,用来同步两端的传输,接受者在时钟信号的辅助下对数据信号采样,就可以正确解析出发送的数据了,比如 I2C 就是这样做的,SDA 来传输数据,SCL 来传输同步时钟:
图为I2C数据编码格式
虽然这样解决了问题,但是却需要附加一根时钟信号线来传输时钟。有没有不需要附加的时钟信号,也能保持两端的同步呢?
有的,这就是 RZ 编码(Return-to-zero Code),也叫做归零编码。
在 RZ 编码中,正电平代表逻辑 1,负电平代表逻辑 0,并且,每传输完一位数据,信号返回到零电平,也就是说,信号线上会出现 3 种电平:正电平、负电平、零电平:
图为归零编码
从图上就可以看出来,因为每位传输之后都要归零,所以接收者只要在信号归零后采样即可,这样就不在需要单独的时钟信号。实际上, RZ 编码就是相当于把时钟信号用归零编码在了数据之内。这样的信号也叫做自同步(self-clocking)信号。
这样虽然省了时钟数据线,但是还是有缺点的,因为在 RZ 编码中,大部分的数据带宽,都用来传输“归零”而浪费掉了。
那么,我们去掉这个归零步骤,NRZ 编码(Non-return-to-zero Code)就出现了,和 RZ 的区别就是 NRZ 是不需要归零的:
图为非归零编码
这样,浪费的带宽又回来了,不过又丧失宝贵的自同步特性了,貌似我们又回到了原点,其实这个问题也是可以解决的,不过待会儿再讲,先看看什么是 NRZI:
NRZI 编码(Non-Return-to-Zero Inverted Code)和 NRZ 的区别就是 NRZI 用信号的翻转代表一个逻辑,信号保持不变代表另外一个逻辑。
USB 传输的编码就是 NRZI 格式,在 USB 中,电平翻转代表逻辑 0,电平不变代表逻辑1:
图为NRZ和NRZI
翻转的信号本身可以作为一种通知机制,而且可以看到,即使把 NRZI 的波形完全翻转,所代表的数据序列还是一样的,对于像 USB 这种通过差分线来传输的信号尤其方便~
现在再回到那个同步问题:
的确,NRZ 和 NRZI 都没有自同步特性,但是可以用一些特殊的技巧解决。
比如,先发送一个同步头,内容是 0101010 的方波,让接受者通过这个同步头计算出发送者的频率,然后再用这个频率来采样之后的数据信号,就可以了。
在 USB 中,每个 USB 数据包,最开始都有个同步域(SYNC),这个域固定为 0000 0001,这个域通过 NRZI 编码之后,就是一串方波(复习下前面:NRZI 遇 0 翻转遇 1
此外,因为在 USB 的 NRZI 编码下,逻辑 0 会造成电平翻转,所以接收者在接收数据的同时,根据接收到的翻转信号不断调整同步频率,保证数据传输正确。
但是,这样还是会有一个问题,就是虽然接收者可以主动和发送者的频率匹配,但是两者之间总会有误差。
假如数据信号是 1000个逻辑1,经过 USB 的 NRZI 编码之后,就是很长一段没有变化的电平,在这种情况下,即使接受者的频率和发送者相差千分之一,就会造成把数据采样成 1001个或者 999个了。
USB对这个问题的解决办法,就是强制插0,也就是传说中的bit-stuffing,如果要传输的数据中有6个连续的1,发送前就会在第6个1后面强制插入一个0,让发送的信号强制出现翻转,从而强制接受者进行频率调整。这就使得接收器,逻辑上至少每7个位会出现一次电平转换,用来保证数据和时钟同步。接受者只要删除6个连续 1 之后的0,就可以恢复原始的数据了。
当然在协议中,这两点说的也比较清晰,对着协议看一下就可以知道NRZI的编码规则。