做FPGA数据采集,AD7606应该算得上是最容易起步的ADC之一了。8通道同步采样、16位精度、双极性输入,输出侧又同时给了并行和SPI两条路,所以很多采集方案里都能看到它。说实话,这颗芯片的驱动本身不难写,难的是很多人第一次接触就被“两种读取模式”搞晕了:并行是DB[15:0]上加CS/RD直接读,SPI则是靠SCLK把数据一位位吐出来,到底什么场合用哪套?Verilog代码分别怎么写才能保证采样率不掉?这篇文章就把我验证过的AD7606驱动拆开讲,覆盖并行模式读取和SPI模式读取两套代码的状态机设计、时序要点、仿真模型和调试经验。如果你正在做电力数据采集、振动监测、医疗设备或者任何需要多通道同步采样的FPGA项目,这套思路可以直接抄。
1. AD7606为什么值得单独写一套FPGA驱动
1.1 芯片接口不是二选一,而是同一组引脚的不同用途
很多第一次接触AD7606的同学,会误以为这芯片有两种封装或者两种型号,一种带并行口,一种带SPI口。其实不是。芯片内部还是同一个核,8个采样保持器加一个16位SAR ADC,转换完成后数据锁存在输出寄存器里。区别只在外部引脚怎么复用。
并行模式下,DB[15:0]这16根线直接输出转换结果,每拉低一次RD,总线上就出现一个通道的数据,读8个通道就是8个RD周期。SPI模式下,这组引脚被重新分配,一部分变成DOUTA、DOUTB串行数据线,SCLK负责把数据移位出来。控制位PAR/SER决定了走哪条路,1是并行,0是串行。所以写驱动之前,第一件事不是打开代码编辑器,而是先确认板子上这组引脚接了哪里、PAR/SER被固定到什么电平。
还有几个引脚是两种模式共用且必须配置的:OS[2:0]决定过采样倍率,一般是000关闭过采样;RANGE决定模拟输入量程,逻辑0是±5V,逻辑1是±10V;RESET用于复位。这些引脚在驱动里虽然大多数时间不需要动态操作,但必须在初始化阶段赋一个确定值,绝对不能悬空。悬空状态下芯片可能在上电瞬间被噪声拉到错误状态,采样数据就会整体异常。
1.2 并行模式与SPI模式的实际选型建议
选型不是越高级越好,而是看你的约束条件。我列一个实际的对比表:
| 对比项 | 并行模式 | SPI模式 |
|---|---|---|
| 数据引脚数量 | 16根(DB[15:0]) | 2根(DOUTA+DOUTB) |
| 必备控制线 | CS、RD、BUSY、CONVST | CS、SCLK、BUSY、CONVST |
| 读8通道所需SCLK/RD次数 | 8次RD脉冲 | 约64或128个SCLK周期 |
| 对FPGA引脚占用 | 高,约20个IO | 低,约5个IO |
| 数据读取速度 | 快,一次RD约几十ns | 受SCLK频率限制 |
| 隔离方案成本 | 数字隔离器数量多 | 隔离通道少,成本低 |
| FPGA内部逻辑开销 | 状态机简单,拼装数据不难 | 需要移位寄存器和位计数 |
一般建议是这样的:如果FPGA引脚不紧张,优先并行模式,因为状态机直观、调试方便、速度上限高。如果系统里FPGA同时要接DDR、PCIe或者对外接口,IO已经排得非常满,那就上SPI模式,省下的十几根脚非常宝贵。还有一种常见场景是模拟前端需要隔离,SPI模式只需要隔离两三条信号线,隔离芯片的成本和面积都能降下来。
另外提醒一句:SPI模式下如果只接DOUTA一根线,有些接法只能拿到部分通道,要读满8个通道通常需要把DOUTA和DOUTB同时接进FPGA,下面第4部分我会按双线同时采样的方式写驱动。
2. 并行读取模式:四个信号的节奏和状态机
2.1 一次采样的完整信号关系
并行模式下,最关键的就是CONVST、BUSY、CS、RD这四个信号。很多人把它想复杂了,其实节奏非常固定。
先把CONVST拉低,保持一段时间(手册典型要求至少25ns),然后拉高。CONVST的上升沿就是转换启动命令,芯片收到这个上升沿后,把8个通道的模拟输入同时采样并开始转换。转换期间BUSY信号输出高电平,转换完成后BUSY拉低。注意,BUSY下降沿才是“数据已经锁存到输出寄存器”的标志,从这个时刻开始才允许做读取操作。
用Verilog描述时,启动一次采集的流程是:
IDLE -> 拉低CONVST -> 拉高CONVST -> 等待BUSY变高 -> 等待BUSY变低 -> 进入读数据阶段为什么要把“等待BUSY变高”单独设计一个状态?因为CONVST拉高之后,BUSY不会立刻变高,中间有很短的时间差,直接跳去等BUSY变低没问题,但如果用BUSY边沿做触发,必须先把BUSY同步过两级寄存器。这也是初学者最容易漏掉的地方:AD7606的BUSY是异步信号,跟FPGA主时钟没有任何相位关系,直接拿它做组合逻辑判断,很容易出现亚稳态。
2.2 读八通道数据时的CS/RD循环
BUSY变低之后,芯片已经准备好8个通道的结果。并行模式下读取方式很直接:先拉低CS使能输出,然后每给一个RD低脉冲,DB总线上就换一个通道的数据。通道顺序是固定的,从V1到V8,不存在地址线,也不需要额外指定通道号。
每次RD周期的典型做法是:
- 拉低CS;
- 拉低RD;
- 等待数据建立;
- 在RD下降沿之后的某个稳定点,把DB[15:0]锁存进FPGA;
- 拉高RD;
- 重复第2到第5步,直到8个通道全部读完;
- 拉高CS,结束本次读操作。
这里最关键的时序参数,是RD下降沿到数据真正有效的间隔,手册里一般叫t9,典型值是几十纳秒级别。FPGA采样点必须落在数据有效窗口内。说白了就是:RD刚拉低时,DB总线上还不是当前通道的稳定数据,你立刻去采,采到的可能是上一通道的尾巴或者中间跳变状态。实际项目里我一般会让RD低电平保持2到3个系统时钟周期,再把采样点放在第2个或第3个时钟沿。以50MHz主时钟为例,一个周期20ns,RD低电平保持40~60ns,数据完全来得及建立。
2.3 状态机如何把这些时序组织起来
状态机设计的大致思路是这样的:
- IDLE:空闲,等待外部启动信号start;
- CONVST低电平状态:把ad_convst拉低;
- CONVST高电平状态:拉高ad_convst,启动转换;
- 等待BUSY变高状态:转换已经开始;
- 等待BUSY变低状态:转换结束,可以读取;
- 读数据循环状态:拉低CS和RD,延时后锁存数据,拉高,切换通道;
- 完成状态:输出done信号,回到IDLE。
读取循环里要有一个通道计数器。每读一次计数器加一,读到第7个(也就是V8)后,说明8个通道都拿到了,拉高CS并进入完成状态。计数器既用于控制循环次数,也用于决定当前锁存的数据应该存进ch1还是ch8。
有一个细节值得强调:复位或者初始化时,CS、RD、CONVST这些输出引脚必须设成默认高电平。AD7606的控制信号大多数是低电平有效,上电如果引脚悬空或者被拉低,芯片会进入奇怪的状态。Verilog里做复位逻辑时,把ad_cs、ad_rd、ad_convst全部置1,把ad_par_ser按模式固定,这样最稳妥。
3. 并行模式Verilog驱动代码:可复用的模块框架
3.1 端口定义和关键参数
并行驱动的顶层模块,我一般写成下面这样。参数部分把等待周期做成可配置的,方便在不同主频下复用:
module ad7606_parallel_drv #( parameter CLK_FREQ = 50_000_000, parameter RD_WAIT_CYC = 3 )( input wire clk, input wire rst_n, input wire start, // 启动一次采样,高有效 output reg done, // 本次采样完成标志 output reg [15:0] ch1, ch2, ch3, ch4, output reg [15:0] ch5, ch6, ch7, ch8, // AD7606物理引脚接口 output reg ad_convst, output reg ad_cs, output reg ad_rd, output reg ad_par_ser, // 1:并行模式 0:串行模式 output reg ad_bytsel, // 0:16位数据模式 output wire ad_os0, ad_os1, ad_os2, output wire ad_range, // 0:±5V 1:±10V input wire ad_busy, input wire [15:0] ad_db ); assign ad_os0 = 1'b0; assign ad_os1 = 1'b0; assign ad_os2 = 1'b0; assign ad_range = 1'b1; // 根据量程需求修改RD_WAIT_CYC就是RD低电平后等待采样的时钟周期数。系统主频越高,需要的等待周期越多;系统主频越低,等待周期可以减少。用一个参数把它显式暴露出来,换平台时只改参数,不用动状态机逻辑。
3.2 状态机与数据锁存
核心状态机的代码如下。为了阅读方便,我省略了每个分支里所有寄存器的完整赋值列表,只保留了状态跳转的骨架,实际使用时要补上对应输出信号的默认赋初值逻辑:
localparam S_IDLE = 5'd0; localparam S_CONVST_LOW = 5'd1; localparam S_CONVST_HIGH = 5'd2; localparam S_WAIT_BUSY_H = 5'd3; localparam S_WAIT_BUSY_L = 5'd4; localparam S_RD_SET = 5'd5; localparam S_RD_WAIT = 5'd6; localparam S_RD_CAPTURE = 5'd7; localparam S_RD_HIGH = 5'd8; localparam S_FINISH = 5'd9; reg [4:0] state; reg [3:0] rd_cnt; reg [1:0] busy_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; ad_convst <= 1'b1; ad_cs <= 1'b1; ad_rd <= 1'b1; done <= 1'b0; rd_cnt <= 4'd0; busy_sync <= 2'b00; end else begin busy_sync <= {busy_sync[0], ad_busy}; // 两级同步 case (state) S_IDLE: begin done <= 1'b0; if (start) state <= S_CONVST_LOW; end S_CONVST_LOW: begin ad_convst <= 1'b0; state <= S_CONVST_HIGH; end S_CONVST_HIGH: begin ad_convst <= 1'b1; state <= S_WAIT_BUSY_H; end S_WAIT_BUSY_H: begin if (busy_sync[1]) state <= S_WAIT_BUSY_L; end S_WAIT_BUSY_L: begin rd_cnt <= 4'd0; if (!busy_sync[1]) state <= S_RD_SET; end S_RD_SET: begin ad_cs <= 1'b0; ad_rd <= 1'b0; state <= S_RD_WAIT; end S_RD_WAIT: begin state <= S_RD_CAPTURE; end S_RD_CAPTURE: begin case (rd_cnt) 4'd0: ch1 <= ad_db; 4'd1: ch2 <= ad_db; 4'd2: ch3 <= ad_db; 4'd3: ch4 <= ad_db; 4'd4: ch5 <= ad_db; 4'd5: ch6 <= ad_db; 4'd6: ch7 <= ad_db; 4'd7: ch8 <= ad_db; endcase state <= S_RD_HIGH; end S_RD_HIGH: begin ad_rd <= 1'b1; ad_cs <= 1'b1; if (rd_cnt == 4'd7) state <= S_FINISH; else begin rd_cnt <= rd_cnt + 1'b1; state <= S_RD_SET; end end S_FINISH: begin done <= 1'b1; state <= S_IDLE; end default: state <= S_IDLE; endcase end end这里S_RD_WAIT只给了一个周期,主要是状态机骨架。如果你用50MHz时钟,我会把S_RD_WAIT扩展成3个周期,办法是加一个RF_WAIT_CYC计数器,或者直接连续排三个等状态。采样点放在最后一个等状态上,确保信号已经过了t9稳定区间。这种细节其实就是并行驱动比SPI驱动好调试的地方——逻辑非常直白,问题几乎只出在等待周期不够长。
3.3 接入FIFO或者AXI的扩展方法
上面的模块只是把数据存进ch1到ch8这8个寄存器。真实项目里一般不会直接读寄存器,而是希望转换完成后自动把数据写进FIFO,或者通过AXI-Lite让CPU来取。
如果接FIFO,做法是把done信号当作写使能,把{ch1,ch2,...}打包成一个128位数据写进FIFO;Zynq软核那边再按固定偏移拆出8个通道。如果接AXI-Stream,则可以在S_FINISH状态里发起一次握手,把8个通道组成两拍AXI数据发出去。这样采样线程和消费线程之间就有了缓冲,不会因为CPU处理慢丢数据。
还有一个可选的改进:把独立寄存器改成内部数组reg [15:0] ch_data [0:7],配合一个写地址计数,代码会更紧凑,也更方便改成任意通道数。不过Xilinx Vivado默认支持SystemVerilog,用数组端口没问题;如果PCB上已经有固定的ch1到ch8输出端口需求,那还是拆开写最直观。
4. SPI模式读取:DOUTA/DOUTB、SCLK边沿和状态机
4.1 串行模式下数据是怎么排列的
SPI模式下,AD7606的引脚分配会变化。并行总线DB[15:0]中的一部分会被复用成串行数据线,典型接法是DOUTA负责输出前4个通道,DOUTB负责输出后4个通道,两个输出共用SCLK。也就是说,每个SCLK周期同时从两根线上各移出一位,16个SCLK周期后,你就同时得到了两个通道的完整16位数据。这样读满8个通道只需要64个SCLK周期,而不是一通道16位乘以8那么夸张。
如果你的板子上只连接了DOUTA一根串行线,那一次转换只能拿到4个通道的数据,需要换一种数据帧格式才能把8个通道都串出来。但标准连接和大多数例程采用的是双线同时读取,硬要单线只会让驱动白白变慢。
这里强调一下:不同批次的AD7606、AD7606B、AD7606C在串行输出帧结构上可能有差异,写代码之前一定要对着自己手上芯片型号的数据手册看时序图。本文的代码是按“DOUTA输出前4通道、DOUTB输出后4通道、共用SCLK”的常规接法写的,如果你的芯片手册画的是单线串行8通道输出,就把采样输入的dout_b直接置0,代码逻辑同样适用。
4.2 从CS到SCLK的时序细节
SPI模式的时序比并行稍微绕一点,主要是SCLK和数据边的配合。
CS拉低后,不能立刻开始打SCLK。手册里有一个叫静默时间的参数,要求CS下降沿到第一个SCLK下降沿之间留出足够时间,典型值是几百纳秒。这个时间是用来等芯片内部输出电路准备数据的。BUSY下降后也是这样,不要刚看到BUSY变低就立刻拉CS,稍微让状态机在中间转一圈,再拉CS,再等一段时间,最后才开始出SCLK。
SCLK的空闲电平是高的。数据在SCLK下降沿从芯片移出,FPGA应该在SCLK上升沿采样。简单理解就是:产生一个下降沿,让芯片把数据放到DOUT线上;等到线上的数据稳定了,再产生上升沿,在上升沿这个点把数据采回来。如果用50MHz主时钟,我一般做4分频,SCLK大概是12.5MHz,每半个周期两个主时钟周期,一个等待电平稳定,一个用来采样,非常稳。
4.3 SPI驱动核心代码
SPI驱动的顶层模块,端口上除了控制信号之外,多了SCLK、dout_a、dout_b三根线:
module ad7606_spi_drv #( parameter CLK_FREQ = 50_000_000, parameter SCLK_DIV = 4 )( input wire clk, input wire rst_n, input wire start, output reg done, output reg [15:0] ch1, ch2, ch3, ch4, output reg [15:0] ch5, ch6, ch7, ch8, output reg ad_convst, input wire ad_busy, output reg ad_cs, output reg sclk, input wire dout_a, input wire dout_b );状态机前面一半跟并行模式一样,都是触发转换、等BUSY上升下降,后面则进入串行读出阶段。我一般用一个位计数器bit_cnt和一个通道计数器ch_cnt。每产生一个SCLK周期,在上升沿处采样dout_a和dout_b,移入两个16位移位寄存器;当bit_cnt计到15时,说明一个通道读完了,把两个移位寄存器的值锁存到对应的输出通道寄存器里,然后ch_cnt加一,bit_cnt清零。
核心片段如下:
localparam S_IDLE = 5'd0; localparam S_CONV_LOW = 5'd1; localparam S_CONV_HIGH = 5'd2; localparam S_WAIT_BUSY_H = 5'd3; localparam S_WAIT_BUSY_L = 5'd4; localparam S_CS_SET = 5'd5; // CS拉低并等待静默时间 localparam S_SCLK_LOW = 5'd6; // SCLK拉低,芯片移出数据 localparam S_HOLD = 5'd7; // 等待数据稳定 localparam S_SCLK_HIGH = 5'd8; // SCLK拉高,采样边沿 localparam S_FINISH = 5'd9; reg [4:0] state; reg [7:0] wait_cnt; reg [3:0] bit_cnt; reg [2:0] ch_cnt; reg [15:0] shift_a, shift_b; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; sclk <= 1'b1; // 空闲高 ad_cs <= 1'b1; ad_convst <= 1'b1; shift_a <= 16'd0; shift_b <= 16'd0; bit_cnt <= 4'd0; ch_cnt <= 3'd0; done <= 1'b0; end else begin case (state) S_IDLE: begin done <= 1'b0; if (start) state <= S_CONV_LOW; end S_CONV_LOW: begin ad_convst <= 1'b0; state <= S_CONV_HIGH; end S_CONV_HIGH: begin ad_convst <= 1'b1; state <= S_WAIT_BUSY_H; end S_WAIT_BUSY_H: begin if (busy_sync[1]) state <= S_WAIT_BUSY_L; end S_WAIT_BUSY_L: begin if (!busy_sync[1]) begin ad_cs <= 1'b0; state <= S_CS_SET; end end S_CS_SET: begin // 静默等待,让芯片准备好输出 if (wait_cnt == 8'd20) begin wait_cnt <= 8'd0; state <= S_SCLK_LOW; end else wait_cnt <= wait_cnt + 1'b1; end S_SCLK_LOW: begin sclk <= 1'b0; // 下降沿,芯片移出下一位 state <= S_HOLD; end S_HOLD: begin // 等数据稳定 state <= S_SCLK_HIGH; end S_SCLK_HIGH: begin sclk <= 1'b1; // 上升沿,此时数据稳定 shift_a <= {shift_a[14:0], dout_a}; shift_b <= {shift_b[14:0], dout_b}; if (bit_cnt == 4'd15) begin bit_cnt <= 4'd0; case (ch_cnt) 3'd0: begin ch1 <= shift_a; ch5 <= shift_b; end 3'd1: begin ch2 <= shift_a; ch6 <= shift_b; end 3'd2: begin ch3 <= shift_a; ch7 <= shift_b; end 3'd3: begin ch4 <= shift_a; ch8 <= shift_b; end endcase if (ch_cnt == 3'd3) begin state <= S_FINISH; end else begin ch_cnt <= ch_cnt + 1'b1; state <= S_SCLK_LOW; end end else begin bit_cnt <= bit_cnt + 1'b1; state <= S_SCLK_LOW; end end S_FINISH: begin ad_cs <= 1'b1; done <= 1'b1; state <= S_IDLE; end default: state <= S_IDLE; endcase end end这段代码的思路是:用状态机每一步产生一个SCLK相位变化,而不是用计数分频器持续翻转SCLK。这样好处特别明显——每个相位之间可以插等待周期,方便把SCLK降到任意频率,调试的时候不用重新算分频系数。采样动作放在S_SCLK_HIGH状态,此时SCLK刚拉高,DOUT线上的数据是在S_HOLD状态里稳定下来的,所以上升沿采到的一定是正确值。
如果主频更高,比如100MHz,就把S_HOLD拆成两个甚至三个状态,或者用一个wait_cnt统一控制。这套状态机模板换平台时几乎不用改结构,只调等待周期。
5. 验证和调试:仿真模型、逻辑分析仪和常见坑
5.1 Testbench里怎么模拟AD7606
裸跑硬件之前,一定要先写个Testbench把状态机跑通。AD7606在仿真里可以做得非常简单,不需要真的构建模拟前端模型,只模拟它的数字行为就够了。
并行模式下,Testbench要做三件事:
- 复位后把ad_busy置低;
- 检测到ad_convst上升沿时,延迟一段时间把ad_busy拉高,再延迟一段时间拉低,模拟一次转换完成;
- 检测到ad_rd下降沿时,延迟约几十纳秒,然后把ad_db总线更新成当前通道的预置数据。
注意第三步要自带一个数据表,比如:
reg [15:0] test_data [0:7]; always @(negedge ad_rd) begin #25 ad_db = test_data[rd_cnt]; // 模拟t9延迟 end这样仿真波形里就能清楚看到:rd拉低、数据延迟出现、FPGA采样点落位、数据正确锁存。如果你发现采回来的数据和test_data对不上,十有八九是采样点落在t9窗口前面了。
SPI模式仿真稍微复杂一点。Testbench需要在sclk下降沿时,把DOUTA和DOUTB更新为当前位:
always @(negedge sclk) begin dout_a <= test_data[ch_idx * 2][bit_idx]; dout_b <= test_data[ch_idx * 2 + 1][bit_idx]; end用这个模型可以验证:位计数是否正确、通道计数跳变时机是否对、最后done信号是否在读完8个通道后才拉高。我在调试SPI驱动时,有一半问题都是靠仿真先暴露的,比如bit_cnt初始化位置不对导致首尾丢位。
5.2 板上时序观察要点
仿真通过不代表板子一定正常,板级调试时逻辑分析仪是必需品。我建议把至少五个信号接进逻辑分析仪:CONVST、BUSY、CS、RD、SCLK。然后依次观察几个时间点:
- CONVST上升沿到BUSY上升沿的间隔,是否符合手册转换时间;
- BUSY下降沿之后,CS是否还等了一段时间才拉低;
- 并行模式下RD低电平宽度和DB数据建立关系;
- SPI模式下CS下降沿到SCLK第一个下降沿的静默时间;
- SCLK的波形占空比和边沿位置。
这些如果都符合预期,再去看数据。如果数据不对,先从数字时序找原因,不要一上来就怀疑ADC精度。之前有次我在板子上看到SPI读回来的数据全是乱码,排查半天,发现是SCLK在FPGA内部产生后,又经过一段很长PCB走线,信号反射严重,下降沿位置漂了。后来把SCLK输出加了一级寄存器约束,并把走线缩短,问题立刻消失。
5.3 我遇到过的读取异常和对应解法
这里整理几个我在实际调试中踩过、也帮同事排查过的典型问题:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 数据全为0或者固定值 | 模拟输入悬空、RANGE配错、DB线连接错误 | 给固定直流电压,看代码是否线性变化 |
| 数据全部满量程 | 输入超量程、RANGE引脚配置与预期不符 | 示波器量V1引脚,确认实际电压范围 |
| 个别通道数据错位 | 通道顺序与PCB布局不一致 | 检查原理图网络名,不要轻信引脚顺序 |
| 读取值始终是上一帧 | 在BUSY高电平期间就开始读 | 扩展等待BUSY变低后的延时 |
| 并行读回数据偶尔跳变 | RD采样点落在t9窗口内 | 增加RD低电平等待周期 |
| SPI读回数据方向反了 | 采样边沿和移出边沿搞反 | 检查SCLK空闲极性和上升沿采样点 |
| 采样率一高就错 | RD或SCLK周期不满足最小时间 | 用逻辑分析仪看脉冲宽度是否够 |
还有一个特别容易犯的低级错误,是把AD7606输出的16位数据当无符号数处理。AD7606输出的是二进制补码,中间零点0x0000,正向满量程接近0x7FFF,负向满量程0x8000。换算电压时,如果是±10V量程,公式是:
电压 = 带符号编码 × 10 / 32768
比如编码0x4000对应大约+5V,0xC000对应大约-5V。如果当成无符号数直接除以65535再乘以量程,结果会把负半轴完全丢掉。调试建议是先用一个可调直流源给某个通道输入固定电压,比如给V1接2.5V,看读回来的编码是否大致对应0x2000附近(±10V量程下)。确认了编码换算,再谈滤波和校准。
写在后面:一点实际体会
两套驱动都调通之后,我的感受是:AD7606的驱动难度真不在状态机,而在时序余量的把握。并行模式只要能确保RD采样点足够靠后,基本不会出问题;SPI模式只要把静默时间和采样边沿处理好,稳定性也不差。真到出问题时,优先怀疑时序,而不是怀疑代码逻辑。
如果让我给一个最实在的建议:动手写代码之前,先把芯片手册上的时序图找出来,在纸上把CONVST、BUSY、CS、RD、SCLK的波形画一遍,再对着状态机一个状态一个状态走一遍。这一步看起来费时间,实际上能帮你省掉后面一整天的排查时间。采集板上最后那层“就差一个坑没踩”的体验,往往就是从这张波形图开始的。