FPGA_Verilog学习之旅(1)---浅谈UART
文章目录 三、UART--物理层 四、FPGA--UART硬件设计五、FPGA--UART程序设计 总结
前言
记录本人第一次CSDN发帖,开启FPGA学习记录之旅,后期会不定时更新
---------此篇文章主要内容:FPGA-UART学习笔记(以RS-232通信为例)
一、UART是什么
UART( and ),一种采用异步串行通信方式的通用异步收发传输器。它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据(实现数据的串并转换)。
(注:发送过程为 “先发送低位,后发送高位”)
1.协议层:通信协议,包括数据格式、传输速率等。
2.物理层:接口类型、电平标准等。
附:常见的串行通信接口:
二、UART–协议层 1、数据格式
UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收。
异步串行通信数据格式:
1.空闲状态:在没有数据传输时,数据线处于空闲状态,保持为高电平。
2.起始位:在空闲状态下,一旦检测到数据线由1->0,则认为0开始的地方为起始位,标志着一帧数据的开始,即接收方检测到起始位时,就开始准备接收数据。
3.数据位:在上图中有7个数据位,实际上数据位可以为5/6/7/8位,而8位才是最常用的。
4.校验位:校验位用来检测数据传输过程中是否出错,分为奇校验和偶校验。
5.停止位:在上图中为1个停止位,实际上可以为1/1.5/2个停止位,停止位标志着一帧数据的结束,在停止位之后,数据线回到空闲状态。若在空闲状态下再次检测到起始位,则标志着下一帧数据的开始。
2、传输速率
串口通信的速率用波特率表示,它表示每秒传输二进制数据的位数,单位bps(位/秒)。常用的波特率有9600/19200/38400/57600以及等。
三、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线半双工;多点双向通信
能够实现多个发送、接收设备双向通信
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开发板,由开发板自带芯片完成电平转换过程。
逻辑0逻辑1
+15V0V
-15V+3.3V
五、FPGA–UART程序设计
本次串口实验测试例程:开发板与上位机通过RS-232通信,完成数据环回实验。
1、系统框图
2、协议层
起始位1位,数据位8位,停止位1位,无校验位,波特率。
3、顶层模块RTL
4、 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接收端口的数据延迟两个时钟周期
beginif(!rst_n) beginuart_rxd_d0 <= 1'b0;uart_rxd_d1 <= 1'b0;endelse 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)
beginif(!rst_n) rxd_flag <= 1'b0;else begin if(start_flag == 1'b1) // 起始位来临后,进入接收状态,rxd_flag标志位置1rxd_flag <= 1'b1;else if((rxd_cnt == 4'd9) && (clk_cnt == BPS_CNT/2)) // 当计数完9个波特率周期(1起始位+8数据位),再计数至停止位中间,关闭接收状态信号rxd_flag <= 1'b0;elserxd_flag <= rxd_flag;end
end// 根据clk_cnt的循环计数,每一帧传输中,累计rxd_cnt个波特率周期。即当处于接收状态时,计算:从起始位开始,累计过了多少个波特率周期
always @(posedge clk or negedge rst_n)
beginif(!rst_n) beginrxd_cnt <= 4'd0;clk_cnt <= 16'd0;endelse 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 beginclk_cnt <= clk_cnt + 1'b1;rxd_cnt <= rxd_cnt;endendelse beginrxd_cnt <= 4'd0;clk_cnt <= 16'd0;end
end// 根据接收数据计数器rxd_cnt来寄存uart接收端口数据,将串口接收到的串行数据转换为并行数据寄存,实现串转并
always @(posedge clk or negedge rst_n)
beginif(!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:; endcaseendelse rxd_data <= rxd_data;end else rxd_data <= 8'd0;
end// 每一帧数据接收完毕之后,给出一个标志信号uart_done,通过uart_data输出接收到的数据
always @(posedge clk or negedge rst_n)
beginif(!rst_n) beginuart_data <= 8'd0;uart_done <= 1'b0;endelse if(rxd_cnt == 4'd9) beginuart_data <= rxd_data;uart_done <= 1'b1;endelse beginuart_data <= 8'd0;uart_done <= 1'b0;end
endendmodule
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) beginuart_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; // 寄存待发送的数据endelse if((txd_cnt == 4'd9)&&(clk_cnt == BPS_CNT/2)) begin // 计数到停止位中间时,停止发送过程 txd_flag <= 1'b0; // 发送过程结束,标志位txd_flag拉低txd_data <= 8'd0;endelse begintxd_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) beginclk_cnt <= 16'd0; // 对系统时钟计数达一个波特率周期后清零txd_cnt <= txd_cnt + 1'b1; // 此时发送数据计数器加1endelse beginclk_cnt <= clk_cnt + 1'b1;txd_cnt <= txd_cnt; endendelse 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: ;endcaseelse uart_txd <= 1'b1; //空闲时发送端口为高电平
endendmodule
------程序中有用到一种经典的边沿检测手段:
即捕获接收端的下降沿(起始位)或上升沿(发送标志位),从而得到一个时钟周期的脉冲信号,下述以检测下降沿为例说明:
assign start_flag = (~uart_rxd_d0) & uart_rxd_d1;always @(posedge clk or negedge rst_n)
beginif(!rst_n) beginuart_rxd_d0 <= 1'b0;uart_rxd_d1 <= 1'b0;endelse begin uart_rxd_d0 <= uart_rxd;uart_rxd_d1 <= uart_rxd_d0;end
end
将输入的信号,进行延时两个周期,同时取反延时信号0,将取反的延时信号0 “与上” 延时信号1,在此期间会产生一个时钟周期的脉冲信号,时序图如下:
注:若为检测上升沿,则只需把程序中 = (~) & ;
替换为 = & (~);即可完成检测上升沿,并给出一个时钟周期脉冲信号。
总结
此篇文章总结了本人前段时间使用DE2-115开发板进行的FPGA—UART串口实验,在此以RS-232串口通信为例进行笔记记录,另有RS-485串口通信代码未在此文给出,不过大致类似,可仿照RS-232写出RS-485通信代码。
(文中多有参考正点原子—FPGA串口开发资料)