【小梅哥FPGA进阶教程】第九章 基于串口猎人软件的串口示波器
九、基于串口猎人软件的串口示波器
1、实验介绍
本实验,为芯航线开发板的综合实验,该实验利用芯航线开发板上的ADC、独立按键、UART等外设,搭建了一个具备丰富功能的数据采集卡,芯航线开发板负责进行数据的采集并将数据通过串口发送到PC机上,PC端,利用强大的串口调试工具——串口猎人,来实现数据的接收分析,并将数据分别以波形、码表、柱状图的形式动态显示出来,以让使用者能够直观的看到ADC采集到的信号细节。同时,用户也可以使用串口猎人通过串口给下位机(FPGA)发送指令,下位机将对接收到的指令进行解码,然后依据解码结果来配置FPGA中各个子模块的控制寄存器,以实现通过串口控制FPGA中子模块工作状态的功能。
本实验中,涉及到的应用模块和知识点如下所示:
串口收发模块的设计和使用;
串口收发模块仿真模型的设计;
串口简单数据帧的解码;
串口帧转Memory Mapped总线的设计;
Memory Mapped Slave模块的设计;
线性序列机设计思想的应用(ADC驱动);
独立按键消抖的分析与实现;
直接数字频率合成(DDS)的设计与实现;
使能时钟对系统间模块协调工作的重要性;
串口猎人的详细使用;
完整系统的仿真验证设计;
头文件在设计中的运用;
Quartus II软件中可定制化存储器ROM的使用;
本实验不仅注重可综合的代码编写,同时更注重代码的仿真验证。通过仿真,我们能够寻找设计中可能存在的问题并修正。最终,在整个系统仿真无误的基础上,下载到开发板上一次性成功。
2、系统结构
下图为本设计的框架结构图:
系统采用模块化设计,在模块划分的过程中,重点考虑了系统的可扩展性,下表为对系统中各模块功能的简单介绍。
系统中各端口和信号的功能介绍如下:
本实验为综合性实验,代码量较大,因此这里只针对部分代码进行讲解。如果文档中没有讲到的内容,大家可以参看代码注释。
模块详解
3.1 Tx_Bps_Gen
Tx_Bps_Gen为发送波特率生成模块,每当有Byte_En信号到来时,即开始产生发送一个完整字节的数据需要的完整波特率时钟信号。
本设计,波特率支持9600bps到921600bps。例如,需要产生的波特率时钟为9600bps,即波特率时钟频率为9600Hz,周期为104.17us。生成9600Hz波特率时钟的核心思想就是对系统时钟进行计数,这里设定系统时钟为50MHz,则一个时钟的周期为20ns,我们只需要对系统时钟计数5208次,每计数5208次产生一个时钟周期的高电平脉冲,即可实现生成9600Hz波特率时钟的功能。相应代码如下所示:
018 parameter system_clk = 50_000_000; /*输入时钟频率设定,默认50M*/ 019 020 /*根据输入时钟频率计算生成各波特率时分频计数器的计数最大值*/ 021 localparam bps9600 = system_clk/9600 - 1; 022 localparam bps19200 = system_clk/19200 - 1; 023 localparam bps38400 = system_clk/38400 - 1; 024 localparam bps57600 = system_clk/57600 - 1; 025 localparam bps115200 = system_clk/115200 - 1; 026 localparam bps230400 = system_clk/230400 - 1; 027 localparam bps460800 = system_clk/460800 - 1; 028 localparam bps921600 = system_clk/921600 - 1; 029 030 reg [31:0]BPS_PARA;/*波特率分频计数器的计数最大值*/ 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 BPS_PARA <= bps9600;/*复位时波特率默认为9600bps*/ 035 end 036 else begin 037 case(Baud_Set)/*根据波特率控制信号选择不同的波特率计数器计数最大值*/ 038 3'd0: BPS_PARA <= bps9600; 039 3'd1: BPS_PARA <= bps19200; 040 3'd2: BPS_PARA <= bps38400; 041 3'd3: BPS_PARA <= bps57600; 042 3'd4: BPS_PARA <= bps115200; 043 3'd5: BPS_PARA <= bps230400; 044 3'd6: BPS_PARA <= bps460800; 045 3'd7: BPS_PARA <= bps921600; 046 default: BPS_PARA <= bps9600; 047 endcase 048 end 049 050 //========================================================= 051 reg[12:0]Count; 052 053 reg n_state; 054 localparam IDEL_1 = 1'b0, 055 SEND = 1'b1; 056 057 reg BPS_EN; 058 059 /*-------波特率时钟生成控制逻辑--------------*/ 060 always@(posedge Clk or negedge Rst_n) 061 if(!Rst_n)begin 062 BPS_EN <= 1'b0; 063 n_state <= IDEL_1; 064 end 065 else begin 066 case(n_state) 067 IDEL_1: 068 if(Byte_En)begin/*检测到字节发送使能信号,则启动波特率生成进程,同时进入发送状态*/ 069 BPS_EN <= 1'b1; 070 n_state <= SEND; 071 end 072 else begin 073 n_state <= IDEL_1; 074 BPS_EN <= 1'b0; 075 end 076 SEND: 077 if(Tx_Done == 1)begin/*发送完成,关闭波特率生成进程,回到空闲状态*/ 078 BPS_EN <= 1'b0; 079 n_state <= IDEL_1; 080 end 081 else begin 082 n_state <= SEND; 083 BPS_EN <= 1'b1; 084 end 085 default:n_state <= IDEL_1; 086 endcase 087 end 088 089 /*-------波特率时钟生成定时器--------------*/ 090 always@(posedge Clk or negedge Rst_n) 091 if(!Rst_n) 092 Count <= 13'd0; 093 else if(BPS_EN == 1'b0) 094 Count <= 13'd0; 095 else begin 096 if(Count == BPS_PARA) 097 Count <= 13'd0; 098 else 099 Count <= Count + 1'b1; 100 end 101 102 /*输出数据接收采样时钟*/ 103 //----------------------------------------------- 104 always @(posedge Clk or negedge Rst_n) 105 if(!Rst_n) 106 Bps_Clk <= 1'b0; 107 else if(Count== 1) 108 Bps_Clk <= 1'b1; 109 else 110 Bps_Clk <= 1'b0;
第18行“parameter system_clk = 50_000_000;”,这里用一个全局参数定义了系统时钟,暂时设定为50M,可根据实际使用的板卡上的工作时钟进行修改。
所谓波特率生成,就是用一个定时器来定时,产生频率与对应波特率时钟频率相同的时钟信号。例如,我们使用波特率为115200bps,则我们需要产生一个频率为115200Hz的时钟信号。那么如何产生这样一个115200Hz的时钟信号呢?这里,我们首先将115200Hz时钟信号的周期计算出来,1秒钟为1000_000_000ns,因此波特率时钟的周期Tb= 1000000000/115200 =8680.6ns,即115200信号的一个周期为8680.6ns,那么,我们只需要设定我们的定时器定时时间为8680.6ns,每当定时时间到,产生一个系统时钟周期长度的高脉冲信号即可。系统时钟频率为50MHz,即周期为20ns,那么,我们只需要计数8680/20个系统时钟,就可获得8680ns的定时,即bps115200=Tb/Tclk - 1=Tb*fclk - 1=fclk/115200-1。相应的,其它波特率定时值的计算与此类似,这里小梅哥就不再一一分析。20行至28行为波特率定时器定时值的计算部分。
为了能够通过外部控制波特率,设计中使用了一个3位的波特率选择端口:Baud_Set。通过给此端口不同的值,就能选择不同的波特率,此端口控制不同波特率的原理很简单,就是一个多路选择器,第32行至第48行即为此多路选择器的控制代码, Baud_Set的值与各波特率的对应关系如下:
000 : 9600bps;
001 : 19200bps;
010 :38400bps;
011 :57600bps;
100 :115200bps;
101 :230400bps;
110 :460800bps;
111 :921600bps;
3.2 Uart_Byte_Tx
Uart_Byte_Tx为字节发送模块,该模块在波特率时钟的节拍下,依照UART通信协议发送一个完整的字节的数据。当一个字节发送完毕后,Tx_Done产生一个高脉冲信号,以告知其它模块或逻辑一个字节的数据已经传输完成,可以开始下一个字节的发送了。其发送一个字节数据的实现代码如下:
33 /*计数波特率时钟,11个波特率时钟为一次完整的数据发送过程*/ 34 always@(posedge Clk or negedge Rst_n) 35 if(!Rst_n) 36 Bps_Clk_Cnt <= 4'b0; 37 else if(Bps_Clk_Cnt == 4'd11) 38 Bps_Clk_Cnt <= 4'b0; 39 else if(Bps_Clk) 40 Bps_Clk_Cnt <= Bps_Clk_Cnt + 1'b1; 41 else 42 Bps_Clk_Cnt <= Bps_Clk_Cnt; 43 44 /*生成数据发送完成标志信号*/ 45 always@(posedge Clk or negedge Rst_n) 46 if(!Rst_n) 47 Tx_Done <= 1'b0; 48 else if(Bps_Clk_Cnt == 4'd11) 49 Tx_Done <= 1'b1; 50 else 51 Tx_Done <= 1'b0; 52 53 /*在开始发送起始位的时候就读取并寄存Data_Byte,以免Data_Byte变化导致数据的丢失*/ 54 always@(posedge Clk or negedge Rst_n) 55 if(!Rst_n) 56 Data = 8'd0; 57 else if(Bps_Clk & Bps_Clk_Cnt == 4'd1) 58 Data <= Data_Byte; 59 else 60 Data <= Data; 61 62 /*发送数据序列机*/ 63 always@(posedge Clk or negedge Rst_n) 64 if(!Rst_n) 65 Rs232_Tx <= 1'b1; 66 else begin 67 case(Bps_Clk_Cnt) 68 4'd1: Rs232_Tx <= 1'b0; 69 4'd2: Rs232_Tx <= Data[0]; 70 4'd3: Rs232_Tx <= Data[1]; 71 4'd4: Rs232_Tx <= Data[2]; 72 4'd5: Rs232_Tx <= Data[3]; 73 4'd6: Rs232_Tx <= Data[4]; 74 4'd7: Rs232_Tx <= Data[5]; 75 4'd8: Rs232_Tx <= Data[6]; 76 4'd9: Rs232_Tx <= Data[7]; 77 4'd10: Rs232_Tx <= 1'b1; 78 default:Rs232_Tx <= 1'b1; 79 endcase 80 end
在UART协议中,一个完整的字节包括一位起始位、8位数据位、一位停止位即总共十位数据,那么,要想完整的实现这十位数据的发送,就需要11个波特率时钟脉冲,如下所示:
BPS_CLK信号的第一个上升沿到来时,字节发送模块开始发送起始位,接下来的2到9个上升沿,发送8个数据位,第10个上升沿到第11个上升沿为停止位的发送。
3.3 Uart_Byte_Rx
单个串口接收模块中实现串口数据接收的主要代码如下所示:
025 always @ (posedge Clk or negedge Rst_n) 026 if(!Rst_n) begin 027 Rs232_Rx0 <= 1'b0; 028 Rs232_Rx1 <= 1'b0; 029 Rs232_Rx2 <= 1'b0; 030 Rs232_Rx3 <= 1'b0; 031 end 032 else begin 033 Rs232_Rx0 <= Rs232_Rx; 034 Rs232_Rx1 <= Rs232_Rx0; 035 Rs232_Rx2 <= Rs232_Rx1; 036 Rs232_Rx3 <= Rs232_Rx2; 037 end 038 039 wire neg_Rs232_Rx= Rs232_Rx3 & Rs232_Rx2 & ~Rs232_Rx1 & ~Rs232_Rx0; 040 041 assign Byte_En = neg_Rs232_Rx; 042 043 /*----------计数采样时钟--------------*/ 044 /*9倍波特率采样时钟,故一个完整的接收过程有90个波特率时钟*/ 045 reg[6:0]Sample_Clk_Cnt; 046 always @ (posedge Clk or negedge Rst_n) 047 if(!Rst_n) 048 Sample_Clk_Cnt <= 7'd0; 049 else if(Sample_Clk)begin 050 if(Sample_Clk_Cnt == 7'd89) 051 Sample_Clk_Cnt <= 7'd0; 052 else 053 Sample_Clk_Cnt <= Sample_Clk_Cnt + 1'b1; 054 end 055 else 056 Sample_Clk_Cnt <= Sample_Clk_Cnt; 057 058 reg [1:0]Start_Bit; /*起始位,这里虽然定义,但并未使用该位来判断接收数据的正确性,即默认接收都是成功的*/ 059 reg [1:0]Stop_Bit; /*停止位,这里虽然定义,但并未使用该位来判断接收数据的正确性,即默认接收都是成功的*/ 060 reg [1:0] Data_Tmp[7:0];/*此部分较为复杂,请参看说明文档中相关解释*/ 061 062 always @ (posedge Clk or negedge Rst_n) 063 if(!Rst_n)begin 064 Data_Tmp[0] <= 2'd0; 065 Data_Tmp[1] <= 2'd0; 066 Data_Tmp[2] <= 2'd0; 067 Data_Tmp[3] <= 2'd0; 068 Data_Tmp[4] <= 2'd0; 069 Data_Tmp[5] <= 2'd0; 070 Data_Tmp[6] <= 2'd0; 071 Data_Tmp[7] <= 2'd0; 072 Start_Bit <= 2'd0; 073 Stop_Bit <= 2'd0; 074 end 075 else if(Sample_Clk)begin 076 case(Sample_Clk_Cnt) 077 7'd0: 078 begin 079 Data_Tmp[0] <= 2'd0; 080 Data_Tmp[1] <= 2'd0; 081 Data_Tmp[2] <= 2'd0; 082 Data_Tmp[3] <= 2'd0; 083 Data_Tmp[4] <= 2'd0; 084 Data_Tmp[5] <= 2'd0; 085 Data_Tmp[6] <= 2'd0; 086 Data_Tmp[7] <= 2'd0; 087 Start_Bit <= 2'd0; 088 Stop_Bit <= 2'd0; 089 end 090 7'd3,7'd4,7'd5: Start_Bit <= Start_Bit + Rs232_Rx; 091 7'd12,7'd13,7'd14:Data_Tmp[0] <= Data_Tmp[0] + Rs232_Rx; 092 7'd21,7'd22,7'd23:Data_Tmp[1] <= Data_Tmp[1] + Rs232_Rx; 093 7'd30,7'd31,7'd32:Data_Tmp[2] <= Data_Tmp[2] + Rs232_Rx; 094 7'd39,7'd40,7'd41:Data_Tmp[3] <= Data_Tmp[3] + Rs232_Rx; 095 7'd48,7'd49,7'd50:Data_Tmp[4] <= Data_Tmp[4] + Rs232_Rx; 096 7'd57,7'd58,7'd59:Data_Tmp[5] <= Data_Tmp[5] + Rs232_Rx; 097 7'd66,7'd67,7'd68:Data_Tmp[6] <= Data_Tmp[6] + Rs232_Rx; 098 7'd75,7'd76,7'd77:Data_Tmp[7] <= Data_Tmp[7] + Rs232_Rx; 099 7'd84,7'd85,7'd86:Stop_Bit <= Stop_Bit + Rs232_Rx; 100 default:; 101 endcase 102 end 103 else ;
根据串口发送协议,一个字节的数据传输是以一个波特率周期的低电平作为起始位的,因此,成功接收UART串口数据的核心就是准确检测起始位。由于外部串口发送过来的数据与接收系统不在同一个时钟域,因此不能直接使用该信号的下降沿来作为检测标志,我们需要在fpga中,采用专用的边沿检测电路来实现,第25行至37行通过四个移位寄存器,存储连续四个时钟上升沿时外部发送数据线的状态,第39行通过比较前两个时钟时数据线的状态与后两个时钟时数据线的状态,来得到该数据线的准确下降沿,以此保证起始位的准确检测。
在简单的串口接收中,我们通常选取一位数据的中间时刻进行采样,因为此时数据最稳定,但是在工业环境中,存在着各种干扰,在干扰存在的情况下,如果采用传统的中间时刻采样一次的方式,采样结果就有可能受到干扰而出错。为了滤除这种干扰,这里采用多次采样求概率的方式。如下图,将一位数据平均分成9个时间段,对位于中间的三个时间段进行采样。然后对三个采样结果进行统计判断,如果某种电平状态在三次采样结果中占到了两次及以上,则可以判定此电平状态即为正确的数据电平。例如4、5、6时刻采样结果分别为1、1、0,那么就取此位解码结果为1,否则,若三次采样结果为0、1、0,则解码结果就为0。
因为采样一位需要9个时钟上升沿,因此,采样一个完整的数据需要10*9,即90个时钟上升沿,这里,采样时钟为波特率时钟的9倍。产生采样时钟的部分代码如下所示:
089 /*-------波特率时钟生成定时器--------------*/ 090 always@(posedge Clk or negedge Rst_n) 091 if(!Rst_n) 092 Count <= 10'd0; 093 else if(BPS_EN == 1'b0) 094 Count <= 10'd0; 095 else begin 096 if(Count == BPS_PARA) 097 Count <= 10'd0; 098 else 099 Count <= Count + 1'b1; 100 end 101 102 //===================================================== 103 /*输出数据接收采样时钟*/ 104 always @(posedge Clk or negedge Rst_n) 105 if(!Rst_n) 106 Sample_Clk <= 1'b0; 107 else if(Count== 1) 108 Sample_Clk <= 1'b1; 109 else 110 Sample_Clk <= 1'b0;
这里,BPS_PARA的计算原理和前面Tx_Bps_Gen模块中的BPS_PARA的计算原理一致,不过这里,因为采样时钟为波特率时钟的9倍,所以,BPS_PARA为Tx_Bps_Gen模块中的BPS_PARA的1/9。计算BPS_PARA的相关代码如下:
018 parameter system_clk = 50_000_000; /*输入时钟频率设定,默认50M*/ 019 020 /*根据输入时钟频率计算生成各波特率时分频计数器的计数最大值*/ 021 localparam bps9600 = system_clk/9600/9 - 1; 022 localparam bps19200 = system_clk/19200/9 - 1; 023 localparam bps38400 = system_clk/38400/9 - 1; 024 localparam bps57600 = system_clk/57600/9 - 1; 025 localparam bps115200 = system_clk/115200/9 - 1; 026 localparam bps230400 = system_clk/230400/9 - 1; 027 localparam bps460800 = system_clk/460800/9 - 1; 028 localparam bps921600 = system_clk/921600/9 - 1; 029 030 reg [31:0]BPS_PARA;/*波特率分频计数器的计数最大值*/ 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 BPS_PARA <= bps9600; /*复位时波特率默认为9600bps*/ 035 end 036 else begin 037 case(Baud_Set) /*根据波特率控制信号选择不同的波特率计数器计数最大值*/ 038 3'd0: BPS_PARA <= bps9600; 039 3'd1: BPS_PARA <= bps19200; 040 3'd2: BPS_PARA <= bps38400; 041 3'd3: BPS_PARA <= bps57600; 042 3'd4: BPS_PARA <= bps115200; 043 3'd5: BPS_PARA <= bps230400; 044 3'd6: BPS_PARA <= bps460800; 045 3'd7: BPS_PARA <= bps921600; 046 default: BPS_PARA <= bps9600;/*异常情况,恢复到9600的波特率*/ 047 endcase 048 end
3.4 CMD
CMD模块为串口数据帧接收与解析模块,该模块负责对串口接收到的每一帧的数据进行解码判断,并从数据帧中提取出地址字节和数据字节。最后将地址字节和数据字节转换为类似于Avalon-MM形式的总线,以实现对其它模块的控制寄存器的读写,从而实现通过串口控制FPGA中各个模块工作的目的。
在工业应用中,串口指令大多以数据帧的格式出现,包含帧头、帧长、帧命令、帧内容、校验和以及帧尾,不会只是单纯的传输数据。在这个实验中,小梅哥也使用了数据帧的形式来通过上位机向FPGA发送命令,不过这里我使用的帧格式非常简单,帧格式以帧头、帧长、帧内容以及帧尾组成,忽略了校验部分内容,帧头、帧长以及帧尾内容都是固定的,不固定的只是帧内容,以下为小梅哥的设计中一帧数据的格式:
由于数据帧本身结构简单,因此数据帧的解析过程也相对简洁,以下为小梅哥的数据帧解析状态机设计,该状态机分为帧头解析、帧长解析、数据接收以及帧尾解析。默认时,状态机处于帧头解析状态,一旦出现帧头数据,则跳转到帧长接收状态,若下一个字节为帧长数据(这里严格意义上并不能算作帧长,因为长度固定,充其量只能算作帧头,读者不须过分纠结),则开始连续接收三个字节的数据,若非指定的帧长内容,则表明这是一次无关传输,状态机将返回到帧头解析状态继续等待新的数据帧到来。在帧尾解析状态,若解析到的数据并非指定的帧尾数据,则表明此次数据帧非有效帧,则将此帧已解析到的数据舍弃。若为帧尾数据,则解析成功,产生命令有效标志信号(CMD_Valid),Memory Mapped 总线进程在检测到此命令有效信号后,即产生写外设寄存器操作。
命令解析的状态机实现代码如下所示:
017 localparam 018 Header = 8'hAA, /*帧头*/ 019 Length = 8'd3, /*帧长*/ 020 Tail = 8'h88; /*帧尾*/ 021 022 /*----------状态定义-----------------*/ 023 localparam 024 CMD_HEADER = 6'b00_0001, 025 CMD_LENGTH = 6'b00_0010, 026 CMD_DATAA = 6'b00_0100, 027 CMD_DATAB = 6'b00_1000, 028 CMD_DATAC = 6'b01_0000, 029 CMD_TAIL = 6'b10_0000; 030 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 reg_CMD_DATA <= 24'd0; 035 CMD_Valid <= 1'b0; 036 state <= CMD_HEADER; 037 end 038 else if(Rx_Int)begin 039 case(state) 040 CMD_HEADER: /*解码帧头数据*/ 041 if(Rx_Byte == Header) 042 state <= CMD_LENGTH; 043 else 044 state <= CMD_HEADER; 045 046 CMD_LENGTH: /*解码帧长数据*/ 047 if(Rx_Byte == Length) 048 state <= CMD_DATAA; 049 else 050 state <= CMD_HEADER; 051 052 CMD_DATAA: /*解码数据A*/ 053 begin 054 reg_CMD_DATA[23:16] <= Rx_Byte; 055 state <= CMD_DATAB; 056 end 057 058 CMD_DATAB: /*解码数据B*/ 059 begin 060 reg_CMD_DATA[15:8] <= Rx_Byte; 061 state <= CMD_DATAC; 062 end 063 064 CMD_DATAC: /*解码数据C*/ 065 begin 066 reg_CMD_DATA[7:0] <= Rx_Byte; 067 state <= CMD_TAIL; 068 end 069 070 CMD_TAIL: /*解码帧尾数据*/ 071 if(Rx_Byte == Tail)begin 072 CMD_Valid <= 1'b1; /*解码成功,发送解码数据有效标志*/ 073 state <= CMD_HEADER; 074 end 075 else begin 076 CMD_Valid <= 1'b0; 077 state <= CMD_HEADER; 078 end 079 default:; 080 endcase 081 end 082 else begin 083 CMD_Valid <= 1'b0; 084 reg_CMD_DATA <= reg_CMD_DATA; 085 end
第23行到第29行为状态机编码,这里采用独热码的编码方式。状态机的编码方式有很多种,包括二进制编码、独热码、格雷码等,二进制编码最接近我们的常规思维,但是在FPGA内部,其译码电路较为复杂,且容易出现竞争冒险,导致使用二进制编码的状态机最高运行速度相对较低。独热码的译码电路最简单,因此采用独热码方式编码的状态机运行速度较二进制编码方式高很多,但是编码会占用较多的数据位宽。格雷码以其独特的编码特性,能够非常完美的解决竞争冒险的问题,使状态机综合出来的电路能够运行在很高的时钟频率,但是格雷码编码较为复杂,尤其对于位宽超过4位的格雷码,编码实现较二进制编码和独热码编码要复杂的多。这里,详细的关于状态机的编码问题,小梅哥不做过多的讨论,更加细致的内容,请大家参看夏宇闻老师经典书籍《Verilog数字系统设计教程》中第12章相关内容。
Memory Mapped 总线进程根据命令有效标志信号产生写外设寄存器操作的相关代码如下所示:
087 /*------驱动总线写外设寄存器--------*/ 088 always@(posedge Clk or negedge Rst_n) 089 if(!Rst_n)begin 090 m_wr <= 1'b0; 091 m_addr <= 8'd0; 092 m_wrdata <= 16'd0; 093 end 094 else if(CMD_Valid)begin 095 m_wr <= 1'b1; 096 m_addr <= reg_CMD_DATA[23:16]; 097 m_wrdata <= reg_CMD_DATA[15:0]; 098 end 099 else begin 100 m_wr <= 1'b0; 101 m_addr <= m_addr; 102 m_wrdata <= m_wrdata; 103 end
在本系统中,需要通过该Memory Mapped 总线配置的寄存器总共有12个,分别位于ADC采样速率控制模块(Sample_Ctrl)、串口发送控制模块(UART_Tx_Ctrl)、直接数字频率合成信号发生器模块(DDS)中,各寄存器地址分配及物理意义如下所示:
指令使用说明:
例如,系统在上电后,各个模块默认是没有工作的,要想在上位机上看到数据,就必须先通过上位机发送控制命令。因为系统上电后默认选择的数据通道为DDS生成的数据,为了以最快的方式在串口猎人上看到波形,一种可行的控制顺序如下所示:
使能DDS生成数据(AA 03 06 00 01 88) —> 使能采样DDS数据(AA 03 0C 00 01 88) —>使能串口发送数据(AA 03 04 00 01 88),
这里,为了演示方便,因此在系统中对数据采样速率和DDS生成的信号的频率初始值都做了设置,因此不设置采样率和输出频率控制字这几个寄存器也能在串口猎人上接收到数据。
经过此操作后,串口猎人的接收窗口中就会不断的接收到数据了。当然,这离我们最终显示波形还有一段距离,这部分内容我将放到文档最后,以一次具体的使用为例,来step by step的介绍给大家。
关于Memory Mapped 总线如何实现各模块寄存器的配置,这里小梅哥以ADC采样控制模块Sample_Ctrl中三个寄存器的配置来进行介绍。Sample_Ctrl中三个寄存器的定义及配置代码如下所示:
14 reg [15:0]ADC_Sample_Cnt_Max_L;/*采样分频计数器计数最大值的低16位,ADDR = 8'd1*/ 15 reg [15:0]ADC_Sample_Cnt_Max_H;/*采样分频计数器计数最大值的高16位,ADDR = 8'd2*/ 16 reg ADC_Sample_En;/*采样使能寄存器,ADDR = 8'd3*/ 17 18 /*-------设置采样分频计数器计数最大值---------*/ 19 always@(posedge Clk or negedge Rst_n) 20 if(!Rst_n)begin 21 ADC_Sample_Cnt_Max_H <= 16'd0; 22 ADC_Sample_Cnt_Max_L <= 16'd49999;/*默认设置采样率为1K*/ 23 end 24 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_L))//写采样分频计数器计数最大值的低16位 25 ADC_Sample_Cnt_Max_L <= m_wrdata; 26 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_H))//写采样分频计数器计数最大值的高16位 27 ADC_Sample_Cnt_Max_H <= m_wrdata; 28 else begin 29 ADC_Sample_Cnt_Max_H <= ADC_Sample_Cnt_Max_H; 30 ADC_Sample_Cnt_Max_L <= ADC_Sample_Cnt_Max_L; 31 end 32 33 /*---------写采样使能寄存器-------------*/ 34 always@(posedge Clk or negedge Rst_n) 35 if(!Rst_n) 36 ADC_Sample_En <= 1'b0; 37 else if(m_wr && (m_addr == `ADC_Sample_En)) 38 ADC_Sample_En <= m_wrdata[0]; 39 else 40 ADC_Sample_En <= ADC_Sample_En;
采样率的控制采用定时器的方式实现。使用一个计数器持续对系统时钟进行计数,一旦计数满设定时间,则产生一个时钟周期的高脉冲信号,作为ADC采样使能信号。这里,系统时钟周期为20ns,因此,如果要实现采样1K的采样率(采样周期为1ms),则需对系统时钟计数50000次;若实现20K的采样率(采样周期为50us),则需要对系统时钟计数2500次。以此类推,可知改变采样率的实质就是改变计数器的计数最大值,因此,我们要想改变采样速率,也只需要改变采样率控制计数器的计数最大值即可。所以这里,我们设计了两个16位的寄存器,分别存储采样率控制计数器的计数最大值的低16位和高16位,如第14、15行所示。当我们需要修改ADC的采样率时,直接通过串口发送指令,修改这两个寄存器中的内容即可。
这里,小梅哥使用自己设计的一个山寨版Memory Mapped 总线来配置各个寄存器,该总线包含三组信号,分别为:
写使能信号:m_wr;
写地址信号:m_addr;
写数据信号:m_wrdata;
那么,这三组信号是如何配合工作的呢?我们以配置ADC_Sample_Cnt_Max_H和ADC_Sample_Cnt_Max_L这两个寄存器来进行介绍,这里再贴上这部分代码:
18 /*-------设置采样分频计数器计数最大值---------*/ 19 always@(posedge Clk or negedge Rst_n) 20 if(!Rst_n)begin 21 ADC_Sample_Cnt_Max_H <= 16'd0; 22 ADC_Sample_Cnt_Max_L <= 16'd49999;/*默认设置采样率为1K*/ 23 end 24 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_L))//写采样分频计数器计数最大值的低16位 25 ADC_Sample_Cnt_Max_L <= m_wrdata; 26 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_H))//写采样分频计数器计数最大值的高16位 27 ADC_Sample_Cnt_Max_H <= m_wrdata; 28 else begin 29 ADC_Sample_Cnt_Max_H <= ADC_Sample_Cnt_Max_H; 30 ADC_Sample_Cnt_Max_L <= ADC_Sample_Cnt_Max_L; 31 end
复位时,让{ ADC_Sample_Cnt_Max_H,ADC_Sample_Cnt_Max_L }为49999,即设置默认采样率为1K,每当m_wr为高且m_addr等于ADC_Sample_Cnt_Max_H寄存器的地址时,就将m_wrdata的数据更新到ADC_Sample_Cnt_Max_H寄存器中,同理,若当m_wr为高且m_addr等于ADC_Sample_Cnt_Max_L寄存器的地址时,就将m_wrdata的数据更新到ADC_Sample_Cnt_Max_L寄存器中。其他寄存器的配置原理与此相同,因此不再做阐述,相信大家举一反三,便可理解了。
4、DDS基本原理
注:本文内容摘抄自周立功编写的教材《EDA实验与实践》196~197页。
DDS(Direct Digital Synthesizer)即数字合成器,是一种新型的频率合成技术,具有相对带宽大,频率转换时间短、分辨率高和相位连续性好等优点,很容易实现频率,相位,和幅度的数控调制,广泛应用于通信领域。
DDS的基本结构图如图1所示:
图1 DDS的基本结构图
主要由相位累加器,相位调制器,正弦数据表,和D/A转换器构成,相位累加器由N位加法器与N位寄存器构成。每来一个时钟,加法器就将频率控制字,与累加寄存器输出的相位数据相加,相加的结果又反馈至累加寄存器的数据输入端,以使加法器在下一个时钟脉冲的作用下继续与频率控制字相加,这样,相位累加器在时钟作用下,不断对频率控制字进行线性相位累加。由此可以看出,在每一个时钟脉冲输入时,相位累加器便把频率控制字累加一次。相位累加器输出的数据就是合成信号的相位,相位累加器的溢出频率,就是DDS输出的信号频率,用相位累加器输出的数据,作为波形存储器的相位采样地址,这样就可以把存储在波形存储器里的波形采样值经查表找出,完成相位到幅度的转换,波形存储器的付出送到D/A转换器,由D/A转换器将数字信号转换成模拟信号输出,DDS信号流程示意图如图4.51所示。
图2 DDS信号流程示意图
由于相位累加器为N位,相当于把正弦信号在相位上的精度定义为N位,(N的取值范围一般为24~32),所以其分辨率为1/2N,若系统时钟频率为Fclk,频率控制字fword为1,则输出频率为Fout=Fclk/2N,这个频率相当于“基频”,若fword为B,则输出频率为
当系统输入时钟频率,Fclk不变时,输出信号频率由频率控制字M所决定,由上式可得:
其中B为频率字,注意B要取整,有时会有误差,在本设计中,N取32位,系统时钟频率Fclk为120兆,
选取ROM的地址(即相位累加器的输出数据)时,可以间隔选通,相位寄存器输出的位数一般取10~16位,这种截取方法称为截断式用法,以减少ROM的容量,M太大会导致ROM容量的成倍上升,而输出精度受D/A位数的限制未有很大改善,在本设计中M取12位。
以上为周立功《EDA实验与实践》一书中对DDS原理的介绍
DDS原理再解释。
上面的对DDS原理的解释,还是有部分同学反映不够直观,读完之后还是不明白DDS究竟是怎么控制频率和相位的,那么,这里小梅哥再用更加通俗的方式给大家讲解一下。
如图3,为一个完整周期的正弦信号的波形,总共有33个采样点,其中第1点和第33点的
值相同,第33点为下一个周期的起始点,因此,实际一个周期为32个采样点(1~32)。因为是在matlab中生成的,因此起始点为1,而不是我们常见的0,这里对我们理解DDS的原理没有任何影响,因此不必过多纠结。
图3 32个采样点的正弦信号波形
图4 16个采样点的正弦信号波形
我们要使用FPGA控制DAC来输出这样一个周期的正弦信号,每1ms输出一个数值。如果每个点都输出,则总共输出这一个完整的周期信号需要输出32个点,因此输出一个完整的信号需要32ms,则输出信号的频率为1000/32Hz。
假如,我们现在用这一组数据来输出一个2*(1000/32)Hz的正弦信号,因为输出信号频率为2*(1000/32)Hz,那么输出一个完整的周期的正弦波所需要的时间为32/2,即16ms,为了保证输出信号的周期为16ms,那么,我们就需要对我们的输出策略进行更改,上面输出周期为32ms的信号时,我们采用的为逐点输出的方式,以32个点来输出一个完整的正弦信号,而我们FPGA控制DAC输出信号的频率固定为1ms,因此,我们要输出周期为16ms的信号,只能输出16个点来表示一个完整的周期。我们这里选择以每隔一个点输出一个数据的方式,例如,我们可以选择输出(1、3、5、7……29、31)这些点,因为采用这些点,我们还是能够组成一个完整的周期的正弦信号,而输出时间缩短为一半,则频率提高了一倍。最终结果如上图4所示。
如果我们需要输出频率为(1/2)*(1000/32)Hz,即周期为64ms,则只需要以此组数据为基础,每2ms输出一个数据即可,例如第1ms和第2ms输出第一个点,第3ms和第4ms输出第二个点,以此类推,第63ms和第64ms输出第32个点,即可实现周期加倍,即频率减半的效果。
对于相位的调整,则更加简单,我们只需要在每个取样点的序号上加上一个偏移量,便可实现相位的控制。例如,上面默认的是第1ms时输出第一个点的数据,假如我们现在在第1ms时从第9个点开始输出,则将相位左移了90度,这就是控制相位的原理。
实现DDS输出时,将横坐标上的数据作为ROM的地址,纵坐标上的数据作为ROM的输出,那么指定不同的地址就可实现对应值的输出。而我们DDS输出控制频率和相位,归结到底就是控制ROM的地址。
了解了以上原理之后,再来设计DDS系统就很容易了,以下为DDS信号发生器的代码:
4.1 DDS_Module
01 module DDS_Module( 02 Clk, 03 Rst_n, 04 EN, 05 Fword, 06 Pword, 07 DA_Clk, 08 DA_Data 09 ); 10 11 input Clk;/*系统时钟*/ 12 input Rst_n;/*系统复位*/ 13 input EN;/*DDS模块使能*/ 14 input [31:0]Fword;/*频率控制字*/ 15 input [11:0]Pword;/*相位控制字*/ 16 17 output DA_Clk;/*DA数据输出时钟*/ 18 output [9:0]DA_Data;/*D输出输出A*/ 19 20 reg [31:0]Fre_acc; 21 reg [11:0]Rom_Addr; 22 23 /*---------------相位累加器------------------*/ 24 always @(posedge Clk or negedge Rst_n) 25 if(!Rst_n) 26 Fre_acc <= 32'd0; 27 else if(!EN) 28 Fre_acc <= 32'd0; 29 else 30 Fre_acc <= Fre_acc + Fword; 31 32 /*----------生成查找表地址---------------------*/ 33 always @(posedge Clk or negedge Rst_n) 34 if(!Rst_n) 35 Rom_Addr <= 12'd0; 36 else if(!EN) 37 Rom_Addr <= 12'd0; 38 else 39 Rom_Addr <= Fre_acc[31:20] + Pword; 40 41 /*----------例化查找表ROM-------*/ 42 ddsrom ddsrom( 43 .address(Rom_Addr), 44 .clock(Clk), 45 .q(DA_Data) 46 ); 47 48 /*----------输出DA时钟----------*/ 49 assign DA_Clk = (EN)?Clk:1'b1; 50 51 endmodule
5、仿真验证:
以上分部分介绍了系统的各个关键模块的设计。接下来,我们来对该设计进行仿真验证。因为该实验是基于串口的,为了实现仿真验证,这里小梅哥分别编写了一个串口发送的仿真模型(Uart_Tx_Model)和一个串口接收的仿真模型(Uart_Rx_Model),两个仿真模型的设计都较为简单,但是我们却可以通过该模型模拟对我们的设计进行串口数据的发送和接收,并实时打印仿真模型发送的数据与接收到的数据。关于仿真模型的代码,这里只贴上代码,不做具体解释。(此贴回复超过100条我就专门开文讲解testbench的编写技巧)
以下为串口接收仿真模型的代码
001 `timescale 1ns/1ps 002 003 module Uart_RX_Model(Baud_Set,uart_rx); 004 005 input [2:0]Baud_Set;/*波特率选择信号*/ 006 input uart_rx;/*仿真模型串口接收引脚*/ 007 008 reg Clk;/*仿真模型内部时钟,50M*/ 009 reg Rst_n;/*仿真模型内部复位信号*/ 010 011 wire Mid_Flag_Receive;/*数据中点(采样点)标志信号*/ 012 013 reg Receive_Baud_Start;/*接收波特率生成使能信号*/ 014 reg [7:0]rx_data;/*接收数据移位寄存器*/ 015 016 reg [7:0]Rx_Byte;/*最终接收结果*/ 017 018 initial Clk = 1; 019 always#10 Clk = ~Clk; 020 021 /*例化波特率设置模块*/ 022 baud_select baud_select_Receive( 023 .Clk(Clk), 024 .Rst_n(Rst_n), 025 .Baud_Set(Baud_Set), 026 .Baud_Start(Receive_Baud_Start), 027 .Mid_Flag(Mid_Flag_Receive) 028 ); 029 030 initial begin 031 Rst_n = 0; 032 Rx_Byte = 0; 033 rx_data = 0; 034 #100 Rst_n = 1; 035 end 036 037 /*接收一个字节的数据*/ 038 initial begin 039 forever begin 040 @(negedge uart_rx) 041 begin 042 Receive_Baud_Start = 1; 043 @(posedge Mid_Flag_Receive); 044 @(posedge Mid_Flag_Receive)rx_data[0] = uart_rx; 045 @(posedge Mid_Flag_Receive)rx_data[1] = uart_rx; 046 @(posedge Mid_Flag_Receive)rx_data[2] = uart_rx; 047 @(posedge Mid_Flag_Receive)rx_data[3] = uart_rx; 048 @(posedge Mid_Flag_Receive)rx_data[4] = uart_rx; 049 @(posedge Mid_Flag_Receive)rx_data[5] = uart_rx; 050 @(posedge Mid_Flag_Receive)rx_data[6] = uart_rx; 051 @(posedge Mid_Flag_Receive)rx_data[7] = uart_rx; 052 @(posedge Mid_Flag_Receive)begin Receive_Baud_Start = 0;Rx_Byte = rx_data;end 053 $display("Master_receive Data = %0h",Rx_Byte); 054 end 055 end 056 end 057 058 endmodule
以下为串口发送仿真模型的设计代码
001 `timescale 1ns/1ps 002 003 module Uart_Tx_Model(Baud_Set,Tx_Data,Tx_En,uart_tx,Tx_Done); 004 005 input [2:0]Baud_Set; /*波特率选择信号*/ 006 input [7:0]Tx_Data; /*待发送数据字节*/ 007 input Tx_En; /*数据字节发送使能信号*/ 008 output reg uart_tx; /*仿真串口发送模型发送信号*/ 009 output reg Tx_Done; /*发送完成信号*/ 010 011 reg Clk; /*仿真模型内部工作时钟*/ 012 reg Rst_n; /*仿真模型内部复位信号*/ 013 014 wire Bps_Clk; /*发送波特率时钟波特率*/ 015 reg Bps_En; /*发送波特率使能信号*/ 016 017 initial Clk = 1; 018 always#10 Clk = ~Clk; 019 020 /*----例化发送波特率时钟生成模块-----*/ 021 TxModel_Bps_Gen TxModel_Bps_Gen_send( 022 .Clk(Clk), 023 .Rst_n(Rst_n), 024 .Baud_Set(Baud_Set), 025 .Tx_Done(Tx_Done), 026 .Bps_Clk(Bps_Clk), 027 .Byte_En(Bps_En) 028 ); 029 030 initial begin 031 Tx_Done = 0; 032 uart_tx = 1; 033 Rst_n = 0; 034 Bps_En = 0; 035 #100; 036 Rst_n = 1; 037 forever@(posedge Tx_En)/*每来一个发送使能信号即执行一次发送过程*/ 038 Uart_Send(Tx_Data); 039 end 040 041 /*执行一次字节数据的发送*/ 042 task Uart_Send; 043 input [7:0]Data; 044 begin 045 Bps_En = 1; 046 Tx_Done = 0; 047 $display("Uart_Send Data = %0h",Data);/*打印发送的数据*/ 048 @(posedge Bps_Clk) #0.1 uart_tx = 0; 049 @(posedge Bps_Clk) #0.1 uart_tx = Data[0]; 050 @(posedge Bps_Clk) #0.1 uart_tx = Data[1]; 051 @(posedge Bps_Clk) #0.1 uart_tx = Data[2]; 052 @(posedge Bps_Clk) #0.1 uart_tx = Data[3]; 053 @(posedge Bps_Clk) #0.1 uart_tx = Data[4]; 054 @(posedge Bps_Clk) #0.1 uart_tx = Data[5]; 055 @(posedge Bps_Clk) #0.1 uart_tx = Data[6]; 056 @(posedge Bps_Clk) #0.1 uart_tx = Data[7]; 057 @(posedge Bps_Clk) #0.1 uart_tx = 1; 058 @(posedge Bps_Clk) #0.1 ; 059 Tx_Done = 1; 060 Bps_En = 0; 061 #20 Tx_Done = 0; 062 end 063 endtask 064 065 endmodule
以下为仿真顶层模块的设计
001 `timescale 1ns/1ns 002 `include "../rtl/header.v" 003 module uart_scope_tb; 004 localparam KEY_WIDTH = 3; 005 006 reg Clk; 007 reg Rst_n; 008 reg [KEY_WIDTH - 1:0]Key_in; 009 010 reg ADC_Din; 011 wire ADC_Clk; 012 wire ADC_Cs_n; 013 014 /*波特率设置总线,此处默认为9600bps,仿真不做波特率修改测试*/ 015 wire [2:0]Baud_Set; 016 reg [7:0]Tx_Data;/*串口发送仿真模型待发送数据字节*/ 017 reg Tx_En; /*串口发送仿真模型发送使能信号*/ 018 wire Rs232_MTSR; /*串口“主机(PC)发送-从机(FPGA)接收”信号*/ 019 wire Rs232_MRST; /*串口“主机(PC)接收-从机(FPGA)发送”信号*/ 020 wire Tx_Done; /*串口字节发送完成信号*/ 021 022 assign Baud_Set = 3'd0;/*设置波特率为固定的9600bps*/ 023 024 localparam 025 Header = 8'hAA, /*帧头*/ 026 Length = 8'd3, /*帧长*/ 027 Tail = 8'h88; /*帧尾*/ 028 029 /*------例化串口示波器顶层模块------*/ 030 uart_scope uart_scope( 031 .Clk(Clk), 032 .Rst_n(Rst_n), 033 .Rs232_Rx(Rs232_MTSR), 034 .Rs232_Tx(Rs232_MRST), 035 .Key_in(Key_in), 036 .ADC_Din(ADC_Din), 037 .ADC_Clk(ADC_Clk), 038 .ADC_Cs_n(ADC_Cs_n) 039 ); 040 041 /*------例化串口发送仿真模型------*/ 042 Uart_Tx_Model Uart_Tx_Model( 043 .Baud_Set(Baud_Set), 044 .Tx_Data(Tx_Data), 045 .Tx_En(Tx_En), 046 .uart_tx(Rs232_MTSR), 047 .Tx_Done(Tx_Done) 048 ); 049 050 /*------例化串口接收仿真模型------*/ 051 //该模型接收FPGA发送出来的数据并打印在modelsim的transcript窗口中 052 Uart_RX_Model Uart_RX_Model( 053 .Baud_Set(Baud_Set), 054 .uart_rx(Rs232_MRST) 055 ); 056 057 /*-------生成50M时钟信号--------*/ 058 initial Clk = 0; 059 always #10 Clk = ~Clk; 060 061 /*-------生成ADC_Din数据-------*/ 062 /*此处不对ADC的采样结果多做计较,只要求保 063 证ADC_Din上有数据即可,有兴趣者可自己编写仿真模型*/ 064 initial ADC_Din = 1; 065 always #1315 ADC_Din = ~ADC_Din; 066 067 initial begin 068 Rst_n = 1'b0; 069 Tx_En = 1'b0; 070 Tx_Data = 8'd0; 071 Key_in = 4'b1111; 072 #200; 073 Rst_n = 1'b1; /*释放复位信号,系统即进入正常工作状态*/ 074 #1000; 075 En_DDS_Run; /*使能DDS信号发生器生成信号数据*/ 076 #10000; 077 En_S_DDS; /*使能采样ADC数据*/ 078 En_S_ADC; /*使能采样DDS数据*/ 079 #10000; 080 En_UART_Send;/*使能串口发送,此时串口猎人软件上将会开始持续接收到数据*/ 081 end 082 083 initial begin 084 #200_000_000;press_key(0); 085 #200_000_000;press_key(1); 086 #200_000_000; 087 $stop; 088 end 089 090 091 092 /*---发送命令帧数据任务-----*/ 093 task Send_CMD; 094 input [7:0]DATAA,DATAB,DATAC;/*用户数据(地址、数据高字节,数据低字节)*/ 095 begin 096 Tx_Data = Header;/*需发送数据为帧头*/ 097 Tx_En = 1; /*启动发送*/ 098 #20 Tx_En = 0; /*一个时钟周期后,清零发送启动信号*/ 099 @(posedge Tx_Done)/*等待发送完成信号*/ 100 #1000; 101 102 Tx_Data = Length;/*需发送数据为帧长,此处帧长只是数据内容的长度*/ 103 Tx_En = 1; /*启动发送*/ 104 #20 Tx_En = 0; /*一个时钟周期后,清零发送启动信号*/ 105 @(posedge Tx_Done)/*等待发送完成信号*/ 106 #1000; 107 108 Tx_Data = DATAA;/*需发送数据第一个字节,此数据代表外设寄存器的地址*/ 109 Tx_En = 1; /*启动发送*/ 110 #20 Tx_En = 0; /*一个时钟周期后,清零发送启动信号*/ 111 @(posedge Tx_Done)/*等待发送完成信号*/ 112 #1000; 113 114 Tx_Data = DATAB;/*需发送数据第二个字节,此数据代表写入外设寄存器的内容高8位*/ 115 Tx_En = 1; /*启动发送*/ 116 #20 Tx_En = 0; /*一个时钟周期后,清零发送启动信号*/ 117 @(posedge Tx_Done)/*等待发送完成信号*/ 118 #1000; 119 120 Tx_Data = DATAC;/*需发送数据第三个字节,此数据代表写入外设寄存器的内容低8位*/ 121 Tx_En = 1; /*启动发送*/ 122 #20 Tx_En = 0; /*一个时钟周期后,清零发送启动信号*/ 123 @(posedge Tx_Done)/*等待发送完成信号*/ 124 #1000; 125 126 Tx_Data = Tail;/*需发送数据为帧尾*/ 127 Tx_En = 1; /*启动发送*/ 128 #20 Tx_En = 0; /*一个时钟周期后,清零发送启动信号*/ 129 @(posedge Tx_Done)/*等待发送完成信号*/ 130 #1000; 131 #10000; 132 end 133 endtask 134 135 task En_DDS_Run;/*使能DDS生成数据*/ 136 begin 137 Send_CMD(`DDS_En, 8'h00, 8'h01); 138 $display("En DDS Run"); 139 end 140 endtask 141 142 task Stop_DDS_Run;/*停止DDS生成数据*/ 143 begin 144 Send_CMD(`DDS_En, 8'h00, 8'h00); 145 $display("Stop DDS Run"); 146 end 147 endtask 148 149 task En_S_DDS;/*使能采样DDS数据*/ 150 begin 151 Send_CMD(`DDS_Sample_En, 8'h00, 8'h01); 152 $display("En Sample DDS data"); 153 end 154 endtask 155 156 task Stop_S_DDS;/*停止采样DDS数据*/ 157 begin 158 Send_CMD(`DDS_Sample_En, 8'h00, 8'h00); 159 $display("Stop Sample DDS data"); 160 end 161 endtask 162 163 task En_UART_Send;/*使能串口发送*/ 164 begin 165 Send_CMD(`UART_En_Tx, 8'h00, 8'h01); 166 $display("En UART Send"); 167 end 168 endtask 169 170 task Stop_UART_Send;/*停止串口发送*/ 171 begin 172 Send_CMD(`UART_En_Tx, 8'h00, 8'h00); 173 $display("Stop UART Send"); 174 end 175 endtask 176 177 task En_S_ADC;/*使能采集ADC数据*/ 178 begin 179 Send_CMD(`ADC_Sample_En, 8'h00, 8'h01); 180 $display("En Sample ADC data"); 181 end 182 endtask 183 184 task Stop_S_ADC;/*停止采集ADC数据*/ 185 begin 186 Send_CMD(`ADC_Sample_En, 8'h00, 8'h00); 187 $display("Stop Sample ADC data"); 188 end 189 endtask 190 191 task Set_ADC_Sample_Speed;/*设置ADC采样率*/ 192 input[25:0] Fs;/*采样率实际频率*/ 193 reg [31:0] S_cnt_top;/*分频计数器计数最大值*/ 194 begin 195 /*由采样实际频率值换算出采样分频计数器计数最大值*/ 196 S_cnt_top = 50000000/Fs - 1; 197 /*写采样分频计数器计数最大值低16位*/ 198 Send_CMD(`ADC_S_Cnt_Max_L,S_cnt_top[15:8],S_cnt_top[7:0]); 199 /*写采样分频计数器计数最大值高16位*/ 200 Send_CMD(`ADC_S_Cnt_Max_H,S_cnt_top[31:24],S_cnt_top[23:16]); 201 $display("Set ADC Sample Speed as = %0d" ,Fs); 202 end 203 endtask 204 205 task Set_DDS_Sample_Speed;/*设置DDS数据的采样率*/ 206 input[25:0] Fs;/*采样率实际频率*/ 207 reg [31:0] S_cnt_top;/*分频计数器计数最大值*/ 208 begin 209 /*由采样实际频率值换算出采样分频计数器计数最大值*/ 210 S_cnt_top = 50000000/Fs - 1; 211 /*写采样分频计数器计数最大值低16位*/ 212 Send_CMD(`DDS_S_Cnt_Max_L,S_cnt_top[15:8],S_cnt_top[7:0]); 213 /*写采样分频计数器计数最大值高16位*/ 214 Send_CMD(`DDS_S_Cnt_Max_H,S_cnt_top[31:24],S_cnt_top[23:16]); 215 $display("Set DDS Sample Speed as = %0d" ,Fs); 216 end 217 endtask 218 219 task Set_DDS_Fout_Speed;/*设置DDS输出信号频率*/ 220 input[25:0] Fs;/*输出信号实际频率*/ 221 reg [31:0] r_fword;/*DDS频率控制字*/ 222 begin 223 /*由实际要求输出频率数据换算出频率控制字*/ 224 r_fword = Fs*65536*65536/50000000; 225 Send_CMD(`DDS_Fword_L,r_fword[15:8],r_fword[7:0]); 226 Send_CMD(`DDS_Fword_H,r_fword[31:24],r_fword[23:16]); 227 $display("Set DDS Fout as = %0d" ,Fs); 228 end 229 endtask 230 231 232 task press_key; 233 input [KEY_WIDTH/2:0]Key; 234 reg [15:0]myrand; 235 begin 236 Key_in = {KEY_WIDTH{1'b1}}; 237 /*按下抖动*/ 238 repeat(20)begin 239 myrand = {$random} % 65536; 240 #myrand Key_in[Key] = ~Key_in[Key]; 241 end 242 Key_in[Key] = 1'b0; 243 244 #22000000;/*稳定期*/ 245 246 /*释放抖动*/ 247 repeat(20)begin 248 myrand = {$random} % 65536; 249 #myrand Key_in[Key] = ~Key_in[Key]; 250 end 251 Key_in[Key] = 1'b1; 252 #22000000;/*稳定期*/ 253 end 254 endtask 255 256 endmodule
下图为系统仿真架构图:
这里,在我们提供的工程中,已经设置好了Nativelink,用户只需要在Quartus II中点击tools—run rtl simulation tool—rtl simulation即可自动调用modelsim-altera并执行仿真,因为这里完全模拟真实时序进行仿真,因此运行完整个仿真大约需要5—10分钟。
仿真完成后,结果如图所示:
其中,Rx_Byte为串口接收仿真模型接收到的数据,这里以波形的方式展示。ADC_Data为ADC采样结果,DDS_Data为DDS输出的数据最下方为按键标志和按键结果,当按下按键1时,数据通道切换为ADC的采样结果,当按下按键2时,数据通道切换为DDS的输出数据。
(如果用户在进行仿真的过程中发现仿真无法运行,在modelsim中提示错误的话,请删除simulation—>modelsim文件夹下除wave.do和mydo.do文件外的其他所有文件,然后在quartus 中重新启动仿真)
6、基于串口猎人的板级验证
这里,我们使用一款功能非常强大的串口调试软件——串口猎人来调试我们的设计。串口猎人的安装这里不做过多的讲述。首先,我们将FPGA系统的sof文件配置到fpga中,然后运行串口猎人软件,串口猎人打开后界面如下所示:
我们点击图中的动画即可让该动画消失。
接下来我们载入预先设置好的配置文件,如下图所示:
我们点击右下角的“载入”按钮,在弹出的界面中,定位到我们本实验的根目录,选择“serialhunter.ini”文件,
点击打开。
切换到高级发码选项卡,显示如下所示:
点击启动自动发码。
回到基本功能选项卡,可以看到,窗口中开始连续不断的接收到数据,如下图所示:
此时,我们切换到波形显示选项卡,可看到如下所示的效果:
表明我们已经正确的接收到了波形数据。
切换到码表选项卡,效果如下图所示:
然后,我们切换到柱状显示选项卡,效果如下所示:
然后,我们回到高级发码选项卡,将0~3组发码列表前的勾选取消,勾选上第4组,然后点击启动自动发码。此时,我们就已经将fpga系统的接收和发送波特率速率切换到了115200,如下图所示:
因为波特率不对,所以接下来接收到的数据就全部是错误的了。我们回到基本功能选项卡,将波特率切换为115200bps,如下图所示:
然后我们再回到波形显示选项卡,结果如下所示:
这时,我们再回到高级发码选项卡,取消第4组发码的勾选,勾选上第5组发码,然后点击自动发码,再回到波形显示选项卡,结果如下所示:
此时,我们的DDS输出信号频率便更改为50Hz了。其他更多指令内容,这里就不一一介绍了,欢迎各位积极探索。
7、总结
当然,这个系统的最终目标是教会大家在fpga中使用串口进行简单的数据收发,离真正的虚拟示波器还相差甚远。此串口猎人显示的波形频率并不能严格的和实际信号的频率对应上,这一点望各位悉知。也欢迎有上位机开发基础的同学来根据本系统开发独立的上位机软件。另外,在使用中,我们只需要按下按键2,就能将数据通道切换到ADC的采样结果上来,此时,给ADC的输入上给出不同的电压,在码表选项卡上就能明显的看到数值的变化,可作为电压表之用。按下按键1则切换到内部DDS通道。
另外,文档中使用的ADC型号为TCL549,我们开发套件后续配备的ADC模块已经更改为TLV1544,因此,我们在工程中使用了条件编译的方式,用户可以根据手头使用的ADC具体型号,设置编译条件即可切换ADC型号,如下图所示:
如果使用TLV1544作为实际采样器件,只需要在uart_scope.v文件的开头,将 “`define USE_TLV1544 1”这句话使能,将“`define USE_TLC549 1”这句话注释掉即可。反之亦然。
由于本系统涉及到的功能模块和代码较多,无法一一为各位讲解,希望各位能够仔细阅读代码,代码中小梅哥都做了详细的注释,希望大家通过代码,能进一步学习verilog语法,增强对系统级仿真的意识。
如有更多问题,欢迎加入芯航线 FPGA 技术支持群交流学习:472607506
小梅哥
芯航线电子工作室
关于学习资料,小梅哥系列所有能够开放的资料和更新(包括视频教程,程序代码,教程文档,工具软件,开发板资料)都会发布在我的云分享。(记得订阅)链接:http://yun.baidu.com/share/home?uk=402885837&view=share#category/type=0
赠送芯航线AC6102型开发板配套资料预览版下载链接:链接:http://pan.baidu.com/s/1slW2Ojj 密码:9fn3
赠送SOPC公开课链接和FPGA进阶视频教程。链接:http://pan.baidu.com/s/1bEzaFW 密码:rsyh