基于状态机的串口收发模块的实现
前言
作为一个FPGA的初学者,实现一个完美的串口收发功能一直是心中的一个小小愿望。之前看过其他许多实现串口功能的Verilog代码,感觉它们都有或大或小的缺陷,例如有的代码看起来十分不严谨、结构混乱,有些代码可读性差,想要把它们读懂做一些修改十分困难。自己动手完成了一个较为满意的串口接收、发送模块,并在黑金的AX309试验板上进行了测试,接收到一帧数据后能够完整地将数据发回,数据帧长度达到256字节未出现任何错误。代码结构清晰,相信对串口协议较为了解的伙伴们会很容易读懂。
串口发送
进行串口调试,首先应实现的串口发送模块。为了提高程序可读性,使用状态机进行编程。闲话不多说,直接看源代码:
module uart_tx
#(
parameter CLK_FREQ = 50 ,//时钟频率(Mhz)
parameter BAUD_RATE = 9600 //波特率(bps)
)
(
input clk ,
input rst_n ,
output reg tx_txd ,//发送端口
input tx_trig ,//启动发送,高电平有效
input [7:0] tx_data ,//发送字节,tx_trig高电平输入
output reg tx_idle //空闲标志位,1:空闲态,0:忙碌态
);
localparam BAUD_NUM = (CLK_FREQ*1000_000) / BAUD_RATE - 1 ;//波特率对应计数值
reg [20:0] baud_cnt ;//波特率计数器
reg [ 7:0] tx_data_r ;//tx_data缓存
//状态机参数
localparam S_IDLE = 0;
localparam S_START = 1;
localparam S_BIT0 = 2;
localparam S_BIT1 = 3;
localparam S_BIT2 = 4;
localparam S_BIT3 = 5;
localparam S_BIT4 = 6;
localparam S_BIT5 = 7;
localparam S_BIT6 = 8;
localparam S_BIT7 = 9;
localparam S_STOP = 10;
reg [4:0] state;//状态机
//tx_data_r, 输入tx_data
always @(posedge clk) begin
if(tx_trig && tx_idle)
tx_data_r <= tx_data;
end
//state, 状态机
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
state <= S_IDLE;
tx_txd <= 1'b1;
tx_idle <= 1'b1;
end
else begin
case(state)
S_IDLE:
if(tx_trig) begin
state <= S_START;
tx_idle <= 1'b0;
end
S_START: begin
tx_txd <= 1'b0;
if(baud_cnt == BAUD_NUM)
state <= S_BIT0;
end
S_BIT0: begin
tx_txd <= tx_data_r[0];
if(baud_cnt == BAUD_NUM)
state <= S_BIT1;
end
S_BIT1: begin
tx_txd <= tx_data_r[1];
if(baud_cnt == BAUD_NUM)
state <= S_BIT2;
end
S_BIT2: begin
tx_txd <= tx_data_r[2];
if(baud_cnt == BAUD_NUM)
state <= S_BIT3;
end
S_BIT3: begin
tx_txd <= tx_data_r[3];
if(baud_cnt == BAUD_NUM)
state <= S_BIT4;
end
S_BIT4: begin
tx_txd <= tx_data_r[4];
if(baud_cnt == BAUD_NUM)
state <= S_BIT5;
end
S_BIT5: begin
tx_txd <= tx_data_r[5];
if(baud_cnt == BAUD_NUM)
state <= S_BIT6;
end
S_BIT6: begin
tx_txd <= tx_data_r[6];
if(baud_cnt == BAUD_NUM)
state <= S_BIT7;
end
S_BIT7: begin
tx_txd <= tx_data_r[7];
if(baud_cnt == BAUD_NUM)
state <= S_STOP;
end
S_STOP: begin
tx_txd <= 1'b1;
if(baud_cnt == BAUD_NUM) begin
state <= S_IDLE;
tx_idle <= 1'b1;
end
end
default:
state <= S_IDLE;
endcase
end
end
//baud_cnt, 波特率计数
always @(posedge clk or negedge rst_n) begin
if(~rst_n)
baud_cnt <= 'd0;
else if(baud_cnt == BAUD_NUM)
baud_cnt <= 'd0;
else if(state >= S_START)
baud_cnt <= baud_cnt + 'd1;
else
baud_cnt <= 'd0;
end
endmodule
先看看串口发送模块的对外接口:
-
参数 CLK_FREQ、BAUD_RATE 可以极为方便地实现输入 时钟频率 和 波特率 的修改;
-
clk为输入时钟,输入时钟频率应和 CLK_FREQ 参数一致;
-
rst_n为复位端口,低电平复位;
-
tx_txd为串口发送端口;
-
tx_trig为串口发送的触发端口,当该管脚为高电平且串口空闲,即 tx_idle == 1 时将触发一次串口发送;
-
tx_data为串口发送模块的数据输入,本程序仅支持8bit位宽数据的发送;
-
tx_idle为串口空闲状态标志,1:串口空闲态,0:串口忙碌态。
模块内首先根据输入的两个参数计算了指定波特率对应的计数值BAUD_NUM,注意由于输入参数CLK_FREQ以MHz为单位,因此计算时进行了 *1000_000 操作。模块内部实现代码注意以下几点:
-
tx_data_r在有效的发送触发信号到来时,将tx_data缓存,可避免串口发送过程中外部数据不稳定带来的问题;
-
状态机按照串口通信协议分为空闲态(S_IDLE)、起始位(S_START)、bit0(S_BIT0) ~ bit7(S_BIT7)、停止位(S_STOP),共11个状态。tx_txd在起始位状态输出0,各个数据位状态输出对应数据位的数值,停止位输出1,停止位之后进入空闲态,等待下一次发送触发操作到来;
-
状态机的状态跳转完全由状态机外部的baud_cnt计数器操控,这样状态机跳转耗费的时间不会对tx_txd的输出波形造成影响;
-
tx_idle在进入起始位时置’0’,停止位结束后置’1’;
串口发送模块的测试代码如下:
module tb_uart_tx;
reg clk ;
reg rst_n ;
wire tx_txd ;//发送端口
reg tx_trig ;//启动发送,高电平有效
reg [7:0] tx_data ;//发送字节,tx_trig高电平输入
wire tx_idle ;//空闲标志位,1:空闲态,0:忙碌态
uart_tx
#(
.CLK_FREQ (50 ),//时钟频率(Mhz)
.BAUD_RATE (460800 ) //波特率(bps)
)
uart_tx_inst
(
.clk (clk ),
.rst_n (rst_n ),
.tx_txd (tx_txd ),//发送端口
.tx_trig (tx_trig ),//启动发送,高电平有效
.tx_data (tx_data ),//发送字节,tx_trig高电平输入
.tx_idle (tx_idle ) //空闲标志位,1:空闲态,0:忙碌态
);
initial begin
clk = 0;
rst_n = 0;
tx_trig = 0;
tx_data = 8'h00;
#100;
rst_n = 1;
#100;
uart_tx_send(8'h55);
end
always #10 clk = ~clk;
task uart_tx_send(
input [7:0] data
);
begin
tx_trig = 1;
tx_data = data;
#20;
tx_trig = 0;
end
endtask
endmodule
串口接收
串口接收模块的源代码如下:
module uart_rx
#(
parameter CLK_FREQ = 50 ,//时钟频率(Mhz)
parameter BAUD_RATE = 9600 //波特率(bps)
)
(
input clk ,
input rst_n ,
input rx_rxd ,//接收端口
output reg [7:0] rx_data ,//接收1字节数据
output reg rx_flag //1字节数据接收完成
);
localparam BAUD_NUM = (CLK_FREQ*1000_000) / BAUD_RATE - 1 ;//波特率对应计数值
localparam HALF_BAUD_NUM = BAUD_NUM / 2 - 1;// 波特率对应计数值/2
reg rx_rxd_r ;
reg rx_rxd_rr ;
wire falling ;
reg [20:0] baud_cnt ;
//state, 状态机
localparam S_IDLE = 0;
localparam S_START = 1;
localparam S_BIT0 = 2;
localparam S_BIT1 = 3;
localparam S_BIT2 = 4;
localparam S_BIT3 = 5;
localparam S_BIT4 = 6;
localparam S_BIT5 = 7;
localparam S_BIT6 = 8;
localparam S_BIT7 = 9;
localparam S_STOP = 10;
reg [4:0] state;
//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 <= 8'h00;
rx_flag <= 1'b0;
end
else begin
case(state)
S_IDLE: begin
rx_flag <= 1'b0;
if(falling)
state <= S_START;
end
S_START:
if(baud_cnt == BAUD_NUM)
state <= S_BIT0;
S_BIT0:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[0] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT1;
S_BIT1:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[1] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT2;
S_BIT2:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[2] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT3;
S_BIT3:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[3] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT4;
S_BIT4:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[4] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT5;
S_BIT5:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[5] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT6;
S_BIT6:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[6] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_BIT7;
S_BIT7:
if(baud_cnt == HALF_BAUD_NUM)
rx_data[7] <= rx_rxd_r;
else if(baud_cnt == BAUD_NUM)
state <= S_STOP;
S_STOP:
if(baud_cnt == BAUD_NUM) begin
rx_flag <= 1'b1;
state <= S_IDLE;
end
default:
state <= S_IDLE;
endcase
end
end
//baud_cnt, 波特率计数器
always @(posedge clk or negedge rst_n) begin
if(~rst_n)
baud_cnt <= 'd0;
else if(baud_cnt == BAUD_NUM)
baud_cnt <= 'd0;
else if(state >= S_START)
baud_cnt <= baud_cnt + 'd1;
else
baud_cnt <= 'd0;
end
endmodule
串口接收模块与发送模块结构类似,先看对外接口:
-
rx_rxd为串口接收端口;
-
rx_data为接收到的一个字节数据,rx_flag为1时有效;
-
rx_flag为接收数据输出标志,rx_flag == 1时输出数据rx_data有效。
内部代码注意事项:
-
rx_rxd直接从外部物理接口输入,因此需要通过打节拍操作统一时钟;
-
串口空闲态rx_rxd为1,起始位rx_rxd为0,因此rx_rxd的下降沿可以作为串口接收操作开始的判断标志;
-
在状态机对应数据位中进行rx_data各个数据位的采集,但需要注意的是,在波特率计数器baud_cnt == HALF_BAUD_NUM,即每一位数据信号的正中央进行数据采集,因为此时的数据信号最稳定。
串口接收模块的测试代码如下:
`timescale 1ns/1ps
module tb_uart_rx;
reg clk ;
reg rst_n ;
reg rx_rxd ;//接收端口
wire [7:0] rx_data ;//接收1字节数据
wire rx_flag ;//1字节数据接收完成
//
localparam CLK_FREQ = 50 ;//时钟频率(Mhz)
localparam BAUD_RATE = 460800 ;//波特率(bps)
localparam BAUD_TIME = 1000_000_000 / BAUD_RATE;//波特率对应周期,单位(ns)
//实例化串口接收
uart_rx
#(
.CLK_FREQ (CLK_FREQ ),//时钟频率(Mhz)
.BAUD_RATE (BAUD_RATE ) //波特率(bps)
)
uart_rx_inst
(
.clk (clk ),
.rst_n (rst_n ),
.rx_rxd (rx_rxd ),//接收端口
.rx_data (rx_data ),//接收1字节数据
.rx_flag (rx_flag ) //1字节数据接收完成
);
//初始化
initial begin
clk = 0;
rst_n = 0;
rx_rxd = 1;
#100;
rst_n = 1;
rxd_1_byte(8'h55);
rxd_1_byte(8'hff);
rxd_1_byte(8'h00);
rxd_1_byte(8'h19);
rxd_1_byte(8'h38);
rxd_1_byte(8'he6);
end
//时钟
always #10 clk = ~clk;
//任务:接收1字节时序
task rxd_1_byte(
input [7:0] rxd_data
);
integer i;
begin
//起始位
rx_rxd = 0;
#BAUD_TIME;
//数据
for(i = 0; i < 8; i = i + 1) begin
rx_rxd = rxd_data[i];
#BAUD_TIME;
end
//停止位
rx_rxd = 1;
#BAUD_TIME;
end
endtask
endmodule
串口接收数据发回
检测串口是否工作正常最简单有效的方式就是让串口把接收到的数据发送回去。这里实现了top模块,内部调用了串口接收模块uart_tx和串口发送模块uart_tx。具体代码如下:
module top
(
input clk ,
input rst_n ,
input uart_rxd ,
output uart_txd
);
localparam CLK_FREQ = 50 ;//时钟频率(Mhz)
localparam BAUD_RATE = 460800 ;//波特率(bps)
localparam BAUD_NUM = (CLK_FREQ*1000_000) / BAUD_RATE - 1 ;//波特率对应计数值// 波特率对应计数值/2
reg tx_trig ;
wire[ 7:0] tx_data ;
wire tx_idle ;
wire[ 7:0] rx_data ;
wire rx_flag ;
reg [20:0] cnt ;
assign tx_data = rx_data;
//state, 状态机:rx_flag置1时, tx_trig置1一个比特位时长; 保证串口接收与发送的衔接
reg [2:0] state ;
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
state <= 3'd0;
cnt <= 'd0;
tx_trig <= 1'b0;
end
else
case(state)
3'd0:
if(rx_flag) begin
state <= 'd1;
tx_trig <= 1'b1;
end
3'd1: begin
cnt <= cnt + 1;
if(cnt == BAUD_NUM) begin
state <= 5'd0;
cnt <= 'd0;
tx_trig <= 1'b0;
end
end
endcase
end
//uart_tx 实例化
uart_tx
#(
.CLK_FREQ (CLK_FREQ ),//时钟频率(Mhz)
.BAUD_RATE (BAUD_RATE ) //波特率(bps)
)
uart_tx_inst
(
.clk (clk ),
.rst_n (rst_n ),
.tx_txd (uart_txd ),//发送端口
.tx_trig (tx_trig ),//启动发送,高电平有效
.tx_data (tx_data ),//发送字节,tx_trig高电平输入
.tx_idle (tx_idle ) //空闲标志位,1:空闲态,0:忙碌态
);
//uart_rx 实例化
uart_rx
#(
.CLK_FREQ (CLK_FREQ ),//时钟频率(Mhz)
.BAUD_RATE (BAUD_RATE ) //波特率(bps)
)
uart_rx_inst
(
.clk (clk ),
.rst_n (rst_n ),
.rx_rxd (uart_rxd ),//接收端口
.rx_data (rx_data ),//接收1字节数据
.rx_flag (rx_flag ) //1字节数据接收完成
);
endmodule
top模块的对外接口很简单,除了时钟复位外,仅包含串口接收和发送两个端口。由于串口接收模块uart_rx的数据输出rx_data可以保持较长时间不变(至少2个串口比特位的时长),因此可以将rx_data直接连接到tx_data。需要注意的是,不能将rx_flag简单的连接到tx_trig,作为串口发送模块的触发信号。这是因为,rx_flag仅仅有一个时钟宽度,当rx_flag=1而串口发送模块uart_tx尚未处于空闲状态(即state != S_IDLE)时,将无法触发串口发送。为了保证每次接收到一个字节数据就能触发一次串口发送,使用了一个简单的状态机,在rx_flag=1时,tx_trig的置1时长可以达到1个比特位的时长。
总结
FPGA编程应多多使用状态机,状态机编程会使程序清晰明了、通俗易懂。但是状态机的跳转可能会影响信号精度,尽量把时钟精度要求较高的操作放在状态机之外进行。其实除了状态跳转操作必须放在状态机之内,其他操作都可以想办法移到状态机之外。另外,灵活运用输入参数parameter,会大大减少修改程序的概率。
最后
以上就是糊涂方盒最近收集整理的关于Verilog——基于状态机的串口收发模块的实现基于状态机的串口收发模块的实现的全部内容,更多相关Verilog——基于状态机内容请搜索靠谱客的其他文章。
发表评论 取消回复