如何在Win32中获取可用串口列表?

64

我有一些遗留代码,通过调用 EnumPorts() 函数并过滤以“COM”开头的端口名称来提供PC上可用COM端口的列表。

为了测试目的,如果我能够使用像com0com这样的东西,它会提供成对的虚拟COM端口循环连接作为空调制解调器,将非常有用。

然而,EnumPorts()函数无法找到com0com端口(即使不过滤“COM”)。 HyperTerminal和SysInternals PortMon都可以看到它们,因此我确信已经正确安装。

那么是否有其他Win32函数可以提供可用串行端口的定义列表?

6个回答

97

EnumSerialPorts v1.20是由Nick D建议使用的,在列出串口时使用了九种不同的方法!我们当然不缺选择,但结果似乎有所不同。

为了方便其他人,我在此列出它们,并指出它们在我的PC(XP Pro SP2)上查找com0com端口时的成功情况:

  1. CreateFile("COM" + 1->255),如Wael Dalloul建议的
    ✔ 找到了com0com端口,用了234毫秒。

  2. QueryDosDevice()
    ✔ 找到了com0com端口,用了0毫秒。

  3. GetDefaultCommConfig("COM" + 1->255)
    ✔ 找到了com0com端口,用了235毫秒。

  4. “SetupAPI1”使用对SETUPAPI.DLL的调用
    ✔ 找到了com0com端口,还报告了“友好名称”,用了15毫秒。

  5. “SetupAPI2”使用对SETUPAPI.DLL的调用
    ✘ 没有找到com0com端口,报告了“友好名称”,用了32毫秒。

  6. EnumPorts()
    ✘ 报告了一些非COM端口,没有找到com0com端口,用了15毫秒。

  7. 使用WMI调用
    ✔ 找到了com0com端口,还报告了“友好名称”,用了47毫秒。

  8. 使用对MSPORTS.DLL的调用的COM数据库
    ✔/✘ 报告了一些非COM端口,找到了com0com端口,用了16毫秒。

  9. 迭代注册表键HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
    ✔ 找到com0com端口,耗时0毫秒。这显然是SysInternals PortMon所使用的方法。

  10. 根据这些结果,我认为WMI方法可能最适合我的要求,因为它相对较快,并且额外提供友好名称(例如“通信端口(COM1)”,“com0com-串行端口仿真器”)。


1
@GrahamS:非常好的答案 - QueryDosDevice() 在搜索 FTDI USB<->Serial 端口设备时非常有效,其他方法都失败了。 - Jon Cage
1
你必须在访问COM > 9之前添加 \\.\,因为它们在NT命名空间中没有保留,并且只能在设备命名空间中访问--https://learn.microsoft.com/ru-ru/windows/desktop/FileIO/naming-a-file#win32-device-namespaces - aitap
1
这些时间并不是那么绝对的:我曾经遇到过一种情况,客户的笔记本电脑上有蓝牙串口,而串口的 WMI 查询需要几分钟的时间。我猜测它试图与蓝牙设备通信。最终我们不得不放弃用户友好的端口名称。 - Mikk L.
2
@GrahamS - 最终我们使用了C# .NET SerialPort.GetPortNames,它提供了简单的端口名称COM1、COM2等。我不知道.NET在底层使用哪种方法,但它的性能很快。我无法评论其他获取友好名称的方法在蓝牙端口上的表现如何。 - Mikk L.
1
@MikkL。显然,.NET使用方法9(迭代注册表键HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM):https://referencesource.microsoft.com/#System/sys/system/io/ports/SerialPort.cs,6e8f9abfa6f4bdef - carlosrafaelgn
显示剩余4条评论

14

谢谢Nick,我在下面对你的回答进行了一些扩展。如果没有人提供更明确的答案,那么我会接受你的回复。 - GrahamS
2
注意:如果您想在应用程序中分发源代码,则只允许分发作者发布的版本。这使得它对于任何开放源代码的东西都是无用的 - 除非原始作者为您修复错误,否则您甚至无法修复错误。我建议任何寻找序列代码的人找到一个更开放的许可证下的东西。 - Glenn Maynard

6
你可以创建一个循环,例如从1到50,并尝试打开每个端口。如果端口可用,则打开将起作用。如果端口正在使用中,则会出现共享错误。如果端口未安装,则会收到文件未找到的错误。
要打开端口,请使用CreateFile API:
HANDLE Port = CreateFile(
                  "\\\\.\\COM1",
                  GENERIC_READ | GENERIC_WRITE,
                  0,
                  NULL,
                  OPEN_EXISTING,
                  FILE_ATTRIBUTE_NORMAL,
                  NULL);

然后检查结果。

8
需要注意的是,如果您尝试使用CreateFile访问COM端口号大于9的端口,即使该端口存在,您也将始终收到ERROR_FILE_NOT_FOUND的错误。为避免这种行为,应将端口名称作为\.\COMx传递(将x替换为要测试的端口号)。 链接:http://support.microsoft.com/kb/115831 - mfriedman

4

现在在Windows中可以使用GetCommPorts直接返回一个COM端口列表。

获取包含格式正确的COM端口的数组。

此函数从HKLM\Hardware\DeviceMap\SERIALCOMM注册表键中获取COM端口号,然后将它们写入调用方提供的数组。如果数组太小,则函数获取所需大小。

为了正确链接函数,您需要添加此代码。

#pragma comment (lib, "OneCore.lib")

1
谢谢。那个函数似乎为您迭代了HKLM\Hardware\DeviceMap\SERIALCOMM,(之前提到过),它只返回一个端口号数组,而不是当您自己进行迭代时可以获得的有用文本名称/描述。 - GrahamS

3
在我的情况下,我需要完整的名称和COM端口地址。我有物理串口、USB串口和com0com虚拟串口。
正如所接受的答案建议的那样,我使用WMI调用。 使用SELECT * FROM Win32_PnPEntity查找所有设备。 它返回像这样的物理设备,并且可以从Caption中解析地址:
Serial Port for Barcode Scanner (COM13)

然而,对于com0com端口,Caption是这样的(没有地址):
com0com - serial port emulator

SELECT * FROM Win32_SerialPort 会返回地址 (DeviceID) 和全名 (Name),但它只能找到物理串口和com0com端口,无法找到USB串口。

因此,最终需要进行两个WMI调用:SELECT * FROM Win32_SerialPort (地址为 DeviceID) 和 SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%' (地址可以从 Caption 中解析出来)。我缩小了 Win32_PnPEntity 的范围,因为它只需要找到在第一个调用中未找到的设备。

以下C++代码可用于查找所有串口:

// Return list of serial ports as (number, name)
std::map<int, std::wstring> enumerateSerialPorts()
{
    std::map<int, std::wstring> result;

    HRESULT hres;

    hres = CoInitializeEx(0, COINIT_APARTMENTTHREADED);
    if (SUCCEEDED(hres) || hres == RPC_E_CHANGED_MODE) {
        hres =  CoInitializeSecurity(
            NULL,
            -1,                          // COM authentication
            NULL,                        // Authentication services
            NULL,                        // Reserved
            RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication
            RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
            NULL,                        // Authentication info
            EOAC_NONE,                   // Additional capabilities
            NULL                         // Reserved
            );

        if (SUCCEEDED(hres) || hres == RPC_E_TOO_LATE) {
            IWbemLocator *pLoc = NULL;

            hres = CoCreateInstance(
                CLSID_WbemLocator,
                0,
                CLSCTX_INPROC_SERVER,
                IID_IWbemLocator, (LPVOID *) &pLoc);

            if (SUCCEEDED(hres)) {
                IWbemServices *pSvc = NULL;

                // Connect to the root\cimv2 namespace with
                // the current user and obtain pointer pSvc
                // to make IWbemServices calls.
                hres = pLoc->ConnectServer(
                     bstr_t(L"ROOT\\CIMV2"),  // Object path of WMI namespace
                     NULL,                    // User name. NULL = current user
                     NULL,                    // User password. NULL = current
                     0,                       // Locale. NULL indicates current
                     NULL,                    // Security flags.
                     0,                       // Authority (for example, Kerberos)
                     0,                       // Context object
                     &pSvc                    // pointer to IWbemServices proxy
                     );
                if (SUCCEEDED(hres)) {
                    hres = CoSetProxyBlanket(
                       pSvc,                        // Indicates the proxy to set
                       RPC_C_AUTHN_WINNT,           // RPC_C_AUTHN_xxx
                       RPC_C_AUTHZ_NONE,            // RPC_C_AUTHZ_xxx
                       NULL,                        // Server principal name
                       RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx
                       RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
                       NULL,                        // client identity
                       EOAC_NONE                    // proxy capabilities
                    );
                    if (SUCCEEDED(hres)) {
                        // Use Win32_PnPEntity to find actual serial ports and USB-SerialPort devices
                        // This is done first, because it also finds some com0com devices, but names are worse
                        IEnumWbemClassObject* pEnumerator = NULL;
                        hres = pSvc->ExecQuery(
                            bstr_t(L"WQL"),
                            bstr_t(L"SELECT Name FROM Win32_PnPEntity WHERE Name LIKE '%(COM%'"),
                            WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
                            NULL,
                            &pEnumerator);

                        if (SUCCEEDED(hres)) {
                            constexpr size_t max_ports = 30;
                            IWbemClassObject *pclsObj[max_ports] = {};
                            ULONG uReturn = 0;

                            do {
                                hres = pEnumerator->Next(WBEM_INFINITE, max_ports, pclsObj, &uReturn);
                                if (SUCCEEDED(hres)) {
                                    for (ULONG jj = 0; jj < uReturn; jj++) {
                                        VARIANT vtProp;
                                        pclsObj[jj]->Get(L"Name", 0, &vtProp, 0, 0);

                                        // Name should be for example "Serial Port for Barcode Scanner (COM13)"
                                        const std::wstring deviceName = vtProp.bstrVal;
                                        const std::wstring prefix = L"(COM";
                                        size_t ind = deviceName.find(prefix);
                                        if (ind != std::wstring::npos) {
                                            std::wstring nbr;
                                            for (size_t i = ind + prefix.length();
                                                i < deviceName.length() && isdigit(deviceName[i]); i++)
                                            {
                                                nbr += deviceName[i];
                                            }
                                            try {
                                                const int portNumber = boost::lexical_cast<int>(nbr);
                                                result[portNumber] = deviceName;
                                            }
                                            catch (...) {}
                                        }
                                        VariantClear(&vtProp);

                                        pclsObj[jj]->Release();
                                    }
                                }
                            } while (hres == WBEM_S_NO_ERROR);
                            pEnumerator->Release();
                        }

                        // Use Win32_SerialPort to find physical ports and com0com virtual ports
                        // This is more reliable, because address doesn't have to be parsed from the name
                        pEnumerator = NULL;
                        hres = pSvc->ExecQuery(
                            bstr_t(L"WQL"),
                            bstr_t(L"SELECT DeviceID, Name FROM Win32_SerialPort"),
                            WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
                            NULL,
                            &pEnumerator);

                        if (SUCCEEDED(hres)) {
                            constexpr size_t max_ports = 30;
                            IWbemClassObject *pclsObj[max_ports] = {};
                            ULONG uReturn = 0;

                            do {
                                hres = pEnumerator->Next(WBEM_INFINITE, max_ports, pclsObj, &uReturn);
                                if (SUCCEEDED(hres)) {
                                    for (ULONG jj = 0; jj < uReturn; jj++) {
                                        VARIANT vtProp1, vtProp2;
                                        pclsObj[jj]->Get(L"DeviceID", 0, &vtProp1, 0, 0);
                                        pclsObj[jj]->Get(L"Name", 0, &vtProp2, 0, 0);

                                        const std::wstring deviceID = vtProp1.bstrVal;
                                        if (deviceID.substr(0, 3) == L"COM") {
                                            const int portNumber = boost::lexical_cast<int>(deviceID.substr(3));
                                            const std::wstring deviceName = vtProp2.bstrVal;
                                            result[portNumber] = deviceName;
                                        }
                                        VariantClear(&vtProp1);
                                        VariantClear(&vtProp2);

                                        pclsObj[jj]->Release();
                                    }
                                }
                            } while (hres == WBEM_S_NO_ERROR);
                            pEnumerator->Release();
                        }
                    }
                    pSvc->Release();
                }
                pLoc->Release();
            }
        }
        CoUninitialize();
    }
    if (FAILED(hres)) {
        std::stringstream ss;
        ss << "Enumerating serial ports failed. Error code: " << int(hres);
        throw std::runtime_error(ss.str());
    }

    return result;
}

1
我已经将PJ Naughter的EnumSerialPorts重组为更便携和个性化的形式,这样更有用。
为了更好的兼容性,我使用C而不是C++。
如果您需要或者对此感兴趣,请访问我在博客中的文章

6
为了更好的兼容性,我使用C语言而不是C++。很抱歉,但这种想法有些傻。现在已经不是1998年了,在Windows上没有任何理由使用C语言。 - Glenn Maynard
7
@GlennMaynard说在Windows上没有理由使用C语言。当然有啊!如果你已经用C语言写了一个库或应用程序,或者你有一个跨平台的应用程序,在只能使用C语言的平台上也可以运行等。还有其他很多原因。 - Oskar N.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接