碎碎念:
明明是周四,这周竟然不开组会_(:з)∠)_
那我可以继续愉快地学习人家的代码了,这篇博客介绍的是脉冲发生器,脉冲和Killer Queen是不是很配呢hhh
目录
1 模块功能
通过设置参数cntr_max与cntr_low,可以产生任意周期数与占空比的脉冲信号。
2 模块代码
//------------------------------------------------------------------------------
// pulse_gen.sv
// Konstantin Pavlov, pavlovconst@gmail.com
//------------------------------------------------------------------------------
// INFO ------------------------------------------------------------------------
// Pulse generator module, ver.2
//
// - generates one or many pulses of given width and period
// - generates constant HIGH, constant LOW, or impulse output
// - features buffered inputs, so inputs can change continiously during pulse period
// - generates LOW when idle
//
// - Pulse period is (cntr_max[]+1) cycles
// - If you need to generate constant LOW pulses, then CNTR_WIDTH should allow
// setting cntr_low[]>cntr_max[]
//
// Example 1:
// let CNTR_WIDTH = 8
// let cntr_max[7:0] = 2^CNTR_WIDTH-2 = 254, pulse period is 255 cycles
// cntr_low[7:0]==255 then output will be constant LOW
// 0<cntr_low[7:0]<=cntr_max[7:0] then output will be generating pulse(s)
// cntr_low[7:0]==0 then output will be constant HIGH
//
// Example 2:
// let CNTR_WIDTH = 9
// let cntr_max[8:0] = 255, pulse period is 256 cycles
// cntr_low[8:0]>255 then output will be constant LOW
// 0<cntr_low[8:0]<=cntr_max[8:0] then output will be generating pulse(s)
// cntr_low[8:0]==0 then output will be constant HIGH
//
// In Example 2 constant LOW state can be acheived also by disabling start
// condition or holding reset input, so cntr_low[8:0] and cntr_max[8:0]
// can be left 8-bit-wide actually
/* --- INSTANTIATION TEMPLATE BEGIN ---
pulse_gen #(
.CNTR_WIDTH( 8 )
) pg1 (
.clk( clk ),
.nrst( nrst ),
.start( 1'b1 ),
.cntr_max( 255 ),
.cntr_low( 2 ),
.pulse_out( ),
.start_strobe,
.busy( )
);
--- INSTANTIATION TEMPLATE END ---*/
module pulse_gen #( parameter
CNTR_WIDTH = 32
)(
input clk, // system clock
input nrst, // negative reset
input start, // enables new period start
input [CNTR_WIDTH-1:0] cntr_max, // counter initilization value, should be > 0
input [CNTR_WIDTH-1:0] cntr_low, // transition to LOW counter value
output logic pulse_out, // active HIGH output
// status outputs
output logic start_strobe = 1'b0,
output busy
);
logic [CNTR_WIDTH-1:0] seq_cntr = '0;
logic seq_cntr_0;
assign seq_cntr_0 = (seq_cntr[CNTR_WIDTH-1:0] == '0);
// delayed one cycle
logic seq_cntr_0_d1;
always_ff @(posedge clk) begin
if( ~nrst) begin
seq_cntr_0_d1 <= 0;
end else begin
seq_cntr_0_d1 <= seq_cntr_0;
end
end
// first seq_cntr_0 cycle time belongs to pulse period
// second and further seq_cntr_0 cycles are idle
assign busy = ~(seq_cntr_0 && seq_cntr_0_d1);
// buffering cntr_low untill pulse period is over to allow continiously
// changing inputs
logic [CNTR_WIDTH-1:0] cntr_low_buf = '0;
always_ff @(posedge clk) begin
if( ~nrst ) begin
seq_cntr[CNTR_WIDTH-1:0] <= '0;
cntr_low_buf[CNTR_WIDTH-1:0] <= '0;
start_strobe <= 1'b0;
end else begin
if( seq_cntr_0 ) begin
// don`t start if cntr_max[] is illegal value
if( start && (cntr_max[CNTR_WIDTH-1:0]!='0) ) begin
seq_cntr[CNTR_WIDTH-1:0] <= cntr_max[CNTR_WIDTH-1:0];
cntr_low_buf[CNTR_WIDTH-1:0] <= cntr_low[CNTR_WIDTH-1:0];
start_strobe <= 1'b1;
end else begin
start_strobe <= 1'b0;
end
end else begin
seq_cntr[CNTR_WIDTH-1:0] <= seq_cntr[CNTR_WIDTH-1:0] - 1'b1;
start_strobe <= 1'b0;
end
end // ~nrst
end
always_comb begin
if( ~nrst ) begin
pulse_out <= 1'b0;
end else begin
// busy condition guarantees LOW output when idle
if( busy &&
(seq_cntr[CNTR_WIDTH-1:0] >= cntr_low_buf[CNTR_WIDTH-1:0]) ) begin
pulse_out <= 1'b1;
end else begin
pulse_out <= 1'b0;
end
end // ~nrst
end
endmodule
3 模块思路
在明确了整个模块的功能之后,也就比较看出主要思路就是利用计数器的原理,利用对计数值的判断,来控制输出脉冲的信号,下面来具体进行说明。
1.模块接口定义部分(58-73行)
包含一个内部寄存器宽度参数CNTR_WIDTH;五个输入端口分别是时钟clk、低电平复位信号nrst、新周期开始信号start、起点计数值cntr_max、跳变计数值cntr_low;三个输出端口分别是脉冲输出信号pulse_out、输出状态信号start_strobe、忙碌信号busy。
这里同样使用了logic类型,在前几期的内容也有涉及到。
2.中间变量定义(76-79行)
定义了存储计数值的寄存器seq_cntr、以及标志其是否为0的寄存器seq_cntr_0。
3.延时一个周期(81-89行)
利用always_ff构建D触发器,从而获得seq_cntr_0延时一周期后的信号seq_cntr_0_d1。
4.输出busy信号的逻辑(93行)
当本周期与上一周期计数值都是0的时候,此时表示不在输出有效时间段,即busy=0;当start恒等于1时,因为计数值一定会变化,因此busy始终都是1。
读者可以结合仿真结果来看,我认为这一设计还是比较巧妙的,利用组合逻辑与延时的处理,让busy信号和计数器的结果就没有出现那种会错开一周期的情况(我本人有时会遇到_(:з)∠)_)。
5.计数器运行逻辑(96-119行)
利用always_ff构建D触发器,来实现整个运行的逻辑。当seq_cntr_0=1时,表示此时计数值为0,因此作为脉冲的起点,开始修改cntr_low等参数。
重点关注107行,当start信号出现一周期的高电平,同时cntr_max信号是有效值(大于0)时,将cntr_max的值赋值给seq_cntr;将cntr_low的值赋值给cntr_low_buf;start_strobe置为到1(持续一周期),表示开始启动脉冲的发生器。
之后start信号变为低电平,此时开始每周期将计数器结果减1。
在这里,cntr_max就控制了脉冲发生器的周期;cntr_low相对于cntr_max的大小,就控制了输出的占比空。
6.脉冲信号输出逻辑(121-133行)
这一部分就是比较简单的组合逻辑啦,使用always_comb搭建。通过判断当前的计数值,来控制脉冲信号的输出。
从代码可以看出:seq_cntr大于等于cntr_low_buf时,输出是1;seq_cntr小于cntr_low_buf时,输出是0。
4 TestBench与仿真结果
//------------------------------------------------------------------------------
// pulse_gen_tb.sv
// Konstantin Pavlov, pavlovconst@gmail.com
//------------------------------------------------------------------------------
// INFO ------------------------------------------------------------------------
// testbench for pulse_gen.sv module
`timescale 1ns / 1ps
module pulse_gen_tb();
logic clk200;
initial begin
#0 clk200 = 1'b0;
forever
#2.5 clk200 = ~clk200;
end
// external device "asynchronous" clock
logic clk33;
initial begin
#0 clk33 = 1'b0;
forever
#15.151 clk33 = ~clk33;
end
logic rst;
initial begin
#0 rst = 1'b0;
#10.2 rst = 1'b1;
#5 rst = 1'b0;
//#10000;
forever begin
#9985 rst = ~rst;
#5 rst = ~rst;
end
end
logic nrst;
assign nrst = ~rst;
logic rst_once;
initial begin
#0 rst_once = 1'b0;
#10.2 rst_once = 1'b1;
#5 rst_once = 1'b0;
end
logic nrst_once;
assign nrst_once = ~rst_once;
logic [31:0] DerivedClocks;
clk_divider #(
.WIDTH( 32 )
) cd1 (
.clk( clk200 ),
.nrst( nrst_once ),
.ena( 1'b1 ),
.out( DerivedClocks[31:0] )
);
logic [31:0] E_DerivedClocks;
edge_detect ed1[31:0] (
.clk( {32{clk200}} ),
.anrst( {32{nrst_once}} ),
.in( DerivedClocks[31:0] ),
.rising( E_DerivedClocks[31:0] ),
.falling( ),
.both( )
);
logic [31:0] RandomNumber1;
c_rand rng1 (
.clk( clk200 ),
.rst( 1'b0 ),
.reseed( rst_once ),
.seed_val( DerivedClocks[31:0] ^ (DerivedClocks[31:0] << 1) ),
.out( RandomNumber1[15:0] )
);
c_rand rng2 (
.clk( clk200 ),
.rst( 1'b0 ),
.reseed( rst_once ),
.seed_val( DerivedClocks[31:0] ^ (DerivedClocks[31:0] << 2) ),
.out( RandomNumber1[31:16] )
);
logic start;
initial begin
#0 start = 1'b0;
#100 start = 1'b1;
#20 start = 1'b0;
end
// Modules under test ==========================================================
// simple static test
/*pulse_gen #(
.CNTR_WIDTH( 8 )
) pg1 (
.clk( clk200 ),
.nrst( nrst_once ),
.start( start ),
.cntr_max( 15 ),
.cntr_low( 0 ),
.pulse_out( ),
.busy( )
);
*/
logic [31:0] in_high_width = '0;
logic out;
logic out_rise;
// random test
pulse_gen #(
.CNTR_WIDTH( 8 )
) pg1 (
.clk( clk200 ),
.nrst( nrst_once ),
.start( start ),
.cntr_max( 16 ),
.cntr_low( {4'b0,RandomNumber1[3:0]} ),
.pulse_out( out ),
.busy( )
);
edge_detect out_ed (
.clk( clk200 ),
.anrst( nrst_once ),
.in( out ),
.rising( out_rise ),
.falling( ),
.both( )
);
always_ff @(posedge clk200) begin
if( ~nrst_once ) begin
in_high_width[31:0] <= 1'b0;
end else begin
if( out_rise ) begin
in_high_width[31:0] <= in_high_width[31:0] + 1'b1;
end
end
end
// PWM test
/*pulse_gen #(
.CNTR_WIDTH( 8 )
) pg1 (
.clk( clk200 ),
.nrst( nrst_once ),
.start( 1'b1 ),
.cntr_max( 15 ),
.cntr_low( {4'b0,in_high_width[3:0]} ),
.pulse_out( out ),
.busy( )
);*/
endmodule
下面开始学习一下TestBench的写法,其中我认为值得关注的地方有这几点:
1.#号表示延迟
此前我很少见到使用小数进行延迟的,看来在具有特殊频率要求的情况下,这也是必要的。
2.复用之前的随机数模块
同样对之前的随机数模块以及时钟分频电路进行了复用,从而提高测试的说服力。同时使用到了边沿检测器,用来对输出的脉冲进行上升沿检测。对随机数产色模块以及边沿检测不了解的,可以跳转:
同时注意到,为了生成32位的随机数,使用了两个16位随机数的生成器,将结果进行了拼接,应该是有降低资源使用量的考虑,毕竟过于高位的乘法器综合起来还是开销比较大的。
最后可以注意到这一TestBench包含两个测试,测试1是单独一次脉冲的结果(即下图);测试2是代码中最后被注释的部分,由于start恒为1,因此会持续不断产生受到随机数调制的PWM信号。
下面来看一下测试1的仿真波形,可以看到cntr_max的值是16,cntr_low的值是13,当start信号来一个周期的高电平时,cntr_low将值存储到cntr_low_buff中。
seq_cntr开始从16依次减小到0;busy这段时间为高电平,表示正在输出信号;seq_cntr变为16时,start_strobe出现一个高电平,表示开始输出的起点;pulse_out在计数值大于等于13时,为高电平,其余为低电平。seq_cntr_0与seq_cntr_0_d1对busy信号的产生就非常巧妙,我认为很有趣。
最终证明,波形的展示与我们的分析是保持一致的~
这就是本期的全部内容啦,如果你喜欢我的文章,不要忘了点赞+收藏+关注,分享给身边的朋友哇~