简介:《深入浅出玩转FPGA》书中逻辑分析仪项目源代码,面向FPGA初学者和数字系统设计爱好者。项目基于Verilog HDL实现逻辑分析仪核心功能,涵盖边沿触发采样、数据缓存、串行输出显示与时钟同步等关键模块,通过阅读源码可理解FPGA内部时序控制与状态机设计思路,并可直接在开发板上仿真验证。
资源共113个文件,压缩包约1.93MB,主体为18个Verilog源码文件与13个TDF设计文件,另有编译报告、工程配置文件、MIF初始化文件及电路截图等辅助内容,工程结构完整,便于用Quartus直接打开学习。目前已有879人学习。
项目的价值在于把书中抽象的FPGA原理落到具体工程实践,读者可逐模块拆解触发器的数据采集流程、多通道并行处理逻辑以及资源优化手法,还能通过修改参数扩展采样通道或提高采样深度,是提升HDL编码能力与调试能力的实用范本。
1. 项目概述与源码价值
说实话,FPGA入门的路子上,《深入浅出玩转FPGA》这本书几乎是绕不开的。我当时啃这本书的时候,最头疼的不是语法,也不是开发流程,而是每次看到书里那些完整项目时,总想亲手把代码跑一遍、改一改、看看波形长什么样。尤其是“逻辑分析仪”这个项目,它不像流水灯、数码管那么“玩具”,又不像PCIe、DDR3那么劝退——属于那种卡在中间、恰好能逼你把状态机、跨时钟域、FIFO、串口通信全部串起来的项目。
先给还没接触过这个项目的朋友说清楚它到底是干嘛的:这个逻辑分析仪,本质上是我们在FPGA内部自己搭一个简易的调试工具。它的核心功能是把FPGA内部几路数字信号的实时电平变化,按照一定的采样率抓下来,存到内部RAM里,等满足触发条件之后,再把存储的数据通过串口发到PC端,由上位机软件绘制成波形。说白了,这就是一个“FPGA版迷你示波器”,只不过它只测数字信号,不测模拟电压。
这个项目为什么值得花时间啃下来?我的看法是,它几乎覆盖了FPGA开发中80%的通用基础模块:时钟分频与使能、异步信号同步、边沿检测、状态机设计、RAM读写控制、FIFO跨时钟域处理、串口收发逻辑。你把这几个模块吃透了,回头再去看那些复杂项目,会发现到处都是这个项目的影子。我自己在做后续的图像采集项目时,很多控制逻辑就是直接从逻辑分析仪这个项目里迁移过去的。
要说这个项目适合谁,我觉得两类人最合适:一是刚学完Verilog语法、正愁找不到“有点难度又不至于看不懂”的练手项目的同学;二是已经在做FPGA开发、但每次调试都只靠ILA(集成逻辑分析仪)点几个信号、不太清楚内部采样存储机制的人。前者可以在源码里学到完整工程的写法,后者可以借着这个项目跳出“只会用工具、不懂原理”的舒适区。
2. 整体架构拆分:一个逻辑分析仪到底由什么组成
2.1 顶层模块的信号流向设计
先看整个项目的顶层设计。打开特权同学书里配套的顶层RTL文件,你会发现它的信号并不复杂:一个系统时钟输入、一个复位按键、一个串口发送引脚,再加上若干路待测信号输入。整个设计的精妙之处不在信号数量,而在于内部的数据流走向。
我习惯把逻辑分析仪内部划分成五个部分来理解:采样时钟生成模块、信号采集模块、触发控制模块、存储控制模块(包含RAM)、串口发送模块。数据流的顺序是:待测信号先经过采集模块,根据采样时钟打拍后,写入一块双端口RAM;同时触发控制模块一直在监测采集到的数据,一旦满足触发条件,就停止写入或标记存储完成;最后存储的数据由读取控制模块读出,交给串口模块逐字节发出去。
这个架构里最值得学习的一点是,采集和发送是分开的。采集时串口不工作,发送时采集已停止。这种“先存后发”的思路,避免了采样过程中被串口占用资源导致丢数的问题,是一种非常实用的串行处理隔离手法。在实际工程中,很多异步数据的处理也都是这个思路——先把实时数据缓冲下来,再从缓冲区里取出来慢慢处理。
2.2 为什么选用双端口RAM而不直接连串口
先来说说存储这块的选型。项目里用的是双端口RAM,一个端口负责写入采样数据,另一个端口负责读出发送数据。写时钟和读时钟虽然在同一个工程里,但它们的频率和相位是不一样的,写逻辑由采样时钟驱动,读逻辑由系统时钟(用于串口波特率生成)驱动。这样两个端口独立工作,互不干扰,简单粗暴地解决了一个跨时钟域的数据交互问题。
可能有人会问:直接采样完就发给串口不行吗?能行,但有个致命问题——串口的波特率最高通常也就几Mbps,而采样率随便就是几十MHz甚至上百MHz。你采5个字节的时间,串口可能只发出去1个字节,数据根本没地方缓冲。所以必须在中间加一块RAM做缓冲,用空间换时间,这也正是真实逻辑分析仪设备的原理。
还有一个细节是RAM深度的选择,这个项目里应该是256字节或者512字节的量级,我没有细翻原始代码验证,但根据书里的调试效果来看,足以捕捉一段连续的信号变化。如果你在实际项目中需要更长的采样窗口,直接例化一个更大的RAM IP核就行,地址位宽改改,其余逻辑不用动。
2.3 模块间的时序配合是怎么协调的
模块拆分清楚了,还要看它们之间怎么协作。这个项目是靠一组“状态指示信号”把各个模块串起来的:采样模块产生一个“写入完成”标志,触发模块产生一个“触发到达”标志,存储模块等触发到达后再停止写入,然后拉高“数据就绪”标志,串口模块看到这个标志后开始读取RAM数据。
这一套握手逻辑,其实就是一个简化版的“生产者-消费者模型”。我在实际做FPGA开发时,发现几乎所有的多模块配合都逃不出这套框架——模块A准备好数据后拉高valid,模块B看到valid后拉高ready,A看到ready后再送下一个数据。你在逻辑分析仪这个项目里先把这套握手玩熟,后面写AXI协议、写图像数据流都会轻松很多。
3. 关键源码模块逐一解析
3.1 采样模块:怎么精确控制采样率
这个模块是整个项目的血压计,它的任务是按照外部设定的采样率,对输入的待测信号进行等间隔采样。如果采样率不稳定,后面存储的数据就是乱序的,波形根本没法看。
核心思路是使用计数器分频产生采样使能信号。假设系统时钟是50MHz,我们需要一个10MHz的采样率,那就计5个系统时钟周期输出一个使能脉冲。这里的重点在于:采样使能信号必须是单周期脉冲,而不是电平信号,否则采样模块就会在一个采样周期内重复采样多次。
我在这个模块里学到的最有价值的设计是“利用计数器产生拍形脉冲,而不是直接用分频时钟”。如果用分频后的时钟去驱动采样模块,会出现时钟偏斜问题,而且综合后时序收敛很难看。用使能脉冲的方式,所有逻辑仍在系统时钟域内,只是通过使能信号控制“什么时候干一次活”,这是FPGA设计中极其实用的一种风格。
// 采样控制核心逻辑(示意) reg [7:0] clk_div_cnt; wire sample_en = (clk_div_cnt == DIV_NUM - 1); always @(posedge clk or negedge rst_n) begin if (!rst_n) clk_div_cnt <= 8'd0; else if (sample_en) clk_div_cnt <= 8'd0; else clk_div_cnt <= clk_div_cnt + 1'b1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) sampled_data <= 1'b0; else if (sample_en) sampled_data <= signal_in; // 仅在使能脉冲到来时采样 end另外要注意,在采样之前最好对待测信号做两级同步处理,消除亚稳态。这在板级调试时尤其重要,因为待测信号可能来自别的时钟域,比如一个外部按键信号或者另一个模块的输出。如果你直接拿原始信号去采样,数据出现毛刺或者随机错误,你会非常困惑——信号明明逻辑上是稳定的,可采样结果就是偶尔跳变。
3.2 触发控制模块:怎么精确捕捉想要的那段波形
逻辑分析仪和普通示波器的一个核心区别,就在于它有灵活的触发机制。这个项目的触发逻辑虽然不算复杂,但麻雀虽小五脏俱全,包含了“边沿触发”和“电平触发”两种基本模式的大致框架。
边沿触发的原理是:模块一直在检测待测信号的上升沿或下降沿,一旦检测到边沿事件,就启动一个触发计数器,从0开始计数,当计到某个预设值时,认为“触发成立”,开始或完成采样存储。电平触发的原理类似,只是把检测条件从“跳边沿”改成了“保持特定电平N个周期”,适用于捕捉长时间保持某种状态的信号。
触发模块和存储模块的配合方式有两种常见做法:一种是在触发之前RAM里已经写入了一部分数据,触发后继续写入一段,然后再停止,这样就能看到“触发前”和“触发后”两段波形;另一种是先空等触发信号到来,触发后才开始写RAM,这种方式存储利用率高,但看不到触发前的状态。特权同学书里的实现更接近第一种,先入后停,方便观察触发前的数据。
这里有个实操细节要提醒一下:触发条件设置得太苛刻,会出现“等半天都触发不了”的情况;设置得太宽松,又会抓一堆无意义的数据。我建议你在理解代码后,把触发相关的参数单独做成几个常量定义,放在文件头部,方便后续更换触发条件,而不需要改动底层逻辑。
3.3 串口发送模块:数据怎么从RAM里“搬”到PC
最后说串口发送。在这个项目里,串口模块的职责是把RAM中存储的采样数据,按照约定的帧格式发送给上位机。帧格式一般包括帧头、有效数据、帧尾,有时还会加一个简单的校验字段。这个格式并不仅仅是随意定的,它决定了上位机软件怎么解析数据流。
我在看这个项目的串口代码时,特别注意到了状态机的写法。整个发送流程被拆分成了若干状态:空闲态、发送帧头态、发送有效数据态、发送帧尾态、结束态。每一个状态都有明确的出口条件和跳转目标。这种写法的好处是:调试起来非常直观,你甚至可以在仿真波形里一眼看出当前发送流程卡在哪个环节。
有个优化点是关于空闲检测的。如果你发送的帧头是一个特定字节(比如0xAA),而上位机接收时不知道数据何时开始,就需要在解析端做“同步搜索”——不断读取字节,直到读到帧头再开始按结构解析。这个逻辑在上位机软件里写,并不在FPGA内部,但你要在FPGA端把帧格式定得足够清晰,最好帧头、帧尾都用不容易和有效数据重复的特殊值。
4. 从源码到仿真验证:一次完整的实操记录
4.1 工程搭建与仿真环境准备
我强烈建议你拿到源代码后,先不要急着上板,先在仿真环境里把整个工程跑一遍,感受一下数据流。我用的是Vivado自带的仿真器,不需要额外安装工具,如果你用的是Quartus也可以用ModelSim,差别不大。
第一步是建立工程,把书里提供的所有RTL文件添加进去。然后新建一个testbench测试文件,在testbench里做几件事:生成时钟,给出复位信号,模拟构建一个待测试的信号输入,这个信号需要包含一个明确的跳变沿,以便触发模块能够检测到。比如你可以让信号保持低电平一段时间,然后突然拉高再拉低,形成一个毛刺,再维持一段时间。
在你跑仿真之前,务必检查一下RAM IP核的配置。如果书上工程使用的RAM是通过IP核例化的,它在不同版本的软件里生成的接口结构可能不同,直接拷贝旧工程的话,可能会出现端口不匹配的问题。以Vivado为例,老版本里的Block Memory Generator和Vivado 2020之后生成的接口在附加端口上有些差别,需要对照IP例化模板重新连接一遍。
4.2 仿真波形里必须确认的几个关键点
波形窗口打开之后,有四个关键点你一定要亲眼看清楚。
第一,采样使能信号的周期是否准确。你设置了分频参数N,那么采样使能脉冲之间的间隔应该恰好是N个系统时钟周期。如果这里不对,后面的数据全都是错的,而且很难定位问题根源。
第二,RAM的写地址是否能正确地从0增长到深度最大值,然后停止。在写使能无效之后,写地址应当保持不变,不能出现跳变或回绕。
第三,触发标志是否在你设定的边沿之后准确拉高。把触发标志信号添加到波形窗口里,然后放大到跳变沿附近,确认标志拉高的时刻与数据变化的时刻相对位置是否符合你的触发深度参数。
第四,串口发送状态机是否完整地走完一轮。检查发送移位寄存器的数据是否和RAM读出的数据一致,尤其要注意帧头帧尾字节的顺序。
4.3 板级调试中的常见问题与解决办法
仿真通过之后就到了上板环节,这一环节才是真正考验耐心的地方。我在软核调试过程中遇到过几个典型问题,这里列出来给大家做个参考。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上位机收不到任何数据 | 串口波特率不匹配 | 检查波特率生成参数是否正确,用串口助手发一个固定字符串验证线路 |
| 串口收到乱码 | 帧格式不一致 | 确认FPGA发送的帧头字节与上位机解析代码中的帧头字节一致 |
| 波形不刷新 | 触发条件没满足 | 把触发条件改为“无条件触发”,排除触发逻辑问题 |
| RAM数据全是FF或00 | 采样时序异常 | 仿真单独测试RAM写入模块,验证地址与数据对不对 |
串口乱码这个坑我踩得最多。很多情况下不是FPGA这边出了问题,而是你的USB转串口模块的晶振精度不够,导致波特率产生了小小的偏移。比如你理论上配置的是115200bps,但实际发送端和接收端之间的误差叠加之后,数据位就开始错位。解决办法是:优先用“10位数据无校验”的模式,不要用奇偶校验,降低出错率。
还有一个比较隐蔽的问题,是触发模块里的边沿检测逻辑。如果沿检测是用的组合逻辑+电平比较实现的,在高频背景下极易产生误触发。正确的做法是先把信号打两拍,再利用reg_q1和reg_q2组合成边沿标志,比如上升沿就是!q1 && q2。我在自己调试时发现,只要这个同步没做,偶发误触发的概率会随采样率提高而急剧上升。
5. 扩展玩法:逻辑分析仪还能往哪个方向升级
5.1 增加通道数量与数据位宽
书里的项目可能只有单通道或四通道,但实际待测信号往往是多路并行总线,比如你要同时观测一组地址线和数据线。扩展方法很简单:把RAM的数据位宽从8位改成32位,采样时同时把32根信号线打入RAM。此时你只需要注意RAM IP核的位宽参数要匹配,其他的控制逻辑改动量不大。
真正需要动脑子的是上位机软件部分。数据位宽变大后,帧格式要做相应调整——是每个通道单独打包发送,还是多通道打包成同一帧?前者解析简单,但通道多了效率很低;后者效率高,但需要约定好每个通道在帧内的排列顺序。我自己的偏向是后者,一次发完所有通道的同一时刻数据,这样在上位机还原波形时可以很容易按时间对齐。
5.2 增加数据压缩或抽点功能
如果你想要观察的信号持续时间很长,RAM有限,就需要考虑压缩采样。一种简单的做法是不连续存储——每隔N个采样周期存一次,牺牲时间分辨率换取更长的观察窗口。这种办法实现极简单,只需要在采样使能的基础上再套一层“抽点使能”。
另一种高级一点是存储有效数据前后沿:只记录信号发生变化的位置和值,不存不变的周期。这个逻辑本身不复杂,但需要额外设计一个编码结构,比如“时间戳-数据值”交替存储,复杂度一下子上升了一个量级。我建议新手先做抽点功能练手即可,这个功能在调试低速外设比如I2C、UART时非常实用。
5.3 自定义协议与上位机对接
如果不想用书里配套的上位机,自己用Python写一个简化的串口解析和波形绘制脚本,并不是很难。Python里用pyserial读串口数据,用matplotlib绘制波形,几十行代码就能跑起来一个工作原型。这样做的额外好处是你对协议的理解会更深——你亲手定义了帧格式,亲手实现了收发,后续再去用商业逻辑分析仪软件时,那些协议设置选项对你来说就不会再是黑盒了。
6. 实践心得:源码之外的几个经验总结
最后说几点我在跟着这本书做项目时沉淀下来的经验。第一,不要一口气把整个工程啃完。这个项目看起来不大,但它串了五六个模块,如果你第一遍就试图逐行理解每一行代码,很容易淹没在细节里。建议第一遍只看数据流——哪个模块输出什么信号给哪个模块,第二遍再细看每一个模块的内部状态机,第三遍再仿真对比。
第二,一定要亲手修改一个参数并观察结果。把采样率从1MHz改成500kHz,把触发条件从上升沿改成下降沿,把RAM深度增加一倍,然后重新跑仿真,看波形有什么区别。这种“主动制造变化”的学习方式,比被动读代码高效得多,你会在修改过程中发现原来有不少信号之间的时序关系你以为懂了,其实没懂。
第三,有条件的话,把整个工程迁移到自己的开发板上跑一遍。哪怕你的板子芯片型号和书上不一样,也没有关系。重新分配引脚、重新约束时序的过程,本身就是一次独立的工程实战。我当时把书里代码从原来的老平台迁到Xilinx 7系列之后,才彻底理解了代码的位宽匹配、时钟约束这些工程细节——这些东西,光看源码是真的看不出来的。
这个项目做完之后,我的收获远远超出了“会写一个逻辑分析仪”本身。它让我建立起了模块化设计的直觉,让我知道了遇到跨时钟域问题应该怎么思考,也让我在后来的调试过程中遇到问题时不再慌乱,而是能静下心来,按数据流的顺序一个一个模块地排查。如果说Verilog语法是FPGA的一门语言课,那这个项目就像一门综合实践课,值得反复做、反复消化。
本文还有配套的精品资源,点击获取