您现在的位置是:首页 >技术教程 >FPGA之手把手教你做多路信号发生器(STM32与FPGA数据互传控制波形生成)网站首页技术教程

FPGA之手把手教你做多路信号发生器(STM32与FPGA数据互传控制波形生成)

技术小董 2024-06-30 12:01:02
简介FPGA之手把手教你做多路信号发生器(STM32与FPGA数据互传控制波形生成)


博主的念叨

博主建了一个技术资源分享的群,开源免费,欢迎进来唠嗑280730348

最近趁热打铁做了一个关于STM32与FPGA通信并且控制高速DA模块产生不同频率信号的正弦波、方波、三角波和锯齿波的项目,从中收获到了很多东西,也踩了一些雷和坑,将分为几篇文章将整个过程分享出来。

这一次准备分享的是将串口解析的出来的波形频率数据以及波形类型数据送入顶层文件中,通过调用不同的ROM核驱动高速DA模块产生对应的信号,通过调用IP核生成特定频率的时钟,使得正弦波能够贴近整数倍的输出频率。

本文参考正点原子EP4CE系列开发板的源码,做了部分修改

一、任务介绍

1、本文目标

实现STM32和FPGA的串口通信,并将STM32传输过来的频率信息和波形信息解析存入定义的reg变量中。通过调用ROM核内部的数据给高速DA模块发送数据,使得高速DA模块能够产生特定频率的波形

2、设计思路

根据设计需求,需要对串口数据进行接收解析,同时需要把解析的数据传送给内存单元。内存单元接收到数据以后传递到顶层模块,在顶层模块中作为其它子模块的输入条件,根据频率的不同改变计数次数,从而实现500KHz波形信号的生成以及其分频信号的产生。根据波形类型值的不同选择不同的波形单元

3、设计注意事项

1、串口通信属于异步通信方式,因此出现信号是不确定的,为了防止亚稳态的产生,需要对信号打拍子避免亚稳态的产生。

2、本文串口通信协议是 EA 频率高位 频率中位 频率低位 波形 AE,其中EA代表起始标志,AE代表停止标志。频率高位代表频率/65536,中位代表%65536/256,低位代表%65526%256。

3、串口波特率要对的上,否则无法进行正常通信

4、生成的波形模拟点数为128点,基频是锁相环产生的500KHz*128=64MHz信号

5、生成了多个ROM核,注意ROM核的调用

6、此设计与博主前面博文串口协议解析相关,请提前了解一下串口解析的过程

二、设计代码

1.顶层文件代码

module hs_da(
    input           sys_clk,       //时钟信号
	 input           sys_rst_n,     //复位信号
	 input           uart_rxd,      //串口接收
	 output          da_clk,        //da时钟
	 output [7:0]    da_data,       //da数据
	 output          uart_txd,       //串口发送
	 output          test_uart_txd       //串口发送

);
wire [6:0]   rd_addr;//ROM读地址
wire [7:0]   rd_data;//ROM读出的数据
wire [6:0]   rd_addr_ju;//ROM读地址
wire [7:0]   rd_data_ju;//ROM读出的数据
wire [6:0]   rd_addr_juchi;//ROM读地址
wire [7:0]   rd_data_juchi;//ROM读出的数据
wire [6:0]   rd_addr_sanjiao;//ROM读地址
wire [7:0]   rd_data_sanjiao;//ROM读出的数据
wire [23:0]  freq;
wire [7:0]   wave;
wire locked;
wire rst_n;
wire clk;
assign rst_n=sys_rst_n&locked;
assign test_uart_txd=uart_txd;
//例化串口接收数据和发送数据模块
uart_loopback_top u_uart_loopback_top(
    .sys_clk     (sys_clk),
	 .sys_rst_n   (sys_rst_n),
	 .uart_rxd    (uart_rxd),
	 .uart_txd    (uart_txd),
	 .freq        (freq),
    .wave        (wave)	 
);

da_wave_send u_da_wave_send(
    .clk               (clk),
	 .rst_n            (sys_rst_n),
	 .freq             (freq),
	 .wave             (wave),
	 .rd_data          (rd_data),
	 .rd_addr          (rd_addr),
	 .rd_data_ju       (rd_data_ju),
	 .rd_addr_ju       (rd_addr_ju),
	 .rd_data_juchi    (rd_data_juchi),
	 .rd_addr_juchi    (rd_addr_juchi),
	 .rd_data_sanjiao  (rd_data_sanjiao),
	 .rd_addr_sanjiao  (rd_addr_sanjiao), 
	 .da_clk           (da_clk),
	 .da_data          (da_data)
);
//正弦波例化
rom u_rom(
	.address ( rd_addr ),
	.clock   ( clk ),
	.q       ( rd_data )
);
//矩形波例化
rom_ju u_rom_ju(
	.address ( rd_addr_ju ),
	.clock   ( clk ),
	.q       ( rd_data_ju )
);

//锯齿波例化
rom_juchi u_rom_juchi(
	.address ( rd_addr_juchi ),
	.clock   ( clk ),
	.q       ( rd_data_juchi )
);

//三角波例化
rom_sanjiao u_rom_sanjiao(
	.address ( rd_addr_sanjiao ),
	.clock   ( clk ),
	.q       ( rd_data_sanjiao )
);
pll_clk u_pll_clk(
    .areset   (~sys_rst_n),
	 .inclk0   (sys_clk),
	 .c0       (clk),
	 .locked   (locked)
	 );
endmodule

输入总共定义三个变量,其中uart_rxd代表串口输入接入。sys_clk代表输入50MHz基准时钟,sys_rst_n代表复位信号。输出da_clk是提供给高速AD/DA模块的时钟,确保高速AD/DA模块能够与FPGA发送数据进行同步。da_data代表发送给DA模块的八位数据信号,uart_txd代表串口发送输出引脚,在其中还设置了test_uart_txd引脚,这个引脚可留下也可去除,主要目的是为了连接到FPGACH340的TX引脚,用于数据发送至电脑端进行数据的查看。

定义了许多wire变量。以正弦波为例,定义正弦波ROM地址为rd_addr,正弦波读出数据为rd_data。其中地址设置为七位,数据设置为八位。原因在于高速AD/DA模块可以接收0-255的数据,而我们整个周期只需要模拟出128个点即可,因此地址位为7位。以下带后缀的ju,juchi,sanjiao也都同理。另外freq和wave是由串口回环模块输出的变量,在顶层文件中被调用。locked,rst_n以及clk信号都是pll锁相环生成的。需要注意的是,我在设计中遇到了一个问题是在底层文件中进行PLL的时钟调用无效,但是到顶层中调用时钟就可行了,网上说PLL时钟的调用必须得放到顶层,然后赋给底层,如果有知道的小伙伴可以在评论区下方留言。

第一个assign语句的作用是生成锁相环输出时钟信号的复位信号,第二个assign语句的作用是连接串口线,也即前文提到的连接到ch340模块TX引脚的接口。

实例化da_wave_send模块,输入包括前面12路信号,输出为da_clk和da_data,其中da_clk直接与锁相环输出的64MHz信号连接起来。rom的例化都是按照指定格式进行例化的,大家只需要注意例化格式即可。

pll锁相环的例化也是如此,在这里博主只选择了一路锁相环的输出,因此输出信号也只有一个c0信号,若需要更改锁相环信号的输出可在ip核界面直接进行修正即可。

2.波形生成模块

module da_wave_send(
    input                 clk    ,  //时钟
    input                 rst_n  ,  //复位信号,低电平有效
    input        [23:0]   freq   ,  //频率信号
	 input        [7:0]    wave   ,  //波形信号
	 
    input        [7:0]    rd_data,  //ROM读出的数据
    output  reg  [6:0]    rd_addr,  //读ROM地址
    input        [7:0]    rd_data_ju,  //ROM读出的数据
    output  reg  [6:0]    rd_addr_ju,  //读ROM地址
    input        [7:0]    rd_data_juchi,  //ROM读出的数据
    output  reg  [6:0]    rd_addr_juchi,  //读ROM地址
    input        [7:0]    rd_data_sanjiao,  //ROM读出的数据
    output  reg  [6:0]    rd_addr_sanjiao,  //读ROM地址	 
    //DA芯片接口
    output                da_clk ,  //DA(AD9708)驱动时钟,最大支持125Mhz时钟
    output  reg  [7:0]    da_data   //输出给DA的数据  
    );

//频率调节控制
wire [23:0] FREQ_ADJ;               
assign FREQ_ADJ= 24'd500_000/freq-1'b1;//分频频率
//assign FREQ_ADJ = 8'd1;//分频频率
//reg define
reg    [19:0]    freq_cnt  ;  //频率调节计数器

//*****************************************************
//**                    main code
//*****************************************************

//数据rd_data是在clk的上升沿更新的,所以DA芯片在clk的下降沿锁存数据是稳定的时刻
//而DA实际上在da_clk的上升沿锁存数据,所以时钟取反,这样clk的下降沿相当于da_clk的上升沿
assign  da_clk = ~clk;       

//频率调节计数器
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        freq_cnt <= 20'd0;
    else if(freq_cnt == FREQ_ADJ)    
        freq_cnt <= 20'd0;
    else         
        freq_cnt <= freq_cnt + 20'd1;
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        da_data <= 8'd0;
    else begin   
        case (wave)
		      8'd0:da_data = rd_data;
				8'd1:da_data = rd_data_ju;
				8'd2:da_data = rd_data_juchi;
				8'd3:da_data = rd_data_sanjiao;
				default da_data = 8'd0;
		  endcase
	 end 
end
//读ROM地址
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rd_addr <= 7'd0;
    else begin
        if(freq_cnt == FREQ_ADJ) begin
          case (wave)
		        8'd0:rd_addr <= rd_addr + 7'd1;
				  8'd1:rd_addr_ju <= rd_addr_ju + 7'd1;
				  8'd2:rd_addr_juchi <= rd_addr_juchi + 7'd1;
				  8'd3:rd_addr_sanjiao <= rd_addr_sanjiao + 7'd1;
				  default rd_addr <= rd_addr + 7'd1;
		    endcase		  
        end    
    end            
end

endmodule

从代码中可见,定义了wire变量FREQ_ADJ,即我们当前的频率计数值。通过assign语句赋值,由于我们最高的频率为500KHz,因此分频计数值为500000/freq-1,假设我们接收到的频率数据为500000,那么计数值就为0。计数值为0的时候在程序内部每隔500KHz重新读一遍地址,从而也就实现了500KHz频率的输出,freq为250000的时候计数值为1就是250KHz频率输出。

第一个always语句是对频率进行计数,由于此时输入的时钟为64MHz,因此此always语句是每隔64MHz执行一次的。第二个always语句是对当前的da_data进行一个赋值,根据wave变量的不同将对应ROM里面的值赋值给da_data。第三个always语句是执行ROM的读取,当计数值等于设定的分频计数值时,就会对ROM进行一次数据读取。根据波形的不同对应的波形ROM地址就会加1,此时读取出来的ROM值就会对应不同的波形ROM。要注意的是,ROM只能够被读取而不能被写入,因此只需要改变读取的地址就能够读到不同地址对应的不同数据。

3.ROM例化

ROM的例化主要包括几个步骤:

1、生成mif文件。这个mif文件由对应的波形生成软件生成,我们可以设置波形的宽度和深度。其中深度就是波形一个完整周期的点数,宽度指的是波形的位宽。在这里我们需要128个点,和最大255的数据,就需要设置宽度为8位,深度为7位。

在这里插入图片描述
2、生成mif文件放入到doc文件夹里面,然后打开Quartus的ip核,搜索ROM
在这里插入图片描述
在这里插入图片描述
我这里由于是已经生成了,所以选择编辑已存在的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
即可

4.PLL例化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.引脚分配

在这里插入图片描述


总结

有什么不懂的可以在下方留言,只要学会了方法,实现信号发生器会比较简单的。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。