一、UART
串口通信,说实话是日常生活中很常见的一种9针型的主机和显示屏之间的通信模式。本次博客,只为自己复盘相关知识,初学者,错误较多,请多指教。所以本文参考(一)FPGA之串口通信(UART)_fpga uart-CSDN博客
而编著的。
FPGA是一种可编程的逻辑器件,用于各种数字电路设计与实现,是一种可重复利用的芯片盒。今天所介绍的是利用赛灵思xc7a35tftg256-1系列的FPGA实现串口通信(UART)。
一个状态机,我们有发送模块(uart_tx)和接收模块((uart_tx)。模块首先得有一个时钟信号CLK,复位信号RESET.
波特率解释:
串口协议中baud_rate波特率是很重要的知识点,字面意思就是一秒钟传输的码元(即一个0-1状态)的个数,就是指的是一秒钟传输的二进制位数,常见单位是bit/s和bps两种,常见的串口速度有115200bps(RS232串口)和9600bps and so on,
波特率和字节转换:
1GB=1024MB 1MB=1024KB 1KB=1024B字节 ,倘若我们串口数据位每秒钟512字节,串口的波特率就是115200位/秒 (没搞懂换算?)
如果我们校验位置零,则位/10=字节 ,如波特率115200,即是115200(位/秒)=11520(b/s) ;除以1024 就可以从位换算到KB:波特率115200=115200((位/秒))=11.25KB/S
今天我们要在FPGA上实现异步串口通信,假设我们FPGA的clk时钟是50Mhz的系统时钟,波特率是9600bps,那么传输一个bit需要的时钟周期个数是50_000_000/9600个个数。得到这个个数,再乘以周期时间,就可以得到这个1bit所需要的时间。
状态机工作原理解释:
1.串口通信的信号线只需要两条线就可以完成。tx和rx, tx是状态机信号发送端,rx是状态机的信号接收端。
2.起始位STATE_START,数据线由高变低,低有效触发,即0开始,数据开始传输。
3.数据位,起始位低有效之后,数据位开始传输,如下图是8为数据,从左向右开始传输低位向着高位前进。
4.校验位PARITY,我们可以将校验位当成一个特殊的空位。通常使用的是奇偶校验位,通常在代码中有说明是否有校验, PARITY_ON=0, //校验位,1为有校验位,0为无校验位,缺省为0。(即在串口通信中,校验位通常不适用)
5.停止位end,高有效=1,它表是一个个字节传输的结束
6.空闲位,是持续的高有效,由于当start=0低电平以后开始数据传输,所以高有效就能够使得状态机处于空闲状态。
7.帧:我们将一个个字节传输从起始位到停止位经历的全部时间称之为一帧。
状态机模型解释:
如下图,我们定义了一个state状态机,用于控制串口通信的不同状态。我们还定义了一个bit_counter计数器用于计算每个位的延迟时间,在串口通信实例中,假设每个位延迟时间为bit_delay。然后定义一个数据寄存器serial_data,用于存储接收到的数据,开始时置位为0
在状态机的always块中,我们根据不同的状态开始发送以及接收。在空闲状态IDLE下,如果检测到起始位,则将状态切换到‘START_BIT’,同时重置计数器和数据寄存器。
在‘start_bit’状态下,通过计数器进行延迟,等待计数器进行延迟,等待起始位结束,然后将状态位切换到data_bits
在‘data_bits’状态下,通过计数器位逐位接收数据,并将存储在数据寄存器‘serial_data’中。当接收完所有数据后,应该切换到parity_bit位
然后根据parity_bit数据校验位的是否需要,进行逻辑处理
在stop位置等待结束后,切换☞IDLE空闲状态,并将发送状态的‘uart_tx’设置为停止位。
下列给出具体实现代码:
发送端uart_tx实现原理:发送思路原理:我们可以通过定义一个计数器,来记录每次发送所需要的的时间,然后用一个状态机来记录发送的状态。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 03/10/2024 08:46:28 PM
// Design Name:
// Module Name: uart_tx
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module uart_tx
#( //搜素一下 这里为什么要加井号才能成功
parameter CLK_FRE=50, //默认时钟频率为50Mhz
parameter DATA_WIDTH=8, //有效数据位,缺省为8位
parameter PARITY_TYPE=0, //校验类型,1为奇校验,0为偶校验,缺省为偶校验
parameter PARITY_ON=0, //校验位,1为有校验位,0为无校验位,缺省为0
parameter BAUD_RATE=9600 //波特率,缺省为9600
)
(
input i_clk_sys,//系统时钟
input i_rst_n,//全局异步复位
input[DATA_WIDTH-1:0] i_data_tx,//传输数据输入
input i_data_valid,//传输数据有效
output reg o_uart_tx//UART输出
);
//状态机定义
reg[2:0] r_current_state;//当前状态
reg[2:0] r_next_state;//次态
localparam STATE_IDLE=3'b000; //空闲状态
localparam STATE_START=3'b001; //开始状态
localparam STATE_DATA=3'b011; //数据发送状态
localparam STATE_PARITY=3'b100; //数据校验计算和发送
localparam STATE_END=3'b101; //结束状态
localparam CYCLE=CLK_FRE*1000000/BAUD_RATE; //波特率计数周期
reg baud_valid;//波特计数有效位
reg[15:0] baud_cnt; //波特率计数器
reg baud_pulse; //波特率采样脉冲
reg[3:0] r_tx_cnt; //接收数据位计数
//波特率计数器
always @(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n) //如果是低电平 则成立
baud_cnt<=16'h0000;
else if(!baud_valid)
baud_cnt<=16'h0000;
else if(baud_cnt==CYCLE-1)
baud_cnt<=16'h0000;
else
baud_cnt=baud_cnt+1;
end
//波特采样脉冲
always @(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n)
baud_pulse<=1'b0;
else if(baud_cnt==CYCLE/2-1)
baud_pulse<=1'b1;
else if(baud_valid&&baud_cnt==16'h0000)
r_current_state<= r_next_state;
end
//状态机的次态定义
always @(*)
begin
case(r_current_state)
STATE_IDLE: r_next_state<= STATE_START;
STATE_START: r_next_state<=STATE_DATA;
STATE_DATA:
if(r_tx_cnt==DATA_WIDTH)
begin
if(PARITY_ON==0)
r_next_state<=STATE_END;
else
r_next_state<=STATE_PARITY;
end
else
begin
r_next_state<=STATE_DATA;
end
STATE_PARITY:r_next_state<=STATE_END;
STATE_END:r_next_state<=STATE_IDLE;
default: ;
endcase
end
reg [DATA_WIDTH-1:0] r_data_tx;
reg r_parity_check;
//状态机的输出逻辑
//状态机的输出逻辑根据当前状态执行相应的操作,如发送开始位、数据位、校验位和结束位
always @(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n)
begin
baud_valid<=1'b0;
r_data_tx<='d0;
o_uart_tx<=1'b1;
r_tx_cnt<=4'd0;
r_parity_check<=1'b0;
end
else
case(r_current_state)
STATE_IDLE:begin
o_uart_tx<=1'b1;
r_tx_cnt<=4'd0;
r_parity_check<=4'b0;
if(i_data_valid)
begin
baud_valid<=1'b1;
r_data_tx<= i_data_tx;
end
end
STATE_START:begin
if(baud_pulse)
o_uart_tx<=1'b0;
end
STATE_DATA:begin
if(baud_pulse)
begin
r_tx_cnt<=r_tx_cnt+1'b1; //这行代码将r_tx_cnt(数据发送计数器)的值增加1
o_uart_tx<= r_data_tx[0];//这行代码将r_data_tx寄存器中的最低位(第0位)输出到o_uart_tx(UART发送输出)。这意味着数据的最低位被发送出去。
r_parity_check<=r_parity_check+r_data_tx[0];
r_data_tx<={1'b0,r_data_tx[DATA_WIDTH-1:1]};
end
end
/*
总结来说,当状态机处于STATE_DATA状态且baud_pulse为高电平时,模块会发送当前数据位,更新发送计数器,计算校验位,并准备下一个数据位的发送。\
这个过程会重复进行,直到所有数据位都被发送完毕。
*/
STATE_PARITY:begin
if(baud_pulse) //判断是否具有高电平脉冲过来
begin
if(PARITY_TYPE == 1)
o_uart_tx<= r_parity_check;
else
o_uart_tx<= r_parity_check+1'b1;
end
end
STATE_END:begin
if(baud_pulse)
begin
o_uart_tx<= 1'b1;
baud_valid<=1'b0;
end
end
default:;
endcase
end
endmodule
下面是数据接收端的verilog代码实现,在串口通信时,一定要实现两边的波特率一致,这样才能够同步接收和发送,本次串口通信设置是baud=9600,表是1秒钟可以发送9600个数据位。
(程序中添加了奇偶校验,但是校验不会进行,校验原则:
- 奇校验(Odd Parity):在数据位中加上一个额外的校验位,使得整个传输的数据(包括校验位)中1的总数为奇数。如果数据位中1的数量已经是奇数,则校验位设为0,以保持总数为奇数;如果是偶数,则校验位设为1。接收端检查接收到的数据中1的总数是否为奇数,如果是,则认为数据传输正确。
- 偶校验(Even Parity):与奇校验相反,偶校验要求加上的校验位保证数据中1的总数为偶数。如果数据位中1的数量是偶数,则校验位设为0;如果是奇数,则校验位设为1。接收端同样通过检查1的总数是否为偶数来判断数据是否正确。
)
接收端rx代码:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 03/13/2024 08:22:34 AM
// Design Name:
// Module Name: uart_rx
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module uart_rx
#(
parameter CLK_FRE=50,//时钟频率,默认为50Mhz
parameter DATA_WIDTH=8,//有效数据位,缺省为8位
parameter PARITY_ON=0,//校验位,1为有校验位,0为无校验位,缺省为0
parameter PARITY_TYPE=0,//校验类型,1为奇校验,0为偶校验,缺省为偶校验
parameter BAUD_RATE=9600//波特率,缺省为9600,每秒传输的位数
)//输入输出端口设置
(input i_clk_sys,//系统时钟脉冲
input i_rst_n,//全局异步复位,低电平有效
input i_uart_rx,//uart输入
output reg[DATA_WIDTH-1:0] o_uart_rx,//uart接收数据
output reg o_ld_parity,//校验位校验LED,高电平 位为校验正确
output reg o_rx_done, //数据接收完成标志
output reg o_uart_data
);
/*
UART是异步输入,最好是同步到FPGA内部的时钟域
可以省略,在异步时钟电路中,保持时钟同步是一个好习惯
*/
reg sync_uart_rx; //同步接受数据端,用于同步输入的UART信号,以避免时钟域之间的问题。
always@(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n)
sync_uart_rx<=1'b1;
else
sync_uart_rx<=i_uart_rx;
end
/*
连续采样五个接收路电平来判断rx是否有信号传来
用五个采样信号来作为判断标准可以有效排除毛刺噪声带来的误判
*/
//接受信号检测
reg[4:0] r_flag_rcv_start;
wire w_rcv_start;
always@(posedge i_clk_sys or negedge i_rst_n)
begin
if(!i_rst_n)
r_flag_rcv_start<=5'b11111;
else
r_flag_rcv_start<={r_flag_rcv_start[3:0],sync_uart_rx};
end
//状态机定义
reg[2:0]r_current_state; //当前状态
reg[2:0]r_next_state; //次态
localparam STATE_IDLE=3'b000; //空闲状态
localparam STATE_START=3'b001; //开始状态
localparam STATE_DATA=3'b011; //数据接收状态
localparam STATE_PARITY=3'b000; //空闲状态
localparam STATE_END=3'b101; //结束状态
localparam CYCLE=CLK_FRE*1000000/BAUD_RATE; //ji计算波特率周期 即单个时长
reg baud_valid; //波特率有效位
reg[15:0]baud_cnt;//波特率计数器
reg baud_pulse;//波特率采样脉冲
reg[3:0] r_rcv_cnt;//接收数据位计数
//波特率计数器
always @(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n)
baud_cnt<=16'h0000;
else if (!baud_valid)
baud_cnt<=16'h0000;
else if(baud_cnt==CYCLE-1)
baud_cnt<=16'h0000;
else
baud_cnt<=baud_cnt+1'b1;
end
//波特采样脉冲
always @(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n)
baud_pulse<=1'b0;
else if(baud_cnt==CYCLE/2-1)
baud_pulse<=1'b1;
else
baud_pulse<=1'b1;
end
//状态机状态变化定义
always @(posedge i_clk_sys or negedge i_rst_n )
begin
if(!i_rst_n) //低电平输入
r_current_state<=STATE_IDLE;
else if(!baud_valid)
r_current_state<=STATE_IDLE;
else if(baud_valid&&baud_cnt==16'h0000)
r_current_state<=r_next_state;
end
//状态机次态定义
always @(*) //。在这个语句中,@(*) 表示该 always 块对敏感列表中的所有信号都敏感。换句话说,当敏感列表中的任何一个信号发生变化时,always 块都会被执行。
begin
case(r_current_state)
STATE_IDLE :r_next_state<=STATE_START;
STATE_START:r_next_state<=STATE_DATA;
STATE_DATA:
if(r_rcv_cnt==DATA_WIDTH)
begin
if(PARITY_ON==0)
r_next_state<=STATE_END;
else
r_next_state<=STATE_PARITY;//校验位开启时进
end
else
begin
r_next_state<=STATE_DATA;
end
STATE_PARITY:r_next_state<=STATE_END;
STATE_END:r_next_state<=STATE_IDLE; //STATE_IDLE 即是空闲位
default:;
endcase
end
reg[DATA_WIDTH-1:0] r_data_rcv; //声明一个8位寄存器
reg r_parity_check; //奇偶校验位寄存器 存储结果
//状态机输出逻辑
always @(posedge i_clk_sys or negedge i_rst_n)
begin
if(!i_rst_n)
begin
baud_valid<=1'b0;
r_data_rcv<='d0;
r_rcv_cnt<=4'd0;
r_parity_check<=1'b0;
o_uart_data<='d0;
baud_pulse<=1'b0;
o_rx_done<=1'b0;
end
else
case(r_current_state)
STATE_IDLE:begin
//闲置状态下 对寄存器进行复位
r_rcv_cnt<=4'd0;
r_data_rcv<='d0;
r_parity_check<=1'b0;
o_rx_done<=1'b0;
//连续检测到低电平,则认为UART传输过来数据,拉高baud_valid。低电平作为数据开始位
if(r_flag_rcv_start==5'b00000)
baud_valid<=1'b1;
end
STATE_START:begin
if(baud_pulse && sync_uart_rx) //波特率采样脉冲到来时
baud_valid<=1'b1;
end
STATE_DATA:begin
if(baud_pulse)
begin
r_data_rcv <={sync_uart_rx,r_data_rcv[DATA_WIDTH-1:1]};//数据移位。将同步串口接收信号(sync_uart_rx)和当前接收到的数据(r_data_rcv)进行移位操作,并将结果赋值给接收数据寄存器(r_data_rcv)。
r_rcv_cnt<=r_rcv_cnt+1'b1;//数据位计数
r_parity_check<=r_parity_check+sync_uart_rx;//校验位做校验
end
end
STATE_PARITY:begin
if(baud_pulse) //如果是高电平
begin
//校验检测,正确则0_ld_parity拉高,则可输出给led检测,如果闪烁则表示有错误数据
if(r_parity_check+sync_uart_rx==PARITY_TYPE)
o_ld_parity<=1'b1;
else
o_ld_parity<=1'b0;
end
else
o_ld_parity<=o_ld_parity;
end
STATE_END:begin
if(baud_pulse)
begin
//没有校验位或校验位正确时才输出数据,否则直接丢弃数据
if(PARITY_ON==0||o_ld_parity)
begin
o_uart_data<= r_data_rcv;
o_rx_done<=1'b1;
end
end
else
begin
o_rx_done<=1'b0;
end
if(baud_cnt==16'h0000)
baud_valid<=1'b0;
end
default:;
endcase
end
endmodule
如上串口通信的发送端程序和接收端程序均已完美。但是还需要做一个顶层程序来调动发送端和接收端。即是给uart_tx发送端和uart_rx 接收端初始化相关寄存器变量。整个模块的功能用来接收来自UART的输入信号,并将其传输给UART模块输出。同时使用校验位检验LED来指示检验是否正确。
如下代码:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 03/13/2024 11:31:45 AM
// Design Name:
// Module Name: uart_loop
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module uart_loop(
input i_clk_sys,
input i_rst_n,
input i_uart_rx,
output o_uart_tx,
output o_ld_parity
);
localparam DATA_WIDTH = 8;
localparam BAUD_RATE = 9600;
localparam PARITY_ON = 1;
localparam PARITY_TYPE = 1;
wire w_rx_done;
wire[DATA_WIDTH-1 : 0] w_data;
uart_rx
#(
.CLK_FRE(50), //时钟频率,默认时钟频率为50MHz
.DATA_WIDTH(DATA_WIDTH), //有效数据位,缺省为8位
.PARITY_ON(PARITY_ON), //校验位,1为有校验位,0为无校验位,缺省为0
.PARITY_TYPE(PARITY_TYPE), //校验类型,1为奇校验,0为偶校验,缺省为偶校验
.BAUD_RATE(BAUD_RATE) //波特率,缺省为9600
) u_uart_rx
(
.i_clk_sys(i_clk_sys), //系统时钟
.i_rst_n(i_rst_n), //全局异步复位,低电平有效
.i_uart_rx(i_uart_rx), //UART输入
.o_uart_data(w_data), //UART接收数据
.o_ld_parity(o_ld_parity), //校验位检验LED,高电平位为校验正确
.o_rx_done(w_rx_done) //UART数据接收完成标志
);
uart_tx
#(
.CLK_FRE(50), //时钟频率,默认时钟频率为50MHz
.DATA_WIDTH(DATA_WIDTH), //有效数据位,缺省为8位
.PARITY_ON(PARITY_ON), //校验位,1为有校验位,0为无校验位,缺省为0
.PARITY_TYPE(PARITY_TYPE), //校验类型,1为奇校验,0为偶校验,缺省为偶校验
.BAUD_RATE(BAUD_RATE) //波特率,缺省为9600
) u_uart_tx
( .i_clk_sys(i_clk_sys), //系统时钟
.i_rst_n(i_rst_n), //全局异步复位
.i_data_tx(w_data), //传输数据输入
.i_data_valid(w_rx_done), //传输数据有效
.o_uart_tx(o_uart_tx) //UART输出
);
endmodule
下面是电路仿真实现:
该模块的输入包括:
i_clk_sys
:系统时钟信号。i_rst_n
:全局异步复位信号,低电平有效。i_uart_rx
:UART输入信号。
该模块的输出包括:
o_uart_tx
:UART输出信号。o_ld_parity
:校验位检验LED,高电平表示校验正确
下面在波形仿真模块,先定义一些寄存器变量,和线型变量,然后分别调用发送模块和数据接收模块。并且初始化全局复位和时钟信号。
`timescale 1ns / 1ps
//
// Company: 南京信息工程大学
// Engineer: 王彪
//
// Create Date: 03/13/2024 07:43:29 PM
// Design Name: 同步串口通信
// Module Name: uart_loop_tb
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module uart_loop_tb(
);
reg clk_sys;
reg rst_n;
reg uart_in;
wire uart_out;
wire parity;
uart_loop u_uart_loop(
.i_clk_sys(clk_sys),
.i_rst_n(rst_n),
.i_uart_rx(uart_in),
.o_uart_tx(uart_out),
.o_ld_parity(parity)
);
initial begin
clk_sys = 1'b0;
rst_n = 1'b0;
uart_in = 1'b1;
end
always #10 clk_sys = ~clk_sys;
localparam ELEMENT_TIME = 104160;
reg [7:0] DATA = 8'hAC;
initial begin
#100 rst_n = 1'b1;
#20000;
// #并且可以使用#来进行时间等待。
uart_in = 1'b0;//uart_in = 1'b0; 将UART输入信号uart_in设置为低电平,模拟开始位的发送。
#ELEMENT_TIME
uart_in = DATA[0];
#ELEMENT_TIME
uart_in = DATA[1];
#ELEMENT_TIME
uart_in = DATA[2];
#ELEMENT_TIME
uart_in = DATA[3];
#ELEMENT_TIME
uart_in = DATA[4];
#ELEMENT_TIME
uart_in = DATA[5];
#ELEMENT_TIME
uart_in = DATA[6];
#ELEMENT_TIME
uart_in = DATA[7];
#ELEMENT_TIME
uart_in = 1'b1;
#ELEMENT_TIME
uart_in = 1'b1;
end
//总结来说,这个测试模块通过模拟发送一个包含8位数据的UART数据帧,包括开始位、数据位、校验位(如果有的话)和停止位。
//ELEMENT_TIME参数用于控制每个位的时间间隔,以确保数据按照正确的时序发送。这个测试模块可以用来检查UART接收模块是否能够正确地接收和处理发送的数据。
endmodule
下面进行全局行为仿真,发送的数8为16进制ac