项目介绍:
本作品是基于小脚丫FPGA——MX02电赛训练板的DDS信号发生器,MX02采用的是一个12M的时钟,用顶层模块来控制频率控制模块,波形控制模块,幅度控制模块以及oled显示模块,输出的波形是用ROM的ip核,储存了正弦波。方波,锯齿波和三角波三种波形则是截取一部分数据作为输出从而产生不同的信号,并采用Diamond自带的PLL IP核输出一个120M的时钟提供给DAC,组合在一起最终构成了DDS信号发生器。
项目要求:
项目三:
- 通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
- 生成模拟信号的频率范围为DC-2MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
- 在OLED上显示当前波形的形状、波形的频率以及幅度
- 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
项目硬件及实现功能:
- 采用全FPGA的MX02系统板配合电赛训练板,项目采用了旋转编码器,6个按键和一个OLED屏幕用来显示参数。
- 按下编码器后,可以大幅改变输出频率,旋转编码器,可以以1HZ的精度改变输出频率,频率范围为DC-2MHz。
- 按下训练板上的按键后,可以改变峰峰值,调节范围为0.1V-1V。
- 按下MX02系统板上的四个按键,可以分别改变输出波形。
- 可以在OLED上显示当前波形的形状、波形的频率以及幅度,每次改变参数,OLED上的数据也会相应的改变。
设计思路:
本次设计的DDS信号发生器主要分为频率设置模块、波形设置模块、幅度设置模块,按键消抖模块、编码器驱动模块、DDS模块、bin转bcd码模块、去零模块、oled显示模块和顶层控制模块。
频率设置模块:
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
fword <= 32'd0;
else if(OK_pulse)
begin
case(counter)
4'd0: fword = 32'd36; // 1HZ
4'd1: fword = 32'd3580; // 100HZ
4'd2: fword = 32'd17896; // 500HZ
4'd3: fword = 32'd35792; // 1KHZ
4'd4: fword = 32'd178958; // 5KHZ
4'd5: fword = 32'd357914; // 10KHZ
4'd6: fword = 32'd1789570; // 50KHZ
4'd7: fword = 32'd3579140; // 100KHZ
4'd8: fword = 32'd17895698; //500KHZ
4'd9: fword = 32'd35791396; //1MHZ
4'd10: fword = 32'd71582790; //2MHZ
default: fword = 32'd36;
endcase
end
else if(Left_pulse)
begin
if(fword == 0)
fword <= fword;
else
fword <= fword - 32'd36;
end
else if(Right_pulse)
begin
if(fword >= 32'd71582790)
fword <= 0;
else
fword <= fword + 32'd36;
end
else
fword <= fword;
end
想要按下编码器时改变频率控制字,从而达到改变波形的目的,根据OK_pulse信号,每出现一次高电平就改变一次fword的值。
波形设置模块:
always@(posedge clk or negedge rst_n)
if(!rst_n)
wave <= 0; //默认为正弦波
else if(key_pulse[0])
wave <= 2'd0; //正弦波
else if(key_pulse[1])
wave <= 2'd1; //三角波
else if(key_pulse[2])
wave <= 2'd2;//锯齿波
else if(key_pulse[3])
wave <= 2'd3;//方波
else
wave <= wave;
例化了按键消抖模块之后,根据核心板上面自带的四个按键来控制波形的改变。
幅度设置模块:
always @(*)
begin
case(cnt)
2'd0: Vpp <= 10'd1023; //1vpp
2'd1: Vpp <= 10'd512; //0.5vpp
2'd2: Vpp <= 10'd256; //0.25vpp
2'd3: Vpp <= 10'd128; //0.125vpp
default:Vpp <= 10'd1023;
endcase
end
按键按下后改变cnt的值,从而改变Vpp。在DDS模块中让wave_data和Vpp相乘,得到改变幅度之后的数据V_data。
reg [19:0] V_data;
always @(posedge clk120M or negedge rst_n) begin
if(!rst_n)
V_data <= 0;
else
V_data <= wave_data*Vpp;
end
assign Data = V_data[19:10];
编码器驱动模块:
//通过旋转编码器A信号的边沿和B信号的电平状态的组合判断旋转编码器的操作,并输出对应的脉冲信号
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
Right_pulse <= 1'b0;
Left_pulse <= 1'b0;
end else begin
if(A_pos && B_state) Left_pulse <= 1'b1;
else if(A_neg && B_state) Right_pulse <= 1'b1;
else begin
Right_pulse <= 1'b0;
Left_pulse <= 1'b0;
end
end
end
主要是对于编码器如何判断左转还是右转有一个了解,直接可以简单地理解为
- 当A信号上升沿时B信号为低电平,或当A信号下降沿时B信号为高电平,证明当前编码器为顺时针转动
- 当A信号上升沿时B信号为高电平,或当A信号下降沿时B信号为低电平,证明当前编码器为逆时针转动
需要注意的是消除亚稳态,以免产生难以预料的后果,本模块基本上是采用的电子森林提供的例程。
按键消抖模块:
本模块自己尝试写过,但是写出来比较冗长,后面发现webide上面提供的例程,十分精炼,故直接采用例程模块。
DDS模块:
DDS信号发生器的重点就是相位累加器部分,总体的代码并不多。
always @(posedge clk120M or negedge rst_n) begin
if(!rst_n)
phase_acc <= 32'b0;
else
phase_acc <= phase_acc + Fword_r;
end
wire [31:0] phase = phase_acc;
reg [9:0] Rom_addr;
always @(posedge clk120M or negedge rst_n) begin
if(!rst_n)
Rom_addr <= 10'd0;
else
Rom_addr <= phase[31:22] + Pword_r;
end
根据板上自带的PLL,产生了一个120M的时钟,ip核的使用方法在官方资料和网上都可以直接查询到。
pll_120M pll_dac_clk(
.CLKI(clk),
.CLKOP(clk120M)
);
期间尝试了各种不同的方式来产生波形,最终采用了rom IP核存储正弦波表,获得查找表,其他波形直接赋值产生的方法。
- 正弦波(存储深度为1024,位宽10bit),直接用网上下载的正弦波数据生成器生成.mif文件,将其内容修改为Lattice支持的.mem文件(具体格式官方数据手册中有写到)。(网上可以找到相关教程Lattice rom的使用教程)
sin_wave sin_wave //调用rom核,
(
.Address(Rom_addr), //地址
.OutClock(clk120M), //时钟
.OutClockEn(1'b1), //输出使能,高有效
.Reset(1'b0), //复位,高有效
.Q(Data0)
);
- 其他波形,学习的电子森林的讲解文章:波形信号发生器设计
//三角波:
assign Data1 = phase[31]? (~phase[30:21]): phase[30:21];
//锯齿波:
assign Data2 = phase1[31:22];
//方波:
assign Data3 = phase1[31] ? 10'd1023:10'd0;
本项目因为是单通道,并不需要改变相位,所以对于相位控制字在例化的时候赋值为0。
生成的方波和锯齿波的频率一直是正弦波和三角波的2倍,,在对频率控制字做了左移一位处理后,频率变得一致,但是在高频时会失真严重,具体如何解决还没找到方法。
reg [31:0] phase_acc1;
always @(posedge clk120M or negedge rst_n) begin
if(!rst_n)
phase_acc1 <= 32'b0;
else
phase_acc1 <= phase_acc1 + (Fword_r>>1);
end
oled显示模块:
因为初次学习FPGA,这个oled驱动模块是我弄的最久的模块,因为找到了oled上面的例程,所以直接在上面修改。刚开始一眼望过去全是代码,看得晕头转向,后来在网上不断翻阅oled驱动的资料,慢慢才开始理解部分代码的具体含义,这里推荐一篇不错的文章,大家想要学习的,可以自行查看:OLED显示模块驱动原理及应用。初次接触到oled驱动的同学,找不到入手的方向的话,建议可以看看指令有哪些,尝试自己修改一下,以及电子森林例程里面的MAIN状态部分代码,尝试修改一些数据,看是否能达到自己想要的效果。根据例程,我改动的地方主要是下面:
MAIN:begin
if(cnt_main >= 5'd16) cnt_main <= 5'd10;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
5'd0: begin state <= INIT; end
5'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "----------------";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "STEP FPGA";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " D D S ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " Parameter ";state <= SCAN; end
5'd5: begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Wav: ";state <= SCAN; end
5'd6: begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Fre: HZ";state <= SCAN; end
5'd7: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Vpp: . V";state <= SCAN; end
5'd8: begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "----------------";state <= SCAN; end
5'd9: begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd8; char <= " sine";state <= SCAN; end
//变化状态
5'd10: begin if(wave == 2'd0) begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd8; char <= " Sine";state <= SCAN; end end
5'd11: begin if(wave == 2'd1) begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd8; char <= "Triangle";state <= SCAN; end end
5'd12: begin if(wave == 2'd2) begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd8; char <= "Sawtooth";state <= SCAN; end end
5'd13: begin if(wave == 2'd3) begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd8; char <=" Square";state <= SCAN; end end
5'd14: begin y_p <= 8'hb5; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd10; char <= freq;state <= SCAN; end
5'd15: begin y_p <= 8'hb6; x_ph <= 8'h16; x_pl <= 8'h08; num <= 5'd1; char <= vpp_p;state <= SCAN; end
5'd16: begin y_p <= 8'hb6; x_ph <= 8'h15; x_pl <= 8'h08; num <= 5'd1; char <= vpp_z; state <= SCAN; end
default: state <= IDLE;
endcase
end
开始的时候尝试直接用频率控制字进行运算后,把得到的输出频率显示在oled上面,但是经过除法和bcd码转换之后,会出现几HZ的误差,于是采用了直接用编码器对Freq的数值进行改变,再将freq转化为bcd码,并进行去零操作的方法,从而勉强达到目的,但是感觉这种做法不是很可行。
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
fout <= 32'd0;
else if(OK_pulse)
begin
case(CNT)
4'd0: fout = 32'd1 ;// 1HZ
4'd1: fout = 32'd100 ;// 100HZ
4'd2: fout = 32'd500 ;// 500HZ
4'd3: fout = 32'd1000 ;// 1KHZ
4'd4: fout = 32'd5000 ;// 5KHZ
4'd5: fout = 32'd10000 ; // 10KHZ
4'd6: fout = 32'd50000 ; // 50KHZ
4'd7: fout = 32'd100000 ; // 100KHZ
4'd8: fout = 32'd500000 ; //500KHZ
4'd9: fout = 32'd1000000; //1MHZ
4'd10: fout = 32'd2000000; //2MHZ
default: fout = 32'd1;
endcase
end
else if(Left_pulse)
begin
if(fout == 0)
fout <= fout;
else
fout <= fout - 32'd1;
end
else if(Right_pulse)
begin
if(fout >= 32'd2000000)
fout <= 0;
else
fout <= fout + 32'd1;
end
else
fout <= fout;
end
//=======数据处理=============//
bin2bcd bin2bcd
(
.clk(clk),
.rst_n(rst_n),
.start(1'b1),
.bin_code(fout),
.done(done),
.bcd_code(bcd_code)
);
zero_clear zero_clear(
.code(bcd_code),
.data1(freq)
);
顶层控制模块:
主要是将所有的子模块例化合并在一起。
//======fword===========//
Fword_set Fword_set(
.clk(clk),
.rst_n(rst_n),
.key_a(key_a),
.key_b(key_b),
.key_ok(key_ok),
.fword(fword)
);
//======wave/vpp========//
wave_set wave_set(
.clk(clk),
.rst_n(rst_n),
.key(key), //4个按键分别控制产生四种波形
.wave(wave)
);
Vpp_set Vpp_set(
.clk(clk),
.rst_n(rst_n),
.SW_S1(SW_S1),
.Vpp(Vpp) //产生4种幅度
);
//==========DDS=============//
DDS DDS(
.clk(clk),
.rst_n(rst_n),
.fword(fword),
.pword(10'd0),
.wave(wave),
.Vpp(Vpp),
.Data(Data),
.Data_clk(Data_clk)
);
//============oled==========//
OLED12832 OLED12864
(
.clk(clk),
.rst_n(rst_n),
.key_a(key_a),
.key_b(key_b),
.key_ok(key_ok),
.Vpp(Vpp),
.wave(wave),
.oled_csn(oled_csn),
.oled_rst(oled_rst),
.oled_dcn(oled_dcn),
.oled_clk(oled_clk),
.oled_dat(oled_dat)
);
modelsim仿真:
最终得到了一个频率、波形、相位可调的DDS信号发生器。
当频率在500KHZ之后,存在比较明显失真。后面发现是调幅度的时候,采用了除法,导致波形失真,将幅度调节模块改良之后,问题得到解决。
成果图展示:
资源报告:
Design Summary
Number of registers: 699 out of 4635 (15%)
PFU registers: 699 out of 4320 (16%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1162 out of 2160 (54%)
SLICEs as Logic/ROM: 1162 out of 2160 (54%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 224 out of 2160 (10%)
Number of LUT4s: 2311 out of 4320 (53%)
Number used as logic LUTs: 1863
Number used as distributed RAM: 0
Number used as ripple logic: 448
Number used as shift registers: 0
Number of PIO sites used: 26 + 4(JTAG) out of 105 (29%)
Number of block RAMs: 2 out of 10 (20%)
Number of GSRs: 1 out of 1 (100%)
EFB used : No
JTAG used : No
Readback used : No
Oscillator used : No
Startup used : No
POR : On
Bandgap : On
Number of Power Controller: 0 out of 1 (0%)
Number of Dynamic Bank Controller (BCINRD): 0 out of 6 (0%)
Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%)
Number of DCCA: 0 out of 8 (0%)
Number of DCMA: 0 out of 2 (0%)
Number of PLLs: 1 out of 2 (50%)
Number of DQSDLLs: 0 out of 2 (0%)
Number of CLKDIVC: 0 out of 4 (0%)
Number of ECLKSYNCA: 0 out of 4 (0%)
Number of ECLKBRIDGECS: 0 out of 2 (0%)
心得体会:
无意间看到了硬禾学堂发起的寒假一起练活动,觉得这是一个很好的锻炼自己能力和经验的机会。从刚上大学的懵懂无知到现在已经1年过下去了,在新的平台就要有新的自己,不断地钻研新知识,就是对自己最大的帮助。
看到活动之后开始学习FPGA,发现其中所包含的东西真的很广阔,就像是苏老师讲的FPGA的世界,是数字的世界。在这个世界里面,只要你可以明白基本的使用方法,自己再慢慢探索原理,就能创造出自己想要的东西。以后的日子还很长,希望自己可以能够勤勤恳恳的钻研知识,一点点感受科学带人们带来的美好。
基于本次项目上的不足,我还会继续学习,争取做到懂得原理之后,能够把疑惑的地方解决得到答案。