找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2228

积分

0

好友

312

主题
发表于 2025-12-25 06:43:21 | 查看: 27| 回复: 0

如果不借助示波器软件,仅使用一片FPGA,能否从零构建一个真正可用的信号源?

本文基于小脚丫FPGA电赛训练平台,利用其板载的125MSPS高速DAC,从直接数字频率合成(DDS)原理出发,完整实现了一台可输出正弦波、三角波、方波的可调波形发生器,并将关键参数实时显示在OLED屏幕上。

项目目标与设计思路

本项目旨在实现以下功能:

  1. 通过FPGA内部的DDS逻辑驱动高速DAC(10位分辨率,125Msps采样率),生成波形形状(正弦波、三角波、方波)、频率(DC-20MHz,1Hz步进)和幅度(0.1V-1V)均可调的模拟信号。
  2. 在OLED屏幕上动态显示当前波形类型、频率值以及幅度值。
  3. 利用板载旋转编码器和按键完成波形切换与参数调节。

整个系统的设计模块主要包括:OLED显示模块、DDS核心模块、旋转编码器解码模块、分频器模块以及锁相环(PLL)时钟管理模块。下图展示了系统的整体设计架构:
系统设计框图

硬件平台与开发环境

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

功能实现与效果展示

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

核心代码解析

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型编码器的正交脉冲,判断左旋或右旋动作,并输出对应的脉冲信号。分频模块则产生较低频率的时钟用于按键消抖。这部分代码参考了成熟的开源实现。

设计总结与思考

遇到的主要难题与解决方法

  1. DDS参数同步:初期对DDS中频率控制字与幅度调节的协同控制理解不深,导致波形失真。通过理论计算结合实验验证,重新调整了相位累加步进与幅度缩放系数的匹配关系。
  2. OLED动态显示:实现变量参数在OLED上的实时刷新起初没有思路。通过研究SPI驱动原理并借鉴开源代码,掌握了将数字变量转换为ASCII码串并送入显示缓冲区的方法。后续了解到更高效的“左移加三”算法可用于二进制转BCD,可作为优化方向。

项目收获

本项目完成了一个从数字逻辑设计(Verilog编码)、系统设计(模块划分与集成)到最终硬件验证的完整流程。它超越了简单的点灯实验,涉及了时钟管理、人机交互、算法(DDS)实现、外设驱动等多个方面,是一次综合性很强的FPGA开发实践。

通过这次实践,不仅巩固了Verilog语言和FPGA开发流程,更重要的是建立了将理论算法(如DDS)转化为实际可测信号的工程化思维。选择一款资源适中、外设丰富(如集成高速DAC)的开发平台,能极大地降低学习门槛,让开发者更专注于核心逻辑与系统级的设计。




上一篇:Qt控件设计器实战:自定义SelectWidget实现控件的自由拉伸与移动
下一篇:程序员职场软实力:三大坏习惯导致职业发展瓶颈与绩效拿C
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:34 , Processed in 0.377437 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表