九、基于串口猎人软件的串口示波器

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

转载于:https://www.cnblogs.com/xiaomeige/p/6390246.html

【小梅哥FPGA进阶教程】第九章 基于串口猎人软件的串口示波器相关推荐

  1. 【小梅哥FPGA进阶教程】第十一章 四通道幅频相可调DDS信号发生器

    十一.四通道幅频相可调DDS信号发生器 本文由山东大学研友袁卓贡献,特此感谢 实验目标 实现多通道可调信号发生器 实验平台 芯航线FPGA核心板.ADDA模块 实验现象 实现基于FPGA的多通道可调信 ...

  2. 小梅哥FPGA:基于线性序列机的TLC5620型DAC驱动设计

    小梅哥FPGA:基于线性序列机的TLC5620型DAC驱动设计 目标:学会使用线性序列机的思想设计常见的有串行执行特征的时序逻辑 实验现象:在QuartusⅡ软件中,使用ISSP工具,输入希望输出的电 ...

  3. javascript进阶教程第一章案例实战

    javascript进阶教程第一章案例实战 一.学习任务 通过几个案例练习回顾学过的知识 通过练习积累JS的使用技巧 二.实例 练习1:删除确认提示框 实例描述: 防止用户小心单击了"删除& ...

  4. javascript进阶教程第二章对象案例实战

    javascript进阶教程第二章对象案例实战 一.学习任务 通过几个案例练习回顾学过的知识 通过案例练习补充几个之前没有见到或者虽然讲过单是讲的不仔细的知识点. 二.具体实例 温馨提示 面向对象的知 ...

  5. 《Go语言圣经》学习笔记 第九章 基于共享变量的并发

    <Go语言圣经>学习笔记 第九章 基于共享变量的并发 目录 竞争条件 sync.Mutex互斥锁 syn.RWMutex读写锁 内存同步 syn.Once初始化 竞争条件检测 示例:并发的 ...

  6. 小梅哥FPGA时序分析和约束实例演练课程

    看过了他的nios课程,对他的能力很认同 只有前5讲是开源的,后面需要在淘宝上购买,暂时用不到,我就没有买课程,只看了前5讲感觉还挺有用,需要的时候再说吧. 小梅哥 FPGA时序分析 FPGA时序约束 ...

  7. 小梅哥FPGA视频教程学习总结(持续学习中……)

    首先附上小梅哥FPGA视频教程链接:https://www.bilibili.com/video/BV1va411c7Dz?p=2&spm_id_from=pageDriver 小梅哥yyds ...

  8. 小梅哥FPGA:嵌入式块RAM使用之FIFO

    小梅哥FPGA:嵌入式块RAM使用之FIFO 课程目标:学会调用QuartusⅡ软件中提供的FIFO核并通过仿真,了解其接口时序 实现现象:通过QuartusⅡ软件中调用FIFO核,通过仿真来验证其接 ...

  9. 小梅哥FPGA学习笔记

    小梅哥FPGA学习笔记 一.38译码器 功能: 译码器其任一时刻的稳态输出,仅仅与该时刻的输入变量的取值有关,它是一种多输入多输出的组合逻辑电路,负责将二进制代码翻译为特定的对象(如逻辑电平等).38 ...

最新文章

  1. bugku 闪得好快
  2. 又臭又长!流着泪我也要把它给改完!
  3. LiveVideoStackCon讲师热身分享 ( 三 ) —— Facebook的工具文化与多媒体QoE
  4. wepy里面两种不同的写回调函数的方法
  5. mybatis延迟加载(lazy)配置生效的陷阱
  6. 【Codeforces 467C】George and Job
  7. 3d安卓环境搭建_RoboCup 仿真3D简介及环境搭建
  8. 旅游后台管理系列——SSM框架Service层整合
  9. 绿坝十年,儿童网络安全保护问题仍未解决
  10. st计算机编程语言,ST语法编程基础-ST语言简介
  11. 昨天发现的一些ip黑名单
  12. Win10突然卡死的原因调查(转自yzhang)
  13. 安装ubuntu 创建分区_安装Ubuntu后如何创建单独的主分区
  14. Qt5中使用lambda表达式
  15. c# chart 各个属性_C# Chart详细解析(待)
  16. http 升级https
  17. Linux之SUSE系统SAP-HANA经常系统内存不足使得SAP应用不能使用解决方案
  18. uniapp轻松集成极光推送SDK(本地插件集成和离线打包两种)
  19. VLC Media Player的简介
  20. 官宣:免费OA办公系统的标准解决方案

热门文章

  1. 为什么同样是跳槽,有些人跳槽薪资能翻三倍?
  2. 利用JavascriptSerializer类 进行Json对象的序列化和反序列化
  3. Android健身APP源码 | 寻找C站宝藏
  4. linux 内存爆浆,内存条为什么会引起花屏
  5. C语言实现贪吃蛇小游戏1.0
  6. android平板电脑怎么样,苹果还是安卓?一图教你如何选择适合自己的平板
  7. 吐血整理!万字原创读书笔记,数据分析的知识点全在这里了
  8. Excel下拉菜单怎么做
  9. 电脑出现“你不能访问此共享文件夹,因为你组织的安全策略阻止未经身份验证的来宾访问。
  10. ktv 系统设计经验