Fix Inconsistent Serial Port Assignments with USBHub3+

2025 March 6

The Problem: Inconsistent Serial Port Assignment for multiple devices

A recurring issue when working with multiple USB-to-serial connections is that the operating system does not consistently assign port names. This is especially painful for end-of-line (EOL) functional test customers who are connecting multiple Devices Under Test (DUTs) at once, since it is important to know which DUT is passing or failing. 

OS port naming depends on the preexisting ports, USB device descriptors, and the order of port enumeration. Devices without unique serial numbers are assigned COM ports based on enumeration order, which can change unpredictably between reboots.  For example, on Windows, if two identical USB-to-serial adapters without unique serial numbers are plugged in simultaneously, they may be assigned COM4 and COM5, but after a reboot, they could swap positions based on enumeration timing.

Devices with unique serial numbers are also non-deterministically assigned names on first connect, but the OS usually will remember its assigned port name across reboots, though it might assign a new ID if the device is plugged into a different port.  While test scripts could hard-code port assignments based on serial numbers, they would need to be rewritten for each test station, and would fail if devices are moved or replaced.

The Solution: Sequential Port Activation and Mapping

To get consistent port assignments, we can use the Acroname USBHub3+'s ability to control individual USB ports.  An initialization script can sequentially enable each port and monitor the OS for new devices to learn the mapping between physical hub ports and OS-assigned serial port names.  

Initialization steps

To initialize the test system after reboot of configuration change, a script could do the following:

  1. Disable all USB ports 
  2. Enable one port at a time – Allow the OS to detect the attached device
  3. Wait for device enumeration – Monitor the system for the new serial port.
  4. Record the mapping – Store the association between the physical USB port and the OS-assigned serial port.
  5. Repeat for all ports

By the end of this process, all ports are enabled and we have a known mapping of hub port number and OS port ID.

 

Example initializing multiple USB to RS-485 adapters in sequence

Example initialization with USB RS-485 adapters

Example Script Using Acroname Brainstem API

The Acroname Brainstem API provides extensive direct control over the USBHub3+, including turning on and off ports. Below is an example cross-platform Python script for initialization:

import time
import pprint
import serial.tools.list_ports
from brainstem import discover 
from brainstem import link
from brainstem.stem import USBHub3p


# Initialize hub
def setup_hub():
    hub = USBHub3p()
    
    # Connect to the first USBHub3+ found
    result = hub.discoverAndConnect(link.Spec.USB)
    
    if result == link.Result.NO_ERROR:
        serial_number = hub.system.getSerialNumber()
        print("Connected to USBHub3+ with serial number: 0x%08X" % serial_number.value)

    else:
        print(f'Could not find an attached USBHub3+')
        return None  
    
    # Ensure all ports are disabled
    for port in range(8):
        hub.usb.setPortDisable(port)
   
    time.sleep(0.5)  # Allow time for OS to process changes
    
    return hub
    
def get_serial_ports():
    return {port.device for port in serial.tools.list_ports.comports()}

def map_ports(hub):
    port_mapping = {}
    found_ports = set()
    
    initial_ports = get_serial_ports()
    print(f"initial_ports {initial_ports}")

    for port in range(8):
        hub.usb.setPortEnable(port)
        
        # Wait for the OS to detect the new serial port
        start_time = time.time()
        timeout = 2  # Set a reasonable timeout per port
        
        while time.time() - start_time < timeout:
            new_ports = get_serial_ports() - initial_ports - found_ports
            #print(f"port{port} new_ports {new_ports}")
            if new_ports:
               new_port = new_ports.pop()
               found_ports.add(new_port) 
               port_mapping[port] = new_port
               print(f"USBHub3+ Port {port} -> {new_port}")
               break  # Exit the loop once the device is found
            time.sleep(0.25)  # Check every 250ms
        
        if port not in port_mapping:
            print(f"USBHub3+ Port {port} -> No serial port detected in {timeout}s")
        
    return port_mapping

def main():
    hub = setup_hub()
    if hub is None:
      return
    mapping = map_ports(hub)
    print('Mapping:')
    pprint.pprint(mapping, width=1)
    hub.disconnect()

if __name__ == "__main__":
    main()

On Windows with eight ports connected, the dictionary, "mapping" might look like this:

{
    0: 'COM3',
    1: 'COM4',
    2: 'COM5',
    3: 'COM6',
    4: 'COM7',
    5: 'COM8',
    6: 'COM9',
    7: 'COM10'
}

MacOS:

{
	0: '/dev/tty.usbserial-FT23ABC1', 
 	1: '/dev/tty.usbserial-FT23ABC2', 
 	2: '/dev/tty.usbserial-FT23ABC3', 
 	3: '/dev/tty.usbserial-FT23ABC4', 
 	4: '/dev/tty.usbserial-FT23ABC5', 
 	5: '/dev/tty.usbserial-FT23ABC6', 
 	6: '/dev/tty.usbserial-FT23ABC7', 
 	7: '/dev/tty.usbserial-FT23ABC8'
}

Linux:

 {
    0: '/dev/ttyUSB0',
    1: '/dev/ttyUSB1',
    2: '/dev/ttyUSB2',
    3: '/dev/ttyUSB3',
    4: '/dev/ttyUSB4',
    5: '/dev/ttyUSB5',
    6: '/dev/ttyUSB6',
    7: '/dev/ttyUSB7'
}

 

Conclusion

Using Acroname USBHub3+ and the Brainstem API, you can be sure of the mapping of OS-detected serial port IDs to USB-serial converters on physical USB ports. The initialization script can be easily integrated into your test framework to make sure there is no confusion over which DUT is which.  Since test scripts can connect to DUTs based on physical port number rather than being hard-coded by the OS serial port ID or device descriptors, test stations can be easily duplicated to scale up capacity.

 

Add New Comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
We are bots. Are you?