在嵌入式开发调试里,USB转I2C适配器几乎是每天都要用的工具。我之前一直在折腾一套基于Excel的I2C设备扫描方案,顺带把总线速率从100kHz一路调到400kHz做压力测试。这个过程踩了不少坑,但也有不少很值得分享的经验。这篇文章把整个方案从选型、原理到实操完整写出来,重点落在400kHz总线速率下,这套USB转I2C方案到底能不能稳定工作,以及怎么用Excel把一个普通的地址扫描过程变成一个可复现、可统计、可导出报表的测试流程。
1. 为什么非要折腾400kHz:USB转I2C的速率瓶颈不在USB端
1.1 400kHz不是一句配置就能跑起来的
很多人拿到USB转I2C适配器,随手在软件里选一个“Standard Mode”或“Fast Mode”就把速率设成400kHz,然后发现读写不稳定、扫描漏设备、时序错乱,就开始怀疑硬件不行。实际上,400kHz才是I2C标准模式里最容易踩坑的档位。
I2C的总线速率由SCL时钟决定,标准模式是100kHz,快速模式是400kHz,高速模式可以到1MHz以上。USB转I2C适配器内部要完成一个很关键的动作:把USB包转换成I2C时序。这个过程不是简单搬砖,中间涉及缓冲区管理、时钟拉伸、ACK状态反馈等一堆细节。适配器宣称支持400kHz,不代表它每条指令都真的工作在400kHz,更不代表在真实负载下还能跑到400kHz。
我测试的这套方案用的是FTDI家族常见的USB转I2C芯片,配置I2C时钟频率时有个底层寄存器值需要计算,驱动层是按2的幂次分频来产生SCL的。如果直接把期望的400kHz传进去,得到的实际时钟频率往往会有偏差,这个偏差在短时序下不明显,但在连续多字节读写或大批量扫描时会被放大,最终表现为地址扫描结果不一致。
1.2 USB到I2C之间到底发生了什么
理解速率瓶颈,要先搞清楚一条USB读I2C的完整链路。假设你发一个“扫描0x50这个地址是否存在”的操作,流程是这样的:
- 上位机(Excel里的VBA)通过驱动API下发一个I2C事务请求,包含起始条件、7位地址、读写位、停止条件。
- USB控制器把这个请求打包成USB包,送往适配器。
- 适配器固件解析USB包,把它转换成I2C总线上的电平变化。这个过程涉及SCL的翻转频率,也就是你设置的400kHz。
- I2C设备如果存在,会在第九个SCL周期拉低SDA表示ACK。这个ACK状态需要被适配器采样,再打包成USB包返回给上位机。
问题就出在第3和第4步的往返打时间。USB本身是异步传输,适配器固件每处理一个USB包都要消耗几个毫秒级的调度时间。真正在总线上产生几百个400kHz的时钟周期也许只需要几毫秒,但加上USB协议开销、驱动调度、VBA调用延迟,实际“扫一个地址”的耗时可能就到几十毫秒了。
对整个总线速率而言,如果只发单字节读写,你感受到的速率更多是USB交互延迟,而不是SCL频率本身。只有当指令包含较长连续数据块(比如一次写32字节EEPROM),SCL频率才会显著影响总耗时。所以我在设计扫描方案时,刻意把“单字节探测”和“多字节批量读写”分开测,结论差异很大。
1.3 选型思路:FT4222、FT2232H还是CH341
USB转I2C适配器市面上很多,但主要分两类。一类是纯软件模拟I2C,典型如CH341系列,它的USB协议里包含了专门的I2C传输命令,由芯片内部状态机直接产生时序,但这种芯片的时钟稳定性一般,在400kHz下波形上升沿偏软,挂的设备稍微多点就容易出错。另一类是基于FTDI的MPSSE引擎,比如FT2232H、FT4232H、FT4222,它们通过MPSSE命令动态配置时钟分频,可以比较精准地生成400kHz的SCL。
我最终用的是FTDI MPSSE方案,原因是它有一个特别适合扫描的场景——可以一次下发一组命令,让适配器自己在总线上完成连续地址扫描,而不是每扫一个地址就和USB通讯一次。FT2232H在高速USB下可以支撑这种批量操作,实际最高能到几百kHz以上的切换速度。如果是CH341那种软件模拟,一个地址一停,扫描几十个地址就会卡顿明显,400kHz的优势根本发挥不出来。
如果你手头的适配器是基于FTDI VCP串口模式工作的,那么它能走I2C是模拟时序,驱动层面的开放性也差一些,很难做精细的时钟控制。建议优先选支持D2XX直驱或者可用MPSSE命令的型号。这块选型直接决定你后面在Excel里能不能做到“批量扫描”,所以我花了不少篇幅强调它。
2. 用Excel做I2C扫描台:为什么不用现成的上位机软件
2.1 现成工具的问题
很多USB转I2C适配器会配一个官方上位机,功能不差,能扫描、能读写寄存器、能导出部分数据。但实际用起来有几个痛点:一是扫描结果不能灵活过滤,比如想只显示某个地址范围内的有应答设备,需要自己记下来再手动处理;二是批量操作不友好,我想对十几个I2C设备连续做“地址扫描+寄存器读回”的循环,官方软件很难配出这种流程;三是测试报告格式不统一,每次都要重新整理数据。
我想到用Excel做扫描台,主要是因为Excel本身就是一个天然的数据容器,扫描结果可以直接落到单元格里,再配合VBA做自动化,既能统计又能可视化。而且团队里其他人都会用Excel,不需要专门装软件,拿过来就能跑。这个方案特别适合产线测试或者实验室快速评估场景。
2.2 驱动层选择:VCP还是D2XX
在VBA里操作USB转I2C适配器,绕不开驱动API的选择。FTDI提供了两种驱动方式:VCP(虚拟串口)和D2XX(直接访问)。VCP方式把适配器枚举为一个串口,你可以用COM口去打开它,但I2C不是串口协议,走VCP路线意味着要用一些虚拟串口上的特殊命令,稳定性一般,不适合400kHz时序要求。D2XX则直接通过FTD2XX.DLL的API访问设备,可以下发MPSSE命令,支持高频切换和批量传输,这正是我要的。
Excel的VBA引用D2XX的方法是在代码里先用Declare语句声明FT_Open、FT_Write、FT_Read、FT_Close等函数,再调用FT_ListDevices枚举设备号。注意VBA里调用DLL函数要处理好字符串和缓冲区类型,否则很容易出现内存访问错。
我实际用的DLL加载方式如下,在模块顶部声明:
Private Declare PtrSafe Function FT_Open Lib "FTD2XX.DLL" (ByVal devIndex As Long, ByRef ftHandle As Long) As Long Private Declare PtrSafe Function FT_Close Lib "FTD2XX.DLL" (ByVal ftHandle As Long) As Long Private Declare PtrSafe Function FT_Write Lib "FTD2XX.DLL" (ByVal ftHandle As Long, ByRef buffer As Byte, ByVal bytesToWrite As Long, ByRef bytesWritten As Long) As Long Private Declare PtrSafe Function FT_Read Lib "FTD2XX.DLL" (ByVal ftHandle As Long, ByRef buffer As Byte, ByVal bytesToRead As Long, ByRef bytesRead As Long) As Long64位Office要用PtrSafe,32位Office可以不加。这个细节我一开始没注意,后来换了台64位电脑,宏直接崩,查了半天才确认是这个原因。
2.3 VBA的最小调用框架
FT2232H要进入I2C模式,需要先通过D2XX的配置API把通道设置为MPSSE模式,然后发送一组初始化命令。初始化命令主要包括:
- 设置时钟分频寄存器,得到目标SCL频率
- 设置I/O引脚方向,把SCL和SDA都设为输出,同时保留SDA的输入能力
- 拉高SCL和SDA,让总线处于空闲状态
这一系列操作的MPSSE指令字节都不长,可以在VBA里用Byte数组组织好,一次性FT_Write发送。发送完再稍等一段时间,让适配器完成内部切换。
这里有个坑:MPSSE的时钟分频不是直接写个400就能得到400kHz。FTDI的公式是:
[ 实际SCL频率 = 60MHz / ((1 + 分频值) \times 2) ]
对很多FT2232H来说,内部基准时钟是60MHz或30MHz,要看具体型号。如果你直接写分频值75,得到的频率可能是60e6/(76*2) = 394.7kHz,接近400但不到。这个精度对大多数I2C设备没有问题,但严格说起来它不是标准400kHz。如果你在应对要求精确时钟的设备时发现异常,可以先算一下实际频率值。
我在VBA里算好分频值,再附带上MPSSE命令:
' 以60MHz基准为例,目标400kHz divisor = 74 ' 实际 60MHz / ((74+1)*2) = 400kHz这里不同芯片可能略有差异,建议看数据手册里的分频公式,别直接照抄我的值。
初始化完成后,就可以发I2C地址扫描命令了。MPSSE的I2C传输命令格式比较固定,需要指定起始条件、地址字节、读/写位、停止条件,还有ACK检查逻辑。官方库里有现成的例程,但VBA版例程极少,我把它们翻译成VBA结构后,再加上扫描循环和Excel输出,就成了一个最小可用的扫描工具。
2.4 波形上确认SCL/SDA对应关系
很多人在Excel脚本里写错引脚配置,导致扫描结果完全反逻辑。最好的办法是先拿示波器或者逻辑分析仪挂在SCL和SDA上,运行一个最简单的“读任意地址”操作,看波形里SCL是不是目标频率,SDA是不是有ACK拉低动作。不要一上来就扫全地址段,先确认基础时序再说。
我习惯先把SCL频率设低一些比如100kHz,跑一遍,确认波形和地址关系,再切到400kHz,这样能隔离“逻辑错误”和“速率问题”。
3. I2C地址扫描的原理与Excel实现细节
3.1 扫描的本质是“发一个地址等一个ACK”
I2C总线上每个设备都有一个7位地址,主设备通信时先发起始条件,然后发送7位地址加一位读写标志。如果总线上某个设备地址匹配,它会在第九个时钟周期拉低SDA作为应答。扫描的过程就是一直重复“起始+发送7位地址+读应答+停止”,然后把有应答的地址记录下来。
7位地址范围是0x00到0x7F,理论上最多128个地址。但其中有一些是保留地址,比如0x00是通用呼叫地址,0x04到0x07等也有特殊用途。扫描时如果遇到这些地址有应答,要额外判断是不是真正的设备,而不是总线上的广播响应。
我用Excel做扫描时,把地址从0到127依次循环,每次调用一次单地址探测函数,记录返回状态。单地址探测的函数内部要做这几件事:
- 发送起始条件命令
- 发送地址字节(地址左移一位,最低位补0表示写操作)
- 读取ACK标志
- 发送停止条件
如果适配器返回ACK标志有效,就判定地址存在。要注意,I2C规范里“写操作”的ACK来自从设备,而“读操作”的ACK也可以来自从设备,但时序上略有不同。扫描时通常用写操作来探测,因为不会触发设备内部的读取行为,更安全。
3.2 每个地址的时序拆分
具体到MPSSE命令层面,一个单地址扫描的事务可以分解为若干条命令。以FT2232H为例,I2C传输命令中包含多个分段:起始命令、地址命令、读/写位、终止。VBA中把每个分段作为字节塞进发送缓冲区,然后一次FT_Write发出,再FT_Read读回状态。
如果地址扫描每循环一次都做一次FT_Write和FT_Read,128个地址会产生128次USB往返,速度很慢。优化方式是MPSSE支持连续事务:把128个地址的扫描命令一次性写入发送缓冲区,然后一次读取所有返回状态。但这样缓冲区和状态对应关系会变得复杂,需要自己记录每条命令的字节偏移。
我实测过,在Excel VBA里一次扫描128个地址,普通方式大概需要3到5秒,批量方式可以压到1秒以内,还是比较明显的。
3.3 扫描结果表和重复地址处理
扫描完成后,结果会写到Excel的单元格里,一列是16进制地址,一列是ACK状态,还有一列可以填设备类型备注。一个总线上可能有多个相同型号的芯片,地址相同,那就需要通过外接地址引脚来区分。扫描表里最好单独加一列“地址引脚状态”,方便你记录这组设备是靠A0/A1/A2引脚区分的。
常见的问题是“总线空闲但SDA被拉低”。如果某个从设备锁死了总线,扫描时会看到大量地址都有ACK,或者所有地址都无ACK。此时要先断开可疑设备,或者对总线做一次复位操作,拉9个SCL时钟让从设备释放SDA。这个功能也可以做进Excel按钮里,相当实用。
3.4 扫描结果表格的格式化
Excel扫描台最好设计成三个区域:参数区、扫描结果区、日志区。参数区放SCL频率、起始地址、结束地址、重复次数。扫描结果区放地址和状态。日志区记录每次扫描的开始时间、结束时间、总耗时、误差率。
我把扫描结果自动用条件格式标色,有ACK的地址显示绿色背景,无ACK显示灰色,异常状态显示红色。这样一眼就能看清总线上有哪些设备。Excel的单元格格式就是现成的,VBA里设置条件格式也很容易。对于400kHz速率测试,我还会增加一个“实际速率估算”列,根据扫描总耗时和地址数反推平均事务速率,虽然不能直接测SCL频率,但能反映整体链路效率。
4. 400kHz速率实测:波形、延迟与失败模式
4.1 我的实测环境
测试板卡上有一颗I2C E2PROM,24C256,挂在总线上,还有一颗温湿度传感器,同样走I2C。适配器通过杜邦线连接到测试板,线长控制在10厘米以内。400kHz下,线材过长或者接触不良都会带来振铃和过冲,所以我专门用了一组短跳线。
驱动部分我已经通过D2XX设置了40MHz基准的MPSSE通道,并通过公式计算分频值,让SCL目标频率落在400kHz附近。示波器探头接在SCL和GND上,测量实际波形频率。同时用逻辑分析仪抓取完整的起始、地址、ACK、停止序列。
4.2 实测结果:实际速率和理论差多少
把示波器的频率测量功能打开,我看到SCL实际频率是396kHz左右,和计算值非常接近。这说明MPSSE计算式在FT2232H通道上是可靠的,误差主要来自分频整数取整。对于24C256这类设备,时序余量足够,完全没问题。
但总线速率还有一个关键指标是上升沿时间。400kHz下的SCL上升沿必须符合规范,通常要求不超过300纳秒。我的测试板上上拉电阻用了4.7k,在这个速率下沿有点偏缓,换用2.2k上拉后波形明显变好。USB转I2C适配器内部往往也有上拉电阻,如果你发现SDA低电平抬起缓慢,大概率是上拉阻值和总线电容不匹配。
4.3 典型的400kHz失败模式
我在测试中发现一个典型现象:地址扫描在100kHz下完全正常,切到400kHz后,某些地址间歇性丢失。起初怀疑是适配器不行,后来用逻辑分析仪抓波形发现,问题出在ACK采样窗口。MPSSE在发送完地址字节后,采样SDA的时机稍晚,如果从设备在400kHz下响应速度偏慢,适配器会误判为无ACK。
解决方案是在MPSSE的I2C命令中加入“时钟拉伸”允许位,让适配器等待从设备释放SDA后再继续。对于某些老款芯片,这是必须的。但设置时钟拉伸后,实际扫描速度会略微下降,因为每个事务要额外等待若干微秒。
另一个失败模式是连续读写大数据块时出现字节错位。例如读取24C256的一页数据时,在400kHz下偶尔会出现多读或少读一个字节。这个问题排查起来比较隐蔽,因为它不是每次必现。最后定位到是MPSSE接收FIFO在高速传输下溢出,解决办法是分批传输,每次最多读32字节,把读回的缓冲区及时清空。这也是为什么我在Excel扫描方案里,把所有多字节操作都拆成小块处理。
4.4 如何用示波器测量实际位速率
示波器测量I2C位速率时,不要直接依赖示波器的频率计,因为I2C总线上有大量空闲态,频率计得出来的值偏低。正确做法是用示波器的光标,量一个完整数据位的时长,比如从SCL第一个上升沿到第二个上升沿,再取倒数。重复测几个位,取平均值,才能得到接近真实SCL的数据。
我也习惯同时抓取SDA线上的ACK位,确认低电平窗口占一个完整SCL周期。如果ACK窗口异常短,说明从设备响应太慢或适配器采样过早,这种情况即便波形频率正确,也不能视为稳定的400kHz通信。
5. 常见问题排查:驱动枚举、上拉电阻和地址换算
5.1 USB枚举和驱动识别
很多人把USB转I2C适配器插上电脑后,设备管理器里看到一个未知设备,就以为驱动坏了。其实USB转I2C适配器走的是复合设备模式,可能同时枚举出一个UART端口和一个MPSSE端口。如果你插上后只看到一个串口,那很可能没有开启MPSSE功能的驱动配置。
FTDI的驱动默认会为D2XX设备生成独立的设备节点。在设备管理器里找“USB Serial Converter”或者对应厂商的D2XX辅助设备。如果只看到COM口,那需要更新驱动或者用FT_Prog工具把端口配置成“D2XX Direct”模式。对于FT2232H这类双通道芯片,还要确认你把I2C相关的通道设置正确,别把SCL/SDA接在UART通道上,那就完全不会出I2C时序。
我在测试时遇到过一种情况:自制的USB转I2C适配器在别人的电脑上正常,在自己的电脑上延迟很大。后来发现是USB节能策略把设备挂起了。Windows默认的USB选择性暂停有时会干扰长时间扫描,尤其是Excel宏跑循环的时候,中间一旦设备挂起,后面所有事务都失败。解决办法是进电源管理,关掉USB选择性暂停设置。这个坑很隐蔽,但影响非常大。
5.2 上拉电阻为什么这么重要
I2C总线是开漏结构,SCL和SDA都要靠上拉电阻拉高。上拉电阻的阻值选择直接决定信号边沿速率。总线电容大、上拉电阻大,上升沿就慢,400kHz下容易造成误采样。我用4.7k上拉时,SCL上升沿大约250纳秒,接近极限。换2.2k后,上升沿压到150纳秒左右,余量明显更大。
上拉电阻也不是越小越好。过小的上拉会让低电平灌电流增大,某些弱驱动力芯片可能无法把总线拉低。一般在400kHz下,上拉电阻选择1k到4.7k之间,具体要根据总线上设备的驱动能力和总线电容平衡。你可以在Excel扫描台上增加一个“上拉测试”工作表,分别记录不同电阻下的扫描成功率,很快就能找到最优值。
5.3 总线空闲状态判断
执行扫描前,最好先判断总线是否空闲。空闲状态是SCL和SDA同时为高。如果SDA一直为低,说明有设备在占用总线或者总线锁死。我在VBA里增加了一个“预热检查”函数,先发送一个无地址的停止条件,尝试释放总线,然后读取SCL和SDA状态。如果SDA仍为低,就报出“总线忙”提示,避免后续扫描全失败。
总线锁死通常由从设备异常引起。一个常见的解锁办法是切换SCL 9个周期,让锁死设备完成内部状态释放。这个功能可以做成Excel里的单按钮,调试时非常方便。
5.4 7位地址和8位地址的换算
很多I2C设备数据手册里写的是8位地址,比如“写地址0xA0,读地址0xA1”。而扫描时你操作的是7位地址,也就是0xA0右移一位得到0x50。这个问题看似简单,但我见过不少人把扫描结果里的0xA0当成7位地址,导致怎么也匹配不上设备。
在Excel扫描表里,我会同时列出三列:7位地址、8位写地址、8位读地址。这样对照起来清晰很多。如果你是用逻辑分析仪解码,也要注意软件里默认展示的是7位地址还是8位带R/W位的地址。确认好基数之间的一致性,能省去大量无谓的排查时间。
6. 从地址扫描到批量读写:扩展你的Excel I2C控制台
6.1 从地址扫描到寄存器读写
一旦扫描确认了设备地址,下一步自然是读寄存器或写配置。我在Excel里做了两个通用函数,一个是“写寄存器”函数,输入设备7位地址、寄存器地址、数据,自动完成起始、写地址、写寄存器地址、写数据、停止;另一个是“读寄存器”函数,要多一个重复起始条件,先写寄存器地址,再重新起始,然后读取数据。
VBA实现时要注意I2C的“重复起始”和“停止后再起始”的区别。很多场景下必须用重复起始,因为在读寄存器时,如果发停止再起始,某些设备会终止内部操作。我的代码里单独封装了一个“发送重复起始”的子程序,避免和普通停止混淆。
读多个连续寄存器时,可以发送地址后连续读取,但每读一个字节要给一个ACK(除了最后一个字节给NACK),这个逻辑在MPSSE命令里也要专门处理。你若是在400kHz下连续读,务必使用前面提到的小块读策略,别贪多。
6.2 批量读写验证
寄存器读写可能一次就成功,但批量验证才能暴露问题。我在Excel里建立了一个“自检”工作表,预设一组已知数据的写读循环:写入一串递增数,再读回比对,记录读写差异。在400kHz下跑1000次批量写读循环,只要有一次不一致,就说明时序不够稳定。
批量测试的结果会生成一个错误率,比如“500次循环,0失败”或者“失败率0.2%”。这个数据比单纯扫描地址有说服力得多。如果你在调一个对时序敏感的设备,这个自检工具能帮你量化不同上拉电阻、不同线长、不同分频系数的影响。
6.3 测试报告的Excel模板
最后的测试报告,我习惯把所有扫描数据做成一个摘要页:测试时间、适配器型号、SCL目标/实际频率、扫描地址范围、发现设备列表、平均扫描耗时、批量自检错误率。下面再附上原始数据页和波形备注。这个模板固定下来之后,不管是产线验证还是给客户做演示,都特别高效。
Excel模板里最重要的是把VBA宏和单元格公式分离。扫描结果放原始数据,错误率用公式统计,阈值判断用条件格式,这样即便不懂代码的人也能根据颜色判断结果。
6.4 如果你要更快,往哪个方向优化
对多数应用来说,Excel里的VBA调用已经足够了。但如果你有更高的速率需求,比如要测试1MHz模式,或者要在一个事务里处理几百字节的数据,VBA就不太够用。一个是VBA变量和内存管理效率偏低,再一个是Excel主线程会卡UI,批量操作时窗口容易无响应。
优化方向有两个:一是把核心I2C事务封装成一个C语言DLL,从VBA调用DLL,这样事务处理效率高很多;二是使用Python的pyusb或ftd2xx库作为测试框架,Excel只负责展示报表。但后者等于放弃了纯Excel方案,如果只是偶尔调试,VBA已经够用。
我个人建议先把扫描和批处理做成宏按钮,不要放在工作表事件里自动跑,因为工作表事件容易被递归调用搞崩。工作完成后,强烈建议清理MPSSE状态,设置总线为空闲,释放设备句柄。否则下次打开Excel时可能提示设备被占用。
写在最后的建议
这套基于Excel的USB转I2C扫描方案,一开始只是为了快速摸底总线上有什么设备,后来慢慢演化成一个带批量自检和数据汇报的小工具。400kHz下最值得关注的不是SCL频率本身准不准,而是从设备ACk时序、上拉匹配、USB交互延迟这些边缘问题。你把它们逐个排查干净,再回到应用层,很多“设备偶发访问失败”的体验问题就能解释清楚。开发调试工具不一定要用重框架,Excel加VBA配合一个可靠的接口芯片,足以覆盖大部分日常场景。