3. PWM呼吸灯

任务目标

在Zynq平台上,通过ARM处理器配置FPGA逻辑产生的PWM信号占空比,驱动LED实现呼吸灯效果。本案例将构建一个AXI-Lite接口的PWM外设,由ARM侧软件动态调节占空比,使LED亮度周期性变化。

硬件设计(FPGA端)

创建Vivado工程,添加Zynq PS核,并通过AXI GPIO IP核输出PWM信号。使用AXI GPIO的通道1作为PWM输出(连接到LED),通道2作为占空比控制寄存器输入。PWM频率设定为1kHz,通过计数器比较产生占空比可调的方波信号。

在Vivado中完成以下连接:

  • Zynq PS的M_AXI_GP0接口连接到AXI GPIO IP核
  • AXI GPIO通道1(GPIO[0])连接到PL端LED引脚
  • AXI GPIO通道2(GPIO[7:0])作为占空比控制输入
  • 设置AXI GPIO为双通道模式,通道1输出,通道2输入

完整代码实现

以下为ARM侧C语言代码,通过Xilinx SDK实现PWM呼吸灯控制:

#include "xgpio.h"
#include "xparameters.h"
#include "sleep.h"

#define PWM_GPIO_DEVICE_ID  XPAR_AXI_GPIO_0_DEVICE_ID
#define PWM_OUTPUT_CHANNEL  1
#define DUTY_CYCLE_CHANNEL  2
#define PWM_PERIOD_MS       1   // 1kHz PWM频率
#define BREATH_CYCLE_MS     2000 // 呼吸周期2秒

XGpio Gpio;

// 将占空比(0-255)映射到PWM输出
void SetDutyCycle(u8 duty) {
    XGpio_DiscreteWrite(&Gpio, DUTY_CYCLE_CHANNEL, duty);
}

int main() {
    u8 duty = 0;
    int direction = 1;  // 1=增加, 0=减少
    int step = 1;       // 占空比步进值

    // 初始化GPIO
    if (XGpio_Initialize(&Gpio, PWM_GPIO_DEVICE_ID) != XST_SUCCESS) {
        return XST_FAILURE;
    }

    // 设置通道方向:通道1输出,通道2输入
    XGpio_SetDataDirection(&Gpio, PWM_OUTPUT_CHANNEL, 0x0);
    XGpio_SetDataDirection(&Gpio, DUTY_CYCLE_CHANNEL, 0xFF);

    // 呼吸灯主循环
    while (1) {
        // 更新占空比
        SetDutyCycle(duty);

        // 根据方向调整占空比
        if (direction) {
            duty += step;
            if (duty >= 250) {
                duty = 250;
                direction = 0;
            }
        } else {
            duty -= step;
            if (duty <= 5) {
                duty = 5;
                direction = 1;
            }
        }

        // 延时控制呼吸速度
        usleep(BREATH_CYCLE_MS * 1000 / 250);
    }

    return 0;
}

FPGA端Verilog实现

若需自定义PWM生成逻辑而非使用AXI GPIO,可参考以下核心模块:

module pwm_controller (
    input wire clk,          // 100MHz系统时钟
    input wire rst_n,
    input wire [7:0] duty,   // 占空比控制(0-255)
    output reg pwm_out
);

reg [15:0] counter;
reg [7:0] duty_reg;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 16'd0;
        duty_reg <= 8'd0;
        pwm_out <= 1'b0;
    end else begin
        duty_reg <= duty;
        if (counter == 16'd99999) begin  // 1kHz @100MHz
            counter <= 16'd0;
        end else begin
            counter <= counter + 1'b1;
        end
        pwm_out <= (counter < duty_reg * 392) ? 1'b1 : 1'b0;
    end
end

endmodule

运行与验证

硬件平台:Zynq-7000系列开发板(如ZedBoard、PYNQ-Z2等)

运行步骤:

  1. 在Vivado中创建Block Design,添加Zynq PS和AXI GPIO IP
  2. 完成引脚约束(LED连接到AXI GPIO通道1的GPIO[0])
  3. 生成比特流并导出硬件,启动Xilinx SDK
  4. 创建C工程,将上述C代码复制到main.c中
  5. 编译并下载到开发板运行

验证方法:

  • 观察LED亮度是否呈现从暗到亮再到暗的周期性变化
  • 完整呼吸周期约2秒(从最暗到最亮再回到最暗)
  • 修改BREATH_CYCLE_MS宏定义可调整呼吸速度
  • 修改step变量可改变亮度变化平滑度

预期结果:LED亮度以约2秒为周期平滑变化,呈现呼吸效果。若使用示波器测量PWM输出引脚,可观察到频率为1kHz、占空比在2%~98%之间周期性变化的方波信号。

常见问题

  • LED不亮:检查AXI GPIO通道1方向设置是否正确,确认引脚约束是否匹配
  • 亮度跳变:减小step值(如改为1),增加延时时间
  • PWM频率异常:确认AXI GPIO输出频率与预期一致,可通过修改计数器上限调整