SparkFun Pro Micro USB descriptor modifications

2022 FEB 11

Today we are going to customize the USB descriptor of the SparkFun Pro Micro. We are going to add a unique serial number and modify its product name. This will achieve two things. First, the Pro Micro will retain its COM-port number regardless of which USB-port its plugged into. Secondly, it will show up in Windows Control Panel with a name of our choice.

Having the device show up under our name is very beneficial. Most importantly it allows us to uniquely identify the device without changing its VID/PID combination (which isn’t allowed unless you pay for a USB license.) With a custom product name we can (somewhat) easily distinguish our Pro Micro from the rest.

Hardware

The SparkFun Pro Micro is unique to two ways: it is tiny and the AVR microprocessor features a USB-module. It is compatible with the normal Arduino IDE and its many libraries. It is very similar to Arduino Micro.

A clone of a SparkFun Pro Micro at 1:1 scale on 96 DPI

The Pro Micro features an AVR ATmega32U4 microprocessor with 32 KB of program memory, 2560 Bytes of RAM, and the usual peripherals: ADC, EEPROM, Timers, PWM, UART, SPI, I2C, etc.

Contrary to Arduino Uno and Arduino Nano this board does not have a USB-to-Serial convert chip. Instead, the built-in USB-module handles all USB-related things. This offers greater control. Instead of only appearing as a serial port the Pro Micro can appear as a Keyboard, a Mouse, a Joystick, or something else.

When programming the USB-module is hidden from view. We interact with it through the Serial/Keyboard/Mouse/Joystick/HID APIs. This affords very little control. Fortunately, Arduino is open source and there is nothing stopping us from modifying the underlaying implementation.

Modifying the USB descriptor

There are two problems with the default USB descriptor that we would like to fix:

  1. there is no serial number, which means the COM-port number changes each time the device is plugged into a new USB-port,
  2. the product name is generic and not unique for our device, which means it is difficult to identify the device (without interrogating the device.)

Both the serial number and the product name are so called string descriptors. Most USB-devices have tree string descriptors: the manufacturer name, the product name, and the serial number.

USB Device Tree Viewer showing three string descriptor of interest.

The default string descriptors for Arduino’s AVR implementation are defined in USBCore.cpp. The product name is stored in STRING_PRODUCT which gets its value from the pre-processor define USB_PRODUCT. The value of USB_PRODUCT is supplied at compile time by Arduino.

const u8 STRING_PRODUCT[] PROGMEM = USB_PRODUCT;
const u8 STRING_MANUFACTURER[] PROGMEM = USB_MANUFACTURER;

The serial number is fetched on-demand from PluggableUSB. I’m not sure what PluggableUSB does and why. We will ignore it going forwards.

Settings a custom product name

We can take the easy route and manually modify USBCore.cpp to hard-code a STRING_PRODUCT value:

const u8 STRING_PRODUCT[] PROGMEM = "My Board";
const u8 STRING_MANUFACTURER[] PROGMEM = "My Company";

Or, we could figure out how Arduino provides the USB_PRODUCT value at compile time: for Pro Micro boards the USB_PRODUCT value is provided by SparkFun’s boards.txt and platform.txt as found in their SparkFun AVR Boards package.

The compiler flags are defined in platform.txt:

build.usb_flags=
    -DUSB_VID={build.vid}
    -DUSB_PID={build.pid}
    '-DUSB_MANUFACTURER={build.usb_manufacturer}'
    '-DUSB_PRODUCT={build.usb_product}' '-DUSB_INTERNAL_SERIAL'

The product name is defined in boards.txt:

promicro.build.usb_product="SparkFun Pro Micro"

We will create a new board definition with our own values. Start by installing SparkFun’s package through Arduino’s Board Manager.

This is the Arduino Board Manager with SparkFun AVR Boards package installed.

On Windows this package will install in %LocalAppData%\Arduino15\packages\SparkFun. Locate this folder and browse into hardware\avr\1.1.13. Copy the contents of this folder into Documents\Arduino\hardware\my_board\avr.

If everything worked out it should look something like this (you can remove all files except these):

Documents
 ┗━ Arduino
   ┗━ hardware
     ┗━ my_board
       ┗━ avr
         ┣━ bootloaders
         ┣━ variants
         ┣━ platform.txt
         ┗━ boards.txt

Open platform.txt change change name=SparkFun AVR Boards at the top to name=My Boards. Save the file.

Open boards.txt and modify promicro.build.usb_product="SparkFun Pro Micro" to anything you like. Save the file.

Choose your new board in Arduino and upload. Use USB Tree View or Windows Control Panel to verify the results.

NOTE If the USB descriptor is invalid your device won’t enumerate, the COM-port won’t show up, and you won’t be able to upload new firmware. You can force the device into bootloader mode by manually pulling the RST pin low. Look for this message in the Arduino output PORTS {COM1, } / {COM1, } => {} then reset the board (enable verbose output.)

Generating a unique serial number

The serial number is returned by the SendDescriptor() method. See the if-statement on line 527 in USBCore.cpp.

We are going to replace the original code with something that returns the unique serial number. The question is: how do we get a unique serial number? Ideally, this serial number should be unique to the device. Luckily, there is an undocumented serial number burnt into the ATmega32U4.

The serial number is located in the device’s Signature Row at address 0x0E. Use boot_signature_byte_get() from avr/boot.h to read the values. See the ArduinoUniqueID package for details.

The modified code looks like this:

else if (setup.wValueL == ISERIAL) {
#if defined(ARDUINO_ARCH_AVR)
#  if defined(__AVR_ATmega328PB__)
    const uint8_t ID_LEN = 10;
#  else
    const uint8_t ID_LEN = 9;
#  endif
    char name[ISERIAL_MAX_LEN] = {};
    for (uint8_t i = 0; i < ID_LEN; i++) {
        const uint8_t byte = boot_signature_byte_get(
            0x0E + i + (ID_LEN == 9 && i > 5 ? 1 : 0));

        // Convert to a human readable HEX value:
        name[i * 2 + 0] = "0123456789ABCDEF"[byte & 0x0F];
        name[i * 2 + 1] = "0123456789ABCDEF"[byte >> 4];
    }
    return USB_SendStringDescriptor((uint8_t*)name, strlen(name), 0);
#elif defined(PLUGGABLE_USB_ENABLED)
    // ...
#endif
}

Conclusion

Modifying the Pro Micro to contains a unique serial number and a product name was messy but well worth the time. Our board now retain its COM-port number between uses on the same computer. The custom product name allows for easier recognition in Windows systems. But most importantly, we can henceforth ignore the COM-port number entirely and instead write our programs to identify our board by name as described in the enumerating USB serial devices article.

Next part

In the next part we will look at a similar trick for the Teensy4.0 board.