目录
- 一、相关知识
- 二、模块设计
- 三、代码设计
-
- 3.1 串口接收模块
- 3.2 控制模块
- 3.3 串口发送模块
- 四、FIFO 核引用
- 五、管脚定义及结果展示
【fpga|【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解】上一篇博文:【入门学习三】基于 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] 为波特率标志位。
- 这样就实现一个串口通讯回传,仔细理清一下思路就简单多了。
- 顶层模块下有三个子模块:串口接收模块、控制模块、串口发送模块。
`define BAUD_115200
//`define BAUD_9600module uart(
inputclk,
inputrst_n,inputrx,
outputtx
);
/*
波特率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;
wirerx_data_vld ;
wire[7:0]tx_data;
wiretx_data_vld ;
wiretx_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
- 顶层模块也就是对各个子模块进行接口连接,就没啥好讲的。
module uart_rx(
inputclk,//时钟信号
inputrst_n,//复位
input[1:0] baud_sel,//波特率标志
inputrx,//数据输入口outputreg[7:0] rx_data,//接收的数据
outputregrx_data_vld//接收完成标志信号
);
//信号定义reg[12:0]cnt_bps;
//波特率时钟周期计数器
regadd_flag;
//计数器使能信号,数据接收标志
reg[3:0]cnt_bit;
//比特计数器
reg[9:0]rx_data_r;
//接收数据寄存器regrx_r0;
//同步
regrx_r1;
//打拍 检测下降沿
wirerx_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
endalways @(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
endalways @(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
endendmodule
程序执行过程:
- 假设接收的数据为 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 拉高一个周期,标志着数据接收完毕。
- 控制模块中,主要是对接收到的数据做缓存处理。
module ctrl(
inputclk,//时钟信号
inputrst_n,//复位
input[7:0]din,//接收到的数据
inputdin_vld ,//数据有效性,高位有效,低位无效
inputtx_rdy,//发送准备信号
outputreg [7:0]dout,//数据输出
outputregdout_vld//数据有效性);
//信号定义
regrd_req;
//使能读操作
wirewr_req;
//使能写操作
wireempty;
//缓存空信号
wirefull;
//缓存满信号
wire [7:0]q_dout;
//缓存输出数据
wire [3:0]usedw;
//缓存数据深度
regrd_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
endalways @(*) 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 表示缓存为空的信号,空则为高电平,不空则为低电平。
wireempty;
//缓存空信号
程序执行过程:
- 首先来看时序图:
文章图片
- 这里假设 rd_flag 一直保持高电平,表示缓存器中一直不为空且数据个数大于 4,至于为什么大于 4 ,看代码:
文章图片
- 控制模块会不断地判断串口接收模块是否传数据过来并且缓存器中是否满了,如果没满并且有数据过来。
文章图片
- 那么就会向缓存器中发送 wr_reg 写信号,此时,缓存器就可以将数据保存在内部,单独只讲数据通过 .data 接口进去,它是不会接收的,除非 wr_reg 为高电平才接收。
文章图片
- 控制器还会不断判断 tx_rdy 使能信号,这是由串口发送端给的信号,表示它请求发送数据,那么为高电平。
文章图片
- 这时让 rd_reg 信号使能,即让缓存器通过接口 p 吐出来一个数据,由控制模块中的 q_dout 接收到。
文章图片
- 然后赋值给 dout,同时拉高一个周期的 dout_vld 表示有数据出来了。
文章图片
module uart_tx (
inputclk,//时钟信号
inputrst_n,//复位
input[1:0]baud_sel,//波特率标志
input[7:0]tx_din,//数据输入
inputtx_din_vld,//数据输入标志位
outputregtx_rdy,//数据发送准备标志
outputregtx//数据发送
);
reg[12:0]cnt_bps;
//波特率周期计数器
regadd_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
endalways @(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
endalways @(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
endalways @(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
endalways @(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;
endendmodule
程序执行过程:
- 先来看时序图:
文章图片
- 其中 tx_din_vld 是控制模块输出的 dout_vld 使能信号,表示有数据可以输出,然后将 add_flag 拉高,开始进行数据的输出,然后使用 tx_data_r 将起始位、数据位、停止位按照协议拼接起来。
文章图片
- 拼接完成后,由 tx 接口以 1 bit 为单位发送出去。
- 发送完毕后,将 add_flag 信号拉低,等待下一个 8 bit 数据传进来发送。
- 我使用的是 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 图。
- 说明一下,打开工程时是选择 uart/prj/ 目录下的 uart.qpf 工程文件,即可打开整个项目。
文章图片
文章图片
推荐阅读
- 基于 Cyclone IV 在 Quartus 中配置 IP 核中的 PLL、RAM 与 FIFO 的详细步骤及仿真验证
- Quartus 使用 tcl 文件快速配置管脚
- 简单接口开发|Verilog实现IIC协议读写EEPROM
- i2c|用verilog 实现的 i2c控制模块
- FPGA|FPGA实验记录五(I2C读取AHT10温湿度传感器)
- FPGA|FPGA实验记录四(基于FPGA的VGA协议实现)
- FPGA|FPGA基础协议二(I2C读写E2PROM)
- FPGA|FPGA图像处理(一)(边缘检测)
- FPGA技巧整理专栏|FPGA中串口通信的时钟频率和波特率计数