☰
基于FPGA的寻迹避障小车设计与Verilog实现
2026/10/10 22:03:10 网站建设 项目流程

简介:基于FPGA(Verilog)的寻迹避障小车完整工程资料,以Basys2开发板为核心,面向电子类课程设计、FPGA初学者和智能车竞赛备赛者。整套方案把蓝牙通信、红外寻迹和避障判断整合在统一的控制逻辑中,实现无线遥控、自主循线与躲避障碍的联动功能。压缩包共518个文件,约1.99MB,包含Verilog源程序、比特流配置、引脚约束文件和集成开发环境工程等核心内容,同时附有综合实现过程生成的日志、时序报告和超文本说明,便于核对工程流程、排查错误或了解工具原理。工程内部模块化程度较高,顶层模块负责例化连接,分频模块产生工作时钟,蓝牙接收模块解析遥控指令,避障和寻迹模块分别处理障碍检测与路线跟踪。已有4071人学习,适合需要完整参考工程、快速复现硬件逻辑或在此基础上扩展功能的读者。 做寻迹避障小车,很多人第一反应就是单片机配个红外对管,便宜又简单。但我偏要在这个题目上换个玩法——用FPGA,用Verilog,把整个控制逻辑做成硬件电路。这个项目做完之后你会发现,FPGA做小车不是炫技,而是真正能把“并行处理”“时序确定性”这些概念落到实处的东西,尤其是在多路传感器同时接入、多任务需要同时处理的时候,优势非常明显。

先说说这个项目适合谁:对FPGA有一定基础、想找一个小而完整的工程练手的人,或者正在准备毕业设计、电子竞赛,需要一个能展示“硬件思维”的项目来做技术储备的人。它需要你接触传感器信号采集、PWM电机驱动、状态机设计、异步信号同步化处理,几乎涵盖了一个中等规模数字系统设计的全部要素。这篇文章会把我从方案选型到最终调试踩过的坑、想明白的道理,全部写出来。

1. 项目整体思路与方案选型

1.1 为什么用FPGA做小车:并行与确定性的胜利

单片机做小车,逻辑是“轮询”的:CPU一条一条地执行指令,先读传感器,再算控制,最后输出PWM。这个过程本身没有错,但当车速提高、传感器数量增多、控制策略变复杂时,CPU的负载会迅速上升,而且任何一个中断响应不及时,都可能导致小车冲出赛道。FPGA的本质是一大堆可以同时工作的逻辑单元,每个传感器通道可以独立占用一组逻辑资源,电机控制可以由独立的硬件定时器完成,寻迹算法和避障算法在硬件上天然并行。

打个比方,单片机就是一个只能一个人干活的小店,忙的时候什么都得排队;FPGA是一间流水线工厂,采购、加工、质检、包装各条线同时运转。对于寻迹避障这种“多输入、多输出、要求实时响应”的场景,FPGA的思维方式天然贴合。

1.2 整体框架:传感器、决策、执行三大部分

这小车的整体结构分成三层:感知层、决策层、执行层。感知层负责采集赛道信息和障碍物信息,决策层在FPGA内部完成逻辑判断,执行层把决策变成电机的实际运动。三层之间通过标准接口互联,每个部分都可以独立调试,这也是硬件工程师做项目的基本素养:不要一开始就整个系统联调,那是给自己挖坑。

感知层我用了两路模块:红外反射式寻迹传感器阵列和超声波测距模块。红外传感器负责检测黑线,超声波负责探测前方障碍物。决策层是整个FPGA逻辑的核心,包括信号同步、滤波、状态判断、PWM生成等模块。执行层是双路直流减速电机配上驱动芯片,靠PWM控制转速,靠方向引脚控制正反转。

系统框图(文字版): 红外寻迹传感器阵列 → 信号同步与滤波 → 寻迹状态判断 → 综合决策状态机 → PWM/方向输出 → 电机驱动 → 左右轮 超声波模块 → 回波计时 → 距离计算 → 避障逻辑 ↗

这个结构看起来平平无奇,但里面每个环节都有值得展开的细节。后面我会逐个拆开讲。

2. 核心硬件设计与模块规划

2.1 寻迹传感器阵列:为什么不用单路对管

入门教程里常用的方案是三个红外对管并排安装,中间那个对准黑线,两边偏离。三个管子输出三路高低电平,逻辑简单,代码好写。但这种方案的上限很低:一旦车速提起来或者弯道半径小,三路传感器能够提供的有效状态非常有限,中间状态判断很粗糙,小车容易在两个值之间来回抖动。

我选的是五路数字量输出的红外阵列模块(市面上常见的那种带比较器输出的模块,每路独立输出高/低电平)。五路的好处是能把“小车与黑线的相对位置”细分成更精确的状态,从而让转向决策更平滑。五路从左边到右边编号为L2、L1、C、R1、R2,状态映射做起来也直观。

单独看每一路,它是一个红外发射管加接收管对地构成的分压电路,输出经过电压比较器后变成0/1数字信号。当发射管对准白色地面时反光强,接收管导通,比较器输出低电平(或者高电平,取决于模块设计);对准黑色线条时反光弱,输出相反电平。用之前务必确认模块输出极性——我见过不少人代码写反了导致小车“找黑走”变成了“见黑躲”。

2.2 超声波避障模块:给小车加一双眼

避障方案有红外避障、激光测距、超声波测距几种。红外避障模块成本最低,但方向性差、环境光干扰大,在室内白天阳光直射时经常误报。激光测距精度最高,但价格对一个小车项目来说偏贵。超声波是折中:成本可控、原理清晰、能测真实距离,特别适合“前方有障碍物就减速/转向”这种宏观避障需求。

我用的HC-SR04超声波模块,工作方式是电平触发:给Trig引脚一个10微秒以上的高电平,模块内部发出8个40kHz的超声波脉冲,同时把Echo引脚拉高,直到收到回波再拉低。Echo引脚高电平的持续时间乘以声速(340 m/s)再除以2,就是障碍物距离。

距离(cm) = Echo高电平时间(us) × 0.034 / 2 = T_us × 0.017

这里有个容易被忽视的点:Echo高电平时间最大值对应距离约4米左右,对小车场景完全够用。真正要注意的是测量周期不能太短——声波传播本身有时间延迟,连续快速触发会导致上一次的回波还没回来,下一次已经发出去了,测量结果直接错乱。我实测的稳定触发周期是50ms以上,换算成小车速度是足够实时避障的。

2.3 电机驱动与PWM控制:TB6612比L298N好用

驱动芯片我在L298N和TB6612之间做过对比。L298N是老牌芯片,驱动能力强,但压降大、发热明显,而且逻辑控制引脚电平兼容性一般,用FPGA的3.3V IO去控制它的逻辑输入不太保险。TB6612FNG是东芝的驱动芯片,体积小、压降低、逻辑输入可以直接接3.3V,双路H桥驱动两个直流电机刚刚好。

TB6612的控制方式很标准:每路电机有AIN1/AIN2两个方向引脚,一个PWMA速度引脚。方向引脚决定正反转,PWM引脚决定占空比。两张表格可以总结清楚:

模式AIN1AIN2PWMA电机状态
正转10占空比前进/右轮正转
反转01占空比后退/右轮反转
刹车11任意快速制动
自由00任意惯性滑行

控制逻辑在Verilog里实现,本质上就是一个组合逻辑加一个PWM生成器。PWM频率我选10kHz,这个频率对直流电机来说是甜点区:太低会听到明显的啸叫和顿挫感,太高驱动芯片的开关损耗会变大。10kHz听不到声音,电机响应也线性,实测下来运转非常安静。

3. Verilog核心逻辑实现与状态机设计

3.1 顶层架构:模块化设计的基本功

FPGA项目最忌讳的就是把所有逻辑堆在一个顶层文件里。我的程序按功能拆成四个模块:uart_debug(调试用串口发送,这个后面说)、line_tracker(寻迹采集与状态映射)、ultrasonic_driver(超声波触发与距离计算)、motor_control(PWM生成与方向控制)。顶层只负责例化这四个模块、连接信号、分配管脚。

每个模块的接口尽量做成标准的输入输出,方便单独仿真和复用。例如ultrasonic_driver对外就四个信号:clk(系统时钟)、trig(触发输出)、echo(回波输入)、distance_cm(距离值输出),里面有多少计数器、多少状态,外界完全不需要关心。这种“黑盒化”设计思维,是FPGA工程化和单片机编程最大的区别——单片机到处是全局变量,FPGA则强调模块间信号接口清晰。

3.2 寻迹模块与滤波消抖:硬件层面解决毛刺问题

寻迹传感器的输出虽然是数字信号,但绝对不是什么干净的0/1。传感器在黑白边缘附近时,比较器输出会抖动,产生大量毛刺;另外电机启动瞬间电流波动,也可能导致电平波动。如果直接把原始信号拿来做状态判断,小车会在直道上左右颤动,像喝醉了一样。

处理手法人人都知道:滤波。但FPGA里的滤波和单片机的软件延时消抖不太一样。我写了一个简单的“连续采样确认”逻辑:对每一路输入信号,用系统时钟连续采样多次,只有当连续N个周期都读到同一个电平时,才认为这个电平是有效的。N的大小决定实时性和稳定性的平衡。

代码示意:单路输入消抖动(三拍确认) reg [2:0] sync_ff; reg filtered; always @(posedge clk) begin if (rst) sync_ff <= 3'd0; else begin sync_ff[0] <= raw_in; sync_ff[1] <= sync_ff[0]; sync_ff[2] <= sync_ff[1]; if (&sync_ff) filtered <= 1'b1; else if (~|sync_ff) filtered <= 1'b0; // 中间态保持不变,继续积攒足够多的同电平样本 end end

理解这个逻辑的关键在于:它不是简单地“读一次就用”,而是引入了时间窗口的概念。只有当一个电平持续足够长的时间才采信,这天然地滤掉了窄脉冲毛刺。实现上用移位寄存器加一个与门、一个或非门即可,资源占用极低。

3.3 寻迹状态判断:五路传感器映射到运动模式

五路传感器滤波之后,就进入组合逻辑映射。这里我定义了一组寻迹状态:

  • 全白或全黑:视为丢线,进入搜索模式
  • 只有C亮(即只有中间传感器检测到黑线):直线前进
  • L1、C亮:轻微偏左,小左转
  • L2、L1、C亮:严重偏左,大左转
  • 右侧同理

为什么这样映射?关键在于人字纹赛道、十字交叉口这类特殊路况。比如十字路口时五路可能全部压线或者全部悬空,如果状态机不处理,小车会冲出去或者原地打转。我的处理是:检测到全亮(认为是十字路口)直接执行直行,因为这种情况下通常赛道是贯通的;检测到全灭(认为是飞线或大弯出界)进入摇头搜索模式,控制电机左转右转交替寻线。

这个逻辑在Verilog里就是一个case语句加几个判断分支,实现起来并不难,难的是要根据自己赛道的实际情况去调整优先级。巡线赛道的实际路况千差万别,任何状态映射都需要实测调参。

3.4 超声波避障逻辑:决策优先级的设计

避障逻辑和寻迹逻辑本质上是一个优先级问题:正常情况下听寻迹的,当前方距离小于阈值时,避障逻辑接管。用状态机的语言来描述,就是有一个主状态(寻迹模式/避障模式),避障模式下再细分具体动作(停车、后退、转向)。

避障决策伪代码: if (distance < 20) → 停车 elsif (distance < 35) → 后退并右转(或左转,取决障碍物分布) else → 正常寻迹行驶

距离阈值的选取和车速有关,不能拍脑袋定。我的车在占空比60%左右时速度大约0.5m/s,20cm的停车距离从发出停车指令到完全刹停,经过状态机切换、PWM输出、电机响应、机械制动这几个环节,实际刹车距离比理论上更长,所以阈值留了余量。这个“停不住”的问题我后面专门讲。

3.5 PWM生成与差速转向:控制小车的真正细节

PWM生成用计数器实现。10kHz的PWM对应时钟周期100微秒,如果系统时钟是50MHz,计数器需要从0数到4999,再根据比较值决定输出高低电平。占空比和方向分离控制:方向由AIN1/AIN2决定,速度由PWM占空比决定。

差速转向是寻迹小车的精髓所在:左转不是简单地把方向引脚翻转,而是左轮减速、右轮保持或加速,利用速度差实现平滑转向。如果直接用“停左轮、转右轮”的方式,小车在高速时容易因为惯性翻车或者甩尾。我的转向策略是:轻微偏转时只降低一侧车轮的PWM占空比到60%左右,严重偏转时把内侧轮占空比降到20%并短暂反转,实现原地大幅度转向。

这个策略调起来非常费时间,但效果也是肉眼可见的。你在逻辑里改一个参数,下载到板子上跑一圈,感受是完全不一样的。我的建议是:把几个关键参数(基础速度、转向差速比例、严重转向比例)定义成参数常量放在文件头部,方便反复调。

4. 实操中的典型问题与排查记录

4.1 传感器信号全是乱码:异步信号同步化

第一次上电调试,我通过串口把五路传感器的原始值发到电脑上,结果数据跳动得完全没法看。一开始怀疑是传感器模块坏了,换了一个还是一样。后来才意识到问题出在异步信号处理上——传感器输出的电平变化与FPGA系统时钟毫无关系,直接用这个信号去采样和判断,必然出现亚稳态。

解决办法是标准的“两级寄存器同步”:跨时钟域信号进入FPGA后,先打两拍再使用。第一拍可能采到不稳定的电平,第二拍理论上就能得到稳定值。虽然两拍不能完全消除亚稳态,但能把亚稳态发生的概率降低到工程上可以忽略的程度。加了两级同步之后,串口输出的数据干净得像仿真一样。

代码示意:异步输入同步化 reg [1:0] sensor_sync; always @(posedge clk) begin sensor_sync[0] <= sensor_in; // 第一拍,可能亚稳态 sensor_sync[1] <= sensor_sync[0]; // 第二拍,基本稳定 end // 后续逻辑只使用 sensor_sync[1]

从这之后我养成一个习惯:所有外部输入的信号,不管传感器、按键、还是通信接口,一律先进同步器。这个习惯救了我后面好多次。

4.2 仿真正常上板不动:综合工具与仿真的差异

我至今记得最抓狂的一次调试:寻迹逻辑在Vivado仿真里跑得完美无缺,波形漂亮,每个状态跳转都符合预期。下载到板子上之后,小车完全不动,传感器状态读回来又全是正常的。后来我逐段排查,发现问题出在一个忘记复位的计数器和一条“仿真能过、综合不能过”的语句上。

具体来说,我在寻迹消抖模块里用了一个计数器,复位时只清了一部分信号,初值情况下计数器的比较条件永远不满足,所以逻辑永远不跳转。仿真时因为初始态默认是X不定态,仿真器按自己的规则处理了,看起来一切正常;上板后实际电路的初值是按配置比特流确定的,计数器进入了错误状态,逻辑就卡死了。

这种“仿真与综合不一致”的坑在FPGA开发里太常见了,原因归根结底是RTL代码写得不严谨,依赖了仿真器的宽容处理。解决办法没有捷径,只能把所有寄存器都显式复位,所有组合逻辑都写全default分支,做到“仿真通过不是标准,综合后行为一致才算数”。

4.3 避障总是刹不住:距离换算和响应延迟

有一次我设置了30cm避障阈值,心想怎么都够安全了。结果小车在靠近障碍物时还是直接顶上去才停。排查后发现问题有两层:一是超声波模块的测量周期是50ms,也就是说每50ms才能更新一次距离数据,而小车在这个间隔内已经跑了2.5厘米;二是从检测到距离过近到PWM实际输出变化,中间还隔着状态机跳转和多个时钟周期。

深挖之后我意识到,避障系统的“延迟”不是某个单一模块造成的,而是整个链路的累计延迟。测量周期、同步打拍、状态机判断、PWM更新,每一环都贡献几个毫秒,累计起来就相当可观了。解决办法是提高超声波触发频率到20ms,同时把避障判定阈值从30cm提高到40cm——既然物理上做不到零延迟,就在逻辑上预留时间余量。

4.4 电机噪声导致传感器误判:电源隔离

还有一个特别典型的工程问题:电机一转,传感器数据就乱。用示波器测传感器电源引脚,发现上面叠加了大量毛刺,幅度能达到几百毫伏。电机是感性负载,启动和换向瞬间会产生很大的反向电动势,如果传感器和电机共用同一路电源,这些噪声会直接串到传感器的供电和参考电压上,导致比较器误翻转。

解决思路不是去“过滤”噪声,而是从根源上切断耦合路径:电机电源和逻辑电源分开供电,各自用独立的稳压芯片;电机驱动芯片的电源引脚旁边加大容量电解电容吸收瞬时电流;传感器模块和FPGA之间的信号线不要和电机线绑在一起走。这一套做完之后,传感器误判的问题几乎消失了。

4.5 编译报错module not found:文件添加遗漏

Vivado里新建工程的时候,如果你把Verilog文件直接拖进工程但忘了加到编译的source列表,综合时会报“module 'xxx' not found”。这个错误我在第一次建工程时就遇到过。原因是Vivado有“自动添加文件”和“手动添加文件”两种模式,从工程目录拖动文件到source视图时,文件可能只是被加入工程目录而没有真正添加到编译目标。解决办法是右键source视图空白处,选择“Add Sources”,用对话框添加文件;或者直接在Sources窗口确认文件是否出现在Design Sources列表中。

这个错误虽然简单,但对新手来说极其劝退——代码明明没有语法错误,却报找不到模块。现在每次新建工程,我加完文件之后第一件事就是检查Design Sources里有没有全部列出。

5. 调试工具与效率心得

5.1 串口调试:给FPGA加一个“眼睛”

FPGA里跑的逻辑对局外人来说是个黑盒,下载到板子上之后,你看不到内部状态,也不知道传感器读回来的值到底是什么。所以我在系统里加了一个UART发送模块,把五路传感器原始值、超声波距离值、当前状态机编号,打包成帧从串口发到电脑上。这个调试手段帮了大忙——所有看起来“不可见”的问题,一旦能输出到电脑上,就变成了可以分析的日志。

UART发送模块本身也很简单:波特率生成器加移位寄存器。我用的波特率是115200,时钟分频后按位发送,起始位1位、数据8位、停止位1位。整个模块不到50行Verilog,但价值巨大。调试时在电脑上开一个串口助手,就能实时观察小车内部决策状态。

5.2 FPGA内部的逻辑分析仪:ILA的使用

如果你用的Xilinx器件,Vivado自带集成逻辑分析仪(ILA),可以把你关注的内部信号在板子上实时抓取出来看波形。用ILA调试这东西第一次用就爱不释手:你不需要在电脑上装任何硬件探针,直接在工程里例化一个ILA核,把想看的信号接上去,综合下载之后就能在硬件上抓波形。

但ILA有个前提:信号必须是被综合保留的。如果你在综合时开了大优化,某些中间信号可能会被优化掉,ILA抓不到。稳妥的做法是给需要观察的信号加上(* keep = "true" *)约束,告诉综合工具不要优化这个信号。我用ILA抓通过一次PWM输出的波形,发现占空比和设置值完全对应,心里立刻踏实了——软硬件的问题定位就靠这些手段一件件缩小范围。

5.3 上板前的仿真清单:提前拦截低级错误

分享一下我现在养成的习惯:上板之前必做一轮“仿真清单”检查。第一,所有外部输入信号有没有做两级同步;第二,所有寄存器的复位值是不是都明确赋值了;第三,组合逻辑有没有写全case分支;第四,PWM比较值和计数器的位宽是否配套,会不会溢出;第五,跨模块的握手信号有没有漏加。

这五条是我几次被坑之后总结出来的,每一条都对应一次真实的翻车经历。曾经有一次因为计数器位宽不够导致PWM输出频率完全错乱,小车电机发出刺耳的尖叫,查了一个多小时才定位。从那以后,位宽检查成了我的基本盘。你现在让我带新人做FPGA项目,头三天不看别的,就练这个检查清单的养成。

6. 调参与实测记录

6.1 刚上电的小车是会“抽风”的:复位与初值

第一次把完整代码下载到板子上,按下电源开关,小车突然猛地原地转了一圈然后才稳定下来。原因是FPGA上电瞬间,各个模块处于未定义状态,电机驱动引脚的电平组合进入了“反转”模式,直到全局复位信号完成所有寄存器清零才恢复正常。这个现象在上电时非常常见。

解决办法是在顶层模块里加一个上电复位电路:用一个计数器从0开始计数,计到某个值(比如几毫秒)之后才释放复位信号。这样FPGA配置完成后,所有模块会先被强制复位一小段时间,等一切稳定之后再开始正常工作。这个简单的时序设计,可以避免绝大多数上电乱动的问题。

6.2 速度参数调优:追求“不抖动的最快速度”

寻迹小车调参的核心指标是“在稳定循迹前提下的最大直线速度”。速度太低走起来没意义,速度太高在弯道就会冲出赛道。我做了个简单的对照实验:固定其他参数,只调整PWM基础占空比,记录不同速度下的跑圈表现。

基础PWM占空比直线表现弯道表现结论
40%稳定轻松过弯保守
55%稳定中等弯道OK,急弯微抖推荐
65%稳定急弯容易冲出边缘
75%有抖动大弯都难过不可用

最终我落在55%附近,同时为急弯准备了一个“急停转弯”逻辑——检测到L2+R2同时有效(说明压到了急弯边缘),立即降低PWM到30%并加大差速。这个逻辑实测应对180°回头弯很有效。

6.3 连续运行稳定性:长时间跑下来会不会漂

小车连续跑十几分钟之后,会出现转向越来越不灵敏的情况。起初我怀疑是FPGA逻辑状态出了问题,但用ILA抓了半天发现逻辑一切正常。后来排查才发现是电机长时间运行发热,扭矩下降,再加上电池电压逐渐下降,同样的PWM占空比对应的实际转速变小了,视觉效果就是“车变钝了”。

这个问题的本质是开环控制在对抗环境变化。要彻底解决得加编码器做闭环PID,但那样复杂度立刻上去一个量级。作为小项目,我选择了“电压补偿”的简易方案:用ADC读电池电压,低于某个阈值就自动提升PWM占空比,抵消电压下降的影响。这个方案虽然粗糙,但很实用,算是性价比极高的改进。

写在最后

这个寻迹避障小车项目做到最后,其实已经不只是一个“小车”了。它像是一个微缩的数字系统设计全流程训练:从需求分析到方案选型,从模块设计到系统集成,从仿真验证到上板调试,每一个环节都在逼你思考“硬件是怎么工作的”而不是“代码是怎么写出来的”。我做完这个项目最大的感受是:FPGA的开发思维和单片机真的不一样,它逼着你养成模块化、同步化、严谨化的设计习惯,任何一个不够严谨的地方都会在硬件上以最直接的方式“报复”你——不是编译报错,而是小车跑飞。

最后再分享一个小经验:调试这类项目时,我强烈建议你用串口把内部状态实时发出来,同时用ILA抓波形,两边对照着看。很多玄学问题,一旦你同时看到“逻辑上的状态”和“物理上的输出”,立刻就不玄学了。还有一个细节,项目文件命名和版本管理要养成好习惯,调参的时候你会频繁修改代码,没有版本管理的话,改到某个参数发现不如之前好的时候,想回退都麻烦。这个项目虽然不算大,但足以帮你建立起一套完整的FPGA工程化工作流,以后再去做图像处理、高速接口这些复杂项目时,这些基本功会让你少走非常多弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询