如果不借助现成的示波器软件,仅使用一块FPGA,如何独立完成一个真正可用的信号源设计与实现?
本文基于小脚丫FPGA电赛训练平台,利用其板载的125MSPS高速DAC,从直接数字频率合成(DDS)的基本原理出发,完整实现了一台可输出正弦波、三角波、方波的可调波形发生器。
项目介绍
本项目旨在实现以下功能:
- 通过板载高速DAC(10位分辨率,125Msps采样率)配合FPGA内部的DDS逻辑,生成波形形状(正弦波、三角波、方波)、频率、幅度均可调节的信号。
- 生成模拟信号的频率范围为DC-20MHz,调节精度可达1Hz。
- 生成模拟信号的幅度最大为1Vpp,调节范围为0.1V至1V。
- 在OLED屏幕上实时显示当前波形类型、频率以及幅度值。
- 利用板上的旋转编码器和按键实现波形切换与参数调节。
设计思路
本次实验主要涉及以下几个核心模块:OLED显示模块、DDS信号生成模块、旋转编码器(旋钮)输入模块、分频器模块以及锁相环(PLL)模块。这些模块协同工作,构成了整个信号发生器的系统架构,其中锁相环和分频器对于生成稳定的系统时钟至关重要,这属于底层系统与时钟管理的范畴。

硬件平台
本次开发使用的是硬禾学堂提供的基于小脚丫FPGA的电赛训练平台,综合开发工具为Lattice Diamond。

功能实现与效果展示
项目成功实现了预设的所有功能,以下为实物运行效果图:


主要代码片段及说明
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屏幕,并包含字符显示逻辑。能够动态显示由旋钮调节的波形频率和幅度参数。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:begin
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 <= " "; state <= SCAN; end
6'd2 : begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; state <= SCAN; end
6'd3 : begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; state <= SCAN; end
6'd4 : begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; 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 <= " "; state <= SCAN; end
6'd9 : begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 8'd0; state <= CHINESE; end
6'd10: begin y_p <= 8'hb0; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 8'd2; state <= CHINESE; end
6'd11: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd4; state <= CHINESE; end
6'd12: begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd6; state <= CHINESE; end
6'd13: if(way == 2'b00) begin
y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd0; state <= SIN;
end
else if(way == 2'b01) begin
y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd8; state <= SIN;
end
else if(way == 2'b10) begin
y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd16; state <= SIN;
end
else if(way == 2'b11) begin
y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_sin_num <= 8'd24; state <= SIN;
end
6'd14: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 8'd8; state <= CHINESE; end
6'd15: begin y_p <= 8'hb3; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 8'd10; state <= CHINESE; end
6'd16: begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd10; char <= {data,"HZ"}; state <= SCAN; end
6'd17: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 8'd12; state <= CHINESE; end
6'd18: begin y_p <= 8'hb6; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 8'd14; state <= CHINESE; end
6'd19: if(state1 == 1) begin
y_p <= 8'hb7; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd4; char <= {"0.",data1,"v"}; state <= SCAN;
end
else if(state1 == 0)begin
y_p <= 8'hb7; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd4; char <= {" ",data1,"v"}; state <= SCAN;
end
6'd21: begin cnt_main <= 6'd9; end
endcase
3. ASCII码转换模块
该模块处理旋钮输入,实现数值的增减,并通过赋值语句拼接成可用于OLED显示的变量参数。这里涉及了数据转换的基本逻辑。Verilog部分代码如下:
module TEMP_1(
clk,
res,
indata1,
indata2,
change,
data,
state1,
data1
);
////// 略///
always@(posedge clk or negedge res) begin
if(!res)begin
temp[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;
end
else if (change == 1)begin
if(indata2 == 1'b1)begin
if(temp[7] > 8'b0011_0000)begin
temp[7] <= temp[7] - 1'b1;
end
else if(temp[7] == 8'b0011_0000)begin
temp[7] <= 8'b0011_1001;
if(temp[6] > 8'b0011_0000)begin
temp[6] <= temp[6] - 1'b1;
end
else if(temp[6] == 8'b0011_0000)begin
temp[6] <= 8'b0011_1001;
if(temp[5] > 8'b0011_0000)begin
temp[5] <= temp[5] - 1'b1;
end
else if(temp[5] == 8'b0011_0000)begin
temp[5] <= 8'b0011_1001;
if(temp[4] > 8'b0011_0000)begin
temp[4] <= temp[4] - 1'b1;
end
else if(temp[4] == 8'b0011_0000)begin
temp[4] <=8'b0011_1001;
if(temp[3] > 8'b0011_0000)begin
temp[3] <= temp[3] - 1'b1;
end
else if(temp[3] == 8'b0011_0000)begin
temp[3] <= 8'b0011_1001;
if(temp[2] > 8'b0011_0000)begin
temp[2] <= temp[2] - 1'b1;
end
else if(temp[2] == 8'b0011_0000)begin
temp[2] <= 8'b0011_1001;
if(temp[1] > 8'b0011_0000)begin
temp[1] <= temp[1] - 1'b1;
end
else if(temp[1] == 8'b0011_0000)begin
temp[1] <= 8'b0011_1001;
if(temp[0] > 8'b0011_0000)begin
temp[0] <= temp[0] - 1'b1;
end
end
end
end
end
end
end
end
end
////// 略///
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. 波形表与幅度调节模块
该模块预存了正弦波、三角波、方波的数字波形表(Look-Up Table),并集成了幅度控制逻辑,配合旋钮实现输出幅度的调节。Verilog部分代码如下:
module lookup_tables(
phase,
check,
range,
sin_out
);
////// 略///
always @(sel or sine_table_out or phase)begin
if(check == 2'b00) begin
case(sel)
2'b00: begin
sine_onecycle_amp = 9'h12C+sine_table_out[8:0];
address = phase[5:0];
end
2'b01: begin
sine_onecycle_amp = 9'h12C+sine_table_out[8:0];
address = ~phase[5:0];
end
2'b10: begin
sine_onecycle_amp = 9'h12C-sine_table_out[8:0];
address = phase[5:0];
end
2'b11: begin
sine_onecycle_amp = 9'h12C-sine_table_out[8:0];
address = ~ phase[5:0];
end
endcase
end
else if(check == 2'b01) begin
case(sel)
2'b00: begin
sine_onecycle_amp = sine_table_out[8:0];
address1 = phase[7:0];
end
2'b01: begin
sine_onecycle_amp = sine_table_out[8:0];
address1 = phase[7:0];
end
2'b10: begin
sine_onecycle_amp = 9'd315 + sine_table_out[8:0];
address1 = phase[7:0];
end
2'b11: begin
sine_onecycle_amp = 9'd315 + sine_table_out[8:0];
address1 = phase[7:0];
end
endcase
end
else if(check == 2'b10) begin
case(sel)
2'b00: begin
sine_onecycle_amp = sine_table_out[8:0];
address2 = phase[7:0];
end
2'b01: begin
sine_onecycle_amp = 9'd315 + sine_table_out[8:0];
address2 = phase[7:0];
end
2'b10: begin
sine_onecycle_amp = 9'd315 + sine_table_out[8:0];
address2 = phase[7:0];
end
2'b11: begin
sine_onecycle_amp = sine_table_out[8:0];
address2 = phase[7:0];
end
endcase
end
end
////// 略///
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)
begin
if(check == 2'b00)
state <= SIN;
else if(check == 2'b01)
state <= Square;
else if(check == 2'b10)
state <= Triangle;
case(state)
SIN:begin
case(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. 旋钮编码器与分频模块
旋钮编码器模块用于检测用户旋转操作,分频模块用于产生系统所需的各种时钟。这部分设计借鉴了成熟的开源代码。
项目总结与思考
1. 遇到的主要难题及解决方法
在DDS模块设计中,如何统一、同步地控制三种不同波形的频率和幅度是主要挑战。这需要精确的理论计算或通过实验进行反复调试来确定参数。
在OLED显示部分,初期对SPI协议驱动和动态内容刷新缺乏思路。通过研究学习相关的开源代码,掌握了SPI通信的实现方法,并解决了动态输出过程中数值到ASCII码实时转换的难题。后续了解到更高效的算法(如左移加三算法)可用于此类转换,但因时间关系未在本项目中实施优化。
2. 经验与展望
通过本项目,对Verilog硬件描述语言在系统级设计中的应用有了更深的体会。未来希望参与更多类似的FPGA实战项目,以积累设计经验、拓宽思路,并探索如何利用更少的逻辑资源实现更复杂的功能,这对于深入理解数字系统设计精髓很有帮助。