如果不借助示波器软件,仅使用一片FPGA,能否从零构建一个真正可用的信号源?
本文基于小脚丫FPGA电赛训练平台,利用其板载的125MSPS高速DAC,从直接数字频率合成(DDS)原理出发,完整实现了一台可输出正弦波、三角波、方波的可调波形发生器,并将关键参数实时显示在OLED屏幕上。
项目目标与设计思路
本项目旨在实现以下功能:
- 通过FPGA内部的DDS逻辑驱动高速DAC(10位分辨率,125Msps采样率),生成波形形状(正弦波、三角波、方波)、频率(DC-20MHz,1Hz步进)和幅度(0.1V-1V)均可调的模拟信号。
- 在OLED屏幕上动态显示当前波形类型、频率值以及幅度值。
- 利用板载旋转编码器和按键完成波形切换与参数调节。
整个系统的设计模块主要包括:OLED显示模块、DDS核心模块、旋转编码器解码模块、分频器模块以及锁相环(PLL)时钟管理模块。下图展示了系统的整体设计架构:

硬件平台与开发环境
本次实验使用的硬件是硬禾学堂提供的小脚丫FPGA电赛训练平台,综合开发软件为Lattice Diamond。

功能实现与效果展示
项目成功实现了预设的所有功能。通过旋转编码器可以流畅地切换波形、调节频率与幅度,所有参数均实时显示在OLED屏上。下图为系统运行时的实物照片与OLED显示效果:


核心代码解析
1. 顶层模块 (TOP_1.v)
顶层模块负责例化并连接所有子模块,是整个设计的枢纽。它定义了系统的输入输出接口,并将按键、编码器信号处理后的参数传递给DDS和OLED显示模块。
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, //OLED复位
output oled_dcn, //OLED数据/指令控制
output oled_clk, //OLED时钟
output oled_dat, //OLED数据
output dac_clk,
output [9:0] dac_data
);
wire clkop;
wire [31:0] F;
wire [3:0] range;
wire [1:0] check_1;
wire [63:0] oleddata;
wire [7:0] data1;
wire state1;
// ... 内部信号声明略
// 实例化各功能模块
dds_main u1(
.clk(clkop),
.dac_data(dac_data),
.dac_clk(dac_clk),
.check(check_1),
.frequence(F),
.range(range)
);
// PLL生成所需时钟
a120M a120M_u3 (
.CLKI(clk_in),
.CLKOP(clkop)
);
// OLED显示驱动模块
OLED_12864 OLED_12864_u1 (
.clk (clk_in),
.rst_n (rst_n_in),
.data (oleddata),
.data1 (data1),
.state1 (state1),
.way (way_1),
.oled_rst(oled_rst),
.oled_dcn(oled_dcn),
.oled_clk(oled_clk),
.oled_dat(oled_dat)
);
// 参数处理与ASCII转换模块
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),
.key_b (key_b),
.clk_500us(clk_500us),
.key1 (key1),
.key2 (key2),
.L_pulse (L_pulse),
.R_pulse (R_pulse)
);
// 分频模块,产生500us时钟用于消抖
DIVIDE_1 #(.WIDTH(32),.N(6)) u4 (
.clk (clk_in),
.rst_n (rst_n_in),
.clkout (clk_500us)
);
endmodule
2. OLED显示驱动模块 (OLED_12864.v)
该模块通过SPI协议驱动OLED屏幕,并包含一个状态机来管理显示内容。它能根据输入参数动态更新波形图标、频率和幅度数值。显示内容分为固定汉字和可变参数两部分。
module OLED_12864(
input clk,
input rst_n,
input [63:0] data, //频率数字串
input [7:0] data1, //幅度数字
input state1, //幅度显示状态
input [1:0] way, //波形选择
output reg oled_csn,
output reg oled_rst,
output reg oled_dcn,
output reg oled_clk,
output reg oled_dat
);
// ... 状态机、SPI驱动等代码略
// 显示状态机部分片段
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_main <= 6'd0;
end else begin
case(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 <= " "; 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
// 根据way信号选择显示波形图标(SIN, SQUARE, TRIANGLE的字符图形)
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
// 显示频率数值
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'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
endcase
end
end
// ... 其他代码
endmodule
3. 参数处理与ASCII转换模块 (TEMP_1.v)
此模块将旋转编码器产生的计数脉冲转换为可供显示和DDS模块使用的数字参数。它内部维护着多个寄存器,用于存储频率和幅度的每一位BCD码,并通过组合逻辑输出拼接好的字符串,是完成硬件设计中“人机交互”的关键部分。
module TEMP_1(
input clk,
input res,
input indata1, //增加脉冲
input indata2, //减少脉冲
input change, //切换频率/幅度调节
output [63:0] data, //8位频率ASCII码
output state1, //幅度显示小数点状态
output [7:0] data1 //幅度ASCII码
);
reg [7:0] temp [7:0]; //存储频率8位数
reg [7:0] temp1; //存储幅度值
reg state1_r;
// 初始化与参数增减控制
always@(posedge clk or negedge res) begin
if(!res) begin
// 初始化频率为“00120000”
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; // 初始化幅度为‘1’
state1_r <= 1;
end
else if (change == 1) begin // 幅度调节模式
if(indata2 == 1'b1) begin // 减少
if(temp1 > 8'b0011_0000) begin
temp1 <= temp1 - 1'b1;
end
end
end
else begin // 频率调节模式
if(indata1 == 1'b1) begin // 频率增加,BCD码进位处理
if(temp[7] < 8'b0011_1001) begin
temp[7] <= temp[7] + 1'b1;
end else begin
temp[7] <= 8'b0011_0000;
// ... 后续位依次进位,代码结构类似,此处省略
end
end
if(indata2 == 1'b1) begin // 频率减少,BCD码借位处理
if(temp[7] > 8'b0011_0000) begin
temp[7] <= temp[7] - 1'b1;
end else begin
temp[7] <= 8'b0011_1001;
// ... 后续位依次借位,代码结构类似,此处省略
end
end
end
end
// 将8个独立的BCD码字节拼接成一个64位输出
assign data = {temp[0], temp[1], temp[2], temp[3], temp[4], temp[5], temp[6], temp[7]};
assign data1 = temp1;
assign state1 = state1_r;
endmodule
4. DDS核心模块 (dds_main.v)
这是波形发生的核心,采用经典的DDS结构。通过一个32位的相位累加器,每次累加一个与设定频率成正比的步进值,取其高8位作为查找表地址,从而循环读出波形数据。
module dds_main(
input clk,
input [31:0] frequence, //频率控制字
input [1:0] check, //波形选择
input [3:0] range, //幅度控制字
output [9:0] dac_data,
output dac_clk
);
reg [31:0] a; // 相位累加器寄存器
wire [31:0] next_phase;
wire [7:0] phase;
wire [9:0] sine_data;
wire [3:0] range_1;
assign range_1 = range;
// 相位累加:基础相位偏移 + 频率控制字 × 缩放系数
assign next_phase = (32'h00000024 + frequence * 32'h24) + a;
always@(posedge clk)
a <= next_phase;
// 取相位累加器的高8位作为波形查找表地址
assign phase = a[31:24];
// 实例化波形查找表(包含正弦、三角、方波及幅度控制)
lookup_tables u_lookup_tables(
.phase(phase),
.check(check),
.range(range_1),
.sin_out(sine_data)
);
assign dac_data = sine_data;
assign dac_clk = ~clk; // DAC时钟为系统时钟反相
endmodule
5. 波形查找表与幅度控制模块 (lookup_tables.v & sin_table.v)
该模块包含波形数据ROM和幅度调节逻辑。根据check信号选择不同的波形(正弦、三角波、方波),并根据range信号对波形数据进行缩放,实现幅度控制。
module lookup_tables(
input [7:0] phase,
input [1:0] check,
input [3:0] range,
output [9:0] sin_out
);
wire [8:0] sine_table_out; // 从ROM读出的原始数据
wire [5:0] address;
wire [7:0] address1;
wire [7:0] address2;
reg [8:0] sine_onecycle_amp;
reg [1:0] sel;
assign sel = range[1:0]; // 取range低位做幅度细分选择
// 幅度调节:通过加减一个偏移量实现直流偏置的调节,模拟幅度变化
always @(*) 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
// ... 其他case
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
// ... 其他case
endcase
end
// 方波处理部分类似,此处省略
end
// 将处理后的9位数据赋给10位输出,高位补0
assign sin_out = {1'b0, sine_onecycle_amp};
// 实例化波形数据ROM
sin_table sin_table_u(
.address(address),
.address1(address1),
.address2(address2),
.check(check),
.sin(sine_table_out)
);
endmodule
// 波形数据ROM
module sin_table(address, address1, address2, sin, check);
output reg [8:0] sin;
input [5:0] address; // 正弦波地址(64点)
input [7:0] address1; // 方波地址
input [7:0] address2; // 三角波地址(256点)
input [1:0] check;
reg [9:0] state;
always @(*) begin
case(check)
2'b00: state = 10'h1; // SIN
2'b01: state = 10'h4; // SQUARE
2'b10: state = 10'h2; // TRIANGLE
default: state = 10'h1;
endcase
case(state)
10'h1: begin // 正弦波表
case(address)
6'd0: sin = 9'd0;
6'd1: sin = 9'd7;
6'd2: sin = 9'd15;
6'd3: sin = 9'd3;
// ... 省略其余点数据
6'd63: sin = 9'd0;
endcase
end
10'h2: begin // 三角波表(上升下降各128点)
if(address2 < 8'd128)
sin = address2 * 2; // 上升沿
else
sin = 9'd511 - (address2 - 8'd128) * 2; // 下降沿
end
10'h4: begin // 方波
sin = (address1 < 8'd128) ? 9'd0 : 9'd511;
end
endcase
end
endmodule
6. 旋转编码器与分频模块
旋转编码器模块用于解码EC11型编码器的正交脉冲,判断左旋或右旋动作,并输出对应的脉冲信号。分频模块则产生较低频率的时钟用于按键消抖。这部分代码参考了成熟的开源实现。
设计总结与思考
遇到的主要难题与解决方法
- DDS参数同步:初期对DDS中频率控制字与幅度调节的协同控制理解不深,导致波形失真。通过理论计算结合实验验证,重新调整了相位累加步进与幅度缩放系数的匹配关系。
- OLED动态显示:实现变量参数在OLED上的实时刷新起初没有思路。通过研究SPI驱动原理并借鉴开源代码,掌握了将数字变量转换为ASCII码串并送入显示缓冲区的方法。后续了解到更高效的“左移加三”算法可用于二进制转BCD,可作为优化方向。
项目收获
本项目完成了一个从数字逻辑设计(Verilog编码)、系统设计(模块划分与集成)到最终硬件验证的完整流程。它超越了简单的点灯实验,涉及了时钟管理、人机交互、算法(DDS)实现、外设驱动等多个方面,是一次综合性很强的FPGA开发实践。
通过这次实践,不仅巩固了Verilog语言和FPGA开发流程,更重要的是建立了将理论算法(如DDS)转化为实际可测信号的工程化思维。选择一款资源适中、外设丰富(如集成高速DAC)的开发平台,能极大地降低学习门槛,让开发者更专注于核心逻辑与系统级的设计。