抱歉不大会在博客插入附件和图片,想看图文版请电邮14518918@qq.com
一, 关于CH347
CH347是南京沁恒最近几年推出来的USB接口芯片。如果你已经很熟悉CH341,那么你一定会爱上CH347。因为,CH347的UART和I2C可以同时存在(CH341只能跳选ACT要不要2k到地,来分时决定要么是UART要么是I2C),CH347的UART在00模式下最高波特率可达9M(CH341只有2M),最招人爱的是CH347的I2C有7种速率可选(CH341只有前4种)。
原厂和淘宝卖家提供的参考文档:
CH347模块,淘宝有售,链接是:https://item.taobao.com/item.htm?abbucket=10&id=849139875587&skuId=5807890887927
实际上CH347T的售价,比CH341T还是要高一点的:
下图是自制GD32E251评估板与CH347模块的I2C互联照片,也是我接下来的硬件测试平台:
二, CVI动态调用CH347DLL.dll
最开始我在官网没有下载到CH347DLL.lib,只搞到了CH347DLL.h和CH347DLL.dll。不得已才开始学CVI动态调用DLL的方法。
有网帖https://blog.csdn.net/qq_41256212/article/details/86743698,《LabwindowsCVI DLL工程的创建及使用》,介绍了CVI动态调用DLL的方法,特摘录如下:
LabwindowsCVI DLL库文件的使用与Visual C++的使用一样,就是调用windows的底层库来实现的,其主要由LoadLibrary、GetProcAddress、FreeLibrary 这3个函数来实现:
1,LoadLibrary函数:
LoadLibrary函数用于加载DLL库文件到内存中,这样我们就能调用DLL函数了,其函数原型如下所示:
HMODULE LoadLibrary(LPCSTR lpLibFileName);
可以看出其参数就是DLL库文件的路径,当其加载DLL库文件成功后,将返回DLL库文件的句柄,否则返回NULL。
2,GetProcAddress函数:
GetProcAddress函数用于提取DLL库文件导出的某个具体DLL函数,其函数原型如下所示:
FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName);
可以看出其有2个参数,一个为DLL库文件的句柄,另一个为DLL导出函数的名称,当其在hModule库中找到与lpProcName想匹配的DLL函数时,返回该DLL函数的地址,否则返回NULL。
3,FreeLibrary函数:
FreeLibrary函数用于释放DLL模块,其函数原型如下所示:
BOOL FreeLibrary( HMODULE hLibModule);
可以看出其只有一个参数,就是DLL模块的句柄,当其释放DLL模块成功时,将返回非零的值,否则返回0值。
然后问Deepseek “CVI如何动态调用DLL函数?”
答:
在LabWindows/CVI中动态调用DLL函数,可以通过Windows API实现。
1,关键步骤说明
加载DLL:使用 LoadLibrary 函数
获取函数指针:使用 GetProcAddress 函数
定义函数指针类型:需与DLL函数原型严格一致
调用函数:通过获取的指针调用DLL函数
释放资源:使用 FreeLibrary 卸载DLL
#include <windows.h>
2,示例代码
// 1. 定义DLL函数原型(需与实际DLL函数签名完全一致)
typedef int (*MyFunctionPtr)(int, char*); // 假设DLL函数返回int,接受int和char*参数
int main()
{
HINSTANCE hDLL = NULL;
MyFunctionPtr pFunction = NULL;
// 2. 加载DLL(注意路径问题)
hDLL = LoadLibrary("MyDLL.dll");
if (hDLL == NULL) {
printf("Error: Failed to load DLL. Code=%d\n", GetLastError());
return -1;
}
// 3. 获取函数地址
pFunction = (MyFunctionPtr)GetProcAddress(hDLL, "MyExportedFunction");
if (pFunction == NULL) {
printf("Error: Function not found. Code=%d\n", GetLastError());
FreeLibrary(hDLL);
return -1;
}
// 4. 调用DLL函数
int result = pFunction(42, "Hello from CVI!");
printf("DLL returned: %d\n", result);
// 5. 释放DLL
FreeLibrary(hDLL);
return 0;
}
我于是照猫画虎,在无lib文件前提下,尝试动态调用CH347DLL.dll中的函数。
1,在CVI中新建一个带GUI的Template Project,取名“DynamicCallDLL.prj”保存在“callDLL2”目录。
2,修改main()函数所在的《DynamicCallDLL.c》
第一行新增
#include <windows.h>
注意如果这句话须放在第一行,如果挪到其他头文件后面,CVI编译时可能会报错。
在mian()函数前面,新增几个CH374DLL中的函数指针类型。
typedef int (__stdcall *CH347OpenDevice) (int);
typedef int (__stdcall *CH347CloseDevice)(int);
typedef int (__stdcall *CH347StreamI2C) (int, int, void*, int, void*);
注意,务必要加上关键字__stdcall,不然后面debug调用DLL函数时CVI会报错,而且原函数有几个变量就要在括号中写几个对应的数据类型。
给界面新增一个按键,其回调函数如下:
int CVICALLBACK on_Test (int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{
HMODULE hDLL; // DLL句柄
CH347OpenDevice CH347OpenDevice_DLL; // 函数指针,打开CH347
CH347OpenDevice CH347CloseDevice_DLL; // 函数指针,关闭CH347
CH347StreamI2C CH347StreamI2C_DLL; // 函数指针,CH374发起I2C时序
uint8_t USBHandle; // CH347的USB句柄,0..15
uint8_t iWriteBuffer[300]={0}; // I2C写缓存
uint8_t oReadBuffer[300]={0}; // I2C读缓存
uint16_t iWriteLength; // I2C写缓存长度
uint16_t iReadLength; // I2C读缓存长度
char str[256]; //字符串
int error;
switch (event)
{
case EVENT_COMMIT:
// 加载DLL
hDLL = LoadLibrary("CH347DLL.dll");
if (hDLL == NULL)
{
// 处理加载失败的情况
MessagePopup("错误", "无法加载DLL文件");
return -1;
}
// 获取CH347OpenDevice函数地址
CH347OpenDevice_DLL = (CH347OpenDevice)GetProcAddress(hDLL, "CH347OpenDevice");
if (CH347OpenDevice_DLL == NULL)
{
// 处理获取函数地址失败的情况
MessagePopup("错误", "无法找到指定函数");
FreeLibrary(hDLL); // 释放DLL
return -1;
}
// 获取CH347CloseDevice函数地址
CH347CloseDevice_DLL = (CH347CloseDevice)GetProcAddress(hDLL, "CH347CloseDevice");
if (CH347CloseDevice_DLL == NULL)
{
// 处理获取函数地址失败的情况
MessagePopup("错误", "无法找到指定函数");
FreeLibrary(hDLL); // 释放DLL
return -1;
}
// 获取CH347StreamI2C函数地址
CH347StreamI2C_DLL = (CH347StreamI2C)GetProcAddress(hDLL, "CH347StreamI2C");
if (CH347OpenDevice_DLL == NULL)
{
// 处理获取函数地址失败的情况
MessagePopup("错误", "无法找到指定函数");
FreeLibrary(hDLL); // 释放DLL
return -1;
}
// 打开CH347
USBHandle=0;
error = CH347OpenDevice_DLL(USBHandle);
if (-1 == error)
{
// 处理获取函数地址失败的情况
MessagePopup("错误", "打开USBIndex=0的CH347失败");
FreeLibrary(hDLL); // 释放DLL
return -1;
}
//I2C随机连续读从机A0[00..07]寄存器,赋值到oReadBuffer[00..07]
USBHandle = 0; //CH347的USBHandle=0
iWriteBuffer[0] = 0xA0; //I2C从机地址
iWriteBuffer[1] = 0x00; //I2C寄存器地址
iWriteLength = 2; //I2C写缓存长度
iReadLength = 8; //I2C读缓存长度
//调用CH347OpenDevice函数发起一次I2C随机连续读
error = CH347StreamI2C_DLL(USBHandle, iWriteLength, iWriteBuffer, iReadLength, oReadBuffer);
if (1 == error)//成功
{
sprintf (str, "Read A0[00..08h]=%02X, %02X, %02X, %02X, %02X, %02X, %02X, %02X",
oReadBuffer[0], oReadBuffer[1], oReadBuffer[2], oReadBuffer[3],
oReadBuffer[4], oReadBuffer[5], oReadBuffer[6], oReadBuffer[7] );
MessagePopup("成功", str);
}
else //失败
{
// 处理I2C操作失败的情况
MessagePopup("错误", "I2C随机连续读从机A0[00..07]寄存器失败");
return -1;
}
// 关闭CH374
error = CH347CloseDevice_DLL(USBHandle);
// 释放DLL
FreeLibrary(hDLL);
break;
}
return 0;
}
尝试运行,可以成功读出I2C从机的A0[00…07]数据:
以上,就是CVI动态调用CH347DLL.dll的核心函数的过程。
三, CVI静态调用CH347DLL.dll
下面,我们看看静态调用CH347DLL.dll,请宝子们待会儿记得把“Easy”扣在公屏上。
第一步,得找齐CH347DLL.dll、CH347DLL.lib、和CH347DLL.h三件套。李大师率先从沁恒官网上下载了“CH341PAR.ZIP”压缩包给我,说在一起的。我尝试在沁恒官网搜索栏输入“CH347”关键词,确实能搜索到一个“CH341PAR.ZIP”:
下载下来,就能找到所需三件套,需要拷贝到CVI的工程文件夹中备用。然后,需要将CH347DLL.lib和CH347DLL.h直接从文件夹拖到CVI的Project Tree窗口中,如下:
第二步,在源代码中直接调用CH347DLL.h中的函数即可:
具体的关键代码,请看回调函数源码:
int CVICALLBACK on_CH347OpenDevice (int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{int iMode = 1; //000=低速/20KHz,001=标准/100KHz(默认值),010=快速/400KHz,011=高速/750KHz,100=低速/50KHz,101=标准/200KHz,110=快速/1MHz
uint32_t I2CRate_Arr[7]={20, 100, 400, 750, 50, 200, 1000};
char str[256]={0}, str1[256]={0};
switch (event)
{
case EVENT_COMMIT:
SetCtrlVal (panel, PANEL_LED_CH347Open, 0);
//从界面获取CH347的USBHandle
GetCtrlVal (panel, PANEL_NUM_USBHandle, &USBHandle);
//从界面获取CH347的iMode
GetCtrlVal (panel, PANEL_RNG_iMode, &iMode);
//尝试打开USBHandle对应的CH347
HANDLE DLLresult = CH347OpenDevice(USBHandle);
if (INVALID_HANDLE_VALUE==DLLresult)
{
sprintf(str, "NG, open CH347 with USBHandle=%d\n", USBHandle);
//打印log到界面
SetCtrlVal (panel, PANEL_TXB_Log, str);
return -1;
}
else
sprintf(str, "OK, open CH347 with USBHandle=%d", USBHandle);
int error = CH347I2C_Set(USBHandle, iMode);
if (0==error)
{
sprintf(str, "NG, set CH347 with I2CRate=%dkHz\n", I2CRate_Arr[iMode]);
//打印log到界面
SetCtrlVal (panel, PANEL_TXB_Log, str);
return -1;
}
else
sprintf(str1, ", I2CRate=%dkHz\n", I2CRate_Arr[iMode]);
strcat(str, str1);
//打印log到界面
SetCtrlVal (panel, PANEL_TXB_Log, str);
SetCtrlVal (panel, PANEL_LED_CH347Open, 1);
break;
}
return 0;
}
int CVICALLBACK on_CH347CloseDevice (int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{
char str[256];
switch (event)
{
case EVENT_COMMIT:
//从界面获取CH347的USBHandle
GetCtrlVal (panel, PANEL_NUM_USBHandle, &USBHandle);
//尝试关闭USBHandle对应的CH347
HANDLE DLLresult = CH347OpenDevice(USBHandle);
if (INVALID_HANDLE_VALUE==DLLresult)
sprintf(str, "NG, close CH347 with USBHandle=%d\n", USBHandle);
else
sprintf(str, "OK, close CH347 with USBHandle=%d\n", USBHandle);
SetCtrlVal (panel, PANEL_LED_CH347Open, 0);
break;
//打印log到界面
SetCtrlVal (panel, PANEL_TXB_Log, str);
break;
}
return 0;
}
int CVICALLBACK on_CH347I2CRead (int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{
int error; // 函数返回值
HMODULE hDLL; // DLL句柄
HANDLE DLLresult; // DLL调用返回值
uint8_t iWriteBuffer[300]={0}; // I2C写缓存
uint8_t oReadBuffer[300]={0}; // I2C读缓存
uint16_t iWriteLength; // I2C写缓存长度
uint16_t iReadLength; // I2C读缓存长度
char str[256]; //字符串
char str1[256]; //字符串
uint8_t i, SlvAdd, RegAdd, Reglen;
int CH347Open;
switch (event)
{
case EVENT_COMMIT:
SetCtrlVal (panel, PANEL_LED_CH347Read, 0);
//从界面获取CH347的CH47Open
GetCtrlVal (panel, PANEL_LED_CH347Open, &CH347Open);
if (0==CH347Open)
{
sprintf(str, "NG, please open CH347 first\n");
//打印log到界面
SetCtrlVal (panel, PANEL_TXB_Log, str);
return -1;
}
//从界面获取CH347的USBHandle
GetCtrlVal (panel, PANEL_NUM_USBHandle, &USBHandle);
//从界面获取SlvAdd
GetCtrlVal (panel, PANEL_NUM_SlvAdd, &SlvAdd);
//从界面获取RegAdd
GetCtrlVal (panel, PANEL_NUM_RegAdd, &RegAdd);
//从界面获取Reglen
GetCtrlVal (panel, PANEL_NUM_Reglen, &Reglen);
//I2C随机连续读从机A0[00..07]寄存器,赋值到oReadBuffer[00..07]
iWriteBuffer[0] = SlvAdd; //I2C从机地址
iWriteBuffer[1] = RegAdd; //I2C寄存器地址
iWriteLength = 2; //I2C写缓存长度
iReadLength = Reglen; //I2C读缓存长度
//调用CH347OpenDevice函数发起一次I2C随机连续读
error = CH347StreamI2C(USBHandle, iWriteLength, iWriteBuffer, iReadLength, oReadBuffer);
if (0==error)
sprintf(str, "NG, read CH347 from %02X[%02X..%02X]\n", SlvAdd, RegAdd, RegAdd+Reglen-1);
else
{
sprintf(str, "OK, read CH347 from %02X[%02X..%02X] = ", SlvAdd, RegAdd, RegAdd+Reglen-1);
for (i=0; i<Reglen; i++)
{
sprintf(str1, "%02X ", oReadBuffer[i]);
strcat(str, str1);
}
strcat(str, "\n");
}
//打印log到界面
SetCtrlVal (panel, PANEL_TXB_Log, str);
SetCtrlVal (panel, PANEL_LED_CH347Read, 1);
break;
}
return 0;
}
你看,动态调用dll,要用到wondws.h中的三个函数,还要事先申明将要被动态调用的函数名及其带的所有参数类型,还要特别注明_stdcall。而静态调用,就是一个原dll的函数名,拿来就用,清爽多了不是嘛?但动态调用dll的方式,豆包说却更流行,这究竟图个啥呢?在求教了余大师之后,我对静态和动态,又有了新的认识。
静态调用DLL又称隐式调用,在程序编译/链接阶段通过导入库(.lib文件)声明DLL函数,系统在程序启动时自动加载所需DLL。DLL打包进入EXE之后不再需要和EXE一起release给用户。当DLL升级之后,EXE需重新编译打包新版本DLL,故EXE就比较臃肿。
动态调用DLL又称显式调用,无需导入库(.lib)或头文件,直接通过函数名(字符串)查找地址,在程序运行时通过代码动态加载DLL(如LoadLibrary、GetProcAddress)。DLL需要和EXE一起release给用户。当DLL升级之后,只需要更换DLL,而EXE无需重新编译,故EXE就比较清爽。
Deepseek给两者的对比表:
维度 静态调用DLL 动态调用DLL
链接时机 编译时(通过.lib) 运行时(代码显式加载)
依赖文件 需.h和.lib 仅需DLL文件
DLL加载时间 程序启动时自动加载 按需加载(如点击功能时)
错误处理 DLL缺失直接崩溃 可捕获错误并处理(如提示用户)
内存占用 可能较高(提前加载所有DLL) 较低(按需加载)
代码复杂度 低(直接调用函数) 高(需函数指针和类型转换)
适用场景 稳定依赖、快速开发 灵活扩展、热更新、插件化系统
两份CVI源码在此:
我专门切换过CH347的SCL频率,拿示波器观察,还是比较准的,完全不像CH341F那么离谱(CH341F的频率实测值是datasheet上写的两倍,差评),我现在代码设置的是100kHz,示波器显示黄线频率92.4kHz。
去除START和STOP等时序,再展开看A0这部分,频率就是100.8kHz了,很准啊。
CH347的I2C速率选项比CH341多,示波器测了都还比较准。100k-200k-400kHz我都试过,访问GD32E251这个I2C从机没问题,但到750KHz就没戏了:
四, Python动态调用CH347DLL.dll
先上Python程序的运行结果:
再上Python源代码屎山:
参考文档:
Python编程不难,只不过GUI控件跟代码不能相互跳转,搞得敲代码的过程一直就比较窝火,仅这一点上讲,CVI完胜!
这里只说几个Tips
1,当一个DLL文件被用ctypes.WinDLL()函数装载时,就是用的stacall方式了:
common.CH347_dll = ctypes.WinDLL(dll_path)
2, CH341只要成功opendevice之后,上位机调用CH341StreamI2C()函数发起I2C读,即使某个从机地址都不存在,该函数都不会返回false。所以我们用CH341去搜索I2C从机,就只能自己凑一个时序,基本思路就是用最底层的I2C指令(要用到比如mCH341A_CMD_I2C_STREAM这样的命令码),发START+SlvAdd,然后检查从机的ACK,最后发STOP。完了判断从机ACK是否正常应答,来判断某个从机地址是否真的挂上来了。CH347我就没有找到这些带“CMD_I2C”字样的宏定义了,但我可以调用CH347StreamI2C()函数发起发起I2C读(实际上这里的回读长度参数可以设成0,所以时序上就和CH341的方式一样,就只有写从机地址的时序),然后判断返回值是false就能说明该从机地址不存在了。
3,本来最开始我是在myTkinter for CH341的源文件基础上,准备修改成CH347适用的版本。结果一来就卡在ch347OpenDevice()函数上了,我才转战CVI的。在Pycharm中Debug到这句话,就没法继续向下跑了。而Run,会在console窗口报错,说在CH341DLL.dll中找不到ch347OpenDevice()这个函数。淘宝商家也提供了python例程但是我用不来,唯独有一个CH347.py文件可以参考下,我看和我的代码大抵都是一样的啊。无奈求助于苟大师,火眼金睛啊,一来就指出,我源代码里面的ch347应该是CH347。真的是改了就对了。我估计是我在Pycharm IDE中Ctrl+R的时候,把原先的“CH341”错误替换成了“ch347”。哎……