C语言动态调用CH341的64位DLL并二次封装成支持自己业务的64位DLL再静态调用
讲真,要不是我最近做的32bit的DLL,拿给别人用,被说道在一个exe里面,我的32bit的DLL无法和其他设备的64bit的DLL可以随意调用,我还真不知道位数不同的DLL,不能共存这个事情。可以看出其有2个参数,一个为DLL库文件的句柄,另一个为DLL导出函数的名称,当其在hModule库中找到与lpProcName想匹配的DLL函数时,返回该DLL函数的地址,否则返回NULL。可以看出其参
不会插入图片,所以要看图文版,请电邮14518918@qq.com或直接下载pdf附件。
一, 前言
讲真,要不是我最近做的32bit的DLL,拿给别人用,被说道在一个exe里面,我的32bit的DLL无法和其他设备的64bit的DLL可以随意调用,我还真不知道位数不同的DLL,不能共存这个事情。
前段时间玩CH347,初步学会了如何用C语言动态调用CH347的64位DLL,大概意思是,先定义函数指针的数据类型,再定义具体的函数指针,最后加载DLL到内存后再执行某个函数指针,就相当于是调用DLL中的某个函数了。
当然,如果有lib文件,我是肯定首选静态调用的。直接把lib拖到CVI的project tree里面,在C文件中#inlude头文件,就可以直接调用头文件里面的函数了。动态调用的过程太繁琐了,用户需要为每一个待用函数都事先在C文件中再申明一次,有点多此一举的感觉。
但要命的就是,南京沁恒官网上根本搜索不到CH341的64bit的lib文件,给tech@wch.cn发邮件也石沉大海半天没有回复,就是连CH341DLLA64.dll,也是在最新版USB驱动程序安装完毕后,才能在本机用everything工具软件搜索到。所以欲基于CH341DLLA64.dll制作支持自己业务的64bit的DLL,显然只能动态调用DLL罗。而h文件,也只能参考32bit的CH341DLL.dll同名头文件CH341DLL.h。国产货就是这么不珍惜自己的羽毛,牛马何苦为难码农?
二, C语言动态调用DLL的方法
C语言动态调用DLL的方法,这里摘录一些CSDN网帖内容如下:
需要调用windows的底层库来实现,故C文件需要#include <windows.h>。然后需要用到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值。
如下是示例代码
// 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;
}
三, 实战CVI动态调用CH341DLLA.dll二次封装成40GPOWER_A64_DLL.dll
1,在CVI中新建一个创建DLL的Template Project,取名“40GPOWER_A64_DLL”,并存放在“D:\40GPOWER_A64_DLL”目录中。
然后就得到了一个可以制作DLL的工程,包含一个C模板文件和一个h模板文件:
此刻应先确保是要跑64bit程序,所以菜单“Build—Configuration”应从默认的32位模式的Dubug,选成Debug64或Released64。
为了方便用户做静态调用,最好把lib文件也一并生成出来,所以点击菜单“Build—Target Settings”
在弹出的窗口中点击“Change”按键,务必勾选上头文件。
最后在生成DLL的时候,点击菜单“Build—Create Debuggable Dynamic Link Library”时:
才会有dll和lib同时生成的效果。注意下图的.cdb文件,不是中央商务区,而是支撑CVI得以单步调试DLL源文件的支持性文件。
2,修改main()函数所在的《DynamicCallDLL.c》
第一行新增
#include <windows.h>
注意,这句话须放在第一行,如果挪到其他头文件后面,CVI编译时可能会报错。
新增定义
在mian()函数前面,定义CH341DLL中三个最重要函数的函数指针,以及DLL句柄变量。
注意,务必要加上关键字__stdcall,不然后面调用DLL函数时CVI会报错,而且原函数有几个变量就要在括号中写几个对应的数据类型。
//定义CH341DLLA64.dll中三个最重要的函数指针数据类型,在CH341DLL.h文件中有同名函数
typedef int (__stdcall *CH341OpenDevice) (int);
typedef int (__stdcall *CH341CloseDevice)(int);
typedef int (__stdcall *CH341StreamI2C) (int, int, void*, int, void*);
//定义函数指针来指向DLL函数在动态链接库文件中的存放地址
CH341OpenDevice CH341OpenDevice_DLL; // 函数指针,打开CH341
CH341OpenDevice CH341CloseDevice_DLL; // 函数指针,关闭CH341
CH341StreamI2C CH341StreamI2C_DLL; // 函数指针,CH374发起I2C时序
// 定义DLL句柄
HMODULE hDLL; // DLL句柄
新增初始化的代码
凭直觉,我觉得这是DLL的main函数入口函数,所有初始化的代码就应该放在下图红框这里:
于是在这里插入如下代码,比如动态加载DLL文件到内存,并使函数指针正确定位到待调用函数:
// 动态加载DLL
hDLL = LoadLibrary("CH341DLLA64.dll");
if (hDLL == NULL)
return -1; //无法加载CH341DLLA64.dll文件;
// 获取CH341OpenDevice函数地址
CH341OpenDevice_DLL = (CH341OpenDevice)GetProcAddress(hDLL, "CH341OpenDevice");
if (CH341OpenDevice_DLL == NULL)
{
FreeLibrary(hDLL); // 释放DLL
return -1; // 获取DLL函数地址失败
}
// 获取CH341CloseDevice函数地址
CH341CloseDevice_DLL = (CH341CloseDevice)GetProcAddress(hDLL, "CH341CloseDevice");
if (CH341CloseDevice_DLL == NULL)
{
FreeLibrary(hDLL); // 释放DLL
return -1; // 获取DLL函数地址失败
}
// 获取CH341StreamI2C函数地址
CH341StreamI2C_DLL = (CH341StreamI2C)GetProcAddress(hDLL, "CH341StreamI2C");
if (CH341OpenDevice_DLL == NULL)
{
FreeLibrary(hDLL); // 释放DLL
return -1; // 获取DLL函数地址失败
}
注意这里动态加载DLL的代码是:
hDLL = LoadLibrary(“CH341DLLA64.dll”);
即使在工程目录下,并没有《CH341DLLA64.dll》这个文件,我二次封装的《40GPOWER_A64_DLL》也能在CVI中成功编译并生成dll。更离谱的是调用《40GPOWER_A64_DLL》的exe,即使exe所在目录没有《CH341DLLA64.dll》运行exe时也没报找不到《CH341DLLA64.dll》的弹窗。分析原因,也许是之前在安装USB驱动程序时,《CH341DLLA64.dll》被自动安装到了C:\Windows\System32目录下。所以即使exe第一时间没有能在exe所在目录中找到必要的dll文件,windows系统也会自动到系统目录中去找到同名的dll文件,所以才没报错。当然,强烈建议把所有必要的dll都放在exe同一目录下。
新增一个内部业务函数,打开一个CH341的USB设备
//打开USBHandle对应的CH341设备
int __stdcall I2C_HOST_INITIALIZATION_DLL(int USBHandle)
//return -1; //打开USBHandle对应的CH341失败
//return 0; //打开USBHandle对应的CH341成功
{int CH341Handle;// USB设备句柄
// 打开CH341
CH341Handle = CH341OpenDevice_DLL(0);
if (-1 == CH341Handle)
{
// 处理获取函数地址失败的情况
//MessagePopup("错误", "打开USBIndex=0的CH341失败");
FreeLibrary(hDLL); // 释放DLL
return -1; //打开USBHandle对应的CH341失败
}
return 0;
}
新增一个内部业务函数,I2C多字节随机读从机寄存器
//多字节随机读(指定寄存器首地址)
int __stdcall I2C_BYTEs_READ_DLL (int USBHandle, int device_addr, int rom_startaddress, int rom_Length, unsigned char *rom_value_arr)
//注意,rom_value_arr[]数组要全部传进去,仅更新rom_value_arr[rom_startaddress]到rom_value_arr[rom_startaddress+rom_Length-1]这段数据
// return 0; //成功
// return -1; //USBHandle越界,合法范围是0..15
// return -2; //I2C随机读失败,可能下位机没响应
{ int error, iWriteLength, iReadLength;
unsigned char iWriteBuffer[300]={0}, oReadBuffer[300]={0};
if ((USBHandle<0) || (USBHandle>15))
return -1;
//I2C随机连续读从机A0[00..07]寄存器,赋值到oReadBuffer[00..07]
iWriteBuffer[0] = device_addr&0xFE; //I2C从机地址
iWriteBuffer[1] = rom_startaddress; //I2C寄存器地址
iWriteLength = 2; //I2C写缓存长度
iReadLength = rom_Length; //I2C读缓存长度
//调用CH341OpenDevice函数发起一次I2C随机连续读
error = CH341StreamI2C_DLL(USBHandle, iWriteLength, iWriteBuffer, iReadLength, oReadBuffer);
if (0 == error)
return -1; //return -2; //I2C随机读失败,可能下位机没响应
//按照寄存器地址复制回读数组到目标字符串
memcpy(rom_value_arr+rom_startaddress, oReadBuffer, rom_Length);
return 0;
}
新增一个可供外部调用的业务函数,要用到上面两个内部函数,可回读I2C从机位于寄存器A0[68…83]的SN字符串
输入0~15的一个USBHandle,输出16位字符串。
注意,h文件应该有此函数头,这样编译dll时生成的lib文件才会包含该函数的二进制代码,则在将来exe链接时才能将lib中的二进制代码完整复制到exe中去。
//输入0~15的USBHandle,回读对应的EVBSN
int __stdcall OSAEVB_GetSN_DLL (int USBHandle, char *EVBSN)
//return 0, 按照CH341的USBHandle成功回读到EVBSN
//return -1, CH341的USBHandle初始化NG
//return -2, I2C随机读NG
{
int error;
unsigned char device_addr, rom_data_arr[256];
//打开一个CH341的USB设备
error = I2C_HOST_INITIALIZATION_DLL (USBHandle);
if (error){return -1;} //return -1, CH341的USBHandle初始化NG
//page read,注意是读的[0~127]单元
device_addr = 0xA0; //40GOSAEVB从机地址限死0xA0
error = I2C_BYTEs_READ_DLL (USBHandle, device_addr, 0, 128, rom_data_arr); //读I2C设备
if (error) {return -2;} //return -2, I2C随机读NG
//赋值EVBSN,注意SN是A0[68..83]一共16位ASCII码
memcpy (EVBSN, rom_data_arr+68, 16);
EVBSN[16]=0;
return 0; //return 0, 按照CH341的USBHandle成功回读到EVBSN
}
四, 实战CVI静态调用40GPOWER_A64_DLL.dll
1,在CVI中新建一个带GUI的Template Project
取名“Call_40GPOWER_A64_DLL”,也存放在创建DLL的“D:\40GPOWER_A64_DLL”目录中,方便待会儿debug单步观察DLL源码走势。然后到菜单“Build—Configuration”把默认的32位模式的Dubug,选成Debug64,不然待会儿拖进来的64bit的lib文件名就是灰色无效的。
然后把刚才生成的Call_40GPOWER_A64_DLL.lib和Call_40GPOWER_A64_DLL.h都拉进CVI左侧的project Tree中,还要在C文件中#include “40GPOWER_A64_DLL.h”,才能顺利调用DLL中的函数。最后在面板上放一个按钮,一个USBHandle的输入框,和一个SN的输出框:
该按钮的回调函数如下:
int CVICALLBACK on_Get_E2SN (int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{int error, USBHandle;
char EVBSN[17];
switch (event)
{
case EVENT_COMMIT:
GetCtrlVal(panelHandle, PANEL_NUM_USBHandle, &USBHandle);
error = OSAEVB_GetSN_DLL(USBHandle, EVBSN);
SetCtrlVal(panelHandle, PANEL_STR_EVBSN, EVBSN);
break;
}
return 0;
}
如果成功回读到SN,会显示SN到界面上:
如果回读SN失败,会弹窗报警:
更多推荐



所有评论(0)