环境:Visual Studio 2017 + Windows 10
参考文档:
1. 新建项目,选择C++和DLL生成空项目SerialPort
2. 新建头文件“SerialPort.h”
- 添加宏定义,
#ifdef SERIALPORT_EXPORTS
#define SERIALPORT_API __declspec(dllexport)
#else
#define SERIALPORT_API __declspec(dllimport)
#endif
- 在生成 DLL 项目里面的预处理器定义有定义SERIALPORT_EXPORTS ,此修饰符指示编译器和链接器从 DLL 导出函数或变量,以便其他应用程序可以使用它。
- 定义API函数返回状态值,后面再添加更多的错误值。
enum eSerialPortStatus
{
SERIALPORT_OK = 0,
SERIALPORT_PARAMETER_ERROR,
SERIALPORT_NO_DEVICE,
};
- 添加第一个API函数USB2SerialSearchPort,通过USB的PID和VID获取对应的COM口编号
extern "C" SERIALPORT_API eSerialPortStatus USB2SerialSearchPort(
unsigned short vid,
unsigned short pid,
unsigned char *port
);
3. 在SerialPort.cpp添加
- include需要用的头文件
#include "stdafx.h"
#include <atlstr.h>
#include <SetupAPI.h>
#pragma comment(lib, "setupapi.lib")
#include "SerialPort.h"
- 修改工程属性字符集属性为“使用多字节字符集”
- 实现API函数USB2SerialSearchPort
a. 调用API函数SetupDiGetClassDevs(用来查询与指定参数匹配的所有已安装设备)。
DWORD dwFlag = (DIGCF_ALLCLASSES | DIGCF_PRESENT);
HDEVINFO hDevInfo = INVALID_HANDLE_VALUE;
hDevInfo = SetupDiGetClassDevs(NULL, NULL, NULL, dwFlag);
if (INVALID_HANDLE_VALUE == hDevInfo)
return SERIALPORT_NO_DEVICE;
DIGCF_ALLCLASSES
返回所有已安装设备的列表或所有设备接口类。
DIGCF_PRESENT
只返回当前系统中存在的(已连接)设备。
b. 将VID和PID转换为搜索关键字字符串,并大写化。
CString strVidPid;
strVidPid.Format(_T("VID_%04x&PID_%04x"), vid, pid);
strVidPid.MakeUpper();
c. 遍历所有设备,找到对应VID和PID设备
for (int i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &sDevInfoData); i++)
{
}
API函数SetupDiEnumDeviceInfo 枚举指定设备信息集合的成员,并将数据放在PSP_DEVINFO_DATA中
for循环内先通过SetupDiGetDeviceInstanceId获取设备的实例ID,ID的字符串放在数组szDis中,然后szDis是否包含VID和PID字符串
DWORD nSize = 0;
TCHAR szDis[MAX_PATH] = { 0x00 };// 存储设备实例ID
TCHAR szFN[MAX_PATH] = { 0x00 };// 存储设备实例属性
// 无效设备
if (!SetupDiGetDeviceInstanceId(hDevInfo, &sDevInfoData, szDis, sizeof(szDis), &nSize))
{
continue;
}
CString strTemp, strName;
// 根据设备信息寻找VID PID一致的设备
strTemp.Format(_T("%s"), szDis);
strTemp.MakeUpper();
if (strTemp.Find(strVidPid, 0) == -1)
{
continue;
}
找到这个设备后,调用API函数SetupDiGetDeviceRegistryProperty获取指定的即插即用设备属性。
nSize = 0;
SetupDiGetDeviceRegistryProperty(hDevInfo, &sDevInfoData,
SPDRP_FRIENDLYNAME,
0, (PBYTE)szFN,
sizeof(szFN),
&nSize);
// "XXX Virtual Com Port (COM?)"
strName.Format(_T("%s"), szFN);
if (strName.IsEmpty())
{
continue;
}
SPDRP_CLASS:The function retrieves a REG_SZ string that contains the device setup class of a device. 返回类型,FT4232H返回“USB”
SPDRP_CLASSGUID:The function retrieves a REG_SZ string that contains the GUID that represents the device setup class of a device. 返回GUID字符串,FT4232H返回 "{36fc9e60-c465-11cf-8056-444553540000}"
SPDRP_DEVICEDESC:The function retrieves a REG_SZ string that contains the description of a device. FT4232H返回"USB Serial Converter C"
SPDRP_ENUMERATOR_NAME:The function retrieves a REG_SZ string that contains the name of the device's enumerator. FT4232H返回“USB”
SPDRP_FRIENDLYNAME:The function retrieves a REG_SZ string that contains the friendly name of a device. 用这个参数对于FTDI的芯片来说,读不到相应信息。这个参数应该是对应设备管理器中的这个属性:
SPDRP_LOCATION_INFORMATION:The function retrieves a REG_SZ string that contains the hardware location of a device. FT4232H返回"0000.0014.0000.004.004.004.000.000.000"
SPDRP_MFG:The function retrieves a REG_SZ string that contains the name of the device manufacturer. 得到制造商字符串。FT4232H返回“FTDI”
SPDRP_PHYSICAL_DEVICE_OBJECT_NAME: The function retrieves a REG_SZ string that contains the name that is associated with the device's PDO.FT4232H返回"\\Device\\0000025a"
SPDRP_SECURITY_SDS: The function retrieves a REG_SZ string that contains the device's security descriptor.
SPDRP_SERVICE:The function retrieves a REG_SZ string that contains the service name for a device. FT4232H返回"FTDIBUS"
最后再szFN字符串中找到字符"COM",从而找到对应串口的COM号,将结果保存在port数组内。
int nStart = -1;
int nEnd = -1;
// 寻找串口信息
nStart = strName.Find(_T("(COM"), 0);
nEnd = strName.Find(_T(")"), 0);
if (nStart == -1 || nEnd == -1)
{
continue;
}
strTemp = strName.Mid(nStart + 4, nEnd - nStart - 2);
port[n] = atoi(strTemp);
n++;
遍历完所有设备后返回,如果n等于0,返回No Device,否则返回OK。
SetupDiDestroyDeviceInfoList(hDevInfo);
hDevInfo = INVALID_HANDLE_VALUE;
if (n > 0)
return SERIALPORT_OK;
else
return SERIALPORT_NO_DEVICE;
4. 新建一个C#项目SerialPortClient来验证DLL,新建一个类SerialPort.cs,引入API函数
using System.Runtime.InteropServices;
public enum eSerialPortStatus
{
SERIALPORT_OK = 0,
SERIALPORT_PARAMETER_ERROR,
SERIALPORT_NO_DEVICE,
};
[DllImport("SerialPort.dll")]
public static extern eSerialPortStatus USB2SerialSearchPort(
UInt16 vid,
UInt16 pid,
byte[] port
);
调用API函数
UInt16 vid = (UInt16)Convert.ToInt32(tbVID.Text, 16);
UInt16 pid = (UInt16)Convert.ToInt32(tbPID.Text, 16);
byte[] port = new byte[128];
SerialPort.USB2SerialSearchPort(vid, pid, port);
for (byte i = 0; i < 128; i++)
{
if (port[i] == 0)
break;
Console.WriteLine("COM" + port[i]);
}
以上的方法并不能识别FT4232H,也有说FTDI的芯片都认不到。试另外一种方式,从注册表中找到对应的COM口(只在Windows 10 64位上验证)。
方法:
插入USB转串口设备,FT4232H会生成4路串口,例如COM12-COM15,在注册表中查找COM12,可以发现2个地方的COM12有特殊意义。
“\HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM”:这里面的COM口是表示当前存在设备的COM口
"\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\COM Name Arbiter\Devices":这里面是COM口对应的VID和PID
这样就把VID/PID和COM口对应起来了。
a. 找到VID/PID一致的COM口,保存在一个数组里
CString strVID, strPID;
strVID.Format(_T("%04X"), vid);
strPID.Format(_T("%04X"), pid);
strTemp.Format(_T("SYSTEM\\CurrentControlSet\\Control\\COM Name Arbiter\\Devices"));
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, strTemp, NULL, KEY_READ, &hKey) == ERROR_SUCCESS)
{
byte index = 0;
for (byte i = 1; i < 128; i++)
{
strTemp.Format(_T("COM%d"), i);
DWORD lValue = MAX_PATH;
TCHAR szValue[MAX_PATH];
lRtn = RegQueryValueEx(hKey, strTemp, NULL, &lValue, (LPBYTE)szValue, &lValue);
if (lRtn == ERROR_SUCCESS)
{
strTemp.Format(_T("%s"), szValue);
strTemp.MakeUpper();
if ((strTemp.Find(strVID, 0) != -1) && (strTemp.Find(strPID, 0) != -1))
{
allPort[index++] = i;
}
}
}
allPort[index] = 0;
RegCloseKey(hKey);
}
b. 在上面数组里面找到当前的设备
strTemp.Format(_T("HARDWARE\\DEVICEMAP\\SERIALCOMM"));
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, strTemp, NULL, KEY_READ, &hKey) == ERROR_SUCCESS)
{
TCHAR szName[MAX_PATH];
int i = 0;
int index = 0;
for (;;)
{
DWORD nLen = 256;
DWORD vLen = 256;
DWORD kType;
TCHAR szValue[MAX_PATH];
if (RegEnumValue(hKey, i, szName, &nLen, 0, &kType, (PBYTE)szValue, &vLen) == ERROR_SUCCESS)
{
if (kType == REG_SZ)
{
for (int j = 0; ; j++)
{
if (allPort[j] == 0)
break;
strTemp.Format(_T("%s"), szValue);
CString strCom;
strCom.Format(_T("COM%d"), allPort[j]);
if (strTemp == strCom)
port[index++] = allPort[j];
}
}
}
else
break;
++i;
}
port[index] = 0;
RegCloseKey(hKey);
}