背景
最近刚开始接触FPGA,在大概看了基本教材学习了Verilog后就买了一块黑金的开发板,开始正儿八经的撸代码。然后就到了UART通信这一节,基本上例程都是顶层模块例化UART收发模块,作者觉得这样不能很好的理解UART的底层协议,UART发送具体是怎么发送,又怎么接收,也不想直接ctrl+ch和ctrl+v,然后作者在参考正点原子的代码后,通过状态机实现500ms发送一个字节的数据和UART接收,具体请看下文,还有请正点原子支付下广告费。
协议
如下图所示,UART通信需要三根线,一根发送TX,一根接收RX,还有一根地。总线(TX和RX)空闲时处于高电平,发送方发送数据时,先发送起始位(逻辑0),然后以地为在前、高位在后的顺序将一个字节的每一个比特位发送出去(这一字节可以是7位或者8/9位),数据发送完成后,再发送1位校验位和停止位(可以是1位或者2位,逻辑1)。接收方的RX是与发送方的TX连接在一起的,因此,接收方检测RX上是否存在下降沿,是,则准备开始接收,因为UART通信没有时钟,因此只能规定多少时间发送一个比特位来保证数据收发不会出错,这就是比特率(或者说是波特率),单位是比特每秒(bit/s),一般情况下 ,比特率使用9600和115200,。本次作者的UART通信是采用8位数据位,1位停止位,没有奇偶检验位,共10位。
代码编写
协议清楚后开始撸代码。
UART发送
作者的思路是用状态机进行数据发送,初始化、发送、结束,发送周期是500ms,波特率是9600,系统频率是50MHz,所以需要对时钟脉冲计数50_000_000/9600=5208.33个,取整5208。下面是代码。
`timescale 1ns/1ps
module uart_tx(
input clk,
input rst_n,
output reg tx_pin);
parameter CLK_FREQ =50000000,
BPS =9600,
BPS_CNT =CLK_FREQ/BPS;
parameter IDLE =4'd0,
SEND =4'd1,
END =4'd2;
reg[7:0] data_buf;
reg[24:0] cnt1;//500ms 计数寄存器
reg tx_start;//计时到了置1,并开始发送
//计时500ms
always@(posedge clk,negedge rst_n)
if(!rst_n)
begin
cnt1<=0;
tx_start<=0;
end
else if(cnt1==25000000)
begin
cnt1<=0;
tx_start<=~tx_start;
end
else
cnt1<=cnt1+1;
reg[3:0] tx_cnt;
reg[24:0] clk_cnt;
reg[3:0] state;
always@(posedge clk,negedge rst_n)
if(!rst_n)
begin
state<=IDLE;
data_buf<=8'h5A;//发送的数据
end
else
case(state)
IDLE: begin
tx_cnt<=0;
clk_cnt<=0;
if(tx_start==1)//500ms发送一次数据
state<=SEND;
end
SEND:begin
if(tx_cnt==4'd9&&clk_cnt==BPS_CNT/2)
begin
tx_cnt<=10;
//data_buf<=0;
state<=END;
end
else if(clk_cnt==BPS_CNT-1)
begin
clk_cnt<=0;
tx_cnt<=tx_cnt+1;
end
else
begin
clk_cnt<=clk_cnt+1;
tx_cnt<=tx_cnt;
end
end
END:begin
if(tx_start==1)
begin
//tx_pin<=1'b1;
state<=END;
end
else
state<=IDLE;
end
default:state<=IDLE;
endcase
always@(posedge clk,negedge rst_n)
if(!rst_n)
tx_pin<=1'b1;
else if(tx_start==1)
case(tx_cnt)
4'd0:tx_pin<=1'b0;
4'd1:tx_pin<=data_buf[0];
4'd2:tx_pin<=data_buf[1];
4'd3:tx_pin<=data_buf[2];
4'd4:tx_pin<=data_buf[3];
4'd5:tx_pin<=data_buf[4];
4'd6:tx_pin<=data_buf[5];
4'd7:tx_pin<=data_buf[6];
4'd8:tx_pin<=data_buf[7];
4'd9:tx_pin<=1'b1;
default:tx_pin<=1'b1;
endcase
endmodule
UART接收
当前作者的思路是检测RX上的降沿,检测到则将将接收标志位置1,因为总线上有数据那么就存在高低电平,然后根据波特率对时钟计数,最后缓存。
module uart_rx(
input clk, //系统时钟
input rst_n, //系统复位,低电平有效
input uart_rxd, //UART接收端口
output reg uart_done, //接收一帧数据完成标志信号
output reg [7:0] uart_data //接收的数据
);
//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter BPS = 9600; //串口波特率
parameter BPS_CNT = CLK_FREQ/BPS; //为得到指定波特率,
//需要对系统时钟计数BPS_CNT次
//reg define
reg [1:0] rxcheck;
reg [15:0] clk_cnt; //系统时钟计数器
reg [ 3:0] rx_cnt; //接收数据计数器
reg rx_flag; //接收过程标志信号
reg [ 7:0] rxdata; //接收数据寄存器
//wire define
wire start_flag;
//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign start_flag = rxcheck[1] & (~rxcheck[0] );
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rxcheck<=0;
end
else begin
rxcheck[0] <= uart_rxd;
rxcheck[1] <= rxcheck[0] ;
end
end
//当脉冲信号start_flag到达时,进入接收过程
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rx_flag <= 1'b0;
else begin
if(start_flag) //检测到起始位
rx_flag <= 1'b1; //进入接收过程,标志位rx_flag拉高
else if((rx_cnt == 4'd9)&&(clk_cnt == BPS_CNT/2))
rx_flag <= 1'b0; //计数到停止位中间时,停止接收过程
else
rx_flag <= rx_flag;
end
end
//进入接收过程后,启动系统时钟计数器与接收数据计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
else if ( rx_flag ) begin //处于接收过程
if (clk_cnt < BPS_CNT - 1) begin
clk_cnt <= clk_cnt + 1'b1;
rx_cnt <= rx_cnt;
end
else begin
clk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零
rx_cnt <= rx_cnt + 1'b1; //此时接收数据计数器加1
end
end
else begin //接收过程结束,计数器清零
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
end
//根据接收数据计数器来寄存uart接收端口数据
always @(posedge clk or negedge rst_n) begin
if ( !rst_n)
rxdata <= 8'd0;
else if(rx_flag) //系统处于接收过程
if (clk_cnt == BPS_CNT/2) begin //判断系统时钟计数器计数到数据位中间
case ( rx_cnt )
4'd1 : rxdata[0] <= uart_rxd_d1; //寄存数据位最低位
4'd2 : rxdata[1] <= uart_rxd_d1;
4'd3 : rxdata[2] <= uart_rxd_d1;
4'd4 : rxdata[3] <= uart_rxd_d1;
4'd5 : rxdata[4] <= uart_rxd_d1;
4'd6 : rxdata[5] <= uart_rxd_d1;
4'd7 : rxdata[6] <= uart_rxd_d1;
4'd8 : rxdata[7] <= uart_rxd_d1; //寄存数据位最高位
default:;
endcase
end
else
rxdata <= rxdata;
else
rxdata <= 8'd0;
end
//数据接收完毕后给出标志信号并寄存输出接收到的数据
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
else if(rx_cnt == 4'd9) begin //接收数据计数器计数到停止位时
uart_data <= rxdata; //寄存输出接收到的数据
uart_done <= 1'b1; //并将接收完成标志位拉高
end
else begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
end
endmodule