亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> VHDL

如何在 VHDL 中创建 PWM 控制器

脉宽调制 (PWM) 是一种从纯数字 FPGA 引脚控制模拟电子设备的有效方法。 PWM 不是试图调节模拟电压,而是在模拟设备的全功率下快速打开和关闭电源电流。这种方法使我们能够精确控制提供给消费设备的能量的移动平均值。

适合 PWM 的用例示例包括音频调制(扬声器)、光强度控制(灯或 LED)和感应电机。后者包括伺服电机、电脑风扇、泵、电动汽车用无刷直流电机,不胜枚举。

另请参阅:
使用来自 FPGA 引脚的 PWM 的 RC 伺服控制器

PWM 的工作原理

通过高频开关设备的电源,我们可以准确地控制流过它的平均电流。下图显示了 PWM 工作原理的基础知识。 PWM 输出控制一个二进制开关,可以将功率设置为 100% 或 0%。通过在两个极端之间快速交替,滑动窗口平均值将成为每个状态所花费时间的函数。

占空比

占空比是控制在 PWM 中提供给模拟设备的功率的关键。术语工作周期 表示 PWM 输出在 ON 位置花费的时间。通常将占空比描述为百分比,如下图所示。但是,在我的 VHDL 示例中,我将在本文后面使用无符号二进制数。对我们来说,使用二进制数更有意义,它可以代表我们的 VHDL 实现中占空比的完整分辨率。

占空比为 0 时,PWM 输出将持续保持在 OFF 位置,而在 100% 时,它将在 ON 位置不间断。 PWM 控制器对有效载荷影响的准确程度与 PWM 计数器的长度直接相关。当我们在本文后面实现一个 PWM 控制器时,我们将在 VHDL 代码中看到它是如何工作的。

将占空比的二进制表示转换为百分比的公式如下所示。

\mathit{duty\_cycle\_percentage} =\frac{\mathit{commanded\_duty\_cycle} * 100}{2^\mathit{pwm\_bits} - 1}

PWM 频率

当谈到 PWM 开关频率时,我们指的是 PWM 输出在 ON 和 OFF 状态之间交替的频率,以及 PWM 计数器回绕所需的时间。与往常一样,频率是整个 PWM 周期的倒数:

\mathit{pwm\_freq} =\frac{1}{\mathit{pwm\_period}}

理想的 PWM 频率取决于您控制的设备类型。如果消费者是 LED,那么任何大于几百赫兹的数字在肉眼看来都是稳定的光源。对于无刷直流电机,最佳点在几十千赫兹范围内。将频率设置得太低,您可能会体验到物理振动。振荡太快,就是在浪费能量。

需要记住的一个问题是,模拟电力电子器件不如数字 FPGA 引脚快。典型的 PWM 设置使用功率 MOSFET 作为开关来控制流过模拟设备的电流。

考虑图像中显示的示意图。它是我的高级点阵 VHDL 课程中使用的 LED 驱动电路的一部分。 FPGA 引脚控制 MOSFET 的栅极,充当串联 LED 的断路器。开关频率越高,晶体管将花费更多时间不完全打开或完全关闭。这会转化为 MOSFET 中的功率浪费和过热。

PWM 发生器模块

让我们在 VHDL 中创建一个标准的、通用的 PWM 控制器实现。我所说的标准是什么意思 如果您要求他们用 VHDL 编写 PWM 控制器,这与大多数有经验的 VHDL 设计人员所创建的内容很接近。这是通用 从某种意义上说,PWM 频率可定制以适应大多数应用。

要在真正的 FPGA 上测试我们的 PWM 发生器,除了 PWM 控制器之外,我们还需要一些模块。稍后我将在使用 PWM 模块控制 Lattice iCEstick FPGA 开发板上的 LED 照明时介绍这些内容。但首先,让我们谈谈 PWM 发生器模块。

PWM 模块实体

为了使模块可定制,我添加了一个通用端口,可让您在实例化时指定两个常量。

第一个,名为 pwm_bits , 确定内部 PWM 计数器的长度。此常量设置位长度,而不是最大计数器值。您将无法将 PWM 频率指定为特定数量的时钟周期。但通常,我们不需要以 100% 的精度设置 PWM 频率。理想的 PWM 频率是一个工作良好的范围,而不是一个确切的数字。

另一个通用常量名为 clk_cnt_len .它指定有效降低 PWM 频率的第二个计数器的长度。它充当时钟分频器,但实际上并未创建派生时钟信号。请注意,为其分配了默认值 1。将此常数设置为 1 会禁用时钟分频器,同时也摆脱了处理它的额外逻辑。

我将对此进行解释,并在本文后面介绍计算精确 PWM 频率的公式。

entity pwm is
  generic (
    pwm_bits : integer;
    clk_cnt_len : positive := 1
  );
  port (
    clk : in std_logic;
    rst : in std_logic;
    duty_cycle : in unsigned(pwm_bits - 1 downto 0);
    pwm_out : out std_logic
  );
end pwm;

因为这是一个全同步模块,所以前两个信号是时钟和复位。

端口声明列表中的第三个输入是占空比。从上面的VHDL代码可以看出,duty_cycle的长度 信号跟随 pwm_bits 通用常量。这意味着 pwm_bits 常数控制着您调节模拟设备电源的精确度。

实体上的最终信号是 pwm_out .那是 PWM 调制控制信号,您将其路由到 FPGA 引脚并连接到 MOSFET 的栅极。

PWM 模块内部信号

PWM 模块仅包含两个内部信号。第一个是 PWM 计数器,与 duty_cycle 相同 输入。就像后者一样,pwm_bits 常数也决定了这个信号的长度。

signal pwm_cnt : unsigned(pwm_bits - 1 downto 0);
signal clk_cnt : integer range 0 to clk_cnt_len - 1;

第二个内部信号被命名为 clk_cnt ,顾名思义,它用于计算时钟周期。它是整数类型,如果你设置 clk_cnt_len 为 1,计数范围将评估为 (0 到 0) ——只是数字 0。

PWM时钟周期计数器处理

实现时钟计数器的过程很简单。如果模块未复位,逻辑将连续计数时钟周期,在 clk_cnt 的最大值处回零 整数可以容纳。

CLK_CNT_PROC : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      clk_cnt <= 0;
      
    else
      if clk_cnt < clk_cnt_len - 1 then
        clk_cnt <= clk_cnt + 1;
      else
        clk_cnt <= 0;
      end if;
      
    end if;
  end if;
end process;

请注意,如果您对 clk_cnt_len 使用默认值 1 一般而言,这个过程应该在合成过程中消失。内部 if 语句将始终为 false,因为 0 < 1 - 1 是假的。 clk_cnt 的值 则始终为 0。大多数综合工具会识别这一点并优化整个过程。

PWM输出过程

设置 PWM 输出信号的过程也控制着 PWM 计数器。当时钟周期计数器为 0 时,它会增加 PWM 计数器。这就是 PWM 频率限制机制的工作原理。

最初,我打算只写 if clk_cnt = 0 then 在第9行,但我发现当我使用默认的clk_cnt_len时,综合工具并没有删除所有与时钟计数器相关的逻辑 值为 1。但是,包括 clk_cnt_len 在 if 语句中成功了。它不应该对合成产生不利影响,因为 clk_cnt_len 是一个常数。综合工具可以在编译时计算出它的值,然后判断过程的内容是否冗余。

PWM_PROC : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      pwm_cnt <= (others => '0');
      pwm_out <= '0';

    else
      if clk_cnt_len = 1 or clk_cnt = 0 then

        pwm_cnt <= pwm_cnt + 1;
        pwm_out <= '0';

        if pwm_cnt = unsigned(to_signed(-2, pwm_cnt'length)) then
          pwm_cnt <= (others => '0');
        end if;

        if pwm_cnt < duty_cycle then
          pwm_out <= '1';
        end if;

      end if;
    end if;
  end if;
end process;

clk_cnt_len 大于 1,pwm_cnt 信号的行为就像一个自由运行的计数器,当 clk_cnt 时递增 为 0。它是一个无符号类型,当它溢出时会自动返回到 0。但是我们必须确保它在回零之前跳过了最大值。

在上面代码的第 14 行,我正在检查计数器是否处于其第二高值。如果是,我们此时将其设置为 0。无论 pwm_cnt 多长时间,我都在使用一种有效的技巧 信号是。通过使用 to_signed 函数,我正在创建一个与 pwm_cnt 长度相同的有符号常量 , 但值为 -2。

VHDL 和一般计算机中的带符号数 -2 始终是一系列 1,最右边的位置是 0。那是因为符号扩展是如何工作的。在我之前的教程中阅读更多相关信息:

如何在VHDL中使用有符号和无符号

最后,通过将有符号类型转换为无符号,我们得到 pwm_cnt 的第二高值 可以坚持。

在第 18 行,我们正在检查自由运行的 PWM 计数器是否大于占空比输入。如果是这样,我们将 PWM 输出设置为“1”,因为我们处于占空比的 ON 周期。

这就是为什么我们必须将 PWM 计数器的第二高值回绕回 0。如果 PWM 计数器可以达到占空比可能具有的最高值,则无法将占空比设置为 100%。 pwm_cnt < duty_cyclepwm_cnt 时,行将始终为假 处于最大值。

这是有道理的,因为除了中间占空比步骤之外,我们还必须表示完全关闭和开启状态。想象一下 pwm_bits 是设置为 2,并运行整个计数序列作为一种心理练习,看看我的意思!

\mathit{pwm\_hz} =\frac{\mathit{clk\_hz}}{(2^\mathit{pwm\_bits} - 1) * \mathit{clk\_cnt\_len}}

考虑到这些事实,我们可以推导出上面显示的公式来计算精确的 PWM 频率。而 clk_hz 是FPGA系统时钟的频率,另外两个变量是通用输入常数。

顶部模块

为了在真实硬件上测试 PWM 模块,我创建了一个实现来调节 Lattice iCEstick 上的开机 LED 的照明。我在 VHDL Fast-Track 初学者课程和高级点阵 FPGA 课程中都使用了这款经济实惠的 FPGA 开发板。

上图显示了 USB 可插拔 iCEstick 的正面,其电源 LED 由箭头指示。 iCEstick 上有五个 LED 以星形排列。开机 LED 为绿色,而其他 LED 发出红色光。在 iCEstick 上,每个 LED 都有一个专用的 FPGA 引脚。请参阅 iCEstick 用户手册以查看控制它们的确切引脚号。

下图显示了顶部模块中的子模块是如何连接的。 PWM模块我们已经讲过了,后面我会简单介绍一下计数器和复位模块。

顶级模块实体

我没有在顶层模块中对常量进行硬编码,而是将它们声明为顶层实体上的泛型。然后,我分配适合 iCEstick 的默认值。这种方法的一个优点是您可以在测试台中覆盖这些值以加快仿真速度。当我综合设计时,我没有为泛型分配任何东西。因此,正确的默认值最终会出现在布线设计中。

我们将传递 pwm_bitsclk_cnt_len 到具有相同名称的 PWM 模块实体上的泛型。 iCEstick 振荡器的时钟频率为 12 Mhz。通过使用前面给出的公式,我们可以代入这些值来计算 PWM 频率: \frac{12e6}{(2^8 - 1) * 47} \approx 1 \mathit{kHz}

entity pwm_led is
  generic (
    pwm_bits : integer := 8;
    cnt_bits : integer := 25;
    clk_cnt_len : positive := 47
  );
  port (
    clk : in std_logic;
    rst_n : in std_logic; -- Pullup

    led_1 : out std_logic;
    led_2 : out std_logic;
    led_3 : out std_logic;
    led_4 : out std_logic;
    led_5 : out std_logic
  );
end pwm_led;

您可能已经注意到还有第三个常量,cnt_bits ,在上面代码的泛型声明中。它控制自缠绕锯齿计数器的长度。我们将使用这个额外的计数器来创建开机 LED 的渐变照明,以便我们可以实时观察 PWM 模块的工作情况。

我们将把这个新计数器的高位连接到 PWM 模块的占空比输入。因为这个计数器会计算时钟周期,cnt_bits 通用确定通电 LED 的脉冲频率。该公式是时钟频率和计数器长度的函数,如下所示。

\frac{2^{\mathit{cnt\_bits}}}{\mathit{clk\_hz}} =\frac{2^{25}}{12e6} \约 2.8 \mathit{Hz}

在端口声明中,我用 _n 对重置输入进行了后缀 ,表示外部复位为负极性。我们将配置 Lattice FPGA 以在该引脚上使用内部上拉电阻。

最后,您可以看到我在端口声明中列出了 iCEstick 上的所有 LED。我们只会使用 5 号 LED,但我们必须主动驱动其他 LED。如果它们未连接,它们会发出微弱的红色光。

如果您想仔细查看 VHDL 代码和约束文件,请在下面的表格中输入您的电子邮件。您将收到一个 Zip 文件,其中包含 ModelSim 和 Lattice iCEcube2 项目的完整代码。

顶部模块内部信号

我喜欢让我的顶级模块没有 RTL 逻辑。这是一个称为结构模块的概念 .根据我的经验,当您分离 RTL 逻辑和互连时,更容易维护结构化的 VHDL 项目。下面的代码显示了顶层模块中的信号声明和并发信号分配。

architecture str of pwm_led is

  signal rst : std_logic;
  signal cnt : unsigned(cnt_bits - 1 downto 0);
  signal pwm_out : std_logic;

  alias duty_cycle is cnt(cnt'high downto cnt'length - pwm_bits);

begin

  led_1 <= '0';
  led_2 <= '0';
  led_3 <= '0';
  led_4 <= '0';

  led_5 <= pwm_out;

首先,我们声明一个复位信号,它将是外部复位的非反相同步版本。

第二个声明的信号,名为 cnt , 是无限循环的时钟周期计数器。这是一个无符号类型,将在任何给定时间保持我们的 LED 强度锯齿波的状态。

接下来是 pwm_out 信号。我们可以连接 pwm_out 来自 PWM 模块的信号直接发送到 led_5 输出,但我想观察 pwm_out 在模拟器中。综合工具会发现这两个信号属于同一个网络。它不会花费额外的资源。

最后是 duty_cycle 的声明 矢量——这一次,我使用了别名 关键字而不是创建新信号。 VHDL 别名的工作方式类似于 C 中的宏。当我们使用 duty_cycle name 从现在开始,编译器将用它替换 cnt 的高位 向量。

开始之后 关键字,我们分配 pwm_outled_5 发出信号 输出。所有其他 LED 都硬连线到“0”,以防止它们发出红色光。

实例化

在 FPGA 内部使用外部信号之前,我们必须始终将它们与内部系统时钟同步。否则,我们可能会遇到亚稳态问题,难以调试的问题。

RESET : entity work.reset(rtl)
  port map (
    clk => clk,
    rst_n => rst_n,
    rst => rst
  );

外部复位也不例外,但因为我不允许在顶层结构模块中使用任何 RTL 逻辑,所以我们将复位同步器作为一个独立的模块来实现。

下一个实例化是 PWM 模块,如下面的代码片段所示。在 PWM 模块实例化中,我们使用 duty_cycle 用于分配 cnt 的最高有效位的别名 duty_cycle 的向量 输入。这将使 LED 的亮度增强,直到计数器达到最大值。当cnt 回零,LED 短暂关闭,循环重复。

PWM : entity work.pwm(rtl)
  generic map (
    pwm_bits => pwm_bits,
    clk_cnt_len => clk_cnt_len
  )
  port map (
    clk => clk,
    rst => rst,
    duty_cycle => duty_cycle,
    pwm_out => pwm_out
  );

顶部模块中的第三个也是最后一个实例化是时钟周期计数器,如下所示。为了使这个模块更通用,我添加了一个 count_enable 信号。但在本设计中,我们将其设置为常数“1”,因为我们要对每个时钟周期进行计数。

COUNTER : entity work.counter(rtl)
  generic map (
    counter_bits => cnt'length
  )
  port map (
    clk => clk,
    rst => rst,
    count_enable => '1',
    counter => cnt
  );

如果您需要此项目的完整 VHDL 代码,请在下表中留下您的电子邮件地址。

模拟 PWM LED 脉冲

通过泛型自定义计数器长度的一个显着优势是它可以让您加快模拟速度。大多数时候,我们对测试逻辑中的转换和事件感兴趣。我们并不热衷于穿过超长柜台,而设计中没有其他任何事情发生。

使用泛型,我们可以从测试台以非侵入性的方式更改这些内容。下面的代码显示了我在测试台中实例化 PWM 模块时分配给通用映射的值。

DUT : entity work.pwm_led(str)
  generic map (
    pwm_bits => 8,
    cnt_bits => 16,
    clk_cnt_len => 1
  )

当我们在 ModelSim 中使用这些常数进行仿真时,以 100 MHz 运行 1400 微秒就足以显示两个完整的 PWM 周期。如果我们使用真实值,我们将不得不模拟接近 6 秒。那是 3200 万个时钟周期,几乎只是计数而已。在 ModelSim 中这将花费很长时间。

下图显示了 ModelSim 中 PWM 仿真的波形。我更改了 duty_cycle 的格式 信号从默认数字类型转换为模拟波表示。您可以在 ModelSim 中执行此操作,方法是右键单击波形中的信号并选择 Format->Analog (custom)... ,并设置像素高度和数据范围以匹配您信号的最小值和最大值。

在波形中,我们可以看到为什么它被称为锯齿信号。自由运转的包裹计数器类似于锯片上的齿。

注意 PWM 输出的高电平周期的持续时间(led_5 ) 随着占空比的增加而增加。我们还可以看到 led_5 是一个连续的“1”,在锯齿的尖端非常短暂。即占空比为最大值 255。

如果我们没有在 PWM 模块中添加额外的 if 语句,那么包装 pwm_cnt 的语句 信号在其第二高值处回到零,我们不会看到这一点。我们永远无法达到最大功率输出。这是实现 PWM 发生器时的常见错误。我也做过一两次。

FPGA 实现

我使用莱迪思的设计软件 iCEcube2 在莱迪思 iCEstick 上实现了设计。下面的清单显示了布局和布线后报告的资源使用情况。尽管 iCE40 FPGA 很小,但 PWM 和支持模块只使用了可用 LUT 的 5%。

Resource Usage Report for pwm_led 

Mapping to part: ice40hx1ktq144
Cell usage:
GND             3 uses
SB_CARRY        31 uses
SB_DFF          5 uses
SB_DFFSR        39 uses
SB_GB           1 use
VCC             3 uses
SB_LUT4         64 uses

I/O ports: 7
I/O primitives: 7
SB_GB_IO       1 use
SB_IO          6 uses

I/O Register bits:                  0
Register bits not including I/Os:   44 (3%)
Total load per clock:
   pwm_led|clk: 1

@S |Mapping Summary:
Total  LUTs: 64 (5%)

在 iCEcube2 中生成编程比特流后,我使用 Lattice Diamond 独立编程器通过 USB 配置 FPGA。

下面的 Gif 动画显示了锯齿波占空比信号如何使 iCEstick 上的开机 LED 工作。它以增加的强度照亮,直到 cnt 柜台包装。然后,占空比全为零,LED 短暂关闭。之后,循环无限重复。

iCEstick 是一款价格低廉且用途广泛的 FPGA 开发板。它适合初学者,但也适用于高级嵌入式项目。此外,Lattice 软件简单易用。这就是我在初学者的 VHDL 课程和高级 FPGA 课程中都使用 iCEstick 的原因。

如果您已经拥有 iCEstick,您可以使用下面的表格下载 iCEcube2 项目。

LED 呼吸效果的正弦波占空比

现在您知道如何使用 PWM 控制 LED 的照明了。

以锯齿波模式脉动的 LED 可以说比简单的开/关闪烁应用更酷。这是 VHDL 学生典型的第一个任务,我敢肯定你在某个时间点闪烁过 LED。

但是,如果您使用正弦波来控制占空比,LED 闪烁会变得更加令人印象深刻。下面的 Gif 动画展示了我们的 PWM 模块以正弦强度随时间变化的方式对 LED 进行脉冲。

我敢肯定,您之前已经在 LED 上看到过这种“呼吸”效应。这就是我手机上的通知 LED 的行为方式,我认为它看起来很自然,因为光强度没有突然变化。

在我的下一篇博文中,我将向您展示如何使用 FPGA 中的块 RAM 创建正弦波发生器。我们将修改 pwm_led 模块以正弦波强度脉动 iCEstick 上的 LED。

点击此处进入下一篇博文:
如何使用存储在块 RAM 中的正弦波创建呼吸 LED 效果

另请参阅:
使用来自 FPGA 引脚的 PWM 的 RC 伺服控制器


VHDL

  1. PWM 功率控制器
  2. 如何在 VHDL 中创建字符串列表
  3. 如何为 VHDL 代码锁定模块创建 Tcl 驱动的测试平台
  4. 如何在 VHDL 测试平台中停止仿真
  5. 如何在 VHDL 中生成随机数
  6. 如何在 VHDL 中创建环形缓冲区 FIFO
  7. 如何创建自检测试平台
  8. 如何在 VHDL 中创建链接列表
  9. 如何在 VHDL 中的进程中使用过程
  10. 如何在 VHDL 中使用不纯函数
  11. 如何在 VHDL 中使用函数
  12. 如何给电容器充电?