Non-deterministic COM port order with Dual Serial and Triple Serial USB on Windows10

CraigF

Well-known member
I have read in several places that when Dual Serial or Triple Serial USB mode is used, the SerialUSB (aka Serial) endpoint will be connected to the lowest-numbered of the assigned COM ports. Among other things, Visual Micro states this in their DBG documentation and suggests that debug should be assigned the highest of the SerialUSBx ports on the teensy side and the highest of the teensy COM ports on the PC side.

Unfortunately, this behavior is not what is observed. When Teensy Dual Serial USB is assigned two COM ports, Serial may appear on either the lower or the higher numbered port. The assignment persists through reconnections, but there are many scenarios in which Windows doesn't handle a disconnect rapidly enough, in which case the reconnect will cause two new COM port numbers to be assigned, and Serial could end up being either the lower or the higher COM number.

Similarly, in Triple Serial USB mode the Serial/SerialUSB port may end up on any of the three COM ports that are assigned (in my tests it most often appeared on the middle numbered port).

Here is an image showing SerialUSB1 on the lower numbered COM port and SerialUSB on the higher numbered Com port:

ComPorts_Inverted_order.png

Here is an image showing the expected behavior: SerialUSB on the lower numbered COM port and SerialUSB1 on the higher numbered port:

ComPorts_Serial_on_lower_number.png

Q1: does anybody know what determines the order in which SerialUSB endpoints will be assigned COM ports?

Q2: is there any way to cause Dual/Triple SerialUSBx COM ports to have descriptive names that distinguish them?

Q3: there are documented methods by which to determine the unique Teensy serial number associated with a connection. Do SerialUSB ports expose unique identifiers as well?
 
Teensy presents the interfaces. The Host determines what the path to them is called.

Once they are established - at least on Windows - they should return to that same COM#.

Not sure off hand about the other answers - if you install TyCommander (forum search will give github install link) it has an 'Information' tab that might give the answers. It seems it can determine the MAIN USB Serial, and it might provide clues to the other details as it sees them that may be useful.
 
First, to try directly answering your 3 specific questions....

Q1: does anybody know what determines the order in which SerialUSB endpoints will be assigned COM ports?

Maybe someone at Microsoft or others very familiar with the inner working of Windows might know. But I do not.

I do know the only way Windows can learn of these ports is by reading the USB configuration descriptor, which tells windows about all the ports within 1 lengthy message which is always transmitted the same way. Why Windows would have inconsistent behavior is a real mystery to me. The info Teensy sends is consistent. The USB protocol provides only 1 way for Windows or any other USB host to read the complete list of all ports (or "interfaces" in USB lingo) as 1 long set of data (called "configuration descriptor" in the USB spec).


Q2: is there any way to cause Dual/Triple SerialUSBx COM ports to have descriptive names that distinguish them?

Maybe, but it's terrible. It's also not free on Windows 7 or later. Whether customized names appear anywhere other than Device Manager (or even there separately for each port) is a good question. For software to query that sort of info requires use of difficult Setup API and Configuration Manager WIN32 functions. Most programs will show only a list of COM numbers without any additional info, because that is relatively easy.

The way involves creating a custom INF file. While INF is just a text file describing hardware and how Windows should use it, the end user experience is that of installing a driver. On Windows 7 or later, you must also create a CAT file with a digital signature, which requires a valid code signing certificate. Microsoft authorizes about half a dozen certificate authorities to issue these certificates. Normally you buy through a reseller rather than directly (which usually costs a *lot* more). My favorite is K Software. The good news is you only need the less expensive OV type to sign INF. The expensive EV type is needed only if your CAT file will also sign a .SYS file or other kernel-level binary executable. A lot of wrong & confusing info exists online because EV is needed to sign actual driver code, and the end user experience of an INF is pretty much the same as installing a driver.

How you learn to create your INF is a good question. Lots of examples exist online, as Windows 8 and earlier didn't include any INF to cause Microsoft's USBSER.SYS driver to actually load when a USB CDC-ACM device was connected. Thankfully Microsoft finally fixed this with Windows 10. But if you search, you'll find lots of examples meant for earlier versions of Windows. Maybe you can find MSDN or other documentation from Microsoft, but at least the last time I tried (many years ago, when crafting the INF we still install for pre-10 Windows), Microsoft's documentation was a fragmented and confusing mess.


Q3: there are documented methods by which to determine the unique Teensy serial number associated with a connection.

Microsoft provides a feature called Setup API for querying installed hardware. It is hard to use, and while Microsoft's documentation leaves a lot to be desired, it's not nearly as horrible as trying to learn how to create an INF. The main function is SetupDiGetClassDevs, which takes a GUID (pre-assigned by Microsoft) for the type of devices you want to discover. The one you want is named GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR. If successful, it returns a DEVINFO handle, which represents a list of all the ports. Then you use SetupDiEnumDeviceInterfaces to get basic info about each, and then SetupDiGetInterfaceDeviceDetail to get more specific info. Among that info should be which USB interface number it is within the USB device. The first port (which is Serial on Teensy) will be interface 0.

You can also get a "devinst" number from the Setup API functions. Then you can use Configuration Manager functions like Get_DevNode_Registry_Property to obtain more info from the Windows Registry. Microsoft's documentation leaves quite a lot to be desired and getting this stuff to work on all versions of Windows (well, at least if you consider Windows XP to be the beginning of time) can be quite tricky. The configuration manager info is arranged in a hierarchical tree, so to get the info you want to might need to move within the hierarchy with functions like CM_Get_Parent, CM_Get_Child, CM_Get_Sibling, etc.

If you really want to dive into this stuff, at least these WIN32 function names can get your searching started with the correct keywords. But be warned, it's a pretty deep rabbit hole of seldom used & arcane Windows stuff.


Do SerialUSB ports expose unique identifiers as well?

No, not really, except for the USB interface number.

The USB spec does have a way to associate a string with each interface. But I've only ever seen Linux and MacOS actually use those. Windows seems to ignore any interface strings. Microsoft's design is for any strings the end users see to come from the INF file.


but there are many scenarios in which Windows doesn't handle a disconnect rapidly enough, in which case the reconnect will cause two new COM port numbers to be assigned, and Serial could end up being either the lower or the higher COM number.

Is it only Windows fault, or does software running and using the COM port at the time of USB disconnect (called "surprise removal" in Microsoft's idioms) have an effect?

If you'd like to experiment, look for this line in startup.c, which waits 20 ms before turning on the USB hardware.

https://github.com/PaulStoffregen/c...412517470f58c84c1f7b38/teensy4/startup.c#L149

Maybe a longer wait here would be beneficial on Windows?

The default location to find startup.c is C:\Program Files (x86)\Arduino\hardware\teensy\avr\cores\teensy4. Arduino should automatically detect if you've edited this file and recompile when you upload. It's always a good idea to first check by just adding any syntax error, so you don't end up wasting a lot of time editing another copy in a wrong location Arduino isn't actually using.
 
Last edited:
Windows maintains a database of connected devices to ensure that it assigns always the same COM port number to a device regardless on what USB port the device is plugged in. However, this database uses VID/PID/serialnumber to identify a device. If the device don't have a serial number or there are more devices with the same serial number Windows can get confused. since all Serial ports of one Teensy have the same SN, Windows can get confused. (It could use the Interface number in addition to VID/PID/SN to identify the ports of a mulitport device, but it doesn't...)

As defragster already mentioned tyCommander handles this pretty well (never got confused COM ports) If you are looking for a command line tool to identify which COM port goes to which Serial you can do:
Code:
tycmd list --verbose

which prints
Code:
add 6151110-Teensy[COLOR="#FF0000"][B]@1[/B][/COLOR] Teensy 4.0 (Triple Serial)
  location: usb-1-2
  capabilities:
    unique
    run
    rtc
    reboot
    serial

  interfaces:
    Serial: COM8

add 6151110-Teensy[COLOR="#FF0000"][B]@[/B]2[/COLOR] Teensy 4.0 (Triple Serial)
  location: usb-1-2
  capabilities:
    unique
    run
    rtc
    reboot
    serial

  interfaces:
    Serial: COM7

add 6151110-Teensy Teensy 4.0 (Triple Serial)
  location: usb-1-2
  capabilities:
    unique
    run
    rtc
    reboot
    serial

  interfaces:
    Serial: COM6
The red @1 and @2 identify the additional ports.

If you want to write something yourself you can have a look at the tycmd code (very clean pure c code) or, if you prefer a high level approach you can use my TeensySharp library (not so clean :) C#) which is also able to unanimously identify the ports.

At the end it is the responsibility of an application like VisualMicro to identify the ports of a connected board correctly. To assume that COM port numbers are fixed in Windows is simply a wrong assumption. Handling this correctly is certainly possible (see the TyTools suite).


'
 
Last edited:
A long time ago
I wrote this
note there is a lot of functions that you probably don't need
but I include them anyway as they could surely be of some use

Code:
using System;
using Microsoft.Win32;
using System.Security;
using System.Security.Permissions;
using System.Management;
using System.Collections.Generic;

namespace Communication
{
    /// <summary>
    /// Description of PortInfo.
    /// </summary>
    public static class PortInfo
    {
        public static Action<string> ErrorOccurred_Action = null;

        /// <summary>@ "SYSTEM\ControlSet001\Enum"</summary>
        static readonly string REG_KEY_PATH__EnumeratedDevices = @"SYSTEM\ControlSet001\Enum";
        /// <summary>@ "SYSTEM\ControlSet001\Control\Class\{4D36E96D-E325-11CE-BFC1-08002BE10318}"</summary>
        static readonly string REG_KEY_PATH__Modems = @"SYSTEM\ControlSet001\Control\Class\{4D36E96D-E325-11CE-BFC1-08002BE10318}";
        /// <summary>@ "SYSTEM\ControlSet001\Control\Class\{4D36E978-E325-11CE-BFC1-08002BE10318}"</summary>
        static readonly string REG_KEY_PATH__Ports = @"SYSTEM\ControlSet001\Control\Class\{4D36E978-E325-11CE-BFC1-08002BE10318}";
        /// <summary>@ "HARDWARE\DEVICEMAP\SERIALCOMM"</summary>
        static readonly string REG_KEY_PATH__SerialComm = @"HARDWARE\DEVICEMAP\SERIALCOMM";
        /// <summary>@ "HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM"</summary>
        static readonly string REG_HKEY_LM_PATH__SerialComm = @"HKEY_LOCAL_MACHINE\" + REG_KEY_PATH__SerialComm;


        public static bool CheckIfComPortExist(string portName)
        {
            List<string> ports = new List<string>(8);
            ports.AddRange(GetPortNames());
            if (ports.Contains(portName))
            {
                ports.Clear();
                ports = null;
                return true;
            }
            ports.Clear();
            ports = null;
            return false;
        }
        public static string[] GetPortNames()
        {
            RegistryKey localMachine = null;
            RegistryKey key2 = null;
            string[] strArray = null;
            new RegistryPermission(RegistryPermissionAccess.Read, REG_HKEY_LM_PATH__SerialComm).Assert();
            try
            {
                localMachine = Registry.LocalMachine;
                key2 = localMachine.OpenSubKey(REG_KEY_PATH__SerialComm, false);
                if (key2 != null)
                {
                    string[] valueNames = key2.GetValueNames();
                    strArray = new string[valueNames.Length];
                    for (int i = 0; i < valueNames.Length; i++)
                    {
                        strArray[i] = (string)key2.GetValue(valueNames[i]);
                    }
                }
            }
            finally
            {
                if (localMachine != null)
                {
                    localMachine.Close();
                }
                if (key2 != null)
                {
                    key2.Close();
                }
                CodeAccessPermission.RevertAssert();
            }
            if (strArray == null)
            {
                strArray = new string[0];
            }
            return strArray;
        }

        public static bool GetEnumeratedSerialCommDevices_FromRegistry(out string[] comNames, out string[] deviceFullNames)
        {
            RegistryKey key = null;
            deviceFullNames = null;
            comNames = null;
            new RegistryPermission(RegistryPermissionAccess.Read, REG_HKEY_LM_PATH__SerialComm).Assert();
            try
            {
                key = Registry.LocalMachine.OpenSubKey(REG_KEY_PATH__SerialComm, false);
                key.SetErrorOccurred_Action(ErrorOccurred_Action);
                key.TryGetRegKeyNamesAndValues(out deviceFullNames, out comNames);
            }
            catch (Exception ex)
            {
                if (ErrorOccurred_Action != null)
                    ErrorOccurred_Action(ex.ToString());
                deviceFullNames = null;
                comNames = null;
            }
            if (key != null)
            {
                key.Close();
                key = null;
            }
            CodeAccessPermission.RevertAssert();
            if (deviceFullNames == null)
                return false;
            return true;
        }

        public static void GetPortNamesAndDescriptionsFromRegistryDevMgr(out string[] names, out string[] descriptions)
        {
            RegistryKey key = null;

            List<string> namesTemp = new List<string>();
            List<string> descrTemp = new List<string>();

            // get modems information
            key = Registry.LocalMachine.OpenSubKey(REG_KEY_PATH__Modems, RegistryKeyPermissionCheck.Default);
            string[] modems = key.GetSubKeyNames();
            key.Close();
            for (int i = 0; i < modems.Length; i++)
            {
                if (modems[i] == "Properties") continue;
                key = Registry.LocalMachine.OpenSubKey(REG_KEY_PATH__Modems + "\\" + modems[i], RegistryKeyPermissionCheck.Default);
                string matchingDeviceId = (string)key.GetValue("MatchingDeviceId");
                GetDevicePortAndDesc(matchingDeviceId, ref namesTemp, ref descrTemp);
                key.Close();
            }

            // get ports information
            key = Registry.LocalMachine.OpenSubKey(REG_KEY_PATH__Ports, false);
            string[] ports = key.GetSubKeyNames();
            key.Close();
            for (int i = 0; i < ports.Length; i++)
            {
                if (ports[i] == "Properties") continue;
                key = Registry.LocalMachine.OpenSubKey(REG_KEY_PATH__Ports + "\\" + ports[i], false);
                string matchingDeviceId = (string)key.GetValue("MatchingDeviceId");
                GetDevicePortAndDesc(matchingDeviceId, ref namesTemp, ref descrTemp);
                key.Close();
            }

            names = namesTemp.ToArray();
            descriptions = descrTemp.ToArray();
        }

        public static void GetDevicePortAndDesc(string matchingDeviceId, ref List<string> names, ref List<string> descr)
        {
            string rootKeyPath = REG_KEY_PATH__EnumeratedDevices + "\\" + matchingDeviceId;
            RegistryKey key = Registry.LocalMachine.OpenSubKey(rootKeyPath, RegistryKeyPermissionCheck.Default);
            if (key == null)
            {
                // System.Windows.Forms.MessageBox.Show(rootKeyPath);
                return;
            }
            string[] deviceSubs = key.GetSubKeyNames(); key.Close();
            string portNameTemp = "";
            string descrTemp = "";
            for (int i = 0; i < deviceSubs.Length; i++)
            {

                try
                {
                    key = Registry.LocalMachine.OpenSubKey(rootKeyPath + "\\" + deviceSubs[i] + "\\Device Parameters", false);
                    portNameTemp = (string)key.GetValue("PortName"); key.Close();
                    key = Registry.LocalMachine.OpenSubKey(rootKeyPath + "\\" + deviceSubs[i], false);
                    descrTemp = (string)key.GetValue("DeviceDesc"); key.Close();
                    names.Add(portNameTemp);
                    descr.Add(descrTemp);
                }
                catch
                { // (Exception ex) { 
                    if (key != null)
                        key.Close();
                    // System.Windows.Forms.MessageBox.Show(rootKeyPath + "\\" + deviceSubs[i] + "\\Device Parameters" + "\n" + ex.ToString());
                }
            }
        }


        public static string[] GetPortNamesWithDescription()
        {
            string[] regDevMgrNames;
            string[] regDevMgrDescriptions;
            GetPortNamesAndDescriptionsFromRegistryDevMgr(out regDevMgrNames, out regDevMgrDescriptions);

            string[] regNames;
            string[] regDescriptions;
            GetEnumeratedSerialCommDevices_FromRegistry(out regNames, out regDescriptions);

            string[] portAndDesc = new string[regNames.Length];
            for (int i = 0; i < regNames.Length; i++)
            {
                string descrTemp = "";
                int indexOf = -1;

                if (regDevMgrNames.TryGetIndexOfAsString(regNames[i], true, true, out indexOf))
                    descrTemp = regDevMgrDescriptions[indexOf];
                else
                    descrTemp = regDescriptions[i];

                portAndDesc[i] = regNames[i] + " (" + descrTemp + ")";

            }
            return portAndDesc;
        }
    }
}
namespace Microsoft.Win32
{
    using System;
    public static class MicrosanExtensions
    {
        private static Action<string> RegistryIO_Error_Action = null;

        public static void SetErrorOccurred_Action(this RegistryKey thisRegistryKey, Action<string> method)
        {
            RegistryIO_Error_Action = method;
        }
        public static bool TryGetRegKeyNamesAndValues(this RegistryKey thisRegistryKey, out string[] names, out string[] values)
        {
            try
            {
                if (thisRegistryKey != null)
                {
                    names = thisRegistryKey.GetValueNames();
                    values = new string[names.Length];
                    for (int i = 0; i < names.Length; i++)
                    {
                        values[i] = (string)thisRegistryKey.GetValue(names[i]);
                    }
                    return true;
                }
                else
                {
                    names = null;
                    values = null;
                    return false;
                }
            }
            catch (Exception ex)
            {
                if (RegistryIO_Error_Action != null)
                    RegistryIO_Error_Action(ex.ToString());
                names = null;
                values = null;
                return false;
            }
        }
    }
}
namespace System
{
    public static class MicrosanExtensions1
    {
        public static bool TryGetIndexOfAsString(this object[] thisObjectArray, string value, bool IgnoreCase, bool trim, out int index)
        {
            for (index = 0; index < thisObjectArray.Length; index++)
            {
                if (thisObjectArray[index].ToString().Equals(value, IgnoreCase, trim))
                    return true;
            }
            index = -1;
            return false;
        }
        public static bool Equals(this string thisString, string value, bool IgnoreCase, bool trim)
        {
            return (thisString.Adjust(IgnoreCase, trim) == value.Adjust(IgnoreCase, trim));
        }
        public static string Adjust(this string thisString, bool toLower, bool trim)
        {
            if (toLower && !trim)
                return thisString.ToLower();
            if (!toLower && trim)
                return thisString.Trim();
            if (toLower && trim)
                return thisString.Trim().ToLower();
            return thisString;
        }
    }
}
and the function you will use is
GetEnumeratedSerialCommDevices_FromRegistry

the usage would be:
Code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Communication;


namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void PrintPorts()
        {
            string[] names;
            string[] desc;

            rtxt.Clear();

            rtxt.AppendText("\n\nGetEnumeratedSerialCommDevices_FromRegistry:\n");
            PortInfo.GetEnumeratedSerialCommDevices_FromRegistry(out names, out desc);
            for (int i = 0; i < names.Length; i++)
            {
                rtxt.AppendText(names[i] + "   " + desc[i] + "\n");
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            PrintPorts();
        }
    }
}

the result in my case for a dual serial Teensy is

WindowsIsConfusing.png

here SerialUSB (Serial) is COM8
and SerialUSB1 is COM7

so there is some order at least in the registry

note. the registry path is
HKLM\HARDWARE\DEVICEMAP\SERIALCOMM
 
Paul wrote:

I do know the only way Windows can learn of these ports is by reading the USB configuration descriptor, which tells windows about all the ports within 1 lengthy message which is always transmitted the same way. Why Windows would have inconsistent behavior is a real mystery to me. The info Teensy sends is consistent. The USB protocol provides only 1 way for Windows or any other USB host to read the complete list of all ports (or "interfaces" in USB lingo) as 1 long set of data (called "configuration descriptor" in the USB spec).

Thanks! I assume the configuration descriptor is constructed from the contents of usb_desc ? I'll look at the internal MSFT code handling the configuration descriptor and try to track down the source of variability in the order of assignment.

Surprise removal used to be a huge problem that's now handled pretty well as of Win10 builds > 11000 or so. Then we had a strange bluetooth bug that caused new COM ports to be assigned on every reconnection -- thankfully now patched. The remaining problem occurs only when PnP is busy or blocked, so the disconnection isn't reported and assigning the same IDs to the reconnect would constitute an illegal duplicate. This is fairly uncommon in user environments but not that uncommon in devtest environments (I've had to wipe the COM_db in windows when my com port numbers got up into the 200s).

I'll experiment with startup delay for USB activation, in case that has an effect. It may make a difference in momentary disconnects caused by connector problems.
 
Last edited:
@luni: I've used TeensySharp -- thank you for that!!! And I'm starting to work through tycmd, which is an education.

Unfortunately my wish was to be able to predict which of several newly-assigned COM ports in Windows will correspond to which of the SerialUSB ports in Teensy. My current solution requires me to probe the ports, which works but shouldn't be required, and is hard to teach newbies. Since Paul confirms that the USB configuration descriptor transmitted by Teensy is invariant, it's evident that Windows is failing to honor the order of presentation (probably a thread race). I will submit a request, but even when I worked at MSFT it was incredibly difficult to get things like this fixed :(

WRT Visual Micro, I think they are merely repeating what they have been told about Teensy Dual Serial COM port enumeration. Their context is the use of DBG wrapped in the Visual Studio GUI for trace- and step-debugging of teensy code, which is TRULY AWESOME when it works. Probably the right answer would be to extend teensy_debug and have the host probe ports looking for a response from teensy_debug (e.g., on SerialUSB1) before connecting.
 
Manicksan wrote:

the result in my case for a dual serial Teensy is

[table of port descriptors]

here SerialUSB (Serial) is COM8
and SerialUSB1 is COM7

This is super helpful! I'll test to see whether USBSER000 consistently corresponds to SerialUSB (etc.) when the order of the COM port numbering is flipped. I am hopeful that this might be so, since the ordination of USBSER follows the teensy order rather than the COM port number order. If true, this would solve my immediate problems completely.
 
This is super helpful! I'll test to see whether USBSER000 consistently corresponds to SerialUSB (etc.) when the order of the COM port numbering is flipped. I am hopeful that this might be so, since the ordination of USBSER follows the teensy order rather than the COM port number order. If true, this would solve my immediate problems completely.

Actually this only lists the currently connected COM Ports. If you have more than one connected the list simply grows. Here the list if only one with triple serial and one with dual serial are connected:
Screenshot 2021-12-25 180820.png

Will be difficult to say which is which without further information unless you have only one on the bus.
 
Unfortunately my wish was to be able to predict which of several newly-assigned COM ports in Windows will correspond to which of the SerialUSB ports in Teensy. My current solution requires me to probe the ports, which works but shouldn't be required, and is hard to teach newbies. Since Paul confirms that the USB configuration descriptor transmitted by Teensy is invariant, it's evident that Windows is failing to honor the order of presentation (probably a thread race).

I kind of still don't understand your goal. Do you want to integrate this information as part of your application (e.g. users don't select COM ports but say, Teensies based on their serial numbers etc.). Or, do you need to feed this information to some production application? Just a manual information to select the right port in VisualMicro?
 
I kind of still don't understand your goal. Do you want to integrate this information as part of your application (e.g. users don't select COM ports but say, Teensies based on their serial numbers etc.). Or, do you need to feed this information to some production application? Just a manual information to select the right port in VisualMicro?

Just at the moment I'm writing instructions for aspiring developers to be able to trace teensy code using DBG in Visual Micro/ Visual Studio. Currently-available instructions do not work much of the time. VM requires that COM ports be chosen for uploading and debugging. Teensy-debug requires a debug.begin() command that takes a SerialUSB parameter. If there is any mismatch in the connection, or any activity whatsoever on the selected port prior to a host-device connection being established, DBG returns a garbage-can error message and users are hopelessly confused. Add to this the fact that after building, a Teensy reboot may appear to occur when it has not, and it becomes quite difficult to work out the connection kinks. Many people just give up.

@manicksan's suggestion seems to be bang-on.

Since it's easy to see which COM ports => USBSERXXX devices appear or re-appear when the Teensy is rebooted, there is no ambiguity about which specific COM ports are of interest (and of course we can check the teensy serial number). So long as the *ordinal* assignment of USBSERXXX devices matches the ordination of Teensy SerialUSB, my immediate problem is solved, since I can look up the COM port for each instance of \Device\USBSERXXX. The root of my problem was that COM port ordination in windows seems to be non-deterministic.

My testing thus far shows that manicksan's suggestion appears to meet my needs:

For any Teensy USB SERIAL device having multiple SerialUSB ports
and any sequential integers N1, N2, and N3 where N1 < N2 < N3
\Device\USBSERN1 is consistently connected to Teensy SerialUSB
\Device\USBSERN2 is consistently connected to Teensy SerialUSB1 (if it exists)
\Device\USBSERN3 is consistently connected to Teensy SerialUSB2 (if it exists)

The order of assignment of COM port numbers to \Device\USBSERXXX instances is variable when first created, but remains fixed until new port numbers are assigned. That doesn't matter since I now have a readily queryable resource that exposes the current assignment of COM port for each Teensy SerialUSB port. Now I can write a small utility that shows the mapping of SerialUSB ports to COM ports, and people can select COM ports more deterministically.

For my own devices (medical devices) -- when they make it into production it's necessary to produce INF files and expose custom names anyway.
 
Got it, thanks. Never tried the debugging capabilities of VM. Might give it a try during the upcoming holidays. Does it use GDB as backend?
 
then there is also this location in the registry

Code:
\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\COM Name Arbiter\Devices

where you can find the VID and PID to filter the options even more

and then when you have the vid and pid

and if you want the names + "everything else" you can get them here

Code:
Computer\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USB\VID_16C0&PID_048B&MI_00\7&2a8cf3a9&0&0000
Computer\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USB\VID_16C0&PID_048B&MI_02\7&2a8cf3a9&0&0002
 
Got it, thanks. Never tried the debugging capabilities of VM. Might give it a try during the upcoming holidays. Does it use GDB as backend?

Yes and no: The Visual Micro extension for Visual Studio supports embedded code development for a large number of devices across both traditional and arduino-compatible ecosystems. It supports a bunch of different ways to get code onto your device, and multiple ways to debug that code as it executes on the device.

The VM primary software debug mode is great for tracking pin state of all pins and variable contents as the program runs; this mode supports pre-compilation breakpoints and tracepoints but does not expose the call stack nor support single-stepping. This mode does not use GDB and it's good enough for many situations.

For chips/boards that support hardware debugging, VM hooks up the appropriate debugger to the Visual Studio GUI debugger, so all the usual stuff "just works." This can be hardware GDB or other debuggers. I've personally found this GUI debugger to be life-changing when debugging obscure crashes involving attached sensors etc.. Remote GUI debugging of embedded code works just as well over WiFi or Ethernet.

For boards like teensy where access to true hardware debugging is absent but the chip manufacturer natively supports GDB, GDB standard debugging is supported using a GDB stub. The one I use is ftrias/TeensyDebug https://github.com/ftrias/TeensyDebug. On the teensy 3.6 I've found that only 3 or 4 breakpoints can be used without creating problems, but for me that's sufficient. The call stack is exposed, the watched variables window works correctly, and single-step works well enough. I can step right into libraries and even into the core if I turn off optimizations. When stopped, I can hover over any variable to see current value & can change variable values before proceeding. All the usual visual studio GUI debugging works almost as if the code were running on the host.

There are some DBG auto-connection timing issues on teensy because we lack a restart, but one click will manually attach the debugger and then Bob's your uncle. It's working for me with TD1.56, Arduino .19, VS2022 (Community edition) and VisualMicro2022.

Since I'm used to Visual Studio (decades of use with a half-dozen languages) I've been doing all my teensy development in the Visual Micro extension anyway. Dev and Debug are literally one click apart in the same GUI, which I find very convenient. But even for those who prefer some competely different dev env, I have to say that for anybody interested in GUI debug, Visual Micro is worth a look.
 
then there is also this location in the registry

Code:
\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\COM Name Arbiter\Devices

where you can find the VID and PID to filter the options even more

and then when you have the vid and pid

and if you want the names + "everything else" you can get them here

Code:
Computer\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USB\VID_16C0&PID_048B&MI_00\7&2a8cf3a9&0&0000
Computer\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USB\VID_16C0&PID_048B&MI_02\7&2a8cf3a9&0&0002


That saves me a bunch of time. Thanks!!
 
Back
Top