2022寒假在家练 - 基于小脚丫FPGA实现本地控制的DDS信号发生器
一、项目介绍
本次项目使用小脚丫FPGA和电赛开发板完成。本次使用的FPGA的具体型号为Lattice LCMXO2-4000HC-4MG132。本次使用的电赛开发板上使用的板载资源有:按键、旋转编码器,128*64的OLED显示屏,10bit/120Msps的高速DAC。硬件框图如下所示:
由于我使用的是FPGA核心板,本次项目完全使用FPGA组合与时序逻辑实现,并未使用软核、硬核的嵌入式实现。本项目主要分为几个部分:DDS波形时序控制模块,OLED初始化与控制模块,编码器等外设控制逻辑模块等。具体的设计思路我会在后面详细说明。
二、设计思路与电路模块
为了实现一个本地控制的DAC我需要解决以下几个问题:①生成的输出信号参数设置:如波形、幅度、频率等等。②调整波形参数的时候,我应该如何控制OLED同步显示当前的波形参数。③给定一个波形参数,如何利用DDS生成信号波形。
围绕着以上三个问题,对于本项目我的设计思路如下图所示:
其中逻辑控制模块是为了解决第一个问题:输出信号的参数设置。参数设置是需要和用户交互的过程,用户可以控制按键、拨码开关、旋转编码器等外设来调整参数。同时,参数设置的时候,也需要实施通过OLED告诉用户当前的选择参数,以便用户能够正确的调整。最后,逻辑控制模块将确定下来的波形参数告诉DDS模块,以便DDS模块能够后续生成正确的信号。实现起来如上述框图:①该模块读取消抖的按键和旋转编码器,用以选择需要修改的参数,并修改。②模块根据内部确定的波形参数,传给OLED控制模块和bin2bcd模块进行回显控制。③模块根据内部确定的波形参数传给DDS生成模块用以生成模型。
OLED控制模块用以控制OLED显示图形,它的时序由两大部分组成:初始化时序和写入数据时序。根据bin2bcd模块的输出,以二进制表示的频率数据被转化为了BCD码表示的数字,这样就可以方便的回显到屏幕上进行显示。具体的逻辑在OLED模块讲解中会提到。
DDS模块用于根据波形、频率、幅度生成DDS所需要的数据。通过PLL ip核为该模块提供一个较高速度的时钟,驱动相位累加器产生索引,读取ROM_IP核中的数据,最终驱动DAC输出波形。
文件结构:
三、开发流程简述与资源使用情况报告
由于曾经设置过中文用户名,我的Lattice Diamond不能高亮,所以我在本次开发中使用VSCODE中的verilog相关插件进行代码编辑,导出test_bench也比较方便。开发流程如下图所示:
编译后文件的资源使用情况报告:
可以看出占用了少部分的IO接口,70%的SLICE,一个GSR,30%的EBR和50%的 PLL。
四、主要模块原理、功能及效果
1.OLED显示模块
模块输入输出介绍:
module OLED12864
(
input clk, //12MHz系统时钟
input rst_n, //系统复位,低有效
input judge_control,//0为选择幅度 1为选择频率
input [1:0] wave_select_control,//0为选择正弦波,1为选择三角波,2位选择方波
input [24:0] freq_input, //输入当前频率
input [3:0] amp_input,//输入当前幅度
input output_control,
output reg oled_rst, //OLCD液晶屏复位
output reg oled_dcn, //OLCD数据指令控制
output reg oled_clk, //OLCD时钟信号
output reg oled_dat //OLCD数据信号
);
控制接口里包括:
①选择幅度和选择频率,在屏幕上会显示一个小三角。
②输入当前的频率与幅度,经过bin2bcd及组合逻辑后显示到屏幕上。
③定义SPI接口,控制OLED芯片。
显示部分的代码主要参考寒假在家练项目(https://www.eetree.cn/project/detail/251)分享的LAttice小脚丫网盘资料(链接:https://pan.baidu.com/s/11iIyV7EqUJo99JAZj0GKwQ 提取码:yaic)中的OLED12832项目。由于代码过长,就不将全部代码放入正文中,我将解释显示逻辑架构并将主要修改的代码部分放入其中。
给出的示例代码的显示逻辑如下:
上述逻辑的主要依据为SSD1306的数据手册。
由于上述示例程序给出的显示时128*32的OLED屏幕,通过对SSD1306寄存器的研究,我发现需要相关的初始化时序修改为如下模式:
//OLED配置指令数据
always@(posedge rst_n)
begin
cmd[ 0] = {8'hae}; //Set Display OFF
cmd[ 1] = {8'h00}; //Set Lower Column Start Address for Page Addressing Modde
cmd[ 2] = {8'h10}; //Set Higher Column Start Address for Page Addressing Mode ->Start Address:0
cmd[ 3] = {8'h40}; //Set Start line
cmd[ 4] = {8'hb0}; //Set Page Start Address:0
cmd[ 5] = {8'h81}; //Set Contrast Control
cmd[ 6] = {8'hff}; //对比度:256拉满
cmd[ 7] = {8'ha1}; //Set Segment Remap:127 is mapped to SEG0
cmd[ 8] = {8'ha6}; //Set Normal Display
cmd[ 9] = {8'ha8}; //Set Multiplex Ratio
cmd[10] = {8'h3f}; //64
cmd[11] = {8'hc8}; //Set COM Output Scan Direction:(N-1)->0
cmd[12] = {8'hd3}; //Set Display Offset
cmd[13] = {8'h00}; //-> vertical shift by COM , 0d
cmd[14] = {8'hd5}; //Set Display Clock Divide and Oscillator Frequency
cmd[15] = {8'h80}; //Oscillator:1000b Divide:0+1=1
cmd[16] = {8'hd9}; //Set Pre-charge Period
cmd[17] = {8'h1f}; //
cmd[18] = {8'hda}; //Set COM Pins Hardware Configuration
cmd[19] = {8'h12}; //Sequential COM pin configuration + Disable COM Left/Right remap
cmd[20] = {8'hdb}; //Set V_COMH Deselect Level
cmd[21] = {8'h40}; //~0.65 VCC
cmd[22] = {8'h8d};
cmd[23] = {8'h14};
cmd[24] = {8'haf}; //Display On
end
将128*32的OLED改为128*64的OLED需要经过很大的修改。在上述的配置下OLED仍然是按PAGE寻址的模式。每一个PAGE包含8行的数据,而中文字符经过汉字取模软件操作后无法压缩到8行的模式(否则会出现严重失真),对于中文字符而言,每个数据应该有16行。经过页面布局的规划,我通过取模软件获得了以下三种字体的数据:5*8的ASCII字符点阵、16*8的大型英文字符点阵、16*16的中文字符点阵。
为了解决上面的问题,我在扫描字符写入阶段定义了了几种不同的模式:
localparam DIS_BLACK=2'd0, DIS_1616=2'd1, DIS_1608=2'd2, DIS_GRAPH=2'd3, DIS_0805=3'd4;
下面以DIS_1616模式扫频为例说明,代码如下所示:
else if(dis_type == DIS_1616) begin
if(dis_cnt_scan_1616 == 8'd38) dis_cnt_scan_1616 <= 8'b0;
else
dis_cnt_scan_1616 <= dis_cnt_scan_1616 + 1'b1;
case(dis_cnt_scan_1616)
8'd 0: begin oled_dcn <= CMD; char_reg <= y_p; state <= WRITE; state_back <= SCAN; end //定位列页地址
8'd 1: begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end //定位行地址低位
8'd 2: begin oled_dcn <= CMD; char_reg <= x_ph; state <= WRITE; state_back <= SCAN; end //定位行地址高位
8'd 3: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][255:248]; state <= WRITE; state_back <= SCAN; end
8'd 4: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][247:240]; state <= WRITE; state_back <= SCAN; end
8'd 5: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][239:232]; state <= WRITE; state_back <= SCAN; end
8'd 6: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][231:224]; state <= WRITE; state_back <= SCAN; end
8'd 7: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][223:216]; state <= WRITE; state_back <= SCAN; end
8'd 8: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][215:208]; state <= WRITE; state_back <= SCAN; end
8'd 9: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][207:200]; state <= WRITE; state_back <= SCAN; end
8'd10: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][199:192]; state <= WRITE; state_back <= SCAN; end
8'd11: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][191:184]; state <= WRITE; state_back <= SCAN; end
8'd12: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][183:176]; state <= WRITE; state_back <= SCAN; end
8'd13: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][175:168]; state <= WRITE; state_back <= SCAN; end
8'd14: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][167:160]; state <= WRITE; state_back <= SCAN; end
8'd15: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][159:152]; state <= WRITE; state_back <= SCAN; end
8'd16: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][151:144]; state <= WRITE; state_back <= SCAN; end
8'd17: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][143:136]; state <= WRITE; state_back <= SCAN; end
8'd18: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][135:128]; state <= WRITE; state_back <= SCAN; end
8'd19: begin oled_dcn <= CMD; char_reg <= y_p+1'b1; state <= WRITE; state_back <= SCAN; end //定位列页地址
8'd20: begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end //定位行地址低位
8'd21: begin oled_dcn <= CMD; char_reg <= x_ph; state <= WRITE; state_back <= SCAN; end //定位行地址高位
8'd22: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][127:120]; state <= WRITE; state_back <= SCAN; end
8'd23: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][119:112]; state <= WRITE; state_back <= SCAN; end
8'd24: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][111:104]; state <= WRITE; state_back <= SCAN; end
8'd25: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][103:96]; state <= WRITE; state_back <= SCAN; end
8'd26: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][95 :88]; state <= WRITE; state_back <= SCAN; end
8'd27: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][87 :80]; state <= WRITE; state_back <= SCAN; end
8'd28: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][79 :72]; state <= WRITE; state_back <= SCAN; end
8'd29: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][71 :64]; state <= WRITE; state_back <= SCAN; end
8'd30: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][63:56]; state <= WRITE; state_back <= SCAN; end
8'd31: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][55:48]; state <= WRITE; state_back <= SCAN; end
8'd32: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][47:40]; state <= WRITE; state_back <= SCAN; end
8'd33: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][39:32]; state <= WRITE; state_back <= SCAN; end
8'd34: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][31:24]; state <= WRITE; state_back <= SCAN; end
8'd35: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][23:16]; state <= WRITE; state_back <= SCAN; end
8'd36: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][15:8]; state <= WRITE; state_back <= SCAN; end
8'd37: begin oled_dcn <= DATA; char_reg <= mem_chinese[dis_index][7: 0]; state <= WRITE; state_back <= SCAN; end
8'd38: begin state <= MAIN; end
default: state <= IDLE;
endcase
end
由于每8个PAGE为一组,通过上面的方式手动操作,一下子对两个PAGE进行了操作,达到了显示16*16字符的目的。剩下的模式DIS_BLACK、DIS_1616、DIS_1608、DIS_GRAPH、DIS_0805均有类似操作,在此就不赘述了,具体可以查看代码。
MAIN中可以通过增加dis_type控制当前应当以何种数据刷屏,使用dis_index控制当前应当显示哪一组点阵数据。y_p依然不变,代表当前的Page,x_pl和x_ph分别代表高位和低位坐标,部分代码如下:
MAIN:begin
if(cnt_main >= 8'd40) cnt_main <= 8'd23;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状�
8'd0: begin state <= INIT; end
8'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_BLACK; dis_index<=8'd0; end//先刷�
8'd2: begin y_p <= 8'hb0; x_ph <= 8'h11; x_pl <= 8'h0c; state <= SCAN;dis_type <= DIS_1608; dis_index<=8'd0;end//D
8'd3: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h04; state <= SCAN;dis_type <= DIS_1608; dis_index<=8'd0;end//D
8'd4: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h0c; state <= SCAN;dis_type <= DIS_1608; dis_index<=8'd1;end//S
8'd5: begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h04; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd0; end//�
8'd6: begin y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h04; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd1; end//�
8'd7: begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h04; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd2; end//�
8'd8: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd15; end//A
8'd9: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd16; end//:
8'd10: begin y_p <= 8'hb6; x_ph <= 8'h11; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd10; end//.
8'd11: begin y_p <= 8'hb6; x_ph <= 8'h12; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd12; end//V
8'd12: begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd14; end//f
8'd13: begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd16; end//:
8'd14: begin y_p <= 8'hb7; x_ph <= 8'h15; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd11; end//H
8'd15: begin y_p <= 8'hb7; x_ph <= 8'h15; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=8'd13; end//z
8'd16: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd3; end//�
8'd17: begin y_p <= 8'hb2; x_ph <= 8'h11; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd4; end//�
8'd18: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_1608; dis_index<=8'd2; end//
8'd19: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd7; end//�
8'd20: begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd11; end//�
8'd21: begin y_p <= 8'hb4; x_ph <= 8'h11; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_1616; dis_index<=8'd12; end//�
8'd22: begin y_p <= 8'hb4; x_ph <= 8'h12; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_1608; dis_index<=8'd2; end//
8'd23: begin y_p <= 8'hb6; x_ph <= 8'h17; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=judge_obey_l1; end//< 选择指示
8'd24: begin y_p <= 8'hb6;x_ph <= 8'h17; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=judge_obey_r1; end//- 选择指示
8'd25: begin y_p <= 8'hb7; x_ph <= 8'h17; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=judge_obey_l2; end//< 选择指示
8'd26: begin y_p <= 8'hb7;x_ph <= 8'h17; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=judge_obey_r2; end//- 选择指示
8'd27: begin y_p <= 8'hb6; x_ph <= 8'h11; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=amp_l; end//0 电压的整数位
8'd28: begin y_p <= 8'hb6; x_ph <= 8'h12; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=amp_r; end//1 电压的小数位
8'd29: begin y_p <= 8'hb7; x_ph <= 8'h11; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_7; end//1 //频率�位[最低位�]
8'd30: begin y_p <= 8'hb7; x_ph <= 8'h11; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_6; end//2 //频率�位[最低位�]
8'd31: begin y_p <= 8'hb7; x_ph <= 8'h12; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_5; end//0 //频率�位[最低位�]
8'd32: begin y_p <= 8'hb7; x_ph <= 8'h12; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_4; end//0 //频率�位[最低位�]
8'd33: begin y_p <= 8'hb7; x_ph <= 8'h13; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_3; end//0 //频率�位[最低位�]
8'd34: begin y_p <= 8'hb7; x_ph <= 8'h13; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_2; end//0 //频率�位[最低位�]
8'd35: begin y_p <= 8'hb7; x_ph <= 8'h14; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_1; end//0 //频率�位[最低位�]
8'd36: begin y_p <= 8'hb7; x_ph <= 8'h14; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=freq_dec_display_0; end//0 //频率�位[最低位�]
8'd37: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_1616; dis_index<=wave_select_obey_l; end//�波形的左面一�
8'd38: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_1616; dis_index<=wave_select_obey_r; end//�波形的右面一�
8'd39: begin y_p <= 8'hb4; x_ph <= 8'h17; x_pl <= 8'h00; state <= SCAN;dis_type <= DIS_0805; dis_index<=output_obey_l; end//- 选择指示
8'd40: begin y_p <= 8'hb4; x_ph <= 8'h17; x_pl <= 8'h08; state <= SCAN;dis_type <= DIS_0805; dis_index<=output_obey_r; end//> 选择指示
default: state <= IDLE;
endcase
end
经过上述操作可以在给定频率、波形、频率、幅度信息、当前修改参数、是否输出的条件下达到如下的显示效果:左箭头表示当前选择的参数,右箭头表示是否输出。
实现效果如下图所示:
2.DDS模块原理
DDS发生器主要有两部分组成:相位累加器,波形查找表。
它的原理是:(图片来自于https://www.eetree.cn/wiki/dds_verilog )
用一个查找表来储存对应信号不同相位所对应的电压数值,再由相位累加器维护t时刻信号的相位信息,由查找表的映射关系就能得到
根据项目的要求:生成模拟信号的频率范围为DC-20MHz,调节精度为1Hz。
本项目实现了一个41bit的相位累加器,采用120MHz的时钟(120MHz的时钟由12MHz时钟倍频10倍得到),相位累加器的精度为:
由于调节精度为1Hz,在该条件下,相位累加器的步进被设置为:
所以一个KHz的频率信号,对应的相位累加器的步进为KS,由于未要求信号的初始相位,则设置为0即可。
为了使得模拟信号的波形更加好看,我在设计的时候使用了255深度的查找表。根据相位累加器和当前所选的波形,只需要把对应的查找表数据给DAC,DDS信号发生器就近乎完成了。
考虑到:生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
还需要对DDS的信号进行一定的缩放,最简单的做法是将幅度乘以一个“浮点数”,由于在整数运算中没有浮点数的概念,所以使用先乘一个数M,再右移动N位,得到近似的M/(2^N)缩放效果。
上述实现的代码如下:
module dds_gen (
input wire clk,
input wire rst_n,
input wire [24:0] freq_input_dds,
input wire [3:0] ampl_input_dds,
input wire [1:0] wave_choice,
input wire output_control,
output wire [9:0] dds_to_dac
);
localparam Hz_step = 15'd18325 ;
wire [7:0] address;
reg [40:0] phase_add ;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
phase_add <=41'd0;
end
else
phase_add <= phase_add + freq_input_dds*Hz_step;
end
assign address = phase_add[40-:8];
reg sin_OutClockEn ;
reg sin_Reset ;
wire [9:0] sin_Q ;
reg tri_OutClockEn ;
reg tri_Reset ;
wire [9:0] tri_Q ;
reg rec_OutClockEn ;
reg rec_Reset ;
wire [9:0] rec_Q ;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
sin_OutClockEn <=1'b0;
tri_OutClockEn <=1'b0;
rec_OutClockEn <=1'b0;
end
else if(!output_control)begin
sin_OutClockEn <=1'b0;
tri_OutClockEn <=1'b0;
rec_OutClockEn <=1'b0;
end
else begin
case(wave_choice)
2'd0:begin sin_OutClockEn <=1'b1;tri_OutClockEn <=1'b0;rec_OutClockEn <=1'b0; end
2'd1:begin sin_OutClockEn <=1'b0;tri_OutClockEn <=1'b1;rec_OutClockEn <=1'b0; end
2'd2:begin sin_OutClockEn <=1'b1;tri_OutClockEn <=1'b0;rec_OutClockEn <=1'b1; end
default:begin sin_OutClockEn <=1'b1;tri_OutClockEn <=1'b0;rec_OutClockEn <=1'b0; end
endcase
end
end
reg [9:0] signal_dat;
reg [9:0] a_ver; //用于调幅的因数
reg [19:0] amp_dat; //调幅后的波形数据
always @(*) begin
case(wave_choice)
2'd0:signal_dat = sin_Q;
2'd1:signal_dat = tri_Q;
2'd2:signal_dat = rec_Q;
default:signal_dat = sin_Q;
endcase
end
always @(*) begin
case(ampl_input_dds)
// 4'd0:a_ver = 10'd0;
// 4'd1:a_ver = 10'd51;
// 4'd2:a_ver = 10'd102;
// 4'd3:a_ver = 10'd153;
// 4'd4:a_ver = 10'd205;
// 4'd5:a_ver = 10'd256;
// 4'd6:a_ver = 10'd307;
// 4'd7:a_ver = 10'd358;
// 4'd8:a_ver = 10'd409;
// 4'd9:a_ver = 10'd461;
// 4'd10:a_ver = 10'd512;
// default:a_ver = 8'd0;
4'd0:a_ver = 10'd0;
4'd1:a_ver = 10'd42;
4'd2:a_ver = 10'd84;
4'd3:a_ver = 10'd125;
4'd4:a_ver = 10'd167;
4'd5:a_ver = 10'd209;
4'd6:a_ver = 10'd251;
4'd7:a_ver = 10'd293;
4'd8:a_ver = 10'd334;
4'd9:a_ver = 10'd376;
4'd10:a_ver = 10'd418;
default:a_ver = 8'd0;
endcase
end
always @(posedge clk) begin
amp_dat = signal_dat +signal_dat * a_ver; //波形数据乘以调幅因数
end
assign dds_to_dac = amp_dat[19-:10] ; //取高十位输出,相当于右移10位
my_sin_rom u_sin_rom (
.Address ( address ),
.OutClock ( clk ),
.OutClockEn ( sin_OutClockEn ),
.Reset ( ~rst_n ),
.Q ( sin_Q [9:0] )
);
my_tri_rom u_tri_rom (
.Address ( address ),
.OutClock ( clk ),
.OutClockEn ( tri_OutClockEn ),
.Reset ( ~rst_n ),
.Q ( tri_Q [9:0] )
);
my_rec_rom u_rec_rom(
.Address ( address ),
.OutClock ( clk ),
.OutClockEn ( rec_OutClockEn ),
.Reset ( ~rst_n ),
.Q ( rec_Q [9:0] )
);
Endmodule
3.调节参数模块及实现:
本项目中,我使用拨码开关来选择当前的输入波形,通过两个按键:一个选择需要调节的参数、另一个选择是否输出信号来控制系统交互,通过旋转编码器来改变电压、频率的大小,通过旋转编码器上的按键来选择步进的挡位:1Hz 10Hz 100Hz 1000Hz 10000Hz
控制逻辑代码如下:
module input_control (
input wire clk,
input wire rst_n,
input wire key_a,
input wire key_b,
input wire key_ok,
input wire judge_control,
input wire [1:0] wave_select_control,
input wire output_control,
input wire Left_pulse ,
input wire Right_pulse ,
input wire OK_pulse ,
output reg [24:0] freq_input, //= 25'h58dd11;
output reg [3:0] amp_input,// = 4'd8;
output reg judge_control_state,
output reg output_control_state
);
reg [24:0] freq_step;// = 25'd10;
reg [2:0] freq_state;
localparam MAX_FREQ = 25'd20000000;
localparam MAX_AMPL = 4'd10;
localparam STEP_NUM =3'd5; //五个档位
wire judege_input_pulse;
wire output_control_pulse;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
freq_state<=3'd0;
end
else begin
if(OK_pulse==1'b1) begin
freq_state <= freq_state + 3'd1;
end
else begin
if(freq_state == STEP_NUM)
freq_state <= 3'd0;
else
freq_state<=freq_state;
end
end
end
always @(*) begin
case(freq_state)
3'd0:freq_step<=25'd100;
3'd1:freq_step<=25'd1000;
3'd2:freq_step<=25'd10000;
3'd3:freq_step<=25'd1;
3'd4:freq_step<=25'd10;
default:freq_step<=25'd100;
endcase
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
freq_input <= 25'd15000;
end
else if(judge_control_state) begin
if(Left_pulse == 1'b1 && (freq_input+freq_step)<=MAX_FREQ)
freq_input <= freq_input+freq_step;
else if(Right_pulse == 1'b1 && (freq_input>=freq_step))
freq_input <= freq_input-freq_step;
else
freq_input <= freq_input;
end
else
freq_input <= freq_input;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
amp_input <= 4'd3;
end
else if(!judge_control_state) begin
if(Left_pulse == 1'b1 && (amp_input+4'd1)<=MAX_AMPL)
amp_input <= amp_input+4'd1;
else if(Right_pulse == 1'b1 && (amp_input>=4'd1))
amp_input <= amp_input-4'd1;
else
amp_input <= amp_input;
end
else
amp_input <= amp_input;
end
debounce u_judge(
.clk(clk),
.rst(rst_n),
.key(judge_control),
.key_pulse(judege_input_pulse)
);
debounce u_output(
.clk(clk),
.rst(rst_n),
.key(output_control),
.key_pulse(output_control_pulse)
);
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
judge_control_state<=1'b0;
else if (judege_input_pulse)
judge_control_state<=~judge_control_state;
else
judge_control_state<=judge_control_state;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
output_control_state<=1'b0;
else if (output_control_pulse)
output_control_state<=~output_control_state;
else
output_control_state<=output_control_state;
end
endmodule
其中旋转编码器的原理及实现未列出,参考:
https://www.stepfpga.com/doc/%E6%97%8B%E8%BD%AC%E7%BC%96%E7%A0%81%E5%99%A8%E6%A8%A1%E5%9D%97
按键消抖的原理及实现,参考:
https://www.stepfpga.com/doc/7._%E6%8C%89%E9%94%AE%E6%B6%88%E6%8A%96
最终实现效果:
五、遇到的主要难题及解决方法
主要难题①:
128*64的显示问题,参考为128*32的oled直接移植也能显示,但中间有空行。
解决难题①:
阅读SSD1306的芯片手册,通过阅读寄存器表,更改初始化参数向量,使得屏幕能够显示为128*64
主要难题②:
8行的以PAGE寻址的刷屏方式,如何操作16行的中文数据。
解决难题②:
通过大量修改代码,增加dis_type寄存器,分类刷新PAGE。
主要难题③:
如何将内部维护的二进制数据,映射为屏幕可以显示的数字。
解决难题③:
通过引入bin2bcd模块,将二进制数转化为BCD码用以映射对应的数组位置。
六:未来的计划及建议:
1.别的都还不错,除了我这个板子是纯FPGA的,如果可以结合STM32硬核嵌入式开发会更好。
2.Lattice diamond有bug,我用ip核的时候特别不方便,在我的机器上会出现ip和窗口紊乱的情况,使用IP核不方便。遇到上述情况我只有重新启动才能解决问题。如果可以在在线编程工具中也提供IP核的接口就更好了。
3.仿真工具结合Modelsim需要引入lattice对应的库文件,而这样的仿真环境可以被放在在线编程调试工具中,这样会大大减少我们的开发学习时间。