VHDL 实现 PWM 控制器:设计、仿真与 FPGA 演示
脉宽调制 (PWM) 将数字 FPGA 引脚变为强大的模拟控制器。通过高速切换电源开关,PWM 在从未产生真实模拟电压的情况下,向负载提供所需精确平均电流。
典型 PWM 应用包括扬声器音频输出、LED 调光、控制感应电机转矩,如伺服电机、电脑风扇、水泵、电动汽车无刷直流电机等。
延伸阅读:FPGA 引脚 PWM 实现 RC 舵机控制器
PWM 工作原理
当电源在高频下在 100% 和 0% 间切换,负载看到的时间平均电流与电源导通时间比例成正比。下图说明原理:

占空比
占空比定义 PWM 输出每个周期保持高电平的时长。以百分比表示,由二进制计数器值计算:

VHDL 实现中将占空比视为无符号整数,获得 PWM 计数器全分辨率。0% 占空比永久关断输出,100% 占空比持续导通。可达分辨率由计数器位宽决定。
将二进制占空比值转百分比,用:
\mathit{duty\_cycle\_percentage} = \frac{\mathit{commanded\_duty\_cycle} * 100}{2^\mathit{pwm\_bits} - 1}
PWM 频率
PWM 频率为周期倒数:
\mathit{pwm\_freq} = \frac{1}{\mathit{pwm\_period}}
选择合适频率取决于目标设备。LED 几百赫兹足避免可见闪烁。无刷直流电机通常工作在数十千赫兹;频率过低产生机械振动,过高浪费功率。

因 MOSFET 开关比 FPGA 逻辑慢,PWM 过快会导致晶体管在部分导通态停留,产生额外热量。
PWM 发生器模块
下面是通用 VHDL 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;
clk 和 rst 提供同步控制。duty_cycle 输入宽度由 pwm_bits 定义,计数器分辨率直接可配。pwm_out 信号路由到驱动 MOSFET 栅极的 FPGA 引脚。
内部信号
signal pwm_cnt : unsigned(pwm_bits - 1 downto 0); signal clk_cnt : integer range 0 to clk_cnt_len - 1;
pwm_cnt 计数器镜像占空比宽度。clk_cnt 计数器实现可选时钟分频;clk_cnt_len 设为 1 禁用分频并移除相关逻辑。
时钟分频进程
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 时,综合工具自动消除此进程。
PWM 输出进程
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 为零时递增,有效节流 PWM 频率。在次高值复位 pwm_cnt 的条件保证 100% 占空比可达。
\mathit{pwm\_hz} = \frac{\mathit{clk\_hz}}{(2^\mathit{pwm\_bits} - 1) * \mathit{clk\_cnt\_len}}
顶层设计
为演示 PWM 控制器,设计驱动 Lattice iCEstick FPGA 上的电源指示 LED。板载 12 MHz 振荡器馈送计数器,以锯齿模式驱动 LED 强度。

实体声明
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; 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;
12 MHz 时钟和所选泛型产生约 1 kHz PWM 频率:
\frac{12e6}{(2^8 - 1) * 47} \approx 1 \text{ kHz}
cnt_bits 泛型驱动 25 位锯齿计数器控制 LED 占空比。其周期约 2.8 Hz:
\frac{2^{\mathit{cnt\_bits}}}{\mathit{clk\_hz}} = \frac{2^{25}}{12e6} \approx 2.8 \text{ Hz}
仅用 LED 5;其他 LED 拉低避免杂光。
内部信号与连线
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;
rst 为外部复位同步后的非反转版本。别名 duty_cycle 将 cnt 最高位映射到 PWM 控制器。
模块实例化
RESET : entity work.reset(rtl) port map ( clk => clk, rst_n => rst_n, rst => rst ); 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 ); COUNTER : entity work.counter(rtl) generic map ( counter_bits => cnt'length ) port map ( clk => clk, rst => rst, count_enable => '1', counter => cnt );
仿真
泛型让仿真加速变易。减小 cnt_bits 和 clk_cnt_len 可在几微秒内观察完整 PWM 周期,而非数秒。
测试平台实例化:
DUT : entity work.pwm_led(str) generic map ( pwm_bits => 8, cnt_bits => 16, clk_cnt_len => 1 );
ModelSim 波形显示锯齿计数器驱动 LED 亮度。占空比达最大值时每周期尖端出现短暂「导通」脉冲。

FPGA 实现
iCEcube2 综合后,设计仅消耗 ice40hx1ktq144 器件 64 个 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%)
用 Diamond 独立编程器通过 USB 编程 FPGA 很顺利。LED 动画显示平滑亮度渐变后短暂变暗,循环重复。

正弦波占空比的呼吸 LED
用正弦波查找表替代锯齿计数器,产生自然「呼吸」效果,常见于通知指示灯。

后续文章将展示如何用块 RAM 构建正弦波发生器并集成到 pwm_led 设计中。
阅读下一篇:如何用块 RAM 存储正弦波创建呼吸 LED 效果
延伸阅读:FPGA 引脚 PWM 实现 RC 舵机控制器
VHDL