文章目录
前言
记录本人第一次CSDN发帖,开启FPGA学习记录之旅,后期会不定时更新
---------此篇文章主要内容:FPGA-UART学习笔记(以RS-232通信为例)
一、UART是什么
UART(Universal Asynchronous Receiver and Transmitter),一种采用异步串行通信方式的通用异步收发传输器。它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据(实现数据的串并转换)。
(注:发送过程为 “先发送低位,后发送高位”)
1.协议层:通信协议,包括数据格式、传输速率等。
2.物理层:接口类型、电平标准等。
附:常见的串行通信接口:
二、UART–协议层
1、数据格式
UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收。
异步串行通信数据格式:
1.空闲状态:在没有数据传输时,数据线处于空闲状态,保持为高电平。
2.起始位:在空闲状态下,一旦检测到数据线由1->0,则认为0开始的地方为起始位,标志着一帧数据的开始,即接收方检测到起始位时,就开始准备接收数据。
3.数据位:在上图中有7个数据位,实际上数据位可以为5/6/7/8位,而8位才是最常用的。
4.校验位:校验位用来检测数据传输过程中是否出错,分为奇校验和偶校验。
- 奇校验:让数据位加上校验位中的“1”的个数保持为奇数。
- 偶校验:让数据位加上校验位中的“1”的个数保持为偶数。
5.停止位:在上图中为1个停止位,实际上可以为1/1.5/2个停止位,停止位标志着一帧数据的结束,在停止位之后,数据线回到空闲状态。若在空闲状态下再次检测到起始位,则标志着下一帧数据的开始。
2、传输速率
串口通信的速率用波特率表示,它表示每秒传输二进制数据的位数,单位bps(位/秒)。常用的波特率有9600/19200/38400/57600以及115200等。
三、UART–物理层
1、接口标准
针对异步通信的接口标准有RS232、RS422、RS485等。
接口标准 | 逻辑1 | 逻辑0 | 说明 | 优缺点 |
---|---|---|---|---|
RS232 | -15V | +15V | 负逻辑电平;3线全双工(TXD、RXD、GND);点对点双向通信 | 传输速度相对较低;传输距离短(15m) |
RS422 | 差值电压+(2-6)V | 差值电压-(2-6)V | 差分传输;4线全双工;点对多主从通信 | 抗干扰能力强;传输速度高;传输距离远 |
RS485 | 差值电压+(2-6)V | 差值电压-(2-6)V | 差分传输;2线半双工;多点双向通信 | 能够实现多个发送、接收设备双向通信 |
- 单端传输(RS232):使用一根信号线加一根地线去传输一路信号,使用信号线与地线的电平差值去表示要传输的信号。
- 差分传输(RS422/RS485):使用两根信号线去传输一路信号,这两根信号线的幅值相等、极性相反,相比于单端传输,抗干扰能力很强。
- 点对点双向通信:只能有两个设备进行双向通信。
- 点对多主从通信:一个主设备加多个从设备,主设备和各个从设备之间可以实现双向通信,但是从设备之间不能通信。
- 多点双向通信:一个主设备加多个从设备,主设备和各个从设备之间可以实现双向通信,且各个从设备之间也可以实现双向通信。
RS-232标准的串口常见接口类型:DB9
DB9接口定义如下:
引脚定义 | 引脚名称 | 功能说明 |
---|---|---|
Pin1 | DCD | 数据载波检测 |
Pin2 | RXD | 接受数据 |
Pin3 | TXD | 发送数据 |
Pin4 | DTR | 数据终端准备 |
Pin5 | GND | 地线 |
Pin6 | DSR | 数据准备就绪 |
Pin7 | RTS | 请求发送 |
Pin8 | CTS | 清除发送 |
Pin9 | RI | 振铃显示 |
注:常用引脚Pin2(RXD)、Pin3(TXD)、Pin5(GND)
另,附:
四、FPGA–UART硬件设计
由于RS232采用负逻辑电平,而FPGA采用TTL电平,故使用FPGA与外部RS232通信时需要一个电平转换过程,由于本人此实验用的是DE2-115开发板,由开发板自带ZT3232芯片完成电平转换过程。
逻辑0 | 逻辑1 | |
---|---|---|
RS232<—>FPGA | +15V<—>0V | -15V<—>+3.3V |
五、FPGA–UART程序设计
本次串口实验测试例程:开发板与上位机通过RS-232通信,完成数据环回实验。
1、系统框图
2、协议层
起始位1位,数据位8位,停止位1位,无校验位,波特率115200bps。
3、顶层模块RTL Viewer
4、Verilog HDL代码实现
本人在代码中进行较为详细的注释,方便理解与学习,注释有误的地方望批评指正。
- 顶层模块:
module Top_UART_RS232(
input sys_clk_50, // 全局时钟信号
input sys_rst_n, // 复位信号(低有效)
input uart_rxd, // UART接收端口
output uart_txd // UART发送端口
);
parameter CLK_FREQ = 50_000_000; // 定义系统时钟频率
parameter UART_BPS = 115200; // 定义串口波特率
wire uart_en_w; // UART发送使能
wire [7:0] uart_data_w; // UART发送数据
uart_recv #( // 串口接收模块
.CLK_FREQ (CLK_FREQ ), // 设置系统时钟频率
.UART_BPS (UART_BPS ) // 设置串口接收波特率
) u_uart_recv(
.clk (sys_clk_50 ),
.rst_n (sys_rst_n ),
.uart_rxd (uart_rxd ),
.uart_done (uart_en_w ), // 用接收端接收完一帧的标志信号作为发送端的使能信号
.uart_data (uart_data_w)
);
uart_send #( // 串口发送模块
.CLK_FREQ (CLK_FREQ ), // 设置系统时钟频率
.UART_BPS (UART_BPS ) // 设置串口发送波特率
) u_uart_send(
.clk (sys_clk_50 ),
.rst_n (sys_rst_n ),
.uart_en (uart_en_w ),
.uart_data (uart_data_w),
.uart_txd (uart_txd )
);
endmodule
- 接收模块:
module uart_recv(
input clk, // 全局时钟信号
input rst_n, // 复位信号(低有效)
input uart_rxd, // UART接收端口(RXD)
output reg uart_done, // 接收完一帧数据的标志信号
output reg[7:0] uart_data // 接收的数据
);
parameter CLK_FREQ = 50_000_000; // 系统时钟频率50MHz
parameter UART_BPS = 115200; // 串口的波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS; // 计算多少个时钟周期为1个波特率周期,在此系统时钟50MHz,波特率115200时,BPS_CNT=434,即计数434个时钟周期为1个波特率周期
reg uart_rxd_d0; // 边沿检测-接收延时中间变量d0
reg uart_rxd_d1; // 边沿检测-接收延时中间变量d1
reg rxd_flag; // 处于接收状态标志信号
reg [ 3:0] rxd_cnt; // 接收数据计数器,计算每一帧中FPGA接收了多少个波特率周期,比如:当开始传送数据时,即过了起始位,此时rxd_cnt为1,已经过了一个波特率周期
reg [15:0] clk_cnt; // 系统时钟计数器,计数BPS_CNT个时钟周期为一个波特率周期,若波特率为115200,则计数‘434个时钟周期=1个波特率周期’
reg [ 7:0] rxd_data; // 接收数据寄存器,串行接收完一帧数据后,先临时寄存到这个寄存器,接收完一帧数据后,统一并行送到输出口,实现串转并
wire start_flag; // 检测起始位下降沿来临,给出一个时钟周期标志位(不过由于每次检测到RXD端口出现下降沿,就会有高脉冲,故在接收有效数据过程中也会出现高脉冲,不过不影响正常工作)
// 常用的边沿检测手段,在这里捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign start_flag = (~uart_rxd_d0) & uart_rxd_d1;
always @(posedge clk or negedge rst_n) // 对UART接收端口的数据延迟两个时钟周期
begin
if(!rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end
// 当start_flag脉冲信号(起始位信号)来临时,FPGA进入一帧数据的接收状态,即将rxd_flag置1
// 一帧数据传输最后,在停止位中间时关闭接收状态,即将rxd_flag置0
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
rxd_flag <= 1'b0;
else begin
if(start_flag == 1'b1) // 起始位来临后,进入接收状态,rxd_flag标志位置1
rxd_flag <= 1'b1;
else if((rxd_cnt == 4'd9) && (clk_cnt == BPS_CNT/2)) // 当计数完9个波特率周期(1起始位+8数据位),再计数至停止位中间,关闭接收状态信号
rxd_flag <= 1'b0;
else
rxd_flag <= rxd_flag;
end
end
// 根据clk_cnt的循环计数,每一帧传输中,累计rxd_cnt个波特率周期。即当处于接收状态时,计算:从起始位开始,累计过了多少个波特率周期
always @(posedge clk or negedge rst_n)
begin
if(!rst_n) begin
rxd_cnt <= 4'd0;
clk_cnt <= 16'd0;
end
else if(rxd_flag == 1'b1) begin // 当系统处于接收状态(rxd_flag为1),根据计多少个时钟周期,算出一个波特率周期,并进行波特率周期累加计算
if(clk_cnt == BPS_CNT - 1'b1) begin // 计数BPS_CNT个时钟周期为1个波特率周期
clk_cnt <= 16'd0;
rxd_cnt <= rxd_cnt + 1'b1;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
rxd_cnt <= rxd_cnt;
end
end
else begin
rxd_cnt <= 4'd0;
clk_cnt <= 16'd0;
end
end
// 根据接收数据计数器rxd_cnt来寄存uart接收端口数据,将串口接收到的串行数据转换为并行数据寄存,实现串转并
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
rxd_data <= 8'd0;
else if(rxd_flag == 1'b1) begin // 当系统处于接收状态(rxd_flag为1),进行数据接收
if(clk_cnt == BPS_CNT/2) begin // 判断系统时钟计数器计数到数据位中间时(即每次计到波特率周期的中间位置),此时采集的数据是最准确的
case(rxd_cnt) // UART协议先发送数据的低位,再发送数据的高位,故先接收低位再接收高位
4'd1: rxd_data[0] <= uart_rxd_d1; // 寄存数据位最低位
4'd2: rxd_data[1] <= uart_rxd_d1;
4'd3: rxd_data[2] <= uart_rxd_d1;
4'd4: rxd_data[3] <= uart_rxd_d1;
4'd5: rxd_data[4] <= uart_rxd_d1;
4'd6: rxd_data[5] <= uart_rxd_d1;
4'd7: rxd_data[6] <= uart_rxd_d1;
4'd8: rxd_data[7] <= uart_rxd_d1; // 寄存数据位最高位
default:;
endcase
end
else
rxd_data <= rxd_data;
end
else
rxd_data <= 8'd0;
end
// 每一帧数据接收完毕之后,给出一个标志信号uart_done,通过uart_data输出接收到的数据
always @(posedge clk or negedge rst_n)
begin
if(!rst_n) begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
else if(rxd_cnt == 4'd9) begin
uart_data <= rxd_data;
uart_done <= 1'b1;
end
else begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
end
endmodule
- 发送模块:
module uart_send(
input clk, // 全局时钟信号
input rst_n, // 复位信号(低有效)
input uart_en, // 发送使能信号
input [7:0] uart_data, // 待发送数据
output reg uart_txd // UART发送端口
);
parameter CLK_FREQ = 50_000_000; // 系统时钟频率50MHz
parameter UART_BPS = 115200; // 串口的波特率
parameter BPS_CNT = CLK_FREQ/UART_BPS;
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; // 系统时钟计数器
reg [ 3:0] txd_cnt; // 发送数据计数器
reg txd_flag; // 发送过程标志信号
reg [ 7:0] txd_data; // 寄存要发送数据
wire en_flag;
// 常用的边沿检测手段,在这里捕获发送使能信号uart_en的上升沿,得到一个时钟周期的脉冲信号
assign en_flag = uart_en_d0 & (~uart_en_d1) ;
always @(posedge clk or negedge rst_n) begin // 对发送使能信号uart_en延迟两个时钟周期
if(!rst_n) begin
uart_en_d0 <= 1'b0;
uart_en_d1 <= 1'b0;
end
else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end
// 当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
txd_flag <= 1'b0;
txd_data <= 8'd0;
end
else if(en_flag) begin // 检测到发送使能上升沿
txd_flag <= 1'b1; // 进入发送过程,标志位txd_flag拉高
txd_data <= uart_data; // 寄存待发送的数据
end
else if((txd_cnt == 4'd9)&&(clk_cnt == BPS_CNT/2)) begin // 计数到停止位中间时,停止发送过程
txd_flag <= 1'b0; // 发送过程结束,标志位txd_flag拉低
txd_data <= 8'd0;
end
else begin
txd_flag <= txd_flag;
txd_data <= txd_data;
end
end
// 进入发送过程后,启动系统时钟计数器与发送数据计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
clk_cnt <= 16'd0;
txd_cnt <= 4'd0;
end
else if(txd_flag) begin // 处于发送过程
if (clk_cnt == BPS_CNT - 1) begin
clk_cnt <= 16'd0; // 对系统时钟计数达一个波特率周期后清零
txd_cnt <= txd_cnt + 1'b1; // 此时发送数据计数器加1
end
else begin
clk_cnt <= clk_cnt + 1'b1;
txd_cnt <= txd_cnt;
end
end
else begin //发送过程结束
clk_cnt <= 16'd0;
txd_cnt <= 4'd0;
end
end
// 根据发送数据计数器来给uart发送端口赋值,实现并转串
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
uart_txd <= 1'b1;
else if(txd_flag)
case(txd_cnt)
4'd0: uart_txd <= 1'b0; //起始位,为一个波特率周期的低电平
4'd1: uart_txd <= txd_data[0]; //数据位最低位
4'd2: uart_txd <= txd_data[1];
4'd3: uart_txd <= txd_data[2];
4'd4: uart_txd <= txd_data[3];
4'd5: uart_txd <= txd_data[4];
4'd6: uart_txd <= txd_data[5];
4'd7: uart_txd <= txd_data[6];
4'd8: uart_txd <= txd_data[7]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
else
uart_txd <= 1'b1; //空闲时发送端口为高电平
end
endmodule
------程序中有用到一种经典的边沿检测手段:
即捕获接收端的下降沿(起始位)或上升沿(发送标志位),从而得到一个时钟周期的脉冲信号,下述以检测下降沿为例说明:
assign start_flag = (~uart_rxd_d0) & uart_rxd_d1;
always @(posedge clk or negedge rst_n)
begin
if(!rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end
将输入的信号,进行延时两个周期,同时取反延时信号0,将取反的延时信号0 “与上” 延时信号1,在此期间会产生一个时钟周期的脉冲信号,时序图如下:
注:若为检测上升沿,则只需把程序中assign start_flag = (~uart_rxd_d0) & uart_rxd_d1;
替换为assign start_flag = uart_rxd_d0 & (~uart_rxd_d1);
即可完成检测上升沿,并给出一个时钟周期脉冲信号。
总结
此篇文章总结了本人前段时间使用DE2-115开发板进行的FPGA—UART串口实验,在此以RS-232串口通信为例进行笔记记录,另有RS-485串口通信代码未在此文给出,不过大致类似,可仿照RS-232写出RS-485通信代码。
(文中多有参考正点原子—FPGA串口开发资料)