ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA实现的PWM电机控制:从Matlab仿真到Verilog编码

FPGA实现的PWM电机控制:从Matlab仿真到Verilog编码 简介面向FPGA开发与电机控制方向学习者这份源码完整呈现了MATLAB与Quartus II 18平台下PWM电机控制系统的实现全过程覆盖Simulink建模、HDL代码生成、Quartus综合布线到FPGA下载验证适合具备MATLAB/Simulink和VHDL或Verilog基础、希望掌握软硬件协同设计流程的读者。压缩包共308个文件约9.25MB除80个tdf和13个v等HDL源文件外还包含Quartus工程配置qpf/qsf/qip、综合与布局布线报告、可下载的sof/pof烧录文件以及Simulink模型相关辅助文件类型齐全且目录清晰便于对照学习。已有125人学习下载。通过阅读代码与工程结构可以深入理解PWM波形生成、PI/PID控制逻辑、编码器与电流传感器接口等关键设计并复现从MATLAB算法验证到FPGA硬件实现的完整链路。1. 在 FPGA 上做 PWM 电机控制优势不在“会发方波”PWM 控制直流电机这件事很多人第一反应是“不就是翻转 IO 吗”。确实用一片单片机定时器就能输出 20kHz 的方波再给 H 桥驱动芯片一个占空比电机就能转。但一旦把问题拉到“控制系统”的维度事情就变了上下桥臂的死区插入、编码器反馈的同步采样、过流故障关断、占空比突变时的电流冲击这些事件挤在同一条时间线上中断响应稍微抖动电流波形上就有毛刺。FPGA 在 PWM 电机控制系统里的价值不是替代某个定时器而是把“发波—死区—反馈—保护”做成一条确定性链路边沿精度只与时钟周期相关不依赖软件调度。Matlab/Simulink 在这条链路里的角色则是把连续控制律变成可离散化、可验证、可交给硬件实现的算法模型。这套东西适合正在做电机闭环、机器人关节、电源或电力电子项目的工程师也适合刚入门 FPGA 但想找一个完整数据链路的开发人员。2. 先拆需求Matlab 负责控制律FPGA 负责发波Quartus II 18 负责把两者接住2.1 控制律放在哪一侧Matlab 不负责“发波”PWM 电机控制系统可以拆成三个层面采样与反馈、控制律计算、PWM 生成与驱动。很多初学者的误区是把三个层面全部塞进 Simulink 模型然后在 FPGA 里跑一个“大而全”的自动生成代码。实际上常见做法是让 Matlab/Simulink 承担算法验证与控制参数整定让 Verilog RTL 承担定时器、死区、正交解码和故障逻辑。反馈回路的采样频率决定了控制律放在哪一侧。以速度环为例10kHz 的采样周期对应 100us 一次控制计算FPGA 状态机在几十个时钟周期内就能完成电流环如果做到 50kHz 以上单片机的定时器中断开销会变得明显而 FPGA 的 PWM 计数器本身就可以作为采样触发源做到“边沿对齐采样”。Matlab 侧的价值在于先用连续模型确认 PID 系数、滤波器时间常数再把这些参数用定点数固化进 RTL。下面这段代码是我在 Simulink 联调前常做的一步预处理% 在 Matlab 侧先把控制参数核一遍再写 RTL Ts 1e-4; % 10kHz 速度环采样周期 Kp 2.5; Ki 0.8; Tf 1/(60e3); % 电流滤波时间常数用于 Simulink 一阶低通 % 转成定点数避免 Simulink 用 double 仿真RTL 里却用 int scale 2^12; Kp_fixed round(Kp * scale); % 输出给 HDL Coder 或手写 Verilog Ki_fixed round(Ki * scale); fprintf(Kp_fixed %d, Ki_fixed %d\n, Kp_fixed, Ki_fixed);这段代码的逻辑说明先把控制器的系数转成定点格式再用fprintf打印出来作为 RTL 参数。这里的scale是定点小数位数12 位小数对应约 0.0002 的精度对速度环足够。注意Kp_fixed是整数写进 Verilog 的localparam时直接用不要在 RTL 里再除一次否则会引入额外的截断误差。2.2 Quartus II 18 与 Matlab 的版本匹配和工具链习惯Quartus II 18 是一个独立的 FPGA 开发环境和 Matlab 之间没有“插件式”的强绑定。两者通过文件交换Matlab 的 HDL Coder 生成 Verilog/SystemVerilogQuartus 负责综合、布局布线和烧写。版本匹配上主要看两件事生成的 RTL 语法是否被 Quartus 支持以及 IP Catalog 中是否有对应接口。使用较新版本 HDL Coder 时生成的代码可能包含always_ff等 SystemVerilog 语法在 Quartus 工程里要把文件类型识别设为 SystemVerilog否则综合报错。工程管理上我一般用 Tcl 脚本建 Quartus 工程而不是每次打开 GUI 点向导。这样做的好处是工程文件可以进 Git换电脑后一条命令重建工程。下面是一个最小脚本# 在 Quartus II 18 的命令行中执行quartus_sh -t build_project.tcl project_new pwm_motor -overwrite -revision v1 set_global_assignment -name FAMILY Cyclone V set_global_assignment -name DEVICE 5CEFA7F31I7 set_global_assignment -name TOP_LEVEL_ENTITY pwm_motor_top set_global_assignment -name VERILOG_FILE rtl/pwm_gen.v set_global_assignment -name VERILOG_FILE rtl/qep_ctr.v set_global_assignment -name SDC_FILE constraints/pwm_motor.sdc project_close这条脚本里DEVICE要按你电路板上实际的 FPGA 型号填写不要照抄。SDC_FILE是时序约束文件PWM 这类低速信号通常只需要约束时钟频率把主时钟约束为 50MHz 即可。quartus_sh -t是命令行执行 Tcl 脚本的入口这条命令在 Windows 和 Linux 下都可用。2.3 最小工程的上板验证顺序新建 Quartus 工程后不要直接写全部功能先把“最小亮灯”跑通在pwm_motor_top.v里做一个 1Hz 分频器驱动 LED烧录后确认时钟和下载链路正常。然后把分频器换成 PWM 模块输出直接接开发板上的 LED通过改变占空比观察 LED 亮度变化确认 PWM 计数器和比较器工作正常。这一步步递进的原因在于PWM 波形用示波器看需要探头和引脚分配而 LED 不需要额外仪器如果 LED 亮度不变化问题大概率在复位或时钟约束而不是电机驱动部分。引脚分配在 Assignments → Pin Planner 里完成把 PWM 输出引脚分配到 H 桥驱动芯片的输入脚之前先确认驱动芯片的输入逻辑有些驱动芯片需要互补输入有些只需要单端 PWM 加方向引脚。分配完引脚后做一次全编译查看 Fitter 报告里有没有拥塞或约束失败的 warning。3. 在 Quartus II 18 里写 RTLPWM 生成、死区插入与编码器反馈3.1 PWM 核心计数器、比较器与饱和保护一个可综合的 PWM 生成器本质是自由计数器加比较器。下面是最小实现适用于直流电机和大多数 H 桥驱动module pwm_gen #( parameter CLK_HZ 50_000_000, parameter PWM_HZ 20_000, parameter WIDTH 16 )( input wire clk, input wire rst_n, input wire [WIDTH-1:0] duty, // 0 ~ PWM_PERIOD output reg pwm ); localparam PWM_PERIOD CLK_HZ / PWM_HZ; reg [WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 0; else if (cnt PWM_PERIOD - 1) cnt 0; else cnt cnt 1; end always (posedge clk or negedge rst_n) begin if (!rst_n) pwm 1b0; else if (duty PWM_PERIOD) pwm 1b1; // 饱和保护避免“常亮”状态异常 else if (duty 0) pwm 1b0; else pwm (cnt duty) ? 1b1 : 1b0; end endmodule这段代码里PWM_PERIOD是时钟频率除以 PWM 频率50MHz 时钟下 20kHz PWM 对应计数周期 2500WIDTH取 12 就够16 是余量。比较逻辑使用cnt duty产生高电平这样的波形在占空比从 0 到 100% 之间是单调的。饱和保护很重要当duty因为 PID 积分饱和被推到超过PWM_PERIOD时输出常高而不是周期性翻转否则电机得到的有效电压反而下降。如果你用这个模块驱动数字隔离器或光耦要注意pwm默认高有效是否匹配后级电路。3.2 H 桥死区用延时计数器实现互补波形H 桥的死区是 PWM 电机控制里最容易出问题的地方。上下两个功率管如果同时导通电源直接短路几微秒就可能烧 MOS 管。驱动芯片大多自带死区但如果你用的是分立 MOS 管加栅极驱动FPGA 必须自己插入死区时间。死区的基本思路是PWM 信号从高变低时先关断上管延时一段时间后再打开下管从低变高时同理。下面是一个用计数器实现死区的模块module pwm_complement #( parameter DT_CYCLES 25 // 50MHz 时钟下 25 个周期 500ns )( input wire clk, input wire rst_n, input wire pwm_in, output reg pwm_h, output reg pwm_l ); reg [$clog2(DT_CYCLES):0] dly_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_h 1b0; pwm_l 1b0; dly_cnt 0; end else begin if (pwm_in) begin // 关下管等死区再开上管 pwm_l 1b0; if (dly_cnt DT_CYCLES - 1) begin pwm_h 1b1; end else begin dly_cnt dly_cnt 1; end end else begin // 关上管等死区再开下管 pwm_h 1b0; if (dly_cnt DT_CYCLES - 1) begin pwm_l 1b1; end else begin dly_cnt dly_cnt 1; end end end end endmodule注意dly_cnt必须有足够的位宽至少能数到DT_CYCLES。$clog2(25)是 5再补一位防止累计到 25 时溢出所以写成$clog2(DT_CYCLES):0。死区时间的取值参考这张表功率器件类型典型死区时间对应 50MHz 时钟周期数小功率 MOSAO3400 级别200~500 ns10~25中功率 MOSIRF540 级别500~1000 ns25~50IGBT1~3 us50~150死区设太短无法覆盖栅极电容充放电时间死区设太长波形失真和占空比损失变大。常见做法是先按驱动芯片数据手册的下限值设置再用电流探头看母线电流出现尖峰就逐步加长。3.3 编码器正交解码把位置和速度喂回控制环速度闭环需要反馈信号。增量式编码器输出 A、B 两相正交脉冲FPGA 用四倍频计数的方式提高分辨率同时通过两相电平关系判断方向。下面是一个简化版的正交解码模块module qep_ctr #( parameter CNT_WIDTH 16 )( input wire clk, input wire rst_n, input wire a, input wire b, output reg [CNT_WIDTH-1:0] pos_count ); reg a_sync, b_sync; reg a_d, b_d; wire a_rise a_sync ~a_d; wire a_fall ~a_sync a_d; wire b_rise b_sync ~b_d; wire b_fall ~b_sync b_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin a_sync 0; b_sync 0; a_d 0; b_d 0; end else begin a_sync a; b_sync b; a_d a_sync; b_d b_sync; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin pos_count 0; end else begin if (a_rise) pos_count b_sync ? pos_count - 1 : pos_count 1; if (a_fall) pos_count b_sync ? pos_count 1 : pos_count - 1; if (b_rise) pos_count a_sync ? pos_count 1 : pos_count - 1; if (b_fall) pos_count a_sync ? pos_count - 1 : pos_count 1; end end endmodule方向判定的逻辑是A 相上升沿时如果 B 相为高说明是反转如果 B 相为低说明是正转。四倍频的意思是 A、B 的每个上升沿和下降沿都触发一次计数分辨率是编码器线数的 4 倍。a_sync和b_sync做两拍同步是为了防止编码器信号抖动引入亚稳态这对任何接在 FPGA 外部的输入信号都是必要的。4. 从 Simulink 到 FPGAHDL Coder 自动生成、协同仿真与数据回读4.1 控制环到底要不要用 HDL Coder 自动生成代码这是做这个项目时大家问得最多的问题。我的观点是PID 控制器、低通滤波器、状态观测器这类“算法型”模块用 HDL Coder 生成是划算的因为手写定点数、溢出处理很费时间而自动生成代码经过充分验证不容易出现算术错误。但 PWM 计数、死区、编码器解码这种“时序型”模块建议手写 Verilog因为自动生成代码的执行模型和实际时序之间往往有较大差距出问题时排查成本很高。使用 HDL Coder 的标准流程如下。先在 Simulink 里搭好控制器模型把求解器设为固定步长比如速度环设为1e-4然后右键被生成模块选择 HDL Code → Generate RTL。也可以用命令行执行% 打开模型并生成控制器 RTL load_system(pwm_motor_controller); hdlsetup(pwm_motor_controller); makehdl(pwm_motor_controller/pi_controller);hdlsetup会自动把模型求解器配置成适合硬件生成的状态比如固定步长、禁用连续状态。makehdl的第二个参数是被生成模块的路径生成物是pi_controller.v。生成的代码里会有大量位宽扩展逻辑不要手动删改改动后反而不容易和 Simulink 模型保持一致性。要把定点小数位数记下来后续手写 Verilog 模块和它对接时使用相同的fixdt(1, 16, 12)之类的设置。4.2 三种桥接方式选型Matlab 和 FPGA 的联调有几种常见方式按实时性和复杂程度排序如下桥接方式实时性适用场景代价FPGA in the Loop (FIL)高频实时算法快速原型验证需要专用 JTAG 或以太网链路UART 回读 Matlab 解析低频数据1kHz 左右观察控制环内部变量数据吞吐受限VCD 文件离线分析非实时定位特定时序问题只能离线看我做的多数项目用的是 UART 回读。FPGA 把pos_count、duty、fault_state打包成固定帧长发送Matlab 解析后画波形成本低且不需要额外调试器。UART 回读的关键是帧同步建议帧头用0xA5 0x5A两个字节避免 0x00 和 0xFF 出现在数据段时造成误判。4.3 UART 回读与有符号数解析FPGA 侧把 16 位速度值拆成两个字节发送时要注意大小端约定。下面这段 Matlab 代码是接收端解析逻辑s serialport(COM3, 115200, Timeout, 0.5); flush(s); % 读一帧帧头 2 字节 数据 4 字节 frame read(s, 6, uint8); if frame(1) 0xA5 frame(2) 0x5A speed_raw uint16(frame(3)) * 256 uint16(frame(4)); duty_raw uint16(frame(5)) * 256 uint16(frame(6)); % 编码器计数有正负转成有符号数 speed_s16 typecast(uint16(speed_raw), int16); fprintf(speed %d, duty %d\n, speed_s16, duty_raw); else warning(帧同步失败); endtypecast是处理“无符号原始值转有符号数”的关键函数它不改变位模式只改变解释方式。如果你直接写int16(speed_raw)当值超过 32767 时会按饱和处理而不是按补码回绕结果就错了。UART 波特率 115200 时每帧 6 字节约耗时 520us对应反馈频率不到 2kHz用来观察速度环可以观察电流环就太慢了电流环需要 FIL 或直接用 SignalTap 抓内部信号。5. 定 PID 之前先同时定 PWM 频率、死区时间与控制周期5.1 三者联调的先后次序PWM 频率、死区时间、PID 采样周期这三个参数不是独立决定的。先定 PWM 频率再根据功率管选型定死区时间。如果选 20kHz 的 PWM0.5us 死区只损失 1% 的占空比电压但如果把 PWM 提高到 100kHz 去降低电流纹波同样 0.5us 死区就吃掉 5% 的占空比电机低速段会出现明显非线性。所以当 PWM 频率提高时死区时间也要同步收紧而 PID 采样周期必须是 PWM 周期的整数倍让采样时刻落在 PWM 计数的同一个相位上避免采到开关噪声。顺序上我会先用示波器确定死区安全值再把 PID 采样时间设为 PWM 周期的整数倍最后整定 Kp/Ki。5.2 用 SignalTap 抓取闭环内部数据如果 UART 回读的带宽不够就回到 Quartus 的 SignalTap II Logic Analyzer。把cnt、duty、pos_count、pwm_h、pwm_l加入信号列表触发条件设为duty变化沿。抓取深度按采样频率确定50MHz 时钟下抓 1ms 数据需要 50000 个采样点一般开发板的片上 RAM 足够存几万点。信号的pos_count是增量式的无法在 SignalTap 里直接看速度我在 RTL 里会加一个速度计算逻辑固定时间窗口内累计pos_count的变化量取绝对值后作为速度值。这个值是有符号数SignalTap 里默认按无符号显示需要手动把 Radix 设为 2 的补码。5.3 故障保护加进 RTL锁存与恢复闭环调通后最后一步是加过流或过温故障保护。故障信号从驱动芯片的FAULT引脚进来异步信号先同步再用锁存器保持住故障状态直到控制逻辑明确清除。否则故障信号只持续几十微秒主控还没反应过来就消失了。下面是锁存逻辑的核心部分reg fault_sync, fault_latch; always (posedge clk or negedge rst_n) begin if (!rst_n) begin fault_sync 1b0; fault_latch 1b0; end else begin fault_sync fault_in; if (fault_sync) fault_latch 1b1; else if (clear_fault) fault_latch 1b0; end end assign pwm_enable ~fault_latch;fault_in接驱动芯片的故障输出clear_fault由上位机或按键产生。pwm_enable为低时所有 PWM 信号在输出前被与掉。故障锁存避免了“故障出现→快速恢复→再次过流”的振荡。验证时用一个按键模拟故障输入确认锁存后电机停止再按清除键恢复整个闭环才算真正可用。本文还有配套的精品资源点击获取
返回列表