设计思想与代码规范均借鉴明德扬至简设计法,有不足之处希望大家多提建议,真正做到至简设计。本篇着重提出FPGA通用设计思想,以计数器为核心的代码规范以及VIVADO debug操作流程。
此次试验旨在通过串口试验,讲述FPGA的硬件设计思想和通用设计流程。串口是电子设计中非常常见,可以说掌握了串口数据收发,就明白了最基本的时序操作。串口的数据收发过程有其固定的数据格式。下面是本次实验使用的数据格式,在满足串口格式规范前提下是可变的:
空闲状态下为高电平,当发送数据时,先发送低电平起始位,后从低位开始逐位发送有效数据比特,数据位位数由双方约定,此处设定为8位。可在数据位后添加数据校验位,但这不是必须的。发送完后发送高电平停止位并持续空闲状态直至下一次发送。虽然本次实验没有用到,但这里简要讲一下奇偶校验的原理:
奇偶校验是一种非常简单常用的数据校验方式,分为奇校验和偶校验。奇校验需要保证传输的数据总共有奇数个逻辑高电平,若是偶校验则要保证传输的数据有偶数个逻辑高电平。即“奇偶”的意思就是数据中(包括该校验位)中1的个数。例如:传输的数据位是0100_0011。如果是奇校验,校验位是0,偶校验校验位是1。
在串口通信中,波特率是一个非常重要的概念。串口通信中常用的波特率是9600、19200、38400、57600、115200。波特率是每个码元传输的速率,在二进制数据传输中,和比特率相同,都是每个比特数据传输的速率,其倒数为1bit数据的位宽,也就是1bit数据持续的时间。有了这一时间段,就可用FPGA构造计数器实现比特周期的延时,从而实现特定的数据传输波特率。
有了这些预备知识,我们开始设计串口发送模块。第一步要明确设计目的:要设计的模块功能当一个时钟周期使能信号有效时,将输入数据通过串口发送给PC机。后续可以通过FIFO缓存数据,实现多个数据的发送。知道设计目的后,通常要开始根据大体功能进行模块划分,模块之间的接口定义以及各模块内部的硬件设计。本次实验只有一个模块,所以直接从模块接口定义开始。每个模块都要有必要的时钟和复位输入,另外串口发送模块需要确保数据不重复发送,因此要有发送使能信号。为了满足不同速率需求,需要波特率设定输入信号来选通不同的波特率。最重要的是待发送数据输入端口。发送侧要有数据串行输出端口和发送完成指示输出。综上,串口发送模块接口示意图如下:
现在开始模块内部功能的硬件实现。首先需要一个参数可变的分频计数器满足不同波特率要求。为此需要一个查找表结构对输入的波特率设定指令进行译码,改变计数器参数。然后要将数据进行并串转换可以通过一个比特位计数器控制数据选择器实现,这样可以将发送比特位数与待发送数据位数相对应。至于发送完成指示信号只需根据比特计数器的数值改变即可。在设计代码前先画出主要信号的时序波形图有助于理清思路:(此处假设比特计数器每个时钟周期计数一次便于画图)
到目前为止最重要的设计工作已经做完了,接下来的代码编写也就没有任何难度可言。
串口发送模块代码:
1 `timescale 1ns / 1ps 2 3 module uart_tx( 4 input clk, 5 input rst_n, 6 input [2:0] baud_set, 7 input send_en, 8 input [7:0] data_in, 9 10 output reg data_out, 11 output tx_done 12 ); 13 14 reg [15:0] CYC; 15 reg [15:0] cnt_div; 16 (*mark_debug = "true"*)reg [3:0] cnt_bit; 17 reg add_flag; 18 19 wire add_cnt_div; 20 (*mark_debug = "true"*)wire end_cnt_div; 21 wire add_cnt_bit,end_cnt_bit; 22 23 //分频计数器 24 always@(posedge clk or negedge rst_n)begin 25 if(!rst_n) 26 cnt_div <= 0; 27 else if(add_cnt_div)begin 28 if(end_cnt_div) 29 cnt_div <= 0; 30 else 31 cnt_div <= cnt_div + 1'b1; 32 end 33 end 34 35 assign add_cnt_div = add_flag; 36 assign end_cnt_div = add_cnt_div && cnt_div == CYC - 1; 37 38 //比特位数计数器 39 always@(posedge clk or negedge rst_n)begin 40 if(!rst_n) 41 cnt_bit <= 0; 42 else if(add_cnt_bit)begin 43 if(end_cnt_bit) 44 cnt_bit <= 0; 45 else 46 cnt_bit <= cnt_bit + 1'b1; 47 end 48 end 49 50 assign add_cnt_bit = end_cnt_div; 51 assign end_cnt_bit = add_cnt_bit && cnt_bit == 10 - 1; 52 53 //发送使能后分频计数器开始计数,直到将起始位、数据位、停止位发送完成为止 54 always@(posedge clk or negedge rst_n)begin 55 if(!rst_n) 56 add_flag <= 0; 57 else if(send_en) 58 add_flag <= 1; 59 else if(end_cnt_bit) 60 add_flag <= 0; 61 end 62 //波特率查找表 63 always@(*)begin 64 case(baud_set) 65 3'b000:CYC <= 20833;//9600 66 3'b001:CYC <= 10417;//19200 67 3'b010:CYC <= 5208;//38400 68 3'b011:CYC <= 3472;//57600 69 3'b100:CYC <= 1736;//115200 70 default:CYC <= 20833;//9600 71 endcase 72 end 73 //根据比特计数器得到对应比特位 74 always@(posedge clk or negedge rst_n)begin 75 if(!rst_n) 76 data_out <= 1; 77 else if(send_en) 78 data_out <= 0; 79 else if(add_cnt_bit && cnt_bit >= 0 && cnt_bit < 8) 80 data_out <= data_in[cnt_bit]; 81 else if((add_cnt_bit && cnt_bit == 8) || end_cnt_bit) 82 data_out <= 1;//结束位或者空闲状态均为高电平 83 end 84 85 assign tx_done = end_cnt_bit; 86 87 endmodule
现编写测试激励,观察仿真波形是否与预期一致:
1 `timescale 1ns / 1ps 2 3 module uart_tx_tb; 4 5 reg clk,rst_n; 6 reg [2:0] baud_set; 7 reg send_en; 8 reg [7:0] data_in; 9 10 wire data_out; 11 wire tx_done; 12 13 uart_tx uart_tx( 14 .clk(clk), 15 .rst_n(rst_n), 16 .baud_set(baud_set),//[2:0] 17 .send_en(send_en), 18 .data_in(data_in),//[7:0] 19 20 .data_out(data_out), 21 .tx_done(tx_done) 22 ); 23 24 parameter CYCLE = 5, 25 RST_TIME = 2; 26 27 initial begin 28 clk = 0; 29 forever #(CYCLE / 2) clk = ~clk; 30 end 31 32 initial begin 33 rst_n = 1; 34 #1; 35 rst_n = 0; 36 #(CYCLE * RST_TIME); 37 rst_n = 1; 38 end 39 40 initial begin 41 baud_set = 3'b000; 42 send_en = 0; 43 data_in = 0; 44 #1; 45 #(CYCLE * RST_TIME); 46 #(CYCLE * 10); 47 send_en = 1; 48 data_in = 8'b0101_0110; 49 #(CYCLE * 1); 50 send_en = 0; 51 #2_000_000; 52 $stop; 53 end 54 55 endmodule
仿真波形如下:
可以看出该模块真确将待发送数据8'b0101_0110 按照串口数据格式发送了出去,分频计数器计数完成后分别发送了0_0110_1010_1.此刻,串口发送模块逻辑功能验证完毕。为了在开发板中运行,添加按键消抖模块,将按键有效输出信号作为发送模块的发送使能,并建立顶层模块。按键消抖模块在上一篇博文中已详细讲述,仅稍作改动调用。下面是顶层模块:
1 `timescale 1ns / 1ps 2 3 module send_data_top( 4 input sys_clk_p, 5 input sys_clk_n, 6 input rst_n, 7 input key, 8 output dout, 9 output tx_done_out 10 ); 11 (*mark_debug = "true"*)wire tx_done; 12 (*mark_debug = "true"*)wire key_en; 13 // 差分时钟转单端时钟 14 // IBUFGDS是IBUFG差分形式,当信号从一对差分全局时钟引脚输入时,必须使用IBUFGDS作为全局时钟输入缓冲 15 wire sys_clk_ibufg; 16 IBUFGDS # 17 ( 18 .DIFF_TERM ("FALSE"), 19 .IBUF_LOW_PWR ("FALSE") 20 ) 21 u_ibufg_sys_clk 22 ( 23 .I (sys_clk_p), //差分时钟的正端输入,需要和顶层模块的端口直接连接 24 .IB (sys_clk_n), // 差分时钟的负端输入,需要和顶层模块的端口直接连接 25 .O (sys_clk_ibufg) //时钟缓冲输出 26 ); 27 28 key_jitter key_jitter( 29 .clk(sys_clk_ibufg), 30 .rst_n(rst_n), 31 .key_i(key), 32 .key_vld(key_en) 33 ); 34 35 uart_tx uart_tx( 36 .clk(sys_clk_ibufg), 37 .rst_n(rst_n), 38 .baud_set(3'b000),//[2:0] 39 .send_en(key_en), 40 .data_in(8'h32),//[7:0] 41 42 .data_out(dout), 43 .tx_done(tx_done)); 44 45 assign tx_done_out = ~tx_done; 46 47 48 endmodule
打开分析后的设计原理图,方便地观察设计整体结构:
HDL代码设计完毕,后需添加约束文件,这里只需为每个端口添加对应的端口号和电平标准即可。注意:当某个信号为多个位时,在后边的方括号内需要用大括号把每一位信号括起来,如:set_property PACKAGE A5 [{led[0]}]
仿真只是通过软件来模拟硬件的场景,尤其在只做了最理想情况下的行为仿真时,并不能完全的体现出所有硬件特性,这时就要进行“在线调试”,也就是使用嵌入式逻辑分析仪,直接抓取芯片内部真实运行的信号数值。它的基本原理是通过IP核的形式嵌入到FPGA芯片内部,不断将要观测数据存入RAM中,当触发条件有效时,停止检测并将信号数据以类似仿真波形的形式显示出来。那么如何选择所要观测的信号呢?观察上面的HDL代码会发现,某些信号定义之前有(*mark_debug = "true"*)。这就是“抓取信号”的方式,在信号定义之前加上这条语句之后,点击Run synthesis,并打开综合后的设计。打开调试界面,点击Set Up Debug 执行ILA调试IP核的生成向导。之前被标注的信号已经自动添加了进来,当然,你可以添加更多的需要观测的信号。
Run implementation并生成比特流后,打开硬件管理器,并自动连接开发板下载比特流。此时debug probles file也同时被加载进来:
下载完毕后debug界面自动打开:
按照图中数字的顺序依次完成抓取模式设置,设置触发条件,启动触发,观测波形。2中设置key_en为高电平时启动触发,观察核心信号数据。
可以看出key_en高电平后发送“0”。由于设置RAM深度太小,导致没有观察到串口数据完整格式。再次将触发条件改为tx_done高电平触发,并修改触发条件所在观测窗口的位置:
tx_done高电平之前比特计数器正确计数到9,tx_done高电平之后一个时钟周期计数值变为0,证明内部逻辑功能正常运行。也可以自行回到综合后界面,再次打开Set Up Debug界面修改数据采样深度观察完整波形:
此时观察串口调试助手,设置好波特率和数据格式,将显示方式设定为16进制。打开串口后,按下按键并松手后,串口调试助手接收到一个8位数据,这里固定让其发送数字8'h32,以下是按两次按键收到的数据:
到此,串口发送模块已设计完毕,将ILA IP核的标注和相关约束去掉可节省逻辑资源。