news 2026/10/30 5:16:50

FPGA纯Verilog循迹小车:从传感器采样到电机驱动的完整RTL实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA纯Verilog循迹小车:从传感器采样到电机驱动的完整RTL实现

简介:这份资源面向电子设计入门者与FPGA初学者,围绕“循迹小车”这一典型项目,给出从Verilog控制逻辑编写、红外传感器数据处理到电机方向调整的完整实现思路,适合用于课程设计、电子竞赛训练或自学硬件描述语言。压缩包共137个文件、约4.57MB,以Quartus II工程文件为主,包含cdb、hdb、ddb等编译数据库,rpt报告、sof与pof配置文件,以及少量v源码、vt与wlf仿真相关文件,另有readme说明,基本覆盖设计输入、综合、布局布线与下载配置全流程。资源已有3233人学习,说明其在同类项目中具备一定参考价值。读者可从中获得可编译的工程结构、testbench验证思路与硬件调试线索,理解红外循迹原理如何落到Verilog行为描述与Quartus II工具链中,并借助仿真结果排查逻辑问题,为后续嵌入式与智能小车方向打下实践基础。

1. 从一块开发板到能跑的小车:这个 FPGA 循迹项目到底能干什么

很多人第一次听到「FPGA 做循迹小车」,第一反应是:这不就是个单片机活儿吗,一块 STM32 加几个红外对管就完事了,何必上 FPGA?我当初也是这么想的,直到把整条链路拆开才明白,这个项目的价值根本不在「让车跑起来」,而在于它把传感器采样、PWM 调速、电机方向控制、状态机决策这几件事全部用 Verilog 在硬件层面并行铺开,你能亲眼看到每个时钟沿上发生了什么。对于学 Verilog、Quartus II 的人来说,这是一个能同时练到「时序逻辑」「组合逻辑」「引脚约束」「板级调试」的完整闭环,比点灯和数码管有意思得多。

这个项目适合两类人:一类是刚学完 Verilog 语法、想找个能上板的综合项目练手的学生;另一类是熟悉单片机、想理解「硬件并行」和「软件顺序执行」差别的开发者。它不依赖任何软核 CPU,纯 RTL 实现,代码量可控,用 Quartus II 建工程、分配引脚、下载到开发板就能跑。下面我按「项目是什么 → 怎么跑起来 → 核心模块怎么拆 → 怎么改」的顺序,把这份源码讲透。

2. 工程结构与技术选型:Quartus II 下的纯 RTL 循迹系统

2.1 为什么是 FPGA + 纯 Verilog,而不是软核方案

先说选型。循迹小车的核心任务其实很朴素:几路红外传感器读回黑线位置,主控根据位置偏差决定左右轮的速度,电机驱动执行。用单片机做,这些是顺序执行的,采样、判断、输出 PWM 挤在一个循环里;用 FPGA 做,采样是采样、PWM 是 PWM、决策是决策,各自独立跑在自己的时钟域或使能节奏上,互不阻塞。这个项目走的就是后者,全部用 Verilog 写成 RTL,没有 Nios II 软核,也没有调用任何 IP 核做 CPU,综合出来就是一堆逻辑门和触发器。

技术栈上,开发环境是Quartus II(老版本如 13.0/17.1 都能开,工程文件是.qpf+.qsf),综合语言是Verilog-2001,顶层用原理图或 Verilog 顶层都行。约束靠.qsf里的引脚分配或者 Assignment Editor 手动指定。这种「纯 RTL + 图形化引脚约束」的组合,是高校实验板和入门开发板最常见的形态,好处是透明,坏处是所有时序都得自己盯。

提示:Quartus II 不同版本对 Verilog-2001 的支持基本一致,但.qsf里的器件型号必须和你手上的板子对上,型号选错综合能过、下载会失败,这是新手第一个坑。

2.2 目录结构与模块划分

拿到源码包,先别急着打开顶层,先看目录。这类工程通常长这样:

track_car/ ├── track_car.qpf # Quartus 工程文件 ├── track_car.qsf # 引脚分配与器件设置 ├── rtl/ │ ├── top.v # 顶层,例化各子模块 │ ├── sensor_ctrl.v # 红外传感器采样与消抖 │ ├── decision_fsm.v # 循迹决策状态机 │ ├── pwm_gen.v # 双路 PWM 生成 │ └── motor_drive.v # 电机方向与使能控制 ├── sim/ │ └── tb_top.v # 顶层测试平台 └── doc/ └── pin_assign.xlsx # 引脚对照表

这个划分很关键:sensor_ctrl负责把几路红外电平同步到系统时钟域并做消抖,decision_fsm是大脑,pwm_gen出占空比,motor_drive把「左转/右转/直行/停止」翻译成左右电机的 IN1/IN2/ENA/ENB 电平。顶层top.v只做连线和时钟分频,不写业务逻辑。这种「一个模块一件事」的写法,是后面能改、能调、能仿真的前提。

模块职责关键输入关键输出
sensor_ctrl采样消抖红外原始电平稳定传感器状态
decision_fsm循迹决策传感器状态左右轮速度档位
pwm_gen占空比生成速度档位双路 PWM
motor_drive电机驱动PWM + 方向电机引脚电平

2.3 时钟与复位策略

整个系统一般跑在一个几十 MHz 的板载晶振上,比如 50MHz。但传感器采样不需要这么快,PWM 频率也就几 kHz 到十几 kHz,所以工程里通常有一个分频器产生采样使能(比如 1kHz)和 PWM 计数基准。复位用低电平有效的按键复位,同步到系统时钟后再分发到各模块。这里有个细节:复位一定要做同步释放,否则异步复位释放时刻靠近时钟沿会导致亚稳态,表现就是上电偶尔不动作,得按好几次复位才好——这个玄学问题后面避坑章节还会提。

3. 核心模块拆解:从红外采样到电机转动的完整链路

3.1 传感器采样与消抖:把抖动的电平变成可信状态

红外对管输出的是模拟比较后的数字电平,黑线白底切换时会有毛刺,直接送进状态机会让小车「抽风」。所以第一级必须是同步 + 消抖。常见做法是打两拍同步,再用一个计数器连续采样 N 次一致才更新状态。

// sensor_ctrl.v:同步 + 计数消抖 module sensor_ctrl ( input wire clk, // 系统时钟 50MHz input wire rst_n, // 低电平复位 input wire [3:0] raw, // 4 路红外原始电平 output reg [3:0] stable // 消抖后的稳定状态 ); reg [3:0] sync0, sync1; reg [15:0] cnt [3:0]; // 每路一个计数器 integer i; // 两级同步,消除跨时钟域亚稳态 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sync0 <= 4'b0000; sync1 <= 4'b0000; end else begin sync0 <= raw; sync1 <= sync0; end end // 每路独立消抖:连续 16 个周期一致才认 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < 4; i = i + 1) cnt[i] <= 16'd0; stable <= 4'b0000; end else begin for (i = 0; i < 4; i = i + 1) begin if (sync1[i] != stable[i]) begin if (cnt[i] == 16'd15) begin stable[i] <= sync1[i]; cnt[i] <= 16'd0; end else begin cnt[i] <= cnt[i] + 1'b1; end end else begin cnt[i] <= 16'd0; end end end end endmodule

这段代码的逻辑说明:sync0/sync1两级触发器把外部异步信号拉进clk域,这是跨时钟域处理的标准动作,别省。消抖部分给每路传感器配一个 16 位计数器,当同步后的电平和当前稳定值不一致时开始计数,连续 16 个时钟周期都一致才真正翻转stable,中途一旦一致就清零重来。参数上,16 个周期在 50MHz 下约 0.32 微秒,实际工程里如果传感器毛刺更严重,可以把阈值调到几百甚至上千,代价是响应变慢。我一般会把这个阈值做成parameter DEBOUNCE_NUM,方便按赛道情况调。

3.2 循迹决策状态机:四路传感器怎么翻译成转向

消抖后的 4 位状态送进决策模块。四路红外一般排成一排,中间两路压线、两侧探边。决策逻辑本质是一个查表 + 状态机:根据哪几路检测到黑线,决定「直行、左微调、右微调、左急转、右急转、停车」。

// decision_fsm.v:基于传感器组合的速度档位决策 module decision_fsm ( input wire clk, input wire rst_n, input wire [3:0] sensor, // 消抖后状态,1 表示检测到黑线 output reg [1:0] left_spd, // 左轮速度档 0~3 output reg [1:0] right_spd // 右轮速度档 0~3 ); // 传感器布局:sensor[3]最左 ... sensor[0]最右 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin left_spd <= 2'd0; right_spd <= 2'd0; end else begin case (sensor) 4'b0011, 4'b0110, 4'b1100: begin // 中间压线,直行 left_spd <= 2'd3; right_spd <= 2'd3; end 4'b0001, 4'b0010: begin // 偏右,左轮加速右轮减速 left_spd <= 2'd3; right_spd <= 2'd1; end 4'b1000, 4'b0100: begin // 偏左,右轮加速左轮减速 left_spd <= 2'd1; right_spd <= 2'd3; end 4'b1111: begin // 全黑,可能是十字路口,直行通过 left_spd <= 2'd3; right_spd <= 2'd3; end default: begin // 全白,丢线,停车或原地找线 left_spd <= 2'd0; right_spd <= 2'd0; end endcase end end endmodule

逻辑说明:case的每个分支对应一种传感器组合,输出左右轮的速度档位(0 停、1 慢、2 中、3 快)。这里用「档位」而不是直接给占空比,是为了把决策和 PWM 生成解耦——决策只管「要多快」,PWM 模块负责「怎么产生这个速度」。参数上,速度档位到占空比的映射在pwm_gen里定义,改赛道难度时只调映射表,不动状态机。注意4'b1111全黑和default全白的处理,这两条是实际跑车时最容易翻车的地方,十字路口和丢线必须有明确策略,否则车会原地打转。

3.3 双路 PWM 生成:占空比怎么算、频率怎么定

PWM 模块的核心是一个自由运行的计数器,计数值和占空比阈值比较,小于阈值输出高、大于输出低。双路电机各要一路 PWM,共用一个计数器即可,阈值分别来自左右速度档。

// pwm_gen.v:双路 PWM,共用计数基准 module pwm_gen #( parameter PERIOD = 16'd50000 // 50MHz 下 1kHz PWM )( input wire clk, input wire rst_n, input wire [1:0] left_spd, input wire [1:0] right_spd, output reg pwm_l, output reg pwm_r ); reg [15:0] cnt; reg [15:0] duty_l, duty_r; // 速度档位映射到占空比阈值 always @(*) begin case (left_spd) 2'd0: duty_l = 16'd0; 2'd1: duty_l = PERIOD / 4; 2'd2: duty_l = PERIOD / 2; 2'd3: duty_l = PERIOD * 3 / 4; default: duty_l = 16'd0; endcase case (right_spd) 2'd0: duty_r = 16'd0; 2'd1: duty_r = PERIOD / 4; 2'd2: duty_r = PERIOD / 2; 2'd3: duty_r = PERIOD * 3 / 4; default: duty_r = 16'd0; endcase end // 自由计数 always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= 16'd0; else if (cnt >= PERIOD - 1) cnt <= 16'd0; else cnt <= cnt + 1'b1; end // 比较输出 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_l <= 1'b0; pwm_r <= 1'b0; end else begin pwm_l <= (cnt < duty_l); pwm_r <= (cnt < duty_r); end end endmodule

逻辑说明:PERIOD决定 PWM 频率,50MHz / 50000 = 1kHz,这个频率对大多数直流电机驱动够用,太低电机会啸叫、太高驱动芯片可能跟不上。duty_l/duty_r用组合逻辑从档位映射,四个档位对应 0%、25%、50%、75% 占空比。参数上,如果你想让车更细腻,可以把档位从 2 位扩到 3 位、映射到 1/8 步进;如果电机启动困难,就把最低档从 25% 提到 40% 左右,避免「给了 PWM 但不转」的尴尬。注意cnt < duty这种写法综合出来是比较器,资源很小,不用担心。

3.4 电机驱动与方向控制:H 桥引脚怎么给

最后一级把 PWM 和方向翻译成电机驱动芯片(常见 L298N 或 TB6612)的引脚电平。以 L298N 为例,每个电机有 IN1/IN2 两个方向脚和一个 ENA 使能脚,ENA 接 PWM,IN1/IN2 决定正反转。

// motor_drive.v:方向 + 使能输出 module motor_drive ( input wire pwm_l, input wire pwm_r, input wire [1:0] left_spd, input wire [1:0] right_spd, output wire in1, in2, in3, in4, output wire ena, enb ); // 速度档为 0 时视为停止,方向脚拉低 assign in1 = (left_spd != 2'd0); assign in2 = 1'b0; assign in3 = (right_spd != 2'd0); assign in4 = 1'b0; assign ena = pwm_l; assign enb = pwm_r; endmodule

逻辑说明:这里做了简化,前进时 IN1=1、IN2=0,后退则反过来;速度档为 0 时方向脚拉低、PWM 也是 0,电机彻底停。实际工程里如果要支持倒车找线,就把in2/in4也接进决策输出。参数上,ena/enb直接接 PWM,注意有些驱动芯片使能脚是高有效、有些是低有效,接反了表现就是「PWM 越大车越慢」,这个在避坑章节细说。

4. 上板跑起来:Quartus II 建工程、约束引脚、下载验证

4.1 新建工程与文件加入

打开 Quartus II,File → New Project Wizard,工程名和顶层实体名保持一致(比如top),器件型号按你板子上的 FPGA 选(常见 Cyclone IV EP4CE6/EP4CE10)。建好后把rtl/下所有.v加进工程,确认顶层是top.v。这一步最容易错的是顶层实体名和文件名不一致,Quartus 会报找不到顶层,血泪经验是建工程时就把名字对齐。

4.2 引脚约束:把 Verilog 端口对到物理引脚

编译前必须做引脚分配,否则综合能过、布局布线会报「未约束引脚」。两种方式:Assignment Editor 里一条条填,或者直接编辑.qsf。我一般用后者,因为可版本管理、可复用。

# track_car.qsf 片段:引脚分配示例 set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_A1 -to raw[0] set_location_assignment PIN_A2 -to raw[1] set_location_assignment PIN_A3 -to raw[2] set_location_assignment PIN_A4 -to raw[3] set_location_assignment PIN_B1 -to pwm_l set_location_assignment PIN_B2 -to pwm_r set_location_assignment PIN_B3 -to in1 set_location_assignment PIN_B4 -to in2

说明:PIN_xx必须查你板子的原理图,不能照抄。-to后面跟的是 Verilog 顶层端口名,位选要写全(raw[0]而不是raw)。分配完在 Pin Planner 里能看到引脚图,确认没有冲突、没有落到专用配置引脚上。

4.3 编译、时序检查与下载

Processing → Start Compilation,等综合、布局布线跑完。重点看两个报告:Compilation Report 里的Timing Analyzer看有没有时序违例(50MHz 这种低速设计一般不会违例,但复位路径要留意),以及Fitter里的引脚分配是否全部成功。然后用 USB-Blaster 接 JTAG,Programmer 里选.sof文件下载。下载成功后车应该能上电动作,如果不动,先查复位按键电平和电机供电,别急着怀疑代码。

注意:下载用的是.sof(掉电丢失),要固化到配置芯片得转.pof走 AS 模式。实验阶段用.sof反复下载就行,别一上来就固化,改一次烧一次很浪费时间。

5. 避坑与排查:上板后车不听话的五个真实原因

5.1 现象:PWM 给了但电机不转,或越大越慢

原因通常有两个:一是使能脚极性接反,L298N 的 ENA 是高有效,但有些模块板载了反相器,实际低有效;二是 PWM 频率太低导致电机嗡嗡响但不启动。解决:先用万用表量 ENA 脚在给 50% 占空比时是不是方波,再看驱动芯片手册确认极性;频率低于 500Hz 就调小PERIOD。

5.2 现象:上电偶尔不动作,按几次复位才好

这就是前面提的异步复位释放亚稳态。原因:复位按键是机械触点,释放时刻随机,直接当异步复位用,释放沿可能撞上时钟沿。解决:复位信号先打两拍同步再分发,或者用「异步置位、同步释放」的标准复位同步器。改完之后上电一次到位,这个后悔药我吃过不止一次。

5.3 现象:小车在直道上左右画龙

原因多半是消抖阈值太小或决策档位太激进。传感器毛刺没滤干净,状态机频繁切换;或者左右轮速度差给得太大,一偏就过冲。解决:把DEBOUNCE_NUM调大,把速度档从 3/1 改成 3/2 这种更温和的差,必要时在决策里加一个「偏差持续 N 个周期才转向」的确认逻辑。

5.4 现象:过十字路口直接冲出赛道

原因:全黑状态4'b1111被当成直行,但十字路口之后如果线偏移,车已经来不及修正。解决:给全黑加一个「保持直行固定时长」的计数器,过了这个时长再重新按传感器决策;或者干脆在十字路口降速通过,把left_spd/right_spd临时压到 2 档。

5.5 现象:综合报错「Can't find top-level entity」

原因:工程顶层实体名和文件名不一致,或者.v文件没加进工程。解决:确认top.v里module top和工程设置里的 Top-Level Entity 完全一致,Files 列表里能看到所有源文件。这个错误看着吓人,其实两分钟能定位。

6. 二次开发:把固定逻辑改成可调参数与可扩展架构

6.1 用 parameter 把「写死」变成「可调」

原始工程里最容易改坏的地方,是把消抖阈值、PWM 周期、速度映射全写成字面量。二次开发第一步,是把它们抽成parameter或顶层localparam,集中在一个car_cfg.vh里。这样换赛道、换电机、换传感器数量时,只改一个文件,不用满工程搜数字。比如把PERIOD、DEBOUNCE_NUM、SPEED_MAP都提出来,顶层例化时传参,仿真和上板用同一套配置,避免「仿真对、上板错」的经典翻车。

6.2 加一个简单的 PID 或比例修正

现在的决策是离散档位,直道画龙的本质是「修正量固定、没有按偏差大小调节」。想进阶,可以把传感器状态编码成偏差值(比如最左 -2、中间 0、最右 +2),乘一个比例系数得到左右轮速度差,这就是最朴素的 P 控制。Verilog 里用移位代替乘法(乘 2 就是左移一位),资源几乎不增加,但循迹平滑度会明显提升。验证方法:先在tb_top.v里灌一组传感器序列,看输出的速度差是不是随偏差线性变化,再上板。

6.3 用仿真先验证再上板

sim/tb_top.v是这个工程最值钱的部分之一。改决策逻辑前,我习惯先在测试平台里把传感器序列写成task,跑一遍看波形,确认状态机跳转和 PWM 占空比都对,再综合下载。上板调试的成本是仿真的几十倍,尤其是电机这种「错了就撞墙」的执行器。ModelSim 或 Quartus 自带仿真都行,关键是养成「先仿真、后上板」的习惯。

改造点改动位置验证方式
参数集中化新增 cfg 头文件编译通过 + 仿真一致
比例修正decision_fsm灌序列看速度差
十字路口策略decision_fsm仿真全黑序列
倒车找线motor_drive上板低速测试

从那以后我每次改完决策逻辑,都强制先在tb_top.v里跑一遍传感器序列、看波形确认,再碰下载器——这个习惯帮我省下了无数次拆车、重新接线的功夫。希望这份拆解能帮你把这个 FPGA 循迹小车顺利跑起来,也能顺着这些改造点玩出自己的版本。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 5:15:36

安全重保应急方案:团队组建、云监控与一小时响应落地

简介&#xff1a;《安全重保应急方案》Word文档面向参与重大活动、关键时期网络安全保障的运维人员、安全应急人员及项目管理者&#xff0c;帮助梳理重保工作的组织分工、技术支撑与处置流程。文档先从重保团队组建切入&#xff0c;说明组长、外部接口副组长、内部接口副组长以…

作者头像 李华
网站建设 2026/10/30 5:15:27

Windows内核设备管理:nt!IopGetRootDevices与PipApplyFunctionToSubKeys解析

1. nt!IopGetRootDevices函数背景解析在Windows内核中&#xff0c;设备管理是一个极其复杂的子系统。nt!IopGetRootDevices函数作为设备栈构建过程中的关键环节&#xff0c;主要负责处理根设备节点的枚举工作。这个函数通常在内核初始化阶段或设备热插拔时被调用&#xff0c;其…

作者头像 李华
网站建设 2026/10/30 5:15:26

HTML5语义化与Web开发最佳实践

1. HTML基础概念与核心特性HTML&#xff08;HyperText Markup Language&#xff09;作为构建万维网的基石语言&#xff0c;其设计哲学始终围绕着内容结构与语义化表达。不同于早期仅关注外观呈现的标记语言&#xff0c;现代HTML5标准更强调元素本身的语义价值。比如<article…

作者头像 李华
网站建设 2026/10/30 5:15:24

C#双重编译揭秘:从MSIL到JIT,理解程序首次运行慢的底层原理

简介&#xff1a;这份PPT面向零基础或刚接触微软技术栈的编程学习者&#xff0c;系统梳理C#语言基础与.NET平台的核心概念&#xff0c;帮助读者建立从语法到运行机制的完整认知。内容围绕C#程序基本结构、面向对象设计、委托与泛型等高级特性、集合类接口以及文本处理与文件IO展…

作者头像 李华
网站建设 2026/10/30 5:12:44

空间分析数据实战:PostGIS存储、坐标系与空间连接优化

简介&#xff1a;「空间分析数据」压缩包面向GIS初学者与需要巩固ArcGIS实操的从业者&#xff0c;围绕空间位置、空间关系、空间统计及栅格与矢量分析等主题&#xff0c;提供可直接加载练习的数据素材&#xff0c;帮助解决缺少案例数据、难以走通完整分析流程的问题。包内共155…

作者头像 李华
网站建设 2026/10/30 5:12:43

DeepSeek-OCR-v2模型架构与部署实践详解

1. DeepSeek-OCR-v2 模型架构解析DeepSeek-OCR-v2作为新一代光学字符识别引擎&#xff0c;其架构设计充分吸收了当前视觉与自然语言处理领域的前沿成果。模型采用多阶段混合架构&#xff0c;核心包含三个关键组件&#xff1a;1.1 特征提取骨干网络基于改进的ResNet-152架构&…

作者头像 李华