1 项目需求
- 通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
- 生成模拟信号的频率范围为DC-20MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为1V-1V
- 在OLED上显示当前波形的形状、波形的频率以及幅度
- 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
2 设计思路及框图
2.1 设计思路
该项目使用的模块包括:板载的12MHz时钟、PLL分频后的192MHz时钟、0.91寸OLED显示屏、高速DAC模块、旋转编码器以及独立按键,大体思路如下:
- 输出的三种波形均使用DDS查找表的方式输出,以实现Top更简单的控制。
- 通过使用旋转编码器、独立按键与OLED的配合,以使得使用最少得器件完成对参数得调节。
2.2 功能框图
3 完成的功能
3.1 oled显示
如下图所示,在OLED屏上第一行到第四行分别显示当前波形的形状、当前波形的频率、当前波形的幅度、调节的步进值。
参数的左侧“*”号为光标。“*”号是为未选中状态,“=”号为选中状态。
3.2 参数调节
①光标控制:在未选中状态下(光标为“*”号),旋转编码器即可使光标在一到四行间切换。当按下板上按键K1后,光标切换状态,
②参数调节:当光标在选中状态下(光标为“=”号),旋转编码器即可调节当前行的参数,即在第一行时旋转编码器控制输出信号在四种状态(关闭、正弦波、方波、三角波)间切换;在第二与第三行时,旋转编码器会根据步进值加或减当前参数(频率调节范围为0-20M,分压值调节范围为0-1023);在第四行时,旋转编码器控制步进值在10的零到七次幂间切换。
3.3 波形输出
①波形可调:通过上述参数调节部分介绍可使输出在关闭、正弦波、方波、三角波四个状态间切换,输出波形见下图。
②频率可调:通过上述参数调节部分介绍可调节输出波形的频率,其调节范围为0-20MHz,调节精度为1Hz。其输出正弦波在频率最大值与最小值时的图片如下所示(由于示波器在1Hz时示波器不会自动计算出频率,故使用2Hz时的图片)。
③幅度可调:通过上述参数调节部分介绍可调节输出波形幅度,在输出5kHz正弦波时无明显失真幅度范围为52mV-2.48V(参数为本人使用DS7102测得,不是很严谨)。
4 主要代码
4.1 主要Verilog模块
4.2顶层模块
顶层模块的功能为调节输出波形的类型以及参数并链接各模组。(代码的具体功能看代码注释)
always @(posedge clk_in or negedge rst)
begin
if (!rst) //系统复位后设定各参数为初始值
begin
mode <= 2'b0;
oled_CSR <= 2'b0;
oled_square <= 3'b0;
Freq <= 5000;
Amp <= 1023;
end
else if(oled_ok)//光标锁定,调节各参数。
begin
if(oled_CSR[1:0] == 2'b00)//光标锁定在第一行控制输出模式
begin
if (Encoder_Left)
begin
if(mode[1:0] == 2'b00) mode[1:0] <= 2'b11;
else mode <= mode - 1;
end
else if (Encoder_Right)
begin
if(mode[1:0] == 2'b11) mode[1:0] <= 2'b00;
else mode <= mode + 1;
end
else mode <= mode;
end
if(oled_CSR[1:0] == 2'b01)//光标锁定在第二行调节输出频率
begin
if (Encoder_Left)
begin
if(Freq + (10 ** oled_square) >= 20_000_000 ) Freq <= 20000000;
else Freq <= Freq + (10 ** oled_square);
end
else if (Encoder_Right)
begin
if(Freq <= (10 ** oled_square)) Freq <= 0;
else Freq <= Freq - (10 ** oled_square);
end
else Freq <= Freq;
end
if(oled_CSR[1:0] == 2'b10)//光标锁定在第二行调节输出分压系数
begin
if (Encoder_Left)
begin
if(Amp <= (10 ** oled_square)) Amp <= 0;
else Amp <= Amp - (10 ** oled_square);
end
else if (Encoder_Right)
begin
if(Amp + (10 ** oled_square) >= 1023 ) Amp <= 1023;
else Amp <= Amp + (10 ** oled_square);
end
else Amp <= Amp;
end
if(oled_CSR[1:0] == 2'b11)//光标锁定在第二行调节步进值
begin
if (Encoder_Left)
begin
if(oled_square ==3'b111) oled_square <= 3'b000;
else oled_square <= oled_square + 1;
end
else if (Encoder_Right)
begin
if(oled_square == 3'b000) oled_square <= 3'b111;
else oled_square <= oled_square - 1;
end
else oled_square <= oled_square;
end
end
else //光标未锁定,控制光标位置。
begin
if (Encoder_Left)
begin
if(oled_CSR[1:0] == 2'b00) oled_CSR[1:0] <= 2'b11;
else oled_CSR <= oled_CSR - 1;
end
else if (Encoder_Right)
begin
if(oled_CSR[1:0] == 2'b11) oled_CSR[1:0] <= 2'b00;
else oled_CSR <= oled_CSR + 1;
end
else oled_CSR <= oled_CSR;
end
end
always @(posedge clk_in or negedge rst)//由独立按键控制光标状态是否锁定
begin
if (!rst) //系统复位后光标默认未锁定
oled_ok <= 1'b0;
else if (key_pulse)
oled_ok <= ~oled_ok;//按下按键切换光标状态
else
oled_ok <= oled_ok;
end
4.3PLL分频
在小脚丫开发板上板载的时钟为12MHz,12MHz不符合输出最高20MHz的需求,故使用PLL锁相环将频率分频至192MHz用于DDS输出频率。
4.4按键消抖与旋转编码器
按键消抖以及编码器均使用电子森林的例程,下面附上源地址:
4.5OLED显示
oled显示的代码使用电子森林的例程改动,可以先看电子森林的例程在参考我附上的代码,具体介绍看代码注释。
在OLED显示数字时需要将一个数字转化为字符串,我这里将二进制数转换成BCD码的形式,采用左移加三的算法(以8’hff为例): 1、左移要转换的二进制码1位 2、左移之后,BCD码分别置于百位、十位、个位 3、如果移位后所在的BCD码列大于或等于5,则对该值加3 4、继续左移的过程直至全部移位完成。
在本项目中将频率的二进制转BCD码程序实现如下:
reg [56:0] shift_Freq;
always@(Freq)begin
shift_Freq = {32'h0,Freq};
repeat(25) begin //循环25次
//BCD码各位数据作满5加3操作,
if (shift_Freq[28:25] >= 5) shift_Freq[28:25] = shift_Freq[28:25] + 2'b11;
if (shift_Freq[32:29] >= 5) shift_Freq[32:29] = shift_Freq[32:29] + 2'b11;
if (shift_Freq[36:33] >= 5) shift_Freq[36:33] = shift_Freq[36:33] + 2'b11;
if (shift_Freq[40:37] >= 5) shift_Freq[40:37] = shift_Freq[40:37] + 2'b11;
if (shift_Freq[44:41] >= 5) shift_Freq[44:41] = shift_Freq[44:41] + 2'b11;
if (shift_Freq[48:45] >= 5) shift_Freq[48:45] = shift_Freq[48:45] + 2'b11;
if (shift_Freq[52:49] >= 5) shift_Freq[52:49] = shift_Freq[52:49] + 2'b11;
if (shift_Freq[56:53] >= 5) shift_Freq[56:53] = shift_Freq[56:53] + 2'b11;
shift_Freq = shift_Freq << 1;
end
end
以下程序为对电子森林的OLED例程更改的部分,以实现对光标、频率、分压系数以及步进值的显示。
MAIN:begin //多少次改这里 ↓
if(cnt_main >= 6'd29) cnt_main <= 6'd9;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN??
//初始化屏幕界面
6'd0: begin state <= INIT; end
6'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " Type: ";state <= SCAN; end
6'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " Freq: ";state <= SCAN; end
6'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " Amp: ";state <= SCAN; end
6'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " SFS: ";state <= SCAN; end
6'd5: begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
6'd6: begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
6'd7: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " ";state <= SCAN; end
6'd8: begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "By:KuKuDePang_er";state <= SCAN; end
//自定义功能
6'd9: //显示光标
begin
if(oled_ok == 0)
begin
if(oled_CSR[1:0] == 2'b00)
begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= 42; state <= SCAN; end
else
begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
else
begin
if(oled_CSR[1:0] == 2'b00)
begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= 61; state <= SCAN; end
else
begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
end
6'd10: //显示光标
begin
if(oled_ok == 0)
begin
if(oled_CSR[1:0] == 2'b01)
begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= 42; state <= SCAN; end
else
begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
else
begin
if(oled_CSR[1:0] == 2'b01)
begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= 61; state <= SCAN; end
else
begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
end
6'd11: //显示光标
begin
if(oled_ok == 0)
begin
if(oled_CSR[1:0] == 2'b10)
begin y_p <= 8'hb2; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= 42; state <= SCAN; end
else
begin y_p <= 8'hb2; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
else
begin
if(oled_CSR[1:0] == 2'b10)
begin y_p <= 8'hb2; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= 61; state <= SCAN; end
else
begin y_p <= 8'hb2; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
end
6'd12: //显示光标
begin
if(oled_ok == 0)
begin
if(oled_CSR[1:0] == 2'b11)
begin y_p <= 8'hb3; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= 42; state <= SCAN; end
else
begin y_p <= 8'hb3; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
else
begin
if(oled_CSR[1:0] == 2'b11)
begin y_p <= 8'hb3; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= 61; state <= SCAN; end
else
begin y_p <= 8'hb3; x_ph <= 8'h00; x_pl <= 8'h00; num <= 5'd01; char <= " "; state <= SCAN; end
end
end
6'd13://波形类型
begin
if(mode == 2'b00) begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= " OFF "; state <= SCAN; end
else if(mode == 2'b01) begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= " sine "; state <= SCAN; end
else if(mode == 2'b10) begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= " square "; state <= SCAN; end
else begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "triangular"; state <= SCAN; end
end
6'd14://Freq_ge 14-23显示输出波形的频率
begin y_p <= 8'hb1; x_ph <= 8'h17; x_pl <= 8'h08; num <= 5'd01; char <= shift_Freq[28:25]; state <= SCAN; end
6'd15://Freq_shi
begin y_p <= 8'hb1; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd01; char <= shift_Freq[32:29]; state <= SCAN; end
6'd16://Freq_bai
begin y_p <= 8'hb1; x_ph <= 8'h16; x_pl <= 8'h08; num <= 5'd01; char <= shift_Freq[36:33] ; state <= SCAN; end
6'd17://千分符
begin y_p <= 8'hb1; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd01; char <= ","; state <= SCAN; end
6'd18://Freq_qian
begin y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h08; num <= 5'd01; char <= shift_Freq[40:37]; state <= SCAN; end
6'd19://Freq_wan
begin y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd01; char <= shift_Freq[44:41]; state <= SCAN; end
6'd20://Freq_shiwan
begin y_p <= 8'hb1; x_ph <= 8'h14; x_pl <= 8'h08; num <= 5'd01; char <= shift_Freq[48:45]; state <= SCAN; end
6'd21://百万分符号
begin y_p <= 8'hb1; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd01; char <= ","; state <= SCAN; end
6'd22://Freq_baiwan
begin y_p <= 8'hb1; x_ph <= 8'h13; x_pl <= 8'h08; num <= 5'd01; char <= shift_Freq[52:49]; state <= SCAN; end
6'd23://Freq_qianwan
begin y_p <= 8'hb1; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd01; char <= shift_Freq[56:53]; state <= SCAN; end
6'd24://步进显示
begin
if(oled_square == 3'b000) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*1 "; state <= SCAN; end
else if(oled_square == 3'b001) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*10 "; state <= SCAN; end
else if(oled_square == 3'b010) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*100 "; state <= SCAN; end
else if(oled_square == 3'b011) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*1000 "; state <= SCAN; end
else if(oled_square == 3'b100) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*10000 "; state <= SCAN; end
else if(oled_square == 3'b101) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*100000 "; state <= SCAN; end
else if(oled_square == 3'b110) begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*1000000 "; state <= SCAN; end
else begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd10; char <= "*10000000 "; state <= SCAN; end
end
6'd25://Amp_ge 25-29显示输出波形的分压系数
begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd01; char <= shift_Amp[19:16]; state <= SCAN; end
6'd26://Amp_shi
begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h08; num <= 5'd01; char <= shift_Amp[23:20]; state <= SCAN; end
6'd27://Amp_bai
begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd01; char <= shift_Amp[27:24]; state <= SCAN; end
6'd28://千分符
begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h08; num <= 5'd01; char <= ","; state <= SCAN; end
6'd29://Amp_qian
begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd01; char <= shift_Amp[31:28]; state <= SCAN; end
default: state <= IDLE;
endcase
end
4.6DDS
DDS在电子森林的例程讲解的非常详细,我只是将查一个正弦波表改为正弦波、方波、三角波三个波表,下面附上电子森林的讲解,以及我改动的地方:
sin_table u_sin_table(address,sine_table_out); //例化正弦波表
square_table u_square_table(address,square_table_out); //例化方波表
triangular_table u_triangular_table(address,triangular_table_out); //例化三角波表
always @(sel or sine_table_out or square_table_out or triangular_table_out or mode)
begin
case(sel)
2'b00: begin
case(mode)
2'b00: onecycle_amp = 10'h0;
2'b01: onecycle_amp = 9'h1ff + sine_table_out[8:0];
2'b10: onecycle_amp = 9'h1ff + square_table_out[8:0];
2'b11: onecycle_amp = 9'h1ff + triangular_table_out[8:0];
endcase
address = phase[5:0];
end
2'b01: begin
case(mode)
2'b00: onecycle_amp = 10'h0;
2'b01: onecycle_amp = 9'h1ff + sine_table_out[8:0];
2'b10: onecycle_amp = 9'h1ff + square_table_out[8:0];
2'b11: onecycle_amp = 9'h1ff + triangular_table_out[8:0];
endcase
address = ~phase[5:0];
end
2'b10: begin
case(mode)
2'b00: onecycle_amp = 10'h0;
2'b01: onecycle_amp = 9'h1ff - sine_table_out[8:0];
2'b10: onecycle_amp = 9'h1ff - square_table_out[8:0];
2'b11: onecycle_amp = 9'h1ff - triangular_table_out[8:0];
endcase
address = phase[5:0];
end
2'b11: begin
case(mode)
2'b00: onecycle_amp = 10'h0;
2'b01: onecycle_amp = 9'h1ff - sine_table_out[8:0];
2'b10: onecycle_amp = 9'h1ff - square_table_out[8:0];
2'b11: onecycle_amp = 9'h1ff - triangular_table_out[8:0];
endcase
address = ~ phase[5:0];
end
endcase
end
5 片上资源
5.1IO口分配
5.2资源占用率
6 遇到的问题
首先给电子森林点个赞,资源非常的多,以至于本项目更像是使用电子森林上例程拼凑起来的一个项目。先一个模块一个模块的学习怎么使用,再添加上自己的想法,最后组合在一起。所以我遇到的问题大部分是这个模块怎么怎么用,还有逻辑上的一些问题。
对于模块在怎么使用在电子森林例程的介绍中都可以解决,搭配上小脚丫STEP从入门一小时就能点亮一个LED。
因为没有系统的学习过Verilog,程序刚开始都是照葫芦画瓢,后面加上自己的想法就会冒出很多的逻辑错误与语法错误,在看了Verilog 教程后一点一点的解决了这些错误。
7 未来的计划
学习一下FPGA仿真的步骤,在本项目开始想要尝试使用仿真,各种不知道怎么解决的问题没有时间去处理,只能是直接上烧程序上机跑。但是仿真是FPGA很重要的一部分,所以第一步要学习一下仿真。
在本学期备赛电子设计大赛中,使用这个电赛训练板做几个历年的信号类的题,让今年省赛不只是能选择控制类的题型,为以后仪器仪表类工作积累经验。
感谢硬禾学堂的本次活动,动辄几百块的FPGA开发板对于学生也是一笔不小的开支,我也一直在犹豫何时入手一块FPGA开发板,感谢硬禾学堂将我带进了FPGA的大门。
最后祝硬禾学堂越办越好!!!电子森林可以帮到更多的同学。