
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:
- Disable all USB ports
- Enable one port at a time – Allow the OS to detect the attached device
- Wait for device enumeration – Monitor the system for the new serial port.
- Record the mapping – Store the association between the physical USB port and the OS-assigned serial port.
- 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 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