dds compiler ip
设置输出频率(output frequencies)
- configuration options只有这一种写法
- parameter selection 同理
不输出相位
1.
- 选system Parameter,固定频率
- 选Hardware Parameter,频率可编程(频率控制字PINC,phase increment)计算公式如下:
f o u t = P I N C × f c l k 2 B ( θ ) f_{out}=\frac{PINC\times f_{clk}}{2^{B(\theta)}} fout=2B(θ)PINC×fclk - 运算示例:
PINC设置位置如下图
B ( θ ) 如下图,为 16 B(\theta)如下图,为16 B(θ)如下图,为16
设计与仿真文件的层次关系
- sim_1中的代码文件会随着design source中的添加而自动添加
- sim中tb是top文件
- vivado中不能对文件直接重命名,ip核也一样,改名比较麻烦
- 括号外的名字是代码中的module名,里面是.v文件名
ip核报错声称没有编译
- 问题描述:…is not compiled in library ‘xil_defaultlib’
- 解决:右键ip,reset output products\
concurrent assignment
- 报错:[VRFC 10-3236] concurrent assignment to a non-net ‘clk’ is not permitted [“D:/vivado_prj/fft_verify/fft_verify.srcs/sources_1/new/fft_verify.v”:2]
- 解决:
input reg clk 改为:input clk
dds采样频率
暂时认定是系统时钟频率
FFT IP
注意
FFT的仿真时间可能较长,实测接近10ms-10_000us-10_000_000ns,不要看见ns为单位数字很大就觉得时间很长哦。另外,modelsim虽说打开仿真的时间相比vivado自带仿真短非常多,但是开始仿真后的运行时间上并没有这么大的差距。
IP核交互是用AXI-Stream接口
AXI-Stream接口分为主机(master)和从机(slave),只有ready信号和valid信号同时为高时数据才能读写。举个例子,主机检测从机发出的ready信号,当ready为高时将valid信号拉高即可读写。
端口例化
xfft_0 xfft_0_u (
.aclk(clk_1M),
.aresetn(rst_n),
.s_axis_config_tdata(8'd1), // input wire [7 : 0]
//s_axis_config_tdata=8'd1时进行FFT, 0时做ifft
.s_axis_config_tvalid(1'd1), // input wire
.s_axis_config_tready(fft_s_config_tready), // output
.s_axis_data_tdata(fft_s_data_tdata), // input wire [15 : 0]信号输入 [15:8]虚部,[7:0]实部,8实8虚
.s_axis_data_tvalid(fft_s_data_tvalid), // input,当s_axis_data_tready高电平后,将s_axis_data_tvalid拉高L个周期,输入L个数据进行fft;L是FFT的点数
.s_axis_data_tready(fft_s_data_tready), //output,rst拉高两个时钟周期后,该口给1输出,此时ip核初始化完成,可进行数据输入,注意,这个是输出,ip核自己给信号,不用你给。管valid不管ready
.s_axis_data_tlast(fft_m_data_tlast), // input,数据输入完毕后为1,数据输入结束
.m_axis_data_tdata(fft_m_data_tdata), // output,48bit频谱输出,[18:0]实部[42:24]虚部,19实,19虚
.m_axis_data_tuser(fft_m_data_tuser), // output,[15 : 0],[9:0]有效,[15:10]恒为0
.m_axis_data_tvalid(fft_m_data_tvalid), // output,有输出时为1,无输出为0
.m_axis_data_tready(1'd1), // input,恒为1
.m_axis_data_tlast(fft_m_data_tlast), // output,当fft输出到最后一个结果时拉高一个时钟周期后,拉低
.event_frame_started(fft_event_frame_started), // output wire event_frame_started
.event_tlast_unexpected(fft_event_tlast_unexpected), // output,当数据输入完毕之前s_axis_data_tlast就拉高时给出“不速之客”信号
.event_tlast_missing(fft_event_tlast_missing), // output,当数据输入完毕而s_axis_data_tlast,没有拉高时给last缺失信号
.event_status_channel_halt(fft_event_status_channel_halt), // output,状态通道输出不正常时给信号
.event_data_in_channel_halt(fft_event_data_in_channel_halt), // output,需要数据输入但未收到时给信号
.event_data_out_channel_halt(fft_event_data_out_channel_halt) // output,需要数据输出但没有输出时给信号
);
输入点数控制代码
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin//复位
fft_s_data_tvalid <= 1'b0;
fft_s_data_tlast <= 1'b0;
data_finish_flag <= 1'b0;
count <= 16'd0;
end
else if (fft_s_data_tready) begin //data_ready高
if(count == Length) begin//点数计满
fft_s_data_tvalid <= 1'b0;//无效
fft_s_data_tlast <= 1'b1;//结束
count <= 16'd0;//计数归零
data_finish_flag <= 1'b1;//结束标志
end
else begin//点数未计满
fft_s_data_tvalid <= 1'b1;//有效
fft_s_data_tlast <= 1'b0;//未结束
count <= count + 1'b1;//计数加一
end
end
else begin//data_ready无效
fft_s_data_tvalid <= 1'b0;//无效
fft_s_data_tlast <= 1'b0;//结束
end
end
FFT波形分析
- 频率理论分辨率:
F s N \frac{Fs}{N} NFs
- Fs为输入FFT的时钟频率,决定了输入FFT的采样间隔
- N为FFT的点数
- 如果假设FFT结果峰值的最佳位置为 f = F s 4 f=\frac{Fs}{4} f=4Fs则 F s = 4 × f Fs=4\times f Fs=4×f
- FFT ip核需要配置target clock,如下图所示,最低1MHz
- PLL分频有限制,当input clk为100MHz时:
频率范围是4.687M到800M
再低的频率只能计数器分频 - 1M clk_fft,1024点下,测得100kHz正弦频率为 102.5 k H z = 105 × 102.5kHz=105\times 102.5kHz=105×,此时理论频率分辨率为 1 0 6 ÷ 1024 = 976.5 {10^6\div1024}=976.5 106÷1024=976.5, 频率误差为2.5kHz。理论上的频点应当在
H题解题
看了一篇H题分析特别好的文章,有所感悟:
H题的难点有两个
- 其一是准确分析得到间隔5kHz的频率,在这个要求下,2.5kHz的误差是无法接受的
- 其二是准确复现波形,其频率差要求在万分之一以内,只用DDS无法做到,必须要通过计算复现信号与原始信号的相位差,实时进行调整。
问题一:准确识别频率
- 思路一:寻找FFT的更准确结果
- 调整Fs , L , CLK
- 改变寻找峰值的方法, 尝试通过多点幅值找到更准确的频点
- 分析现有频点不准的原因, 重点分析头尾部分
- 分析结果:现有FFT可以满足5kHz间隔的频率分隔,尝试一下50k和100k正弦的混合、分离
- 思路二: 在想要的频点上使用DFT, 从而更灵活地选择点数. 要求理解点数和采样率的倍数关系对结果造成的影响, 以及DFT的实际编程实现
50k和100k分离
- DDS加法
- FFT找最高峰和次高峰
下图是100k正弦的[47:0]fft_abs(实际上是实数虚数的平方和)
- 尝试截取[40:30],还活着的信号,就是比较大的了,记录每个非零数据以及对应的tuser,搞个3位的数组,每个fft_clk都判断一下。因为关注的频率分量比较少,图省事可以来个数组,后面来的数据就不要了,根据满没满的标志位来判断。如果要“喜新厌旧”,那就搞一个数组构成的移位寄存器,或者FIFO ip,如果数据量比较大的话。
- 上述方法有个不太通用的问题,就是峰值要多大才算大,要截取的位数不好确定也不好调整
- 最好的还是设一个最大值判断,再来一个例如8个数的移位寄存,采用FIFO的队列模式,把历史的f_max都记录在内,这样可以随时读取到最大的8个值。其实找最大值的过程本身就是2个数的移位寄存,结合数组和循环的语言,可以更清晰地实现多个数的移位寄存,改写这段代码,替换原代码中找最值和对称取反的部分,只考虑0-511的点位即可。
- 上面的思路有个问题,就是不能用最大值判断当做移位寄存器的输入,因为前面的较大值会掩盖后面的较小值
- 首先应当是舍弃一些位数节约资源,本身这个平方和就没必要,直接取abs的和也能体现幅值大小,不过这里要研究一下负数取模怎么写
- signed值其实就是补码,那么正数的补码不变,负数按位取反+1即可,代码如下:
`timescale 1ns / 1ps
//输入signed数(其实是补码),输出模值
module signed_to_abs(
input signed [23:0]A ,
output signed [23:0]B
);
always @(*) begin
if (A >= 0)
B = A;
else
B = ~A + 1;
end
endmodule
-
从fft_m_data_tdata里面接出来的低24位是实部,高24位是虚部,这里要注意,xfft的summary界面虽然如下写着只有[18:0]位是实部,但其实[19-23]仍然被当做实部的一部分,在表示负数时也会补上符号位1或者0。因此可以总结一下,ip核中的位数提示了实际有用位数,高位可以随意戒掉,但是不戒掉也没有关系,前提是一定要当做signed数处理
-
fft_m_data_tvalid为高电平是有输出的时候
-
fft_m_data_tuser从0到 L e n g t h 2 − 1 \frac{Length}{2}-1 2Length−1
-
移位寄存器示例代码:
reg [7:0] shift_reg [0:7];
integer i;
always @(posedge clk or posedge reset) begin
if (reset) begin
for (i = 0; i < 8; i = i + 1) begin
shift_reg[i] <= 8'b0;
end
end else begin
shift_reg[0] <= data_in;
for (i = 1; i < 8; i = i + 1) begin
shift_reg[i] <= shift_reg[i-1];
end
end
end
- 移位寄存器方法思路:首先将shift_reg初始化为0(最小值),新来的数使用插入排序,遍历过程中,一旦大于当前值[i]就插入。具体如何插入?[idx_max]到[i+1]移位,腾出[i]的位置,之后将新数写入[i].
- 如何给新数?如何给valid信号:新数从fft_amp输入,以clk_fft为时钟,fft_m_data_tvalid(高电平)结合fft_m_data_tuser为信号
关于for循环问题
- vivado仿真器可以运行for(integer i…)的写法,modelsim的报错说是只有systemverilog才能这么写,不给仿真
- 同时, 应当想想for循环在verilog中究竟是展开成全部并行,还是类似状态机顺序执行,实际情况如下:
- 仿真(Simulation):
在仿真中,for 循环是顺序执行的。每次仿真时钟周期中,循环体都会按顺序执行。仿真主要用于描述和验证设计的行为,不会自动展开成并行逻辑。 - 综合(Synthesis):
综合工具在转换 Verilog 代码为实际硬件时,可能会将 for 循环展开成并行硬件。综合工具会根据循环的内容、迭代次数以及设计的具体需求来决定是否展开以及如何展开。如果循环中的操作是独立的,那么工具可能会将其展开成并行逻辑。这种展开使得每个循环迭代可以在硬件中同时进行,提高处理速度。 - 总的来说,正确使用for是一个比较省心的方法,可以把问题丢给synthesis.如果不用,自行展开时要考虑是并行,还是线性顺序执行(if else),还是网状状态机执行
关于数组问题
- 数组是不好当in/out端口的,建议还是展开来写,除非是内部信号可以用一下简化代码
关于signed
- output signed reg [23:0]B这行代码在vivado和modelsim中都会报错,去掉signed后正常。这个值本身是无符号,这告诉我们非必要不用signed
找多个最大值
- 如上图,找最值已经完成,接下来就是要同时存储对应的频点。思路:将每个频率amp和idx绑定操作,sorted变成二维数组,后面一个存储tuser,排序只看amp,移动数时一组一组移动,idx永远跟着amp走
- 使用结构体组合amp和idx(这个只支持system verilog,哭,该花时间升级工具了)
// 结构体定义
typedef struct {
reg [24:0] amp;
reg [9:0] index;
} amp_index_t;
- 数据进来时先创建好struct
- a<=a没有什么硬件意义,空的else语句可以补血,if后面不是一定要有else
- 这里有一个问题,一直没考虑过,tuser对应的应该是mdata,但是mdata经过abs运算后有延迟,按理说应该从mdata开始就要和tuser绑定才对
- 另一种思路就是只处理幅值,得到几个值之后,idx只跟着对应到abs,后面找到amp后,去abs里面搜索这几个值的idx
- 鉴于之前平方和运算没有时延影响,先这么搞,看看结果准不准
- 看了眼结果,影响挺大的,idx和amp对不上,没啥用。而且这样采集的前8,很可能还是扎堆在那个最高峰周围的,应该设立一个机制,一定频率内的只能取一个代表,避免泄露出去的频谱一起作妖。后面的工作:1.测试用50k和100k的加法来做。2.idx和amp要想办法对应上。3.要设立上述的代表机制