1 项目需求
项目7 - 频谱分析类电赛题目。1.通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形。
-
生成模拟信号的频率范围为DC-20MHz,调节精度为1Hz
-
生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
-
利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
2 硬件介绍
项目使用的开发板为搭载小脚丫FPGA核心板(全FPGA方案)的电赛训练板:
系统框图如下:
项目用到的功能包括:编码器,按键,高速DAC。
3 完成的功能及达到的性能
3.1 波形输出
通过板载高速DAC输出任意波形,默认为正弦波,将输出引脚接在示波器上即可观察。
3.2 按键切换波形
通过扩展板上的按键K1,K2切换波形,目前可输出正弦波,三角波,方波三种波形。
3.3 编码器调频
按下编码器可切换“调频/调幅模式”,可在调频模式下通过编码器左右旋转调节输出波形的频率。
3.4 编码器调幅
按下编码器可切换“调频/调幅模式”,可在调幅模式下通过编码器左右旋转调节输出波形的幅值。
4 实现思路
-
使用FPGA内置DDS逻辑产生10位数字电压信号,输出给板载高速DAC模块,由DAC模块输出相应的波形。
-
设置波形状态机,管理FPGA输出的波形,可通过按键在正弦波,方波,三角波之间切换。
-
设置编码器状态机,管理当前编码器的调节对象,可通过编码器OK键在调频和调幅之间切换。
5 实现过程
5.1 程序架构图
(注:每个框图右下角名称灰色名称为文件执行的主要功能)
5.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
5.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
5.4 按键消抖及编码器解码
按键消抖和编码器解码内容参考了电子森林的代码,网址如下:
-
按键消抖: https://www.eetree.cn/wiki/7._%E6%8C%89%E9%94%AE%E6%B6%88%E6%8A%96
-
编码器解码: [旋转编码器模块 电子森林] (eetree.cn)
其中,按键消抖模块调用时可自定义按键个数,仅例化一个模块即可实现多个按键的消抖操作。
5.5 调频及调幅
调频和调幅原理均参考了电子森林的DDS原理文章:[dds_verilog 电子森林] (eetree.cn)
其中,调频通过改变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
6.资源报告
7 遇到的主要难题
7.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核内部逻辑有误导致。为了生成一个完美的正弦波,在这里,我将sin_table输出的wire整体加上了10'd512,即:
ori_out <= dds_out_sin_temp + 10'd512;
得到的波形即为一个完美的正弦波。
7.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还可以起到减小毛刺的作用。
8 改进措施
本项目已经成功实现了简易信号发生器的功能,并达到了预期指标,但还有许多可以提升与扩展的地方:
-
调用板载OLED对波形,参数灯进行显示,提高用户交互体验。
-
按键使用不是很充分,可增加状态机的复杂度,实现波形频率和幅值的按位调节。
-
调用板载ADC,实现一个简易的示波器,并与信号发生器联动,形成一个完整的系统。