ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CH341 StreamI2C详解:从参数配置到EEPROM读写实战

CH341 StreamI2C详解:从参数配置到EEPROM读写实战 我最早被 CH341 吸引是想把它当 USB 转 I2C 调试器用一头插 PC另一头通过 P0.0/P0.1 接一个 AT24C02然后用官方 DLL 把EEPROM 的内容读出来。理想很丰满实际一打开手册就懵了——CH341 的 I2C 操作被拆成“快速命令”和“流模式”里面还有一个 CH341StreamI2C 函数参数有六七个iWriteBuffer 里到底该填什么网上说法还五花八门。这篇文章就把我后来踩通的路子讲清楚StreamI2C 的参数配置逻辑、怎么用它实现 EEPROM 读写以及怎么把它扩展到任意自定义 I2C 设备上。如果你也在用 CH341 折腾 I2C 时序、调试传感器或者做自定义设备驱动这篇应该能帮你少走不少弯路。1. StreamI2C 和“快速命令”之间该怎么选1.1 两种 I2C 操作方式定位完全不同CH341 芯片内部其实提供了两套 I2C 玩法。一套是快速命令官方文档里有时候也叫它 Fast I2C / I2C 快速模式。这种模式的特点是主机发起一次 USB 控制传输芯片自动完成“起始条件 器件地址 若干数据字节 停止条件”整个时序对用户几乎是黑盒。你只需要告诉它设备地址、写缓冲区和读长度它就把事情办完了。平时大家用 CH341 读写 24 系列 EEPROM 时最常见的 CH341ReadI2C / CH341WriteI2C 就是走这条路。另一套就是本文要重点讲的 StreamI2C也叫 I2C 流模式。它的思路完全反过来不给你做任何封装而是把 I2C 总线上的每一个电平变化、每一个时钟周期都暴露给主机。你要产生一个起始条件就得告诉芯片“先让 SCL 保持高、SDA 从高拉低”你要发一个数据位就得告诉芯片“现在 SDA 输出高/低”。所有时序细节由你自己拼装芯片只负责按顺序在引脚上执行。1.2 为什么调自定义设备时必须用 StreamI2C快速命令虽然省事但灵活性非常低。我遇到的第一个痛点是要读写一个寄存器地址为 2 字节的温湿度传感器。快速命令的驱动实现里往往只能发“器件地址 单字节寄存器地址 数据”寄存器地址一宽就出问题。第二个痛点是要做“先写寄存器地址再重复起始再读数据”这种操作。快速命令里有的封装会把读写拆成两次独立事务中间总线会被释放某些严格要求原子操作的外设就会返回错误数据。StreamI2C 在这两个场景下就是救星。因为一切的起始、停止、重复起始、应答、非应答都由你手动控制理论上你只要能把 I2C 时序图画出来就能把时序用 CH341StreamI2C 逐位“翻译”出来。它本质上就是一个可批量执行的软件模拟 I2C只不过底层用了 USB 控制传输来抬高速度比你用 GPIO 一根一根翻要快得多。1.3 什么情况别用 StreamI2C也不是说流模式万能。如果你只是电脑端批量读个 AT24C02、AT25 系列的 SPI Flash或者只是简单给某个 I2C 设备写几个配置寄存器那快速命令更合适。流模式一次调用要构造的缓冲区长度很大每个 bit 都要占字节USB 控制传输的开销会被放大大量读写时明显更慢。我自己的习惯是调试阶段用它因为能看到每一步固化成工具或脚本跑量的时候如果设备支持就换回快速命令。2. CH341StreamI2C 的 6 个参数逐个拆开看2.1 函数原型和 iIndex先看官方 DLL 里这个函数的原型BOOL CH341StreamI2C( ULONG iIndex, ULONG iChipMode, ULONG iWriteLength, PVOID iWriteBuffer, ULONG iReadLength, PVOID iReadBuffer );iIndex 是设备编号。机器上只插了一块 CH341 的时候就传 0这个参数本身没什么坑。但如果你同时插了好几块 CH341就要注意Windows 下的设备列表索引不一定跟物理插入顺序完全对应最好先用官方工具确认你要操作的到底是哪一块否则可能出现“明明插的是 A 板却写到了 B 板”这种离奇问题。2.2 iChipMode速度档位的选择和坑iChipMode 用来选择 I2C 通信速度档位。不同版本的驱动对它的定义不完全一样常见做法是 0 代表低速/标准模式1 代表高速模式。我强烈建议调试阶段一律从低速开始不要一上来就追求 400kHz。之前我调一个 STM32 和 CH341 对接的板子EEPROM 偶发写错字节抓破头皮也没想明白后来才发现是 iChipMode 设成了高速档而板上的上拉电阻偏大SCL 上升沿不够陡。CH341 内部产生 SCL 是靠定时器延时的速度档越高对总线负载和上拉越敏感。把速度降下来之后问题立刻消失。所以遇到奇怪的时序毛刺先把档位降一档再说。2.3 iWriteBuffer不是“要写的数据”而是“时序剧本”这是 StreamI2C 最核心的认知转换。很多第一次用的人会想iWriteBuffer 是不是直接放要写入 EEPROM 的内容真这么想就错了。iWriteBuffer 里放的是一段“时序命令序列”每个字节代表总线上的一个最小动作。最常见的编码方式是每个字节的低 3 位有效。bit0标志位。为 1 表示这一字节会执行一次数据位传输/采样动作CH341 会在 SCL 上自动产生一个时钟脉冲为 0 表示这一字节是电平控制字节直接控制 SCL/SDA 的电平状态。bit1当 bit0 为 0 时用来设置 SCL 的电平。bit2当 bit0 为 0 时用来设置 SDA 的电平当 bit0 为 1 时它表示想要输出到 SDA 的数据值。所以“写一个数据位 0”的命令数组大概可以拆成先把 SCL 拉低再让 SDA 输出 0 并产生一个 SCL 脉冲最后再拉低 SCL。至于具体是哪个字节值要看你手上驱动头文件里对位域的定义。不同版本驱动里 bit1 和 bit2 的位置有可能交换我写代码时习惯在驱动头文件里先确认清楚这三个 bit 的含义再开始拼字节省得后面返工。2.4 iReadBuffer把采到的电平捞回来流模式里“读”并不是一次读一个字节这么简单。你在 iWriteBuffer 里每安排一个“读数据位”动作CH341 就会在该动作对应的 SCL 高电平期间把 SDA 线上的电平采样下来存到 iReadBuffer 里。一个常见的对应关系是一个读位动作对应返回缓冲区里的一个字节解析时只看这个字节的约定 bit 位比如 bit2。也就是说如果我要读一个字节那 iWriteBuffer 里要连发 8 个“读位”动作iReadBuffer 里会返回至少 8 个字节每个字节里藏着那一位的电平值。很多人以为 iReadBuffer 会像普通读函数一样直接返回一个完整的字节这就是时序错位的根源。你把 iWriteBuffer 构造对了返回数据也是严格按位存放的解析方式必须跟着位走。2.5 返回值不等于 I2C 成功最后一个参数认知特别重要CH341StreamI2C 返回 TRUE只代表 USB 控制传输成功发出去并且驱动正常处理完了不代表 I2C 总线上的从设备给了 ACK也不代表你的数据被正确应答。要确认设备是否 ACK你得自己在命令序列里安排一个读位在这个读位期间释放 SDA然后检查采样回来的电平。这个位是高还是低才是从设备给你的回应。我见过不少人卡在“写入返回成功但数据不对”上其实就是把 USB 层成功和 I2C 层成功混为一谈了。3. 手搓 I2C 时序起始、停止、读写位、应答一次说清3.1 先封装底层函数不要裸拼字节用流模式写代码如果每个函数里都直接塞一堆命令字节过两天你自己都看不懂。正确的做法是先封装出几个底层动作函数比如 i2c_start、i2c_stop、i2c_write_bit、i2c_read_bit。这些函数内部拼装命令缓冲区对外暴露的是和 I2C 协议一一对应的接口。我通常在头文件里定义几个宏用来说明控制字节和数据字节的位域含义#define I2C_CTL_SCL_HIGH (0u 1) // SCL 高电平 #define I2C_CTL_SCL_LOW (1u 1) // SCL 低电平 #define I2C_CTL_SDA_HIGH (0u 2) // SDA 高电平 #define I2C_CTL_SDA_LOW (1u 2) // SDA 低电平 #define I2C_DATA_VALUE(v) ((v) 2)这只是示意真正要用什么宏还是以你的驱动头文件为准。关键不在于宏的具体值而在于你脑子里要有一个清晰的层次底层是“电平控制 / 数据位”中间是“起始/停止/ACK”上层才是“字节读写”。3.2 起始和停止两个电平动作的组合I2C 起始条件从时序图上看很清楚SCL 保持高电平时SDA 由高变低。对应到流模式就是两个控制字节先是 SCL高、SDA高接着 SCL高、SDA低。中间 SDA 的变化发生在 SCL 为高的时候这正是起始条件的特征。停止条件则是反过来SCL 保持高电平时SDA 由低变高。同样是两个控制字节先 SCL高、SDA低再 SCL高、SDA高。我自己写代码的时候会额外注意一点起始和停止之间每个命令字节执行后总线引脚状态要停留足够的时间。流模式下这个时间由芯片内部定时器决定一般来说低速档是够用的。3.3 写一个 bit 和读一个 bit写数据位的逻辑是先把 SCL 置低然后把 SDA 设置成目标电平接着产生一个 SCL 高脉冲最后 SCL 回到低。如果驱动支持数据字节自动产生脉冲那“产生高脉冲”这一步可以交给数据字节完成如果不支持就要用控制字节手动拉高再拉低。读数据位有一点不同。要读数据时主机必须先把 SDA 释放成高电平也就是“不驱动”总线把它交给从设备控制然后才能打一个 SCL 脉冲在 SCL 高电平期间采样。很多库的实现里这一步通过一个特殊的读数据字节完成返回值里带出采样结果。3.4 ACK 和 NACK 是手动发的最容易漏用快速命令时ACK/NACK 是驱动自动处理的切换到流模式之后这个环节就变成你的责任了。每个 8 bit 字节之后第 9 个时钟周期就是应答位。主机发完 8 个数据位之后要释放 SDA然后打一个 SCL 脉冲再采样 SDA从设备拉低说明 ACK保持高说明 NACK。主机读数据的时候反过来前几个字节读完要主动回 ACK告诉从设备“继续给下一个”最后一个字节要回 NACK告诉从设备“到此为止”。实战里我看到最多的问题就是这里。只写了 8 个读位没写第 9 个 ACK/NACK 位结果后面的数据全部错位。这个坑几乎每个人都踩好在只要抓一次波形就能发现。4. 实战AT24C02 的字节写、页写与随机读4.1 先把地址换算搞清楚AT24C02 容量 256 字节器件地址是 7 位典型值 0b101000。在写方向时第 8 位是 0所以拼成 0xA0读方向时第 8 位是 1拼成 0xA1。新手最容易搞混的是 7 位地址和 8 位地址。很多人直接拿 0x50 当器件地址发出去波形上就完全对不上了。记住换算关系8 位地址 7 位地址 1| 方向位。0x50 是 7 位地址0xA0/0xA1 才是你要在 I2C 线上实际发送的字节。4.2 字节写START 地址 数据 STOP给 AT24C02 写一个字节的时序是起始条件发送 0xA0等 ACK发送内存地址等 ACK发送数据字节等 ACK停止条件。用流模式实现时我会把整段时序的命令字节全部填进一个数组然后调用一次 CH341StreamI2C。示意代码大概长这样uint8_t buf[128]; uint8_t rd[128]; int len 0; // 起始条件SCL 高SDA 高 - 电平控制字节(scl1,sda1) // SCL 高SDA 低 - 电平控制字节(scl1,sda0) // 然后逐位发送 0xA0对每一位调用 i2c_write_bit产生 8 个命令字节 // 发送完地址后主机释放 SDA 并打一个 SCL 脉冲读回 ACK 电平 // 再逐位发送内存地址和数据... // 最后停止条件SCL 高SDA 低 - 电平控制字节(scl1,sda0) // SCL 高SDA 高 - 电平控制字节(scl1,sda1) CH341StreamI2C(0, 0, len, buf, rd_len, rd);这里每个字节数据位我都会单独调用一个辅助函数把它展开成“SCL 低、SDA 设值、SCL 脉冲、SCL 回到低”的多个命令字节。虽然看起来啰嗦但逻辑非常清晰之后查问题也好定位。4.3 页写注意 8 字节页边界回绕AT24C02 内部按 8 字节划分页一次页写最多写 8 个字节。写入超过页边界时地址会回绕到当前页开头也就是把之前的数据覆盖掉而不是自动进到下一页。这是 EEPROM 存储器原理里容易忽略的一点也是“为什么我明明写得很顺序结果数据错乱”的常见原因。所以页写的代码要自己判断当前要写的地址距离所在页的末尾还剩多少字节一次最多写“页大小 - 地址偏移”那么多。写不完就分多次每次回到页边界重置计数。4.4 随机读先假写再重复起始再切方向随机读 AT24C02 本质上是两个阶段先做一个“假写”起始 - 发 0xA0 - 发目标内存地址 - 等 ACK再发重复起始Repeated START然后发 0xA1读 8 个数据位主机发 NACK停止条件。重复起始和停止条件的区别在于重复起始之前并没有先拉一个停止条件出来总线始终被主机占着中间没有释放给其他设备。快速命令模式下有些封装实现不了真正的重复起始会把一次随机读拆成“写地址”和“读数据”两次独立事务中间总线释放了这在某些多设备总线上就会出问题。而 StreamI2C 可以完整保真这套时序这也是它不可替代的地方。4.5 写完先上逻辑分析仪代码写完别急着看读回的数据先挂一个逻辑分析仪抓 SCL 和 SDA。对照 i2c 时序图慢慢看起始条件是否干净、地址字节顺序对不对、ACK 位有没有发、停止条件有没有出来。我调试 StreamI2C 时90% 的问题靠抓波形一眼就能定位。尤其是 ACK 位漏发、地址第 8 位写错、速度档太高导致毛刺这三类问题在波形上都特别明显。如果你现在还没买逻辑分析仪调试 I2C 真的建议备一个几十块钱的就能干活。5. 从 EEPROM 到自定义设备抽象一个通用 I2C 读写层5.1 大多数 I2C 设备都是同一个模型EEPROM 其实是 I2C 设备里最简单的一种。你往外设看温度传感器、光传感器、ADC、IO 扩展器等绝大多数都长一个样一个 8 位器件地址7 位地址 方向位一个寄存器地址可能是 0 字节、1 字节或 2 字节之后跟着要读写的连续数据。只要把流模式的底层操作封装成 write_reg 和 read_reg 两个函数就能覆盖绝大多数场景。这也是我向你推荐 StreamI2C 的另一个原因它向上层暴露的抽象能力比快速命令强太多。5.2 write_reg 的封装思路write_reg(dev_addr, reg_addr, reg_len, data, data_len) 内部做的事情START发送 dev_addr 1 | 0写方向发送 reg_addr注意按 reg_len 决定发 1 字节还是 2 字节先发高字节发送 data 数组里的每个字节STOP。每一步之间都不忘处理 ACK 判断。遇到没有寄存器地址的设备比如某些单字节命令的开关模块reg_addr 传 NULLreg_len 传 0 就行但要注意别把“无地址”和“地址 0”混为一谈。5.3 read_reg 的封装思路read_reg 的内部逻辑稍微多一步START发送 dev_addr 1 | 0写方向发送 reg_addr重复 START发送 dev_addr 1 | 1读方向读 data_len 个字节前 data_len-1 个字节回 ACK最后一个回 NACKSTOP。这个模型基本通吃所有寄存器式 I2C 设备。AT24C02 随机读其实只是它的一个特例reg_addr 为 1 字节、data_len 为 1。所以你把底层写好后后面接新设备只是调参数的事不需要重写时序逻辑。5.4 移植到 Linux 和 libusb 时要注意什么CH341 在 Windows 下有官方 DLLLinux 下也有官方库或者第三方基于 libusb 的实现。函数名往往都是 CH341StreamI2C但更重要的是命令字节的位域定义。我踩过的坑是在 Windows 上调好的命令数组原封不动搬到 Linux 的 libusb 实现里发现起始条件不对。一查头文件才发现两边对“bit0 表示控制还是数据”的约定正好相反。所以移植的时候别只替换函数名一定把底层构造字节的宏重新核对一遍。这个坑很容易被忽略因为代码编译检查不出来只有上波形才知道。6. 我踩过的坑参数配置里的隐形杀手6.1 把 7 位地址当 8 位地址发前面提过一遍但这里必须再强调一次因为这是出现频率最高的错误。很多芯片的数据手册里写的是“器件地址 0x50”但实际 I2C 线上要发的是 0xA0。0x50 左移一位才是线地址方向和读写位要放在最低位。我一开始写代码时就吃了这个亏读出的数据全是 0xFF后来抓波形才发现地址从第一个字节就不对。6.2 高速档位导致的偶发失败iChipMode 设成高速档之后EEPROM 不是完全读不出来而是偶尔错一个字。这种偶发问题比完全失败难查多了。后来我是在逻辑分析仪上看到 SCL 个别周期高电平宽度不足才意识到是速度档的问题。调试期坚持用低速档等波形稳定了再逐步升档。6.3 上拉电阻和电平匹配CH341 模块的 I2C 引脚是开漏输出必须外接上拉电阻。不同模块板载电阻值不同2.2k 到 10k 都是常见范围。负载比较重、走线比较长的时候用小一点的上拉比如 4.7k频率高的时候上升沿会更陡。另外注意 CH341 的电平有的模块是 5V 逻辑、有的能切 3.3V如果你要接 1.8V 的传感器必须加电平转换不能直接怼上去否则轻则通信失败重则烧器件。6.4 缓冲区复用和数据解析错位调用 CH341StreamI2C 时iWriteBuffer 和 iReadBuffer 可以复用同一块内存但要注意长度和生命周期。如果你动态申请缓冲区调用完再释放没问题但如果在一个循环里反复填充 buf每轮循环记得清空或者严格按 len 覆盖否则上一轮残留的命令字节会被一起发出去。读数据的解析错位则更隐蔽iReadBuffer 里存的是按位采样的电平不是按字节整理好的数据。我见过有人直接从 iReadBuffer[0] 取一个字节拿来用结果自然是乱七八糟。正确做法是把 8 个读位拼接成一个字节再继续处理。6.5 从“手拼字节”到“函数化封装”的转变最后一条与其说是坑不如说是建议。StreamI2C 的命令数组一旦长起来人眼很难直接看出哪个字节对应哪个时序动作。我后来强制自己把所有 I2C 动作封装成函数禁止在业务代码里直接出现裸命令字节。这样每次调试都只在底层函数里改上层读写逻辑几乎不用动。这个习惯帮我省了很多排查时间。6.6 调试路径低速、波形、单字节一步一步来如果你现在正准备用 StreamI2C 调一个新设备我建议的路径是先接一个已知好的 AT24C02用最低速档跑通整个时序用逻辑分析仪抓一份标准波形存档作为后续对照的基准再切换成目标设备先做单字节读写成功后再上批量操作最后按需要逐步提高速度档。这套方法看起来很慢但实际是最快的。因为流模式每一步都可以显式控制出问题时只需要对比波形就能在半小时内定位到是地址错、ACK 漏发还是速度档太高。我在实际使用中还有一个体会流模式虽然每条命令看起来都很多余、很啰嗦但它逼着你把 I2C 协议的每一个动作都想清楚。你把 AT24C02 完整调通过一遍之后再去接任何 I2C 设备都会顺手很多。因为本质上你已经在“裸写”协议了后续那些所谓复杂的传感器时序都只是在这个基础上加几个字节的事。所以如果你刚开始接触这个芯片别嫌麻烦直接拿 StreamI2C 下手比什么都学得快。
返回列表