
简介这份源码包收录了《深入浅出玩转FPGA》特权同学书中逻辑分析仪项目的完整工程实现面向正在接触或进阶学习FPGA的开发者适合通过实际项目掌握数字系统设计、时序约束与片上调试方法。包体共113个文件、约1.93MB核心代码以Verilog.v为主包含18个源文件另有AHDL设计文件.tdf、Quartus工程配置.qpf/.qsf、存储器初始化文件.mif、编译与下载文件.sof/.pof覆盖从源码编写、综合布线到硬件配置的完整流程。目前已有879人学习项目延续作者“动手做”的教学风格围绕采样控制、触发设计、VGA显示等模块展开代码配有测试激励tb_*与备份文件便于仿真验证和二次修改。研读后可以直观理解多时钟域处理、数据采集乒乓缓冲、实时显示控制等FPGA核心知识点也能在此基础上扩展通道数或采样深度兼具学习与实用价值。 手上这块板子调I2C接口的时序时我又一次体会到什么叫望眼欲穿示波器探头能抓到引脚上的电平变化但FPGA内部某个寄存器的跳变、某个状态机的流转、某根内部信号线上的毛刺全部藏在封装底下看不见摸不着。用SignalTap II固然省事可逻辑分析仪占用的存储资源和调试引脚在复杂工程里往往不够用。折腾了几个晚上后我翻出特权同学《深入浅出玩转FPGA》里的逻辑分析仪项目源码照着思路自己搭了一个内嵌的虚拟逻辑分析仪把困扰已久的那根信号彻底看清楚了。这篇文章把整个项目的实现思路、源码模块拆解和实操要点整理出来适合正在学FPGA但苦于内部信号不可见的初学者也适合想给自己的设计加一个轻量调试通道的工程师。核心内容围绕采样、触发、存储、上传四条主线展开照着做就能在自己板子上跑起来。1. 为什么要在FPGA里造一个逻辑分析仪外部逻辑分析仪和开发板自带的在线调试工具各有各的局限。外置逻辑分析仪比如LA1010探头只能接到FPGA的引脚上能看到的是已经过引脚约束和IO逻辑的信号而FPGA内部真正出问题的点往往在跨时钟域的同步器出口、状态机的次态组合逻辑、FIFO的空满标志翻转这些内部节点上这些位置没有任何物理引脚可以引出。SignalTap II这类工具虽然能直接观测内部信号但综合时会额外插入调试逻辑占用片上存储和布线资源在资源紧张或者时序收敛吃力的工程里插上分析仪综合结果就变了排错反而失真。特权同学这本书里给出的思路很有意思用FPGA自身实现一个片上逻辑分析仪把被测信号按预设条件采样下来暂存在片上RAM或者FIFO里然后通过串口发送到PC端的上位机软件显示波形。它的价值不光是省一台几千块的仪器更关键的是——你观测的就是真实运行环境中、综合布局布线之后信号的实际行为不附加任何外部探头的负载效应也不改变原设计的时序约束。整个项目的逻辑其实很朴素被测信号进采样模块按设置的采样率打拍触发模块在信号满足设定条件时拉高触发标志存储控制逻辑把触发前后的采样数据写入FIFO串口发送模块在触发后把FIFO里的数据按帧协议搬到PC端。PC上位机负责解析串口数据、还原波形并显示。四个环节各司其职任何一个环节做扎实了整条链路就通了。2. 整体架构拆解上位机与FPGA内部逻辑的分工先看清楚这个项目的宏观图景。PC端上位机软件负责三件事下发采样参数采样深度、触发方式、触发位置、接收串口数据、把数据点还原成时间轴上的波形。FPGA内部逻辑则承担四块任务信号采样、触发判断、数据缓存、串口上传。上下游通过UART这一条最朴素的物理链路沟通不依赖JTAG、不依赖厂商专用调试IP这也是这套方案移植性极好的原因——只要是带UART外设的FPGA基本都能跑起来。在《深入浅出玩转FPGA》配套源码中FPGA侧的逻辑分析仪核心被封装成几个相对独立的模块工程结构上可以清晰地区分sampling模块负责以设定的采样时钟对被测信号打拍同时处理跨时钟域问题trigger模块检测预设触发条件上升沿、下降沿、电平值或特定码型storage模块控制FIFO/RAM的读写时序决定触发前记录多少数据、触发后继续记录多少数据uart_tx模块把缓存区数据按帧协议逐字节发送给上位机uart_rx模块可选接收上位机下发的参数配置top层完成例化和信号连接并提供给用户一个简单的断言接口上位机侧相对简单打开串口、按波特率收数据、按帧格式解包、把采样点映射到坐标上绘制波形并提供缩放、滚动、测量光标等查看功能。这里有一个容易忽视的设计要点上下位机之间必须约定好数据帧格式。源码里通常采用帧头 数据长度 采样数据 校验的格式帧头用于对齐字节流数据长度告诉上位机本次需要收多少字节才算一帧完整波形校验用于丢弃被干扰的坏帧。如果把上下位机比作两个人隔着墙递砖头上位机只关心砖头波形数据好不好用不关心FPGA内部怎么码砖而FPGA只管按协议把砖头码整齐扔出去也不关心对面怎么堆放。协议就是墙上那个窗口的尺寸定了就不能随便改改一处两边就得同步调。3. 核心源码逐段拆解触发、采样、存储与上传这一节是重头戏我把源码里最关键的几段逻辑拆开讲每个模块解决什么问题、为什么这么写都结合我重写和调试时的体会来说明。3.1 采样模块里的跨时钟域处理被测信号可能来自任意时钟域而逻辑分析仪的采样时钟通常是系统主时钟或者独立的采样时钟。直接拿采样时钟去打拍外部信号如果外部信号正好在时钟边沿附近变化就会出现亚稳态。源码中标准的做法是两级触发器同步打拍reg [1:0] sync_ff; always (posedge clk_sample or negedge rst_n) begin if (!rst_n) sync_ff 2b00; else sync_ff {sync_ff[0], probe_signal}; end assign sampled_signal sync_ff[1];两级触发器防止亚稳态传播这个所有FPGA工程师都熟。但这里有个细节值得注意经过两级同步之后信号实际上被延迟了两个采样时钟周期。在后续计算触发位置和波形时间戳时要把这固定延迟计入偏差好在逻辑分析仪观测的都是纳秒到微秒级别的相对时序两个采样周期的固定延迟不影响相对关系的判断但你心里得有这笔账。如果被测信号是一组并行总线比如要同时观测8位的地址总线每个bit单独打拍会导致bit之间出现不可预测的相位差尤其在采样时钟较快时容易踩到建立保持时间的临界点。稳妥的做法是先用采样时钟对整组总线统一寄存一拍再做同步保证所有bit在同一时钟边沿被捕获。3.2 触发模块的判定逻辑与触发位置控制触发是整个逻辑分析仪的灵魂。没有触发你只能盲录一段数据然后碰运气找目标有了触发你可以让采集行为在目标事件发生时精确停止或精确前后对齐。源码里的触发逻辑通常这样实现wire trigger_condition; assign trigger_condition (sampled_data trigger_value) ? 1b1 : 1b0; reg trigger_flag; always (posedge clk_sample or negedge rst_n) begin if (!rst_n) trigger_flag 1b0; else if (trigger_condition) trigger_flag 1b1; else trigger_flag trigger_flag; end这是一个最简电平触发实际工程里会扩展出上升沿触发当前拍为高且上一拍为低、下降沿触发、任意值变化触发、以及一个窗口内的码型匹配触发。实现上升沿判定的核心是保留上一拍的值reg [7:0] data_d1; always (posedge clk_sample or posedge rst_n) begin if (rst_n) data_d1 8b0; else data_d1 sampled_data; end wire rising_edge_trigger (sampled_data trigger_value) (data_d1 ! trigger_value);触发位置的控制是另一个容易被低估的点。你要抓的是事件发生前的一段上下文还是事件发生后的行为还是事件前后各留一半这直接决定存储控制逻辑怎么设计。源码里一般用一个可配置的预触发深度参数触发信号到来时FIFO里已经存了多少拍数据多出的部分继续往后存满为止。这样在恢复波形时触发点之前的N拍数据和之后的M拍数据都被保留下来。实现方式是在FIFO写计数上做文章——触发前每写一拍计数加一触发后继续写直到总深度达到设定值才停止而不是触发瞬间立刻停写。3.3 存储控制FIFO深度与位宽的计算逻辑存储模块最直观的指标是采样深度一共记录多少拍和采样位宽同时记录多少根信号。片内RAM是有限的这两个参数此消彼长。一个典型的8通道、深度2048的逻辑分析仪在50MHz采样率下只能记录约41微秒的波形这在调试I2C这类慢速协议时绰绰有余但想抓一串高速差分信号就捉襟见肘了。源码里的FIFO控制逻辑本身并不复杂核心是处理好读写指针和触发标志之间的配合// 伪代码示意实际工程见完整源码 reg wr_en; reg rd_en; always (posedge clk or negedge rst_n) begin if (!rst_n) wr_en 1b0; else if (!fifo_full !acquisition_done) wr_en 1b1; else wr_en 1b0; end我实际调试中发现最容易出问题的地方是采集完成信号的去抖。如果acquisition_done在触发后立即拉高而FIFO里预触发的深度还没写够就会导致触发点之前的记录不完整。正确做法是触发后需要等待FIFO写满剩余深度再拉高完成标志。这个逻辑和3.2节提到的预触发深度是配套的两者必须对齐否则你看到的上位机波形要么缺头要么缺尾。FIFO的读侧相对简单在完成标志拉高后按串口发送模块的请求逐字节读出。需要注意异步FIFO的读写时钟可能不同采样时钟和串口时钟通常不是一个源此时务必使用异步FIFO的原语或者IP核并正确处理读空信号防止读了空数据导致波形错位。3.4 串口发送模块的时序与帧协议数据从FIFO出来之后要通过UART发送出去。UART发送模块的核心是一个按位发送的状态机起始位、8个数据位、停止位波特率由计数器分频产生parameter BAUD_DIV CLK_FREQ / BAUD_RATE - 1; reg [15:0] baud_cnt; wire baud_pulse (baud_cnt BAUD_DIV);这里的CLK_FREQ是串口模块的工作时钟频率BAUD_RATE是约定好的波特率。假设系统时钟50MHz、波特率115200那么分频计数器的周期值是434。每收到一次上位机命令或者每完成一次采集发送模块按帧格式把数据搬到移位寄存器里逐位移出。务必在发送过程中避免对FIFO的读使能施加不稳定的控制信号否则可能出现丢字节。帧协议我在移植时做了小小的改动把原始源码中固定长度的帧改成帧头8位长度数据CRC8校验的变长帧。固定长度帧的好处是上位机解析简单但灵活性差变长帧让采样深度不再受限于帧格式代价是上位机需要多解析两个字节。实战中这个改动收益远大于成本——同一个工程里我既能用256深度快速抓一段毛刺也能用4096深度完整记录一整次总线事务。4. 从源码到板子移植适配、参数调整与波形验证光看代码不动手永远只能停留在看懂了的层面。这一节把我把源码跑到自己板子上的全过程和调整点写出来你可以照着操作。第一步确认资源与接口打开工程先看三件事目标FPGA型号、可用Block RAM总量、UART引脚分配。逻辑分析仪位宽与深度选择直接受BRAM容量约束。比如Cyclone IV E系列的EP4CE10片上RAM约414Kbit做一个8通道、4096深度的分析仪约需32Kbit完全没压力但要上32通道、16384深度就会逼近极限。我的建议是先用最小的资源配置把链路跑通验证完功能再按需加大。第二步修改采样位宽和深度参数源码顶层一般会有如下宏定义或参数parameter CHANNEL_NUM 8; // 采样通道数 parameter SAMPLE_DEPTH 2048; // 采样深度 parameter SAMPLE_CLK_DIV 1; // 采样时钟分频系数按自己需要改但注意FIFO的位宽和地址位宽要同步更新。这里我踩过一次坑只改了FIFO深度参数忘了改FIFO IP核的地址宽度仿真正常但上板后波形后半段全是重复数据。第三步把被测信号引出到顶层把需要观察的信号从原工程内部拉出来接到逻辑分析仪模块的被测输入端。这一布线的核心要求是不能影响原信号路径——只增加扇出不插入任何逻辑门避免改变原时序。如果被测信号本身经过了一个组合逻辑链观测点应选在寄存器输出端这样看的是时序上确定的值而不是组合逻辑的瞬态毛刺。第四步配置串口参数并烧录打开上位机串口配置波特率设为115200数据位8、停止位1、无校验。将编译好的bit文件烧录进FPGA按下复位键后fpga侧应该发送一帧握手信息给上位机上位机显示设备已连接表示链路通了。如果收不到握手先用串口助手裸看数据确认有没有字节输出有字节但上位机不识别多半是帧格式没对齐。第五步设置触发条件抓一段真实波形把触发方式设为上升沿触发采样深度设为1024触发位置设为50%即触发前记录512点、触发后记录512点。然后制造一个目标事件——比如按下板子上的按键产生一个按键消抖后的跳变。你会看到上位机画出事件前后的完整波形。对比示波器直接测引脚的结果两者应该完全吻合。我建议在验证阶段做一次双保险对比用SignalTap II同时抓同一个信号和自己写的逻辑分析仪对比波形。两者重合度越高说明采样和同步逻辑越可靠。这一步能快速发现跨时钟域处理是否引入了多余毛刺也能验证触发位置的精度。5. 源码阅读和移植时容易踩的坑下面按踩坑频率从高到低列出我实际遇到过的问题如果还在调试先对照这份清单排查坑一触发后FIFO没有写够预触发深度就停止现象上位机波形总是缺触发点之前的数据。原因触发标志直接连到了FIFO写使能的关闭控制上没有等待剩余深度写满。解决在存储控制中增加一个状态机收到触发标志后继续写直至总采样深度满足设定值。坑二跨时钟域导致的偶发毛刺在波形上表现为亮瞎眼的尖刺现象明明被测信号很干净波形上却出现一个采样周期宽度的异常跳变。原因单bit信号直接用采样时钟打拍没问题但多bit总线各bit同步延迟不一致。解决先用采样时钟寄存一拍再整体同步对多bit信号增加一位对齐标志。坑三串口丢字节导致上位机波形整体错位一截现象波形前面一段是正常的中段突然出现整帧错乱。原因波特率分频数的计算方法不对或者发送模块在FIFO读空后仍然读数据。解决按BAUD_DIV CLK_FREQ / BAUD_RATE - 1重算分频参数在FIFO空信号拉高时禁止读使能。坑四采样时钟过快导致BRAM带宽不够现象FIFO写数据时丢拍。原因当采样位宽大于FIFO数据位宽时FIFO一次只能写入一个字节而采样一拍产生多个字节的数据造成带宽不匹配。解决在采样模块和FIFO之间加一个数据宽度转换的缓冲寄存器组确保每个采样周期内FIFO能完整吞下所有通道的数据。坑五复位释放时序不对抓的第一帧全是0或FF现象上电后第一次采集的数据全是0。原因复位信号释放时FIFO状态还没就绪写使能已经打开导致写入了无效数据。解决在复位释放后增加一个固定的FIFO初始化延迟等empty信号确认有效后再允许写。这些坑都不是源码本身的逻辑错误更多是移植过程中上下文环境的差异导致的。读源码时除了看懂每个模块的功能还要刻意去理解模块之间的接口时序约定——比如触发标志是电平还是脉冲、FIFO的读使能是高有效还是低有效、帧头是单字节还是双字节。这些接口细节才是移植时真正会咬人的地方。6. 我的一点使用心得这个项目能走多远在我自己的工程里这个片上逻辑分析仪已经不只用来调试还承担了小型产测工具的角色。把采样深度配到最大通过串口接一个简单的Python脚本就能在PC端自动完成对某几个关键节点信号的频率和电平判断。实际用下来配合好触发条件这套方案能稳定观测到几十纳秒级的事件对于绝大多数板级调试场景已经足够。有个小建议留给想深入改造的朋友可以给逻辑分析仪增加一个连续采集模式不依赖触发周期性地上传数据类似一个简易的逻辑分析仪上的滚动显示。这个功能在上电时序分析和异常崩溃后的现场复现上特别有用。在源码基础上做这个扩展不难核心是存储控制里增加一个按周期自动清零覆写的状态分支上位机同步增加连续显示模式即可。如果你正在被FPGA内部信号看不见的问题困扰先别急着下单买几千块的逻辑分析仪。翻翻书上的源码花一两个晚上把它移植到自己的板子上你会突然发现原来自己手里这块芯片早就藏着一个小型的逻辑分析仪只是你还没把它唤醒而已。本文还有配套的精品资源点击获取