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
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).
An I²C scan through ms-tools shows ACKs on 0x50–0x57 — 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
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.
MACROSILICON string in the dump is EDIDThe 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 (0x10–0x2F) 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.
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.
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).
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.
/dev/hdmi-msN instead of port-based hdmi-nucOld 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.
| Stick | USB ID | Symlink |
|---|---|---|
| MS1 | 534d:2101 | /dev/hdmi-ms1 |
| MS2 | 534d:2102 | /dev/hdmi-ms2 |
| MS3 | 534d:2103 | /dev/hdmi-ms3 |
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/.