Skip to content

Instantly share code, notes, and snippets.

@supermar1010
Last active October 5, 2026 02:38
Show Gist options
  • Select an option

  • Save supermar1010/3e81e3f23ce16c826e383edbf9442351 to your computer and use it in GitHub Desktop.

Select an option

Save supermar1010/3e81e3f23ce16c826e383edbf9442351 to your computer and use it in GitHub Desktop.
Xiaomi Mi Curved Gaming Monitor 34" (2020): OSD goes black after using a DDC tool. Fix

Xiaomi Mi Curved Gaming Monitor 34" (2020): OSD goes black after using a DDC tool. Fix

Use at your own risk. This worked on my monitor, but there is no guarantee it works on yours. It writes to the monitor's settings EEPROM. See the disclaimer at the bottom.

Symptom

  • Pressing the joystick to open the on-screen menu (OSD) turns the screen black for ~2 seconds. Then the picture comes back, but no menu.
  • Happens on every input (DP, HDMI) and every source device.
  • It typically starts after a DDC/CI tool was used, for example a brightness app like Monitorian, Twinkle Tray, ClickMonitorDDC, ddcutil, or anything with a "turn display off" function.
  • Factory reset isn't possible because you can't reach the menu. Power-cycling doesn't help.

Tested on: EDID XMI "Mi Monitor", product code 0x3444, MStar MST9U scaler, built 2020. Other Mi monitors with the same MStar firmware might be affected. The script checks before it changes anything.

Cause

The monitor firmware has a bug in its handler for the DDC/CI power mode command (VCP 0xD6):

  1. When the monitor receives D6 with almost any value while it's on (including D6 01 = "on" and D6 04 = "off", which DDC tools send), it sets a "power-down requested via DDC" flag. That's bit 0x10 of settings byte 0x43. The value check is broken: it accepts everything except 5.
  2. The flag is saved to the settings EEPROM, so it survives power-cycles.
  3. While the flag is set, the menu code doesn't open the OSD on a joystick press. It re-syncs the current input instead. That's the 2-second black screen.
  4. No reachable code ever clears the flag. D6 05, the DDC factory reset (VCP 04) and power-cycling don't touch it.

The fix is to clear that one bit in the EEPROM. The monitor itself provides vendor DDC commands to read and write its settings EEPROM (7B 99 22 read, 7B 99 55 write, 16 bytes at a time), so no hardware is needed.

Fix (Linux)

You need a Linux machine (a live USB works) with the monitor connected by HDMI or DisplayPort directly to the GPU. Docks and adapters often don't pass DDC through. You also need Python 3 and ddcutil.

1. Make DDC work

sudo modprobe i2c-dev
sudo ddcutil detect

Note the I2C bus: /dev/i2c-N line of the "Mi Monitor" entry. N is your bus number. If ddcutil reports "DDC communication failed", power-cycle the monitor and try again.

2. Check (read-only)

Download mi_monitor_osd_fix.py and run:

sudo python3 mi_monitor_osd_fix.py N

The script:

  • reads the monitor's EDID and checks that bus N is the Mi Monitor (vendor XMI, product code 0x3444). On any other monitor it prints a warning and aborts without sending any DDC command,
  • verifies it is talking to the expected firmware (EEPROM byte 0x00 must be 0x74; it aborts otherwise),
  • saves a backup of EEPROM 0x00–0xFF to mi_monitor_eeprom_0000-00ff_<timestamp>.bin,
  • reports byte 0x43.

If it says "bit 0x10 is NOT set", your problem is something else. Stop here.

3. Apply the fix

Read this first: the monitor periodically saves its in-RAM settings back to the EEPROM. RAM still holds the old value until the next reboot. If you wait after writing, the periodic save will undo the fix. That happened during testing. So have the power cable within reach.

sudo python3 mi_monitor_osd_fix.py N --write

It reads the 16-byte block at 0x40, clears only bit 0x10 of byte 0x43, writes the block back and reads it again to verify. When it prints OK:

  1. Immediately pull the monitor's power cable. Don't use the power button.
  2. Wait ~10 seconds, plug it back in.
  3. Press the joystick. The OSD should open.

If the OSD still goes black, run step 2 again. If byte 0x43 has 0x10 set again, you were too slow: repeat step 3.

4. Avoid it happening again

Don't let DDC tools send VCP 0xD6 (power mode) to this monitor. Brightness, contrast and the other VCP codes are fine. In tools with a "turn off display via DDC" feature, disable that for this monitor.

Technical details

DDC read command 51 86 03 7B 99 22 HH LL <chk> → reply 6E 92 <16 bytes> …
DDC write command 51 96 03 7B 99 55 HH LL <16 bytes> <chk>
Address mapping EEPROM address = (HH << 8) + LL + 0xAA4 (16-bit wrap, LL unsigned)
EEPROM layout 0x000–0x03E factory/calibration, 0x03F–0x0E3 user settings, 0x0E4–… per-mode tables. Copied 1:1 into the firmware's settings struct
Bad flag 0x43 bit 0x10, set in the VCP D6 handler, tested in the OSD key handler (keys 0–3 → input re-sync instead of menu), never cleared
Periodic save the whole user block 0x3F–0xE3 is rewritten every 900 ticks of the power-on counter at 0x9E–0xA1

Found by dumping the SPI flash (read-only, via flashrom -p mstarddc_spi), decompressing the MStar main application (mstar_extract_decompress) and decompiling it with Ghidra and ghidra-aeon.

Disclaimer: this writes to your monitor's settings EEPROM. It worked on my unit, but there is no guarantee it will work on yours, and I can't promise it won't cause problems. The script refuses to write if anything looks unexpected and saves a backup first, but you use it entirely at your own risk. I'm not responsible for any damage to your monitor or data.

#!/usr/bin/env python3
"""Fix the Xiaomi Mi Curved 34" (2020) "OSD goes black" problem over DDC/CI (Linux).
Clears the stuck "DDC power-down pending" bit (0x10) in settings EEPROM byte 0x43.
Without --write it only reads and reports. With --write it changes exactly one bit
and verifies the result. Run it, then IMMEDIATELY pull the monitor's power cable.
Usage:
sudo python3 mi_monitor_osd_fix.py <i2c-bus> # check only (read-only)
sudo python3 mi_monitor_osd_fix.py <i2c-bus> --write # apply the fix
Find the bus with `ddcutil detect` (the "/dev/i2c-N" line of the Mi Monitor).
The script reads the EDID first and aborts with a warning if the bus is not
the Mi Monitor (vendor XMI, product code 0x3444).
Use at your own risk.
"""
import fcntl, os, sys, time
I2C_SLAVE = 0x0703
DDC_ADDR = 0x37
BASE = 0xAA4 # firmware adds this to the requested EEPROM address
BLOCK = 0x40 # 16-byte block containing byte 0x43
FLAGS = 0x43
BIT = 0x10
EDID_ADDR = 0x50
EDID_HEADER = b"\x00\xff\xff\xff\xff\xff\xff\x00"
EDID_VENDOR = "XMI" # EDID manufacturer ID of the Mi Monitor
EDID_PRODUCT = 0x3444 # EDID product code of the Mi Curved 34" (2020)
def read_edid(bus):
"""Read the 128-byte base EDID block from the monitor on this bus."""
fd = os.open(f"/dev/i2c-{bus}", os.O_RDWR)
try:
fcntl.ioctl(fd, I2C_SLAVE, EDID_ADDR)
os.write(fd, b"\x00")
return os.read(fd, 128)
finally:
os.close(fd)
def check_monitor(bus):
"""Abort with a warning unless the bus belongs to the expected Mi Monitor."""
try:
edid = read_edid(bus)
except OSError as e:
sys.exit(f"WARNING: cannot read the EDID on /dev/i2c-{bus} ({e}). "
"Cannot confirm this is the Mi Monitor, aborting. Nothing was changed.")
if len(edid) < 12 or edid[:8] != EDID_HEADER:
sys.exit(f"WARNING: no valid EDID on /dev/i2c-{bus}. "
"Cannot confirm this is the Mi Monitor, aborting. Nothing was changed.")
mfg = edid[8] << 8 | edid[9]
vendor = "".join(chr(((mfg >> s) & 0x1F) + 64) for s in (10, 5, 0))
product = edid[10] | edid[11] << 8
if (vendor, product) != (EDID_VENDOR, EDID_PRODUCT):
sys.exit(f"WARNING: /dev/i2c-{bus} is monitor {vendor} {product:#06x}, not the "
f"Mi Monitor ({EDID_VENDOR} {EDID_PRODUCT:#06x}). This fix is only for "
"that monitor, aborting. Nothing was changed.")
print(f"monitor on /dev/i2c-{bus}: {vendor} {product:#06x} (Mi Monitor), OK")
def hi_lo(addr):
want = (addr - BASE) & 0xFFFF
return want >> 8, want & 0xFF
def xfer(fd, payload, rlen=40):
pkt = bytes([0x51, 0x80 | len(payload)]) + bytes(payload)
chk = 0x6E
for b in pkt:
chk ^= b
os.write(fd, pkt + bytes([chk]))
time.sleep(0.08)
return os.read(fd, rlen)
def read16(fd, addr):
"""Vendor command 7B 99 22: read 16 EEPROM bytes."""
for _ in range(3):
r = xfer(fd, [0x03, 0x7B, 0x99, 0x22, *hi_lo(addr)])
if len(r) >= 18 and r[0] == 0x6E and r[1] == 0x92:
return bytes(r[2:18])
time.sleep(0.2)
sys.exit(f"no valid reply reading EEPROM {addr:#x} (is this the Mi Monitor's bus?)")
def write16(fd, addr, data):
"""Vendor command 7B 99 55: write 16 EEPROM bytes."""
return xfer(fd, [0x03, 0x7B, 0x99, 0x55, *hi_lo(addr), *data])
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
bus, do_write = sys.argv[1], "--write" in sys.argv[2:]
check_monitor(bus)
fd = os.open(f"/dev/i2c-{bus}", os.O_RDWR)
fcntl.ioctl(fd, I2C_SLAVE, DDC_ADDR)
head = read16(fd, 0x00)
if head[0] != 0x74:
sys.exit(f"EEPROM byte 0x00 is {head[0]:#04x}, expected 0x74: not the expected "
"monitor/firmware, aborting")
backup = b"".join(read16(fd, a) for a in range(0x00, 0x100, 16))
name = f"mi_monitor_eeprom_0000-00ff_{time.strftime('%Y%m%d-%H%M%S')}.bin"
open(name, "wb").write(backup)
print(f"backup of EEPROM 0x00-0xFF saved to {name}")
cur = read16(fd, BLOCK)
if cur != backup[BLOCK:BLOCK + 16]:
sys.exit("block read is unstable, aborting")
val = cur[FLAGS - BLOCK]
print(f"byte 0x43 = {val:#04x}")
if not val & BIT:
print("bit 0x10 is NOT set: this fix does not apply to your monitor.")
return
print("bit 0x10 is set: this is the stuck 'DDC power-down pending' flag.")
if not do_write:
print("Read-only run. Re-run with --write to clear it.")
return
new = bytearray(cur)
new[FLAGS - BLOCK] = val & ~BIT
print("old block:", cur.hex(" "))
print("new block:", new.hex(" "))
write16(fd, BLOCK, new)
time.sleep(0.3)
back = read16(fd, BLOCK)
os.close(fd)
if back != bytes(new):
sys.exit(f"readback mismatch: {back.hex(' ')}\nRestore from {name} if needed.")
print("\nOK, written and verified.")
print(">>> PULL THE MONITOR'S POWER CABLE NOW (not the power button), wait 10 s, "
"plug it back in. <<<")
print("The monitor periodically saves its RAM copy (still with the bit set) back "
"to the EEPROM, which would undo the fix if you wait.")
if __name__ == "__main__":
main()
@Batmaaaaan

Copy link
Copy Markdown

YOOOOOOO!!! I wonder if it will work with my 27 2k mi gaming monitor!!

@Batmaaaaan

Copy link
Copy Markdown

WONDERFUL!! My god, wouldn't think AI wilould help here.. Thanks man!! I'll try it once I'm back home!!

@supermar1010

Copy link
Copy Markdown
Author

I added a check for my type of monitor, as I only know it works there and the memory layout is correct. But I'm sure claude can figure out if your monitor is the same and do the same fix.

@Batmaaaaan

Copy link
Copy Markdown

I added a check for my type of monitor, as I only know it works there and the memory layout is correct. But I'm sure claude can figure out if your monitor is the same and do the same fix.

Alright, never used claude but I'll see then 👌

@Batmaaaaan

Copy link
Copy Markdown

Now if it'll let me know how to do it on windows as welll I'll be golden.

@brlin-tw

brlin-tw commented Oct 2, 2026 •

Copy link
Copy Markdown

In my specific (functional) unit the script bails at:

$ sudo python3 mi_monitor_osd_fix.py 17
monitor on /dev/i2c-17: XMI 0x3444 (Mi Monitor), OK
EEPROM byte 0x00 is 0x72, expected 0x74: not the expected monitor/firmware, aborting

One might want to disable the check in:

https://gist.github.com/supermar1010/3e81e3f23ce16c826e383edbf9442351#file-mi_monitor_osd_fix-py-L101-L103

as the check might be too strict.

After bypassing that check the script correctly detects that the unit is in fact not need fixing:

bit 0x10 is NOT set: this fix does not apply to your monitor.

@supermar1010

Copy link
Copy Markdown
Author

@brlin-tw maybe yes. I err'ed on the side of being too cautious, I just posted this as a reference for others on how I fixed mine, so they could use it to fix theirs. But the script might need updates for them.

But also this directly writes to the monitor, so if a different monitor has a different eeprom layout this might just brick that's why I guarded it.

@rm-killer

Copy link
Copy Markdown

Thanks to @supermar1010 i managed to fix my monitor as well. it was a little bit different and i was in Windows 11 as well. https://github.com/rm-killer/xiaomi-curved-34-osd-fix-windows
you guys can check it out and as always Do it with your responsibility.

@rm-killer

Copy link
Copy Markdown

by the way you guys can use this app for the Monitor Controlling. its optimized for Xiaomi monitors that dose not make OSD disappear.

https://github.com/rm-killer/RM-Display-Commander

@Batmaaaaan

Copy link
Copy Markdown

by the way you guys can use this app for the Monitor Controlling. its optimized for Xiaomi monitors that dose not make OSD disappear.

https://github.com/rm-killer/RM-Display-Commander

You're both godsend!

@chid

chid commented Oct 2, 2026

Copy link
Copy Markdown

Thanks for publishing the protocol and analysis. I made a macOS port using this gist and @rm-killer's Windows revision mapping:

https://github.com/chid/xiaomi-curved-34-osd-fix-macos

Tested on Apple Silicon with macOS 26.5 and an XMI 0x3444. The script cleared bit 0x10 at EEPROM byte 0x43, verified the full block readback, and unplugging/reconnecting power restored the OSD.

It defaults to read-only, checks the physical EDID and vendor reply checksums, and saves a verified 256-byte backup before writing. It also includes the 0x3445 profile from the Windows fix and an Intel IOKit backend, but those paths have not been tested on real hardware. The README credits the sources and explains the immediate-unplug requirement.

@leag

leag commented Oct 3, 2026

Copy link
Copy Markdown

@supermar1010

Copy link
Copy Markdown
Author

Damn folks this makes me so happy that you all could also fix your monitors and made little scripts!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment