一.RX控制器(接收数据)的Verilog实现
对串口来讲,TX和RX方向数据传输的格式都是一样的。毕竟对于参与传输的双方来讲,一方是TX,对应的一方就是RX。
对于TX方向,串口控制器要根据波特率产生TX方向的时钟(TX_CLK),其时钟的相位是没有要求的,由设计本身决定。而在RX方向,时钟(RX_CLK)的生成则有所不同。首先时钟频率也是根据波特率产生,这对于TX和RX都是一样的,但RX_CLK的相位则需要与TX_CLK相近,最佳的实现是RX_CLK与TX_CLK同步。但对于RX(接收数据的一方)来讲,是没有办法预测TX_CLK的相位的,没有办法做到RX_CLK与TX_CLK的绝对同步,不过近似的同步则可以实现。
虽然RX_CLK和TX_CLK原则上都是由波特率决定的,但是由于很多系统在由波特率计算这两个时钟的时候,误差会比较大,当系统长时间的工作的时候会带来较大的误差。所以最好的办法是在每次开始传输的时候,进行一次TX_CLK与RX_CLK的近似同步。
下面给出一种RX_CLK与TX_CLK近似同步的方法以及RX控制器的verilog实现。
先给出一个较高频率的基准时钟(RX_BCLK),这个时钟的频率越高,RX_CLK与TX_CLK就越接近于同步,这里选择RX_BCLK的频率为48MHz。串口的波特率假设为115200。
RX控制器的设计以RX_BCLK为基准,控制器逻辑都在RX_BCLK的时钟域内,而RXD属于TX_CLK时钟域,与RX_BCLK是异步的,要先用CDC电路,将RXD转换到RX_BCLK时钟域里,下面是verilog的具体实现:
reg RXD_T, RXD_2T;
always @(posedge RX_BCLK or negedge RESET)
begin
if(~RESET)
{RXD_T, RXD_2T} <= 2'b11;
else
{RXD_T, RXD_2T} <= {RXD, RXD_T};
end
RXD_2T可以认为是RX_BCLK时钟域的信号了,它相对于RXD的最大延迟为两个RX_BCLK的周期。下面是仿真的波形。
同TX方向状态机的实现一样,RX方向也用一个4状态的状态机,其定义与TX方向状态机一一对应,分别为UARTRX_IDLE,UARTRX_START,UARTRX_DATA和UARTRX_STOP。
当没有数据传输时,系统处于UARTRX_IDLE空闲状态,当RXD从高电平变为低电平时,状态机进入UARTRX_START状态,表明一次传输的开始。然后状态机直接进入UARTRX_DATA状态,在这个状态要保持8个RX_CLK的时间,用于接收8bit的数据。当数据接收完毕后,状态机进入UARTRX_STOP状态。当状态机在UARTRX_STOP状态时,RXD_2T从高电平转为低电平的话,意味着下一次传输已经开始了,所以状态机直接进入UARTRX_START状态,否则状态机回到UARTRX_IDLE状态。下面是状态机的实现:
parameter UARTRX_IDLE = 4'b0001,
UARTRX_START = 4'b0010,
UARTRX_DATA = 4'b0100,
UARTRX_STOP = 4'b1000;
reg [3:0] UARTRXSM, UARTRXSMNXT;
wire PHASE_UARTRX_IDLE = UARTRXSM[0];
wire PHASE_UARTRX_START = UARTRXSM[1];
wire PHASE_UARTRX_DATA = UARTRXSM[2];
wire PHASE_UARTRX_STOP = UARTRXSM[3];
wire PHASENXT_UARTRX_IDLE = UARTRXSMNXT[0];
wire PHASENXT_UARTRX_START = UARTRXSMNXT[1];
wire PHASENXT_UARTRX_STOP = UARTRXSMNXT[3];
reg [2:0] UARTRX_DATA_CNT;
always @(posedge RX_CLK or negedge RESET)
begin
if(~RESET)
UARTRX_DATA_CNT <= 3'b0;
else if(PHASE_UARTRX_DATA)
UARTRX_DATA_CNT <= UARTRX_DATA_CNT + 3'b1;
else
UARTRX_DATA_CNT <= 3'b0;
end
always @(posedge RX_CLK or negedge RESET)
begin
if(~RESET)
UARTRXSM <= UARTRX_IDLE;
else
UARTRXSM <= UARTRXSMNXT;
end
always @( UARTRXSM or RXD_2T or UARTRX_DATA_CNT) begin
UARTRXSMNXT = UARTRXSM;
case (UARTRXSM)
UARTRX_IDLE:
if(~RXD_2T)
UARTRXSMNXT = UARTRX_START;
else
UARTRXSMNXT = UARTRX_IDLE;
UARTRX_START:
UARTRXSMNXT = UART_DATA;
UARTRX_DATA:
if(UARTRX_DATA_CNT == 3'b111)
UARTRXSMNXT = UARTRX_STOP;
else
UARTRXSMNXT = UARTRX_DATA;
UARTRX_STOP:
if(~RXD_2T)
UARTRXSMNXT = UARTRX_START;
else
UARTRXSMNXT = UARTRX_IDLE;
default:
UARTRXSMNXT = UARTRX_IDLE;
endcase
end
RX_CLK由RX_BCLK经分频生成,分频数根据RX_BCLK的频率和波特率计算得出。比如RX_BCLK为48MHz,波特率为115200时,分频数为416。为了便于串口数据的获取,在生成RX_CLK的同时,生成一个与RX_CLK反相的RX_CLK_。根据之前的讨论,需要在每次传输开始的时候,进行RX_CLK与TX_CLK的近似同步。从上面的状态机可以看到,有两种情况可以认为是一个传输的开始,一个是状态机在UARTRX_IDLE状态遇到RXD_2T为0,另外一个是状态机在UARTRX_STOP状态遇到RXD_2T为0。根据这两种情况,来生成RX_CLK的同步使能信号RX_CLK_EN。
reg PHASE_UARTRX_STOP_Q;
always @(posedge RX_CLK or negedge RESET)
begin
if(~RESET)
PHASE_UARTRX_STOP_Q <= 1'b0;
else
PHASE_UARTRX_STOP_Q <= PHASE_UARTRX_STOP;
end
reg RX_CLK_EN_PRE, RX_CLK_EN_Q;
always @(posedge RX_BCLK or negedge RESET)
begin
if(~RESET)
RX_CLK_EN_PRE <= 1'b0;
else if( (PHASE_UARTRX_IDLE | (~PHASE_UARTRX_STOP & PHASE_UARTRX_STOP_Q) ) & ~RXD_2T)
RX_CLK_EN_PRE <= 1'b1;
else
RX_CLK_EN_PRE <= 1'b0;
End
always @(posedge RX_BCLK or negedge RESET)
begin
if(~RESET)
RX_CLK_EN_Q <= 1'b0;
else
RX_CLK_EN_Q <= RX_CLK_EN_PRE;
end
wire RX_CLK_EN = RX_CLK_EN_PRE & ~RX_CLK_EN_Q;
下面是第一种情况下RX_CLK_EN信号的仿真波形:
那么重点就是根据RX_CLK_EN来生成RX_CLK和RX_CLK_了。见下面的verilog代码。在RX_CLK_EN有效的时候,先让RX_CLK为0,然后延时50个RX_BCLK周期,再让RX_CLK变为0,是为了使RX_CLK在传输开始以后有一个上升沿,从而保证状态机可以正确跳转。
reg [13:0] RX_CLK_CNT;
always @(posedge RX_BCLK or negedge RESET)
begin
if(~RESET)
begin
{RX_CLK, RX_CLK_} <= 2'b01;
RX_CLK_CNT <= 14'b0;
end
else if(RX_CLK_EN)
begin
{RX_CLK, RX_CLK_} <= 2'b01;
RX_CLK_CNT <= 14'b0;
end
else if(PHASENXT_UARTRX_START & (RX_CLK_CNT == 14'd50) )
begin
{RX_CLK, RX_CLK_} <= 2'b10;
RX_CLK_CNT <= RX_CLK_CNT + 14'b1;
end
else if(RX_CLK_CNT == 14'd207)
begin
{RX_CLK, RX_CLK_} <= { RX_CLK_, RX_CLK};
RX_CLK_CNT <= RX_CLK_CNT + 14'b1;
end
else if( RX_CLK_CNT == 14'd416)
begin
{RX_CLK, RX_CLK_} <= { RX_CLK_, RX_CLK};
RX_CLK_CNT <= 14'd0;
end
else
begin
{RX_CLK, RX_CLK_} <= { RX_CLK, RX_CLK_};
RX_CLK_CNT <= RX_CLK_CNT + 14'b1;
end
end
从上面的设计可以看到,RX有效数据的跳变都是在RX_CLK的上升沿发生的,我们在RX_CLK_的上升沿去采集RX的数据,是最安全的。可以用下面的代码实现对8bit数据的采集和存储:
reg [7:0] UART_RX_DATA;
always @(posedge RX_CLK_ or negedge RESET)
begin
if(~RESET)
UART_RX_DATA <= 8'b0;
else if(PHASE_UARTRX_DATA)
UART_RX_DATA <= {RXD_2T, UART_RX_DATA[7:1]};
else if (PHASE_UARTRX_IDLE)
UART_RX_DATA <= 8'b0;
else
UART_RX_DATA <= UART_RX_DATA;
end
UART_RX_DATA即为从串口获取的一个字节的数据。可以采用异步FIFO把数据存储并转换到其他时钟域。具体的实现跟TX的异步FIFO相同,不再做详细的介绍。
下面是一次传输的仿真波形。
二.TX和RX方向的硬件验证
用loopback测试的模式,在硬件上验证串口的TX和RX控制器。将FPGA的串口接到PC机的串口上,用串口调试助手从PC机发数据给FPGA,FPGA的RX控制器接到数据后,再用TX控制器将数据发回给PC。
下面是波特率为115200时,连续发送1000次数据“11”的结果,数据没有错误。
下面是波特率为115200时,连续发送1000次数据“5a”的结果,数据没有错误。