• 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 模块代码

    1. //------------------------------------------------------------------------------
    2. // pwm_modulator.sv
    3. // Konstantin Pavlov, pavlovconst@gmail.com
    4. //------------------------------------------------------------------------------
    5. // INFO ------------------------------------------------------------------------
    6. // Pulse width modulation (PWM) generator module
    7. //
    8. // - expecting 8-bit control signal input by default
    9. // - system clock is 100 MHz by default
    10. // - PWM clock is 1.5KHz by default
    11. //
    12. // - see also pdm_modulator.sv for pulse density modulation generator
    13. /* --- INSTANTIATION TEMPLATE BEGIN ---
    14. pwm_modulator #(
    15. .PWM_PERIOD_DIV( 16 ) // 100MHz/2^16= ~1.526 KHz
    16. .MOD_WIDTH( 8 ) // from 0 to 255
    17. ) pwm1 (
    18. .clk( clk ),
    19. .nrst( nrst ),
    20. .control( ),
    21. .pwm_out( ),
    22. .start_strobe( ),
    23. .busy( )
    24. );
    25. --- INSTANTIATION TEMPLATE END ---*/
    26. module pwm_modulator #( parameter
    27. CLK_HZ = 100_000_000,
    28. PWM_PERIOD_DIV = 16, // must be > MOD_WIDTH
    29. PWM_PERIOD_HZ = CLK_HZ / (2**PWM_PERIOD_DIV),
    30. MOD_WIDTH = 8 // modulation bitness
    31. )(
    32. input clk, // system clock
    33. input nrst, // negative reset
    34. input [MOD_WIDTH-1:0] mod_setpoint, // modulation setpoint
    35. output pwm_out, // active HIGH output
    36. // status outputs
    37. output start_strobe, // period start strobe
    38. output busy // busy output
    39. );
    40. // period generator
    41. logic [31:0] div_clk;
    42. clk_divider #(
    43. .WIDTH( 32 )
    44. ) cd1 (
    45. .clk( clk ),
    46. .nrst( nrst ),
    47. .ena( 1'b1 ),
    48. .out( div_clk[31:0] )
    49. );
    50. // optional setpoint inversion
    51. logic [MOD_WIDTH-1:0] mod_setpoint_inv;
    52. assign mod_setpoint_inv[MOD_WIDTH-1:0] = {MOD_WIDTH{1'b1}} - mod_setpoint[MOD_WIDTH-1:0];
    53. // pulse generator
    54. pulse_gen #(
    55. .CNTR_WIDTH( MOD_WIDTH+1 )
    56. ) pg1 (
    57. .clk( div_clk[(PWM_PERIOD_DIV-1)-MOD_WIDTH] ),
    58. .nrst( nrst ),
    59. .start( 1'b1 ),
    60. .cntr_max( {1'b0, {MOD_WIDTH{1'b1}} } ),
    61. .cntr_low( {1'b0, mod_setpoint_inv[MOD_WIDTH-1:0] } ),
    62. .pulse_out( pwm_out ),
    63. .start_strobe( start_strobe ),
    64. .busy( busy )
    65. );
    66. 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。

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

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

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

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

    4 TestBench与仿真结果

    1. //------------------------------------------------------------------------------
    2. // pwm_modulator_tb.sv
    3. // Konstantin Pavlov, pavlovconst@gmail.com
    4. //------------------------------------------------------------------------------
    5. // INFO ------------------------------------------------------------------------
    6. // testbench for pwm_modulator.sv module
    7. `timescale 1ns / 1ps
    8. module pwm_modulator_tb();
    9. logic clk200;
    10. initial begin
    11. #0 clk200 = 1'b0;
    12. forever
    13. #2.5 clk200 = ~clk200;
    14. end
    15. // external device "asynchronous" clock
    16. logic rst;
    17. initial begin
    18. #0 rst = 1'b0;
    19. #10.2 rst = 1'b1;
    20. #5 rst = 1'b0;
    21. //#10000;
    22. forever begin
    23. #9985 rst = ~rst;
    24. #5 rst = ~rst;
    25. end
    26. end
    27. logic nrst;
    28. assign nrst = ~rst;
    29. logic rst_once;
    30. initial begin
    31. #0 rst_once = 1'b0;
    32. #10.2 rst_once = 1'b1;
    33. #5 rst_once = 1'b0;
    34. end
    35. logic nrst_once;
    36. assign nrst_once = ~rst_once;
    37. logic [31:0] DerivedClocks;
    38. clk_divider #(
    39. .WIDTH( 32 )
    40. ) cd1 (
    41. .clk( clk200 ),
    42. .nrst( nrst_once ),
    43. .ena( 1'b1 ),
    44. .out( DerivedClocks[31:0] )
    45. );
    46. // Modules under test ==========================================================
    47. localparam MOD_WIDTH = 5;
    48. logic [MOD_WIDTH-1:0] sp = '0;
    49. logic [31:0][MOD_WIDTH-1:0] sin_table =
    50. { 5'd16, 5'd19, 5'd22, 5'd25, 5'd27, 5'd29, 5'd31, 5'd31,
    51. 5'd31, 5'd31, 5'd30, 5'd28, 5'd26, 5'd23, 5'd20, 5'd17,
    52. 5'd14, 5'd11, 5'd8, 5'd5, 5'd3, 5'd1, 5'd0, 5'd0,
    53. 5'd0, 5'd0, 5'd2, 5'd4, 5'd6, 5'd9, 5'd12, 5'd15};
    54. logic strobe;
    55. always_ff @(posedge DerivedClocks[0]) begin
    56. if( ~nrst_once ) begin
    57. sp[MOD_WIDTH-1:0] <= '0;
    58. end else begin
    59. if( strobe ) begin
    60. sp[MOD_WIDTH-1:0] <= sp[MOD_WIDTH-1:0] + 1'b1;
    61. end
    62. end
    63. end
    64. pwm_modulator #(
    65. .PWM_PERIOD_DIV( MOD_WIDTH+1 ), // MOD_WIDTH+1 is a minimum
    66. .MOD_WIDTH( MOD_WIDTH )
    67. ) pwm1 (
    68. .clk( clk200 ),
    69. .nrst( nrst_once ),
    70. .mod_setpoint( sin_table[sp[MOD_WIDTH-1:0]][MOD_WIDTH-1:0] ),
    71. .pwm_out( ),
    72. .start_strobe( strobe ),
    73. .busy( )
    74. );
    75. 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指示的是本次脉冲的参数,而不是下一个脉冲的参数,这与我们之前的分析也是一致的。


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

  • 相关阅读:
    深度学习——python中的广播
    【面试HOT100】子串&&普通数组&&矩阵
    详解 spring data jpa,全方位总结,干货分享
    RAII技术学习
    [Ynoi2016] 镜中的昆虫——浅谈区间种类数问题
    如何使用Vuex来管理应用程序的状态?
    在GitHub上学黑客 --- 黑客成长技术清单
    敏捷开发工具:提升软件研发效率的重要利器
    ZEMAX | 室内照明案例分享2 —— 室内场景模拟
    python之最简单的车辆检测(opencv方式)
  • 原文地址:https://blog.csdn.net/Alex497259/article/details/126293547