1. 从零开始为什么用FPGA控制直流电机大家好我是老李在嵌入式控制和FPGA这块摸爬滚打了十几年。今天想和大家聊聊一个既经典又实用的项目用FPGA来给直流电机调速。你可能用过单片机比如STM32的PWM功能来控制电机简单方便那为啥还要折腾FPGA呢这就像你平时开家用轿车单片机上下班很顺手但当你需要极限的操控、毫秒级的响应或者同时处理好几项高精度任务时你可能就需要一台专业的赛车FPGA了。FPGA最大的优势就是“并行处理”和“硬件级定时精准”。想象一下单片机就像一个单核CPU所有指令得排着队一条条执行。而FPGA内部则像是一个可以随意搭建的乐高城市你可以同时搭建出分频器、PWM发生器、按键消抖、速度计算等多个独立的“硬件电路”它们同时工作互不干扰。这意味着你的PWM波生成极其稳定不受其他程序分支的影响响应速度是纳秒级的。这对于一些要求高精度调速、多电机同步控制或者需要在控制环路中实时加入复杂算法比如PID的场景FPGA的优势就非常明显了。这次我们要做的就是基于Altera现在是Intel的Quartus II开发环境用Verilog HDL这门“硬件描述语言”亲手搭建一个直流电机PWM调速系统。我们会从最基础的PWM原理讲起然后设计H桥驱动逻辑接着写代码、建工程、分配管脚最后把程序烧录到FPGA开发板上亲眼看着电机听我们的话转起来、快起来、慢下来。整个过程就像在芯片内部“焊接”一个专属的电机控制器既有硬件的扎实感又有软件设计的灵活性特别有成就感。2. 核心原理拆解PWM与H桥是如何让电机听话的2.1 PWM调速不是持续给力而是“频繁点刹”PWM中文叫脉宽调制是控制直流电机转速最核心的技术。它的原理听起来很高大上但其实理解起来非常生活化。我们不要把电机的供电想象成一直拧着水龙头放水而是想象成一个人在用锤子有节奏地敲打一个飞轮。这个锤子敲打的频率是固定的比如每秒钟敲1000次对应PWM频率1kHz。但每次敲击的力度持续时间即脉宽是可以变化的。如果锤子每次接触飞轮的时间很长比如占了一个敲击周期90%的时间那么飞轮获得的平均推动力就大转速就快。如果每次接触时间很短只占周期的10%那飞轮大部分时间都在靠惯性旋转平均速度就慢。这个“占空比”高电平时间占整个周期的比例就是咱们控制转速的“油门”。在咱们的FPGA系统里我们会用一个计数器来生成这个精准的PWM波。比如我们设定一个16位的计数器从0累加到65535再归零这就是一个周期。我们再设定一个“比较值”。当计数器的值小于这个比较值时输出高电平“敲击”大于等于时输出低电平“离开”。通过按键改变这个“比较值”就轻松改变了占空比从而改变了电机的平均电压实现了无级调速。这种纯数字逻辑生成PWM的方法比模拟电路稳定得多也精准得多。2.2 H桥驱动电路让电机既能正转也能反转PWM解决了调速问题那怎么让电机换向呢这就需要H桥驱动电路出场了。为什么叫“H桥”你把四个开关和一个电机画在纸上形状就像一个“H”字母。电机在中间一竖四个开关在四个角落。它的控制逻辑非常巧妙核心是对角线导通原则当我们需要电机正转时我们让左上和右下两个开关闭合右上和左下断开。电流从电源正极经左上开关、流过电机从左到右、再经右下开关流回负极。当我们需要电机反转时就让右上和左下两个开关闭合左上和右下断开。电流方向就反过来了电机也就反转了。在我们的数字系统中这四个开关通常用MOSFET或集成电机驱动芯片如L298N来实现。而FPGA的I/O口就负责输出两路控制信号假设叫MOTOA和MOTOB来控制这“两对”开关。为了避免上下两个开关同时导通导致电源短路这叫“直通”是毁灭性的我们的控制逻辑必须确保在切换方向时有一个“死区时间”即两路信号都短暂为低让所有开关都断开一下然后再开启新的通路。虽然在我们这个基础项目里可能用简单的逻辑也能工作但了解这个“死区”概念对设计可靠的驱动电路至关重要。3. 手把手搭建FPGA工程从代码到原理图3.1 工程创建与代码编写三个核心模块首先打开Quartus II新建一个工程。器件选择根据你的开发板来比如经典的Cyclone IV EP4CE6或Cyclone V系列都行。工程名就叫motor_pwm。接下来我们要创建三个Verilog文件对应三个核心模块。第一个模块分频器 (div_clk.v)。开发板上的晶振通常是50MHz这个频率太高了直接用来生成PWM和扫描按键会造成资源浪费和逻辑复杂。我们需要把它分到一个较低的频率比如1MHz或更低作为我们系统的主时钟。这里给出一个更健壮的分频器代码module div_clk ( input wire clk_50m, // 输入50MHz时钟 input wire rst_n, // 低电平复位信号 output reg clk_1m // 输出1MHz时钟 ); // 50MHz / 1MHz 50 需要计数50次。因为要产生对称方波所以每计25次翻转一次。 parameter DIV_CNT 25; reg [5:0] counter; // 计数器位宽根据分频比调整 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin counter 0; clk_1m 0; end else begin if (counter DIV_CNT - 1) begin counter 0; clk_1m ~clk_1m; // 计满后时钟翻转 end else begin counter counter 1; end end end endmodule第二个模块PWM发生器 (pwm_gen.v)。这是核心中的核心。它接收分频后的时钟和来自按键的“占空比设定值”输出PWM波。module pwm_gen ( input wire clk, // 分频后的系统时钟如1MHz input wire rst_n, input wire [3:0] duty_set, // 4位占空比设定0-15代表0%-100%16级 output reg pwm_out ); reg [3:0] counter; // 4位计数器计数0-15与duty_set位宽匹配 always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 0; pwm_out 0; end else begin counter counter 1; // 计数器循环累加 // 比较计数器和设定值生成PWM if (counter duty_set) begin pwm_out 1b1; end else begin pwm_out 1b0; end end end endmodule这段代码实现了一个4位精度的PWM有16个调速档位。你可以通过增加duty_set和counter的位宽比如改成8位来获得256级更精细的调速。第三个模块顶层控制模块 (motor_ctrl.v)。这个模块像是一个总指挥它把分频器、PWM发生器、按键处理、方向控制逻辑全部连接起来。它需要处理按键消抖这是硬件项目必须做的否则按键一次会被误读多次、根据按键调整占空比和方向并输出最终控制H桥的两路信号。module motor_ctrl ( input wire clk_50m, input wire rst_n, input wire key_speed_up, // 加速按键 input wire key_speed_down, // 减速按键 input wire key_dir, // 方向切换按键 input wire key_en, // 启停按键 output wire motor_a, // 连接H桥输入A output wire motor_b, // 连接H桥输入B output reg [3:0] led_indicator // LED指示当前速度档位 ); wire clk_sys; wire pwm_wave; reg [3:0] duty_cycle 4d8; // 初始占空比设为中间值 reg direction 1b0; // 方向寄存器0正转1反转 reg enable 1b0; // 使能寄存器 // 实例化分频模块 div_clk u_div_clk ( .clk_50m(clk_50m), .rst_n(rst_n), .clk_1m(clk_sys) ); // 实例化PWM生成模块 pwm_gen u_pwm_gen ( .clk(clk_sys), .rst_n(rst_n), .duty_set(duty_cycle), .pwm_out(pwm_wave) ); // 按键消抖与处理逻辑简略示意实际需要更完整的消抖状态机 // 这里用一个简单的计数器延时消抖 reg [19:0] debounce_cnt; reg [3:0] key_r, key_stable; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin key_r 4‘b1111; // 假设按键低电平有效初始化为高 key_stable 4‘b1111; debounce_cnt 0; end else begin key_r {key_speed_up, key_speed_down, key_dir, key_en}; if (key_r ! key_stable) begin debounce_cnt 100_000; // 计数约2ms50MHz时钟 end else if (debounce_cnt ! 0) begin debounce_cnt debounce_cnt - 1; end else begin key_stable key_r; // 稳定后的按键值 end end end // 根据稳定按键值改变控制参数 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin duty_cycle 4d8; direction 0; enable 0; led_indicator 4b0000; end else if (debounce_cnt 0) begin // 在消抖结束后处理 // 处理加速键 if (!key_stable[3] enable) begin // 按下且电机使能 if (duty_cycle 4‘d15) duty_cycle duty_cycle 1; end // 处理减速键 if (!key_stable[2] enable) begin if (duty_cycle 4‘d1) duty_cycle duty_cycle - 1; end // 处理方向键 if (!key_stable[1]) direction ~direction; // 处理使能键 if (!key_stable[0]) enable ~enable; // LED指示当前速度档位二进制显示 led_indicator duty_cycle; end end // 输出电机控制信号结合PWM波和方向、使能信号 assign motor_a enable ? (direction ? 1‘b0 : pwm_wave) : 1‘b0; assign motor_b enable ? (direction ? pwm_wave : 1‘b0) : 1‘b0; endmodule这个顶层模块把一切都串起来了。注意motor_a和motor_b的赋值逻辑它确保了电机使能时根据方向信号将PWM波正确地分配给H桥的一路另一路给固定电平从而形成对角线导通。当电机不使能时两路都输出0电机自由停止。3.2 原理图连接与管脚分配把代码“画”成电路代码写完后在Quartus II里分别对这三个.v文件“Create Symbol Files for Current File”。这步操作会为每个模块生成一个图形化的符号.bsf文件就像电子元器件的封装一样。然后新建一个Block Diagram/Schematic File.bdf文件。在空白处双击在Symbol对话框的Project目录下你能找到刚刚生成的div_clk、pwm_gen、motor_ctrl这三个符号。把它们拖到画布上。接着从左侧工具栏找到“Pin”工具添加输入输出引脚并把这些引脚用导线Bus或者Node连接到各个模块的端口上。比如把输入引脚CLK_50M、RST_N、KEY[3..0]连接到motor_ctrl模块的对应输入口把motor_ctrl模块输出的MOTOR_A、MOTOR_B、LED[3..0]连接到输出引脚。接下来是最关键的一步管脚分配。你需要根据自己FPGA开发板的原理图找到晶振连接到了哪个FPGA引脚比如PIN_E1按键连接到了哪些引脚准备驱动H桥的电机控制信号输出到哪些引脚LED指示灯连接到哪些引脚。在Quartus II的Assignment Editor或者Pin Planner工具中将这些逻辑信号名如CLK_50M与实际芯片的物理引脚号一一对应起来。这一步错了下载后板子肯定没反应。还有一个非常重要的安全设置处理未使用的引脚。在Assignments - Device - Device and Pin Options中找到Unused Pins选项卡一定要将其设置为As input tri-stated作为三态输入。这可以防止未使用的引脚随机输出电平导致芯片电流过大甚至损坏。4. 仿真、综合与下载验证你的设计4.1 功能仿真在电脑上“预演”在把代码烧进板子前强烈建议先做一下仿真这能帮你提前发现很多逻辑错误。Quartus II自带的仿真工具叫ModelSim可能需要单独安装和设置关联。你需要编写一个测试平台文件Testbench例如tb_motor_ctrl.v。在这个文件里你模拟给设计输入各种激励信号比如产生一个50MHz的时钟模拟按键的按下和释放过程。然后观察motor_a、motor_b和led_indicator这些输出信号的变化是否符合你的预期。比如当你模拟按下加速键时led_indicator输出的二进制数是否递增motor_a输出的PWM波占空比是否在变宽。通过波形图你能直观地看到整个系统的时间序逻辑是否正确。4.2 全编译与下载让电机转起来仿真没问题后回到Quartus II主界面将.bdf原理图文件设置为顶层实体Set as Top-Level Entity。然后点击“Start Compilation”进行全编译。这个过程包括综合把你的Verilog代码转换成逻辑门网表、布局布线把这些逻辑门映射到FPGA芯片的具体位置和连线资源上、时序分析检查信号传输是否满足时间要求等。编译成功后会生成一个.sofSRAM Object File文件。用USB-Blaster下载器连接电脑和FPGA开发板的JTAG口。给开发板上电在Quartus II的Programmer工具中添加生成的.sof文件点击“Start”。程序就会被下载到FPGA的SRAM中运行。此时你应该就能通过板子上的按键控制电机的启动、停止、加速、减速和正反转了LED灯也会以二进制形式显示当前的速档。如果电机不转别慌按以下顺序排查1. 电源都接了吗FPGA板、电机驱动模块、电机本身2. 下载线连接和编程软件识别正常吗3. 管脚分配确认和开发板原理图一致吗特别是电机驱动模块的控制线。4. H桥驱动模块本身工作正常吗可以先用杜邦线给个固定电平测试一下电机能否简单转动。5. 进阶思考与优化让项目更上一层楼做到这一步一个基本的FPGA直流电机调速系统就完成了。但如果你想更深入这里有几个可以优化的方向也是我在实际项目中常遇到的第一加入PID速度闭环控制。现在的系统是开环的你设定一个占空比电机就以一个大致对应的转速旋转。但如果负载变化比如电机轴上的摩擦变大转速就会掉下来。要实现精准稳速就需要闭环。你需要一个编码器装在电机轴上实时反馈转速信号给FPGA。FPGA内部再实现一个PID控制器算法不断比较目标转速和实际转速动态调整PWM占空比。这才是工业级控制的样子。在FPGA里用Verilog写PID虽然挑战大但响应速度极快性能远超单片机。第二提高PWM分辨率与频率。我们之前用了4位计数器只有16级调速。你可以轻松地改为8位256级甚至16位65536级实现极其平滑的调速。同时PWM频率的选择也有讲究。频率太低比如几十Hz电机会听到明显的啸叫声人耳可闻频率太高比如上百kHz虽然听不到了但MOSFET开关损耗会增大驱动电路设计难度也增加。对于普通直流有刷电机1kHz到20kHz是一个常用范围需要根据实际电机和驱动电路测试确定。第三完善用户交互。除了用LED显示二进制档位你还可以用FPGA驱动一个七段数码管或者LCD屏直接显示转速百分比或估算的实际转速值。还可以增加更多的控制模式比如缓启动占空比从0慢慢增加到设定值避免启动电流冲击、速度曲线预设等。第四时序约束与可靠性设计。对于更复杂的系统必须在Quartus II中设置正确的时序约束.sdc文件告诉工具你的时钟频率是多少哪些是异步信号。这样才能保证综合布线后的电路在真实的物理延迟下依然稳定工作。此外代码中的按键消抖逻辑我们只是简单示意一个健壮的状态机式消抖模块是必不可少的。对于电机控制这种可能涉及强干扰的环境在FPGA的I/O口前加入施密特触发器缓冲或者进行软件上的滤波也是提高系统抗干扰能力的常用手段。这个项目虽然基础但它是一个完美的起点串联了数字逻辑、硬件描述语言、EDA工具使用、外设驱动和控制系统概念。我当年第一次用FPGA点亮LED时兴奋了半天后来让电机按自己的想法转动时那种对硬件直接操控的感觉更是令人着迷。希望你在动手实现的过程中不仅能掌握流程更能体会到这种底层控制的乐趣。遇到问题多查资料、多调试FPGA的世界深度和乐趣成正比。