FPGA串口实现(带FIFO)
0赞看了CrazyBingo的书的前几章,开始写代码实践了。刚好自己手上有一个xilinx的开发板,上面有串口的资源。索性,就来实现这个串口的功能。实现串口的发送和接收。
串口的协议很简单,从网上找了个图说明下:
串口所用的协议时UART协议。UART协议是异步的通信协议。只用一根线完成数据的传输。协议的时序如上图所示:
在空闲状态,即没有数据传输时,线上电平保持为高电平。当开始有数据传输时,线上电平会拉低,表示开始传输数据。第一个数据位开始位,然后是数据位,数据位可以是8位,也可以使8位到4位,但是顺序是低位在前,高位在后。最后是一个高电平的停止位。这样就完成了一次数据传输。
以发送8位数据8’h28(二进制00101000)为例。
则线上的电平依次为 1->0->0->0->0->1->0->1->0->0->1。
由于是异步,没有时钟同步,所以通信的双方要约定好,传输每一位的时间,这个时间就是由波特率来决定的。
波特率是指传输每一位所用的时间,如果采用波特率256000.那么每一位传输的时间为:
1/256000 = 3.9025us。
通信是有发送和接收的,所以串口就有两根线,一根线发送数据,一根线接收数据。
以上就是串口的一些介绍,详细的介绍可以自行百度,可以了解到更多串口的细节。
以下实现一个全双工的串口接收和发送,采用FIFO对接受到的数据进行储存,然后接收外部发送信号,就将FIFO中的接收到的数据,通过串口发送。波特率为256000,数据为8位。
以上是结构图,画的有点挫。接收模块接收外部发送的串口数据,将数据储存到FIFO中,FIFO有外部使能信号(这里是按键按下),就将FIFO中的数据传给发送模块,发送模块在将数据发送出去。
因为是全双工模式,所以对于发送和接收各有一个波特率产生模块。用来定时数据每一位的时间。以满足串口协议的要求。
对于发送模块:
波特率程序:
module band_generate_tx #( parameter baud = 115200 //baud 9600-256000 ) ( input clk, input rst_n, input start, //start send data mode input finish, //send data mode finish output reg time_arr_tx //baud overflow ); localparam cnt_16 = 15; /*********************baud counter value calculate*********************/ reg [7:0] cnt_baud; //calculate baud counter value //counter value = 50_000_000 / baud /16 -1; initial begin case(baud) 9600 : cnt_baud = 328 -1; 14400 : cnt_baud = 217 -1; 19200 : cnt_baud = 163 -1; 28800 : cnt_baud = 109 -1; 38400 : cnt_baud = 81 -1; 56000 : cnt_baud = 56 -1; 57600 : cnt_baud = 54 -1; 115200: cnt_baud = 27 - 1; 128000: cnt_baud = 24 -1; 256000: cnt_baud = 12 -1; default :cnt_baud = 12 -1; endcase end /*********************baud counter value calculate*********************/ reg [3:0] count_16; reg start_flag; always@(posedge clk or negedge rst_n) begin if(!rst_n) start_flag <= 'd0; else begin if(start) start_flag <= 1; else if(finish) start_flag <= 0; else start_flag <= start_flag; end end always@(posedge clk or negedge rst_n) begin if(!rst_n) begin count_16 <= 0; end else begin if(start_flag) begin if(count_16 >= cnt_16) begin count_16 <= 0; end else begin count_16 <= count_16 + 1'b1; end end end end reg [4:0] count_baud; always@(posedge clk or negedge rst_n) begin if(!rst_n) begin count_baud <= 0; time_arr_tx <= 0; end else begin if(start_flag) begin if(count_16 >= cnt_16) begin if(count_baud>=cnt_baud) begin count_baud <= 'd0; end else count_baud <= count_baud + 1'b1; if(count_baud == cnt_baud - 1'b1) time_arr_tx <= 1; else time_arr_tx <= 0; end else begin time_arr_tx <= 0; end end else begin count_baud <= 0; time_arr_tx <= 0; end end end endmodule
该模块可以实现波特率从9600到256000.只需要例化模块的时候,改变波特率参数值即可。这里采用initial语句来计算产生不同的波特率需要的计数值。因为要产生串口协议波特率规定的定时时间,所以要有一个计数器,来产生这样的一个定时时间。而计数器计数是需要一个计数值的。这里采用initial来计算该计数值。
我们知道initial语句是不可综合的,但是在有些情况下,它又是可以综合的。在计算初始值的时候,是可以综合的。比如这里计算计数器的初始值。或者对寄存器的初始化。
分析一下上面这段程序。这段程序是实现波特率控制的,波特率产生不是什么时候都产生,是需要的时候才产生。在这里,就是要有发送数据的时候,才进行波特率产生。这里的start信号是外部给的串口发送数据信号,但是这样信号只会持续一个时钟周期,而后面的波特率产生是判断这个信号一直使能在进行波特率产生,那这里就有一个问题,怎么把这一个只持续一个时钟周期的信号给扩展为一直持续使能了。这个就是上面代码的功能,将一个只持续一个时钟周期的信号给扩展为一直持续使能。
当start信号有效的时候,start_flag为高,即使能,一个时钟周期后,start信号无效,但是start_flag保持使能。一旦发送数据完成后,finish信号有效到来,将start_flag无效,是关闭波特率产生。
那么问题来了,上述代码会对应怎样的一个数字电路,大家可以去想想。我想的结果是一个除法器加3个门。
波特率产生模块,主要是给发送模块提供一个波特率溢出信号,提示发送模块,该位数据发送完成,准备发下一位数据。
接下来是发送模块。采用状态机设计:
module uart_txd( //global signal input clk, //clk 50M input rst_n, //reset signal, active-low //input signal input start, //start uart send mode input time_arr, //baud overflow signal input [7:0] data_in, //8-bits data to be send //output signal output reg finish, //send mode finish output reg uart_txd //serial send data ); //state localparam idle_state = 4'd0; localparam start_state = 4'd1; localparam send_0_state = 4'd2; localparam send_1_state = 4'd3; localparam send_2_state = 4'd4; localparam send_3_state = 4'd5; localparam send_4_state = 4'd6; localparam send_5_state = 4'd7; localparam send_6_state = 4'd8; localparam send_7_state = 4'd9; localparam stop_state = 4'd10; reg [3:0] state; reg [3:0] state_next; always@(posedge clk or negedge rst_n) begin if(!rst_n) state <= idle_state; else state <= state_next; end always@(*) begin state_next = state; finish = 0; uart_txd = 0; case(state) idle_state: begin //idle state , uart_txd value is hign level. uart_txd = 1; if(start) state_next = start_state; end start_state : begin //send start bit. uart_txd value is 0 uart_txd = 0; if(time_arr) begin state_next = send_0_state; end end send_0_state : begin uart_txd = data_in[0]; if(time_arr) begin state_next = send_1_state; end end send_1_state : begin uart_txd = data_in[1]; if(time_arr) begin state_next = send_2_state; end end send_2_state : begin uart_txd = data_in[2]; if(time_arr) begin state_next = send_3_state; end end send_3_state : begin uart_txd = data_in[3]; if(time_arr) begin state_next = send_4_state; end end send_4_state : begin uart_txd = data_in[4]; if(time_arr) begin state_next = send_5_state; end end send_5_state : begin uart_txd = data_in[5]; if(time_arr) begin state_next = send_6_state; end end send_6_state : begin uart_txd = data_in[6]; if(time_arr) begin state_next = send_7_state; end end send_7_state : begin uart_txd = data_in[7]; if(time_arr) begin state_next = stop_state; end end stop_state : begin uart_txd = 1; if(time_arr) begin state_next = idle_state; finish = 1; end end default: state_next = idle_state; endcase end endmodule
代码也很简单,只要有发送使能信号,就启动状态机,发送数据,在每一个波特率溢出信号作用下,进行状态转移,以完成每一位数据的发送。这里,要注意,停止位的数据位一定要为1.这里为什么要为1,后面再解释。
接着,就用一个发送顶层模块将上述两个模块进行封装。
module uart_tx #( parameter baud = 256000 ) ( input clk, input rst_n, input start, input [7:0] tx_data, output uart_txd, output finish ); wire time_arr_tx; band_generate_tx #(.baud(baud)) band_generate_tx_1 ( .clk(clk), .rst_n(rst_n), .start(start), .finish(finish), .time_arr_tx(time_arr_tx) ); uart_txd uart_txd_1( .clk(clk), .rst_n(rst_n), .start(start), .time_arr(time_arr_tx), .data_in(tx_data), .finish(finish), .uart_txd(uart_txd) ); endmodule
封装的好处,是方面顶层的调用。
接着就是接受数据的模块:
首先是波特率产生:
module band_generate_rx #( parameter baud = 115200 //baud 9600-256000 ) ( input clk, input rst_n, input start, input finish, output reg time_arr_rx ); localparam cnt_16 = 15; /*********************baud counter value calculate*********************/ reg [7:0] cnt_baud; //calculate baud counter value //counter value = 50_000_000 / baud /16 -1; initial begin case(baud) 9600 : cnt_baud = 328 -1; 14400 : cnt_baud = 217 -1; 19200 : cnt_baud = 163 -1; 28800 : cnt_baud = 109 -1; 38400 : cnt_baud = 81 -1; 56000 : cnt_baud = 56 -1; 57600 : cnt_baud = 54 -1; 115200: cnt_baud = 27 - 1; 128000: cnt_baud = 24 -1; 256000: cnt_baud = 12 -1; default :cnt_baud = 12 -1; endcase end /*********************baud counter value calculate*********************/ reg [3:0] count_16; reg start_flag; always@(posedge clk or negedge rst_n) begin if(!rst_n) start_flag <= 'd0; else begin if(start) start_flag <= 1; else if(finish) start_flag <= 0; else start_flag <= start_flag; end end always@(posedge clk or negedge rst_n) begin if(!rst_n) begin count_16 <= 0; end else begin if(start_flag) begin if(count_16 >= cnt_16) begin count_16 <= 0; end else begin count_16 <= count_16 + 1'b1; end end end end reg [4:0] count_baud; always@(posedge clk or negedge rst_n) begin if(!rst_n) begin count_baud <= 0; time_arr_rx <= 0; end else begin if(start_flag) begin if(count_16 >= cnt_16) begin if(count_baud>=cnt_baud) begin count_baud <= 'd0; end else count_baud <= count_baud + 1'b1; if(count_baud == cnt_baud/2 ) time_arr_rx <= 1; else time_arr_rx <= 0; end else begin time_arr_rx <= 0; end end else begin count_baud <= 0; time_arr_rx <= 0; end end end endmodule
采用和发送模块的波特率产生模块一样的结构,只是波特率的溢出信号时间不一样。发送的波特率溢出是在波特率时间的末尾,而接受的波特率溢出是在波特率时间的中间。因为在中间采集的数据才是正确有效的。
这里要明白一个东西,因为数据是一位一位传输的,所以数据改变的时间也是要注意的,不能在数据改变的时候接收数据,这样接受的数据就有可能不正确。数据变化是在波特率时间的末尾,所以接收数据要在波特率时间的中间。
下面是接收模块:
module uart_rxd( //global signal input clk, //clk signal .50M input rst_n, //reset signal , active-low //input signal input time_arr, //baud overflow signal input rxd, //rxd input data output reg [7:0] rxd_data, //receive rxd 8-bits data output start, //start receive mode output reg finish //receive mode finish ); /********************judge rxd falling edge*********************/ reg rxd_r ; reg rxd_r_r ; wire rxd_falling ; always@( posedge clk ) begin if( !rst_n ) begin rxd_r <= 'b1 ; rxd_r_r <= 'b1 ; end else begin rxd_r <= rxd ; rxd_r_r <= rxd_r ; end end assign rxd_falling = rxd_r_r & ( !rxd_r ) ; /********************judge rxd falling edge*********************/ reg [3:0] i; reg start_flag; //receive 9-bits data. but the high 8-bits is real data. the last bit is start data reg [8:0] rxd_data_reg; always@(posedge clk or negedge rst_n) begin if(!rst_n) begin rxd_data_reg <= 'd0; i <= 'd0; end else begin if(start_flag) //receive data begin if(time_arr ==1 ) begin rxd_data_reg <= {rxd,rxd_data_reg[8:1]}; i <= i +4'd1; end end else begin rxd_data_reg <= 'd0; i <='d0; end end end always@(posedge clk or negedge rst_n) begin if(!rst_n) begin start_flag <= 'b0; rxd_data <= 'd0; finish <= 'b0; end else if(rxd_falling) start_flag <= 1'b1; else if(i >= 9 && start_flag == 1) begin start_flag <= 1'b0; rxd_data <= rxd_data_reg[8:1]; finish <= 1'b1; end else finish <= 1'b0; end //if rxd generate a falling edge,that indicate receive mode start assign start = rxd_falling; endmodule
这里没有采用状态机的方式设计,这里可以看出,采用状态机设计,代码便于理解和编写。
封装上述两个模块,成一个接收模块:
module uart_rx #( parameter baud = 256000 ) ( input clk, input rst_n, input uart_rxd, output [7:0] receive_data, output finish ); wire time_arr_rx; wire start; band_generate_rx #(.baud(baud)) band_generate_rx_1 ( .clk(clk), .rst_n(rst_n), .start(start), .finish(finish), .time_arr_rx(time_arr_rx) ); uart_rxd uart_rxd_1 ( .clk(clk), .rst_n(rst_n), .time_arr(time_arr_rx), .rxd(uart_rxd), .rxd_data(receive_data), .start(start), .finish(finish) ); endmodule
然后在用一个串口顶层模块对上面发送和接受模块封装。
module uart_top #( parameter baud_tx = 256000, parameter baud_rx = 256000 ) ( input clk, input rst_n, input uart_rxd, //input serial rxd data input tx_start, //input start send data module signal input [7:0] tx_data, //input 8-bits send data output tx_finish, //output send mode finish output rx_finish, //output receive mode finish output uart_txd, //output serial txd data output [7:0] receive_data //output receive 8-bits data ); //receive module uart_rx #(.baud(baud_rx)) uart_rx_1 ( .clk(clk), .rst_n(rst_n), .uart_rxd(uart_rxd), .receive_data(receive_data), .finish(rx_finish) ); //send module //when tx_start is 1, send module will send 8-bit input tx_data to the serial txd uart_tx #(.baud(baud_tx)) uart_tx_1 ( .clk(clk), .rst_n(rst_n), .start(tx_start), .tx_data(tx_data), .uart_txd(uart_txd), .finish(tx_finish) ); endmodule
这样,就完成了整个的串口设计模块了。
剩下的就是FIFO模块了,这里是调用xilinx的FIFO IP核。直接拿来使用,位宽8位,深度4096.意思可以存储4096个外部发送的字节数据。
module test_uart_fifo( input clk, input rst_n, input key, input rx_finish, input [7:0] rx_data, input tx_finish, //input start, output reg tx_start, output [7:0] tx_data ); //wire start; //wire full; wire empty; reg rd_en; key_button key_button_1 ( .clk(clk), .rst_n(rst_n), .key(key), .key_down(start) ); fifo_uart fifo_uart_1 ( .clk(clk), // input clk .rst(rst_n), // input rst .din(rx_data), // input [7 : 0] din .wr_en(rx_finish), // input wr_en .rd_en(rd_en), // input rd_en .dout(tx_data), // output [7 : 0] dout .full(), // output full .empty(empty) // output empty ); localparam idle_state = 0; localparam send_state = 1; reg start_flag; reg state; always@(posedge clk or negedge rst_n) begin if(!rst_n) begin state <= idle_state; tx_start <= 0; rd_en <= 0; end else begin case(state) idle_state: begin if(start_flag) begin tx_start <= 1; state <= send_state; rd_en <= 1; end else begin tx_start <= 0; state <= idle_state; rd_en <= 0; end end send_state: begin tx_start <= 0; rd_en <= 0; if(tx_finish) state <= idle_state; end endcase end end always@(posedge clk or negedge rst_n) begin if(!rst_n) start_flag <= 0; else begin if(start) start_flag <= 1; else if(empty) start_flag <= 0; end end endmodule
其中key_button模块,是检测按键下降沿的。我这里是设定我按下按键,就将FIFO的所有数据发送出去。而fifo_uart模块,是例化FIFO的IP。
最终的顶层代码:
module test_uart( input clk, input rst_n, input uart_rxd, input key, //input start, output uart_txd, output [7:0] LED ); wire tx_start; wire rx_finish; wire tx_finish; wire [7:0] tx_data; wire [7:0] receive_data; uart_top #(.baud_tx(256000), .baud_rx(256000)) uart_top_1 ( .clk(clk), .rst_n(rst_n), .uart_rxd(uart_rxd), .tx_start(tx_start), .tx_data(tx_data), .tx_finish(tx_finish), .rx_finish(rx_finish), .uart_txd(uart_txd), .receive_data(receive_data) ); test_uart_fifo test_uart_fifo_1 ( .clk(clk), .rst_n(rst_n), .key(key), //.start(start), .rx_finish(rx_finish), .rx_data(receive_data), .tx_finish(tx_finish), .tx_start(tx_start), .tx_data(tx_data) ); assign LED = ~receive_data; endmodule
代码也很简单,就将两个模块连接起来。
综合,分配管脚,然后布局布线,最后下载。使用串口猎人,用来发送数据和接收数据。
串口测试成功了。
最终效果如下,很酷吧。。发送的数据是Crazybingo写的书的自序的一部分。
发送数据后,按下开发板按键,就将发送的数据发回。使用串口猎人捕获,即可得到数据。
在实现这个功能时候,有遇到一下问题:
发送单个数据,接收单个数据正确,但是发送一串数据,接收就只能接受到最后一个数据,而不是发送的一串数据。
这个问题,我可折腾了好久。最开始以为是FIFO没有正常工作,写testbench仿真,发现还真的是有这个问题。FIFO的复位信号弄反了。这个系统是设定的低电平复位,而FIFO设定的高电平复位,所以接收数据不对。将复位信号更正后,发现还是有问题。在仿真FIFO,发现FIFO是正常工作的。那出现的问题,肯定就是我的串口模块的问题。
各种写testbench代码仿真,仿真后,发现,接收模块是没有问题的,那问题就应该是发送模块的问题。通过仿真,发现发送的时序是对的,确实将每个数据按照规定的时序发送出去。但是发现发完一个数据后,只隔了一个系统时钟周期,就马上发送第二个数据。这里,就猜想,是不是数据发送太快,数据发送完,要在等一个波特率时间在发送下一个数据。百度下,发现,原来,发送的停止位的数据是要为1的。而我写程序的时候,以为停止位是0或1都没有关系,就给了个0.将这里改正,发现,程序对了。能实现发一串数据,然后FPGA在回发一串数据了。
所以要注意,发送的时候,停止位的数据一定要为高电平!!!!!!!!!!!!!!!!