简介:基于FPGA的逻辑分析仪设计源码,面向数字电路与嵌入式方向的电子类学生、FPGA初学者及参加课程设计或电子竞赛的开发者。设计实现16个采样通道,可通过开关切换采样触发模式与采样频率,并借助VGA接口将采集到的逻辑时序实时显示在屏幕上,适用于波形观测、时序调试等应用场景。压缩包共106个文件,约1.71MB,以Verilog/VHDL源码(v、tdf)为核心,包含Quartus工程配置(qpf、qsf、qip)、IP核与符号文件(bsf)、初始化数据(mif)、时序与引脚约束(sdc、pin)、编译下载文件(sof、pof)以及仿真波形记录等,结构清晰,便于直接打开工程进行仿真、综合与上板验证。已有951人学习下载,可作为FPGA逻辑分析仪设计的完整参考资料,帮助读者理解多通道采样、触发控制及VGA显示的数据通路实现思路。
1. 基于FPGA的逻辑分析仪:为什么我要亲手造一台硬件示波器
调试一块跑飞的单片机板子,手上没有逻辑分析仪,只能用万用表一处一处量电平,一个UART波形要猜半天——这是我决定动手搭这个基于FPGA的逻辑分析仪的直接原因。这份源码把探针采集、触发、存储和波形显示整条链路做在了一起:被测信号通过排针引入FPGA引脚,经采样与触发判断后写入片上RAM,最终由VGA接口把8通道时序波形直接画在显示器上。你不用再依赖商用逻辑分析仪,手头有一块带VGA和排针的FPGA开发板就能复现,特别适合做单片机、嵌入式调试和FPGA内部信号抓取的开发者。下面从架构、核心Verilog代码到避坑与二次开发,按我实际拆过这个工程的顺序讲。
2. 系统架构与硬件选型:探针到VGA的三条主线
2.1 整体数据流:信号从引脚到屏幕经过哪些环节
拆工程我有个习惯:先不看代码,沿着数据流把模块画出来。这个基于FPGA的逻辑分析仪,内部可以切成六段:输入适配、同步采样、触发判定、波形存储、VGA显示、按键控制。
输入适配层最薄。被测数字信号经过板载排针进入FPGA IO,数字逻辑分析仪只关心电平的0/1,不需要像模拟示波器那样做幅度调理,通常只加串阻保护和静电防护。真正决定系统能不能稳定工作的是同步采样这层:外部信号相对内部采样时钟是异步的,必须先用采样时钟打两拍,把亚稳态消化掉,之后所有比较、触发、存储才有意义。
再往后是触发判定和波形存储。触发判定监视预设条件,比如某通道上升沿,满足时通知存储逻辑锁定写地址;波形存储用片上Block RAM,每个采样点把8个通道的电平打包成一个bit向量写入。最后两步是VGA显示和按键控制,显示侧把RAM内容还原成8条通道波形,按键侧负责切换触发方式、通道和时基。
两条主线在这里交汇:一条是采样时钟域,从探针输入一路到RAM写端口;另一条是像素时钟域,从VGA时序生成器一路到RAM读端口。两个时钟域通过双口RAM解耦,这也是我后来调试时反复确认的地方——只要RAM读写两侧的时钟和地址各自独立,显示就不会因为采样过程抖动而花屏。按键控制属于第三时钟域,频率低,用简单的消抖和边沿检测后跨到采样时钟域即可,不需要复杂握手。
为什么不直接用FIFO而要选双口RAM?我的判断是:FIFO擅长"边采边读",但逻辑分析仪的场景是"采完一整段、再慢慢读出来画",天然分段。双口RAM的随机读地址能力可以把触发点放到屏幕任意位置,显示侧想从哪个地址开始读都行。这份工程选双口RAM,我认为是对的设计。
2.2 板级资源分配:存储、时钟与VGA接口怎么配
这份源码没有绑定某一块具体开发板,但它充分利用了FPGA上三类通用资源——普通IO做探针输入、PLL产生多档采样时钟、Block RAM做波形存储。按中端FPGA的常见资源量估算:8通道、每通道8K采样点、每点1bit,总存储需求是 8×8192 = 65536 bit,约64Kb,对带几百Kb片上RAM的FPGA来说非常宽裕。如果把深度提到每通道32K,总量256Kb,主流器件也撑得住,所以完全不用外扩存储芯片。
存储的组织方式我拆出来看是这样:不是给每个通道分一块独立RAM,而是把8个通道的bit打包成一个8bit字、按采样时间顺序连续存放。这样做的收益是写侧只需一个写地址,读侧按地址读出8bit后按位分离画到不同通道行,地址和通道完全解耦;代价是想单独看某一通道时得把整段读出来再挑bit,但VGA逐行刷新天然扫全图,这个代价无所谓。
采样时钟是选型时最关键的参数。数字逻辑分析仪的性能直接由采样率决定:100MHz采样率能可靠分辨脉宽10ns以上的信号,抓UART、SPI、I2C、PWM常见总线绰绰有余;如果被测信号里有更窄的毛刺,就得把采样率提到200MHz甚至更高。100MHz和25MHz两路时钟都可以用PLL从板载50MHz晶振倍频分频得到,不需要额外的时钟芯片,这也是这套设计成本低的原因。
VGA显示侧用的是最省资源的电阻网络方案:RGB每通道3bit,把三路电阻按二进制权重接到VGA座,FPGA IO输出高/低电平决定对应位是否参与混色,组合出8种颜色。640×480@60Hz需要25MHz像素时钟。逻辑分析仪画波形只需要背景、网格、信号线三种颜色,3bit完全够用,所以全工程逻辑单元占用不到三千,属于轻量级设计。按键和LED用板载资源,两个按键分别管触发启动和触发模式/时基切换。
2.3 采样率、存储深度与时基换算
时基是逻辑分析仪上最容易算错账的地方,我先给结论:屏幕上横向一格代表的时间 = 每像素采样点数 ÷ 采样率。假设采样率100MHz、每像素显示2个采样点,一个像素就是20ns,一行640像素总共12.8μs;想看得更宽就增大每像素采样点数,想看得更细就减小。
存储深度决定"触发前能存多少、触发后还能存多少"。工程里常见的做法是把总深度切成预触发和后触发两段,比如8K深度配2K预触发加6K后触发:启动后先写,触发信号到来时地址已经走了2K,继续写满6K停止。这样屏幕上呈现的波形,触发点左边保留了一段上下文,右边能看到触发后的完整行为,排查问题时能同时看清"触发前发生了什么"和"触发后发生了什么"。
采样率的档位设置我一般建议用整数分频。PLL输出100MHz后,通过内部计数器分频出50MHz、25MHz、10MHz等档位,而不是动态改PLL参数。原因很简单:逻辑分析仪采集过程中PLL重配置会引起短暂失锁,采样时钟抖动一下,RAM里就会出现半个错误地址,整段波形错位。配套设计是换档位必须"先停采集、再切时钟、再重新触发",绝不能在采集进行中切换。
还有一个容易忽略的点:数字信号的可靠捕捉要求"信号最窄脉宽 ≥ 3个采样周期"。采样率100MHz时,一个采样周期10ns,3个周期30ns意味着脉宽小于30ns的毛刺可能被漏采。这不是bug,是采样原理决定的物理限制。真要抓更窄的信号,只能提高采样率。
3. 核心模块代码拆解:采样状态机、边沿触发与RAM显示联动
3.1 采样控制状态机:采集过程的四个阶段
整个逻辑分析仪的心脏是一个四状态状态机:IDLE(空闲)、ACQUIRE(等待触发)、POST_TRIG(触发后采集)、DONE(采集完成)。
module sample_fsm #( parameter CH_NUM = 8, parameter ADDR_W = 13 // 8K 深度,地址位宽 13 )( input wire clk, // 采样时钟,例如 100MHz input wire rst_n, input wire start, // 按下启动键 input wire trig_ok, // 触发条件已满足 output reg [ADDR_W-1:0] wr_addr, output reg wr_en, output wire done, output reg [1:0] state ); localparam IDLE = 2'd0; localparam ACQUIRE = 2'd1; localparam POST_TRIG = 2'd2; localparam DONE = 2'd3; reg [ADDR_W-1:0] cnt_post; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; wr_addr <= {ADDR_W{1'b0}}; wr_en <= 1'b0; cnt_post <= {ADDR_W{1'b0}}; end else begin case (state) IDLE: begin if (start) state <= ACQUIRE; end ACQUIRE: begin wr_en <= 1'b1; // 进触发前就开始写 if (trig_ok) begin state <= POST_TRIG; cnt_post <= {ADDR_W{1'b0}}; end end POST_TRIG: begin if (cnt_post == {ADDR_W{1'b1}}) begin state <= DONE; // 后触发深度采满 wr_en <= 1'b0; end else begin cnt_post <= cnt_post + 1'b1; end end DONE: begin wr_en <= 1'b0; if (start) state <= ACQUIRE; // 重新触发 end endcase if (wr_en) wr_addr <= wr_addr + 1'b1; end end assign done = (state == DONE); endmodule这段代码的逻辑说明:IDLE下按下启动键后进入ACQUIRE,此时wr_en拉高,RAM从地址0开始连续写,实现"预触发"效果。trig_ok到来后进入POST_TRIG,继续写直到cnt_post记满——这个计数器决定了触发后还想看多长波形,也就是后触发深度。wr_en为1时wr_addr每个时钟加1,保证RAM里存的是触发前后连续的一段采样。
参数说明:CH_NUM是探针通道数,这里取8;ADDR_W=13对应2^13=8192深度。这里的后触发深度是满量程,实际工程里会把总深度切成"预触发+后触发"两段,并在ACQUIRE阶段给wr_addr一个起始偏移,这部分留给读者按需求改造。DONE状态把wr_en拉低,RAM内容固定,显示侧可以安全读取。
3.2 边沿触发模块:两级同步与上升沿检测
触发模块最容易出问题的地方是异步信号处理。被测信号相对采样时钟是异步的,直接拿它做判断会引入亚稳态,通用的做法是先打两拍同步,再做边沿检测:
module edge_trigger #( parameter CH_NUM = 8 )( input wire clk, input wire rst_n, input wire [CH_NUM-1:0] probe_in, // 同步后的探针输入 input wire [CH_NUM-1:0] trig_ch, // 触发通道掩码,bit有效 input wire trig_mode, // 1:上升沿 0:下降沿 output reg trig_ok ); reg [CH_NUM-1:0] din_d1; reg [CH_NUM-1:0] din_d2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin din_d1 <= {CH_NUM{1'b0}}; din_d2 <= {CH_NUM{1'b0}}; end else begin din_d1 <= probe_in; din_d2 <= din_d1; end end wire rising = trig_ch & din_d1 & ~din_d2; // 上一拍0,这一拍1 wire falling = trig_ch & ~din_d1 & din_d2; // 上一拍1,这一拍0 always @(posedge clk or negedge rst_n) begin if (!rst_n) trig_ok <= 1'b0; else begin if (trig_mode) trig_ok <= |rising; else trig_ok <= |falling; end end endmodule逻辑说明:din_d1和din_d2是两级同步寄存器,probe_in先打两拍再参与运算,避免亚稳态传到触发逻辑。rising的计算方法是"上一拍是0、这一拍是1",用位与操作把选中通道过滤出来,只要任意选中的通道出现上升沿,trig_ok就拉高一个周期。trig_mode为1时抓上升沿,为0时抓下降沿,通过按键切换。
参数说明:trig_ch是按位选择的通道掩码,只触发第0通道就传8'b0000_0001。这里的trig_ok是一个周期脉冲,采样状态机在同一个时钟域里直接消费它,不需要再做跨时钟域处理。要注意的是:两级同步会让触发判定比真实信号晚两个采样周期,这对波形显示来说无感,但如果要做精确时间测量,得在软件层把这个固定延迟扣掉,这是第5章避坑部分会展开讲的一条。
3.3 波形存储与VGA显示:双口RAM的读写仲裁
存储侧的核心是一个简单的双口RAM(simple dual-port RAM),写端口接采样状态机,读端口接VGA显示逻辑。工程里用厂商IP或纯手写寄存器的都有,重点是读写两个时钟域互不干扰:
// 双口RAM例化:写口跑采样时钟,读口跑25MHz像素时钟 wire [ADDR_W-1:0] rd_addr; wire [CH_NUM-1:0] rd_data; dpram #( .DATA_W (CH_NUM), // 每个地址存8个通道的bit .ADDR_W (ADDR_W) // 13位地址 = 8192深度 ) u_dpram ( .wr_clk (clk_samp), .wr_en (wr_en), .wr_addr (wr_addr), .wr_data (probe_in), .rd_clk (clk_pix), .rd_en (1'b1), .rd_addr (rd_addr), .rd_data (rd_data) );逻辑说明:写数据直接放同步后的probe_in,每个时钟一个采样点写进一个地址;读侧由VGA时序生成器产生rd_addr,每个像素时钟读出一个地址的数据。因为是双口RAM,读写使用各自独立的时钟和地址,彼此不用握手。写侧什么时候停由采样状态机的DONE状态决定,DONE之后读侧无论怎么扫,读到的都是稳定的波形数据,花屏问题的根源在这里就被掐掉了。
VGA时序生成部分,640×480@60Hz的标准时序参数如下:
module vga_timing #( parameter H_ACTIVE = 640, parameter H_FRONT = 16, parameter H_SYNC = 96, parameter H_BACK = 48, parameter V_ACTIVE = 480, parameter V_FRONT = 10, parameter V_SYNC = 2, parameter V_BACK = 33 )( input wire clk_pix, input wire rst_n, output reg hsync, output reg vsync, output reg [9:0] h_cnt, output reg [9:0] v_cnt, output reg active_en ); always @(posedge clk_pix or negedge rst_n) begin if (!rst_n) begin h_cnt <= 0; v_cnt <= 0; hsync <= 1; vsync <= 1; end else begin if (h_cnt < H_ACTIVE + H_FRONT + H_SYNC + H_BACK - 1) h_cnt <= h_cnt + 1; else begin h_cnt <= 0; if (v_cnt < V_ACTIVE + V_FRONT + V_SYNC + V_BACK - 1) v_cnt <= v_cnt + 1; else v_cnt <= 0; end hsync <= (h_cnt >= H_ACTIVE + H_FRONT) && (h_cnt < H_ACTIVE + H_FRONT + H_SYNC); vsync <= (v_cnt >= V_ACTIVE + V_FRONT) && (v_cnt < V_ACTIVE + V_FRONT + V_SYNC); active_en <= (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE); end end endmodule逻辑说明:h_cnt和v_cnt是行/场计数器,hsync、vsync在消隐区间拉低,active_en在有效显示区间拉高。这套参数是VGA 640×480@60Hz的标准值,直接照抄就能点亮绝大多数显示器。显示器对时序有一定容忍度,hsync/vsync的极性和前后肩参数差几个像素一般也能显示,但把H_ACTIVE改大超出显示器能力,就会出现画面偏移或黑边,改参数时要小心。
波形映射规则工程里是这样做的:每一行显示一个通道的波形,8个通道占用纵向480行中的8个水平带;横向h_cnt对应时间轴,把rd_addr从0到8191映射到640个像素上,每像素约12.8个采样点;当前像素对应地址的rd_data某一位为1就画亮线,为0画暗线,通道之间用网格线隔开。这个映射逻辑放在显示模块里,和VGA时序生成器解耦,想改成16通道就多画几个水平带,属于纯显示侧改动。
3.4 顶层模块与引脚约束:把模块粘起来
顶层把采样、触发、存储、显示四个部分例化在一起,并通过按键输入把控制参数分发下去。这里只贴关键例化与端口连接逻辑:
module logic_analyzer_top #( parameter CH_NUM = 8, parameter ADDR_W = 13 )( input wire clk_50m, // 板载时钟 input wire rst_n, input wire [CH_NUM-1:0] probe_in, // 探针输入 input wire [1:0] key, // 按键:触发/时基 output wire vga_hsync, output wire vga_vsync, output wire [2:0] vga_rgb ); wire clk_samp, clk_pix; wire [CH_NUM-1:0] probe_sync; wire trig_ok, wr_en, done; wire [ADDR_W-1:0] wr_addr, rd_addr; wire [CH_NUM-1:0] rd_data; pll_clk u_pll( .clk_in (clk_50m), .clk_samp(clk_samp), // 100MHz 采样时钟 .clk_pix (clk_pix) // 25MHz 像素时钟 ); // 输入同步打拍 always @(posedge clk_samp) begin probe_sync <= probe_in; end sample_fsm #(.CH_NUM(CH_NUM), .ADDR_W(ADDR_W)) u_fsm ( .clk(clk_samp), .rst_n(rst_n), .start(key[0]), .trig_ok(trig_ok), .wr_addr(wr_addr), .wr_en(wr_en) ); edge_trigger #(.CH_NUM(CH_NUM)) u_trig ( .clk(clk_samp), .rst_n(rst_n), .probe_in(probe_sync), .trig_ch(8'b0000_0001), .trig_mode(key[1]), .trig_ok(trig_ok) ); // 双口RAM、VGA时序、显示映射例化省略,思路见3.3节 endmodule引脚约束在工程约束文件里完成。核心约束分两类:一类是时钟引脚,必须绑到板载晶振对应的专用时钟引脚,否则PLL无法工作;另一类是探针输入,绑到扩展排针所在bank,IO电平标准要和排针供电电压一致,常见LVCMOS33或LVCMOS25。约束文件里如果bank电压是3.3V却写LVCMOS25,下载后IO行为会不正常,这是下板调试的第一坑。
4. 从工程导入到屏幕出波形:完整跑通流程与校准
4.1 工程目录与文件组织
源码的目录结构保持得很克制,我按下面的顺序读:
| 目录 | 内容 | 作用 |
|---|---|---|
| rtl/ | 各模块Verilog源文件 | 采样、触发、RAM、VGA、顶层 |
| sim/ | 仿真testbench与波形脚本 | 综合前做功能验证 |
| constraints/ | 引脚与时钟约束文件 | 绑定IO位置、电平标准、时钟周期 |
| doc/ | 设计文档与接线说明 | 排针定义、按键功能、VGA接口定义 |
| script/ | 自动化脚本 | 一键综合、布局布线、生成比特流 |
doc目录的接线说明建议第一个读,它定义了CH0到CH7对应排针哪几个引脚、按键各自的功能、VGA座的颜色定义。这份文档和约束文件是配套的,焊线或接杜邦线之前先对一遍,能省掉后面一大半排查时间。rtl目录里是全部Verilog源文件,文件命名和模块名一一对应:sample_fsm、edge_trigger、dpram、vga_timing、top,没有把多个模块塞在一个文件里的坏习惯,二次开发时容易定位。
sim目录放着testbench,我拆工程时先跑了一遍功能仿真:在仿真工具里把tb_top设为顶层,跑几百微秒就能看到触发、写RAM、读RAM的完整时序。下面这个testbench片段展示了怎么构造一个带跳变的被测信号:
// sim/tb_top.v —— 给采样状态机喂一个可预期的上升沿 initial begin clk_50m = 1'b0; forever #10 clk_50m = ~clk_50m; // 50MHz 板载时钟 end initial begin rst_n = 1'b0; probe_in = 8'h00; #100 rst_n = 1'b1; // 复位释放 #50; probe_in = 8'h01; // CH0 出现上升沿,应触发 #100; probe_in = 8'h00; // 拉低,制造下降沿 #500; if (u_top.u_fsm.state == 2'd3) $display("PASS: capture done"); else $display("FAIL: state = %0d", u_top.u_fsm.state); $finish; end这段测试的意思是:复位释放后,先把probe_in保持低电平,再拉高一个周期产生上升沿,观察状态机是否按预期进入POST_TRIG并最终停在DONE。tb里通过层次化引用 u_top.u_fsm.state 直接看内部状态,仿真波形里能同时看到wr_addr持续递增、wr_en在DONE拉低。仿真通过后再走下板流程,能省掉大量烧录-看屏-再烧录的循环。
4.2 导入综合布局布线到烧录
跑通整条流程需要一块带VGA、排针、按键和板载时钟的开发板,以及对应的FPGA厂商IDE工具。步骤是固定的:
第一步,新建工程,把rtl目录下所有.v文件加进去,顶层设为logic_analyzer_top。第二步,把constraints目录下的约束文件加入工程,重点核对clk_50m绑定的是板载晶振专用时钟引脚,probe_in的8个引脚与doc里的排针定义一致、IO电平标准与排针bank电压一致。第三步,启动综合和布局布线,正常情况逻辑单元占用不超过3%、BRAM占用10%左右;如果资源用量异常高,多半是某个模块被综合成了大ROM或RAM位宽配错,回到3.3节核对参数。
第四步,生成比特流下载到FPGA。下载成功先别接被测信号,把板载的一个已知时钟(比如时钟输出引脚)接到CH0,按启动键,VGA上应出现稳定方波,说明采样-存储-显示主链路是通的。第五步接真实信号,开始4.3节的校准。
注意:VGA线务必在断电状态下插拔。我因为这个翻车烧坏过一个开发板的VGA座,热插拔瞬间的电流冲击对板卡电平转换电路不友好,后来全改成上电前接好线。
4.3 用已知信号校准与交叉验证
逻辑分析仪看到的波形对不对,最好先拿已知信号校一遍,而不是直接信屏上的结果。我常用的两个信号源:一是板载时钟分频产生的方波,频率已知且稳定;二是用单片机以9600波特率发送0x55,理想波形是下降沿起始位、8个数据位交替01010101、高电平停止位。
校准步骤:把已知信号接到CH0,时基调到能容纳一个完整UART字节的位置,触发方式选下降沿(起始位是下降沿),触发通道选CH0。触发后屏幕应精确显示一个字节的时序,数据位宽度均匀、起始位低电平、停止位高电平。如果位宽不均匀,先怀疑时基档位设置;如果波形整体偏移,查触发深度配置;如果完全没波形,回到5.2节查触发通道位序。
交叉验证我用厂商IDE自带的在线调试核,把wr_en、wr_addr、trig_ok这些内部信号挂进去,和VGA显示结果对比。两边一致说明显示映射没有bug;不一致就一定存在地址或bit顺序错位。这一步特别值得做,因为VGA显示链路一旦错位,屏幕上呈现的波形会"看起来合理但实际是错位的",肉眼很难发现,只有和内部真实信号对比才能暴露。
5. 避坑指南:逻辑分析仪调试中我踩过的5个问题
5.1 波形毛刺:干净信号在屏上出现窄脉冲
现象:输入是干净的方波,VGA屏幕上波形在跳变沿附近出现一两个像素宽的窄脉冲,位置不固定,重新触发后毛刺位置还会变化。
原因:probe_in是异步信号,采样点落在跳变沿附近时,同一信号在不同采样周期可能采到0或1,产生虚假跳变。另一个隐藏原因是显示侧rd_addr没有按像素单调递增,地址来回跳导致重读旧数据,也会表现为沿附近的杂散亮线。
解决:先查显示侧,确认rd_addr每像素递增;再处理异步问题,给probe_in到同步寄存器之间的路径设置false path时序约束,内部打两拍后再参与触发判断。两处都改完后毛刺消失。排查顺序建议先显示后同步,因为显示映射问题改起来快,能排除一大半干扰。
5.2 触发不生效:条件满足却不触发
现象:触发通道、极性都设置正确,信号也确实有上升沿,但按启动键后VGA一直停在等待触发状态。
原因:最常见的两个。一是触发通道掩码写错,trig_ch里没把目标通道位置1;二是极性搞反,trig_mode=0抓下降沿,拿上升沿信号去触发自然永远不触发。还有一种情况是通道位序反了,CH0实际对应的是最高位而不是bit0,触发掩码对应错位。
解决:把trig_ch先设成单一通道、触发模式轮着切换验证;对照doc里通道编号和probe_in位序,确认CH0到底对应bit0还是bit7。这个顺序在接线文档里会写,但不同工程定义可能相反,务必先核对再下结论。触发通道和触发方式每次只改一个变量,不要同时调两个。
5.3 VGA画面撕裂或错位
现象:采集完成后,VGA画面能显示,但波形在水平方向周期性错位,或者整个画面上下滚动。
原因:读地址和写地址没有对齐。DONE之后wr_en拉低,如果读侧从地址0开始读,而触发点数据实际在地址5000附近,屏幕上看到的就是触发后的一部分和触发前的旧数据混在一起。滚动则是hsync/vsync时序不稳定或PLL输出时钟抖动引起的。
解决:DONE之后把rd_addr复位到固定起始位置,或者更省事的做法是读侧从0开始读整段RAM,把"触发点固定在起始地址"设计成不变式。显示错位先关掉所有动态时基切换,确认是滚动还是错位:滚动检查PLL输出时钟是否稳定,错位检查地址复位逻辑。这个问题的根因通常是采样状态机和显示逻辑对RAM内容的"归属权"没定清楚,谁负责在DONE后把读指针归位,代码里要写死。
5.4 时基与实际时间对不上
现象:给CH0输入1kHz方波,屏上量出来一个周期占了300个像素,按当前时基换算应该只占100像素。
原因:时基参数算错或每像素采样点数填错。逻辑分析仪的时基定义是"每像素对应多少采样点",不是"每像素对应多少秒"。如果采样率100MHz、每像素采样点数设成8,那每像素80ns,1kHz的周期1ms应占12500像素,远超屏幕宽度,看到的根本不是完整周期;占300像素说明换算时把采样率当成了其他数值。
解决:统一用公式"像素对应时间 = 每像素采样点数 ÷ 当前采样率"。改时基时不直接改采样率,而是改每像素采样点数,让采样率保持整数分频,避免PLL重配置导致采集中断。这个换算关系我建议直接写进doc,每次调完设置后用已知信号量一遍,而不是靠肉眼估。
5.5 下载后IO无反应或电平错乱
现象:比特流下载成功,按键和LED正常,但probe_in读到的电平始终为0,或者整体反相。
原因:引脚约束和板子硬件对不上。排针所在bank的IO电压是3.3V,约束里却写了LVCMOS25;或者排针某个引脚被板载外设复用,默认被驱动成固定电平,探针信号根本进不到FPGA内部。
解决:对着开发板原理图核对排针bank电压、复用功能、默认上下拉。约束文件里把probe_in全部显式声明IO标准,不要依赖默认值;检查排针附近是否有跳线帽切换复用。整体反相的情况多半是板子上接了反相器或三极管做电平转换,优先查硬件连接而不是改代码。
6. 二次开发思路:协议触发、模拟通道与数据导出
6.1 加协议触发:从边沿触发到帧触发
基础版只有边沿触发,抓UART时只能等到起始位下降沿,看不到完整字节前后的内容。我建议第一步把触发升级成协议帧触发:加一个UART接收状态机,检测到起始位下降沿后再确认数据位序列,命中特定字节(比如0x55或0xAA)才拉高trig_ok。改动点在edge_trigger模块外面包一层协议解析,采样时钟改成波特率的16倍频,状态机用空闲、起始、数据、停止四个状态,其余采样和显示逻辑不用动。
6.2 扩展模拟通道:加外部ADC看模拟信号
如果被测对象不只有数字电平,还想看PWM的模拟平均值或传感器输出曲线,可以在探针输入前面加一个并口ADC芯片,FPGA用采样时钟直接读ADC转换结果。改法是把probe_in从1bit扩展成8bit或10bit数据总线,RAM的DATA_W相应加宽,VGA显示从"画线"改成"按数值画柱状图或连线"。注意ADC的转换延迟和采样时钟的时序关系,查参考手册的转换时间参数,在状态机里加一拍延迟对齐。
6.3 波形导出:UART把采样数据送电脑
VGA只适合实时观察,想留档或做进一步分析,就加一个UART发送模块,DONE之后把RAM里的数据按地址顺序读出,通过串口发给PC。PC端用几十行Python脚本就能恢复波形:
import serial baud = 2000000 ser = serial.Serial('COM3', baud, timeout=1) depth = 8192 ch_data = [0] * depth for i in range(depth): frame = ser.read(2) # 每帧:1字节数据 + 1字节校验 ch_data[i] = frame[0] # ch_data 里每个bit对应一个通道,按通道切片画图即可发数据时要注意速度匹配:2Mbps波特率下,8192字节约需33ms,VGA侧保持显示上一帧,等新数据收完再刷新,避免边发边刷导致画面跳变。串口模块用独立的发送时钟域,和显示侧的RAM读取做互斥,读地址在DONE之后由UART模块接管。
这份工程的rtl、sim、constraints、doc都整理在下载包里,拿到后先读doc里的接线说明,再按第四章的流程走一遍,半小时内应该能看到第一条触发波形。每次改完模块,我现在的习惯都是先跑一遍sim下的testbench,再走一遍4.3节的已知信号校准,这两步花不了十分钟,但能挡住九成以上的低级错误。从那以后,我每改一版逻辑分析仪源码,都强制走一遍"仿真-校准-交叉验证"三件套,不再相信一次综合直接出波形这种玄学。希望帮到你。
本文还有配套的精品资源,点击获取