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

635

积分

0

好友

79

主题
发表于 3 天前 | 查看: 12| 回复: 0

一块 FPGA,玩转任意波形发生,调频调幅全可控!这个项目利用小脚丫FPGA 开发板中的 DDS 模块与高速 DAC,实现了正弦波、方波、三角波等多种波形的高质量输出。控制方式也非常直观:按键切换波形模式,旋转编码器切换“调频/调幅”,再旋转进行参数微调。整个系统逻辑清晰、交互友好,是硬件爱好者体验 FPGA 数字波形合成的绝佳入口。

项目需求

通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形。

  • 生成模拟信号的频率范围为DC-20MHz,调节精度为1Hz;
  • 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V;
  • 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节。

硬件介绍

项目使用的开发板为搭载小脚丫FPGA核心板(全FPGA方案)的电赛训练板:

搭载小脚丫FPGA的电赛训练板

系统框图如下:

系统硬件架构框图

项目用到的功能包括:编码器,按键,高速DAC。

完成的功能及达到的性能

1. 波形输出

通过板载高速DAC输出任意波形,默认为正弦波,将输出引脚接在示波器上即可观察。

正弦波输出示波器测试图

2. 按键切换波形

通过扩展板上的按键K1,K2切换波形,目前可输出正弦波,三角波,方波三种波形。

方波输出示波器测试图
三角波输出示波器测试图

3. 编码器调频

按下编码器可切换“调频/调幅模式”,可在调频模式下通过编码器左右旋转调节输出波形的频率。

编码器调节频率操作示意图

4. 编码器调幅

按下编码器可切换“调频/调幅模式”,可在调幅模式下通过编码器左右旋转调节输出波形的幅值。

编码器调节幅度操作示意图

实现思路

  • 使用FPGA内置DDS逻辑产生10位数字电压信号,输出给板载高速DAC模块,由DAC模块输出相应的波形。
  • 设置波形状态机,管理FPGA输出的波形,可通过按键在正弦波,方波,三角波之间切换。
  • 设置编码器状态机,管理当前编码器的调节对象,可通过编码器OK键在调频和调幅之间切换。

实现过程

1. 程序架构图

FPGA程序模块化设计框图
(注:每个框图右下角名称灰色名称为文件执行的主要功能)

2. DDS波形产生

板载了10bit/120Msp 高速DAC,通过FPGA内部的DDS逻辑产生10bit的数字量电压值,向DAC模块输出电压值数据,DAC解析数据并在输出引脚输出对应的波形信号。

为了提高生成波形的最大频率,调用了Diamond内置的锁相环ip核,将12MHz的时钟信号倍频到120MHz输入给dds产生模块,使整个系统能产生的最大波形信号提高到20MHz左右。但波形在20MHz左右,每个周期的采样点仅有6个,波形的是真也较为严重。

具体波形产生方面,定义了32位累加器(即代码中cnt),120MHz的时钟信号上升沿用以使累加器累加。三角波和方波均可直接通过累加器得到,其中三角波获得方法为:

ori_out <= (cnt[31])?cnt[30:21]:~cnt[30:21];

方波获得方法为:

ori_out <= {10{ cnt[31] }};

正弦波无法通过累加器获得,项目采用查找表的方式,调用了diamond内置的sin-cos查找表IP核,输入8bit地址,输出10bit的数据。

//sin table
wire [9:0] dds_out_sin_temp;
wire sin_clk_en = 1'b1;
wire sin_reset = 1'b0;
dds_sin_table u_dds_sin_table(
.Clock(clk_pll),  
.ClkEn(sin_clk_en),
.Reset(sin_reset),
.Theta(cnt[31:24]),
.Sine(dds_out_sin_temp)
);

DDS产生文件(文件:dds.v,调用位置:main.v):

module dds(
    input clk_pll,              //输入时钟
    input [2:0] wave_st,
    input [31:0] fm_step,
    input [7:0] am_factor,
    output reg [9:0]dds_out     //输出设置成reg,可去毛刺
);
//状态机
parameter WAVE_STATE_SIN = 3'b110;  //正弦波
parameter WAVE_STATE_SQUARE = 3'b101;//方波
parameter WAVE_STATE_TRI = 3'b011;  //三角波
//调幅后data
reg [17:0] am_out;
//原始输出
reg [9:0] ori_out;
//累加器
reg [31:0] cnt;
always @(posedge clk_pll) cnt <= cnt + fm_step;
//sin table
wire [9:0] dds_out_sin_temp;
wire sin_clk_en = 1'b1;
wire sin_reset = 1'b0;
dds_sin_table u_dds_sin_table(
    .Clock(clk_pll),  
    .ClkEn(sin_clk_en),
    .Reset(sin_reset),
    .Theta(cnt[31:24]),
    .Sine(dds_out_sin_temp)
);
always @ (posedge clk_pll) 
begin
    case(wave_st)
        WAVE_STATE_SIN:
            ori_out <= dds_out_sin_temp + 10'd512;
        WAVE_STATE_SQUARE:
            ori_out <= {10{ cnt[31] }};
        WAVE_STATE_TRI:
            ori_out <= (cnt[31])?cnt[30:21]:~cnt[30:21];
    default:
            ori_out <= dds_out_sin_temp + 10'd512;
    endcase
//调幅
    am_out <= ori_out * am_factor;
    dds_out <= am_out[17:8];
end
endmodule

3. 状态机

状态机是FPGA编程重要的一环,由于系统需要完成切换波形和调节波形的幅值和频率,系统的状态主要分为当前波形种类状态和编码器状态,项目对其定义如下:

//状态机wave
parameter WAVE_STATE_SIN = 3'b110;  //正弦波
parameter WAVE_STATE_SQUARE = 3'b101;//方波
parameter WAVE_STATE_TRI = 3'b011;  //三角波
reg [2:0] curr_wave_st;
reg [2:0] next_wave_st;
//状态机enc
parameter ENC_STATE_FM = 3'b110;  //调频
parameter ENC_STATE_AM = 3'b101;     //调幅
reg [2:0] curr_enc_st;
reg [2:0] next_enc_st;

状态切换方面,系统采用按键的左右键切换波形显示,编码器OK键切换调频/调幅模式。对上述两个状态分别采用三段式编写了状态切换代码,可减少代码重复,同时便于管理,提高时序逻辑的稳定性。

状态切换代码如下:

//第一段 同步逻辑 描述次态到现态的转移
    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n) 
            curr_wave_st <= WAVE_STATE_SIN;
        else 
            curr_wave_st <= next_wave_st;
    end
    //第二段 组合逻辑描述状态转移的判断
    always @ (curr_wave_st or rst_n or key_pulse)
    begin
        if(!rst_n) begin
                next_wave_st = WAVE_STATE_SIN;
            end
        else begin
            case(curr_wave_st)
                WAVE_STATE_SIN: begin
                    if(key_pulse[0]) 
                        next_wave_st = WAVE_STATE_TRI;
                    else if(key_pulse[1])
                        next_wave_st = WAVE_STATE_SQUARE;
                    else
                        next_wave_st = WAVE_STATE_SIN;
                end

                WAVE_STATE_SQUARE: begin
                    if(key_pulse[0]) 
                        next_wave_st = WAVE_STATE_SIN;
                    else if(key_pulse[1])
                        next_wave_st = WAVE_STATE_TRI;
                    else
                        next_wave_st = WAVE_STATE_SQUARE;
                end

                WAVE_STATE_TRI: begin
                    if(key_pulse[0]) 
                        next_wave_st = WAVE_STATE_SQUARE;
                    else if(key_pulse[1])
                        next_wave_st = WAVE_STATE_SIN;
                    else
                        next_wave_st = WAVE_STATE_TRI;
                end

                default: next_wave_st = WAVE_STATE_SIN;
            endcase
        end
    end
    //第三段  同步逻辑 描述次态的输出动作
    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n==1) begin
            led_out[2:0] <= WAVE_STATE_SIN;
            end 
        else begin
            case(next_wave_st)
                WAVE_STATE_SIN: begin
                    led_out[2:0] <= WAVE_STATE_SIN;
                end

                WAVE_STATE_SQUARE: begin
                    led_out[2:0] <= WAVE_STATE_SQUARE;
                end

                WAVE_STATE_TRI: begin
                    led_out[2:0] <= WAVE_STATE_TRI;
                end

                default:begin
                    led_out[2:0] <= WAVE_STATE_SIN;
                    end
            endcase
        end
    end

    //编码器的状态机,和上面一样
    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n) 
            curr_enc_st <= ENC_STATE_FM;
        else 
            curr_enc_st <= next_enc_st;
    end
    //第二段 组合逻辑描述状态转移的判断
    always @ (curr_enc_st or rst_n or enc_pulse_ok)
    begin
        if(!rst_n) begin
                next_enc_st = ENC_STATE_FM;
            end
        else begin
            case(curr_enc_st)
                ENC_STATE_FM: begin
                    if(enc_pulse_ok) 
                        next_enc_st = ENC_STATE_AM;
                    else
                        next_enc_st = ENC_STATE_FM;
                end

                ENC_STATE_AM: begin
                    if(enc_pulse_ok) 
                        next_enc_st = ENC_STATE_FM;
                    else
                        next_enc_st = ENC_STATE_AM;
                end

                default: next_enc_st = ENC_STATE_FM;
            endcase
        end
    end
    //第三段  同步逻辑 描述次态的输出动作
    always @ (posedge clk or negedge rst_n)
    begin
        if(!rst_n==1) begin
            led_out[5:3] <= ENC_STATE_FM;
            end 
        else begin
            case(next_enc_st)
                ENC_STATE_FM: begin
                    led_out[5:3] <= ENC_STATE_FM;
                end

                ENC_STATE_AM: begin
                    led_out[5:3] <= ENC_STATE_AM;
                end

                default:begin
                    led_out[5:3] <= ENC_STATE_FM;
                    end
            endcase
        end
    end

4. 按键消抖及编码器解码

按键消抖和编码器解码内容参考了电子森林的代码,网址如下:

5. 调频及调幅

调频和调幅原理均参考了电子森林的DDS原理文章:[dds_verilog:https://www.eetree.cn/wiki/dds_verilog]

其中,调频通过改变DDS累加器的累加值实现,调幅通过将产生的波形乘一个因数实现,具体请参考上述链接的文章。

将DDS累加器的累加值设置为fm_step,将调幅的因数值设为am_factor。分别定义两个模块负责调节这两个值,并通过顶层文件main.v例化,将调节的两个参数通过input的方式输入给dds波形产生模块。

其中,调频的代码为:

module dds_fm(
input clk, //输入时钟
input rst_n,
input enc_pulse_l,
input enc_pulse_r,
input [2:0] enc_st,
output reg [31:0] dds_fm_step //调频累加值
);
//状态机
parameter ENC_STATE_FM = 3'b110;  //调频
parameter ENC_STATE_AM = 3'b101;     //调幅

//调频
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
dds_fm_step <= 32'h00_00_ff_ff;
else
case(enc_st)
ENC_STATE_FM:
if(enc_pulse_l)
dds_fm_step <= dds_fm_step - 8'hff;
else if(enc_pulse_r)
dds_fm_step <= dds_fm_step + 8'hff;
else
dds_fm_step <= dds_fm_step;
default:
dds_fm_step <= dds_fm_step;
endcase
end

endmodule

调幅的代码为:

module dds_am(
    input clk,              //输入时钟
    input rst_n,
    input enc_pulse_l,
    input enc_pulse_r,
    input [2:0] enc_st,
    output reg [7:0] dds_am_factor   //调幅因数
);
//状态机
parameter ENC_STATE_FM = 3'b110;    //调频
parameter ENC_STATE_AM = 3'b101;     //调幅

//调幅
always @(posedge clk or negedge rst_n) 
begin
    if(!rst_n) 
        dds_am_factor <= 8'hff;
    else
        case(enc_st)
            ENC_STATE_AM: 
                if(enc_pulse_l)
                    dds_am_factor <= dds_am_factor - 8'h0f;
                else if(enc_pulse_r)
                    dds_am_factor <= dds_am_factor + 8'h0f;
                else
                    dds_am_factor <= dds_am_factor;
            default: 
                dds_am_factor <= dds_am_factor;
        endcase
end

endmodule

资源报告

FPGA设计资源使用总结报告

遇到的主要难题

1. 内置sin_table的ip核波形不正常

系统在生成DDS波形时,调用了Diamond内置的ip核,调用方式如下:

//sin table
wire [9:0] dds_out_sin_temp;
wire sin_clk_en = 1'b1;
wire sin_reset = 1'b0;
dds_sin_table u_dds_sin_table(
.Clock(clk_pll),  
.ClkEn(sin_clk_en),
.Reset(sin_reset),
.Theta(cnt[31:24]),
.Sine(dds_out_sin_temp)
);

但是,该调用方式查找到的波是如下图所示的一种奇怪波形:

异常的IP核正弦波输出

猜测是由于ip核设置问题,或ip核内部逻辑有误导致。为了生成一个完美的正弦波,在这里,我将sin_table输出的wire整体加上了10'd512,即:

ori_out <= dds_out_sin_temp + 10'd512;

得到的波形即为一个完美的正弦波。

2. reg与wire的区别问题

在初期学习时,我基本上使用的都是默认的wire类型,对wire与reg的区别理解也不是很深入,曾误认为wire和reg无法相互赋值,但在日渐的使用中,通过尝试,发现了wire和reg是可以相互赋值的。

由于前期使用的都是wire,无法在always内赋值,故在根据状态机确定波形时较难操作,在三选一的选择语句中,只能通过如下形式撰写:

assign dds_out =
(wave_st == WAVE_STATE_SIN)? dds_out_sin:
((wave_st == WAVE_STATE_SQUARE)?dds_out_square:dds_out_tri)

这种方式的可读性和逻辑性都较差,后来,将dds_out及有关变量均改为reg类型,即可将根据状态确定波形种类的语句迁移进always内处理(代码见上文4.2),可大大提高代码可读性。

另外,经测试,使用reg代替wire还可以起到减小毛刺的作用。这涉及到对硬件描述语言底层机制的理解,如果想深入学习数字逻辑设计原理,可以参考 计算机基础 板块中的相关内容。

改进措施

本项目已经成功实现了简易信号发生器的功能,并达到了预期指标,但还有许多可以提升与扩展的地方:

  • 调用板载OLED对波形,参数灯进行显示,提高用户交互体验。
  • 按键使用不是很充分,可增加状态机的复杂度,实现波形频率和幅值的按位调节。
  • 调用板载ADC,实现一个简易的示波器,并与信号发生器联动,形成一个完整的系统。

这个项目展示了如何利用FPGA的并行处理能力和DDS技术构建灵活的信号源,其核心思想与数字系统设计、信号与系统等 计算机基础 知识紧密相连。如果你对FPGA开发或更多硬件项目感兴趣,欢迎到 云栈社区 与更多开发者交流讨论。




上一篇:深入理解代理、节点、TUN与Clash模式:告别网络配置玄学
下一篇:小肩膀逆向 零基础RPA自动化实战 JavaScript核心语法到浏览器自动化项目精讲
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-1-24 02:54 , Processed in 0.361988 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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