第一次接触CH341T模块时,我完全被它的小身材大能量震惊了。这个看起来像普通USB转接芯片的小东西,竟然能轻松搞定I2C通信。I2C(Inter-Integrated Circuit)是一种简单实用的串行通信协议,只需要两根线(SDA和SCL)就能连接多个设备,特别适合传感器、EEPROM这类低速外设。
CH341T本质上是个多功能USB转接芯片,支持UART、SPI、I2C等多种模式。我最喜欢用它做I2C主机,因为它价格便宜(某宝上十几块钱就能买到)、驱动完善,而且Windows和Linux都有现成的库。记得去年做智能家居项目时,我用它同时读取了五个房间的温湿度传感器数据,稳定运行半年多从没出过问题。
说到I2C协议,新手最容易混淆的就是设备地址。I2C设备通常有7位地址,比如0x50(二进制1010000)。但在实际通信时,我们需要在地址末尾加上读写位——0表示写,1表示读。所以向0x50设备写入数据时,实际发送的是0xA0(10100000);读取时则是0xA1(10100001)。这个细节在后面的代码示例中会特别重要。
拿到CH341T模块后,首先要注意引脚定义。市面上常见的有两种封装:直插式和贴片式。我手头这个直插式模块的引脚排列如下:
连接I2C设备时,只需要接VCC、GND、SDA和SCL四根线。这里有个实用技巧:一定要在SDA和SCL线上各加一个4.7kΩ的上拉电阻。我刚开始偷懒没加,结果通信时灵时不灵,折腾了一整天才发现问题。电阻可以直接焊在模块背面,既美观又省空间。
Windows用户需要先安装CH341官方驱动。我推荐去厂商官网下载最新版,第三方修改版可能会遇到兼容性问题。安装完成后,插入模块会在设备管理器看到"USB2.0-Serial"设备。Linux用户就幸福多了,内核自带ch341驱动,插上就能用。
验证驱动是否正常工作有个小技巧:在Windows下打开设备管理器,右键查看属性→详细信息→硬件ID,应该能看到"VID_1A86&PID_5512"。如果显示未知设备,试试换个USB口或者重新安装驱动。我在笔记本上测试时发现,某些USB3.0接口会有兼容性问题,换成USB2.0接口就正常了。
官方提供的CH341DLL.dll是开发利器,但文档全是中文的,对新手不太友好。我的建议是直接拿现成的示例代码修改,比从头开始写快多了。下面这个批处理脚本可以一键配置VS2019开发环境:
bash复制@echo off
set VS_PATH="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community"
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" x86
mkdir build
cd build
cmake -G "Visual Studio 16 2019" ..
start CH341Demo.sln
记得把DLL文件放在执行目录下,或者添加到系统PATH环境变量。我第一次用时因为DLL路径问题卡了半天,后来发现可以用Depends工具查看DLL依赖关系。
在树莓派上使用CH341T更简单,内核已经集成了驱动。先确认模块被识别:
bash复制lsmod | grep ch341
dmesg | grep ch341
如果需要用户态控制,可以安装libusb开发包:
bash复制sudo apt-get install libusb-1.0-0-dev
我常用的一个开源项目是ch341prog,提供了命令行工具:
bash复制git clone https://github.com/setarcos/ch341prog
cd ch341prog
make
sudo ./ch341prog -i
先看设备初始化的关键代码。这里有个坑要注意:CH341OpenDevice的参数是设备索引,从0开始。如果只插了一个模块,就用0;多个模块时需要遍历测试。
cpp复制HANDLE h341 = CH341OpenDevice(0);
if (h341 == INVALID_HANDLE_VALUE) {
std::cerr << "打开设备失败,错误码: " << GetLastError() << std::endl;
return -1;
}
// 设置I2C速率,400kHz是常用标准速率
if (!CH341SetStream(h341, 400000)) {
std::cerr << "设置I2C速率失败" << std::endl;
CH341CloseDevice(h341);
return -1;
}
实测发现,某些廉价模块在400kHz下不稳定。如果遇到数据错误,可以降到100kHz试试。我在驱动OLED屏时就遇到过这个问题,降速后立即稳定。
读寄存器函数最体现I2C协议的精髓。以读取AT24C32 EEPROM为例:
cpp复制bool ReadEEPROM(HANDLE hDev, uint8_t addr, uint16_t memAddr, uint8_t* data) {
uint8_t cmd[5];
ULONG len;
// 写入内存地址(16位)
cmd[0] = CH341A_CMD_I2C_STREAM;
cmd[1] = CH341A_CMD_I2C_STM_STA; // 起始信号
cmd[2] = 0xA0; // 设备地址+写
cmd[3] = (uint8_t)(memAddr >> 8); // 地址高字节
cmd[4] = (uint8_t)memAddr; // 地址低字节
len = 5;
if (!CH341WriteI2C(hDev, len, cmd)) return false;
// 重复起始信号+读取
cmd[0] = CH341A_CMD_I2C_STREAM;
cmd[1] = CH341A_CMD_I2C_STM_STA; // 重复起始
cmd[2] = 0xA1; // 设备地址+读
len = 3;
if (!CH341WriteI2C(hDev, len, cmd)) return false;
// 读取数据
cmd[0] = CH341A_CMD_I2C_STREAM;
cmd[1] = CH341A_CMD_I2C_STM_IN; // 输入1字节
cmd[2] = CH341A_CMD_I2C_STM_NAK; // 最后字节发NAK
cmd[3] = CH341A_CMD_I2C_STM_STO; // 停止信号
len = 4;
if (!CH341WriteI2C(hDev, len, cmd)) return false;
len = 1;
return CH341ReadI2C(hDev, &len, data);
}
写操作相对简单,但要注意某些设备需要等待写入完成。比如EEPROM的页写入周期约5ms,连续写入时需要延时:
cpp复制void WriteEEPROM(HANDLE hDev, uint8_t addr, uint16_t memAddr, uint8_t data) {
uint8_t cmd[6];
cmd[0] = CH341A_CMD_I2C_STREAM;
cmd[1] = CH341A_CMD_I2C_STM_STA;
cmd[2] = 0xA0; // 设备地址+写
cmd[3] = (uint8_t)(memAddr >> 8);
cmd[4] = (uint8_t)memAddr;
cmd[5] = data;
CH341WriteI2C(hDev, 6, cmd);
// 发送停止信号
cmd[0] = CH341A_CMD_I2C_STREAM;
cmd[1] = CH341A_CMD_I2C_STM_STO;
CH341WriteI2C(hDev, 2, cmd);
Sleep(5); // 等待写入完成
}
当I2C通信失败时,我通常按照这个流程排查:
i2cdetect -y 1)上周调试BMP280气压传感器时就遇到地址问题。数据手册说地址是0x76,但实际模块可能是0x77。最后用逻辑分析仪抓包才发现问题。
需要高速读取时,可以试试这些方法:
有个实际案例:我在读取MPU6050传感器数据时,原始代码每次读取一个寄存器要10ms。改成连续读取后,读取6轴数据只需要2ms,帧率直接提升5倍。关键代码如下:
cpp复制// 一次性读取6个寄存器(0x3B-0x40)
uint8_t cmd[] = {
CH341A_CMD_I2C_STREAM,
CH341A_CMD_I2C_STM_STA,
0xD0, // MPU6050地址+写
0x3B, // 起始寄存器
CH341A_CMD_I2C_STM_STA,
0xD1, // MPU6050地址+读
CH341A_CMD_I2C_STM_IN,
CH341A_CMD_I2C_STM_IN,
CH341A_CMD_I2C_STM_IN,
CH341A_CMD_I2C_STM_IN,
CH341A_CMD_I2C_STM_IN,
CH341A_CMD_I2C_STM_IN | CH341A_CMD_I2C_STM_NAK,
CH341A_CMD_I2C_STM_STO
};
CH341WriteI2C(hDev, sizeof(cmd), cmd);
uint8_t data[6];
ULONG len = 6;
CH341ReadI2C(hDev, &len, data);
CH341T的一个优势是可以同时操作多个I2C设备。我设计过一个环境监测系统,用一片CH341T同时读取:
关键是要合理安排设备地址,避免冲突。有个小技巧:很多I2C设备可以通过跳线改变地址。比如PCF8574的地址范围是0x20-0x27,可以通过A0-A2引脚配置。
CH341T可以和Arduino组成主从系统。比如用CH341T作为主设备,Arduino作为从设备:
cpp复制// Arduino端代码
#include <Wire.h>
#define SLAVE_ADDR 0x08
void setup() {
Wire.begin(SLAVE_ADDR);
Wire.onReceive(receiveEvent);
Wire.onRequest(requestEvent);
}
void receiveEvent(int bytes) {
while(Wire.available()) {
byte cmd = Wire.read();
// 处理命令...
}
}
void requestEvent() {
Wire.write("Hello CH341!");
}
PC端用CH341T发送数据:
cpp复制uint8_t cmd[] = {
CH341A_CMD_I2C_STREAM,
CH341A_CMD_I2C_STM_STA,
0x08, // Arduino地址+写
'G', // 发送命令
'E',
'T',
CH341A_CMD_I2C_STM_STO
};
CH341WriteI2C(hDev, sizeof(cmd), cmd);
这种架构特别适合需要复杂计算的场景——Arduino负责实时采集,PC端进行大数据处理。去年做的智能温室系统就采用这种方案,Arduino负责控制传感器和继电器,PC端运行Python算法优化控制参数。