23、电机控制:PWM生成、H桥驱动原理、直流电机PID调速、步进电机驱动

电机控制,是FPGA实战中非常有意思的一个方向。你想想看,用逻辑电路去驱动一个物理世界里的旋转部件,那种感觉挺奇妙的。我个人习惯把电机控制分成两大类:直流电机和步进电机。它们的驱动逻辑完全不同,但底层都离不开PWM(脉宽调制)这个核心。

23.1 PWM生成:FPGA最拿手的活儿

PWM说白了,就是让一个信号在高电平和低电平之间快速切换,通过调整高电平占整个周期的比例(占空比)来控制平均电压。FPGA做这个,简直是杀鸡用牛刀——太合适了。

我一般用计数器加比较器的方式生成PWM。核心思路很简单:

  • 一个自由运行的计数器,从0累加到最大值(决定PWM频率)
  • 一个比较阈值寄存器(决定占空比)
  • 当计数值小于阈值时输出高电平,否则输出低电平

嗯,这里要注意:计数器的位宽决定了占空比的分辨率。比如你用10位计数器,占空比就可以精细到1/1024。我在项目中遇到过,有些电机对PWM频率很敏感,频率太低会听到明显的啸叫声,频率太高又可能增加开关损耗。一般直流电机用10kHz-20kHz比较合适。

关键参数:

  • PWM频率 = 系统时钟 / (计数器最大值 + 1)
  • 占空比分辨率 = 1 / (计数器最大值 + 1)
  • 实际占空比 = 比较阈值 / (计数器最大值 + 1)
// 紫光同创PGL22G平台,PWM生成模块
module pwm_gen (
    input  wire        clk,      // 系统时钟 50MHz
    input  wire        rst_n,    // 复位
    input  wire [9:0]  duty,     // 占空比 0-1023
    output reg         pwm_out   // PWM输出
);

    reg [9:0] cnt;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 10'd0;
        else
            cnt <= cnt + 1'b1;
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            pwm_out <= 1'b0;
        else
            pwm_out <= (cnt < duty) ? 1'b1 : 1'b0;
    end

endmodule

这段代码生成的PWM频率是50MHz/1024 ≈ 48.8kHz。如果你觉得频率太高,可以增加一个分频器,或者把计数器最大值调大。我曾经在一个项目中,为了匹配某个老式电机的驱动特性,硬是把PWM频率降到了1kHz,结果电机嗡嗡响得像蜜蜂窝——后来还是老老实实换回了10kHz。

23.2 H桥驱动原理:让电机正反转

直流电机要正反转,光靠一个PWM信号是不够的。你需要一个H桥电路。H桥由四个开关管(MOSFET或三极管)组成,形状像字母"H",电机接在中间横杠上。

工作原理其实很简单:

  • 正转:左上和右下导通,电流从左到右流过电机
  • 反转:右上和左下导通,电流从右到左流过电机
  • 刹车:同时导通上管或同时导通下管,电机两端短路
  • 滑行:所有管子关断,电机自由旋转

FPGA这边需要输出四个控制信号,分别控制四个开关管。但要注意,绝对不能出现同一侧上下管同时导通的情况——那叫"直通",会瞬间烧毁MOSFET。我刚开始做电机驱动时,就犯过这个错误,一个不小心,板子上冒了一缕青烟...嗯,从那以后我每次写H桥控制逻辑,都会加死区时间。

死区时间:在切换上下管状态时,先关断一个管子,等待一小段时间(比如1微秒),再导通另一个管子。这个时间就是死区时间,用来防止直通。

// H桥控制逻辑示例
module h_bridge_ctrl (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        dir,      // 方向:0正转,1反转
    input  wire        pwm_in,   // PWM输入
    output reg         h1, h2,   // 左上、右上
    output reg         h3, h4    // 左下、右下
);

    reg [3:0] dead_cnt;
    reg       dead_en;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            {h1, h2, h3, h4} <= 4'b0000;
            dead_cnt <= 4'd0;
            dead_en <= 1'b0;
        end else begin
            if (dead_en) begin
                if (dead_cnt < 4'd10) begin  // 死区时间约200ns (50MHz)
                    dead_cnt <= dead_cnt + 1'b1;
                    {h1, h2, h3, h4} <= 4'b0000;  // 全部关断
                end else begin
                    dead_en <= 1'b0;
                    // 根据方向和PWM输出
                    if (dir == 1'b0) begin  // 正转
                        h1 <= pwm_in;
                        h4 <= pwm_in;
                        h2 <= 1'b0;
                        h3 <= 1'b0;
                    end else begin          // 反转
                        h2 <= pwm_in;
                        h3 <= pwm_in;
                        h1 <= 1'b0;
                        h4 <= 1'b0;
                    end
                end
            end else begin
                // 检测到方向或PWM变化时,启动死区
                // ... (实际项目中需要更复杂的逻辑)
            end
        end
    end

endmodule

23.3 直流电机PID调速:让转速稳如老狗

开环控制直流电机,转速会随负载变化而波动。要精确控制转速,就得用PID(比例-积分-微分)闭环控制。说白了,就是不断测量实际转速,跟目标转速比较,然后调整PWM占空比。

PID公式长这样:

输出 = Kp * 误差 + Ki * 误差积分 + Kd * 误差微分

在FPGA里实现PID,我建议用定点数而不是浮点数。浮点运算太耗资源,而且速度慢。定点数只要选好小数点的位置,精度完全够用。

我的经验:PID参数整定是个玄学。我一般先调Kp,让系统不振荡;再加一点Ki消除稳态误差;最后加Kd改善响应速度。如果电机嗡嗡响,通常是Kp太大了。如果响应慢得像蜗牛,那就是Ki太小。

// 定点数PID控制器(16位,Q8.8格式)
module pid_ctrl (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [15:0] target,    // 目标转速
    input  wire [15:0] actual,    // 实际转速(来自编码器)
    output reg  [15:0] pwm_duty   // PWM占空比输出
);

    reg [15:0] error;
    reg [31:0] integral;
    reg [15:0] derivative;
    reg [15:0] last_error;

    // PID参数(需根据实际系统调整)
    parameter KP = 16'd128;   // 比例系数 0.5
    parameter KI = 16'd26;    // 积分系数 0.1
    parameter KD = 16'd51;    // 微分系数 0.2

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            error     <= 16'd0;
            integral  <= 32'd0;
            derivative<= 16'd0;
            last_error<= 16'd0;
            pwm_duty  <= 16'd0;
        end else begin
            error     <= target - actual;
            integral  <= integral + error;
            derivative<= error - last_error;
            last_error<= error;

            // 计算PID输出(注意防止溢出)
            pwm_duty <= (KP * error) + (KI * integral[23:8]) + (KD * derivative);
        end
    end

endmodule

这里有个坑:积分项如果不加限幅,会产生"积分饱和"现象。比如电机被卡住了,积分项会一直累加,等电机恢复后,输出会过大导致超调。我一般会给积分项加一个上下限,比如±10000。

23.4 步进电机驱动:精准定位的利器

步进电机跟直流电机不一样,它是靠脉冲信号一步步转动的。每给一个脉冲,电机就转一个固定的角度(步距角)。所以步进电机天生适合开环位置控制。

驱动步进电机,FPGA需要做两件事:

  • 产生脉冲序列:脉冲频率决定转速,脉冲数量决定转角
  • 控制方向信号:高电平正转,低电平反转

但实际项目中,我很少用整步驱动。因为整步驱动在低频时会有明显的振动和噪声。更好的方式是"细分驱动"——把每一步再细分成若干微步,让电流平滑变化。

细分驱动原理:

步进电机的两相绕组(A相和B相)需要通入正弦波电流。整步驱动时,电流是方波;细分驱动时,电流是阶梯状的正弦波。比如16细分,就是把一个整步分成16个微步,每个微步的电流值按正弦表查出来。

// 步进电机细分驱动(16细分,正弦表)
module stepper_driver (
    input  wire        clk,        // 系统时钟
    input  wire        rst_n,
    input  wire        step_pulse, // 步进脉冲
    input  wire        dir,        // 方向
    output reg  [7:0]  a_curr,     // A相电流
    output reg  [7:0]  b_curr      // B相电流
);

    // 正弦表(0-90度,16个点,8位精度)
    reg [7:0] sin_table [0:15];
    initial begin
        sin_table[0]  = 8'd0;
        sin_table[1]  = 8'd25;
        sin_table[2]  = 8'd50;
        sin_table[3]  = 8'd74;
        sin_table[4]  = 8'd98;
        sin_table[5]  = 8'd121;
        sin_table[6]  = 8'd142;
        sin_table[7]  = 8'd162;
        sin_table[8]  = 8'd180;
        sin_table[9]  = 8'd196;
        sin_table[10] = 8'd209;
        sin_table[11] = 8'd220;
        sin_table[12] = 8'd228;
        sin_table[13] = 8'd234;
        sin_table[14] = 8'd237;
        sin_table[15] = 8'd238;
    end

    reg [3:0] micro_step;  // 微步计数器 0-15

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            micro_step <= 4'd0;
        else if (step_pulse) begin
            if (dir)  // 正转
                micro_step <= micro_step + 1'b1;
            else      // 反转
                micro_step <= micro_step - 1'b1;
        end
    end

    // 根据微步位置查表输出电流
    always @(*) begin
        // A相:sin(θ)
        a_curr = sin_table[micro_step];
        // B相:cos(θ) = sin(θ + 90°),即偏移4个微步
        b_curr = sin_table[micro_step + 4'd4];
    end

endmodule

这段代码只是个简化版。实际项目中,你还需要考虑电流的过零点处理、PWM调制生成模拟电流、以及电机启动时的加减速控制(S曲线加速)。我记得有一次调试一个3D打印机的步进电机,没加加减速控制,电机一启动就丢步,打印出来的模型歪歪扭扭的。后来加了梯形加减速,问题就解决了。

避坑指南:我曾经在驱动大功率步进电机时,发现电机在某个速度点剧烈振动。查了半天,原来是PWM频率和电机的机械谐振频率重合了。解决办法很简单——换个PWM频率,或者调整细分倍数。

好了,电机控制这块内容不少,但核心就这几个点:PWM生成是基础,H桥是直流电机的"方向盘",PID是让转速听话的"教练",步进电机细分驱动是精准定位的"尺子"。你在实际项目中遇到问题,可以回头看看这些原理,多半能找到答案。