PLL锁相环介绍以及IP调用
我对vivado一无所知,如果有更好的操作,希望评论我
1. PLL锁相环
1.1. PLL锁相环介绍
PPL(Phase Locked Loop),能够对输入的周期信号进行任意分频、倍频、相位调整、占空比调整,从而输出期望时钟。也可以对不进行分频倍频的时钟信号进行优化,使输出的时钟信号比输入的时钟信号在抖动方面性能更好。
模拟锁相环:输出的信号稳定性更高,相位连续可调,延时连续可调。温度过高或电磁辐射过强会导致失锁。
-
基本工作原理:反馈系统
ref_clk:参考时钟
FD/PD:鉴频鉴相器。比较参考时钟和对比时钟的大小,若两者频率相同,输出0;若参考时钟大,输出一个变大的、成正比的值;若参考时钟小。输出一个变小的、成正比的值
LF:环路滤波器。输出不同电压幅值的信号
VCO:压控振荡器。电压越高,输出信号的频率越大
pll_out:输出时钟
举例:参考时钟为50MHz,VCO基准震荡频率为10HMz
两路信号传入FD/PD鉴频鉴相器,参考频率>对比频率,输出一个变大的、成正比的值,LF环路滤波器接收到后,输出电压变大,VCO接收到后将输出的频率变大,作为反馈信号又重新送给鉴频鉴相器。经过多次调整,最终对比时钟会等于参考时钟 -
PLL倍频
DIV为分频器,分频参数可调节
举例:ref_clk为50MHz,DIV分频器送给FD/PD鉴频鉴相器的时钟也会是50MHz,反馈调节
若DIV实现2分频,输出为50MHz,则VCO输出100MHz,pll_out = 100MHz
若DIV实现3分频,输出为50MHz,则VCO输出150MHz,pll_out = 150MHz
(疑问:都已经有分频器模块了,为啥还要pll) -
PLL分频
先将输入的参考时钟进行分频,
举例:若ref_clk为50MHz,分频器进行5分频,则VCO输出10MHz
1.2. IP调用
-
新建工程后,打开IP Catalog
-
搜索找到Clocking Wizard,双击开始进行配置
-
配置IP核
输出时钟配置
-
配置一直ok,完了继续generate
-
之后就能看到文件目录刷新,点击clk_wiz_o前面的展开
-
新建源文件,实例化IP
IP Sources选项卡中,打开.v文件,可以看到模块的端口信息,参考这个端口实例化
-
生成RTL
1.3. 仿真
// 生成50MHz的系统时钟
parameter clk_period_50 = 20;
always #(clk_period_50 / 2) clk_in1 = ~clk_in1;
后面有规律的生成倍频和分频时钟可以理解,就是不太理解复为无效后隔一段时钟才能生成有效的输出时钟,locked信号可以作为信号稳定的标志
2. ROM
2.1 IP调用
-
Vivado选择IP核,开始配置
-
选择添加.coe文件,因为没有写好的.coe文件,点击Edit,新建并存放在默认路径下
ROM实现的是非易失性存储器,需要对其进行初始化
这里就可以编辑.coe文件,也可以先保存
可以看到文件路径是红色的,这就代表不可用
之后用编辑器打开.coe文件,写入一下信息
(如何快速填充设置内容我还不太会,可以用C或者其他语言循环打印,然后粘贴过来)
radix代表进制格式
vector代表数据memory_initialization_radix = 16 ; memory_initialization_vector = 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1a, 1b, 1c, 1d, 1e, 1f ;
重新保存.coe文件,可以看到上图文件路径变成黑色,就证明好了
可以选中“Fill …”,将剩余没有指定数据的内存位置用“FF”填充,然后OK
可以看到文件目录更新,IP添加成了,系数文件也添加成了
-
实例化IP
打开IP Sources中的.v文件,查看端口信息
(这里是可以看到用来实例化的端口名称,端口比较少,见名知意,如果是端口比较多的可能就不太清除怎么连线了,不知道那里可以查看到端口解释信息)
或者还可以打开RAM_IP\RAM_IP.ip_user_files\ip\ram_8x256_one这个路径里面的.veo文件,直接可以看到人家弄好的实例化,自己改一下实例化名就好了(这个看起来好操作一点)
5. 生成RTL图
2.2. 仿真
分为两部分,前一部分地址由随机数种子产生,后一部分为顺序读取
module sim_rom(
);
reg clk ;
reg oe ;
reg [7:0] addr ;
wire [7:0] data ;
rom_useIP rom_useIP0 (
.clk (clk) ,
.oe (oe) ,
.addr (addr) ,
.data (data)
);
parameter clk_period_50 = 20;
always # (clk_period_50 / 2) clk = ~clk;
initial begin
clk = 0;
oe = 0;
#100
read_rom; #500
oe = 1;
#100
read_rom; #500
oe = 0;
#100
oe = 1;
addr = 0;
read_rom_2;
end
task read_rom;
begin
repeat (200) begin
#20
addr = {$random} % 256;
end
end
endtask
task read_rom_2;
begin
repeat (200) begin
#20
addr = addr + 1'b1;
end
end
endtask
endmodule
局部放大可以看到输入地址和输出数据中间差了两个两个节拍,这是因为默认有了寄存器
差不多是这个意思,可以试想一下,如果选择不添加时钟使能信号,输入地址和输出数据中间是不是差一个节拍?
我对vivado里面IP的调用一无所知
在Quartus里的IP是可以设置这个输出寄存器的有无(visio图画的是Quartus),我对veivadou一无所知,不太会设置
看了官方文档,说是对数据进行了流量控制,也就是打拍(底下的截图是Vivado文档说的,同样适用于单双端口ROM、RAM)
3. RAM
3.1. RAM介绍
- RAM是随机存取存储器(Random Access Memory),是一个 易失性存储器;可以对任意一个指定的地址写入或读出数据。这是ROM不具备的功能,它只能提前准备好数据,以后只能读出。
- 双端口RAM,可能会存在在同一时间,两个端口对同一地址同时读写,就会引发冲突。简单双端口(Single Dual Port RAM),一个端口只负责读,另一个端口只负责写;真正双端口(True Dual Port RAM),两个端口都可以进行数据读写。
- 单端口RAM(Single Port RAM),读写数据都用一个地址端口,读写操作不能同时进行。
- 因为RAM实现的是易失性存储器,能实现读写操作,那么这里就会涉及到几种冲突:
- 读写冲突:影响有效数据输出
- 写写冲突:影响内存内容
3.2. IP调用
-
选择IP Core,和前面的ROM用的时同一个
-
配置IP核
补充:操作模式有一下三种,差别就在dout端口的输出上
关于可选择的寄存器:Primitives Output Register(原语输出寄存器,默认勾选)、Core Output Register(核输出寄存器)。两者都会增加最终输出的延迟,多打拍
点击ok,配置,然后就能看见文件目录更新,IP添加成功
这次给系数文件里面写这0-15的循环数据memory_initialization_radix = 10 ; memory_initialization_vector = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1, 2, 3, 4, 5, 6, 7, 8, ... ... ;
3.3. 仿真
// 根据输入的读写信号,产生控制ramIP核的一些控制信息
module ram_ctrl(
input wire sys_clk ,
input wire sys_rst_n ,
input wire wr_flag ,
input wire rd_flag ,
output reg wr_en ,
output reg [7:0] addr ,
output wire [7:0] wr_data ,
output reg rd_en
);
parameter CNT_MAX = 24'd9_999_999;
reg [23:0] cnt_200ms ; // 让变化的地址保持200ms,也就是200_000_000ns
//输入时钟为50MHz(每一个周期20ns),可以确定计数器的计数范围为0~9_999_999
// cnt_200ms:读出时间间隔
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
cnt_200ms <= 24'd0; // 不工作
else if ((cnt_200ms == CNT_MAX) || (wr_flag == 1'b1) || (rd_flag == 1'b1))
cnt_200ms <= 24'd0; // 计数器清零
else if (rd_en == 1'b1)
cnt_200ms <= cnt_200ms + 1'b1;
else
cnt_200ms <= cnt_200ms;
// wr_en:写使能信号
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
wr_en <= 1'b0;
else if (addr == 8'd255)
wr_en <= 1'b0;
else if (wr_flag == 1'b1)
wr_en <= 1'b1;
// addr:读写地址
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
addr <= 8'd0; // 不工作
else if (((addr == 8'd255) && (wr_en == 1'b1)) // 进行到写操作的最后一步
|| ((addr == 8'd255) && (cnt_200ms == CNT_MAX)) // 读操作读到最后一个地址
|| (wr_flag == 1'b1) // 读操作过程中按下写操作按键
|| (rd_flag == 1'b1)) // 读操作过程中按下大操作按键
addr <= 8'd0; // 清零
else if ((wr_en == 1'b1) || ((rd_en == 1'b1) && (cnt_200ms == CNT_MAX)))
addr <= addr + 1'b1;
else
addr <= addr;
// wr_data:写数据,写操作过程中一直等于地址
assign wr_data = (wr_en == 1'b1) ? addr : 8'd0;
// rd_en: 读使能信号
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
rd_en <= 1'b0;
else if (wr_flag == 1'b1)
rd_en <= 1'b0;
else if (rd_flag == 1'b1)
rd_en <= 1'b1;
else
rd_en <= rd_en;
endmodule
然后对ram_ctrl模块进行仿真,在sim文件中需要实例化RAM-IP,将ctrl模块生成的控制信号送给IP核,将IP核的读出信息用连线引出以便观察
module sim_ram ( );
reg sys_clk ;
reg sys_rst_n ;
reg wr_flag ;
reg rd_flag ;
wire wr_en ;
wire [7:0] addr ;
wire [7:0] wr_data ;
wire rd_en ;
wire [7:0] data_out ;
ram_ctrl ram_ctrl_inst (
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.wr_flag (wr_flag ),
.rd_flag (rd_flag ),
.wr_en (wr_en ),
.addr (addr ),
.wr_data (wr_data ),
.rd_en (rd_en )
);
// 为了能实现读写数据,这里还需要例化RMA-IP核
ram_8x256_one ram_8x256_one_name (
.clka (sys_clk), // 时钟信号
.ena (sys_rst_n), // 使能信号
.wea (wr_en), // 读写使能信号
.addra (addr), // 读写地址
.dina (wr_data), // 写入数据
.douta (data_out) // 读出数据
);
// 参数重定义
defparam ram_ctrl_inst.CNT_MAX = 10; // 这里可以将ctrl模块中的参数定义为10,变小有利于仿真
// 50MHz
parameter clk_period_50M = 20;
always # (clk_period_50M / 2) sys_clk = ~sys_clk;
initial begin
sys_clk = 1;
sys_rst_n = 0;
wr_flag = 0;
rd_flag = 0;
#20
sys_rst_n = 1;
#1000
// rd_flag 先读出原始数据
rd_flag = 1;
#20
rd_flag = 0;
# 60_000 // 256个数据,每个等待11个时钟周期,一个时钟周期20ns
// 256 * 11 * 20 == 56320
// wr_flag 写入一组新数据
wr_flag = 1;
#20
wr_flag = 0;
#6_000 // 256个数据,每个等待一个时钟周期,一个时钟周期20ns
// 256 * 20 = 5120
// rd_flag 读出新数据
rd_flag = 1;
#20
rd_flag = 0;
# 60_000 // 256个数据,每个等待11个时钟周期,一个时钟周期20ns
// 256 * 11 * 20 == 56320
// rd_flag 读操作中重新开始读操作
rd_flag = 1;
#20
rd_flag = 0;
# 60_000 // 256个数据,每个等待11个时钟周期,一个时钟周期20ns
// 256 * 11 * 20 == 56320
;
end
endmodule
第一次,读取原始数据
第二次,写入新数据
第三次,读出新数据
第四次,读操作被读操作打断