1 项目背景 (技术Q交流群:544453837)
RS-485 是从RS-422基础上发展而来的,所以RS-485许多电气规定与RS-422相仿。如都采用平衡传输方式、都需要在传输线上接终接电阻等。RS-485可以采用二线与四线方式,二线制可实现真正的多点双向通信,而采用四线连接时,与RS-422一样只能实现点对多的通信,即只能有一个主(Master)设备,其余为从设备,但它比RS-422有改进,无论四线还是二线连接方式总线上可多接到32个设备。 RS-485与RS-422的不同还在于其共模输出电压是不同的,RS-485是-7V至+12V之间,而RS-422在-7V至+7V之间,RS-485接收器最小输入阻抗为12kΩ、RS-422是4kΩ;由于RS-485满足所有RS-422的规范,所以RS-485的驱动器可以在RS-422网络中应用。 RS-485与RS-422一样,其最大传输距离约为1219米,最大传输速率为10Mb/s。平衡双绞线的长度与传输速率成反比,在100kb/s速率以下,才可能使用规定最长的电缆长度。只有在很短的距离下才能获得最高速率传输。一般100米长双绞线最大传输速率仅为1Mb/s。 CH340 由于串口(COM)不支持热插拔及传输速率较低,目前大部分新主板和便携电脑已开始取消该接口,只有工控和测量设备以及部分通信设备中还保留有串口。 现在的电脑大部分都有USB接口而没有串口,为了使用串口,我们需要一个USB转串口的芯片,它的功能是让电脑把USB当串口来使用。这种类型的芯片很多,明德扬教学板使用的是CH340芯片。 CH340是一个USB总线的转接芯片,实现USB转串口、USB转IrDA红外或者USB转打印口。 在串口方式下,CH340提供常用的MODE联络信号,用于为计算机扩展异步串口中,或者将普通的串口设备直接升级到USB总线。
明德扬教学板的串口功能原理如下图所示。电脑通过USB线,连接到教学板上的USB接口,USB接口连接到CH340芯片,CH340芯片与FPGA相连。在FPGA看来,串口其实就是两根线:输入线USB_RXD和输出线USB_TXD,其他电气特性、电平转换的工作,都由CH340搞好了。FPGA通过USB_RXD接收来自电脑过来的串口数据;通过USB_TXD发数据给电脑。
串口时序 USB_RXD和USB_TXD传输数据时,是将传输数据的每个字符一位接一位地传输。下面是USB_RXD和USB_TXD的时序。USB_RXD的时序由CH340芯片产生,FPGA根据时序来接收数据;USB_TXD的时序由FPGA芯片产生,FPGA要按规范来产生时序,使得CH340可以正确地接收。我们可以把产生时序的叫MASTER(主),接收数据叫SLAVE(从)。
串口时序主要包括:空闲、起始位、数据拉、校验位和停止位。 空闲:空闲状态下,数据线一直处于高电平状态。 起始位:当MASTER要发送数据时,首先会将数据线拉低“一段时间”,从而告知SLAVE有数据要传输了,要做好准备。 数据位:起始位之后是数据位,数据位的位数由双方约定,支持4、5、6、7、8位等。双方约定后才能正确地传输。每个数据位传输时都会占用“一段时间”,并且是从低位开始传输。图中LSB是低位的意思,MSB是高位的意思。例如要传输数据8’b00000001,传输时是先送最低位的“1”。 检验位:奇偶校验是一种非常简单常用的数据校验方式,分为奇校验和偶校验。奇校验需要保证传输的数据总共有奇数个逻辑高电平,若是偶校验则要保证传输的数据有偶数个逻辑高电平。即“奇偶”的意思就是数据中(包括该校验位)中1的个数。例如:传输的数据位是0100_0011。如果是奇校验,校验位是0,偶校验校验位是1。校验位不是必须项,双方可以约定不需要校验位,或者用奇校验,或者使用偶校验。 停止位:最后一个是停止位,MASTER必须保证有停止位,即把数据线变高“一段时间”。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。让SLAVE可以正确地识别下一轮数据的起始位。假如没有停止位,校验码刚好是0,数据连续发送,那么SLAVE就没法判断下一轮的起始位。对于SLAVE来说,接收完数据位或校验位后就表示接收完成,在停止位不需要做什么,只是等待下一轮起始就够了。 在时序图中,每个数据都会传输“一段时间”,这个一段时间非常重要,传输双方都要做好约定,否则就不能正确地通信。那么这个“一段时间”是多长时间呢?这跟波特率有关。在串口通信中,波特率是一个非常重要的概念。串口通信中常用的波特率是9600、19200、38400、57600、115200。波特率是每个码元传输的速率,在二进制数据传输中,和比特率相同,都是每个比特数据传输的速率,其倒数为1bit数据的位宽,也就是1bit数据持续的时间。有了这一时间段,就可用FPGA构造计数器实现比特周期的延时,从而实现特定的数据传输波特率。 例如,假设波特率为9600,数据位为8位,没有校验位,电脑要发数据8’b00110001给FPGA。考虑到波特率为9600,即每位占用时间为1s/9600=104166ns。那么FPGA的USB_RXD(图中的rx_uart)这根线将如下图变化。
中间信号,trigger连到触发器的信号输入端D,触发器的输出器连的是tri_ff0。将trigger取反,与tri_ff0相与,就得到信号neg_edge,如果neg_edge=1就表示检测到trigger的下降沿。将tri_ff0取反,与trigger相与,就得到信号pos_edge,如果pos_edge=1,就表示检测到trigger的上升沿。 我们来讲解这个原理,画出信号的波形图。
Tri_ff0是触发器的输出,因此tri_ff0的信号与trigger信号相似,只是相差一个时钟周期。我们也可以这样理解:每个时钟上升沿看到的tri_ff0的值,其实就是triffer信号上一个时钟看到的值,也就是tri_ff0是trigger之前的值。 然后我们在看第3时钟上升沿,此时trigger值为0,而tri_ff0的值为1,即当前trigger的值为0,之前的值为1,这就是下降沿,此时neg_edge为1。当看到neg_edge为1,就表示检测到trigger的下降沿了。 同样道理,在第7个时钟上升沿,看到trigger值为1,而之前值为0,pos_edge为1,表示检测到trigger的上升沿。 Verilog实现边沿检测电路的代码。
3.2.2 异步信号同步化 在讨论边沿检测的波形中,我们把trigger当成理想的同步信号,也就是trigger是满足D触发器的建立和保持时间的,这在同步系统中不是问题。但如果trigger不是理想的同步信号,例如外部按键信号,例如本工程的rx_uart信号。这些信号什么时候变化,完全是随机的。很有可能,在时钟上升沿变化,从而不满足触发器的建立时间和保持时间要求,从而出现亚稳态,导致系统崩溃。详细的原因,可以看D触发器中,亚稳态一节的内容。根据这一节内容的结论,我们需要对进来的信号打两拍(用两个触发器寄存一下),再来使用。
假设输入的信号trigger不是同步信号,那么要将该信号用2个触发器进行寄存,得到tri_ff0和tri_ff1。需要特别注意的是,tri_ff0绝对不要拿来当条件使用,只能使用tri_ff1。我们还需要检测边沿,根据前面所说,再用寄存器寄存,得到tri_ff2。根据tri_ff1和tri_ff2,我们就可以得到边沿检测。当tri_ff1==1且tri_ff2==0时,上升沿的pos_edge有效;当tri_ff1==0且tri_ff2==1时,下降沿的neg_edge有效。
我们总结一下。如果通过打两拍的方式,实现了信号的同步化。我们通过打一拍的方式,实现边沿检测电路。这两者不是一定同时出现的。如果进来的信号是异步信号,那就必须先同步化,然后再做检测。如果进来的信号本身就是同步信号,那就没有必要做同步化了,直接做边沿检测即可。 回到本工程的设计,我们需要检测rx_uart的下降沿,从而让flag_add变高。同时,我们注意到rx_uart是异步信号(PC 什么时候发送数据就是随机的)。所以需要将rx_uart先同步化,再做下降沿检测。所以先设计如下代码:
这样,flag_add变1的条件就变成:rx_uart_ff1==0&& rx_uart_ff2==1。 Flag_add变0的条件,可以完成收完9比特数据就变0,不用再计数了。所以变0条件:end_cnt1。 综上所述,可以写出flag_add的代码。
设计下data信号,该信号的值来自于图中第2~第9比特的值。第2比特的值赋给data[0],第3比特的值赋给data[1],以此类推,第9比特的值赋给data[7]。
由于每一个比特都持续5208个时钟周期,我们必须选定一个时刻,将值赋给data。
首先,不能在end_cnt0的时候赋值,如上图的点。因为我们这里的5208个时钟周期是理想、估算的数值,实际上是非常有可能有偏差的。如果我们在end_cnt0的时候取值,就有可能采错。 最保险的做法是在中间点取值。这样,即使有比较多的偏差,都不会影响到采样的正确性。
综上所述,我们在cnt0数到一半时采到当前rx_uart的值赋给dout,其中第2比特赋给led[0],第3比特赋给led[1],以此类推,第9比特赋给led[7]。 进一步用信号表示,可翻译成:数到add_cnt0 && cnt0==5208/2 -1时,如果cnt1==1,则将rx_uart_ff1赋给led[0]。如果cnt1==2,则将rx_uart_ff1赋给led[1],以此类推,如果cnt1==8,将rx_uart_ff1赋给led[7]。 那么直接翻译成代码。
上面代码可优化,简写成如下:
通常我们设计时,首先是想到实现功能,所以会先写出前面代码。在功能实现的前提下,再考虑有没有优化空间,从而写出后面代码。好代码都是一步步优化出来的。 注意,上面代码,我们采集的是rx_uart_ff1而不是rx_uart信号。这是因为rx_uart是异步信号,我们只能用同步化后的信号,否则会引起亚稳态。所以只能是rx_uart_ff1。 至此,主体程序已经完成。接下来是将module补充完整。
3.3 信号定义 cnt0是用always产生的信号,因此类型为reg。cnt0计数的最大值为5208,需要用13根线表示,即位宽是13位。因此代码如下:
add_cnt0和end_cnt0都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1个线表示即可。因此代码如下:
cnt1是用always产生的信号,因此类型为reg。cnt1计数的最大值为9,需要用4根线表示,即位宽是4位。因此代码如下:
add_cnt1和end_cnt1都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1根线表示即可。因此代码如下:
flag_add是用always方式设计的,因此类型为reg。并且其值是0或者1,1根线表示即可。因此代码如下:
rx_uart_ff0、rx_uart_ff1和rx_uart_ff2是用always方式设计的,因此类型为reg。并且其值是0或1,需要1根线表示即可。因此代码如下:
4 综合工程和上板4.1 新建工程 1.首先在d盘中创建名为“uart”的工程文件夹,将写的代码命名为“uart.v”,顶层模块名为“uart”。
|