Cheap MS2109 HDMI capture sticks: unique USB PIDs via EEPROM (no serial)

July 29, 2026    Linux USB udev HDMI MacroSilicon MS2109 EEPROM

TL;DR:

I own three cheap MacroSilicon MS2109 HDMI→USB capture sticks. They all show up as 534d:2109, with iSerial = 0, so classic ATTRS{serial} udev matching is impossible. Port-based rules (KERNELS=="1-3") work until someone moves a cable.

I dumped the external 24C16 EEPROM over the factory HID interface with ms-tools, confirmed that the nice-looking MACROSILICON string in the dump is EDID (not the USB product string — those come from mask ROM), then patched each stick’s EEPROM so it overrides the USB Product ID at boot (2101 / 2102 / 2103). After that, udev can create stable /dev/hdmi-ms1, /dev/hdmi-ms2, /dev/hdmi-ms3 regardless of which port I use.

Full notes, dumps, and the patch script: stulluk/macrosilicon-usb-serial

The hardware

Two tiny “Video Capture” USB dongles and one slightly larger “Video Capture With Loop” box — all MS2109-based under the hood:

Same three from the connector side (HDMI in / USB out, plus the loop-out box):

Before any patching, Linux sees them like this:

idVendor=534d  idProduct=2109
Manufacturer=MACROSILICON
Product=USB Video
iSerial=0

Composite device: UVC + USB audio + a vendor HID interface (that HID endpoint is the unlock for EEPROM access).

What we investigated

External EEPROM: yes

An I²C scan through ms-tools shows ACKs on 0x500x57 — the classic signature of a 24C16 (2 KiB) that aliases across those addresses. So the stick is not “ROM only”; there is a real config EEPROM on the board.

Dump (pure-Go HID build of ms-tools, no host Go toolchain needed if you build in Docker):

sudo ./bin/ms-tools-cli --raw-path=/dev/hidrawN --no-firmware \
  read EEPROM 0 --filename=ms2109_orig.bin

USB serial: no

MS2109 stock layouts do not expose a programmable USB serial string the way an FT232R does. Community firmware generators say the same thing: serial is a thing on MS2130, not on MS2109.

That MACROSILICON string in the dump is EDID

The dump contains ASCII MACROSILICON, but it sits in the HDMI EDID Display Product Name field (tag 0xFC, fixed 13-byte payload: MACROSILICON + 0x0A). That is what an HDMI source may show as the sink name — not what udev uses for ATTRS{product}.

USB string slots in the EEPROM header (0x100x2F) were all 0xFF on my sticks, so MACROSILICON / USB Video on the USB side are mask ROM defaults. Editing the EDID name does not give you unique /dev names.

VID/PID are not a header field on MS2109

On MS2107, EEPROM header bytes can hold VID/PID. On MS2109, 0x04 is hook flags. Live XDATA shows the USB device descriptor at 0xC688 with little-endian 4d 53 09 21 (534d:2109) coming from ROM.

So the workable approach (same idea as kraln/macrosilicon_firmware _set_usb_pid) is a tiny 8051 stub in the EEPROM code that writes the desired PID into XDATA 0xC692 after boot, then fixes the MS2109 checksums.

Will Debian’s kernel “forget” the device if we change PID? No — uvcvideo binds by interface class, not by a hard-coded 534d:2109. Capture still works; we only need udev to match the new idProduct.

Patch + write-back

python3 scripts/patch_ms2109_pid.py ms2109_orig.bin ms2109_pid2101.bin 0x2101

sudo ./bin/ms-tools-cli --raw-path=/dev/hidrawN --no-firmware \
  write-file --verify EEPROM 0 ms2109_pid2101.bin
# Wrote 2048 bytes to EEPROM:0000.
# Verification OK.

Then unplug/replug that stick only. Always keep the original dump; a bad image can brick useful USB enumeration (recovery is an external I²C programmer on the 24C16, often at 2.5 V with USB unplugged).

dmesg after a good replug

usb 1-3: New USB device found, idVendor=534d, idProduct=2101, bcdDevice=21.00
usb 1-3: Manufacturer: MACROSILICON
uvcvideo 1-3:1.0: Found UVC 1.00 device USB Video (534d:2101)
hid-generic ... MACROSILICON USB Video on usb-...-3/input4

Same pattern for 2102 / 2103.

udev: /dev/hdmi-msN instead of port-based hdmi-nuc

Old rules matched stock 2109 plus a USB path (hdmi-nuc / hdmi-rpi). New rules match only the patched PID and the capture V4L node:

SUBSYSTEM=="video4linux", ATTRS{idVendor}=="534d", ATTRS{idProduct}=="2101", \
  ENV{ID_V4L_CAPABILITIES}==":capture:", SYMLINK+="hdmi-ms1"

SUBSYSTEM=="video4linux", ATTRS{idVendor}=="534d", ATTRS{idProduct}=="2102", \
  ENV{ID_V4L_CAPABILITIES}==":capture:", SYMLINK+="hdmi-ms2"

SUBSYSTEM=="video4linux", ATTRS{idVendor}=="534d", ATTRS{idProduct}=="2103", \
  ENV{ID_V4L_CAPABILITIES}==":capture:", SYMLINK+="hdmi-ms3"

After reload:

/dev/hdmi-ms1 -> video4

Functional check was boring in the best way: ffmpeg grabbed a 1280×720 still, and mpv played the device live.

Assignment I used

StickUSB IDSymlink
MS1534d:2101/dev/hdmi-ms1
MS2534d:2102/dev/hdmi-ms2
MS3534d:2103/dev/hdmi-ms3

Repo

Everything lives here (README, patch script, sample dumps, udev file):

https://github.com/stulluk/macrosilicon-usb-serial

If you only needed FTDI-style serial strings instead, that is a different chip class — I wrote about that path earlier: /blog/ft232-eeprom-serial-linux/.



comments powered by Disqus