简介:这套基于FPGA的PCF8591 AD采样程序以Verilog完整实现,面向需要学习FPGA数据采集、I²C总线通信与数码管/串口交互的电子开发者。工程涵盖模拟信号采样、8位数码管显示、UART串口发送及I²C状态机控制等核心模块,支持0~3.3V电压输入转换,适合作为数字电路系统设计的进阶实践项目。压缩包共145个文件,4.15MB,包含Verilog源码(.v)、Quartus工程文件(.qpf/.qsf)、编译仿真报告(.rpt/.sdo)、配置下载文件(.sof/.pof)及MIF初始化文件等,目录结构清晰,便于按模块查阅。已有1802人学习下载。参考描述与实际代码包,读者可获得完整的PCF8591驱动状态机、数码管译码逻辑、串口发送模块,以及从工程建立、仿真验证到硬件下载的FPGA开发全流程参考,对理解ADC采样、I²C时序和模块化设计均有直接帮助。 如果你和我一样,FPGA的学习路线是从流水灯、按键消抖、UART发送一路走过来的,那么基于PCF8591做AD采样这个项目,算是从“点亮管脚”跨向“和外部真实世界打交道”的第一道坎。很多初学者在这里第一次接触I2C协议,第一次在Verilog里用状态机去模拟一个双向协议,第一次发现仿真明明过了、板子却不工作。这个项目的基本框架是:FPGA通过I2C接口配置PCF8591的通道与控制字,再发起读操作,把8位ADC采样结果拿回来。适合所有正在学FPGA、想弄懂I2C时序,或者打算做一个低速数据采集小项目的朋友参考。这篇文章就基于我实际调试的完整过程,把硬件连接、I2C细节、Verilog程序拆分、仿真方法和实测踩坑都过一遍,有些坑我踩了好几周才想明白,希望能帮你少走弯路。
1. 为什么不是片内ADC,偏偏选了PCF8591
很多人一听到FPGA做AD采样,第一反应是“Xilinx 7系列不是自带XADC吗,折腾外挂芯片干什么”。这个质疑没错,但要看场景。XADC确实方便,可它的模拟输入引脚是专用的,而且配置起来依赖Xilinx提供的IP核,本质上是在学XADC那套寄存器,而不是在学“怎么用FPGA驱动一颗通用ADC芯片”。我更推荐用PCF8591练手,有几个非常实际的好处。
1.1 PCF8591到底是个什么芯片
PCF8591是NXP(原Philips)生产的一颗8位CMOS逐次逼近型ADC,同时集成了一路DAC输出,工作电压2.5V到6V,接口是I2C,最高大概支持400kbps的快速模式。4路模拟输入可以配置成单端输入、差分输入或者混合模式,这一点在低成本的8位ADC里算是很灵活的。它的7位I2C地址由芯片的A0、A1两个引脚的电平决定,两个引脚都接地时,写地址是0x90,读地址是0x91,很多例程里直接写这两个魔数。
这颗芯片内部有一个逐次逼近寄存器(SAR),转换所需的时钟是从I2C的SCL上取出来的,所以它不需要外部提供单独的转换时钟,只要I2C时序正常工作,转换就在读的过程中完成了。这个特性决定了它和SPI 接口的AD7680这类芯片读法不太一样:你读到的第一个字节往往是上一次转换的结果,而不是当前这一次。
1.2 为什么拿FPGA驱动它更有练手价值
如果你用STM32驱动PCF8591,那基本就是一个现成硬件I2C外设的寄存器操作,逻辑上不用操心太多,芯片已经把时序管好了。但换到FPGA上,情况完全不同:FPGA没有硬件I2C控制器,所有SCL的翻转、SDA的数据变化、起始条件、停止条件、ACK应答判断,都必须自己用Verilog一个周期一个周期地写出来。这个过程走一遍之后,你对I2C的理解会比看一百遍协议文档都深。
而且I2C是一种双向半双工协议,这逼迫你去处理三态门,处理“什么时候把SDA释放成高阻、什么时候把它拉低”这类细节。这个经验是可以迁移的,后面驱动EEPROM、各类I2C传感器、甚至去理解SMBus协议都会顺很多。相比之下,SPI虽然简单,但它的单向主从结构练不到双向IO控制这块。
1.3 相比片内ADC和SPI方案怎么选
做这个项目之前,我特意对比过三类方案,这里直接给出我的结论:

如果你只是想要一个能出数的ADC,那选SPI芯片确实省事。但如果想借这个项目把I2C吃透,PCF8591是性价比极高的选择,一颗芯片几块钱,面包板上就能搭起来。
2. I2C协议里三个不影响读通、但影响读准的细节
很多初学Verilog的人看I2C协议,觉得无非就是起始、停止、应答三板斧,照着时序图写状态机就行了。但实际调起来会发现,总线的物理层、应答位的数据方向、以及PCF8591特殊的读流程,这三点任何一个没搞透,都会出现“仿真没问题、上板读出来是乱的”这种局面。
2.1 两根线都需要上拉电阻,不是FPGA引脚能直接搞定的
I2C的SCL和SDA都是漏极开路结构,也就是说器件只能把线拉低,不能主动输出高电平。高电平必须靠外部上拉电阻提供。在开发板上调试时,有些FPGA开发板已经给I2C引脚挂了上拉电阻,但如果是自己搭面包板,这个电阻很容易忘。
我第一次搭的时候就没装上拉,结果SDA的高电平被阻性负载拖到只有1.2V左右,从机应答有时候能识别、有时候不能,异常随机。四通道示波器一抓才发现高电平根本没到3.3V。后来在SCL和SDA上各接了一个4.7kΩ电阻到3.3V,波形立刻干净了。之前看文档里写“I2C总线上需要上拉电阻”,一直觉得是废话,直到自己踩了才明白,这不是可选项而是必须项。如果线长超过20厘米,建议用2.2kΩ的上拉,信号质量会好不少,代价是功耗稍微高一点。
2.2 读ACK位的时候,主机必须先释放SDA
I2C的ACK应答机制是:发送方每发完8个数据位,在第9个SCL周期释放SDA,接收方如果正常收到,就会在这个周期把SDA拉低,表示应答。问题在于,FPGA写出的代码里,SDA常常是用一个寄存器驱动的输出引脚,如果你在第9个周期不把它切成高阻态,SDA就被FPGA死死拉住,从机根本没有机会把它拉低应答。
这个坑的典型表现是:状态机停在等待ACK的位置,永远超时。我当时查了很久,最后发现是sda_out_en信号没拉低,SDA一直被主机占用。正确的做法是给SDA做一个三态门:
assign sda = sda_out_en ? sda_out : 1'bz;
发送数据时sda_out_en为高,等待ACK或读数据时切为低,让总线由从机控制。这里还有一个细节:切到高阻之后,FPGA内部读到的sda电平取决于外部上拉和从机的拉低动作,所以sda输入信号最好打两拍同步,避免亚稳态影响后面的判断。
2.3 PCF8591的读流程:先写配置,再假读一字节,才能拿到真数据
PCF8591的完整读流程比一般I2C从机多一个“假读”步骤,顺序是这样的:
发出起始条件。
发送写地址0x90。
等待从机ACK。
发送控制字,低两位选通道,bit6:4选输入模式。
等待从机ACK。
发出重复起始条件。
发送读地址0x91。
等待从机ACK。
读第一个字节(这个字节是上一次转换的结果,当作哑元丢弃)。
主机回ACK表示还要继续读。
读第二个字节,这才当前通道的有效转换结果。
主机回NACK表示不再读。
发出停止条件。
很多踩坑的人,问题都出在少了第9步,直接把第一次读到的数据当有效值用。PCF8591的转换是边读边进行的,启动转换需要一次读操作,所以第一个字节本质上是为了触发转换而存在的。如果你只需要采一次,流程就是“假读+真读”两个字节;如果连续轮询,每次轮询都必须重新走一遍这个流程,不能偷懒。
控制字的含义也很容易记错,这里给个速查表:
所以AIN0的控制字是0x40,AIN1是0x41,AIN2是0x42,AIN3是0x43。写错位最直接的后果是数据跳到别的通道去,我一度以为是芯片坏了,后来发现是控制字算错。
3. Verilog程序框架:状态机怎么拆才不把自己绕晕
FPGA驱动I2C从机,最忌讳的就是把整个时序写在一个巨大的状态机里,所有分支堆在一起,出问题根本没法查。我建议按功能把代码拆成两个模块:一个负责产生I2C时序的控制器,一个负责发起读写操作协议状态机。这样层次清晰,调试的时候也能快速定位问题。
3.1 模块划分与接口设计
我在这个项目里用的是顶层模块加内部状态机的经典结构,核心端口如下:
module pcf8591_adc #(
parameter CLK_FREQ = 50_000_000,
parameter SCL_FREQ = 100_000
)(
input wire clk,
input wire rst_n,
input wire [1:0] ch_sel,
input wire start,
inout wire sda,
output wire scl,
output reg [7:0] adc_data,
output reg data_valid
);
CLK_FREQ和SCL_FREQ做成参数,方便不同开发板之间移植。SCL频率选择100kHz比较稳妥,PCF8591标准模式下的时序余量充足,跑400kHz虽然理论上支持,但我实测在杜邦线连接的情况下信号质量会变差,偶尔出现误码。
时钟分频这块,我用一个计数器去产生SCL。50MHz系统时钟跑100kHz I2C时,一个SCL周期是10us,半周期是5us,换算成时钟周期就是250个,所以每计到250翻转一次SCL,就能得到100kHz的SCL。如果系统时钟不是50MHz,只要改CLK_FREQ参数就行,这段逻辑不依赖具体频率。
3.2 状态机的状态定义与转移逻辑
状态机是整个程序的核心。我定义的状态比较多,但胜在直白,出问题能一眼看出卡在哪:
localparam [4:0] IDLE = 5'd0,
START1 = 5'd1,
ADDR_W = 5'd2,
ACK1 = 5'd3,
CTRL = 5'd4,
ACK2 = 5'd5,
START2 = 5'd6,
ADDR_R = 5'd7,
ACK3 = 5'd8,
DUMMY = 5'd9,
ACK4 = 5'd10,
DATA = 5'd11,
NACK = 5'd12,
STOP = 5'd13,
DONE = 5'd14;
每个状态内部需要用一位计数器去记录当前发送到第几位数据,我习惯用一个bit_cnt变量,在SCL为低的时候更新SDA数据,SCL为高时保持。这样设计有个好处:SDA的数据变化永远发生在SCL低电平期间,完全符合I2C协议对数据建立时间和保持时间的要求,也避免了上板后出现建立时间不足的时序违规。
读数据的采样点要放在SCL高电平的中间位置,而不是上升沿。这是因为从机在SCL下降沿之后才改变SDA,如果你在上升沿就去采,采到的可能是上一个bit的残留电平。我用了一个内部计数器,当SCL高电平持续到一半时采样SDA,采到的数据最稳定。
3.3 双向SDA的实现细节
SDA的驱动逻辑虽然只有一行三态门,但sda_out_en的时序控制才是关键。写数据阶段sda_out_en为高,把要发送的bit赋到sda_out上;等待ACK阶段sda_out_en拉低,释放总线;读数据阶段sda_out_en同样保持低电平,由从机驱动SDA。
这里有一个容易被忽略的点:从写数据切换到读数据的时候,sda_out_en拉低的时机必须和SCL配合。如果太早释放SDA,主机在数据位上就松手了,SDA会被上拉电阻拉高,导致发送的数据变成全1;如果太晚释放,就会占住总线影响从机发数据。我的做法是:在SCL下降沿之后的半个周期内完成sda_out_en的切换,给总线留出足够的翻转时间,实测效果很稳定。
3.4 状态机主流程关键代码
由于完整的状态机代码有一两百行,这里贴出最核心的IDLE到ADDR_W转移逻辑做参考:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
bit_cnt <= 0;
scl <= 1;
sda_out_en <= 0;
end
else if (start && state == IDLE) begin
state <= START1;
end
else begin
case (state)
START1: begin
// SCL高电平期间SDA由高到低,产生起始条件
sda_out_en <= 1;
sda_out <= 1;
if (scl_cnt == HALF_SCL - 1) begin
sda_out <= 0;
state <= ADDR_W;
bit_cnt <= 0;
end
end
ADDR_W: begin
sda_out_en <= 1;
// 移位输出写地址0x90
if (scl == 0) begin
if (bit_cnt < 8) begin
sda_out <= addr_w[7 - bit_cnt];
bit_cnt <= bit_cnt + 1;
end
else begin
bit_cnt <= 0;
state <= ACK1;
end
end
end
// 后续ACK、CTRL、DUMMY、DATA等状态类似处理
endcase
end
end
这段代码省略了后面所有状态,但逻辑思路是一致的:每个状态负责一个I2C阶段,通过bit_cnt记录当前bits位置,通过scl_cnt产生时序延迟。这样拆开写虽然有点啰嗦,但每个状态的逻辑都非常清晰,上板出问题后用逻辑分析仪一看波形,马上能定位到具体是哪个状态没跳过去。
4. 没有真实芯片也能验证设计:Testbench自建从机模型
写Verilog和写C语言最大的不同,就是你没办法printf调试,所有问题都得靠波形说话。在把程序下载到板子之前,我强烈建议先做仿真,把整个I2C交互过程跑通。但这里有另一个问题:你的Testbench里得有一个PCF8591的模型,仿真才有意义。FPGA开发环境不会自带这个芯片模型,所以需要自己写一个简化的从机模拟器。
4.1 自己动手写一个行为级PCF8591模型
不需要把PCF8591的内部寄存器结构完整建模,只需要把对FPGA侧验证最关键的行为模拟出来:识别起始条件、接收地址、产生ACK应答、接收控制字、输出预设的ADC数据。我在Testbench里用了一个简单的状态机来模拟从机的行为:
always @(posedge scl) begin
case (slave_state)
SLAVE_IDLE: begin
// 检测到SDA下降沿,开始接收地址
slave_bit_cnt <= 0;
slave_state <= SLAVE_ADDR;
end
SLAVE_ADDR: begin
if (slave_bit_cnt < 8) begin
slave_addr_buf <= {slave_addr_buf[6:0], sda};
slave_bit_cnt <= slave_bit_cnt + 1;
end
else begin
// 第9个时钟拉低SDA表示ACK
if (slave_addr_buf == 8'h90) begin
// 是写地址,继续接收控制字
end
else if (slave_addr_buf == 8'h91) begin
// 是读地址,开始输出数据
end
end
end
endcase
end
实际写的时候还要处理很多边界情况,但核心思路就是这样:在SCL上升沿采样SDA,拼出接收到的字节,然后根据字节内容决定应答还是发数据。我预设了一个8位的测试数据,比如 test_read_data = 8'hA5 ,当FPGA发出读地址后,从机就按bit把这个值输出到SDA上,然后在仿真波形里检查FPGA读到的值是不是0xA5。
4.2 Testbench里双向引脚和高阻态的处理
写Testbench时最容易出问题的是SDA这个双向信号的激励方式。Testbench里从机驱动SDA,主机(被测试的FPGA设计)也驱动SDA,两根线是同一根物理线,所以握手过程必须用高阻态来模拟。我用了这样的写法:
wire sda;
reg sda_slave_drive;
reg sda_slave_out;
assign sda = sda_slave_drive ? sda_slave_out : 1'bz;
还有一个关键的上拉处理:由于I2C总线是高电平靠上拉电阻提供的,仿真环境也需要模拟这个行为,否则当所有器件都释放SDA时,总线上是一个不确定态z。最好的办法是在Testbench顶层加上 pullup(sda); 和 pullup(scl); ,这样总线释放时会被上拉为高电平,仿真结果更接近真实情况。
4.3 仿真时看哪些波形、设多长的时间
I2C是慢速协议,大部分人第一次仿真I2C都会把时间设得太短。一个完整的读流程要发送写地址、控制字、读地址、假读、真读,每一字节都要9个SCL周期,一次完整轮询下来差不多要50多个SCL周期,加上SCL本身被分频得很慢,所以在Vivado或者ModelSim里仿真,至少要把时间设到5毫秒以上,才能看到完整的一轮波形。
仿真时重点看这几个位置:起始条件有没有出现在SCL高电平期间、SDA从高到低的沿是否干净、每个ACK周期SDA是否被从机拉低、读数据阶段SDA上的8个bit电平变化是否符合预期。我用ModelSim仿真的时候,会在这几个关键位置打上标记,配合 $display 在控制台打印读回来的数据,比单纯看波形方便很多:
if (data_valid) begin
$display("ADC data = 0x%02x, voltage = %0.3f V", adc_data, adc_data * 3.3 / 256.0);
end
这一步很能说明问题:如果仿真打印出的数据和Testbench里预设的值完全一致,那状态机的逻辑基本就对了,剩下的问题大概率出在硬件连接上。
5. 上板实测:五次踩坑和对应的排查链路
仿真通过只是一个开始,把程序下载到FPGA开发板之后,才真正进入“和现实搏斗”的阶段。我前后在这个项目上踩了五个比较大的坑,每一个都花了不少时间排查。下面按我实际遇到的顺序列出来,每一条都附上排查思路,希望能帮你在遇到类似问题时少走冤枉路。
5.1 第一坑:A0、A1地址引脚悬空导致I2C地址飘忽不定
板子刚焊好的时候,我以为PCF8591的A0、A1不接也能工作,结果FPGA发0x90地址之后迟迟等不到ACK,I2C状态机一直卡在ACK1状态。用逻辑分析仪抓SDA,发现从机确实把SDA拉低了,但拉低的时机和SCL对不上,说明地址没有匹配上。
排查过程:我先用万用表量了A0、A1引脚的电压,发现都处在一个中间的浮动电平,这就不对了。PCF8591的I2C地址是 1001 A1 A0 ,A0、A1必须确定接高或接低,悬空的时候芯片内部逻辑读到不定的电平,地址就在0x48到0x4B之间飘。解决办法很简单,把A0、A1都接地,地址固定为0x48,FPGA侧用0x90/0x91发读写地址,问题立刻解决。如果你需要挂多颗PCF8591,记得把A0、A1分别接不同电平来区分。
5.2 第二坑:漏装上拉电阻,SDA高电平被拖低
上拉电阻这个坑我在第二章讲过,但它也是我上板实测中最先遇到的大问题。当时波形上SDA高电平只有1.2V,从机虽然能拉低,但在SCL上升沿时刻SDA的电平并不稳定,导致FPGA采到错误的bit。
排查过程:第一次怀疑是FPGA的IO驱动能力不够,但查了数据手册,FPGA的IO在输出模式下驱动能力完全够,问题在于I2C是漏极开路结构,需要外部电阻把线拉高。我在SCL和SDA上分别加了4.7kΩ上拉到3.3V,再抓波形,高电平恢复到了3.3V,ACK和数据都正常了。这里有个经验:如果你的I2C波形高电平静态不是电源电压,优先检查上拉电阻。
5.3 第三坑:把第一次读到的数据当有效值,结果数据一直不对
仿真的时候Testbench里的从机模型行为是按照标准I2C流程写的,所以FPGA读出来的第一个字节就是预设值,我感觉一切正常。但上板之后,把电位器从一端拧到另一端,读出来的数据始终是0x80附近,变化幅度很小,完全不符合输入电压变化。
排查过程:我用万用表量了电位器分压点的电压,确实是0到3.3V之间变化,但FPGA读出来的数据不对。后来仔细翻PCF8591的数据手册,看到一句不太起眼的话:“转换在读取时发生,读到的第一个字节是上一次的转换结果。”我这才意识到问题:我把第一次读到的数据当成了当前通道的结果,而实际上它是上一次转换的残影。解决办法就是第二章说的,加一个dummy读取,丢掉第一字节,再用第二字节作为有效数据。加了这个逻辑之后,电位器从另一端拧到另一端,数据范围变成了0x05到0xF5左右,线性关系也很正常。
5.4 第四坑:控制字写错,数据读出来是另一个通道的
这个坑有点隐蔽。我在调试多通道切换时,想采AIN1通道,配置字写了0x41,但读出来的数据和AIN0接的电压一致,怎么都不对。
排查过程:我用按键切换通道,同时用数码管显示当前选的通道号和ADC数据,发现一个规律:选AIN0时数据正常,选AIN1时数据还是AIN0的。怀疑是控制字发送有误,用逻辑分析仪抓I2C波形,逐bit分析FPGA发出去的控制字,发现发送的数据确实是0x41,没有拼写错误。但再仔细查数据手册发现一个问题:PCF8591的控制字里bit1:0是通道选择,bit6:4是输入模式选择,0x41的二进制是 0100 0001 ,低两位是01,对应AIN1,看起来没错。后来才发现我的状态机在发送控制字的时候,bit_cnt的计数起点错了,导致实际发送顺序变成了 1000 0010 ,低两位变成10,对应的是AIN2,但AIN2悬浮没接,所以数据随机。修正bit_cnt和移位顺序之后,四个通道都能正常切换。这个问题的教训是:不要只看逻辑分析仪解出来的字节,还要确认自己的发送顺序和PCF8591要求的MSB-first一致。
5.5 第五坑:模拟输入超过VREF导致采样值锁死
最后一个坑是输入电压范围问题。我把电位器分压后的模拟输入接到了AIN0,但分压参考电压用的是5V,而PCF8591的VREF接的是3.3V,结果输入电压超过3.3V之后,读出来的一直是0xFF,往回拧到3.3V以下,数据也不恢复正常。
排查过程:这个现象看起来像是芯片锁死了,复位重新采样也一样。后来查资料才知道,PCF8591的模拟输入电压如果超过VREF,内部的逐次逼近比较器会进入异常状态,需要把输入电压降到VREF以下才能恢复。这也解释了为什么数据会一直卡在满量程。解决办法是把模拟输入的范围限制在0到VREF之间,给VREF接一个干净的3.3V基准,然后用电位器从3.3V分压给AIN0。这个问题在工业现场特别容易出现,如果你准备接外部传感器,一定要先确认传感器输出范围不会超过VREF。
现象可能原因排查方法
状态机卡在等ACKA0/A1悬空或地址错误万用表量A0/A1电平,确认地址
波形高电平偏低缺少上拉电阻或阻值过大示波器抓SCL/SDA,检查静态电平
读出的值与输入不符漏掉dummy字节对照数据手册流程,检查读取逻辑
通道切换数据错误控制字bit顺序或数值错误逻辑分析仪抓控制字波形逐位核对
数据锁死在0xFF输入超过VREF万用表量输入电压,确认采样范围
6. 从能读到数到能采集波形:几个顺手的扩展方向
PCF8591读通之后,这个项目就算迈过了FPGA和模拟世界打交道的门槛。但如果你和我一样,觉得光在板子上看到几个十六进制数据不够过瘾,可以沿着下面几个方向继续扩展,每一个都能让你对这个系统有更深的理解。
6.1 把数据传输到电脑:加一个UART输出
我之前已经写好了UART发送模块,所以给这个项目加串口输出非常顺手。思路是每完成一次AD采样,就把8位数据打包成数据帧,通过UART以115200波特率发给电脑端的串口助手。这样在电脑上就能实时看到电压值,还能用串口绘图功能画出波形,比看数码管直观多了。需要注意的是,UART的发送耗时远大于一次I2C采样耗时,所以顶层要做一个节流控制,比如每10ms触发一次采样和发送,避免数据堆积。
6.2 采集一段连续波形:在中间加一个FIFO
如果你想把一段连续波形抓下来分析,可以在ADC采样和UART发送之间加一个FIFO,FPGA内部用Block RAM实现即可,不需要外接芯片。FPGA按固定采样率把AD数据写入FIFO,UART模块再从FIFO另一端把数据搬出去,这样即使UART速度跟不上采样速度,也不会丢数据。我实际用的方案是:采样率1kSPS,FIFO深度1024,UART波特率115200,可以一次性把约1秒的波形数据完整上传到电脑。至于FIFO的读写指针和空满标志,建议理解清楚再直接用IP核,这本身也是一个很好的学习环节。
6.3 多通道轮询和采样率评估
PCF8591有4路模拟输入,控制字里的自动递增功能可以让芯片每读完一个字节自动切到下一通道,配合FPGA的通道切换逻辑,就能实现四路同时采集。但这里要提醒一句:每一路通道切换之后,第一个采样数据最好是丢弃的,因为模拟开关切换后需要一点稳定时间。
关于采样率,我实际测试下来,I2C跑100kHz时,一次完整ADC转换流程大概需要半个多毫秒,换算下来连续采样率也就在1kSPS到2kSPS的水平。如果你的应用需要连续监测多个通道,每一路能分到的采样率还会更低。所以PCF8591更适合温度、湿度、电池电压这类缓变信号,如果要做音频采样或者振动分析,还是老老实实换SPI接口的ADC,或者上并行接口的高速ADC吧。
最后分享一个我做这个项目时体会最深的一点:PCF8591虽然只是颗8位的低速ADC,但它逼着我把I2C物理层、状态机设计、三态门控制、Testbench建模整个闭环走了一遍,之后再去看任何带I2C接口的传感器芯片,心里都有底。如果你正在被状态机卡住,别急着怀疑自己,先拿逻辑分析仪把每一段波形抓出来看,波形会告诉你所有答案。