一、项目需求
-
通过板上的高速DAC(10bits/125Msps)配合FPGA内部DDS的逻辑,生成波形可调(正弦波、三角波、方波)、频率可调(DC-)、幅度可调的波形
- 生成模拟信号的频率范围为DC-20MHz,调节精度为1Hz
- 生成模拟信号的幅度为最大1Vpp,调节范围为0.1V-1V
- 在OLED上显示当前波形的形状、波形的频率以及幅度
- 利用板上旋转编码器和按键能够对波形进行切换、进行参数调节
二、方案论证与比较
- 使用单片机配合DAC制作DDS。由单片机存储波形数据控制DAC实现DDS任意波形发生器,好处在于实现流程较为直观,难度低。
- 使用fpga配合外置DAC实现DDS。FPGA主频足够高,并且代码效率更高,配合外置高速DAC可以实现更高性能的DDS。
- 使用锁相环电路。由鉴相器、环路滤波器与压控振荡器组成锁相环电路,实现预期目标。在频率锁定之后,输出的频率将会比较稳定。
使用单片机实现难度低,但受限于单片机的架构,很难实现高性能的DDS任意波形发生器。即使使用高速外置DAC也会遇到一些麻烦,比如即使使用主频高达480M的stm32h7系列,IO翻转速率也只有16.7M,效果极差,不及定时器中断等外设产生的效果。但即使是STM32H7系列,进出中断的速率也只有12.5M,发挥不出高速DAC的性能,故不宜作为实现DDS的平台。
与锁相环相比,FPGA同样可以达到较好效果,并且实现功能更多更灵活,本次也借助硬禾学堂的学习活动,使用FPGA实现DDS任意波形发生器。经验证,效果很好。
三、方案描述
- 在PC中的lattice专属ide diamond中编写程序,编译、综合并生成jed文件。通过stm32l0mcu生成的虚拟U盘,对FPGA进行烧录和调试。
- 在FPGA中并行实现OLED屏幕内容的刷新,DAC波形的输出,对按键和旋转编码器输入信号的消抖并作出对应的处理。
- 使用逻辑分析仪代替板载调试器对过程中变量进行调试。
- 使用定时器+DMA+ADC的方式对stm32进行配置,对DDS生成的波形进行采样,通过UART串口发送回上位机。在上位机上通过matlab进行快速傅里叶变换,绘制波形与频谱,进行结果的验证,并进行进一步调整。
四、关键代码与分析
1、FPGA中记录最近一次调整的是频率还是幅度,以便于使用正交编码器进行数据的调整。
reg fre_change_enable,altitude_change_enable;
reg sw2_debounce_reg,sw3_debounce_reg;
wire sw2_debounce_pos,sw3_debounce_pos;
always@(posedge clk_in or negedge rst_n_in)begin
if(!rst_n_in) begin sw2_debounce_reg <= 1'b1; sw3_debounce_reg <= 1'b1; end
else begin sw2_debounce_reg<= sw2_debounce; sw3_debounce_reg <= sw3_debounce; end
end
assign sw2_debounce_pos = !sw2_debounce_reg&&sw2_debounce;
assign sw3_debounce_pos = !sw3_debounce_reg&&sw3_debounce;
always@(posedge clk_in or negedge rst_n_in)begin
if(!rst_n_in)begin
fre_change_enable <= 1'b0;
altitude_change_enable <= 1'b0;
end
else begin
if(sw2_debounce_pos) begin fre_change_enable <= 1'b1; altitude_change_enable<=1'b0; end
if(sw3_debounce_pos) begin fre_change_enable <= 1'b0; altitude_change_enable<=1'b1; end
end
否则经常会调整错数据,调试时造成较大麻烦。
2、matlab中快速傅里叶变换,进行波形与频谱的绘制。
clc;
clear all;
ad_read = importdata('1.2M.txt');
ad_data = ad_read';
ad_fft_1 = fft(ad_data);
ad_fft = fftshift(ad_fft_1);
figure(1)
subplot(211)
stem(ad_fft);
subplot(212)
plot(ad_data);
清屏后,对文件的数据进行读取。数据文件的形式最好是每行只有一个数据,一个数据占据一行的位置,此处文件名可以进行修改,数据文件须与matlab脚本文件处于同一文件夹下。
之后使用fft函数与fft_shift函数对方才读取的数据进行快速傅里叶变换,通过subplot函数将频谱与波形绘制在同一张Figure上,便于查看与分析。效果如下图所示。
3、信号消抖
always@(posedge clk,negedge rst_n)begin
if(!rst_n)begin
count <= 0;
clk_10ms <= 1'b0;
end
else begin
if(count < 32'd12_000)begin//10ms消抖,25MCLK
count <= count + 1'b1;
clk_10ms <= 1'b0;
end
else begin
count <= 0;
clk_10ms <= 1'b1;
end
end
end
always@(posedge clk,negedge rst_n)begin
if(!rst_n)begin
signal0 <= 1'b0;
signal1 <= 1'b0;
end
else begin
if(clk_10ms)begin
signal0 <= signal;
signal1 <= signal0;
end
end
end
assign signal_debounce = signal&&signal0&&signal1;
常规操作,没啥好说的,三个信号同时做与操作,可以更有效的消抖,避免受到劣质按键的干扰。
4、旋转编码器
always@(posedge clk,negedge rst_n)begin
if(!rst_n)begin
rotary_right <= 1'b1;
rotary_left <= 1'b1;
end
else begin
if(A_pos && !B) rotary_right <= 1'b1;//A的上升沿时候如果B为低电平,则旋转编码器是向右转
if(A_pos && B) rotary_left <= 1'b1;//A上升沿时候如果B为低电平,则旋转编码器是向左转
if(A_neg && B) rotary_right <= 1'b0;//A的下降沿B为高电平,则向右转结束
if(A_neg && !B) rotary_left <= 1'b0;//A的下降沿B为低电平,则向左转结束
end
end
主要参考电子森林中的例程并进行少许修改。在代码中只需在A或B中选择一个信号进行上升沿或下降沿的处理,同时判断另一个信号的高低电平即可判断旋转的方向。最好对A,B信号也进行消抖,以免受到干扰。
可以将debounce消抖部分的代码进行ip的封装,后续调用更加方便,而且不需要多次综合,在工程开发的过程中带来极大便利。
5、使用独热码与移位操作进行当前模式的切换
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
mode <= 8'b1111_1110;
end
else if(btn_pos) mode <= {mode[0],mode[7:1]};
end
使用独热码可以将模式state实时反映在核心板的一排小灯上,便于调试。使用移位操作而不是状态机mux,能节省很多资源。
6、频率数据的控制
wire rotary_event;
assign rotary_event = rotary_right_pos || rotary_left_pos;//转动标志位
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
bit_val_0_reg <= 4'b0;
bit_val_1_reg <= 4'b0;
bit_val_2_reg <= 4'b0;
bit_val_3_reg <= 4'b0;
bit_val_4_reg <= 4'b10;
bit_val_5_reg <= 4'b0;
bit_val_6_reg <= 4'b0;
bit_val_7_reg <= 4'b0;
end
else if(rotary_event&&enable)begin
if(rotary_right_pos)begin
case(bit_mode)
8'b1111_1110:
//bit_val_0_reg <= {bit_val_0_reg[0],bit_val_0_reg[3:1]};
if(bit_val_0_reg >= 4'd2) bit_val_0_reg <= 4'd0;
else bit_val_0_reg <= bit_val_0_reg + 1'b1;
8'b1111_1101:
if(bit_val_1_reg >= 4'd9) bit_val_1_reg <= 4'd0;
else bit_val_1_reg <= bit_val_1_reg + 1'b1;
8'b1111_1011:
if(bit_val_2_reg >= 4'd9) bit_val_2_reg <= 4'd0;
else bit_val_2_reg <= bit_val_2_reg + 1'b1;
8'b1111_0111:
if(bit_val_3_reg >= 4'd9) bit_val_3_reg <= 4'd0;
else bit_val_3_reg <= bit_val_3_reg + 1'b1;
8'b1110_1111:
if(bit_val_4_reg >= 4'd9) bit_val_4_reg <= 4'd0;
else bit_val_4_reg <= bit_val_4_reg + 1'b1;
8'b1101_1111:
if(bit_val_5_reg >= 4'd9) bit_val_5_reg <= 4'd0;
else bit_val_5_reg <= bit_val_5_reg + 1'b1;
8'b1011_1111:
if(bit_val_6_reg >= 4'd9) bit_val_6_reg <= 4'd0;
else bit_val_6_reg <= bit_val_6_reg + 1'b1;
8'b0111_1111:
if(bit_val_7_reg >= 4'd9) bit_val_7_reg <= 4'd0;
else bit_val_7_reg <= bit_val_7_reg + 1'b1;
endcase
end
if(rotary_left_pos)begin
case(bit_mode)
8'b1111_1110:
if(bit_val_0_reg == 4'd0) bit_val_0_reg <= 4'd2;
else bit_val_0_reg <= bit_val_0_reg - 1'b1;
8'b1111_1101:
if(bit_val_1_reg == 4'd0) bit_val_1_reg <= 4'd9;
else bit_val_1_reg <= bit_val_1_reg - 1'b1;
8'b1111_1011:
if(bit_val_2_reg == 4'd0) bit_val_2_reg <= 4'd9;
else bit_val_2_reg <= bit_val_2_reg - 1'b1;
8'b1111_0111:
if(bit_val_3_reg == 4'd0) bit_val_3_reg <= 4'd9;
else bit_val_3_reg <= bit_val_3_reg - 1'b1;
8'b1110_1111:
if(bit_val_4_reg == 4'd0) bit_val_4_reg <= 4'd9;
else bit_val_4_reg <= bit_val_4_reg - 1'b1;
8'b1101_1111:
if(bit_val_5_reg == 4'd0) bit_val_5_reg <= 4'd9;
else bit_val_5_reg <= bit_val_5_reg - 1'b1;
8'b1011_1111:
if(bit_val_6_reg == 4'd0) bit_val_6_reg <= 4'd9;
else bit_val_6_reg <= bit_val_6_reg - 1'b1;
8'b0111_1111:
if(bit_val_7_reg == 4'd0) bit_val_7_reg <= 4'd9;
else bit_val_7_reg <= bit_val_7_reg - 1'b1;
endcase
end
end
end
增加旋转标志位,能同时在正时针旋转与逆时针旋转的时候进行判断,可同时增减数据,调试与使用更方便。
给每一位数据都单独开一个4位二进制数进行存储会耗费更多资源,但可以单独操作十进制数据中的每一位,不至于说要靠旋转从100hz调整到20Mhz,大幅节省时间。
7、将几个4位二进制数转换成一个ascii码构成的字符串
assign char = {bit_val_0+8'd48 ,
bit_val_1+8'd48 ,
bit_val_2+8'd48 ,
bit_val_3+8'd48 ,
bit_val_4+8'd48 ,
bit_val_5+8'd48 ,
bit_val_6+8'd48 ,
bit_val_7+8'd48};
转换为ASCII的形式,便于在OLED显示的过程中进行字模的选取,个人认为比BCD更合适使用。
8、DDS部分关键代码
wire [9:0] cos_dac;
wire [9:0] square_dac;
wire [9:0] trig_dac;
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
wave_dac <= 10'd0;
end else begin
case(wave_type)
3'b110: wave_dac <= cos_dac;
3'b011: wave_dac <= square_dac;
3'b101: wave_dac <= trig_dac;
endcase
end
end
波形模式的选取,对三种波形的数据分开进行存储。
assign dds_phase_add = (wave_freq << 1) + (wave_freq >> 3) + (wave_freq >> 4) + (wave_freq >> 5) + (wave_freq >> 6) +
(wave_freq >> 9) + (wave_freq >> 11) + (wave_freq >> 13);
通过移位相加的方式,在120Mhz主频的情况下,将目标波形的频率转化为相位累加器的增幅频率字。
此处不使用乘法器的module,好处在于可以省下非常多的资源,并且以wire的形式计算,可以直接产生对应结果。而乘法器起码需要一个周期,甚至几个周期都无法得到结果,将会大大影响系统的稳定性。
assign square_dac = {10{dds_phase[27]}};
assign trig_dac = dds_phase[27] ? ~dds_phase[26:17] : dds_phase[26:17];
方波与三角波对应的dac数据的产生方式如上。
always@(address)begin
case(section)
2'b00: begin
lut_address = address[5:0];
cos = 9'h1ff + lut_cos;
end
2'b01: begin
lut_address = ~address[5:0];
cos = 9'h1ff + lut_cos;
end
2'b10: begin
lut_address = address[5:0];
cos = 9'h1ff - lut_cos;
end
2'b11: begin
lut_address = ~address[5:0];
cos = 9'h1ff - lut_cos;
end
endcase
end
取address的前两位做case,可以使用对用的1/4波形数据查找表,在不使用block ram与ROM的情况下可以省下很多lut,以作其他用途。
五、测试结果
测试正弦波的波形和频谱如下所示。
测试三角波的波形和频谱如下所示。
测试方波的波形和频谱如下所示。
六、FPGA资源占用报告
七、结果分析与不足
由于疫情原因,学校电子电工实验室暂时封闭了,只好现学stm32做了一个简单的波形采集。由于DMA使用不熟练,所以采样率一直上不去,最后大概是个100ksps左右的采样率,并且具有较大毛刺,因此数据并不好看。
但结合matlab对数据进行处理分析并绘图以后,同样可以获得较为直观的分析,并可得频谱较为干净,实验结果符合预期。
另一方面,由于数据存储方案选择的失误,忽略了verilog对乘法的支持问题,导致最后面临移位乘法的近似处理与标准乘法器方案的抉择。一个会导致小幅误差,一个需要多周期进行运算,影响系统稳定性。最终选择放弃了部分精度。造成这个问题的主要原因还是对fpga与verilog的不熟悉,好在吃一堑长一智,类似的问题下次就不会再犯了。