Python控制USBSPI转接板读写Flash:从环境搭建到时序调优实战

发布时间:2026/9/1 22:23:55
Python控制USBSPI转接板读写Flash:从环境搭建到时序调优实战 简介本资源是基于Python与图莫斯TOMOS平台开发的USBSPI转接板完整软件实现方案面向计算机、人工智能、通信工程、自动化及电子信息等专业的在校学生、教师与工程师解决USB接口与SPI总线设备间高效通信的软硬件协同问题适用于毕设、课程设计、项目原型验证及嵌入式系统学习进阶。压缩包含554个文件总计12.85MB涵盖33个Python主控脚本、240个VI图形化模块LabVIEW、60个SO动态库、54个MNU菜单配置及多种协议适配BAS文件如usb2spi、usb2can、usb2iic等支撑多协议转换与底层驱动调用。已有319人下载学习所有代码均经实机测试运行通过附带README说明文档结构清晰、模块解耦支持在现有基础上快速扩展CANFD、LIN、PWM等其他外设协议功能具备直接复用与二次开发双重价值。 上周帮同事调一块新打样的PCBA板子上面有一颗SPI NOR Flash和几片MEMS传感器。系统还没起来第一件事就是把Flash的JEDEC ID读出来确认贴片没贴错。我手头正好有一块图莫斯的USBSPI转接板本来想用厂家自带的上位机点几下就行结果发现要连续测8片板子、每片还要扫三组时钟频率用图形界面一个个点完全不现实。于是干脆用Python基于这块转接板写了一套控制和自动化测试脚本整个过程踩了不少坑这篇就把从零到能稳定读写Flash的完整过程记录下来。如果你手上也有类似图莫斯这种USBSPI转接板不想被厂商的上位机绑住想用Python自己控制时序、批量操作、甚至接入自动化流程这篇文章应该能帮你省下不少时间。我会从环境准备、代码结构、时序细节、踩坑排查到最终验证按一条实际跑通的技术路线来讲。1. 图莫斯这类USBSPI转接板为什么值得用Python重新写一套上位机很多人拿到USBSPI转接板的第一反应是厂商不是送上位机了吗为什么还要自己写这个问题我在一开始也被问过。但真正用到批量产测和参数扫描场景时图形界面软件的局限性会很快暴露出来。1.1 厂商自带工具的痛点单次调试够用自动化完全乏力厂商的通用上位机一般长这个样子选设备、选SPI模式、填几个十六进制字节、点发送、看返回。用来单个芯片点两下没问题但我的需求是连续测8片板子每片读取Flash ID、写一段测试数据、再读回来校验三组时钟频率各跑一轮。手动操作一两次还能忍24次下来既费时间又容易漏看结果。更麻烦的是通用上位机大部分不支持断言逻辑。你点一下收回来一堆数据还得自己用眼睛对比是不是预期值。而用Python写脚本读回数据之后立刻就可以做assert判断失败就打印板号、频率、命令阶段直接告诉你哪一片哪一步出了问题。数据也可以直接落到CSV或者数据库里后续做质量分析非常方便。1.2 Python和USBSPI转接板之间的通讯链路到底长什么样想明白怎么写代码先得清楚数据从Python脚本到SPI从机芯片之间经过了哪几层。以图莫斯这类USBSPI转接板为例链路是这样的Python脚本 → 厂商DLL/系统驱动 → USB协议 → USB桥接芯片 → SPI引脚 → 从机芯片USB桥接芯片是核心它把PC发过来的USB命令包解释成具体的SPI时序。常见方案有FTDI的FT2232H/FT232H也有国产的CH347甚至有些低成本板子用一颗带USB的MCU模拟时序。Python层做的事情其实只是“下发命令”和“读取返回”真正产生SCLK时钟脉冲的是桥接芯片内部的硬件引擎或固件。这条链路里最容易出问题的是中间两层驱动装的不对Python库打开不了设备或者时序参数配置不对SCLK波形和从机要求不匹配。后面第4节会详细讲排查过程。1.3 拿到板子第一件事确认芯片方案再决定用哪条技术路线我见过不少人在这一步栽跟头。拿到图莫斯转接板插上电脑然后直接去网上搜“Python USBSPI”找到的代码跑不通原因就是不同板子的底层芯片方案不一样Python库也跟着不一样。所以我建议拿到板子的第一件事是看手册或者拆开看主控丝印。网上资料和驱动安装包通常都会写明用的是FTDI还是CH347或者直接提供自己的DLL/SDK。判断方法很简单Windows下打开设备管理器看“通用串行总线控制器”里出现的是什么设备名。FTDI方案一般显示“USB Serial Converter”或“FDTI FT2232H”CH347会显示“CH347”或“USB-VID/PID”。Linux下用lsusb命令看VID/PID。FTDI通常是0403:6014CH347是1a86:55db。在Windows上打开设备管理器看“通用串行总线控制器”里出现的是什么设备名。FTDI方案一般显示“USB Serial Converter”或类似名称CH347会显示“CH347”或对应的VID/PID。Linux下用lsusb命令看VID/PID。FTDI通常是0403:6014CH347是1a86:55db。确认方案之后再选技术路线芯片方案推荐的Python路线说明FTDI FT2232H/FT232Hftd2xx、pylibftdi生态最成熟文档多网上的例子也多CH347CH347官方DLL ctypes官方提供Windows DLL需要自己写封装厂商私有MCU方案厂商DLL ctypes/cffi一般跟着SDK走用官方协议命令最稳图莫斯如果用的是FTDI兼容方案那恭喜你Python生态里选项很丰富。如果是私有方案也别慌用ctypes调DLL一样能搞定只是需要多点耐心读厂商的头文件。2. 环境搭建驱动链路和Python库选型最容易卡住的地方说句实话这个项目里最容易让人想砸电脑的环节不是写代码而是环境准备。我见过太多人卡在设备枚举这一步明明转接板插上了Python里就是打不开。2.1 D2XX、VCP、libusb三种驱动模式的区别USBSPI转接板插上电脑后可以工作在几种不同的驱动模式下理解这个区别能少走很多弯路。VCP模式虚拟串口设备枚举成一个COM口你通过串口协议和它通信。这种模式最简单用pyserial就能收发但缺点是很多板子的VCP固件只支持厂商私有命令时序控制能力有限适合低速简单场景。D2XX模式FTDI私有驱动FTDI芯片的专属模式绕过系统串口层直接调用FTDI的DLL。这种模式延迟低、能直接发MPSSE命令是做SPI/I2C/JTAG的正统玩法。libusb/WinUSB模式通用USB驱动跨平台pyusb就是基于它。理论上最灵活但如果板卡固件不是标准类协议你就要自己构造USB控制传输和批量传输包非常痛苦。图莫斯这类板卡如果基于FTDI方案出厂默认装的是D2XX驱动这会让你少折腾不少。如果系统把它识别成COM口了你就要注意是不是被VCP驱动占了。提示如果你发现ftd2xx找不到设备但设备管理器里显示的是COM口多半是VCP驱动把D2XX模式覆盖了。去设备管理器手动把驱动换成FTDI的D2XX版本或者用FTDI官方的Driver Fix工具重新绑定。2.2 Python库选型别急着用pyusb网上搜索“Python USBSPI”出来的第一屏结果大概率是pyusb但说实话我不推荐日常调试直接上pyusb。pyusb是通用USB库它需要你自己组织符合设备固件协议的数据包不同厂家的板子协议不一样网上抄的代码几乎不可能开箱即用。更实际的选型思路是这样的库/方案优点缺点适用场景厂商DLL ctypes最稳定完全匹配硬件协议API各家不一样不通用正式项目、量产脚本ftd2xxFTDI官方DLL的Python封装MPSSE命令可控只适用于FTDI芯片命令偏底层FT2232H/FT232H板卡pylibftdi封装得比较简洁跨平台需要额外装libftdi偏向FTDI方案的快速开发pyusb通用、不用装厂商SDK要自己拼协议包调试成本高学习USB协议或产品是标准HID/批量传输最终我建议的路线是如果厂商提供了DLL用ctypes调DLL是优先级最高的因为厂商在上位机里做的所有时序优化你都能直接拿到。如果厂商没给DLL但板子是FTDI方案那就用ftd2xx。只有前两条路都走不通才考虑pyusb。2.3 先把设备枚举跑通再继续写业务逻辑不要一上来就写完整读写代码。先做最小验证确认Python能打开设备。以FTDI方案为例装好D2XX驱动后在虚拟环境里安装ftd2xxpip install ftd2xx然后跑这段import ftd2xx # 返回当前接入的FTDI设备数量 count ftd2xx.createDeviceInfoList() print(f检测到 {count} 个FTDI设备) if count 0: info ftd2xx.getDeviceInfoDetail(0) print(f设备索引0: {info})如果这里能打印出设备信息说明驱动链路通了。如果返回0按照上面的建议去检查驱动模式。对于CH347或者私有DLL方案类似的最小调用一般是import ctypes # 假设厂商DLL名字为 TuomosUsbSpi.dll具体以实际SDK为准 lib ctypes.WinDLL(TuomosUsbSpi.dll) dev_count ctypes.c_int() lib.USBSPI_GetDeviceCount(ctypes.byref(dev_count)) print(f检测到 {dev_count.value} 台设备)不同DLL的接口名不同但思路一致先枚举设备数量再打开索引0然后才能配置SPI参数。把这一步跑通后面的代码才有意义。3. 软件实现从枚举设备到一次完整的SPI读写事务环境通了以后接下来就是写核心代码。这一节讲的不是简单的“打开设备、发数据”而是一个能应付实际项目的工程结构。我在写这个项目时把代码拆成了驱动层、协议层和业务层好处是以后换转接板品牌只需要替换最底一层。3.1 工程目录怎么组织代码模块怎么拆一个比较顺手的目录结构长这样usbspi_tool/ ├── main.py # 命令行入口解析参数 ├── requirements.txt ├── usbspi/ │ ├── __init__.py │ ├── driver.py # 驱动层封装ctypes/ftd2xx统一入口 │ ├── spi.py # SPI协议层时钟、模式、读写事务 │ └── devices/ │ ├── flash.py # SPI NOR Flash 的常用命令封装 │ └── sensor.py # 传感器寄存器读写封装 ├── tests/ │ ├── loopback.py # 发送回环测试 │ └── flash_id.py # Flash ID验证用例 └── scripts/ ├── scan_clock_rate.py # 不同频率下扫描读取 └── batch_test.py # 多板批量测试驱动层只负责跟USB设备通信不管SPI协议协议层负责组织SCLK、MOSI、MISO、CS的时序关系业务层才关心读Flash ID、写传感器寄存器这些具体功能。分层清楚之后排查问题可以快速定位读数据不对先看协议层设备打不开只看驱动层。3.2 封装底层驱动把厂商API收敛到一个文件里无论底层是ftd2xx还是厂商DLL我建议都封装成一个统一的Driver类。这里以FTDI的ftd2xx为例做一个最小可用的SPI基础驱动# usbspi/driver.py import ftd2xx class FtdiSpiDriver: def __init__(self, device_index0): self.dev ftd2xx.open(device_index) # FT2232H通常需要把配置切到MPSSE模式 self.dev.setBitMode(0, 0x02) # 0x02 MPSSE模式 def close(self): if self.dev: self.dev.close() def write(self, data: bytes) - None: 向MPSSE通道发送裸数据命令或命令数据 self.dev.write(data) def read(self, length: int) - bytes: 从MPSSE通道读取指定长度的返回数据 return self.dev.read(length, timeout2000)如果用的是厂商DLL也在这个文件里用ctypes绑一下对外暴露同样的write/read接口。这样上层的SPI时序代码完全不用关心底层到底是FTDI还是CH347。3.3 SPI参数配置频率、CPOL、CPHA、片选模式SPI是四线协议SCLK、MOSI、MISO、CS但真正决定一次通信成败的是时钟极性CPOL和时钟相位CPHA。这两个参数组合成四种模式SPI模式CPOLCPHA特点Mode 000SCLK空闲低电平数据在上升沿采样最常用Mode 101SCLK空闲低电平数据在下降沿采样Mode 210SCLK空闲高电平数据在下降沿采样Mode 311SCLK空闲高电平数据在上升沿采样Flash常见很多从机芯片数据手册里不会直接写“Mode 0”而是写“SCLK idle low, data captured on rising edge”这种话翻译过来就是Mode 0。我在这个项目里用的是W25Q128这颗Flash数据手册明确支持Mode 0和Mode 3。新手最容易犯的错误是默认所有芯片都是Mode 0。我实测过一颗湿度传感器SHT30它要求的是Mode 3用Mode 0去读返回的数据全是乱的。所以每接一颗新芯片第一件事就是查手册里的时序图确认CPOL和CPHA。在ftd2xx里配置SPI模式需要直接给MPSSE引擎下发命令字节# usbspi/spi.py # 以FT2232H的MPSSE为例配置SCLK频率和模式 def _build_clk_config(self, freq_hz: int) - bytes: 根据目标频率计算SCLK分频系数 base_clock 60_000_000 # FT2232H内部时钟60MHz divisor max(0, min(65535, int(base_clock / freq_hz) - 1)) return bytes([0x86, divisor 0xFF, (divisor 8) 0xFF]) def _build_mode_config(self, cpol: int, cpha: int) - bytes: MPSSE设置CPOL/CPHA的命令 # 0x8A 是设置SPI模式的命令具体位定义见FTDI AN_108 val (cpol 1) | cpha return bytes([0x8A, val 0xFF])在实际使用中一般把频率、CPOL、CPHA封装成一个configure_spi()方法在每次打开设备后调用一次即可。CH347方案也有类似的分频寄存器但命令格式不同看官方手册对照着改就行。3.4 写读时序一次事务内完成CS拉低、发送、接收、CS拉高SPI通信是一次完整事务CS信号从高拉低表示从机被选中然后SCLK开始翻转数据在MOSI/MISO上流动最后CS拉高表示事务结束。Python代码写得好不好关键看能不能保证这个事务的完整性。如果板卡的CS是硬件自动控制那么一次写操作通常就是CS自动拉低、发完数据、CS自动拉高。但很多USBSPI转接板的CS引脚是独立的GPIO需要软件手动拉低再拉高。我强烈建议在事务封装里同时支持这两种方式。下面是我在项目里实际使用的写读封装# usbspi/spi.py class SpiDevice: def __init__(self, driver, freq_hz1_000_000, mode0): self.drv driver self.freq_hz freq_hz self.mode mode self.cs_manual False # 是否手动控制CS def transfer(self, tx: bytes, rx_len: int 0) - bytes: 执行一次完整SPI事务发送tx同时接收rx_len个字节 if not self.cs_manual: # 硬件自动CS模式直接把数据发出去 self.drv.write(tx) rx b if rx_len 0: rx self.drv.read(rx_len) return rx # 手动CS模式拉低、传输、拉高 self.set_cs(False) # CS低选中 self.drv.write(tx) rx b if rx_len 0: rx self.drv.read(rx_len) self.set_cs(True) # CS高释放 return rx注意一个容易忽略的细节SPI是全双工协议发送和接收是同时进行的。也就是说你发送一个读命令字节的同时从机会在MISO线上回一个字节但那个字节通常是无效的垃圾数据。所以读Flash ID时标准的做法是先写0x9F命令再写三个空字节或者NOP同时把MISO上返回的三个字节读出来。很多新手在这里写错读命令只写了命令字节就试图读数据结果读回来的东西对不上。以W25Q128读JEDEC ID为例完整代码def read_jedec_id(flash: SpiDevice) - bytes: # 9F命令 3个dummy时钟字节同时读回3字节ID tx bytes([0x9F, 0x00, 0x00, 0x00]) rx flash.transfer(tx, rx_len4) # rx[0]是发送0x9F时从机回显的垃圾真正ID在rx[1:]里 return rx[1:]如果不理解全双工这个特性你就有可能把MISO上第一个时钟周期的垃圾数据当成ID高位字节然后怎么调都发现ID第一位不对。4. 实测中踩过的坑从全零读数到状态机错乱代码写完只是开始真正让人长教训的是调试过程中的各种“看似随机”的问题。这一节把我踩过的坑完整列出来每个问题都包含根因分析让你以后遇到类似现象能快速定位。4.1 读回全0xFF时钟极性和相位检查的完整排查链路第一次跑通代码我高高兴兴去读Flash ID结果返回FF FF FF。看到这个结果第一反应是Flash里本来就是空白但空白Flash的ID不应该受影响。所以问题出在通信链路。我当时按这个顺序排查检查接线MOSI/MISO两根线是不是接反了。MOSI接成了MISO、MISO接成了MOSI最常见的低级错误。用万用表蜂鸣档量一遍更稳妥。检查供电Flash有没有上电。3.3V电源没接芯片根本不会响应。检查电平是否匹配转接板输出3.3V还是5V。后文第4.4节细说。检查CPOL/CPHA用逻辑分析仪抓SCLK波形确认空闲电平是不是和配置一致。如果配置的是Mode 0但波形空闲高电平等于实际工作在Mode 2采样点全乱了。第四步是最容易忽略的。我用逻辑分析仪抓了波形才发现驱动库默认把MPSSE配置成某种模式但我前面又额外发了一条0x8A模式设置命令导致最后生效的模式和我以为的不一样。调试时我教训是每配置一次模式就抓一次波形确认不要靠猜。4.2 CS时序不对事务结束过早导致从机状态机错乱有段时间我读Flash ID是正常的但写数据后读回来全是乱的。后来发现是CS时序问题。很多SPI Flash芯片要求在命令最后一个SCLK边沿之后CS保持低电平至少一段时间然后才能拉高。如果CS拉得太快芯片内部状态机还没锁存完命令就以为事务结束了数据就丢了。USBSPI转接板通过USB传输有固有延迟Windows下USB调度又可能引入毫秒级不稳定性所以软件控制CS时拉高之前加一个微小延时是比较稳妥的做法def end_transaction(self): # 确保从机有足够时间锁存数据 time.sleep(0.0001) # 100us实际按从机手册调整 self.set_cs(True)延时太长会拉低吞吐太短又不够具体数值看从机数据手册里的CS高/低电平保持时间。W25Q128这类芯片写寄存器指令通常需要几十微秒的保持时间100us足够。4.3 大数据块读写失败缓存、超时和分块策略第一次尝试用Python读整个Flash的1MB内容时我直接发了一次传输命令结果超级慢还经常超时失败。后来明白USBSPI转接板的FIFO和DLL读写有内部缓存上限一次性传几MB数据底层早就溢出了。解决办法是分块。对NOR Flash来说按扇区或者按4KB块读既符合Flash本身的存储结构又不会碰到USB传输的缓存限制def read_flash(flash: SpiDevice, start_addr: int, length: int, chunk_size: int 256) - bytes: result bytearray() addr start_addr remaining length while remaining 0: cur min(chunk_size, remaining) # 读命令 0x03 3字节地址 dummy数据 tx bytes([0x03, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF]) b\x00 * cur rx flash.transfer(tx, rx_len4 cur) result.extend(rx[4:]) # 去掉命令和地址段的无效数据 addr cur remaining - cur return bytes(result)分块大小要根据实际从机来定。有些从机在CS拉高之后才会处理数据那每读一块都要重新拉低CS有些从机允许CS持续拉低连续读下一块那就可以用更大块或者保持CS不释放进一步提升速度。这个优化点需要看具体芯片手册。4.4 电平匹配问题3.3V从机接5V转接板这块板子如果IO电平是5V而你的传感器是3.3V供电那SPI通信大概率不稳定甚至可能烧坏从机引脚。我实测过一次把一颗3.3V的MEMS传感器接到5V电平转接板上读数完全随机刚开始还以为是时序配置问题查了半天发现是电平不匹配。图莫斯这类板子的IO电压一般会有跳线或通过VCCIO引脚设置。用之前一定要确认转接板的逻辑电平是3.3V还是5V。从机芯片的输入引脚能不能容忍5V。如果不能容忍就需要加电平转换芯片比如TXS0108E、TXB0108这类双向电平转换器。千万不要因为“我上次也这么接没坏”就放松警惕逻辑电平不匹配的随机故障比烧芯片更难查。5. 端到端验证用逻辑分析仪和Flash ID建立信心代码能跑起来只是第一步真正能放心把它用于产测脚本还需要一套端到端验证方法。我对项目负责所以我给自己定了三条验证标准回环通过、波形正确、真实芯片能读到稳定ID。5.1 回环测试先验证发送通路在接任何真实从机之前先把MOSI和MISO用杜邦线短接。然后写一串数据读回来的数据应该完全一样。回环测试能验证USB链路、驱动配置、读写接口有没有问题def loopback_test(spi: SpiDevice): tx_data bytes(range(256)) # 0x00-0xFF rx_data spi.transfer(tx_data, rx_lenlen(tx_data)) # 回环模式下读到的就是当前时钟周期发送的字节 # 注意不同板卡回环模式下数据偏移量可能不同对比时需调整 if tx_data in rx_data or rx_data in tx_data: print(回环测试通过) else: print(f回环测试失败发送{len(tx_data)}字节接收{len(rx_data)}字节)回环测试通过只代表你的发信通路没问题不代表和某个具体从机就能握手成功。但它能帮你把问题快速分层回环都不过问题在USB/驱动/接线回环过了但接从机不对问题在时序参数或从机本身。5.2 逻辑分析仪抓波形正确的SPI时序长什么样不要凭感觉调参。只要你手头有逻辑分析仪哪怕是几十块钱的USB逻辑分析仪接上SCLK、MOSI、MISO、CS四根线立刻就能看清问题。我一般这样抓波形把逻辑分析仪的采样率设成SCLK的10倍以上。如果SPI时钟1MHz采样率至少10MHz最好20MHz。触发方式设成CS下降沿触发因为一次SPI事务从CS拉低开始。抓一次读Flash ID的命令看四个通道的波形。一个正确的读ID波形应该满足CS从高拉低然后SCLK开始翻转。MOSI上依次出现0x9F和3个空字节每个字节8个bit顺序清晰。MISO上在命令字节期间是垃圾位后面3个字节才是有效的ID数据而且数据在SCLK采样沿是稳定的。CS拉高SCLK停止翻转。如果数据在SCLK边沿上跳变而不是稳定说明CPHA选错了。如果SCLK空闲电平不对说明CPOL选错了。这些用逻辑分析仪一眼就能看出来比来回改代码有效率得多。5.3 读取Flash JEDEC ID作为稳定用例JEDEC ID是SPI Flash的身份证命令固定是0x9F返回三个字节厂商ID、容量ID、类型ID。以W25Q128为例返回EF 40 18。这个用例干净利落不涉及擦除写入不会破坏Flash内容用来做每日冒烟测试再合适不过。我写的Flash设备类里专门有个方法def read_id(self): tx bytes([0x9F, 0x00, 0x00, 0x00]) rx self.transfer(tx, rx_len4) if len(rx) 4: return rx[1], rx[2], rx[3] return None, None, None实际批量测试时我会把读到的ID和预期值做对比如果不一致就报错并记录当前板号。这套逻辑后来直接变成了产线测试的第一步几十块板子插上去自动跑大大节省了人工确认的时间。5.4 真实场景下的性能摸底能跑多快、瓶颈在哪做产测最好先知道工具的极限在哪里。以FT2232H在20MHz SCLK下为例理论带宽看着不错但实际受USB传输开销影响Python脚本测下来纯读取吞吐在5~8Mbps左右。如果是CH347方案SCLK频率标称更高但Python逐次调用的开销仍然会是瓶颈。如果你需要烧录几十MB的固件用Python硬跑会非常慢。我的建议是区分场景如果是产线批量烧录用厂商的C/SDK或者专业烧录器如果是实验室做小批量验证、读回固件做对比、跑自动化寄存器测试Python足够胜任。性能摸底也很简单就是读固定大小的数据用time模块统计耗时import time start time.perf_counter() data read_flash(flash, 0, 65536, chunk_size256) elapsed time.perf_counter() - start print(f读取64KB耗时 {elapsed:.3f}s吞吐约 {65536 / elapsed / 1024:.1f} KB/s)实测下来你会发现USB传输延迟占了很大一部分时间分块大小选得好不好直接决定吞吐量。这个数据留着以后做固件升级时间估算时就能直接用。6. 从单一任务到通用工具多从机、模式切换与工程化封装如果只是读一块Flash这篇文章可以到此为止。但实际项目中USBSPI转接板经常要接多个设备还可能在同一块板子上切换SPI、I2C、UART等不同模式。这一节讲讲工程化扩展的思路。6.1 多片SPI从机的CS管理USBSPI转接板上一般只有一组SPI总线和几个CS引脚。如果你要接多片从机需要把每个CS描述成一个独立的“选择线”。软件上我建议为每个从机创建一个设备对象内部记录它占用哪个CS传输时先切到对应CS再发命令。class MultiCsSpi: def __init__(self, driver, cs_pins): self.drv driver self.cs_pins cs_pins # 例如 [0, 1] 表示两个CS引脚 def select(self, cs_index: int): # 拉低目标CS同时确保其他CS保持高 # 具体GPIO操作取决于板卡API pass def transfer(self, cs_index: int, tx: bytes, rx_len: int 0) - bytes: self.select(cs_index) self.drv.write(tx) rx self.drv.read(rx_len) if rx_len 0 else b self.deselect(cs_index) return rx多从机场景下容易犯的错是切换CS时没有先释放上一个从机。有些从机在CS拉高之后还要恢复时间才能再次被选中连续快速切换会导致第二次通信失败。我的做法是在每次select前面强制拉高所有CS加一小段延时再拉低目标CS。6.2 同一块板子切I2C/UART/GPIOFT2232H这类芯片支持多个通道比如A通道做SPIB通道做UART。CH347也支持SPII2C组合模式。这意味着同一块USBSPI转接板可以通过Python切换工作模式变成多协议调试器。但这里有个很大的坑很多板卡在切换工作模式后USB设备会重新枚举。比如从SPI模式切到UART模式设备会短暂断开再重新连接之前打开的句柄就失效了。我第一次遇到时脚本直接抛错花了好久才反应过来是设备重枚举导致的。解决办法是在切换模式之后做“重试等待”def wait_device_ready(timeout5.0): start time.time() while time.time() - start timeout: count ftd2xx.createDeviceInfoList() if count 0: time.sleep(0.5) # 等驱动重新绑定稳定 return True time.sleep(0.1) return False切换到UART之后就可以用pyserial直接操作串口。注意FTDI的VCP串口被pyserial打开时波特率配置、读写超时这些都需要重新设置和SPI通道的状态没有任何关系。6.3 给工具包一层CLI或HTTP接口写到最后如果只有你自己能看懂代码工具价值有限。我最后做的一件事是封装了一个简单的命令行入口让产线同事不写代码也能用python -m usbspi flash_id --device 0 python -m usbspi read --address 0x000000 --size 1024 --file dump.bin python -m usbspi verify --file firmware.bin命令行的好处是容易集成到各种脚本和CI流程里。如果需求更复杂比如让网页或者手机端触发测试可以再用FastAPI包一层HTTP接口。不过我要提醒一句HTTP序列化开销对大数据块读写影响很大如果要做烧录这类高速操作别走HTTP直接本地Python调用要好得多。这个项目我用了一个多月最大的体会是图莫斯这类USBSPI转接板本身不算复杂复杂的是各种隐性条件——驱动模式、时钟参数、CS时序、电平匹配每一个都能让人卡上半天。而Python的优势在于你可以把排查过程快速变成可复现的脚本这次踩过的坑下次就不会再踩。建议你拿到板子后先把回环测试和Flash ID读取做成两个固定用例每次新接芯片都先跑一遍很多莫名其妙的问题都能提前暴露出来。本文还有配套的精品资源点击获取