目录
目录
前言
刚开始学习FPGA开发,项目中用到了串口RS232协议进行通信,记录一下设计思路和设计过程,开发板是野火的征途pro开发板,软件采用Quartus II 13.0。(参考资料:《FPGA实战开发指南》)
一、RS232协议
RS232协议是UART的一种,只有两根数据线,分别是rx和tx,分别用来接收数据和发送数据,数据收发基于帧结构,每次按照8bit的大小来接收和发送数据。数据线空闲状态下为高电平,开始发送数据后将电平拉低一帧作为起始位,随后8帧的数据为数据内容,发送完毕后将数据线电平拉高一帧作为停止位,然后一直拉高回到空闲状态。
二、模块框图
本次实例将编写串口接收模块以及串口发送模块,并用顶层模块实例化调用这两个模块,组成串口回环,来进行功能验证,接下来分模块介绍这几个模块的代码编写。
三、代码编写
本次实例串口波特率采用常见的9600 bit/s,开发板系统时钟为50MHz,所以每一帧数据所需的周期为1/9600 s,因此所需开发板计数到1/9600/(1/50_000_000) ≈ 5208,采用计数和清零的方式确定每一帧的开始和结束。
1.串口接收
采用寄存器放置读取的异步串口数据后将数据打两拍,减小亚稳态的可能性。随后在起始位的下降沿标记一个标志位start_flag产生一个周期的高电平,通过该信号控制读取数据的使能信号打开,波特率计数器baud_cnt在使能有效时进行计数,0-5207周期循环,当计数器计数到最大值5207的一半时拉高数据采集标志位bit_flag一个时钟周期,在该标志信号下降沿进行bit_cnt计数,在计数器1-8之间并且采集标志位bit_flag拉高时,将串口数据串联到rx_data的最高位,形成一个移位寄存器,移位八次后生成一个周期的高电平,将其打一拍后与数据同步输出到下一级模块。
代码如下:
module uart_rx
#(
parameter UART_BPS = 'd9600 ,//串口波特率
parameter CLK_FREQ = 'd50_000_000//时钟频率
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire rx ,//输入数据
output reg [7:0] out_data ,//输出数据
output reg out_flag //输出标志位
);
parameter BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
//中间变量声明
reg rx_reg1 ;//打一拍把异步串口信号同步读取
reg rx_reg2 ;
reg rx_reg3 ;//打两拍减小亚稳态影响
reg start_flag ;//开始标志位
reg work_en ;//读取数据的使能信号
reg [15:0] baud_cnt ;//波特率计数器
reg bit_flag ;//数据读取标志位
reg [3:0] bit_cnt ;//数据读取计数器
reg [7:0] rx_data ;//并行已读数据
reg rx_flag ;//并行已读数据标志位
//同步读取信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg1 <= 1'b1;
else
rx_reg1 <= rx;
//打两拍减小亚稳态影响
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg2 <= 1'b1;
else
rx_reg2 <= rx_reg1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg3 <= 1'b1;
else
rx_reg3 <= rx_reg2;
//第三个寄存器下降沿开始,起始标志位一个clk的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
start_flag <= 1'b0;//增加使能信号判据防止数据变化误判
else if((rx_reg3 == 1'b1)&&(rx_reg2 == 1'b0)&&(work_en == 1'b0))
start_flag <= 1'b1;
else
start_flag <= 1'b0;
//提取数据使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
work_en <= 1'b0;
else if(start_flag == 1'b1)
work_en <= 1'b1;//增加标志位信号严格条件
else if((bit_cnt == 4'd8)&&(bit_flag == 1'b1))
work_en <= 1'b0;
else
work_en <= work_en;
//波特率计数器
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
baud_cnt <= 16'd0; //计数满或使能信号无的时候清零
else if((baud_cnt == BAUD_CNT_MAX - 1'b1)||(work_en == 1'b0))
baud_cnt <= 16'd0;
else
baud_cnt <= baud_cnt + 1'b1;
//数据读取标志位
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_flag <= 1'b0;
else if(baud_cnt == BAUD_CNT_MAX / 2 - 1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
//数据读取计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_cnt <= 4'd0; //计数满或使能信号无的时候清零
else if((bit_flag == 1'b1)&&(bit_cnt == 4'd8))
bit_cnt <= 4'd0;
else if(bit_flag == 1'b1)
bit_cnt <= bit_cnt + 1'b1;
//并行已读数据标志位
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_flag <= 1'b0;
else if((bit_cnt == 4'd8)&&(bit_flag == 1'b1))
rx_flag <= 1'b1;
else
rx_flag <= 1'b0;
//并行已读数据
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
rx_data <= 8'b0;
else if((bit_cnt >= 4'd1)&&(bit_cnt <= 4'd8)&&(bit_flag == 1'b1))
rx_data <= {rx_reg3,rx_data[7:1]};//在第八个数据到来时仍需要移位
//输出数据
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
out_data <= 8'b0;
else if(rx_flag == 1'b1)
out_data <= rx_data;
//输出标志位
always@(posedge sys_clk or negedge sys_rst_n)
if((sys_rst_n == 1'b0))
out_flag <= 1'b0;
else
out_flag <= rx_flag;//与输入标志位信号同步,实际刚好打一拍
endmodule
2.串口发送
同样的,将需要发送的数据放置在寄存器in_data中,in_flag为与数据同步的标志信号,使能信号和计数器作用与串口接收模块类似,此处不再赘述。使能有效后每当bit_cnt计数到1时bit_flag拉高一个周期,并在其下降沿对数据进行按位输出(一共10位宽,包含起始低电平和停止位高电平)并同时按位计数,完成10位宽输出后拉高电平。
代码如下:
module uart_tx
#(
parameter UART_BPS = 'd9600 ,//串口波特率
parameter CLK_FREQ = 'd50_000_000//时钟频率
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [7:0] in_data , //wire型数据,大小在wire后面
input wire in_flag ,
output reg tx //always中reg型数据输出
);
parameter BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
//中间参数定义
reg work_en ;
reg [16:0] baud_cnt ;
reg [3:0] bit_cnt ;
reg bit_flag ;
//使能信号,读到输入标志位后拉高,数据读取9次后拉低
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
work_en <= 1'b0;
else if(in_flag == 1'b1)
work_en <= 1'b1;
else if((bit_cnt == 4'd9)&&(bit_flag == 1'b1))
work_en <= 1'b0;
else
work_en <= work_en;
//波特率计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
baud_cnt <= 16'd0;
else if((baud_cnt == BAUD_CNT_MAX - 1'b1)||(work_en == 1'b0))
baud_cnt <= 16'd0;
else if(work_en == 1'b1)//时序逻辑不需要列出所有,不会产生多余latch
baud_cnt <= baud_cnt + 1'b1;
//数据提取标志位
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_flag <= 1'b0;
else if(baud_cnt == 16'd1) //此处因为每个计数周期“1”只出现一次
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
//提取数据计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_cnt <= 4'd0;
else if((bit_cnt == 4'd9)&&(bit_flag == 1'b1))
bit_cnt <= 4'd0;
else if((bit_flag == 1'b1)&&(work_en == 1'b1))
bit_cnt <= bit_cnt + 1'b1;
else
bit_cnt <= bit_cnt;
//输出数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx <= 1'b1; //空闲状态为高电平
else if(bit_flag == 1'b1)
begin
case(bit_cnt)
0: tx <= 1'b0; //起始位
1: tx <= in_data[0];
2: tx <= in_data[1];
3: tx <= in_data[2];
4: tx <= in_data[3];
5: tx <= in_data[4];
6: tx <= in_data[5];
7: tx <= in_data[6];
8: tx <= in_data[7];
9: tx <= 1'b1; //停止位
default: tx = 1'b1;
endcase
end
endmodule
3.顶层模块
实例化两个模块即可:
module RS232
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire rx ,
output wire tx
);
wire [7:0] rx_data ;
wire rx_flag ;
uart_rx
#(
.UART_BPS (9600 ),//串口波特率
.CLK_FREQ (50_000_000)//时钟频率
)
uart_rx_inst
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.rx (rx) ,//输入数据
.out_data (rx_data) ,//输出数据
.out_flag (rx_flag) //输出标志位
);
uart_tx
#(
.UART_BPS (9600 ),//串口波特率
.CLK_FREQ (50_000_000)//时钟频率
)
uart_tx_inst
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.in_data (rx_data) , //wire型数据,大小在wire后面
.in_flag (rx_flag) ,
.tx (tx) //always中reg型数据输出
);
endmodule
四、仿真验证
通过串口发送数据并返回可以看到功能无误,模块代码编写正确。
总结
本次实例采用波形图法,编写了常见的低速通信协议UART中的RS232协议并验证正确,这样的话一个简单的串口通信就完成了。