不会插入图片,所以要看图文版,请电邮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失败,会弹窗报警:

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐