FPGA逻辑分析仪设计:从采样触发到串口波形还原

发布时间:2026/9/1 9:24:08
FPGA逻辑分析仪设计:从采样触发到串口波形还原 简介面向FPGA初学者的逻辑分析仪项目源代码源自《深入浅出玩转FPGA》一书由作者特权同学分享定位于帮助读者通过实际工程理解FPGA工作原理并系统实践数字系统设计流程适合正在学习Verilog/VHDL、时序逻辑与调试工具的入门及进阶开发者。压缩包共113个文件约1.93MB包含v和tdf格式的HDL源码、qsf/qpf等Quartus工程文件、rpt/qmsg等编译报告、sof/pof等配置文件另有mif初始化文件与仿真波形等中间产物目录结构清晰便于按模块研读与复现。项目已有879人学习下载。核心知识点覆盖硬件描述语言搭建组合与时序逻辑、触发器捕获条件设计、多通道数据采集与缓冲管理、跨时钟域同步处理、并行转串行通信以及位宽裁剪等资源优化技巧。通过阅读与运行这份代码读者能够完整理解逻辑分析仪从采样、存储到显示输出的实现思路并可在现有工程基础上扩展通道数、提高采样率或增加串口回传强化板级调试与动手能力。整体而言是一份兼顾原理讲解与实践扩展的高质量FPGA学习资料。1. 项目价值与设计思路1.1 这个项目解决的痛点做FPGA开发的人几乎都遇到过这种场景调一个串口通信或者调一组并口时序逻辑明明仿真过上板后却死活不对。手边没有逻辑分析仪只能靠万用表测电平、靠LED闪烁猜状态效率低到让人怀疑人生。厂商自带的SignalTap、ILA工具虽然好用但你没吃过猪肉也见过猪跑心里清楚这类工具本身就是在FPGA里嵌入了一个逻辑分析仪背后的采样、触发、存储原理恰恰就是这本书里这个项目要教你的。特权同学的《深入浅出玩转FPGA》里逻辑分析仪项目是我个人认为全书实操价值最高的一个。它的核心思路很直接把被测信号接到FPGA的普通IO上内部用逻辑代码做采样、触发、存储再把数据通过串口发给上位机显示波形。你不需要额外买几百上千块的专业分析仪一块入门级FPGA开发板加上一段代码就能临时顶上一个基础逻辑分析仪的角色。更关键的是把这段源代码从头到尾啃明白你就真正理解了逻辑分析仪内部那些“采样深度”“触发位置”“预触发”的概念以后再看到厂商工具里的参数设置完全不会有那种“对着选项却不知道该填什么”的迷茫。这个项目适合两类人一是刚学完Verilog语法、想找一个综合性项目练手的FPGA初学者二是工作中经常要调试数字接口、但预算和工具都不太充裕的嵌入式或硬件工程师。它不要求你有很深的数字信号处理背景只要会写状态机、知道FIFO和RAM的基本用法就能跟上我下面的拆解。1.2 数据链路与模块划分整个逻辑分析仪的代码框架本质是一条单向数据通路外部信号进来经过同步、采样、触发判断写进存储区最后通过串口发出去。我习惯把它拆成五个模块模块作用关键技术点时钟/采样率控制产生采样节拍决定波形时间分辨率分频计数、采样使能输入同步消除外部信号与FPGA内部时钟跨时钟域引入的亚稳态两级寄存器同步触发控制在满足设定条件后决定何时停止采样边沿检测、状态机数据存储暂存触发前和触发后的采样数据双口RAM、写地址回卷串口发送把存储数据按帧结构上传PCUART协议、波特率产生这样的模块划分不是随手分的是为了对后续调试有利。如果你把所有逻辑写在一个always块里状态一多仿真出问题根本不知道从哪查起。把触发状态机独立出来把存储接口做成标准RAM接口后面不管是换芯片平台还是把存储深度从4096加深到16384都只需要改局部参数不需要动整个架构。1.3 为什么是“FPGA采集串口上传”的方案我知道很多人会有疑问网上几十块钱的USB逻辑分析仪一抓一大把采样率还能到24M甚至更高为什么要费劲用FPGA自己做答案不完全是省钱而是学习链路完整。从采样原理到触发逻辑从存储回卷到串口波形还原整个系统没有黑盒每个环节出了问题都能在代码层面定位。等你理解了这个框架再去看厂商自带工具或成品逻辑分析仪时你看到的不再是几个配置选项而是背后那一串状态机在怎么工作。还有一个现实原因在调试FPGA内部信号时外部逻辑分析仪根本接不到你FPGA内部的那根信号线。你只能靠片内逻辑分析仪或者自己写采集逻辑。这个项目某种程度上等于自己动手写了一个简化版的片内逻辑分析仪以后做DDR3读写调试、视频接口调试的时候这套思路完全可以复用。2. 核心源代码逐段拆解2.1 时钟与采样率控制采样率决定了逻辑分析仪能观察多快的信号。根据奈奎斯特定理采样率至少是被测信号频率的两倍实际调试中我一般建议4到5倍起步否则连信号有没有毛刺都看不清楚。书里的例子通常会做几档采样率供上位机选择我用一个分频计数器来实现module sample_clk_gen #( parameter DIV_WIDTH 16 )( input wire clk, input wire rst_n, input wire [DIV_WIDTH-1:0] div_param, output reg sample_en ); reg [DIV_WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt d0; sample_en 1b0; end else begin if (cnt div_param - 1b1) begin cnt d0; sample_en 1b1; end else begin cnt cnt 1b1; sample_en 1b0; end end end endmodule这里我特意用sample_en使能信号而不是分频时钟因为分频时钟作为普通信号接进采样模块后续做时序约束会比较麻烦。用主时钟统一驱动再用使能信号控制采样节拍是更稳的写法。div_param的值由上位机下发比如主频50MHz、目标采样率5MHz那div_param就是10。2.2 输入同步与亚稳态处理外部信号和FPGA内部时钟之间没有任何相位关系直接拿外部信号做采样或触发判断大概率偶发出现亚稳态。我在实际调试中遇到过一种情况触发点总在某个固定区域抖动怎么调参数都没用最后发现就是没做同步。标准做法是打两拍reg sync_1, sync_2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sync_1 1b0; sync_2 1b0; end else begin sync_1 ext_signal_in; sync_2 sync_1; end endsync_2就是同步后的信号后续采样、触发判断都基于它。这个操作会带来1到2个时钟周期的延迟对逻辑分析仪来说完全能接受。需要提醒的是如果你采集通道数很多最好每个通道都做同步寄存器不要只挑一个通道做。我在最初做8通道版本时只对触发通道做了同步结果其他通道的数据偶尔会整体错一位后来才反应过来是不同IO引脚的外部信号在采样相位上不一致导致的。2.3 触发状态机设计触发模块是逻辑分析仪的灵魂没有它采集回来的数据就是一锅粥你根本不知道从哪段开始看。触发条件通常可以是上升沿、下降沿、特定电平值甚至更复杂的状态匹配。我先从最简单的上升沿触发说起。localparam IDLE 2d0; localparam ARM 2d1; localparam TRIG 2d2; reg [1:0] state; reg din_d, din_cur; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; din_d 1b0; end else begin din_cur sync_2; // 当前采样值 din_d din_cur; // 上一拍采样值 case (state) IDLE: state ARM; // 启动后开始预填数据 ARM: begin // 上升沿检测上一拍为低当前拍为高 if (din_d 1b0 din_cur 1b1) state TRIG; end TRIG: begin // 触发后继续采样直到预设深度填满 if (capture_done) state IDLE; end endcase end end这里从ARM到TRIG的跳变意味着已经检测到了上升沿。但要注意触发条件满足只是启动“触发后数据填充”的起点存储区里还保留了触发前的一段数据。这个“触发前数据保留多少、触发后数据保留多少”的比例是逻辑分析仪最重要的参数之一。常见的做法是把它做成一个pre_ratio寄存器上位机下发的设置直接决定存储地址在触发前回卷到多少位置、触发后到多少位置停止。2.4 存储控制与写地址回卷存储模块一般用双口RAM写端口归采集逻辑管读端口归串口发送管。写地址在采样期间一直递增到设定深度后回到0实现循环覆盖这个过程叫“回卷”。触发条件满足后再按预设的后续深度继续写直到填满就拉停。reg [ADDR_WIDTH-1:0] waddr; reg write_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin waddr d0; end else if (write_en) begin if (waddr DEPTH - 1b1) waddr d0; else waddr waddr 1b1; end endDEPTH根据RAM资源选比如4096点深度就需要ADDR_WIDTH1216384点需要14。在实际学习中我建议先在代码里把深度写成参数方便后面在约束文件或顶层模块里调整。写使能的控制逻辑要和触发状态机配合在ARM状态写使能一直拉高数据不断回卷进入TRIG状态后写使能继续拉高直到再写满设定的后触发深度然后拉停。这样存储区里保留的就是触发前触发后的完整窗口。2.5 UART发送与帧协议串口发送模块是最直接的UART TX只发不收。波特率常见的有115200和460800低速调试建议先上115200压力小、不容易出错。发送端把RAM里读出来的数据按字节发送数据位8位停止位1位不带校验。// 以50MHz系统时钟、115200波特率为例 localparam BAUD_DIV 50_000_000 / 115200 / 2 - 1;这个参数是波特率周期半周期分频值。UART发送逻辑我在这里不展开完整的移位发送代码只提醒一个容易犯的错发送模块的读使能一定要等RAM写入完全停止后再拉起来否则你会读到一半的未稳定数据上位机波形图就会出现莫名其妙的毛刺。可以加一个capture_done标志触发状态机在存储区填满后置位串口发送模块检测到这个标志后再从地址0开始按顺序读RAM。这样发送和采集在时序上是天然错开的调试起来省心很多。3. 关键参数与工程化取舍3.1 采样深度、通道数与触发位置的选择这三个参数是互相制约的。片上RAM总量有限通道数越多、深度越深能用的采样窗口就越小。我做学习版时选的参数是8通道、4096采样点、触发位置50%换算下来存储区大小是4096×832768bit也就是32Kb对入门级芯片来说压力不大。触发位置50%的意思是触发点前保留一半数据触发点后保留一半数据适合观察串口起始位这种“关键边沿前后都要看”的场景。如果调试的是I2C或SPI这类总线我倾向于把触发位置改成20%或更小因为大部分有效信息在触发点之后。这里要特别提醒触发位置改的不是RAM深度而是存储区地址回卷策略。简单来说触发点越靠前ARM阶段回卷的时间越短触发后能记录的数据越长。这个参数如果能做成上位机可动态配置调试体验会好非常多。3.2 上位机配合与帧格式设计书里通常会配套一个简单的上位机接收串口数据并绘制波形。如果你找不到现成软件或者想练手用Python的pyserial加matplotlib也能快速实现。但不管用什么工具上位机和FPGA之间必须约定一个稳定的帧格式。我建议的帧结构是2字节帧头比如0xAA 0x55、1字节通道状态、N字节采样数据、1字节校验。校验可以用最简单的累加和不要把格式搞得太复杂否则上位机解析一个错一个调起来非常痛苦。帧格式定好后FPGA发送端就在RAM读取时按这个格式打包。上位机收到一帧后去掉帧头、校验数据再把每个采样点的通道位拆出来就能画成波形。这里有个经验之谈第一次联调时先发一个固定已知数据比如方波或者递增序列确认上位机能正确还原再切到实际被测信号。很多同学上来就连真实信号出问题后根本分不清是采集错了还是发送格式错了。4. 调试实录与常见问题4.1 触发不稳定始终抓不到想要的波形这个现象我在复刻项目时出现过很多次。触发点跳动、波形窗口偏移、甚至完全抓不到信号我整理了一个排查表现象可能原因处理办法触发点反复跳动外部信号未做两级同步存在亚稳态检查同步寄存器确认用的是同步后信号做触发抓不到触发边沿采样率比信号慢太多提高采样率至少4倍于信号频率触发后波形缺失预触发比例设得过大后触发深度不够调小预触发比例波形整段偏移触发沿检测用错了寄存器打拍位置检查din_d和din_cur的先后关系核心经验就一句话触发逻辑必须基于同步后的信号不能直接拿原始输入引脚做边沿检测。我见过有人为了省一拍延迟直接用ext_signal_in做边沿判断结果就是触发点忽前忽后查了一晚上才定位到问题。4.2 数据错位或者乱码串口上传的数据出现乱码最常见的原因是波特率分频算错或者系统时钟频率不是你以为的那个值。建议在代码里加一个测试模式发送固定字节0xA5用示波器或者另一个串口终端看发送结果如果0xA5能被稳定接收说明UART发送链路没问题再排查RAM读取时序。此外如果读RAM地址和写RAM地址没有错开很容易读出“写到一半”的数据表现就是每帧数据里固定有几个采样点不对。解决办法我在2.5里已经提过用capture_done标志把写入和读取在时序上完全分开。4.3 上板跑通但结果和仿真不一致这是FPGA开发里的经典问题更多是约束问题而不是逻辑问题。外部输入引脚的IO标准没设置成板子实际的电压等级或者引脚分配和原理图对不上都会导致上板后信号异常。我的习惯是每次新建工程后第一时间写一个.qsf或.xdc文件把外部输入引脚都设为3.3V-LVTTL并逐个核对原理图上的网络名。千万不要等到代码全写完再补约束否则调试时你会分不清是逻辑问题还是引脚问题。另外分频计数的采样使能信号在仿真时没问题不代表上板后时序能收敛。如果工程跑到较高的采样率出现时序违规检查一下是不是把分频信号当成时钟接到了太多寄存器上。尽量用“系统时钟使能”的设计方式这样时序分析会好做很多。5. 个人体会与扩展方向5.1 做完这个项目后最大的收获把逻辑分析仪项目完整啃下来之后我最大的感受是以前用SignalTap或ILA时“填参数”的方式发生了根本变化。你再看到“采样深度”“触发位置”“预触发百分比”这些词脑子里会很自然地浮现出背后那套状态机和地址回卷逻辑。这种理解带来的直接好处是遇到复杂调试任务你能估算出应该设多大深度、多少个通道而不是靠猜。还有一点是代码组织能力的提升。这个项目体量不大但模块划分非常典型做完之后我再写其他的FPGA逻辑会下意识地先把模块边界画清楚再把接口定义好最后才写具体逻辑。这种习惯对后来做图像采集、PCIe接口移植这类复杂工程帮助很大。5.2 后续扩展方向如果学有余力这个项目可以往三个方向扩展。一是加通道数从8通道扩到16通道但要留意存储资源占用率可能需要换更大容量的芯片或者降低深度。二是把串口换成USB用FT2232这类桥接芯片或者直接在FPGA里挂USB PHY上传速度能提升一个数量级但这部分工作量不小需要参考对应的USB IP核文档。三是在FPGA端做协议解析比如对I2C或SPI数据帧解码之后只上传解析结果上位机就能变成简易协议分析仪。最后再分享一个小经验如果你想加深对触发逻辑的理解可以在现有基础上加一个“毛刺触发”功能也就是检测到信号在极短时间内出现多次跳变时就触发。这个功能在调试时钟毛刺或复位抖动时特别实用而且代码改动量不大本质上是在边沿检测基础上加一个时间窗口计数。把它做出来你对逻辑分析仪的理解又能上一个台阶。本文还有配套的精品资源点击获取