简介:这份资源面向电子设计入门者与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 循迹小车顺利跑起来,也能顺着这些改造点玩出自己的版本。
本文还有配套的精品资源,点击获取