Verilog:【6】PWM调制器(pwm_modulator.sv)

碎碎念:

这一篇带来的是比较常用的PWM调制器,仿真的波形很好看哟!

感觉之前模块功能说明部分有些简单了,之后会加上专门讲解端口定义的表格!

这里发现了原作者的小bug,稍微有一丢丢成就感,在和人家进行沟通交流ing

目录

1 模块功能

2 模块代码

3 模块思路

4 TestBench与仿真结果


1 模块功能

输入8比特控制信息,输出对应的脉冲宽度调制信号。具体端口信息如下:

参数与输出输出端口
参数/端口名功能说明
CLK_HZ默认系统时钟频率
PWM_PERIOD_DIV与MOD_WIDTH共同控制信号宽度
PWM_PERIOD_HZPWM频率(实际上没用用到这个参数)
MOD_WIDTH调制控制信号的宽度
clk输入系统时钟
nrst系统复位信号
mod_setpoint调制的设定值
pwm_outPWM输出信号
start_strobe开始某一次调制的输出标志
busy运行状态忙碌标志

2 模块代码

//------------------------------------------------------------------------------
// pwm_modulator.sv
// Konstantin Pavlov, pavlovconst@gmail.com
//------------------------------------------------------------------------------

// INFO ------------------------------------------------------------------------
// Pulse width modulation (PWM) generator module
//
// - expecting 8-bit control signal input by default
// - system clock is 100 MHz by default
// - PWM clock is 1.5KHz by default
//
// - see also pdm_modulator.sv for pulse density modulation generator


/* --- INSTANTIATION TEMPLATE BEGIN ---

pwm_modulator #(
  .PWM_PERIOD_DIV( 16 )              // 100MHz/2^16= ~1.526 KHz
  .MOD_WIDTH( 8 )                    // from 0 to 255
) pwm1 (
  .clk( clk ),
  .nrst( nrst ),

  .control(  ),
  .pwm_out(  ),

  .start_strobe(  ),
  .busy(  )
);

--- INSTANTIATION TEMPLATE END ---*/

module pwm_modulator #( parameter
  CLK_HZ = 100_000_000,
  PWM_PERIOD_DIV = 16,                // must be > MOD_WIDTH
  PWM_PERIOD_HZ = CLK_HZ / (2**PWM_PERIOD_DIV),

  MOD_WIDTH = 8                       // modulation bitness
)(
  input clk,                          // system clock
  input nrst,                         // negative reset

  input [MOD_WIDTH-1:0] mod_setpoint, // modulation setpoint
  output pwm_out,                     // active HIGH output

  // status outputs
  output start_strobe,                // period start strobe
  output busy                         // busy output
);


// period generator
logic [31:0] div_clk;
clk_divider #(
  .WIDTH( 32 )
) cd1 (
  .clk( clk ),
  .nrst( nrst ),
  .ena( 1'b1 ),
  .out( div_clk[31:0] )
);


// optional setpoint inversion
logic [MOD_WIDTH-1:0] mod_setpoint_inv;
assign mod_setpoint_inv[MOD_WIDTH-1:0] = {MOD_WIDTH{1'b1}} - mod_setpoint[MOD_WIDTH-1:0];


// pulse generator
pulse_gen #(
  .CNTR_WIDTH( MOD_WIDTH+1 )
) pg1 (
  .clk( div_clk[(PWM_PERIOD_DIV-1)-MOD_WIDTH] ),
  .nrst( nrst ),

  .start( 1'b1 ),
  .cntr_max( {1'b0, {MOD_WIDTH{1'b1}} } ),
  .cntr_low( {1'b0, mod_setpoint_inv[MOD_WIDTH-1:0] } ),

  .pulse_out( pwm_out ),

  .start_strobe( start_strobe ),
  .busy( busy )
);


endmodule

3 模块思路

整个模块的思路,通过外部输入调制的设定值,结合前期提及的脉冲发生器,产生脉宽符合要求的PWM信号。下面开始逐行介绍一下整个的思路。

1.定义端口与参数(34-50行)

这部分就比较简单,用来定义模块使用到的参数以及一些端口信息,具体的功能见上面的表格。

2.实例化时钟分频器(53-62行)

时钟分频器的详细介绍见传送门。用来产生计数后宽度为32的计数值div_clk,可把其中的某一位作为后续脉冲发生器的驱动时钟(妙啊)。

3.可供选择的设定值反转(65-67行)

这里的思路,就是用全为1的变量去减去设定值mod_setpoint,从而获得互补的设定值mod_setpoint_inv。由于设定值就对应了脉宽调制的cntr_low部分,即为高低电平跳变的位置,因此可以理解为修改了占空比为原来的(1-占空比)。

值得注意的是,N位全为1的寄存器,可以表示为{N{1'b1}},相比手写就方便了不少。

4.实例化脉冲发生器(70-85行)

脉冲发生器的详细介绍见传送门。这里是一个比较重要的地方,有很多值得关注的点。

.CNTR_WIDTH( MOD_WIDTH+1 )

这一句定义了脉冲发生器的CNTR_WIDTH 。结合78-79行,这两行定义了脉冲发生器的cntr_max与cntr_low数值。由脉冲发生器的原理可知,这三个数必须得是相同宽度的,因为涉及到比较大小。在这里MOD_WIDTH+1的操作,就是为了和78-79行在左侧补充的符号位0相匹配。

我的理解是,在本模块pwm_modulator.sv中我定义一个5位的数值,其最大表示到31,但是调用内部模块pulse_gen.sv时,我需要使用有符号数(否则内部因为是减1操作,如果表示范围不足,会导致出现溢出bug),因此需要补充一个符号位。

另一个值得关注的点是这一行代码

  .clk( div_clk[(PWM_PERIOD_DIV-1)-MOD_WIDTH] ),

这句话提供了脉冲发生器需要的时钟信号,同时也表明了PWM_PERIOD_DIV与MOD_WIDTH和所产生的脉冲信号的关系,以及为什么PWM_PERIOD_DIV要大于MOD_WIDTH,我们一个一个说。

在时钟分频器中我们得到,div_clk是一个32位计数器的计数结果,如果计数器的输入时钟(即系统时钟频率)频率是100MHz,也就说明计数器最低位div_clk[0]的变化频率是50MHz,递推可以得到div_clk[1]的变化频率就是25MHz,越高的位上变化频率依次减半。

假设调制后的信号每个脉冲长度是32个单位,那么就对应了MOD_WIDTH=5(即2的5次方是32)。下面来说这里的单位到底指什么,在脉冲发生器我们提到过脉冲的占空比是由内部的计数器决定的,计数器在系统时钟上升沿计数一次,因此一个单位就是一个时钟周期。而在本模块中单位的长度受到PWM_PERIOD_DIV的控制,PWM_PERIOD_DIV - MOD_WIDTH=1,则单位长度是两个系统时钟周期,PWM_PERIOD_DIV - MOD_WIDTH=2,则单位长度是四个系统时钟周期。

回到表达式div_clk[(PWM_PERIOD_DIV-1)-MOD_WIDTH],其中PWM_PERIOD_DIV越大,则对原始时钟进行了2指数的分频,而MOD_WIDTH的增大,则表明按照2的指数进行倍频,最终决定计数器输入时钟(脉冲发生器输入时钟)的频率。

由于表达式中(PWM_PERIOD_DIV-1)-MOD_WIDTH的计算,这一结果必须是大于等于0的,因此也就决定了PWM_PERIOD_DIV要大于MOD_WIDTH。

这里提到的时钟比较多,读者一定要注意分辨。系统时钟、时钟分频器输出的时钟(派生时钟)并不是等价的,读者一定要注意。

  .cntr_max( {1'b0, {MOD_WIDTH{1'b1}} } ),
  .cntr_low( {1'b0, mod_setpoint_inv[MOD_WIDTH-1:0] } ),

算法核心就是这两行啦,输入的cntr_low的值就决定了输出的每一个脉冲的宽度。 

start_strobe的高电平就标志了每一个脉冲的起点,具体细节可以查看传送门,这个标志也是很关键的。

4 TestBench与仿真结果

//------------------------------------------------------------------------------
// pwm_modulator_tb.sv
// Konstantin Pavlov, pavlovconst@gmail.com
//------------------------------------------------------------------------------

// INFO ------------------------------------------------------------------------
// testbench for pwm_modulator.sv module


`timescale 1ns / 1ps

module pwm_modulator_tb();

logic clk200;
initial begin
  #0 clk200 = 1'b0;
  forever
    #2.5 clk200 = ~clk200;
end

// external device "asynchronous" clock

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] )
);

// Modules under test ==========================================================

localparam MOD_WIDTH = 5;

logic [MOD_WIDTH-1:0] sp = '0;
logic [31:0][MOD_WIDTH-1:0] sin_table =
{ 5'd16, 5'd19, 5'd22, 5'd25, 5'd27, 5'd29, 5'd31, 5'd31,
  5'd31, 5'd31, 5'd30, 5'd28, 5'd26, 5'd23, 5'd20, 5'd17,
  5'd14, 5'd11, 5'd8,  5'd5,  5'd3,  5'd1,  5'd0,  5'd0,
  5'd0,  5'd0,  5'd2,  5'd4,  5'd6,  5'd9,  5'd12, 5'd15};

logic strobe;
always_ff @(posedge DerivedClocks[0]) begin
  if( ~nrst_once ) begin
    sp[MOD_WIDTH-1:0] <= '0;
  end else begin
    if( strobe ) begin
      sp[MOD_WIDTH-1:0] <= sp[MOD_WIDTH-1:0] + 1'b1;
    end
  end
end

pwm_modulator #(
  .PWM_PERIOD_DIV( MOD_WIDTH+1 ),     // MOD_WIDTH+1 is a minimum
  .MOD_WIDTH( MOD_WIDTH )
) pwm1 (
  .clk( clk200 ),
  .nrst( nrst_once ),

  .mod_setpoint( sin_table[sp[MOD_WIDTH-1:0]][MOD_WIDTH-1:0] ),
  .pwm_out(  ),

  .start_strobe( strobe ),
  .busy(  )
);


endmodule

经过对核心模块的分析,目前可以得知每次输入应该只能获得一个调制脉冲,那么为了获得多个PWM脉冲,需要由TestBench实现相关的逻辑。

下面开始介绍TestBench的实现原理,主要从第46行及以下的部分开始,值得关注的地方有三处:

1.System Verilog 二维数组(第63行)

参考博客:[SV]SystemVerilog二維數組的初始化和約束

此处构建了一个元素数量是32,每个元素宽度为5的数组,用来作为正弦函数的查找表。注意System Verilog中的数都是一位一位保存的,因此在使用二维数组时,与Python等编程语言对比会些许不同。

2.sp指针(第62行)

学习过计组的同学,应该对指针比较熟悉,这里就是套用了概念。用来标志每个脉冲信号输入的宽度信息mod_setpoint对应的数组元素下标。

3.strobe(69-78行)

由之前的介绍,没发送一个脉冲时,strobe都会输出一个时钟(派生时钟)的高电平信息。这个信号可以作为sp指针递增的控制信号。

在这段代码中,利用always_ff构建了D触发器,每当开始发送新的脉冲时,就会同步增加sp的值。这一部分的代码我发现了作者一个小问题,主要是D触发器的驱动时钟。原始代码中的驱动时钟是系统时钟,这会导致sp在一个strobe高电平时间段内增加两个值,显然是不对的。我修改为了派生时钟DerivedClocks[0]。

在每个脉冲开始的时候,sp递增一次,从而控制本次脉冲的输出受到二维数组中特定数值的控制。

下面给出原始代码的仿真结果以及和我改正后的对比:

可以看到,修改后变得更加流畅了,同时输出的调制脉冲与cntr_low是相互对应的,证明功能正确。SP指示的是本次脉冲的参数,而不是下一个脉冲的参数,这与我们之前的分析也是一致的。


这就是本期的全部内容啦,如果你喜欢我的文章,不要忘了点赞+收藏+关注,分享给身边的朋友哇~

  • 5
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex-YiWang

不要打赏,想要一个赞

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值