跳转至

修复绿联 UG307-95348 HDMI 采集卡清晰度与颜色问题

背景

上文 提到,我打算用采集卡来录制鸿蒙电脑的输出,作为 OBS 的输入来做软件导播,用的采集卡型号是采用了 MS2130S 芯片的绿联 UG307-95348 采集卡。在使用过程中,遇到了清晰度和颜色的问题,下面介绍我是怎么研究和解决的。

清晰度问题

首先是遇到了清晰度问题,在 macOS 上为 OBS 设置采集卡输入时,需要关闭 Use Preset 选项,选择 3840x2160 (16:9) - 30, 60 FPS - CS 709 - NV12 (420v),而不是 3840x2160 (16:9) - 30 FPS - CS 709 - NV12 (420v)。后者明显更糊,尽管从名称上看似乎只差一个帧率。如果勾选了 Use Preset,分辨率选 3820x2160,效果和上面第二种 4K 选项一样,也有些糊。

用下面这个 Swift 脚本打印采集卡的各种信息,可以发现 60 FPS 的那个版本经过了 MJPEG 压缩,从 dmb1 字段即可看出:

$ swift list_formats.swift
  3840x2160  420v  fps=30.0..30.0  dur=33333..33333us
      ext CVImageBufferColorPrimaries = ITU_R_709_2
      ext CVImageBufferTransferFunction = SMPTE_240M_1995
      ext CVImageBufferYCbCrMatrix = ITU_R_709_2
  3840x2160  420v  fps=60.0..60.0  dur=16667..16667us  fps=30.0..30.0  dur=33333..33333us
      ext CVImageBufferColorPrimaries = ITU_R_709_2
      ext CVImageBufferTransferFunction = SMPTE_240M_1995
      ext CVImageBufferYCbCrMatrix = ITU_R_709_2
      ext com.apple.cmio.format_extension.decompressed_from_format_type = 1684890161 (dmb1)

对应的 Swift 源码:

import AVFoundation
import CoreMedia

func fourcc(_ v: FourCharCode) -> String {
  let b: [UInt8] = [
    UInt8((v >> 24) & 255), UInt8((v >> 16) & 255),
    UInt8((v >> 8) & 255), UInt8(v & 255),
  ]
  let s = String(bytes: b, encoding: .ascii) ?? "?"
  return s.allSatisfy { $0.isLetter || $0.isNumber } ? s : String(format: "0x%08x", v)
}

let session = AVCaptureDevice.DiscoverySession(
  deviceTypes: [.external],
  mediaType: .video,
  position: .unspecified)

for d in session.devices {
  print("DEVICE \(d.localizedName) [\(d.uniqueID)]")
  print("  model=\(d.modelID)  manufacturer=\(d.manufacturer)")

  for f in d.formats {
    let dim = CMVideoFormatDescriptionGetDimensions(f.formatDescription)
    let sub = CMFormatDescriptionGetMediaSubType(f.formatDescription)

    var line = "  \(dim.width)x\(dim.height)  \(fourcc(sub))"
    for r in f.videoSupportedFrameRateRanges {
      line += String(
        format: "  fps=%.1f..%.1f  dur=%.0f..%.0fus",
        r.minFrameRate, r.maxFrameRate,
        CMTimeGetSeconds(r.minFrameDuration) * 1e6,
        CMTimeGetSeconds(r.maxFrameDuration) * 1e6)
    }
    print(line)

    if let ext = CMFormatDescriptionGetExtensions(f.formatDescription) as? [String: Any] {
      for k in ext.keys.sorted() {
        var v = "\(ext[k]!)"
        // decode fourcc-valued extensions such as
        //   com.apple.cmio.format_extension.decompressed_from_format_type
        if k.contains("format_type"), let n = ext[k] as? NSNumber {
          v = "\(n.uint32Value) (\(fourcc(n.uint32Value)))"
        }
        print("      ext \(k) = \(v)")

      }
    }
  }
}

猜想压缩的版本,实际的分辨率更高,经过压缩后可以通过 USB 5Gbps 正常传输;不压缩的版本,由于带宽限制,内部不是真正按照 4K@30Hz 处理的,导致画质有损耗。

颜色问题

除了清晰度问题,采集卡采到的鸿蒙电脑画面颜色不对。在鸿蒙电脑上打开 Lagom 白饱和测试图,采集到的 RGB 与预期对不上,大致关系如下:

  • 原来 200 -> 显示 219
  • 原来 244 -> 显示 255

用 ffmpeg 观察后发现,采集卡实际给出的是 204;由于这是 limited range(16-235)下的 204,转换到 full range 后就是 (204 - 16) / 219 * 255 = 219。若把鸿蒙电脑直接接到显示器上,显示则正常。

深入研究后,我找到了一些通过设置 MS2130S 寄存器来改变其行为的方法(参考 steve-m/hsdaoh)。在 AI 的帮助下定位到了问题:只要关闭 MS2130S 自带的 luma processing(即把寄存器 0xfc8e 从原来的 0x00 改为 0x11),颜色就会恢复正常。下面这个小工具可以在 OBS 开始录制后运行,用来 toggle luma processing,从而实时看到颜色变化:

/*
 * ugreen_fix_toggle - minimal hidapi-only tool for the UGREEN 95348
 *                     (MS2130S, 2b89:5348).
 *
 * Reads a video-processing register and toggles it:
 *   0x00 -> 0x11   (disable the chip's luma processing / fix the 200->219 lift)
 *   0x11 -> 0x00   (re-enable it / reproduce the bug)
 *
 * Default register is 0xfc8e (confirmed to be the luma-processing register).
 * Pass another address as the first argument if needed, e.g.
 *   ./ugreen_fix_toggle 0xfc80
 *
 * build (macOS/homebrew, hidapi only):
 *   cc -O2 -I/opt/homebrew/include/hidapi ugreen_fix_toggle.c \
 *      -L/opt/homebrew/lib -lhidapi -o ugreen_fix_toggle
 */
#include <hidapi.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define VID 0x2b89
#define PID 0x5348
#define DEFAULT_REG 0xfc8e

static hid_device *h;

/* MS2130S vendor HID feature report:
 *   [0x01, 0xb6, addrH, addrL, val, 0, 0, 0, 0]  write
 *   [0x01, 0xb5, addrH, addrL, 0, 0, 0, 0, 0]    read request
 * GET_REPORT returns 64 bytes; the value is byte 4. */
static int reg_write(uint16_t addr, uint8_t val) {
  unsigned char buf[9] = {0x01, 0xb6, addr >> 8, addr & 0xff, val, 0, 0, 0, 0};
  return hid_send_feature_report(h, buf, sizeof(buf));
}

static int reg_read(uint16_t addr, uint8_t *val) {
  unsigned char cmd[9] = {0x01, 0xb5, addr >> 8, addr & 0xff, 0, 0, 0, 0, 0};
  unsigned char rsp[64];

  if (hid_send_feature_report(h, cmd, sizeof(cmd)) < 0)
    return -1;
  memset(rsp, 0, sizeof(rsp));
  rsp[0] = 0x01;
  if (hid_get_feature_report(h, rsp, sizeof(rsp)) < 0)
    return -1;
  *val = rsp[4];
  return 0;
}

int main(int argc, char **argv) {
  uint16_t addr = DEFAULT_REG;
  uint8_t cur, next;

  if (argc > 1)
    addr = (uint16_t)strtoul(argv[1], NULL, 0);

  if (hid_init() < 0) {
    fprintf(stderr, "hid_init failed\n");
    return 1;
  }
  h = hid_open(VID, PID, NULL);
  if (!h) {
    fprintf(stderr, "UGREEN %04x:%04x not found (is it plugged in?)\n", VID,
            PID);
    return 1;
  }

  if (reg_read(addr, &cur) < 0) {
    fprintf(stderr, "register read failed: %ls\n", hid_error(h));
    hid_close(h);
    return 1;
  }

  if (cur == 0x00) {
    next = 0x11;
  } else if (cur == 0x11) {
    next = 0x00;
  } else {
    fprintf(stderr, "%04x = 0x%02x (unexpected, not touching)\n", addr, cur);
    hid_close(h);
    return 2;
  }

  if (reg_write(addr, next) < 0) {
    fprintf(stderr, "register write failed: %ls\n", hid_error(h));
    hid_close(h);
    return 1;
  }

  printf("%04x: 0x%02x -> 0x%02x\n", addr, cur, next);
  printf("(0x11 = luma processing disabled = fix on; 0x00 = default/bug)\n");

  hid_close(h);
  hid_exit();
  return 0;
}

编译和运行:

$ brew install hidapi
$ cc -O2 -I/opt/homebrew/include/hidapi ugreen_fix_toggle.c -L/opt/homebrew/lib -lhidapi -o ugreen_fix_toggle
# 此时是有问题的状态
$ ./ugreen_fix_toggle
fc8e: 0x00 -> 0x11
(0x11 = luma processing disabled = fix on; 0x00 = default/bug)
# toggle 以后,颜色问题修复
$ ./ugreen_fix_toggle
fc8e: 0x11 -> 0x00
(0x11 = luma processing disabled = fix on; 0x00 = default/bug)
# 再次 toggle,颜色问题重新出现

修复后,200 变成 199,244 变成 243。虽然仍有很小的偏差,但可以认为问题已经解决。

不过每次开始采集后都要重新跑一次这个工具,还是有点麻烦。一个一劳永逸的办法是参考 steve-m/ms2130_patcher,给固件打补丁,让硬件往 0xfc8e 寄存器写入 0x11 而不是 0x00。

首先用 steve-m/ms213x_flash 导出绿联 95348 自带的固件,然后让 AI 进行逆向,这个固件就是一个 8051 代码,有很多成熟的工具。具体的补丁方法和上面类似,下面直接给出 AI 对固件代码以及如何修复的分析:

补丁的原理

复位流程

0xfc8e 有两个相关的位:bit 0(掩码 0x01)和 bit 4(掩码 0x10)。流重初始化流程 FUN_CODE_c220() 会通过位掩码辅助函数 FUN_CODE_87c7(mask, addrH, addrL, value) 把这两位都清零。要写入的值通过 R3 传入:非零表示置位被掩码选中的位,零表示清零。

CPU 地址(bank 1) 代码 作用
c268 MOV R3,#01h ; JNB bit05,c26f ; MOV R3,#00h
MOV R5,#01h ; MOV R7,#8eh ; MOV R6,#fch ; LJMP 87c7h
清除 0xfc8e 的 bit 0
c27e MOV R3,#01h ; JNB bit05,c285 ; MOV R3,#00h
MOV R5,#10h ; MOV R7,#8eh ; MOV R6,#fch ; LJMP 87c7h
清除 0xfc8e 的 bit 4

两次调用之后 0xfc8e = 0x00

具体改动

把两处 MOV R3,#00h7b 00)指令改成 MOV R3,#01h7b 01),这样每次掩码更新都会走置位分支,寄存器最终变成 0x11

文件偏移 原始值 补丁值 含义
0x1429e(bank1 c26e 00 01 0xfc8e bit 0 的取值操作数
0x142b4(bank1 c284 00 01 0xfc8e bit 4 的取值操作数
0x18033 7c 7e 代码校验和 0x797c0x797e

反汇编打过补丁的字节,可以看到两处立即数现在都加载 0x01

c268: 7b01  MOV R3, #01h
c26a: 300502 JNB bit05, c26fh
c26d: 7b01  MOV R3, #01h      <- 原来是 #00h
c26f: 7d01  MOV R5, #01h
c271: 7f8e  MOV R7, #8eh
c273: 7efc  MOV R6, #fch
c275: 0287c7 LJMP 87c7h

小结

核心就是把上面我通过 hidapi 从 host 端写入寄存器的操作,换成了直接在固件里写入:固件本来是 clear,改成了 set,这样就禁用了 luma processing,持久化了这个改动。

这部分代码以及固件已经开源到 jiegec/ugreen-95348-patcher,感兴趣的读者可以尝试一下,尝试之前记得备份固件,而且有变砖的风险。

P.S. 实测发现,把 0xfc8e 改为 0x11 只对 3840x2160 (16:9) - 30, 60 FPS - CS 709 - NV12 (420v) 模式生效;对 3840x2160 (16:9) - 30 FPS - CS 709 - NV12 (420v) 模式则无效:前者画面清晰、颜色正确,后者画面模糊、颜色也不对。具体原因尚未深入分析。

总结

其实 MS2130S 这款芯片在网络上已经有很多现成的研究,从寄存器用法、hidapi 访问到固件补丁,都能找到前人的成果。这次能比较顺利地定位并解决问题,很大程度上是站在这些探索的肩膀上,在此对这些作者表示感谢。

相关项目链接整理如下:

  • steve-m/hsdaoh:通过 hidapi 访问 MS2130S 寄存器的库,本文从 host 端修改 0xfc8e 的思路就来自这里。
  • steve-m/ms2130_patcher:直接给固件打补丁、持久化寄存器配置的工具,是本文固件补丁的重要参考。
  • steve-m/ms213x_flash:用来导出/烧写 MS213x 固件的工具,本文用它导出了绿联 95348 的原始固件。

这些项目大多出自 steve-m 之手,感谢他的开源工作。

评论 / Comments