【读书笔记】详细解析基于FPGA的串口通信
1赞【主题】:详细解析基于FPGA的串口通信
【作者】:LinCoding
【时间】:2016.11.28
串口通信,用的实在太广泛了,原理也很简单,在串口通信中最重要的是波特率的概念,大家不懂的可以百度,然后就是数据的格式,1位起始位,8位数据位,1位校验位(可选),1位停止位。
先看结果(PC端将数据下发至FPGA,FPGA接收到数据后原封不动的将数据上发至PC端):
(源码出自CrazyBingo,尊重版权)
下面直接看程序。
1、串口接收模块
module uart_receiver ( input clk, //global clock input rst_n, //global reset input clk_16bps, input rxd, output reg [7:0] rxd_data, output reg rxd_flag );
第一部分是输入输出定义,有两点需要注意:
1、对于串口接收模块,原理上要使用16倍的波特率去采样,因此输入clk_16bps,而这个输入信号来自于——《详细解析基于FPGA的任意分频》这篇文章的divide_clken的输出。
2、对于一个系统而言是由众多模块组成的,模块与模块之间肯定是要沟通的,那么对于输入模块而言,就就少不了“输入完成标志位信号”,并且,这个标志位信要与输入的数据同步输出,如本例中的rxd_flag,还有之前按键消抖文章中的key_flag,SPI文章中的rxd_flag都是如此,因此,这也要成为一个固定的模式!
//----------------------------------- //synchronize the input signal reg rxd_sync; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) rxd_sync <= 1'b1; else rxd_sync <= rxd; end
第二部分,由于是接收模块,接收的信号是来自其他时钟域的,因此,这里需要打一拍将信号同步到自己的时钟域,当然,打两拍也是可以的!
//----------------------------------- //receive FSM: encode localparam R_IDLE = 4'd0; localparam R_START = 4'd1; localparam R_SAMPLE = 4'd2; localparam R_STOP = 4'd3; //----------------------------------- //receive FSM localparam SMP_TOP = 4'd15; localparam SMP_CENTER = 4'd7; reg [3:0] smp_cnt; reg [3:0] rxd_cnt; reg [3:0] rxd_state; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) begin smp_cnt <= 4'd0; rxd_cnt <= 4'd0; rxd_state <= R_IDLE; end else case ( rxd_state ) R_IDLE: begin smp_cnt <= 4'd0; rxd_cnt <= 4'd0; if ( ! rxd_sync ) rxd_state <= R_START; else rxd_state <= R_IDLE; end R_START: if ( clk_16bps ) begin smp_cnt <= smp_cnt + 1'b1; if ( smp_cnt == SMP_CENTER && rxd_sync ) begin rxd_cnt <= 4'd0; rxd_state <= R_IDLE; end else if ( smp_cnt == SMP_TOP ) begin rxd_cnt <= 4'd1; rxd_state <= R_SAMPLE; end else begin rxd_cnt <= rxd_cnt; rxd_state <= rxd_state; end end else begin smp_cnt <= smp_cnt; rxd_cnt <= rxd_cnt; rxd_state <= rxd_state; end R_SAMPLE: if ( clk_16bps ) begin smp_cnt <= smp_cnt + 1'b1; if ( smp_cnt == SMP_TOP ) begin if ( rxd_cnt < 4'd8 ) begin rxd_cnt <= rxd_cnt + 1'b1; rxd_state <= R_SAMPLE; end else begin rxd_cnt <= 4'd9; rxd_state <= R_STOP; end end else begin rxd_cnt <= rxd_cnt; rxd_state <= rxd_state; end end else begin smp_cnt <= smp_cnt; rxd_cnt <= rxd_cnt; rxd_state <= rxd_state; end R_STOP: if ( clk_16bps ) begin smp_cnt <= smp_cnt + 1'b1; if ( smp_cnt == SMP_TOP ) begin rxd_cnt <= 4'd0; rxd_state <= R_IDLE; end else begin rxd_cnt <= rxd_cnt; rxd_state <= rxd_state; end end else begin smp_cnt <= smp_cnt; rxd_cnt <= rxd_cnt; rxd_state <= rxd_state; end default: begin smp_cnt <= 4'd0; rxd_cnt <= 4'd0; rxd_state <= R_IDLE; end endcase end
第三部分就是长长的状态机了,真的是太长了,不过基本都是重复的内容,有以下几点需要注意:
1、对于串口接收模块,我们为了使接收到的数据最大程度的稳定,使用了16倍波特率去采样,也就是在每个数据位上,有16个采样点,这样的话,当然在最中间的采样点的数据原则上是最稳定的,事实也确实如此。因此需要一个smp_cnt信号去计数当前是第几个采样点,并且当采样到16个点后使得rxd_cnt加1以采样下一位数据。
2、使用rxd_cnt信号 来计数获得当前采样的第几位数据。需要注意的是本always块只涉及rxd_cnt的变迁,而在另一个always中会根据rxd_cnt的情况来取值各个位上的数据。
3、由于串口传输的特性,起始位变为低电平视为传输的开始,因此也就意味着,起始位其实是个传输开始的使能信号,所以要使用状态机的IDLE态来始终监测起始位是否变化。
reg [7:0] rxd_data_r; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) rxd_data_r <= 8'd0; else if ( rxd_state == R_SAMPLE && clk_16bps && smp_cnt == SMP_CENTER ) case ( rxd_cnt ) 4'd1 : begin rxd_data_r[0] <= rxd_sync; end 4'd2 : begin rxd_data_r[1] <= rxd_sync; end 4'd3 : begin rxd_data_r[2] <= rxd_sync; end 4'd4 : begin rxd_data_r[3] <= rxd_sync; end 4'd5 : begin rxd_data_r[4] <= rxd_sync; end 4'd6 : begin rxd_data_r[5] <= rxd_sync; end 4'd7 : begin rxd_data_r[6] <= rxd_sync; end 4'd8 : begin rxd_data_r[7] <= rxd_sync; end default : begin rxd_data_r <= 8'd0; end endcase else if ( rxd_state == R_STOP ) rxd_data_r <= rxd_data_r; else rxd_data_r <= rxd_data_r; end
第四部分就是取值了,根据rxd_cnt的值来取值不同位上的数据。
always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) begin rxd_data <= 8'd0; rxd_flag <= 1'b0; end else if ( rxd_state == R_STOP && clk_16bps && smp_cnt == SMP_TOP ) begin rxd_data <= rxd_data_r; rxd_flag <= 1'b1; end else begin rxd_data <= rxd_data; rxd_flag <= 1'b0; end end
第五部分就是同步输出rxd_data和rxd_flag信号了。而为了同步输出,同样又采用了一级D触发器来给rxd_data打了一拍,使其与rxd_flag同步输出。
2、串口发送模块
module uart_transfer ( input clk, //global clock input rst_n, //global reset input clk_16bps, input txd_en, input [7:0] txd_data, output reg txd, output reg txd_flag );
第一部分是输入输出定义, 既然是发送模块,当然需要发送使能信号了,其次,还需要发送完成标志位信号。当然了,既然有了发送使能信号,状态机是不可避免的了。clk_16bps与串口接收模块同理。
//----------------------------------- //receive FSM: encode localparam T_IDLE = 4'd0; localparam T_SEND = 4'd1; //----------------------------------- //receive FSM localparam SMP_TOP = 4'd15; localparam SMP_CENTER = 4'd7; reg [3:0] smp_cnt; reg [3:0] txd_cnt; reg [3:0] txd_state; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) begin smp_cnt <= 4'd0; txd_cnt <= 4'd0; txd_state <= T_IDLE; end else case ( txd_state ) T_IDLE: begin smp_cnt <= 4'd0; txd_cnt <= 4'd0; if ( txd_en ) txd_state <= T_SEND; else txd_state <= T_IDLE; end T_SEND: if ( clk_16bps ) begin smp_cnt <= smp_cnt + 1'b1; if ( smp_cnt == SMP_TOP ) begin if ( txd_cnt < 4'd9 ) begin txd_cnt <= txd_cnt + 1'b1; txd_state <= T_SEND; end else begin txd_cnt <= 4'd0; txd_state <= T_IDLE; end end else begin txd_cnt <= txd_cnt; txd_state <= txd_state; end end else begin smp_cnt <= smp_cnt; txd_cnt <= txd_cnt; txd_state <= txd_state; end default: begin smp_cnt <= 4'd0; txd_cnt <= 4'd0; txd_state <= T_IDLE; end endcase end
第二部分就是长长的发送状态机了,相比串口接收模块相对简单,而与之不同的是,由于是发送,我们只需在数据的第一个采样点将数据发送出去,在采样点到达16个时,换下一位数据进行发送就可以了。
always @ ( * ) begin if ( txd_state == T_SEND ) case ( txd_cnt ) 4'd0 : begin txd = 1'b0; end 4'd1 : begin txd = txd_data[0]; end 4'd2 : begin txd = txd_data[1]; end 4'd3 : begin txd = txd_data[2]; end 4'd4 : begin txd = txd_data[3]; end 4'd5 : begin txd = txd_data[4]; end 4'd6 : begin txd = txd_data[5]; end 4'd7 : begin txd = txd_data[6]; end 4'd8 : begin txd = txd_data[7]; end 4'd9 : begin txd = 1'b1; end default : begin txd = 1'b1; end endcase else txd = 1'b1; end
第三部分就是根据txd_cnt的计数来发送数据了,注意的是本模块为组合逻辑,为的就是使数据对齐!
图1
图2
图2是图1的放大版本,可见,在时钟边沿是对齐的。
如果改为时序逻辑,如下程序:
always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) txd <= 1'b1; else if ( txd_state == T_SEND ) case ( txd_cnt ) 4'd0 : begin txd = 1'b0; end 4'd1 : begin txd = txd_data[0]; end 4'd2 : begin txd = txd_data[1]; end 4'd3 : begin txd = txd_data[2]; end 4'd4 : begin txd = txd_data[3]; end 4'd5 : begin txd = txd_data[4]; end 4'd6 : begin txd = txd_data[5]; end 4'd7 : begin txd = txd_data[6]; end 4'd8 : begin txd = txd_data[7]; end 4'd9 : begin txd = 1'b1; end default : begin txd = 1'b1; end endcase else txd = 1'b1; end
则如下图所示:
txd比txd_cnt晚了一个clk,当然晚了一个clk是无所谓的,但是为了时序的完美,还是用组合逻辑吧!
always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) txd_flag <= 1'b0; else if ( txd_state == T_SEND && clk_16bps && smp_cnt == SMP_TOP && txd_cnt == 4'd9 ) txd_flag <= 1'b1; else txd_flag <= 1'b0; end
最后一个部分就是输出发送完成标志位信号。
这样一个基于FPGA的串口通信就完成了,在顶层模块中我们可以将接收到的数据直接交给发送模块,通过PC端的串口调试助手下发数据,然后FPGA会原封不动的将数据再发回来完成板级验证。
总结:
1、对于接收类的模块,接收完成后需要有接收完成标志位信号,并且要与所接收的数据同步输出。
2、对于发送类的模块,要有发送使能信号和发送完成标志位信号,并且使用状态机的IDLE态来监测发送使能信号的变化。