碎碎念:
这一篇带来的是比较常用的PWM调制器,仿真的波形很好看哟!
感觉之前模块功能说明部分有些简单了,之后会加上专门讲解端口定义的表格!
这里发现了原作者的小bug,稍微有一丢丢成就感,在和人家进行沟通交流ing
目录
输入8比特控制信息,输出对应的脉冲宽度调制信号。具体端口信息如下:
参数/端口名 | 功能说明 |
CLK_HZ | 默认系统时钟频率 |
PWM_PERIOD_DIV | 与MOD_WIDTH共同控制信号宽度 |
PWM_PERIOD_HZ | PWM频率(实际上没用用到这个参数) |
MOD_WIDTH | 调制控制信号的宽度 |
clk | 输入系统时钟 |
nrst | 系统复位信号 |
mod_setpoint | 调制的设定值 |
pwm_out | PWM输出信号 |
start_strobe | 开始某一次调制的输出标志 |
busy | 运行状态忙碌标志 |
- //------------------------------------------------------------------------------
- // 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
-
整个模块的思路,通过外部输入调制的设定值,结合前期提及的脉冲发生器,产生脉宽符合要求的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的高电平就标志了每一个脉冲的起点,具体细节可以查看传送门,这个标志也是很关键的。
- //------------------------------------------------------------------------------
- // 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指示的是本次脉冲的参数,而不是下一个脉冲的参数,这与我们之前的分析也是一致的。
这就是本期的全部内容啦,如果你喜欢我的文章,不要忘了点赞+收藏+关注,分享给身边的朋友哇~