【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
2021/7/15 23:12:44
本文主要是介绍【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录
- 一、相关知识
- 二、模块设计
- 三、代码设计
- 3.1 串口接收模块
- 3.2 控制模块
- 3.3 串口发送模块
- 四、FIFO 核引用
- 五、管脚定义及结果展示
上一篇博文:【入门学习三】基于 FPGA 使用 Verilog 实现按键状态机代码及原理详解
本文内容:从 PC 上位机通过 COM 发送数据给 FPGA ,FPGA 接收到数据后,将数据回传给 PC 上位机。
一、相关知识
- 串口通信分为串行通信和并行通信,这里主要将串行通信,因为要用到。
- 串行通信主要分为同步通信和异步通信。
串行通信 | 同步通信 | 带时钟同步信号的数据传输 | 如 I2C、SPI |
异步通信 | 不带时钟同步信号的数据传输 | 如 UART、单总线 | |
传输方向 | 单工 | ||
半双工 | 如 I2C、单总线 | ||
全双工 | 如 SPI、UART |
- 这里我们使用的是 UART 串口通信,它是全双工的异步通信。
- 它的协议主要由四部分组成
- 起始位(1 bit)
- 数据位(6/7/8 bit)
- 奇偶校验位(1 bit)
- 停止位(1 bit)
- 原理图如下:
- 上图中接收的时候,当接收到下降沿(起始位)后,开始接收到 8 位的数据(包含奇偶校验位),当接收到上升沿(停止位)后,接收停止。
- 上图中发送的时候,首先发送一个起始位(下降沿),然后发送连续的 8 位数据,然后再发送一个停止位(上升沿),到此,发送完毕。
- 串口通讯接口主要使用 TXD 和 RXD 管脚,TXD 发送数据,RXD 接收数据,两个设备的 TXD 和 RXD 要交叉连接。
- 由于 UART 是串行通信,所以接收的时候需要把总线上的串行数据转换成并行数据存储,而在发送的时候,就需要把并行数据转化成串行数据。
二、模块设计
- 从上面的讲解看起来串口通信也就那么回事对吧,都熟悉,可是呢,在设计的时候可就没那么简单了。
- 前面三篇文章,都是我自己画的模块,不那么严谨,这里我直接用 Quartus 生成的模块图。
- 输入口为时钟 clk 、复位 rst_n 以及数据接收 rx。
- 串口接收模块从 rx 接收到数据后,在内部实现接收方法,接收完毕后将数据发送给控制模块,也就是由 rx_data[7:0] 连接到 din[7:0],同时发送数据接收完成标志 rx_data_vld 连接到 din_vld。
- 控制模块主要是对数据进行缓存,串口发送模块发送 tx_rdy 标志信号,表示它要发送数据了,控制模块根据 tx_rdy 发送标志将数据由 dout[7:0] 发送到 tx_din[7:0],以及 dout_vld 发送标志位。
- 最后串口发送模块将数据发送出去。其中 2’h0 baud_set[1:0] 为波特率标志位。
- 这样就实现一个串口通讯回传,仔细理清一下思路就简单多了。
三、代码设计
顶层模块设计 uart.v:
- 顶层模块下有三个子模块:串口接收模块、控制模块、串口发送模块。
`define BAUD_115200 //`define BAUD_9600 module uart( input clk , input rst_n , input rx , output tx ); /* 波特率9600 19200 38400 115200 如果定义 BAUD_115200,那么 baud_sel 为 0,则波特率为 115200 如果定义 BAUD_9600,那么 baud_sel 为 3,则波特率为 9600 */ wire [1:0] baud_sel; `ifdef BAUD_115200 assign baud_sel = 0; `elsif BAUD_9600 assign baud_sel = 3; `endif //信号定义 wire [7:0] rx_data ; wire rx_data_vld ; wire [7:0] tx_data ; wire tx_data_vld ; wire tx_rdy ; uart_rx u_uart_rx( //接收模块 串并转换 .clk (clk ), .rst_n (rst_n ), .baud_sel (baud_sel ), .rx (rx ), .rx_data (rx_data ), .rx_data_vld(rx_data_vld) ); ctrl u_ctrl( .clk (clk ), .rst_n (rst_n ), .din (rx_data ), .din_vld (rx_data_vld), .dout (tx_data ), .dout_vld (tx_data_vld), .tx_rdy (tx_rdy ) ); uart_tx u_uart_tx( //发送模块 并串转换 .clk (clk ), .rst_n (rst_n ), .baud_sel (baud_sel ), .tx_din (tx_data ), .tx_din_vld (tx_data_vld), .tx_rdy (tx_rdy ), .tx (tx ) ); endmodule
- 顶层模块也就是对各个子模块进行接口连接,就没啥好讲的。
3.1 串口接收模块
uart_rx.v
module uart_rx( input clk , //时钟信号 input rst_n , //复位 input [1:0] baud_sel , //波特率标志 input rx , //数据输入口 output reg [7:0] rx_data , //接收的数据 output reg rx_data_vld //接收完成标志信号 ); //信号定义 reg [12:0] cnt_bps ; //波特率时钟周期计数器 reg add_flag ; //计数器使能信号,数据接收标志 reg [3:0] cnt_bit ; //比特计数器 reg [9:0] rx_data_r ; //接收数据寄存器 reg rx_r0 ; //同步 reg rx_r1 ; //打拍 检测下降沿 wire rx_nedge ; //接收信号的下降沿 reg [12:0] baud_bps ; //根据不同波特率选择的计数值 //波特率时钟周期计数器 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin cnt_bps <= 0; end else if(add_flag)begin //接收到使能信号,开始计数一个波特周期 if(cnt_bps == baud_bps - 1) cnt_bps <= 0; else cnt_bps <= cnt_bps + 1; end end //比特计数器 always @(posedge clk or negedge rst_n) begin if(!rst_n)begin cnt_bit <= 0; end else if(cnt_bps == baud_bps - 1)begin //计数 0-9 比特,第 0 比特为低电平起始信号 //第 1-8 比特为数据位 //第 9 比特为停止位 if(cnt_bit == 9 || rx_data_r[0] == 1'b1) //如果计满到了第 9 比特或者第 0 比特起始位为高电平 //那么就归零 //这里起始电平为高电平说明数据接收错误,也要归零 cnt_bit <= 0; else cnt_bit <= cnt_bit + 1; end end always @(posedge clk or negedge rst_n) begin if(!rst_n)begin add_flag <= 1'b0; end else if(rx_nedge)begin //检测到下降沿,那么使能信号拉高,标志有数据输入 add_flag <= 1'b1; end else if(cnt_bps == baud_bps-1 && (cnt_bit == 9 || rx_data_r[0] == 1'b1))begin //如果数据接收了 10 个比特或者起始电平为高(数据接受错误) //那么使能信号归零 add_flag <= 1'b0; end end //baud_bps always @(*) begin //根据不同的波特率选择不同的时钟周期 case(baud_sel) 0:baud_bps = 434; 1:baud_bps = 1302; 2:baud_bps = 2604; 3:baud_bps = 5208; default:baud_bps = 434; endcase end always @(posedge clk or negedge rst_n) begin if(!rst_n)begin rx_r0 <= 0; rx_r1 <= 0; end else begin rx_r0 <= rx; //同步 rx_r1 <= rx_r0; //打拍 end end //获取到下降沿 assign rx_nedge = ~rx_r0 & rx_r1; //rx_data_r always @ (posedge clk or negedge rst_n)begin if(!rst_n)begin rx_data_r <= 0; end else if(add_flag & cnt_bps == (baud_bps>>1))begin //如果使能信号且当前为一个波特周期的一半 //那么将此时 rx 的数据输入电平存储到数据寄存器中 //rx_data_r <= {rx,rx_data_r[9:1]}; //第一种表达方式 rx_data_r[cnt_bit] <= rx; //第二种表达方式 end end //rx_data always @(posedge clk or negedge rst_n)begin if(!rst_n)begin rx_data <= 0; end else begin //将数据寄存器中的值传给 rx_data //由 rx_data 传递接收数据给控制模块处理 rx_data <= rx_data_r[8:1]; end end //rx_data_vld always @(posedge clk or negedge rst_n)begin if(!rst_n)begin rx_data_vld <= 0; end else if(cnt_bps == baud_bps - 1 && cnt_bit == 9)begin //接收完毕后,传递完成接收标志信号 rx_data_vld <= 1'b1; end else begin rx_data_vld <= 1'b0; end end endmodule
程序执行过程:
- 假设接收的数据为 h96 ,串口接收的波形图如下(途中所有数字都为 16 进制):
- 当检测到下降沿后,add_flag 数据使能信号拉高,此时 add_flag 高电平期间就是对 rx 进行数据接收期间。
- 同时,比特计数器 cnt_bit 不断累加计满 10 个 bite,其中有 1 bit 起始位,8 bit 数据位,1 bit 停止位,这里没有设置校验位。
- 如果起始位为高电平,说明数据有问题,那么就不会计数,同时 add_flag 拉低。
- 当数据接收完毕后,rx_data_vld 数据接收完的标志位会拉高 1 个时钟周期,而数据寄存器会不断的接收数据,数值从 0 至 12D,这是因为程序中不断地接收第 cnt_bit 的数据放入寄存器中。
- 其中 rx_data_r 中的数据变化为([0]为低位,[9]为高位):
rx_data_r[0] | rx_data_r[1] | rx_data_r[2] | rx_data_r[3] | rx_data_r[4] | rx_data_r[5] | rx_data_r[6] | rx_data_r[7] | rx_data_r[8] | rx_data_r[9] | 十六进制 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2 |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 |
1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 9 |
0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 12 |
1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 25 |
1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 48 |
0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 96 |
1 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 12D |
- 最后可以看到从 rx_data_r[8]——[1] 的数据为:10010110,和发出来的数据一致。
- 图中这里 rx_data 为什么会慢一个周期,这里因为代码中只取 [8:1] 的数据,因为 rx_data_r[0] 为停止位,rx_data_r[9] 为起始位,[8:1] 才为数据位。
- 而 rx_data_r 第二次接收时,只有 rx_data_r[0] = 1,其余为 0,这里只取 [8:1] ,所以就慢了一个周期。
- 当数据接收完成后,会将 rx_data_vld 拉高一个周期,标志着数据接收完毕。
3.2 控制模块
ctrl.v:
- 控制模块中,主要是对接收到的数据做缓存处理。
module ctrl( input clk , //时钟信号 input rst_n , //复位 input [7:0] din , //接收到的数据 input din_vld , //数据有效性,高位有效,低位无效 input tx_rdy , //发送准备信号 output reg [7:0] dout , //数据输出 output reg dout_vld //数据有效性 ); //信号定义 reg rd_req ; //使能读操作 wire wr_req ; //使能写操作 wire empty ; //缓存空信号 wire full ; //缓存满信号 wire [7:0] q_dout ; //缓存输出数据 wire [3:0] usedw ; //缓存数据深度 reg rd_flag ; //rd_flag always @(posedge clk or negedge rst_n)begin if(!rst_n)begin rd_flag <= 0; end else if(usedw >= 4)begin //如果缓存数据深度大于等于 4 //拉高读标志 rd_flag <= 1'b1; end else if(empty)begin //如果缓存数据深度小于等于 0 //拉低读标志 rd_flag <= 1'b0; end end always @(*) begin if(rd_flag && tx_rdy) //如果读标志且输出数据标志位 //那么使能读缓存标志 rd_req = 1'b1; else rd_req = 1'b0; end //dout always @(posedge clk or negedge rst_n) begin if(!rst_n)begin dout <= 0; end else begin //从缓存中获取数据 dout <= q_dout; end end //dout_vld always @(posedge clk or negedge rst_n) begin if(!rst_n)begin dout_vld <= 1'b0; end else begin //使能数据输出标志 dout_vld <= rd_req; end end //fifo核,先进先出缓存 fifo fifo_inst ( .aclr (~rst_n ), .clock (clk ), .data (din ), .rdreq (rd_req ), .wrreq (wr_req ), .empty (empty ), .full (full ), .q (q_dout ), .usedw (usedw ) ); //如果缓存中没满并且有数据进来 //那么使能读操作 assign wr_req = full == 1'b0 && din_vld; endmodule
- 控制模块中使用了 FIFO 核,后面会讲述怎么设置 FIFO 核,其实这是一个缓存,将接收到的数据按照以 8 bit 为单位(这个可以自己设置,比如 7 bit、6 bit 等)进行存储,也就是说,类似于队列,先放进来的数据,会先发送出去。
- 这里设置的深度(最多存储多少个数据)为 4 位 bit,也就是 16 个。
wire [3:0] usedw ; //缓存数据深度
- 其中 empty 表示缓存为空的信号,空则为高电平,不空则为低电平。
wire empty ; //缓存空信号
程序执行过程:
- 首先来看时序图:
- 这里假设 rd_flag 一直保持高电平,表示缓存器中一直不为空且数据个数大于 4,至于为什么大于 4 ,看代码:
- 控制模块会不断地判断串口接收模块是否传数据过来并且缓存器中是否满了,如果没满并且有数据过来。
- 那么就会向缓存器中发送 wr_reg 写信号,此时,缓存器就可以将数据保存在内部,单独只讲数据通过 .data 接口进去,它是不会接收的,除非 wr_reg 为高电平才接收。
- 控制器还会不断判断 tx_rdy 使能信号,这是由串口发送端给的信号,表示它请求发送数据,那么为高电平。
- 这时让 rd_reg 信号使能,即让缓存器通过接口 p 吐出来一个数据,由控制模块中的 q_dout 接收到。
- 然后赋值给 dout,同时拉高一个周期的 dout_vld 表示有数据出来了。
3.3 串口发送模块
uart_tx.v
module uart_tx ( input clk , //时钟信号 input rst_n , //复位 input [1:0] baud_sel , //波特率标志 input [7:0] tx_din , //数据输入 input tx_din_vld , //数据输入标志位 output reg tx_rdy , //数据发送准备标志 output reg tx //数据发送 ); reg [12:0] cnt_bps ; //波特率周期计数器 reg add_flag ; //计数器使能信号 reg [3:0] cnt_bit ; //比特计数器 reg [9:0] tx_data_r ; //接收数据寄存器 reg [12:0] baud_bps ; //根据不同波特率选择的计数值 //计数器 always @(posedge clk or negedge rst_n) begin if(!rst_n)begin cnt_bps <= 0; end else if(add_flag)begin //计数器开始计数 //计满一个波特率周期 if(cnt_bps == baud_bps-1) cnt_bps <= 0; else cnt_bps <= cnt_bps + 1; end end always @(posedge clk or negedge rst_n) begin if(!rst_n)begin cnt_bit <= 0; end else if(cnt_bps == baud_bps-1)begin //对比特计数,共 10 位 if(cnt_bit == 9) cnt_bit <= 0; else cnt_bit <= cnt_bit + 1; end end always @(posedge clk or negedge rst_n) begin if(!rst_n)begin add_flag <= 1'b0; end else if(tx_din_vld)begin //数据发送使能信号 add_flag <= 1'b1; end else if(cnt_bit == 9 && cnt_bps == baud_bps-1)begin //数据发送完毕,则归零处理 add_flag <= 1'b0; end end //baud_bps always @(*) begin //波特率周期选择 case(baud_sel) 0:baud_bps = 434; 1:baud_bps = 1302; 2:baud_bps = 2604; 3:baud_bps = 5208; default:baud_bps = 434; endcase end always @(posedge clk or negedge rst_n) begin if(!rst_n)begin tx_data_r <= 0; end else if(tx_din_vld)begin //准备要发送的数据 //在数据两端添加起始位和停止位 tx_data_r <= {1'b1,tx_din,1'b0}; end end always @(posedge clk or negedge rst_n) begin if(!rst_n)begin tx <= 1'b1; end else if(add_flag && cnt_bps == 1)begin //发送数据 tx <= tx_data_r[cnt_bit]; end end //tx_rdy always @(*) begin if(tx_din_vld || add_flag) //使能数据准备发送标志 tx_rdy = 1'b0; else tx_rdy = 1'b1; end endmodule
程序执行过程:
- 先来看时序图:
- 其中 tx_din_vld 是控制模块输出的 dout_vld 使能信号,表示有数据可以输出,然后将 add_flag 拉高,开始进行数据的输出,然后使用 tx_data_r 将起始位、数据位、停止位按照协议拼接起来。
- 拼接完成后,由 tx 接口以 1 bit 为单位发送出去。
- 发送完毕后,将 add_flag 信号拉低,等待下一个 8 bit 数据传进来发送。
四、FIFO 核引用
- 我使用的是 Quartus 18.1,引用方法和低版本的不太一样,可以自行百度,我之前看过有挺多的,但是就是没有 18.1 的引用方法。
- 首先打开【Tools】→【IP Catalog】。
- 搜索框内输入【fifo】,然后双击打开 FIFO。
- 输入文件名(最后没有斜线),默认选择 Verilog ,然后点击【OK】。
- 选择数据宽度,本文是用的 8 bit;
- 数据深度,也就是最多缓存多少个数据;
- 缓存器的写操作和读操作的时钟频率是否相同,这里选择 Yes 相同,如果是在 fpga 上写数据,然后让 VGA 读数据,那么就要选择 No 不同了,因为两者所需的时钟频率是不同的。
- 然后点击【Next >】。
- 选择需要的管脚,这里默认这三个即可,点击【Next >】。
- 然后一直默认设置,点击【Next >】。
- 在下图界面时,勾选上倒数第二个文件,然后点击【Finish】完成。
- 在 fifo 同路径下,可以看到有一个 fifo_inst.v 文件,就是刚刚勾选的文件,打开后如下图所示,全部复制下来粘贴到要调用 fifo 内核的模块文件中,然后修改括号内的变量名即可只用 fifo 核了。
五、管脚定义及结果展示
管脚定义
- 按照自己的开发板原理图进行管脚配置,开发板不同,管脚也就不同
效果展示
- 由于今天挺晚了,且没有下载串口调试助手,等明天再补上结果 GIF 图。
附带整个项目文件:https://pan.baidu.com/s/174JjbUw1mYEUKV_VA3-DWQ——提取码:psdo
- 说明一下,打开工程时是选择 uart/prj/ 目录下的 uart.qpf 工程文件,即可打开整个项目。
这篇关于【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南