【精品博文】基于FPGA的串口通信时序设计
这篇博文主要总结一下串口通信的收发时序。上周基于IIC,SPI,RS232串口写了几个简单的小实验,感觉对于这种常用协议的写法大体上都差不多,主要是读懂时序图,然后根据时序图,用HDL语言描述出来就可以了。之前写过一篇IIC的博客,因为IIC是一种半双工的通信协议,收发共用一条数据线,串口是一种全双工的通信协议,和IIC存在比较大的区别,所以今天就把串口也总结一下。至于SPI,因为SPI比较灵活,有三线制SPI,也有四线制,不太好总结。所以就总结一下串口吧,而且串口也更常见一些。
1、串口时序
前面已经说过串口是一种全双工的通信协议,所谓全双工就是收和发可以同时进行,互不干扰。所以相应的串口通信的时序也就存在收时序和发时序,虽然两种时序基本一样,但还是要写两个模块的。
其实串口通信简化来看就两条线,一条接收一条发送。和IIC不一样的地方是,他没有时钟线。没有时钟线他是怎么做到数据的收发同步呢?这就涉及到一个很重要的概念——波特率。在下面介绍时序的时候我再一起说明。
一帧数据包括起始位、数据位(注意:低位在前)、停止位、一般情况下还有一个奇偶校验位。但是由于这个实验传递的数据比较简单,所以就省略了奇偶校验位(奇偶校验是一种校验代码传输正确性的方法。根据被传输的一组二进制代码的数位中“1”的个数是奇数或偶数来进行校验。采用奇数的称为奇校验,反之,称为偶校验。采用何种校验是事先规定好的。通常专门设置一个奇偶校验位,用它使这组代码中“1”的个数为奇数或偶数。若用奇校验,则当接收端收到这组代码时,校验“1”的个数是否为奇数,从而确定传输代码的正确性)。
下面再说一下波特率,以我们这个时序为例,一帧数据总共10位,假设一秒钟传送960帧数据,那么波特率就是9600 bd/s。收和发必须约定好以同样的波特率传输数据才能正常通信。
2、系统框图
我们利用开发板上的矩阵键盘、数码管和上位机的串口调试助手进行联合调试。调试发送模块时,我们通过矩阵键盘的按键获得键值,把数据存入FIFO,串口发送模块读取FIFO的数据,送给上位机的串口调试助手来显示。调试接收模块时,串口调试助手发送一个数据,接收模块接收到后存入FIFO,数码管再从FIFO中把数读出来显示。所以,整体上系统的框架就很明朗了。
上面两个就是收和发两个模块的RTL视图了。从图中我们基本上就能很清楚的看清数据的流向了。需要简单说一下就是FIFO的使用。
FIFO是一个先入先出(first in first out)的数据存储器。他的作用是解决多时钟域中的数据传递的问题。比方说,现在从串口调试助手一次传递了10字节的地址过来,如果让数码管字节接到串口模块上,那么这个数据变化很快,人眼根本看不清,如果我在中间加一个FIFO,让串口先将数据写入FIFO,数码管0.5s发出一个读使能信号,每0.5秒读出一个数据,那么我们就能把这10个数据都看清了。
读需要读使能信号,同样的写也需要写使能信号。只有当相应的使能信号拉高的时候,才能对FIFO进行读写操作。
3、代码解析
串口接收模块
下面来解释一下这段代码。时钟50M,低电平复位,rxd串口接收数据线,wr_req是FIFO的写使能信号,rdata是数据输出,给FIFO写数据。
0状态,检测rxd,如果有低电平,则说明是起始信号。重点来了,1状态计数7812是个什么意思呢?
现在假设上面是我们的快时钟50M,下面是9600的波特率(实际倍数关系比这个更大),我们在0状态检测到低电平是不是应该在箭头所指的地方。现在,离第一个数据位是不是几乎还有一整位数据的距离。按照50M/9600 = 5208.
也就是起码还要打5207拍才能接收到第一位数据,那为什么要计数7812呢?因为在边沿的地方采集信号是非常的不稳定和不安全的,所以,打5208*1.5 = 7812拍,在电平的中间采集信号。除了第一个数据位需要打1.5个波特率宽度之外,后面每一个数据由于是电平中间到电平中间,只有一个波特率的宽度,所以只用打5208拍就可以了。在接收完最后一个数据后将wr_req拉高,给FIFO写数据。另外注意一下移位的顺序,为什么是data_reg <= {rxd,data_reg[7:1]};而不是data_reg <= {data_reg[6:0],rxd};
。这样1状态和2状态就都解决了。到3状态就是接受停止信号。不用打拍延时了,因为空闲状态也是高电平,而起始信号是低电平,只要接受到低电平,我们就认为开始下一轮信号的接受。
接受模块解决了,那么发送模块也就好办了。
发送模块就不细说了,收发的时序是差不多的,唯一有点区别就是发送的时候是在边沿就开始发送,接受的时候是要在电平的中间去采集。这里主要就说一下0状态和1状态。因为这里是从FIFO里面去读数据来进行发送,这里rd_empty是由FIFO发出的,如果FIFO为空的话,那么rd_empty为1,相反如果FIFO不空,有数据,那么rd_empty为0。所以,当串口模块检测到FIFO有数据,就将rd_req拉高,把数据读出来,由于串口读数据的速度是比键盘写入数据的速度要快的,所以当FIFO一有数据,马上就会被读出来,所以0状态只有1个周期有效。我在一开始写的时候是并没有加flag这个变量和1状态的,但是出现的一个问题就是,按第二下的时候才能够将第一次按键的数据发送出去,而且用singaltap调试,问题就是出在data_reg这个地方。所以,我怀疑1拍并不能将fifo里面的数据读出来,所以我加了一个flag让data_reg多读一拍,结果才正常。
我本来打算讲一下singaltap的用法的,但是板子没带回来,以后再加上吧。因为有些情况,modelsim并不能找出问题所在,因为那些仿真时候的外部激励毕竟是我们认为给的,并不能代表真实的情况,所以singaltap有些时候比modelsim要好用。