Click here to Skip to main content
Click here to Skip to main content

Enumerating serial ports - W2K style.

By , 20 Jul 2002
 

Introduction

You might think that determining which serial ports are present on a Windows PC would be an easy task. It seems like a reasonable enough thing for the OS to support. Unfortunately, there was no support for it at all (short of reading the registry yourself) before Win2K, and even then, the API is a bit cumbersome.

The attached serial port enumeration code first determines which operating system it is running under, and then runs the appropriate routine to enumerate the serial ports. In Win 9x (and Me) it uses the registry. In W2K and later it uses the SetupAPI that was included in that version of the WinSDK. It also has support for "brute force" enumeration of serial ports under NT4.

Unfortunately, I statically linked with setupapi.lib, so the provided executable won't actually run under 95 and nt4 (I didn't really need to support those OS's for my application.) This could be finagled by replacing the SetupDi* function calls with dynamic binding via LoadLibrary if needed.

To use the EnumSerial code, simply include EnumSerial.cpp and .h in your project, and link with setupapi.lib in the win32 sdk (this is under "additional dependencies" in the project link settings in Visual Studio).

All you have to do now is #include "EnumSerial.h" in your source code, allocate an empty CArray of SSerInfo structs, and make a call to EnumSerialPorts. It will populate your array with filled-out SSerInfo structs which contain the following information:

CString strDevPath;      // Device path for use with CreateFile()
CString strPortName;     // Simple name (i.e. COM1)
CString strFriendlyName; // Full name to be displayed to a user
BOOL bUsbDevice;         // Provided through a USB connection?
CString strPortDesc;     // friendly name without the COMx

Example

CArray<SSerInfo,SSerInfo&> asi;
EnumSerialPorts(asi,FALSE/*include all*/);

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Zach Gorman
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralUSB/Serial cables in Win98 &amp; WinMEmemberHoward C. Anderson10 Nov '03 - 5:26 
Hi. There are at least 15 types of USB/Serial cables available at the moment. Very few of them seem to conform to ANYthing "standard".
 
For example, the KEYSPAN USA19QW has its own non-conforming registry key: HKLM\Enum\KEYSPAN\*USA19WMAP\00_00. This key cannot be found in Win98 and WinME by EnumSerial currently because of this line:
 
BOOL bMatch = (strcmp(acSubSubEnum,"*PNP0500")==0 ||
strcmp(acSubSubEnum,"*PNP0501")==0 ||
bUsbDevice);
 
I was able to find the KEYSPAN cable and presumably all other USB/Serial cables (assuming they do not depart even further from the "standard") by setting:
 
bMatch = TRUE;
 
(This will slow down the performance slightly of course since now more subkeys must be searched. However this is apparently what Device Manager is doing or KEYSPAN would not have been able to place their key where they have placed it...)
 
I also then had to filter out LPT ports and anything non-"COM" in SearchPnpKeyW9x:
 
if (!bDup) {
// Add an entry to the array
if(strPortName.Left(3) == "COM"){//Add COM ports only
asi.Add(si);
}
}
 
Of course if there is some serial port that has a name that does NOT start with COM, it would then not be listed... (Something I read indicated that COM ports do not HAVE to be named "COM" ports... Why are "standards" so non-standard?)
 
This is not the appropriate thread but I have discovered MANY problems with USB/Serial cables. Very few behave like a UART. For example, if you wire CTS to RTS on the Keyspan cable then assert RTS, the cable cannot see its own RTS signal on the CTS pin until 24 milliseconds have elapsed... (We use this to see if the user has our board plugged in...) Lots of little odds and ends that make these cables a nightmare if your software has to support their use...
 
Thanks,
 
Howard

 
Howard in Arizona
http://www.astroshow.com
QuestionHow to identify application which has access to port?memberJoerg Hoffmann10 Apr '03 - 23:35 
For diagnosis it could be helpfull to get the application which
blocks an comprt.
 
Any idea?
AnswerRe: How to identify application which has access to port?memberZach Gorman11 Apr '03 - 5:00 
I wish I knew how - I've often wanted this for files. Windows tells me a file is in use, but not which process has it open.
QuestionAny idea how to get friendly names on NT?memberJoerg Hoffmann10 Apr '03 - 4:54 
Many thanks to Zach Gorman.
You did really a good job.
 
Because of my need for using it on Windows NT machines I've tried to replace the fixed linking to setupapi.lib to the LoadLibrary stuff.
(Don't know if it really works on W2K / not testet at the moment)
 
Know I' searching for a possibility to get the friendly names on NT, too?
 
How can I do this?
AnswerRe: Any idea how to get friendly names on NT?memberZach Gorman10 Apr '03 - 6:19 
On the LoadLibrary note, it turns out that there is an easier way in Visual Studio 6 (I assume it's there in .net too but haven't tried it). In the Link tab of 'Project Settings' for all builds, include 'delayimp.lib' under "Object/library modules." Then, at the bottom of "Project Options" add '/delayload:setupapi.dll'
 
This invokes the compiler's built-in support for delay loading; your program will not attempt to load setupapi.dll until you call a function that lives in it. Since no code in the NT4 execution path calls SetupDi functions, this enables NT4 support.
 
Regarding friendly names on NT4, I wasn't able to find any during my preliminary search. If they exist at all, you may be able to find them by digging around in the registry; my suggestion is to crack open a copy of regedit and search for COM1 on an NT4 box.
 
If they exist in the registry, you can add a function to dig them out based on EnumSerial's W98 code. If not, you can always just make up a friendly name - most ports end up being called "Communications Port" anyway.
GeneralRe: Any idea how to get friendly names on NT?memberJoerg Hoffmann10 Apr '03 - 20:22 
Zach Gorman wrote:
On the LoadLibrary note, it turns out that there is an easier way in Visual Studio 6 (I assume it's there in .net too but haven't tried it). In the Link tab of 'Project Settings' for all builds, include 'delayimp.lib' under "Object/library modules." Then, at the bottom of "Project Options" add '/delayload:setupapi.dll'
 
Thank you for this tip. It works good.Laugh | :laugh:
But for my use it will be better to use it with LoadLibrary because it will be easier to share the code between different projects or with other people here on this site.
 
It seems that on NT no friendly names are used. But there is another way to enumerate serial ports using "QueryDosDevice()".
 
I'll try to add it to your code on weekend and send it to you by email.
 
Bye
GeneralFTDI(Win98) and PCI modem(WinXP)sussPabloII22 Dec '02 - 20:59 
It doesn't found USB <-> Serial converter in Win 98. The key of FTDI converter is placed in the HKLM\Enum\FTDIBUS section.
 
Other thing: I'm looking for a solution to exclude modems from obtained list. I have PCI modem wich was also detected as a port in Win XP. There is a problem with it, because it takes a lot of time to operate on it.
 
P_awe_L
GeneralRe: FTDI(Win98) and PCI modem(WinXP)memberZach Gorman7 Jan '03 - 9:40 
Sorry for the delayed reply. I tested the prolific & aten usb2serial converters; I haven't tried the ftdi. If you have a code patch to add ftdi compatibility, post it here.
 
Regarding filtering out modems, the only approach I can think of off the top of my head is to hack it and look for the word "modem" in the device description.
GeneralRe: FTDI(Win98) and PCI modem(WinXP)memberChristian B21 Apr '03 - 9:05 
Hi,
 
Regarding modems, why not try to enumerate the MODEMS class instead of the PORTS class.
 
regards
// Christian
GeneralRe: FTDI(Win98) and PCI modem(WinXP)memberSamuliA4 Sep '06 - 23:26 
I have an Edgeport USB Converter with 4 serial ports, which are not found by the code.
 
I can sometimes find the ports reading straight from registry HKLM\HARDWARE\DEVICEMAP\SERIALCOMM,
but this seems not to be foolproof, as sometimes the ports are not found.
Anyone know why?
GeneralRe: FTDI(Win98) and PCI modem(WinXP)membergrandmasta14 Nov '07 - 21:32 
ftdichip.com
GeneralData Structure Item Missing in EnumPortsWdmmemberavidgator7 Aug '02 - 5:25 
The "strPortName" part of the SSerInfo structure does not seem to get populated in the EnumPortsWdm method. It does in SearchPnpKeyW9x and EnumPortsWNt4 (although that one is a bit of a no-brainer.) Is this an oversight? What is the "reccomended" method for pulling the port name using the Wdm method? I imagine it could be stripped off of the Firendly Name, but only when the port is not taken up by a device (a practical example is a friendly name of "Xircom Cardbus Ethernet 100 + Modem 56 (Modem Interface)" when the same item is inserted into the PCMCIA slot of a laptop. It "ties up" COM2, but I cannot determine COM2 from the Friendly Name or the Port Desc...)
 
Ideas? Suggestions? Thanks much
 
--JMG
GeneralRe: Data Structure Item Missing in EnumPortsWdmmemberZach Gorman10 Aug '02 - 7:36 
The SetupDi API didn't provide any simple port name, so I left it out of the WDM method. I didn't need it for my application, so I gave up without much of a fight.
 
Perhaps the information you need is available through the SetupDiGetDeviceRegistryProperty( ..., SPDRP_LOCATION_INFORMATION, ...) call.
GeneralRe: Data Structure Item Missing in EnumPortsWdmmemberChristian B21 Apr '03 - 9:02 
Hi,
 
Attached is my version of portlister for w2k that I used a couple of years ago. There is a SetupApi for W9x aswell that can be used with the same result, not suplied here.
 
Pros:
It uses the ClassName "PORTS" instead of the hard-coded guid.
It also extracts the PortName from the registry in a proper way.
 
Enjoy!
// Christian
 

// portlister.cpp : Defines the entry point for the console application.
//
 
#include "stdafx.h"
#include "tchar.h"
#include "setupapi.h"
 
int main(int argc, char* argv[])
{
GUID ClassGuid[1];
DWORD dwRequiredSize;
BOOL bRet;
HDEVINFO DeviceInfoSet = NULL;

SP_DEVINFO_DATA DeviceInfoData;
DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
DWORD dwMemberIndex = 0;
 
// Get ClassGuid from ClassName for PORTS class
bRet = SetupDiClassGuidsFromName(_T("PORTS"), (LPGUID)&ClassGuid, 1, &dwRequiredSize);
if (!bRet) goto cleanup;
 
// Get class devices
DeviceInfoSet = SetupDiGetClassDevs(ClassGuid, NULL, NULL, DIGCF_PROFILE);
 
if (DeviceInfoSet)
{
// Enumerate devices
dwMemberIndex = 0;
while (SetupDiEnumDeviceInfo(DeviceInfoSet, dwMemberIndex++, &DeviceInfoData))
{
TCHAR szFriendlyName[MAX_PATH];
TCHAR szPortName[MAX_PATH];
TCHAR szMessage[MAX_PATH];
DWORD dwReqSize = 0;
DWORD dwPropType;
DWORD dwType = REG_SZ;
HKEY hKey = NULL;
 
// Get friendlyname
bRet = SetupDiGetDeviceRegistryProperty(DeviceInfoSet,
&DeviceInfoData,
SPDRP_FRIENDLYNAME,
&dwPropType,
(LPBYTE)szFriendlyName,
sizeof(szFriendlyName),
&dwReqSize);

// Open device parameters reg key
hKey = SetupDiOpenDevRegKey(DeviceInfoSet,
&DeviceInfoData,
DICS_FLAG_GLOBAL,
0,
DIREG_DEV,
KEY_READ);
if (hKey)
{
// Qurey for portname
dwReqSize = sizeof(szPortName);
long lRet = RegQueryValueEx(hKey,
_T("PortName"),
0,
&dwType,
(LPBYTE)&szPortName,
&dwReqSize);

// Close reg key
RegCloseKey(hKey);
}

wsprintf(szMessage, _T("Name: %s\nPort: %s\n"), szFriendlyName, szPortName);
MessageBox(NULL, szMessage, _T("Found Port"), MB_OK);
}
}
 
cleanup:
// Destroy device info list
SetupDiDestroyDeviceInfoList(DeviceInfoSet);
 
return 0;
}

GeneralRe: Data Structure Item Missing in EnumPortsWdmmemberPeter Wurmsdobler10 Jun '04 - 3:40 
Hi,
 
I have added parts of the proposed code to my EnumSerial, in
 
void EnumPortsWdm(CArray &asi)
 
right before
 
if
(bSuccess) { // ...
 
// Open device parameters reg key
HKEY hKey = SetupDiOpenDevRegKey
(hDevInfo, &devdata, DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_READ);
 
TCHAR szPortName[MAX_PATH];
if (hKey)
{
DWORD dwType = REG_SZ;
DWORD dwReqSize = sizeof(szPortName);
 
// Query for portname
long lRet = RegQueryValueEx
(hKey,_T("PortName"), 0, &dwType, (LPBYTE)&szPortName, &dwReqSize);
if (lRet == ERROR_SUCCESS)
bSuccess &= TRUE;
else
bSuccess &= FALSE;
}
else
bSuccess &= FALSE;
 

and add the folllowing line in
 

if (bSuccess) {
si.strPortName = szPortName;
 


GeneralToo complicated -> use GetDefaultCommConfig()memberAndre Gleichner22 Jul '02 - 2:17 
Why don't you simply call the Win32 API GetDefaultCommConfig() to figure out whether a specific port exists. Optionally followed by CreateFile() if you like to know if another process currently has opened that port.
Works on all Win32 platforms and is much simpler.
 
Attention:
COM10 or greater must be preceeded with "\\.\" for calls to CreateFile(). See Q115831 for details. WinCE needs a trailing ":" for both GetDefaultCommConfig() and CreateFile().
GeneralRe: Too complicated -> use GetDefaultCommConfig()memberZach Gorman22 Jul '02 - 18:27 
If all you need to know is which com #'s exist and whether or not they can be opened, your solution is indeed simpler and more elegant.
 
If you would like to know the "friendly name" of the serial port, whether it is built-in or comes from a USB adapter, and the full WDM device name, I know of no better way than what I have. The friendly name is especially useful since this is the only piece of information you can get from a WM_DEVINFO message when a new serial port is connected.
 
Anyway, the fact the the implementation is complicated is no problem for someone who is just trying to use the thing (as long as it's not buggy, which it isn't).
GeneralRe: Too complicated -> use GetDefaultCommConfig()memberJoerg Hoffmann24 Apr '03 - 21:46 
Zach Gorman wrote:
If all you need to know is which com #'s exist and whether or not they can be opened, your solution is indeed simpler and more elegant.
 
Another reason against this method is: it takes to much time (10ms per port).
 
It will be faster to open all ports with CreateFile(...) and if it fails look at the error code with GetLastError().
 
For example:
 
void EnumSerialPortsWithoutFriendlyNames( CArray<SSerInfo,SSerInfo&> &asi, BOOL bIgnoreInvalid )
{
  SSerInfo si;
 
  // open all possible ports
  for( int i=1; i <= 256; i++ )
  {
    si.strDevPath.Format( _T("\\\\.\\COM%d"), i );
    si.strPortName.Format( _T("COM%d"), i ); 
    si.strFriendlyName = si.strPortName;
    si.bIsInvalid = FALSE;
    si.dwError    = 0L;
    
    HANDLE hSerialPort = CreateFile( si.strDevPath, GENERIC_READ | GENERIC_WRITE, 
                                     0, NULL, OPEN_EXISTING, 0, NULL );
  
    if( hSerialPort == INVALID_HANDLE_VALUE ) 
    {
      // It can't be opened
  
      // Get Reason
      si.dwError = GetLastError();
 
      // mark it as invalid
      si.bIsInvalid = TRUE;
 
      // get a description only for debug
      // or possible add a string to SSerInfo-struct
      LPVOID lpMsgBuf;
      FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                     FORMAT_MESSAGE_FROM_SYSTEM | 
                     FORMAT_MESSAGE_IGNORE_INSERTS,
                     NULL,
                     si.dwError,
                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
                     (LPTSTR) &lpMsgBuf,
                     0,
                     NULL 
                   );
 
      // Display the string.
      TRACE( "%s\n", (LPCTSTR)lpMsgBuf );
 
      // Free the buffer.
      LocalFree( lpMsgBuf );
    }
    else 
    {
      ::CloseHandle(hSerialPort);
    }
 
    if( (!bIgnoreInvalid && (ERROR_FILE_NOT_FOUND!=si.dwError)) 
        || 0==si.dwError )
    {
		    asi.Add(si);
    }
  }
}
 

Zach Gorman wrote:
If you would like to know the "friendly name" of the serial port, whether it is built-in or comes from a USB adapter, and the full WDM device name, I know of no better way than what I have. The friendly name is especially useful since this is the only piece of information you can get from a WM_DEVINFO message when a new serial port is connected.
 
Yes, it would be very nice to have information about the friendly names of the serial ports. But what's about virutal ports which have no friendly name and which will not be listet in the device manager?
 
Any idea?
 
I've tried this software from Wiesemann & Theis GmbH to add some virtual ports:
 
COM Port Redirector for Windows NT/2000/XP
 
COM port redirector for Win 95 / 98 / Me
 
The ports will not be found by using your function EnumPortsWdm().
 
BTW this ports was not found on Windows 98 until I add this to the EnumPortsW9x(...) function:
 
  BOOL bMatch = ( strcmp(acSubSubEnum,"*PNP0500") == 0 ||
                  strcmp(acSubSubEnum,"*PNP0501") == 0 ||
                  strcmp(acSubSubEnum,"Ports"   ) == 0 ||  // Added for virtual ports 
                  bUsbDevice 
                );
 
I don't know if this will be a solution for all virtual ports but for me it works good.
 
Here is another sugestion to add support for enumerationg serial ports on NT4:
(without friendly names):
 
void EnumPortsWNt4(CArray<SSerInfo,SSerInfo&> &asi)
{
  //
  // HINT: On NT4 friendly names not existing
  //
  // Enumerating serial ports by searching in the registry key:
  // "HKLM\Hardware\DeviceMap\SerialComm"
  // 
	
  SSerInfo si;
  vector<string> vec;
  
  HKEY hKey;
  TCHAR caDevName[40], caPortName[20];
  DWORD dwDevNameSize, dwPortNameSize, dwType;
  int i;
 
  try
  {
    if( RegOpenKeyEx( HKEY_LOCAL_MACHINE, "Hardware\\DeviceMap\\SerialComm", 0, KEY_READ,
            &hKey) == ERROR_SUCCESS) 
    {
      for( i = 0; TRUE; i++ )
      {
        dwDevNameSize = sizeof( caDevName );
        dwPortNameSize = sizeof( caPortName );
 

        if( RegEnumValue( hKey, i, caDevName, &dwDevNameSize, NULL, 
                          &dwType, (LPBYTE)caPortName, &dwPortNameSize ) != ERROR_SUCCESS )
        { 
          // no more entries found
          // quit the loop
          break;
        }
        
        // entry found add it to the array
        si.strDevPath  = CString("\\\\.\\") + CString(caPortName);
        si.strPortName = CString(caPortName);
        si.bIsBusy     = FALSE;
        si.dwError     = 0;
        asi.Add(si);
      }
 
      RegCloseKey( hKey );
    }
  }
  catch (CString strError) 
  {
    if (hKey != NULL)
      RegCloseKey(hKey);
    throw strError;
  }
}
 
Perhaps you can use this for future versions of your great code.Smile | :)
 
Have a nice weekend.
 
Joerg Hoffmann
 

GeneralRe: Too complicated -&gt; use GetDefaultCommConfig()memberMike Pulice23 Dec '03 - 6:26 
Are you saying that virtual ports can systematically not report themselves?
If they are enumerated on the system then you can find them with the setup api.
To get friendly names or any other registry item is simple with the setup api.
Look if you have an application that needs to list which ports are presently on the system
or have ever enumerated on the system ie USB to Serial then the setup api is best.
 
All you have to do is call to setupdigetclassdevs what is so complicated?
 

 
Mike "Cop" Pulice

GeneralRe: Too complicated -&gt; use GetDefaultCommConfig()memberZach Gorman4 Jan '04 - 14:23 
The WDM functionality in the posted code uses the Setup API. Because the Setup API does not exist prior to Windows 2000, there is separate code for enumerating serial ports on NT4 and 98.
GeneralRe: Too complicated -&gt; use GetDefaultCommConfig() [modified]memberCrashTest31 Aug '09 - 9:22 
If the actual aim is to enumerate the existing/available COM ports, I'd say the MSDN section about COM ports is sufficient in itself.
Just do an openfile (read/write, exclusive access) using "COM1" to "COM9" for filename (and "\\.\COMxx" if you think your PC has so many UARTs to spare Smile | :) ), check the error code (not forgetting to close the handle if the port exists and is available) and Bob's your uncle!
 
Maybe the article name is misleading, though. The function proposed here is supposed to get the "decorated" names of all serial devices (alas, no real mean of using them is provided hereafter, which seriously reduces the usefulness of the whole bunch of code IMHO).
 
EDIT : oops, sorry, hit the wrong button while trying to post my own 2 cents of code Blush | :O
 
TCHAR port_name[5] = _T("COMx");
for (TCHAR i = _T('1') ; i <= _T('9') ; i++)
{
    port_name[3] = i;
    HANDLE hport = CreateFile (
                                    port_name,
                                    GENERIC_READ | GENERIC_WRITE,
                                    0, 
                                    NULL,
                                    OPEN_EXISTING,
                                    0,
                                    NULL);
    switch (GetLastError())
    {
    case ERROR_SUCCESS        : // yay, now I can play
        CloseHandle (hport);
        break;
 
    case ERROR_ACCESS_DENIED  : // port already in use
         break;
 
    case ERROR_FILE_NOT_FOUND : // no port with this name in the system
         break;
 
    default                   : // Mmm... would not try to push bytes through that, if I were you :)
        ;          
    }

 
modified on Monday, August 31, 2009 7:09 PM

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 21 Jul 2002
Article Copyright 2002 by Zach Gorman
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid