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是让转速听话的"教练",步进电机细分驱动是精准定位的"尺子"。你在实际项目中遇到问题,可以回头看看这些原理,多半能找到答案。