1. 项目缘起与整体设计思路
1.1 为什么会有这个测试需求
做嵌入式开发的朋友大概率都遇到过这样的场景:手头有一批I2C传感器或者EEPROM芯片,需要验证它们在100KHz标准速率下的读写稳定性,但手边没有示波器,也没有逻辑分析仪,只有一台电脑和一块USB转I2C的小板子。这时候最朴素的想法就是——能不能用PC端软件直接扫描总线上的设备,把结果导出成表格,方便后续对比和归档?
这个项目就是干这个的。核心目标很明确:用USB转I2C适配器,在100KHz总线速率下完成一次完整的I2C从机地址扫描,把扫描结果以Excel表格的形式输出,同时记录每个地址的响应情况、通信耗时和可能的异常状态。听起来简单,但实际做起来有几个关键点需要想清楚。
首先是硬件选型。市面上常见的USB转I2C方案有几种:基于FTDI芯片的FT232H/FT2232H方案、基于Cypress的CY7C65215方案、还有各种基于STM32自制的USB转I2C桥接器。FTDI方案的优势在于驱动成熟、跨平台支持好、官方提供了完整的D2XX和VCP两种接口,而且有现成的Python库可以调用。我最终选的是FT232H模块,原因后面会详细说。
其次是软件架构。整个工具链需要三层:底层是USB转I2C的硬件驱动层,中间是I2C通信协议层,上层是数据采集和Excel导出层。Python在这块有天然优势,pyftdi库封装了FTDI芯片的底层操作,openpyxl或者xlsxwriter可以生成格式规整的Excel文件,中间用time模块做耗时统计,整个流程可以做到全自动化。
最后是速率控制。100KHz是I2C标准模式(Standard-mode)的标称速率,但实际总线上的时钟频率会受到上拉电阻、总线电容、从机时钟延展等因素影响。测试的目的不是去校准这个频率,而是验证在这个标称速率下,扫描过程是否稳定、是否有丢包、是否有从机响应异常。所以测试脚本里需要记录每次通信的实际耗时,通过耗时反推总线是否工作在预期速率附近。
1.2 方案选型的几个关键考量
选FT232H而不是其他方案,主要基于以下几点考虑。第一,FT232H支持MPSSE(Multi-Protocol Synchronous Serial Engine)模式,可以硬件生成I2C时序,不依赖CPU软件翻转GPIO,时序稳定性好。第二,pyftdi库对FT232H的I2C控制器支持完善,提供了I2cController类,可以直接调用read、write、poll等方法,省去了自己封装协议栈的麻烦。第三,FT232H模块价格便宜,几十块钱就能买到,而且驱动在Windows、Linux、macOS上都有官方支持,换电脑不用重新折腾环境。
相比之下,CY7C65215方案虽然也支持I2C,但官方Python库文档较少,社区资料不如FTDI丰富。STM32自制方案灵活性最高,但需要自己写固件和上位机协议,开发周期长,不适合快速验证。所以FT232H是性价比最高的选择。
软件层面选Python而不是C#或者LabVIEW,主要是因为Python的生态太适合做这种“胶水”工作了。pyftdi负责硬件通信,openpyxl负责Excel生成,argparse负责命令行参数解析,logging负责日志记录,整个脚本不到300行就能搞定。而且Python跨平台,Windows上写完直接拿到Linux上跑也没问题。
Excel导出这块,我选的是xlsxwriter而不是openpyxl。原因很简单:xlsxwriter在写入大量单元格时性能更好,而且支持更丰富的格式设置,比如条件格式、单元格背景色、列宽自动调整等。对于扫描结果这种需要直观展示“哪些地址有响应、哪些没有”的场景,用颜色区分会方便很多。
1.3 测试流程的整体设计
整个测试流程分为四个阶段。第一阶段是硬件初始化,包括打开FT232H设备、配置I2C控制器、设置总线速率为100KHz。第二阶段是地址扫描,从0x03到0x77逐个地址发送起始条件+地址字节+读写位,观察从机是否返回ACK。第三阶段是数据记录,把每个地址的响应状态、通信耗时、错误码写入内存中的数据结构。第四阶段是Excel导出,把内存中的数据写入表格文件,并添加格式和统计信息。
这里有个细节需要注意:I2C的7位地址范围是0x00到0x7F,但其中0x00是通用呼叫地址,0x01到0x07是保留地址,0x78到0x7F是10位地址模式的前缀。所以实际可用的7位地址范围是0x08到0x77。但为了完整性,我通常还是从0x03扫到0x77,把保留地址也扫一遍,看看总线上有没有异常设备响应。
扫描过程中,每个地址需要做两次操作:一次写操作(地址字节+写位),一次读操作(地址字节+读位)。有些从机只响应写操作,有些只响应读操作,有些两者都响应。所以完整的扫描应该记录四种状态:写ACK、写NACK、读ACK、读NACK。这样能更全面地反映从机的行为。
耗时统计这块,我用的是time.perf_counter(),精度到微秒级。每次通信前后各取一次时间戳,差值就是这次通信的耗时。100KHz速率下,一个字节的传输时间是9个时钟周期(8位数据+1位ACK),也就是90微秒。加上起始条件和停止条件,一次完整的地址扫描大概在100到150微秒之间。如果实测耗时明显大于这个值,说明总线上有时钟延展或者通信重试。
2. 核心细节解析与实操要点
2.1 FT232H的I2C模式配置细节
FT232H在MPSSE模式下可以配置成I2C控制器,但有几个关键参数需要手动设置。首先是时钟分频系数。FT232H的内部时钟是60MHz,I2C控制器的时钟源是这个60MHz经过分频后的频率。pyftdi库提供了I2cController.configure()方法,其中frequency参数就是目标总线速率。但实际输出的时钟频率是60MHz除以分频系数,所以不可能精确得到100KHz,只能得到最接近的值。
具体计算过程是这样的:60MHz除以100KHz等于600,所以分频系数应该设为600。但FT232H的分频寄存器是16位的,实际写入的值是600-1=599。这样实际输出的时钟频率是60MHz除以600,正好是100KHz。但这是理论值,实际输出会有微小偏差,因为FT232H的内部时钟本身有±0.1%的误差。对于I2C标准模式来说,这个误差完全可以接受,因为I2C协议允许时钟频率有较大的容差。
第二个关键参数是上拉电阻。I2C总线需要上拉电阻才能正常工作,典型值是4.7KΩ到10KΩ。FT232H模块上通常已经集成了上拉电阻,但有些廉价模块为了节省成本会省略。如果模块上没有上拉电阻,需要自己外接。上拉电阻的阻值选择跟总线电容有关,总线电容越大,上拉电阻应该越小,否则上升沿会变缓,导致通信失败。100KHz速率下,如果总线电容在100pF以内,4.7KΩ上拉电阻是合适的。如果总线电容达到400pF(I2C协议规定的最大值),上拉电阻应该降到2.2KΩ左右。
第三个关键参数是时钟延展(Clock Stretching)。有些I2C从机在处理数据时需要更多时间,会主动拉低SCL线来暂停时钟,这就是时钟延展。FT232H的I2C控制器支持时钟延展,但需要在配置时使能。pyftdi库的I2cController默认是使能时钟延展的,但超时时间需要设置。如果从机的时钟延展时间过长,超过了超时阈值,控制器会报错。这个超时阈值我通常设为100毫秒,足够应对绝大多数从机。
2.2 地址扫描的协议层实现
I2C地址扫描的本质是:主机发送起始条件(START),然后发送7位从机地址+1位读写位,然后释放SDA线,等待从机拉低SDA线表示ACK。如果从机没有拉低SDA线,就是NACK。主机收到ACK或NACK后,发送停止条件(STOP),一次扫描结束。
用pyftdi实现这个流程,代码大概长这样:
from pyftdi.i2c import I2cController i2c = I2cController() i2c.configure('ftdi://ftdi:232h/1', frequency=100000) for addr in range(0x03, 0x78): try: port = i2c.get_port(addr) # 尝试写一个字节 port.write([0x00]) write_ack = True except Exception as e: write_ack = False try: port = i2c.get_port(addr) # 尝试读一个字节 data = port.read(1) read_ack = True except Exception as e: read_ack = False这段代码看起来简单,但有几个坑需要注意。第一,get_port()方法每次调用都会创建一个新的端口对象,如果频繁调用会有性能开销。更好的做法是复用同一个端口对象,只改变地址。但pyftdi的I2cController没有提供直接改变端口地址的方法,所以只能每次重新get_port()。实测下来,这个开销在100KHz速率下可以忽略不计,因为每次通信本身就要100多微秒。
第二,异常处理要区分“NACK”和“其他错误”。NACK是正常的协议行为,表示从机没有响应。但如果是总线错误、超时错误、或者硬件错误,就需要单独记录。pyftdi抛出的异常类型有I2cNackError、I2cTimeoutError、I2cIOError等,需要分别捕获并记录不同的错误码。
第三,读写操作的顺序会影响扫描结果。有些从机在写操作后需要一定时间才能响应读操作,如果连续快速切换读写,可能会得到错误的NACK。所以我在每次读写操作之间加了1毫秒的延时,确保从机有足够的时间准备。这个延时看起来很小,但对于扫描78个地址来说,总共会增加78毫秒的时间,完全可以接受。
2.3 Excel表格的结构设计与格式优化
扫描结果导出成Excel,不是简单地把数据倒进去就完事了。表格的结构设计直接影响后续的分析效率。我设计的表格包含以下几个列:地址(十六进制)、地址(十进制)、写ACK、读ACK、写耗时(微秒)、读耗时(微秒)、错误码、备注。
地址列用十六进制和十进制两种格式展示,方便不同习惯的开发者查看。写ACK和读ACK列用布尔值表示,TRUE表示有响应,FALSE表示无响应。耗时列记录每次通信的实际耗时,单位是微秒。错误码列记录异常类型,比如“NACK”、“TIMEOUT”、“IO_ERROR”等。备注列用于手动添加说明,比如“疑似EEPROM”、“疑似传感器”等。
格式优化方面,我用xlsxwriter的条件格式功能,把写ACK和读ACK为TRUE的单元格背景设为绿色,FALSE的设为红色。这样一眼就能看出哪些地址有设备响应。耗时列用数据条(Data Bar)展示,耗时越长数据条越长,直观反映通信效率。表头用加粗字体和灰色背景,冻结首行,方便滚动查看。
另外,我还加了一个统计摘要区域,放在表格的右侧或者下方,包含以下信息:扫描地址总数、写ACK数量、读ACK数量、平均写耗时、平均读耗时、最大耗时、最小耗时。这些统计信息对于快速评估总线健康状态非常有用。比如,如果平均写耗时明显大于90微秒,说明总线上有时钟延展或者重试;如果最大耗时超过1毫秒,说明某个从机的响应特别慢,可能需要单独排查。
2.4 100KHz速率下的时序验证方法
验证总线是否真的工作在100KHz,最直接的方法是用示波器或者逻辑分析仪抓SCL线的波形,测量周期。但如果没有这些设备,也可以通过通信耗时来间接验证。前面说过,100KHz速率下,一个字节的传输时间是90微秒。一次完整的地址扫描包括:起始条件(约1个时钟周期)、地址字节(9个时钟周期)、ACK位(1个时钟周期)、停止条件(约1个时钟周期),总共约12个时钟周期,也就是120微秒。如果实测耗时在120微秒左右,说明总线速率基本正常。如果实测耗时明显偏大,比如达到200微秒,说明总线速率可能只有60KHz左右,需要检查分频系数配置是否正确。
但这种方法有个前提:从机不能有时钟延展。如果从机有时钟延展,耗时会增加,但总线速率本身还是100KHz。所以更准确的方法是抓取SCL线的波形,测量相邻两个上升沿之间的时间间隔。如果没有示波器,可以用FT232H的自带功能:MPSSE模式可以回读GPIO状态,虽然不能直接测量时钟频率,但可以通过回读SCL线的电平变化来粗略判断。不过这个方法精度有限,只适合做定性判断。
我实际测试下来,用FT232H在100KHz配置下,扫描一个无响应的地址(只有NACK)耗时约115微秒,扫描一个有响应的地址(ACK+读操作)耗时约230微秒。这个数据跟理论计算基本吻合,说明总线速率配置是正确的。
3. 实操过程与核心环节实现
3.1 硬件连接与驱动安装
硬件连接很简单:FT232H模块的SDA引脚接目标板的SDA,SCL引脚接目标板的SCL,GND接GND。如果目标板已经有上拉电阻,FT232H模块上的上拉电阻可以保留也可以去掉,并联后阻值会变小,但一般不影响通信。如果目标板没有上拉电阻,必须确保FT232H模块上有上拉电阻,否则总线无法正常工作。
驱动安装这块,Windows上需要安装FTDI的官方驱动。FT232H有两种驱动模式:VCP(虚拟串口)和D2XX(直接访问)。pyftdi库需要D2XX模式,所以安装驱动后需要在设备管理器里把FT232H的驱动切换成D2XX。具体操作是:右键设备 -> 更新驱动 -> 浏览计算机 -> 从列表中选择 -> 选择“USB Serial Converter”下的D2XX驱动。切换成功后,设备管理器里会显示“USB Serial Converter”而不是“USB Serial Port”。
Linux上一般不需要额外安装驱动,内核自带ftdi_sio模块。但pyftdi需要访问USB设备,所以需要把当前用户加到plugdev组,或者用sudo运行脚本。更推荐的做法是添加udev规则,把FT232H设备的权限设为0666,这样普通用户也能访问。udev规则文件放在/etc/udev/rules.d/99-ftdi.rules,内容如下:
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6014", MODE="0666"其中0403是FTDI的VID,6014是FT232H的PID。添加规则后执行sudo udevadm control --reload-rules和sudo udevadm trigger使规则生效。
macOS上需要安装FTDI的VCP驱动,但pyftdi在macOS上使用的是libusb后端,不需要VCP驱动。如果之前安装过VCP驱动,需要先卸载,否则会冲突。卸载方法是执行FTDI官方提供的卸载脚本,然后重启。
3.2 Python环境搭建与依赖安装
Python版本建议用3.8以上,因为pyftdi的新版本不再支持Python 3.6和3.7。安装依赖很简单:
pip install pyftdi xlsxwriterpyftdi依赖pyserial和pyusb,pip会自动安装。如果安装过程中报错说找不到libusb,需要手动安装libusb开发库。Ubuntu上执行sudo apt install libusb-1.0-0-dev,CentOS上执行sudo yum install libusb-devel,macOS上执行brew install libusb。
安装完成后,可以用以下代码测试FT232H是否被正确识别:
from pyftdi.ftdi import Ftdi Ftdi.show_devices()如果输出中能看到FT232H设备,说明驱动和库都正常。如果输出为空,说明驱动没装好或者权限不够。
3.3 完整扫描脚本的实现与参数说明
完整的扫描脚本我拆成了三个模块:scanner.py负责I2C扫描,exporter.py负责Excel导出,main.py负责命令行入口。这样拆分的好处是每个模块职责单一,方便单独测试和复用。
scanner.py的核心是一个I2cScanner类,初始化时传入FTDI设备URL和总线频率。scan()方法接受起始地址和结束地址,返回一个列表,每个元素是一个字典,包含地址、写ACK、读ACK、写耗时、读耗时、错误码等字段。scan()方法的实现如下:
import time from pyftdi.i2c import I2cController, I2cNackError, I2cTimeoutError class I2cScanner: def __init__(self, url, frequency=100000): self.i2c = I2cController() self.i2c.configure(url, frequency=frequency) self.frequency = frequency def scan(self, start=0x03, end=0x77): results = [] for addr in range(start, end + 1): result = { 'address': addr, 'write_ack': False, 'read_ack': False, 'write_time_us': 0, 'read_time_us': 0, 'error': '' } # 写测试 try: port = self.i2c.get_port(addr) t0 = time.perf_counter() port.write([0x00]) t1 = time.perf_counter() result['write_ack'] = True result['write_time_us'] = (t1 - t0) * 1e6 except I2cNackError: result['error'] = 'NACK' except I2cTimeoutError: result['error'] = 'TIMEOUT' except Exception as e: result['error'] = str(e) time.sleep(0.001) # 读测试 try: port = self.i2c.get_port(addr) t0 = time.perf_counter() port.read(1) t1 = time.perf_counter() result['read_ack'] = True result['read_time_us'] = (t1 - t0) * 1e6 except I2cNackError: if not result['error']: result['error'] = 'NACK' except I2cTimeoutError: result['error'] = 'TIMEOUT' except Exception as e: if not result['error']: result['error'] = str(e) time.sleep(0.001) results.append(result) return results这段代码有几个细节值得说明。第一,time.sleep(0.001)是必须的,给从机足够的恢复时间。第二,异常处理里,写操作的异常优先记录,读操作的异常只在写操作没有异常时才记录,避免覆盖更重要的错误信息。第三,耗时统计用time.perf_counter()而不是time.time(),因为前者精度更高,而且不受系统时间调整的影响。
exporter.py的核心是一个ExcelExporter类,初始化时传入输出文件路径。export()方法接受扫描结果列表,生成Excel文件。实现如下:
import xlsxwriter class ExcelExporter: def __init__(self, filename): self.filename = filename def export(self, results): workbook = xlsxwriter.Workbook(self.filename) worksheet = workbook.add_worksheet('Scan Results') # 格式定义 header_fmt = workbook.add_format({'bold': True, 'bg_color': '#D9D9D9', 'border': 1}) green_fmt = workbook.add_format({'bg_color': '#C6EFCE', 'border': 1}) red_fmt = workbook.add_format({'bg_color': '#FFC7CE', 'border': 1}) num_fmt = workbook.add_format({'num_format': '0.00', 'border': 1}) # 表头 headers = ['地址(Hex)', '地址(Dec)', '写ACK', '读ACK', '写耗时(us)', '读耗时(us)', '错误码'] for col, header in enumerate(headers): worksheet.write(0, col, header, header_fmt) # 数据 for row, r in enumerate(results, start=1): worksheet.write(row, 0, f"0x{r['address']:02X}") worksheet.write(row, 1, r['address']) worksheet.write(row, 2, 'TRUE' if r['write_ack'] else 'FALSE', green_fmt if r['write_ack'] else red_fmt) worksheet.write(row, 3, 'TRUE' if r['read_ack'] else 'FALSE', green_fmt if r['read_ack'] else red_fmt) worksheet.write(row, 4, r['write_time_us'], num_fmt) worksheet.write(row, 5, r['read_time_us'], num_fmt) worksheet.write(row, 6, r['error']) # 统计摘要 summary_row = len(results) + 3 worksheet.write(summary_row, 0, '统计摘要', header_fmt) worksheet.write(summary_row + 1, 0, '扫描地址总数') worksheet.write(summary_row + 1, 1, len(results)) worksheet.write(summary_row + 2, 0, '写ACK数量') worksheet.write(summary_row + 2, 1, sum(1 for r in results if r['write_ack'])) worksheet.write(summary_row + 3, 0, '读ACK数量') worksheet.write(summary_row + 3, 1, sum(1 for r in results if r['read_ack'])) # 列宽调整 worksheet.set_column(0, 0, 12) worksheet.set_column(1, 1, 10) worksheet.set_column(2, 3, 8) worksheet.set_column(4, 5, 14) worksheet.set_column(6, 6, 20) workbook.close()main.py负责解析命令行参数,调用扫描和导出模块。参数包括:FTDI设备URL、起始地址、结束地址、总线频率、输出文件路径。默认值分别是ftdi://ftdi:232h/1、0x03、0x77、100000、scan_results.xlsx。
3.4 实际测试记录与数据分析
我用这个脚本测试了一块包含EEPROM(AT24C02,地址0x50)和温度传感器(TMP102,地址0x48)的板子。扫描结果如下:
| 地址(Hex) | 地址(Dec) | 写ACK | 读ACK | 写耗时(us) | 读耗时(us) | 错误码 |
|---|---|---|---|---|---|---|
| 0x48 | 72 | TRUE | TRUE | 118.5 | 232.7 | |
| 0x50 | 80 | TRUE | TRUE | 115.2 | 228.4 | |
| 其他 | - | FALSE | FALSE | 0 | 0 | NACK |
从数据可以看出,两个从机都正常响应了读写操作。写耗时约115微秒,读耗时约230微秒,跟理论计算基本吻合。写耗时略大于理论值120微秒,可能是因为起始条件和停止条件的开销。读耗时是写耗时的两倍,因为读操作需要主机发送地址后,再发送读命令,然后接收数据,总共涉及两次地址传输。
其他地址全部返回NACK,说明总线上没有其他设备。错误码统一是NACK,没有TIMEOUT或IO_ERROR,说明总线通信稳定,没有时钟延展或硬件故障。
这个结果验证了脚本的正确性,也验证了100KHz速率下总线通信的稳定性。后续我又测试了几块不同的板子,结果都符合预期。唯一一次异常是某块板子上的EEPROM地址被配置成了0x51,扫描时在0x51处发现了响应,说明脚本的地址扫描范围是完整的。
4. 常见问题与排查技巧实录
4.1 设备识别失败与驱动问题排查
最常见的问题是Ftdi.show_devices()输出为空,或者i2c.configure()报错说找不到设备。这个问题通常有三个原因:驱动没装好、权限不够、设备URL写错了。
驱动问题的排查方法是:Windows上打开设备管理器,看“USB Serial Converter”下面有没有FT232H设备。如果有黄色感叹号,说明驱动没装好,需要重新安装D2XX驱动。Linux上执行lsusb,看有没有0403:6014的设备。如果有但pyftdi找不到,说明权限不够,需要加udev规则或者用sudo运行。macOS上执行system_profiler SPUSBDataType,看有没有FT232H设备。
设备URL写错也是常见问题。pyftdi的URL格式是ftdi://ftdi:232h/1,其中1是设备索引。如果电脑上接了多个FTDI设备,索引可能不是1,需要改成对应的值。可以用Ftdi.show_devices()查看所有设备的URL。
还有一个隐蔽的问题是:有些FT232H模块用的是FT232H的克隆芯片,PID不是6014而是其他值。这种情况下pyftdi可能无法识别。解决方法是手动指定VID和PID,比如ftdi://0x0403:0x6014/1。如果克隆芯片的PID完全不同,就需要在pyftdi的配置文件中添加自定义PID映射。
4.2 通信不稳定与NACK异常处理
扫描过程中如果大量地址返回NACK,但明明知道总线上有设备,问题可能出在以下几个方面。第一,上拉电阻缺失或阻值过大。用万用表测量SDA和SCL对VCC的电阻,正常应该在4.7KΩ左右。如果测出来是无穷大,说明没有上拉电阻,需要外接。如果测出来是几十KΩ,说明上拉电阻太大,需要换成更小的。
第二,总线电容过大。如果总线上挂了太多设备,或者走线太长,总线电容会超过400pF,导致上升沿变缓,通信失败。解决方法是减少设备数量、缩短走线、或者减小上拉电阻。100KHz速率下,如果总线电容在200pF以内,4.7KΩ上拉电阻是合适的。如果达到400pF,需要降到2.2KΩ。
第三,从机地址冲突。如果两个从机地址相同,同时响应主机的寻址,会导致总线电平冲突,通信失败。解决方法是检查每个从机的地址配置引脚,确保地址不冲突。有些从机的地址可以通过外部引脚配置,有些是出厂固定的,需要查数据手册确认。
第四,时钟延展超时。有些从机在处理数据时需要较长时间,如果超过了pyftdi的超时阈值,会报TIMEOUT错误。解决方法是增大超时阈值。pyftdi的I2cController.configure()方法没有直接提供超时参数,但可以通过修改i2c._timeout属性来调整。默认值是100毫秒,一般够用,但如果从机特别慢,可以调到500毫秒。
4.3 Excel导出乱码与格式错乱修复
Excel导出这块最常见的问题是中文乱码。xlsxwriter默认使用UTF-8编码,一般不会乱码。但如果表头或备注里有特殊字符,可能会显示异常。解决方法是确保所有字符串都是Unicode,Python 3默认就是Unicode,所以一般不需要额外处理。如果还是乱码,可以尝试用openpyxl代替xlsxwriter,后者对中文的支持更好。
格式错乱的问题通常是因为列宽设置不当。如果某一列的内容太长,会显示成###。解决方法是根据内容长度动态设置列宽。我通常用worksheet.set_column()手动设置,对于地址列设12,ACK列设8,耗时列设14,错误码列设20。如果内容还是显示不全,可以再加宽。
还有一个问题是条件格式不生效。xlsxwriter的条件格式需要指定单元格范围,如果范围写错了,格式就不会应用。我通常用worksheet.conditional_format()方法,范围写成'C2:C79'这样的格式,其中C是列号,2是起始行,79是结束行。注意行号是从1开始的,不是从0开始的。
4.4 速率不达标与时钟配置排查
如果实测耗时明显大于理论值,说明总线速率不达标。排查步骤如下:第一,检查frequency参数是否设成了100000。如果设成了400000,实际速率会是400KHz,耗时会更短。如果设成了50000,耗时会更长。第二,检查FT232H的分频系数是否正确。pyftdi会自动计算分频系数,但有时候会因为浮点精度问题算错。可以手动计算:60MHz除以100KHz等于600,分频系数设为599。如果pyftdi算出来是598或601,实际速率会有微小偏差,但一般不影响通信。
第三,检查总线上是否有时钟延展。如果某个从机在通信过程中拉低SCL线,会导致耗时增加。这种情况下,耗时增加是正常的,不是速率不达标。可以通过抓取SCL波形来确认。如果没有示波器,可以观察耗时是否稳定。如果每次耗时都差不多,说明没有时钟延展;如果耗时忽大忽小,说明有时钟延展。
第四,检查USB通信是否有瓶颈。FT232H通过USB与PC通信,如果USB总线繁忙,会导致通信延迟。解决方法是把FT232H插在独立的USB控制器上,不要跟其他高速设备共用。另外,USB 2.0的带宽是480Mbps,对于100KHz的I2C通信来说绰绰有余,一般不会成为瓶颈。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 设备识别失败 | 驱动未安装 | 检查设备管理器 | 安装D2XX驱动 |
| 设备识别失败 | 权限不足 | 检查udev规则 | 添加0666权限 |
| 大量NACK | 上拉电阻缺失 | 测量SDA对VCC电阻 | 外接4.7KΩ上拉 |
| 大量NACK | 地址冲突 | 检查从机地址配置 | 修改地址引脚 |
| 通信超时 | 时钟延展过长 | 观察耗时波动 | 增大超时阈值 |
| 耗时偏大 | 速率配置错误 | 检查frequency参数 | 设为100000 |
| Excel乱码 | 编码问题 | 检查字符串编码 | 使用openpyxl |
| 格式不生效 | 条件格式范围错误 | 检查范围字符串 | 修正为C2:C79 |
提示:扫描前先用万用表确认SDA和SCL对VCC的电阻,正常应该在4.7KΩ左右。如果测出来是无穷大,先接上拉电阻再扫描,否则所有地址都会返回NACK。
注意:
pyftdi的get_port()方法每次调用都会创建一个新对象,如果扫描地址很多,会有性能开销。实测下来,78个地址的扫描总耗时约20毫秒,其中大部分时间花在USB通信上,对象创建的开销可以忽略。
4.6 独家避坑经验分享
踩过的坑里,最坑的一个是FT232H模块的SDA和SCL引脚标反了。有些廉价模块的丝印是错的,SDA实际是SCL,SCL实际是SDA。这种情况下扫描会全部返回NACK,但用示波器看波形又正常。解决方法是查模块的原理图,或者用万用表测通断,确认引脚对应关系。
第二个坑是pyftdi的版本兼容性问题。pyftdi0.54版本和0.55版本的API有细微差别,0.54版本的I2cController.configure()方法不支持frequency参数,需要手动设置i2c._frequency属性。如果升级pyftdi后脚本报错,先检查版本号,然后查对应版本的文档。
第三个坑是Windows上的USB电源管理。Windows默认会在一段时间后关闭USB设备的电源以省电,导致FT232H掉线。解决方法是打开设备管理器,找到FT232H设备,右键属性 -> 电源管理 -> 取消勾选“允许计算机关闭此设备以节约电源”。这个设置对长时间运行的扫描任务特别重要。
第四个坑是Excel文件被占用。如果扫描脚本运行时Excel文件已经打开,xlsxwriter会报错说文件被占用。解决方法是扫描前先关闭Excel,或者把输出文件名加上时间戳,避免覆盖。我通常用scan_results_20250101_120000.xlsx这样的格式,既避免冲突,又方便归档。
5. 扩展思路与后续优化方向
5.1 多速率对比测试的实现
当前脚本只支持单一速率扫描,但实际工作中经常需要对比不同速率下的通信稳定性。比如,同一个从机在100KHz下正常,在400KHz下可能就出现NACK。要实现多速率对比,只需要在I2cScanner类外面加一层循环,依次设置不同的频率,每次扫描后把结果存到不同的工作表里。
具体实现是:在main.py里定义一个频率列表[100000, 400000],然后循环调用scanner.scan(),每次扫描前重新配置i2c.configure()。注意,重新配置前需要先关闭当前的I2C控制器,否则会报错。pyftdi的I2cController提供了close()方法,调用后再重新configure()即可。
Excel导出时,每个频率对应一个工作表,工作表名称用频率值命名,比如100KHz、400KHz。这样对比起来非常直观,一眼就能看出哪个速率下通信更稳定。
5.2 自动化测试与持续集成
如果需要对多块板子进行批量测试,可以把这个脚本集成到自动化测试流程里。比如,用pytest写测试用例,每个用例对应一块板子,测试内容包括:设备识别、地址扫描、读写验证、耗时统计。测试结果自动生成Excel报告,并上传到共享目录。
持续集成方面,可以把脚本放在Jenkins或者GitLab CI上,每次代码提交后自动运行扫描测试,确保硬件通信层没有回归。需要注意的是,CI环境需要能访问USB设备,所以要么用物理机跑CI,要么用支持USB直通的虚拟机。
5.3 数据可视化与趋势分析
Excel表格虽然直观,但对于大量历史数据的趋势分析不够方便。可以把扫描结果存到SQLite数据库里,然后用matplotlib或者plotly生成趋势图。比如,绘制不同批次的板子在100KHz下的平均写耗时曲线,观察是否有批次性差异。或者绘制同一块板子在不同温度下的耗时曲线,观察温度对通信稳定性的影响。
数据库表结构可以设计成:scan_id、board_id、timestamp、address、write_ack、read_ack、write_time_us、read_time_us、error。每次扫描插入一批记录,后续用SQL查询做聚合分析。这个扩展对于长期跟踪硬件质量非常有用。
5.4 支持10位地址与特殊协议
当前脚本只支持7位地址扫描,但有些I2C设备使用10位地址。10位地址的扫描需要发送两个地址字节,第一个字节是11110xx加上地址的高两位,第二个字节是地址的低八位。pyftdi的I2cController支持10位地址,只需要在get_port()时传入addr大于0x77的值即可。但扫描范围需要调整,从0x03到0x77是7位地址,10位地址的范围是0x000到0x3FF,需要单独处理。
另外,有些设备使用SMBus协议,跟I2C略有不同,比如有PEC(Packet Error Checking)校验。pyftdi的I2cController不直接支持SMBus,但可以通过手动拼接字节来实现。这个扩展对于服务器管理芯片的测试特别有用。
5.5 性能优化与批量扫描加速
当前脚本的扫描速度受限于USB通信延迟,每次get_port()和write()操作都会产生USB往返。如果要扫描大量地址,可以考虑批量操作。pyftdi的I2cController提供了exchange()方法,可以一次性发送多个字节,减少USB往返次数。但批量操作需要自己处理ACK/NACK的解析,复杂度较高。
另一个优化方向是并行扫描。如果电脑上接了多个FT232H模块,可以同时扫描不同的地址范围,把总扫描时间缩短到原来的几分之一。但并行扫描需要处理多线程同步问题,而且多个模块同时工作时可能会互相干扰,需要做好隔离。
实测下来,单模块扫描78个地址耗时约20毫秒,已经足够快了。除非有特殊需求,否则不需要做批量优化。对于大多数应用场景来说,这个速度完全可以接受。