前言
在之前的文章中【FPGA学习】实例一、Cyclone IV串口通信(RS232)我们已经能够采用波形图的方法,实现9600bps的Uart通信。近期笔者在整理了状态机和计数器组合的设计方法以后,对状态机的设计又有了新的感悟和体会,所以又把经典的RS232协议拉出来当状态机的例子练手了哈哈哈。数据有效位为8bit,功能上增加了奇校验,并将波特率设置为115200bps,并借助这篇文章梳理一下状态机和计数器组合设计的思路和设计要点,文章奉上:
一、数据帧结构
含校验位的RS232数据帧结构如上图所示,其中三个单元格为一个数据帧,数据有效为8bit 空闲状态下,rx信号为高电平,rx拉低一帧作为起始位。接下来按照数据位由低到高每一帧输出一位数据,发送到最高位Data[7]
以后,有效数据发送完毕。
接着PARITY
为数据校验位。本文采用奇校验,即判断是否数据位和校验位的1
的数量之和是否为奇数,若为奇数则满足奇校验,否则就认为数据在传输过程中出现了错误,从而将本次的8bit 数据丢弃。
奇校验帧结束后为停止位,将rx拉高一帧作为结束信号,等待下一组数据传入。
二、接收模块
接收模块是将接收到的串行数据转换为并行数据的模块,并同步输出一个数据有效使能信号。
首先因为系统晶振为50MHz,串口的波特率为115200bps,因此每一帧数据所需时钟周期数为N = (1/115200)/(1/50000000) = 434
,所以每一帧数据需要借助一个计数器判断当前的位数,该计数器计数到433
后清零,采用组合逻辑赋值baud_max
,当计数到433计满清零时拉高。
接下来结合时序图介绍笔者的状态机部分设计思路:
2.1 状态设置
接收模块状态机状态共包含IDLE、START、DATA、PARITY、STOP、WAIT
六个状态,前五个状态望文生义即可,而WAIT
状态是在校验位校验错误以及停止位没有读取到高电平时进入到的状态,意味着本次读取的8bit 数据是失效的,当在此状态下识别到停止位后直接进入IDLE
状态,不进行数据的输出。
2.1 状态跳转
状态机采用三段式的写法,比特率计数器baud_cnt
在next_state == DATA
或者next_state == PARITY
时进行循环计数(见时序图)。由于计数器慢一拍,所以进入状态DATA
以后,baud_cnt
从1开始计数,计数一帧即434
个周期后,计数值为0
,因此baud_cnt==0
作为每一个状态跳转的必要条件(也保证了未开始计数时IDLE
状态能在识别到起始位后直接进入到START
状态)。
START
状态满足一帧时间就跳转到DATA
状态,为了表征当前是数据的第几帧,引入帧计数器bit_cnt
,该计数器在next_state == DATA
或者next_state == PARITY
时进行循环计数(见时序图),识别到帧结束信号baud_max==1
以后,自加1。其他状态下,帧计数器清零。
2.2 奇校验
由于帧计数器也慢实际数据一拍,和状态变化同步,因此判断校验位时,校验位相对于实际数据在第9帧。帧计数器慢一拍,所以当bit_cnt==8 && baud_max==1
时对校验位进行判断。本文的奇数校验方式为判断parity == ~(^po_data)
是否成立,若成立,则校验位和数据位中1
的个数之和为奇数(见时序图)。其中po_data
为进行移位拼接后,准备并行输出的8位宽数据。
满足校验位则进入校验位状态PARITY
1帧,识别到停止位就进入到STOP
状态。
2.3 数据输出
接收模块的数据即为拼接结束以后的并行输出数据,并同步输出一个使能信号,在使能信号拉高时的数据信号才有效。因为是要把数据拼接为8位宽的数据,要保证移位8次,并且每次移位时数据正确,设置了移位标志信号shift
,shift
信号当波特率计数器计数到一帧的一半时state == DATA && baud_cnt == BAUD_CNT_MAX / 2
拉高信号(见代码数据移位输出部分),此时数据稳定,并且在校验位前就可将八位数据移位完成,同时满足奇校验要求。又由于STOP
状态只要发生错误就不会进入,保证了数据的正确性,所以选择当state==STOP
时输出数据有效使能信号,并输出拼接完成的并行数据po_data
。完整代码如下:
// *****************************************************************************************************************************
// ** 作者 : 彼方云城
// ** 邮箱 : zwr2059341302@163.com
// ** 博客 : https://blog.csdn.net/weixin_45451974?spm=1000.2115.3001.5343
// ** 日期 : 2022/10/10
// ** 功能 : 1、串口接收模块,接收串行数据,转换成并行数据输出
// 2、8bit数据(先接受低字节数据,再接收高字节数据),奇校验(不可在宏定义处删除奇校验)
// *****************************************************************************************************************************
module uart_rx_fsm
#(
parameter BAUD = 115200 , //串口波特率
CLK_FREQ = 50_000_000, //时钟周期
BAUD_CNT_MAX = CLK_FREQ / BAUD //每帧数据对应的时钟周期
)
(
//系统接口
input wire sys_clk ,
input wire sys_rst_n ,
//输入数据
input wire in_data ,
//输出数据
output reg [7:0] po_data ,
output po_flag
);
//in_data_reg同步数据,in_data_reg1和in_data_reg2打两拍减小亚稳态
//in_data_reg2为稳定数据
reg in_data_reg ;
reg in_data_reg1 ;
reg in_data_reg2 ;
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
in_data_reg <= 1'b1;
in_data_reg1 <= 1'b1;
in_data_reg2 <= 1'b1;
end
else begin
in_data_reg <= in_data;
in_data_reg1 <= in_data_reg;
in_data_reg2 <= in_data_reg1;
end
end
//**********************状态机********************//
parameter IDLE = 3'd0,START = 3'd1,DATA = 3'd2;
parameter PARITY = 3'd3,WAIT = 3'd4,STOP = 3'd5;
reg [2:0] state,next_state;
//状态机状态转换
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
//波特率计数器及计满脉冲信号
reg [9:0] baud_cnt;
wire baud_max;
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
baud_cnt <= 10'd0;
end
else if(baud_cnt == BAUD_CNT_MAX - 1'b1)begin
baud_cnt <= 10'd0;
end
else if(next_state==START || next_state==DATA
|| next_state==PARITY || next_state==STOP)begin
baud_cnt <= baud_cnt + 1'b1;
end
end
assign baud_max = (baud_cnt == 10'd0);
//起始位及结束位标志信号
wire start;
wire stop;
assign start = (in_data_reg2 == 1'b0) & baud_max;
assign stop = (in_data_reg2 == 1'b1) & baud_max;
//bit位计数器
reg [3:0] bit_cnt;
wire bit_done;//计数到8时拉高
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
bit_cnt <= 4'd0;
end
else if(next_state == DATA & bit_cnt == 4'd9 & baud_max)begin
bit_cnt <= 4'd0;
end
else if((next_state == DATA || next_state == PARITY)&& baud_max)begin
bit_cnt <= bit_cnt + 1'b1;
end
else if(next_state == DATA)begin
bit_cnt <= bit_cnt;
end
else begin
bit_cnt <= 4'd0;
end
end
assign bit_done = (bit_cnt == 4'd8);
//奇校验位
wire parity;
assign parity = baud_max && bit_done && in_data_reg2 == ~(^po_data);
//*******当奇校验位与数据奇数相反时数据正确
//状态机下一状态描述
always@(*)begin
case(state)
IDLE : next_state = start?START:IDLE;
START : next_state = baud_max?DATA:START;
DATA : next_state = (bit_done&baud_max)?(parity?PARITY:WAIT):DATA;
PARITY : next_state = baud_max?(stop?STOP:WAIT):PARITY;
WAIT : next_state = baud_max?(stop?STOP:WAIT):WAIT;
STOP : next_state = baud_max?(start?START:IDLE):STOP;
default : next_state = IDLE;
endcase
end
//数据移位标志信号
wire shift;
assign shift = (next_state == DATA && baud_cnt == BAUD_CNT_MAX/2);
//当数据在波特率计数器满值一半时提取
//状态机输出及数据有效使能信号
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
po_data <= 8'd0;
end
else if(shift)begin
po_data <= {in_data_reg2,po_data[7:1]};
end
else begin
po_data <= po_data;
end
end
//输出同步使能信号
assign po_flag = (state==STOP);
endmodule
三、发送模块
发送模块主要是将接受到的并行多位宽数据以串行形式RS232的协议发出,接下来结合时序图和状态转移图介绍发送模块状态机实现的设计思路:
3.1 状态跳转
发送模块的状态去除了WAIT
状态,同样采用一个波特率计数器对一帧对应的434个时钟周期进行计数,baud_cnt
以及baud_max
作为每帧的时钟计数信号和跳转信号,初始状态为IDLE
,接收到pi_flag
后跳转为START
状态,经过一帧跳转到DATA
状态,进入到DATA
状态后,用计数器bit_cnt
来计数数据帧数,计满8位数据后跳转到校验位状态PARITY
,校验位采用parity_data = (state==PARITY)?(~(^pi_data)):1'b0;
进行赋值,仅在校验位状态下将校验位数据赋值为原数据缩减异或运算的取反。一帧后进入STOP
状态,输出1帧高电平。随后判断是否有输入使能,有就跳转到START
状态,否则进入IDLE
状态,等待下一个有效数据使能信号。
3.2 数据输出
发送模块数据的输出采用组合逻辑输出的方式,因为延迟next_state
一拍的帧计数器bit_cnt
和当前状态state
同步,所以可以在DATA
状态下用计数器来读取并输出采集数据的由低到高的不同位,而在START
状态输出低电平,STOP
状态输出高电平,其他状态均为空闲状态的高电平。完整代码如下:
// *****************************************************************************************************************************
// ** 作者 : 彼方云城
// ** 邮箱 : zwr2059341302@163.com
// ** 博客 : https://blog.csdn.net/weixin_45451974?spm=1000.2115.3001.5343
// ** 日期 : 2022/10/13
// ** 功能 : 1、串口发送模块,接收并行数据,读取数据后串行输出
// 2、8bit数据(先接受低字节数据,再接收高字节数据),奇校验(不可在宏定义处删除奇校验)
// 3、波特率115200
// *****************************************************************************************************************************
module uart_tx_fsm
#(
parameter BAUD = 115200 , //串口波特率
CLK_FREQ = 50_000_000, //时钟周期
BAUD_CNT_MAX = CLK_FREQ / BAUD //每帧数据对应的时钟周期
)
(
//系统接口
input wire sys_clk ,
input wire sys_rst_n ,
//输入数据及同步信号
input wire [7:0] pi_data ,
input wire pi_flag ,
//输出串行数据
output reg out_data
);
//**********************状态机********************//
parameter IDLE = 3'd0,START = 3'd1,DATA = 3'd2;
parameter PARITY = 3'd3,STOP = 3'd4;
reg [2:0] state,next_state;
//状态机状态转换
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
//波特率计数器及计满脉冲信号
reg [9:0] baud_cnt;
wire baud_max;
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
baud_cnt <= 10'd0;
end
else if(baud_cnt == BAUD_CNT_MAX - 1'b1)begin
baud_cnt <= 10'd0;
end
else if(next_state==START || next_state==DATA
|| next_state==PARITY || next_state==STOP)begin
baud_cnt <= baud_cnt + 1'b1;
end
end
assign baud_max = (baud_cnt == 10'd0);
//起始位及结束位标志信号
wire start;//进入start状态
assign start = pi_flag;
//bit位计数器
reg [3:0] bit_cnt;
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
bit_cnt <= 4'd0;
end
else if((next_state == DATA || next_state == PARITY)&& baud_max)begin
bit_cnt <= bit_cnt + 1'b1;
end
else if(next_state == DATA || next_state == PARITY)begin
bit_cnt <= bit_cnt;
end
else begin
bit_cnt <= 4'd0;
end
end
//奇校验位数据
wire parity;
wire parity_data;
assign parity = (bit_cnt==4'd8) && baud_max;
assign parity_data = (state==PARITY)?(~(^pi_data)):1'b0;
//*******仅在PARITY状态下有效
//状态机下一状态描述
always@(*)begin
case(state)
IDLE : next_state = start?START:IDLE;
START : next_state = baud_max?DATA:START;
DATA : next_state = parity?PARITY:DATA;
PARITY : next_state = baud_max?STOP:PARITY;
STOP : next_state = baud_max?(start?START:IDLE):STOP;
default : next_state = IDLE;
endcase
end
//状态机输出
always@(*)begin
case(state)
START : out_data = 1'b0;
DATA : out_data = pi_data[bit_cnt - 1'b1];
PARITY : out_data = parity_data;
STOP : out_data = 1'b1;
default : out_data = 1'b1;
endcase
end
endmodule
四、顶层模块
实例化连接两个模块,代码如下:
// *****************************************************************************************************************************
// ** 作者 : 彼方云城
// ** 邮箱 : zwr2059341302@163.com
// ** 博客 : https://blog.csdn.net/weixin_45451974?spm=1000.2115.3001.5343
// ** 日期 : 2022/10/14
// ** 功能 : 1、RS232协议顶层模块
// 2、实例化接收模块和发送模块,并组成串口回环
// 3、波特率115200
// *****************************************************************************************************************************
module rs232_fsm(
//系统接口
input sys_clk ,
input sys_rst_n ,
//输入串行数据
input in_data ,
//输出串行数据
output out_data
);
wire [7:0] po_data;
wire po_flag;
uart_rx_fsm
#(
.BAUD (115200 ), //串口波特率
.CLK_FREQ (50_000_000) //时钟周期
)
uart_rx_fsm_inst
(
//系统接口
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
//输入数据
.in_data (in_data),
//输出数据
.po_data (po_data),
.po_flag (po_flag)
);
uart_tx_fsm
#(
.BAUD (115200 ), //串口波特率
.CLK_FREQ (50_000_000) //时钟周期
)
uart_tx_fsm_inst
(
//系统接口
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
//输入数据及同步信号
.pi_data (po_data),
.pi_flag (po_flag),
//输出串行数据
.out_data (out_data)
);
endmodule
将串口接收模块和发送模块直接相连,形成一个串口回环,并上板进行验证,验证结果如下,输入数据"112256478ff",设置波特率为115200,数据位8,校验位为奇校验,发送后返回数据与输入数据相同,可知模块设计无误。文章来源:https://www.toymoban.com/news/detail-410701.html
总结
本文采用状态机和计数器组合的方式实现了RS232协议,在代码中笔者也采用了将时序逻辑中较为复杂的条件判断条件以组合逻辑的形式单独列出来的方式,的确使得状态机的转变条理更加清晰,也易于修改拓展了,笔者能力尚浅,还请读者多多指正!文章来源地址https://www.toymoban.com/news/detail-410701.html
到了这里,关于【FPGA学习】状态机实现UART通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!