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

788

积分

0

好友

100

主题
发表于 昨天 18:08 | 查看: 0| 回复: 0

在信号源、示波器等电子系统中,任意波形发生器几乎是所有电子工程师都会接触到的基础仪器,而其背后的核心思想——DDS(直接数字频率合成),也是数字系统与模拟信号之间非常经典的一座桥梁。

项目介绍

本次项目在单块 FPGA 训练平台上,实现了完整的 DDS 任意波形发生器本地控制。系统能够通过开发板上的 K1、K2 按钮与旋转编码器,实现对输出信号振幅频率的独立控制。核心板上的四个拨码开关,则分别用于选择正弦波、三角波、方波和直流信号输出。同时,通过板载按键可以实现波形量程的快速切换。最终,信号经由高速 DAC 模块输出,而当前波形的所有参数则实时显示在 OLED 屏幕上。

设计思路

整个系统通过旋转编码器和按键进行参数调节,通过拨码开关实现波形选择。核心原理是相位累加器:通过改变输入到累加器的相位增量值,来实现输出频率的稳定与可调。幅度调节则通过一个独立的调幅因数乘法器来完成。其中,正弦波的生成采用了经典的查找表(LUT)方式。

在 Lattice Diamond 软件中综合后的电路原理图如下:

DDS数字电路综合原理图

系统功能框图清晰地展示了编码器输入、OLED驱动和DAC数据输出三个主要流程:

DDS任意波形发生器系统功能框图

下图展示了该设计在目标FPGA芯片上的资源使用情况报告:

FPGA设计资源使用情况总结报告

简单硬件介绍

DAC模块:本次项目使用了10位分辨率、125Msps采样率的高速DAC模块。在实际设计中,提供给DAC的时钟(DAC_clk)频率为120MHz。该模块的核心是3PD5651E这款10位CMOS数模转换器,其输入信号为时钟信号与10位的并行数据信号。

OLED模块:项目采用了一块128*64分辨率的OLED显示屏,其驱动芯片为SSD1306。因此,从软件驱动角度,我们只需要与SSD1306芯片通信即可控制屏幕。由于开发板引脚已预先配置,本项目采用了4线串行通信方式(SPI),通过DC管脚进行数据/命令模式选择(DATA = 1‘b1, CMD = 1’b0),SCL为时钟线,SDA为数据线,RES用于复位清零。

功能实现

如下图所示,OLED屏幕清晰地显示了当前输出的波形类型(sin, square, triangle)。用户可以选择调节波形的幅值(A)频率(f)。幅值调节范围为0~1.00V,频率调节范围则为0~20MHz。频率的读数由三部分相加构成:兆赫(M)千赫(K)赫兹(Hz),即 (xxM + xxK + xxx) Hz。所有操作均通过旋转编码器调整数值,通过K1/K2按键切换调整对象(频率/幅度),通过拨码开关切换波形。

DDS信号发生器硬件平台正面视图
硬件平台显示正弦波参数
硬件平台运行整体俯视图

板上按键功能分配说明图:

按键与编码器功能分配说明图

实际输出波形效果

正弦波输出
硬件显示与示波器捕获波形:
正弦波输出硬件显示
示波器显示正弦波形

方波输出
硬件显示与示波器捕获波形:
方波输出硬件显示
示波器显示方波形

三角波输出
硬件显示与示波器捕获波形:
三角波输出硬件显示
示波器显示三角波形

主要代码片段分析

旋转编码器解码

旋转编码器A、B两相之间存在90度相位差。判断逻辑是:若在A相的上升沿时刻B相为低电平,或在A相的下降沿时刻B相为高电平,则编码器为顺时针旋转;反之则为逆时针旋转。由此得到的旋转检测Verilog代码如下:

always@(posedge clk_in or negedge rst_n_in)begin
if(!rst_n_in)begin
        Right_pulse <= 1'b0;
        Left_pulse <= 1'b0;
end else begin
        if(A_pos && B_state) Left_pulse <= 1'b1;
        else if(A_neg && B_state) Right_pulse <= 1'b1;
else begin
        Right_pulse <= 1'b0;
            Left_pulse <= 1'b0;
end
end
end

这段代码可以准确记录编码器左旋或右旋的脉冲。我们需要根据这个脉冲来增减对应的参数(频率或幅度)。在数字电路设计中,参数的存储方式采用了“用16进制数模拟10进制数”的技巧。例如,若幅度数据为 8‘h056,它代表的实际幅度是0.56V。当每一位(每4个二进制位)加满或减满需要进位/借位时,需要编写模仿十进制运算的逻辑。项目中所有数据均按此方法存储和处理。

if(Right_pulse && num_A <= 25'd255) begin
             if(num_A[3:0] !=  9) begin
                    num_A<=num_A+25'd1;
               end else begin

                       num_A[3:0] <= 0;
                       if(num_A[7:4] !=  9) begin
                       num_A[7:4] <= num_A[7:4] + 1;
                       end  else  begin
                           num_A[7:4] <= 0;
                           num_A[11:8] <= num_A [11:8] + 1;
                       end
               end
           end else begin

           if(Left_pulse && num_A >= 25'd1) begin
               if(num_A[3:0] !=  0) begin
                   num_A<=num_A-25'd1;
               end else begin

                       num_A[3:0] <= 9;
                       if(num_A[7:4] !=  0) begin
                       num_A[7:4] <= num_A[7:4] - 1;
                       end  else  begin
                           num_A[7:4] <= 9;
                           num_A[11:8] <= num_A [11:8] - 1;
                       end
               end
           end
       end

DDS核心:相位累加器与波形生成

直接数字频率合成的核心是一个相位累加器。对于正弦波,我们使用查找表来实现;通过改变相位累加器的步进来实现任意频率输出,幅度调节则通过后续的乘法器实现。

相位累加器本身就是一个计数器,其高位截断输出作为正弦查找表的地址。查找表的每个地址对应正弦波0°到360°的一个相位点值。利用正弦波的对称性,实际上只需存储1/4周期的数据即可。

相位累加器的代码如下:

always @(posedge clk_120m or negedge    rst) begin
if (!rst)   begin
        phase_acc<=0;
end else begin
            phase_acc <= phase_acc + num_phase;
end
end

根据公式 Fout = M * Fclk / 2^N 可知,只需改变M值(即相位步进值 num_phase)即可改变输出频率。若M变为原来的两倍,输出频率也变为原来的两倍。因此,需要确定一个基准步进值ΔM,使得此时输出频率为1Hz。当需要N Hz的频率时,将M设为N倍的ΔM即可。本设计采用120MHz时钟,30位累加器,经计算ΔM为9。因此,相位步进值按9的整数倍进行调节,具体计算代码如下:

always@(posedge clk) begin
        num_phase_1 = 9*(100*num_f0[11:8] + 10*num_f0[7:4] + num_f0[3:0])
                       + 9*(100*num_f1[11:8] + 10*num_f1[7:4] +num_f1[3:0])*1000
                       + 9*(10*num_f2[7:4] +num_f2[3:0])*1000000;
end

对于其他波形,方波和三角波可以直接对相位累加器的高位进行简单处理得到。

方波生成:直接取相位累加器的最高位(MSB)。

wire    square_tap=phase_acc[29];
assign  square_data={10{square_tap}};

三角波生成:取相位累加器的次高位段,并根据最高位进行取反操作,形成锯齿的上升沿和下降沿。

assign  triangle_data=phase_acc[29] ? ~phase_acc[28:19]:phase_acc[28:19];

正弦波生成:这是最复杂的部分,涉及到利用1/4周期查找表和象限判断逻辑来重建完整正弦波。

assign sin_out = sine_onecycle_amp[9:0];

    assign sel = phase[9:8];

    sin_table u_sin_table(address,sine_table_out);

always @(sel or sine_table_out)
begin
case(sel)
2'b00:  begin
            sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];
            address = phase[7:0];
            end
    2'b01:  begin
            sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];
            address = ~phase[7:0];
            end
    2'b10:  begin
            sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];
            address = phase[7:0];
            end
    2'b11:  begin
            sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];
            address = ~ phase[7:0];
            end
    endcase
end
//查找表:
always @(address)
begin
                 case(address)
                     8'h0: sin=9'h000;
                      … …
                     8'hff: sin=9'h1FE;
                 endcase
             end

本例中使用的查找表为10位地址、9位数据精度的1/4周期正弦表,共存储256个数据点。

OLED驱动显示

OLED驱动通过状态机循环刷新不同区域的显示内容来实现。每个状态(case)对应屏幕上的一行或一个特定区域的显示数据与坐标,在主循环中不断轮询这些状态,即可实现动态刷新。

 if(cnt_main >= 5'd20) cnt_main <= 5'd4;
                       else cnt_main <= cnt_main + 1'b1;

                        case(cnt_main)  //MAIN状态
                            5'd0:   begin state <= INIT; end
5'd1:   begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
                            5'd2:   begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "A:           V  ";state <= SCAN; end
5'd3:   begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "f:          Hz  ";state <= SCAN; end
  ... ...

                             5'd19:   begin
                                 ... ...
                                    end

default: state <= IDLE;
                        endcase
                   end

如上代码所示,通过设定显示起始坐标(y_p, x_ph, x_pl)和要显示的字符串(char),然后跳转到扫描发送状态(SCAN),即可完成一行的更新。在所有显示状态间循环,就能实现整个屏幕信息的实时更新。

项目总结与思考

在本次项目开发过程中,花费了相当精力去研究OLED与高速DAC的工作原理,并仔细阅读了示例程序以整合实现自定义功能。在数据表示上,没有采用常见的二进制转BCD码算法,而是直接使用十六进制数来模拟十进制存储。这样做的好处是,在送往OLED显示时,可以直接按4位一组取出作为显示字符,非常方便。但相应地,在计数(加减)逻辑上就变得较为复杂,需要编写模仿十进制进位/借位的代码。

另外,由于采用查找表法,在120MHz系统时钟下,要输出最高20MHz的信号而不失真,需要进行信号处理和滤波。为此,在DAC输出后端设计了相应的滤波器,确保了高频信号的输出质量。

DDS系统连接示波器测试场景
示波器测量DDS输出正弦波

未来的计划是继续学习开发板上尚未使用的ADC模块,并尝试实践一些更具挑战性的项目,例如闭环控制系统或更复杂的通信协议实现。

通过这个完整的项目,我们从理论到实践,跑通了DDS信号源设计的全部关键环节。对于想要深入学习FPGA和直接数字频率合成技术,或者正在准备电子设计竞赛的同学来说,这是一个非常不错的练手项目。你可以在云栈社区找到更多类似的开源实战项目分享和讨论,与广大开发者一起交流成长。




上一篇:使用Docker和Ruffle在NAS部署个人Flash游戏库
下一篇:自动驾驶感知融合与决策挑战:移动式临时红绿灯的应对策略解析
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-1-31 03:21 , Processed in 0.296151 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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