系列文章目录
FPGA Verilog 串口发送
使用modelsim进行Verilog仿真(包含testbench编写)
前言
前面的文章实现了简单的串口发送,本来设计是继续进行串口接收的学习的,但是FPGA串口接收的学习不方便观察现象,所以采用串口收发的形式,将上位机发送的byte再返回回去。
PS:在写代码的过程中明显感觉到在逻辑电路中我失去了逻辑,调试过程十分痛苦,整个实现历时估摸着得有个两三天吧。最后测试每330个字符有一个误码。
串口条件:
- 波特率:115200;
- 无校验位;
- 数据位8位;
- 停止位;
一、串口收发
串口收发的时序倒比较简单,采用状态机的方式实现。
1.串口发送
这个在前面已经有做过了,上面目录里面有。发送总共有4个状态,分别为空闲状态(记作S_IDLE)、起始状态(S_START)、发送状态(S_SEND_BYTE) 以及结束状态(S_STOP)。发送的状态机代码如下。我们初学时主要关注状态机之间的状态转换:
首先,在默认状态下为S_IDLE,当有信号要发送时,即tx_data_valid为高电平时,进入S_STRAT状态。注意,在S_IDLE状态时,对时序没有要求,但是从S_START时,就必须对时钟严格要求了,本文从进入S_START后进行计数(记作cnt_bit),每计434个数作为一个串口传输bit。同时进入S_START时,输出信号线需要有一个下降沿,并在此状态下保持低电平。当cnt_bit计满434后,进入到S_SEND_BYTE状态,从此刻开始每434个cnt_bit的时间发送一个bit,发8个bit后,进入到S_STOP,计434个cnt_bit后转入S_IDLE,此时意味着结束。大概可以意思如下图所示。
module uart_tx
#(
parameter CLK_FRE = 50, //clock frequency(Mhz)
parameter BAUD_RATE = 115200 //serial baud rate
)
(
input clk, //clock input
input rst_n, //asynchronous reset input, low active
input[7:0] tx_data, //data to send
input tx_data_valid, //data to be sent is valid
output reg tx_data_ready, //send ready
output tx_pin //serial data output
);
//calculates the clock cycle for baud rate
localparam CYCLE = CLK_FRE * 1000000 / BAUD_RATE;
//state machine code
localparam S_IDLE = 0;
localparam S_START = 1;//start bit
localparam S_SEND_BYTE = 2;//data bits
localparam S_STOP = 3;//stop bit
//reg [7:0] tx_data;
reg[2:0] state;
reg[15:0] cycle_cnt; //baud counter
reg[2:0] bit_cnt;//bit counter
reg[7:0] tx_data_latch; //latch data to send
reg tx_reg; //serial data output
assign tx_pin = tx_reg;
initial
begin
tx_reg<=1;
// tx_data<=8'ha5;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
state <= S_IDLE;
else
case(state)
S_IDLE:
if(tx_data_valid == 1'b1)//触发状态
begin
tx_data_latch<=tx_data;
tx_data_ready<=0;
state <= S_START;
end
else
state <= S_IDLE;
S_START:
if(cycle_cnt == CYCLE - 1)
state <= S_SEND_BYTE;
else
state <= S_START;
S_SEND_BYTE:
if(cycle_cnt == CYCLE - 1 && bit_cnt == 3'd7)
state <= S_STOP;
else
state <= S_SEND_BYTE;
S_STOP:
if(cycle_cnt == CYCLE - 1)
begin
state <= S_IDLE;
tx_data_ready<=1;
end
else
state <= S_STOP;
default:
state <= S_IDLE;
endcase
end
/****************cnt计数****************/
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
cycle_cnt<=0;
else
if(state==S_IDLE)
cycle_cnt<=0;
else
if(state>=S_START && cycle_cnt<CYCLE-1)
cycle_cnt<=cycle_cnt+1;
else
cycle_cnt<=0;
end
/******************bit计数****************/
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
bit_cnt<=0;
else
if(bit_cnt==3'd7 && cycle_cnt==CYCLE-1)
bit_cnt<=0;
else
if(state==S_SEND_BYTE && cycle_cnt==CYCLE-1)
bit_cnt<=bit_cnt+1;
end
always@(posedge clk or negedge rst_n)
begin
case(state)
S_IDLE:
tx_reg<=1;
S_START:
tx_reg<=0;
S_SEND_BYTE:
tx_reg<=tx_data_latch[bit_cnt];
S_STOP:
tx_reg<=1;
default:
tx_reg<=1;
endcase
end
endmodule
2.串口接收
与串口发送类似,不过发送是主动的,接收是被动的,最主要的是检测接收线的起始信号,即下降沿。下降沿的判断很有意思,如下代码块所示。
always @(posedge clk) begin
rx_rxd_r <= rx_rxd;
rx_rxd_rr <= rx_rxd_r;
end
//falling, 检测rx_rxd下降沿
assign falling = ~rx_rxd_r & rx_rxd_rr;
检测实际上是利用同一个线上面在相邻两个时钟周期的电平不同来实现的。
检测到下降沿之后,进入到S_START状态,然后后面的状态就和发送一样了,只不过发送变成了接收。
module uart_rx
#(
parameter CLK_FRE = 50 ,//时钟频率(Mhz)
parameter BAUD_RATE = 115200 //波特率(bps)
)
(
input clk ,
input rst_n ,
input rx_rxd ,//接收端口
input rx_data_valid,
output reg [7:0] rx_data ,//接收1字节数据
output reg rx_data_ready
);
localparam CYCLE = CLK_FRE * 1000000 / BAUD_RATE;
reg rx_rxd_r ;
reg rx_rxd_rr ;
wire falling ;
reg [20:0] cycle_cnt ;
//state, 状态机
localparam S_IDLE = 0;
localparam S_START = 1;
localparam S_Receive = 2;
localparam S_STOP = 3;
reg [2:0] state;
reg [3:0] bit_cnt;
//rx_rxd_r, rx_rxd_rr, 打节拍
always @(posedge clk) begin
rx_rxd_r <= rx_rxd;
rx_rxd_rr <= rx_rxd_r;
end
//falling, 检测rx_rxd下降沿
assign falling = ~rx_rxd_r & rx_rxd_rr;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
state <= S_IDLE;
rx_data_ready <= 1'b0;
end
else
begin
case(state)
S_IDLE:
begin
if(rx_data_valid==0)
rx_data_ready<=0;
if(falling)
begin
state <= S_START;
rx_data<=7'd0;
end
end
S_START:
if(cycle_cnt == CYCLE-1)
state <= S_Receive;
S_Receive:
if(cycle_cnt ==0)
rx_data[bit_cnt]<=rx_rxd;
else
if(cycle_cnt == CYCLE-1 && bit_cnt == 3'd7)
state<=S_STOP;
S_STOP:
if(cycle_cnt == CYCLE-1)
begin
state <= S_IDLE;
rx_data_ready<=1;
end
default:
begin
if(rx_data_valid==0)
rx_data_ready<=0;
state <= S_IDLE;
rx_data_ready<=0;
end
endcase
end
end
/*******cycle_cnt, 波特率计数器*******/
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
cycle_cnt <= 'd0;
else
if(state==S_IDLE)
cycle_cnt<=0;
else
if(state>=S_START && cycle_cnt <CYCLE-1)
cycle_cnt<=cycle_cnt+1;
else
cycle_cnt<=0;
end
/*******bit_cnt, 波特率计数器*******/
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
bit_cnt<=0;
else
if(state!=S_Receive)
bit_cnt<=0;
else
if(bit_cnt==7 && cycle_cnt==CYCLE-1)
bit_cnt<=0;
else
if(cycle_cnt==CYCLE-1)
bit_cnt<=bit_cnt+1;
end
endmodule
3.发送与接收的连接
发送与接收的连接很重要,因为是要实现收到一个就转发一个,所以怎么样只发送一次是需要考虑的。类似于STM32单片机接收中断中,在中断服务函数执行完成后清除中断标志位,退出中断,否则将一直卡在中断服务函数中或者重复进入。
在程序中引入几个信号来实现,先放一张图
图中,发射模块多了rx_data_ready和rx_data_valid,同样地,接收模块多了tx_data_ready和tx_data_valid两个信号线。
连接:
rx_data_ready<---->tx_data_valid
tx_data_ready<---->rx_data_valid
作用:首先当接收进入到S_STOP时,认为一个byte已经接收完成,此时接收模块拉高rx_data_ready,告诉发送模块我有一个byte已经收到,可以发送;随后发送模块将8bits的数据进行锁存,再拉低tx_data_ready告诉接收模块已经收到数据,最后接收模块把rx_data_ready拉低,防止发送模块重复触发工作。
二、仿真
1.使用quartusII仿真
在软件中将模块转化为RTL模型,再使用原理图进行仿真,原理图在上面已经给出。但是该软件只能仿真100us内的情况,所以只能对这段时间内的波形进行仿真,最后选择放弃,改用modelsim进行仿真。
2.使用modelsim进行仿真
testbench如下
`timescale 1 ns/ 1 ns
module uart_TR_tb;
localparam CLK_FRE = 50; //clock frequency(Mhz)
localparam BAUD_RATE = 115200; //serial baud rate
reg clk,rstn,rx_rxd;
wire [7:0]rx_data;
initial
begin
rstn=0;
rx_rxd=0;
#10 rstn=1;
clk=0;
forever #10 clk=~clk;
end
initial
begin
#17361 rx_rxd=~rx_rxd;
#17361 rx_rxd=~rx_rxd;
#17361 rx_rxd=~rx_rxd;
#17361 rx_rxd=~rx_rxd;
#17361 rx_rxd=~rx_rxd;
#17361 rx_rxd=~rx_rxd;
end
uart_tx
#(
.CLK_FRE (CLK_FRE ),//时钟频率(Mhz)
.BAUD_RATE (BAUD_RATE ) //波特率(bps)
)
uart_tx_inst
(
.clk(clk), //clock input
.rst_n(rstn), //asynchronous reset input, low active
.tx_data(rx_data[7:0]), //data to send
.tx_data_valid(tx_data_valid), // input
.tx_data_ready(rx_data_valid), //send ready output
.tx_pin(tx_pin) //serial data output
);
uart_rx
#(
.CLK_FRE (CLK_FRE ),//时钟频率(Mhz)
.BAUD_RATE (BAUD_RATE ) //波特率(bps)
)
uart_rx_inst
(
.clk (clk ),
.rst_n (rstn ),
.rx_data_valid(rx_data_valid),
.rx_rxd (rx_rxd ),//接收端口
.rx_data (rx_data[7:0] ),//接收1字节数据
.rx_data_ready (tx_data_valid) //1字节数据接收完成 output
);
endmodule
仿真结果如下图所示。
实际测试结果如下图
总结
提示:这里对文章进行总结:
实现了串口的收发,经过测试,连续发送330byte回传无误,但是第331就出问题了。可能还是对时序的控制不够精确吧,后面可以尝试使用SPI进行通信。