大多电子工程师都喜欢DIY,今天给大家分享一个不靠 MCU,用 FPGA + DAC 实现可调信号源的项目。利用板载 125MSPS 高速 DAC,从 DDS 原理出发,完整实现了一台可输出正弦波、三角波、方波的可调波形发生器。
项目介绍
1.通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形。
2.生成模拟信号的频率范围为DC-20MHz,调节精度为1Hz。
3.生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V。
4.在OLED上显示当前波形的形状、波形的频率以及幅度。
5.利用板上旋转编码器和按键能够对波形进行切换、进行参数调节。
设计思路
本次实验中使用的模块包括OLED显示模块,DDS模块,旋钮模块,分频器模块,锁相环模块。

硬件介绍
本次使用的是硬禾学堂提供的基于小脚丫FPGA的电赛训练平台。使用的综合软件是Lattic Diamond。

小脚丫STEP企业店在售的便携示波器:

实现的功能以及图片展示:




主要代码片段及说明
1.顶层模块
通过顶层代码连接各个模块并传递参数。Verilog部分代码如下:
module TOP_1(input clk_in, //系统时钟input rst_n_in, //系统复位,低有效input key_a, //旋转编码器A管脚input key_b, //旋转编码器B管脚input change, //切换按钮 频率 幅度input way, //切换按钮 波形图像output oled_rst, //OLCD液晶屏复位output oled_dcn, //OLCD数据指令控制output oled_clk, //OLCD时钟信号output oled_dat, //OLCD数据信号output dac_clk,output [9:0] dac_data);//////略///dds_main u1(.clk(clkop),.dac_data(dac_data),.dac_clk(dac_clk),.check(check_1),.frequence(F),.range(range));posedge_check posedge_check_u5(.clk(clk_in),.rst_n(rst_n_in),.check(way),.pos_check(pos));a120M a120M_u3 (.CLKI(clk_in),.CLKOP(clkop));OLED_12864 OLED_12864_u1 (.clk (clk_in) ,.rst_n (rst_n_in),.data(oleddata),.data1(data1),.state1(state1),.way(way_1),.oled_csn(oled_csn),.oled_rst(oled_rst),.oled_dcn(oled_dcn),.oled_clk(oled_clk),.oled_dat(oled_dat));TEMP_1 TEMP_1_u2 (.clk(clk_in),.res(rst_n_in),.indata1(key1),.indata2(key2),.change(sum),.data(oleddata),.state1(state1),.data1(data1));XUANNIU_1 XUANNIU_1_u3(.clk(clk_in), //系统时钟.rst_n(rst_n_in), //系统复位,低有效.key_a(key_a), //旋转编码器A管脚.key_b(key_b), //旋转编码器B管脚.clk_500us(clk_500us),.key1(key1),.key2(key2),.L_pulse(L_pulse),.R_pulse(R_pulse));DIVIDE_1 #(.WIDTH(32),.N(6)) u4 (.clk(clk_in),.rst_n(rst_n_in),.clkout(clk_500us));endmodule
2.OLED模块
通过SPI协议向OLED屏幕书写数据,OLED模块中连接着ASCII码模块,可以通过旋钮模块进行动态调节数据,显示生成波形的频率和幅度。Verilog部分代码如下(部分代码参考电子森林开原代码):
module OLED_12864(input clk, //12MHz系统时钟input rst_n, //系统复位,低有效input [3:0] sw,input key_a,input key_b,input [63:0] data,input [7:0] data1,input state1,input [1:0] way,output reg oled_csn, //OLCD液晶屏使能output reg oled_rst, //OLCD液晶屏复位output reg oled_dcn, //OLCD数据指令控制output reg oled_clk, //OLCD时钟信号output reg oled_dat //OLCD数据信号);////// 略///MAIN:begin6'd0 : begin state <= INIT; end6'd1 : begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd2 : begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd3 : begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd4 : begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd5 : begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd6 : begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd7 : begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd8 : begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end6'd9 : begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 8'd0; state <= CHINESE; end6'd10: begin y_p <= 8'hb0; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 8'd2; state <= CHINESE; end6'd11: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd4; state <= CHINESE; end6'd12: begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd6; state <= CHINESE; end6'd13: if(way == 2'b00) beginy_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd0; state <= SIN;endelse if(way == 2'b01) beginy_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd8; state <= SIN;endelse if(way == 2'b10) beginy_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd16; state <= SIN;endelse if(way == 2'b11) beginy_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd24; state <= SIN;end6'd14: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 8'd8; state <= CHINESE; end6'd15: begin y_p <= 8'hb3; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 8'd10; state <= CHINESE; end6'd16: begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd10; char <= {data,"HZ"};state <= SCAN; end6'd17: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 8'd12; state <= CHINESE; end6'd18: begin y_p <= 8'hb6; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 8'd14; state <= CHINESE; end6'd19: if(state1 == 1) beginy_p <= 8'hb7; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd4; char <= {"0.",data1,"v"};state <= SCAN;endelse if(state1 == 0)beginy_p <= 8'hb7; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd4; char <= {" ",data1,"v"};state <= SCAN;end6'd21: begin cnt_main <= 6'd9; endendcase
3.ASCII码转换模块
通过旋钮进行自加自减,最后通过assign进行拼接,完成OLED动态显示过程中的变量参数设计。Verilog部分代码如下:
module TEMP_1(clk,res,indata1,indata2,change,data,state1,data1);////// 略///always@(posedge clk or negedge res) beginif(!res)begintemp[0] <= 8'b0011_0000;temp[1] <= 8'b0011_0000;temp[2] <= 8'b0011_0001;temp[3] <= 8'b0011_0010;temp[4] <= 8'b0011_0000;temp[5] <= 8'b0011_0000;temp[6] <= 8'b0011_0000;temp[7] <= 8'b0011_0000;temp1 <= 8'b0011_0001;state1 <= 1;endelse if (change == 1)beginif(indata2 == 1'b1)beginif(temp[7] > 8'b0011_0000)begintemp[7] <= temp[7] - 1'b1;endelse if(temp[7] == 8'b0011_0000)begintemp[7] <= 8'b0011_1001;if(temp[6] > 8'b0011_0000)begintemp[6] <= temp[6] - 1'b1;endelse if(temp[6] == 8'b0011_0000)begintemp[6] <= 8'b0011_1001;if(temp[5] > 8'b0011_0000)begintemp[5] <= temp[5] - 1'b1;endelse if(temp[5] == 8'b0011_0000)begintemp[5] <= 8'b0011_1001;if(temp[4] > 8'b0011_0000)begintemp[4] <= temp[4] - 1'b1;endelse if(temp[4] == 8'b0011_0000)begintemp[4] <=8'b0011_1001;if(temp[3] > 8'b0011_0000)begintemp[3] <= temp[3] - 1'b1;endelse if(temp[3] == 8'b0011_0000)begintemp[3] <= 8'b0011_1001;if(temp[2] > 8'b0011_0000)begintemp[2] <= temp[2] - 1'b1;endelse if(temp[2] == 8'b0011_0000)begintemp[2] <= 8'b0011_1001;if(temp[1] > 8'b0011_0000)begintemp[1] <= temp[1] - 1'b1;endelse if(temp[1] == 8'b0011_0000)begintemp[1] <= 8'b0011_1001;if(temp[0] > 8'b0011_0000)begintemp[0] <= temp[0] - 1'b1;endendendendendendendendend////// 略///assign data1 = temp1;assign outdata1 = temp[0];assign outdata2 = temp[1];assign outdata3 = temp[2];assign outdata4 = temp[3];assign outdata5 = temp[4];assign outdata6 = temp[5];assign outdata7 = temp[6];assign outdata8 = temp[7];assign data = {outdata1,outdata2,outdata3,outdata4,outdata5,outdata6,outdata7,outdata8};
4.DDS模块
通过相位累加器完成频率的控制Verilog代码如下(部分参考电子森林开源代码):
module dds_main(clk,frequence,check,range,dac_data,dac_clk);input clk;input [1:0] check;output [9:0] dac_data; output dac_clk;input [31:0] frequence;input [3:0] range;wire [3:0] range_1;assign range_1 = range;wire [31:0] next_phase;wire [7:0] phase;reg [31:0] a;// 相位累加器assign next_phase = (32'h00000024 + frequence * 32'h24) + a;always@(posedge clk)a <= next_phase;assign phase = a[31:24];wire [9:0] sine_data;lookup_tables u_lookup_tables(phase,check,range_1,sine_data);assign dac_data = sine_data;assign dac_clk = ~clk;endmodule
5.正弦波 三角波 方波的波表以及幅度调节模块
通过制作的波表使DAC可以输出想要的波形,此模块中还含有波形幅度控制模块,可以配合旋钮进行波形幅度的控制。Verilog部分代码如下(部分参考电子森林开源代码):
module lookup_tables(phase,check,range,sin_out);////// 略///always @(sel or sine_table_out or phase)beginif(check == 2'b00) begincase(sel)2'b00: beginsine_onecycle_amp = 9'h12C+sine_table_out[8:0];address = phase[5:0];end2'b01: beginsine_onecycle_amp = 9'h12C+sine_table_out[8:0];address = ~phase[5:0];end2'b10: beginsine_onecycle_amp = 9'h12C-sine_table_out[8:0];address = phase[5:0];end2'b11: beginsine_onecycle_amp = 9'h12C-sine_table_out[8:0];address = ~ phase[5:0];endendcaseendelse if(check == 2'b01) begincase(sel)2'b00: beginsine_onecycle_amp = sine_table_out[8:0];address1 = phase[7:0];end2'b01: beginsine_onecycle_amp = sine_table_out[8:0];address1 = phase[7:0];end2'b10: beginsine_onecycle_amp = 9'd315 + sine_table_out[8:0];address1 = phase[7:0];end2'b11: beginsine_onecycle_amp = 9'd315 + sine_table_out[8:0];address1 = phase[7:0];endendcaseendelse if(check == 2'b10) begincase(sel)2'b00: beginsine_onecycle_amp = sine_table_out[8:0];address2 = phase[7:0];end2'b01: beginsine_onecycle_amp = 9'd315 + sine_table_out[8:0];address2 = phase[7:0];end2'b10: beginsine_onecycle_amp = 9'd315 + sine_table_out[8:0];address2 = phase[7:0];end2'b11: beginsine_onecycle_amp = sine_table_out[8:0];address2 = phase[7:0];endendcaseendend////// 略///module sin_table(address,address1,address2,sin,check);output [8:0] sin;input [5:0] address;input [7:0] address1;input [7:0] address2;input [1:0] check;reg [9:0] state;reg [8:0] sin;localparam SIN = 10'h1, Triangle = 10'h2, Square = 10'h4;always @(address)beginif(check == 2'b00)state <= SIN;else if(check == 2'b01)state <= Square;else if(check == 2'b10)state <= Triangle;case(state)SIN:begincase(address)6'd0: sin=9'd 0 ;6'd1: sin=9'd 7 ;6'd2: sin=9'd 15 ;6'd3: sin=9'd 3 ;6'd4: sin=9'd 29 ;6'd5: sin=9'd 36 ;6'd6: sin=9'd 44 ;6'd7: sin=9'd 51 ;////// 略///
6.旋钮模块以及分频模块
借鉴电子森林开源代码,这里就不过多描述了。
资源占用

项目总结
遇到的主要难题及解决方法
在DDS模块中,频率的控制和幅度的控制需要统一,所以要同步控制三种波形,需要提前计算或者一步一步尝试。
在OLED模块部分,一开始没有什么思路,通过学习电子森林开源代码了解了SPI协议的书写,完善了动态输出过程中ASCII转换的问题,在后面的学习中发现可以通过左移加三算法进行快速计算,相比我自己写出来的部分要好很多,但由于时间较紧张就没有进行完善。
------------ END ------------