From 65a3117d10e6a18eed2841350494d6b67c0e8f19 Mon Sep 17 00:00:00 2001 From: chenjiangqun Date: Wed, 1 Jul 2026 18:58:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DN=E5=8D=A1DDC=E9=80=9A?= =?UTF-8?q?=E9=81=93=E9=99=90=E5=88=B6=E5=AF=BC=E8=87=B4=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E8=B6=85=E8=BF=8715=E5=AD=97=E8=8A=82?= =?UTF-8?q?=EF=BC=8C=E5=AF=BC=E8=87=B4=E5=8F=AF=E8=83=BD=E6=BC=8F=E5=88=A4?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WinISP/BaseClass/MTKDebugCmd.cs | 31 +++ .../WinISP/FormISP_MSI.cs | 219 +++++++++++++++--- 2 files changed, 220 insertions(+), 30 deletions(-) diff --git a/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/BaseClass/MTKDebugCmd.cs b/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/BaseClass/MTKDebugCmd.cs index 7fe420a..23b9753 100644 --- a/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/BaseClass/MTKDebugCmd.cs +++ b/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/BaseClass/MTKDebugCmd.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using WinISP; namespace MTK { @@ -72,6 +74,12 @@ namespace MTK strData = ""; if (_i2cCtrl.ReadData(recvBuf, wrCmds) == true) { + frmISP.WriteLog( + $"[DDC_Read RAW] cmd=0x{data[1]:X2} recvLen={recvLen} " + + $"raw=[{string.Join(" ", recvBuf.Select(b => b.ToString("X2")))}]", + "debug"); + + // ──────── for (int i = 0; i < recvBuf.Length; ++i) strData += recvBuf[i].ToString("X2") + " "; @@ -102,6 +110,29 @@ namespace MTK return null; } + /// + /// 从 DDC 0x6F 继续读取数据(不重新发送命令)。 + /// 用于 N卡 DP AUX 通道 16 字节限制导致截断时的分段续读。 + /// 在 DDC_Read 成功返回后立即调用,尝试读取响应缓冲区中尚未消费的剩余字节。 + /// + /// 要读取的字节数 + /// 读取到的字节数组,失败返回 null + public static Byte[] DDC_ReadContinue(int recvLen) + { + const Byte dest = 0x6F; + Byte[] wrCmds = { dest }; + Byte[] recvBuf = new Byte[recvLen]; + if (_i2cCtrl.ReadData(recvBuf, wrCmds)) + { + frmISP.WriteLog( + $"[DDC_ReadContinue] rdLen={recvLen} raw=[{string.Join(" ", recvBuf.Select(b => b.ToString("X2")))}]", + "debug"); + return recvBuf; + } + frmISP.WriteLog($"[DDC_ReadContinue] rdLen={recvLen} read failed", "debug"); + return null; + } + private static Byte CalCheckSum(Byte[] data, int startIdx = 0) { Byte chkSum = 0; diff --git a/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/FormISP_MSI.cs b/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/FormISP_MSI.cs index 2dcb4d0..12a876e 100644 --- a/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/FormISP_MSI.cs +++ b/WIN10_WinISP_SrcCode_For_MOKA_20250416_1/WinISP/FormISP_MSI.cs @@ -275,7 +275,7 @@ namespace WinISP if (display) MessageBox.Show(msg, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } - /// + /// /// Send a 2-byte DDC command and parse the ASCII string returned by the monitor firmware. /// recv layout: [hdr0][hdr1][char0][char1]...[\0] /// @@ -283,11 +283,15 @@ namespace WinISP { Byte[] cmd = { 0xCC, subCmd }; int[] readLens = { 36, 32, 24, 20, 16, 12, 8 }; - string bestPartial = String.Empty; + string bestPartial = string.Empty; + bool segReadTried = false; // 分段续读只尝试一次 + + // N卡驱动已知的单次有效数据上限(超过此长度才信任 isComplete 判断) + // 低于或等于此值时,即使找到\0也可能是驱动填充,不能轻易认为完整 + const int NV_SUSPECT_TRUNCATE_LEN = 15; foreach (int readLen in readLens) { - // Retry each readLen up to 3 times to handle NVIDIA DP AUX bus instability. for (int attempt = 0; attempt < 3; attempt++) { Byte[] recv = MTKDebugCmd.DDC_Read(cmd, readLen, delayTime); @@ -297,16 +301,22 @@ namespace WinISP continue; } - // Error response pattern in logs: 6E 80 BE ... + // Error response: 6E 80 BE ... if (recv[1] == 0x80) { WinIOLib.DelayMs(20); continue; } - int len = 0; - while ((2 + len) < recv.Length && recv[2 + len] != 0) - len++; + int reportedDataLen = recv[1] & 0x7F; // DDC响应头中声明的数据长度 + + // 先按原始方式扫描整个buffer找\0(用于完整性判断) + // 然后截断到 reportedDataLen,排除末尾DDC校验和字节 + int rawLen = 0; + while ((2 + rawLen) < recv.Length && recv[2 + rawLen] != 0) + rawLen++; + + int len = Math.Min(rawLen, reportedDataLen); if (len <= 0) { @@ -315,28 +325,137 @@ namespace WinISP } string value = System.Text.Encoding.ASCII.GetString(recv, 2, len).Trim(); + value = TrimNonStandardDdcSuffix(value); if (string.IsNullOrWhiteSpace(value)) { WinIOLib.DelayMs(20); continue; } - // Only return when the null terminator was found inside the buffer (complete string). - bool isComplete = (2 + len) < recv.Length; + // nullTerminatorFound: 基于原始扫描长度判断(rawLen 可能超过 reportedDataLen, + // 因为扫过了 checksum 字节才在 padding 区遇到 \0) + bool nullTerminatorFound = (2 + rawLen) < recv.Length; + + // ── 排除"驱动填充零"误判 ────────── + // 条件:\0确实存在 AND 读到的有效长度 > 驱动截断嫌疑值 + // 即:len > NV_SUSPECT_TRUNCATE_LEN 才认为是真正的字符串结束符 + // len <= NV_SUSPECT_TRUNCATE_LEN 时,\0 很可能是驱动填充,不可信 + bool isComplete = nullTerminatorFound && len > NV_SUSPECT_TRUNCATE_LEN; + if (isComplete) return value; - // Truncated (buffer full, no null found) - keep as best partial and keep retrying. + // ── N卡截断修复:尝试分段续读 ────────────────────────── + // 当读到的有效长度 <= NV_SUSPECT_TRUNCATE_LEN,且 DDC 响应头声明 + // 还有更多数据时,尝试从 0x6F 续读剩余数据。 + // N卡 DP AUX 通道单次 I2C 读取上限约 16 字节(2头 + 14数据), + // 导致长字符串(如 PanelName/BoardName)被截断。 + if (!segReadTried && len <= NV_SUSPECT_TRUNCATE_LEN) + { + if (reportedDataLen > len) + { + segReadTried = true; // 标记已尝试,不再重复 + int remainDataLen = reportedDataLen - len; + // 续读剩余数据 + 校验和(多读1字节余量) + Byte[] cont = MTKDebugCmd.DDC_ReadContinue(remainDataLen + 1); + + if (cont != null && cont.Length >= 1) + { + // 续读数据需排除"新响应头"误判: + // 只有 [0x6E] + [Len|0x80] 同时匹配才算监视器重置了指针。 + // 单独一个 0x6E(ASCII 'n')可能是字符串内容,不能误判。 + bool contIsNewResponse = (cont.Length >= 2 + && cont[0] == 0x6E && (cont[1] & 0x80) != 0); + + if (!contIsNewResponse) + { + // 续读数据不是新响应头 → 可能是真正的续传数据 + // 只取可打印ASCII (0x20-0x7E),校验和字节(>=0x80)会被自然截断 + int contLen = 0; + while (contLen < cont.Length + && cont[contLen] >= 0x20 && cont[contLen] <= 0x7E) + contLen++; + + if (contLen > 0) + { + string contStr = TrimNonStandardDdcSuffix( + Encoding.ASCII.GetString(cont, 0, contLen).Trim()); + string fullValue = TrimNonStandardDdcSuffix(value + contStr); + + frmISP.WriteLog( + $"[DDC] cmd=0x{subCmd:X2} 分段续读成功: '{value}' + '{contStr}' = '{fullValue}' " + + $"(first={len}, cont={contLen}, reported={reportedDataLen})", + "debug"); + + return fullValue; + } + } + else + { + // 续读返回新响应头 → 监视器重置了指针,不支持续读 + frmISP.WriteLog( + $"[DDC] cmd=0x{subCmd:X2} 分段续读: 监视器重置指针,续读返回新响应头 " + + $"(first={len}, reported={reportedDataLen})", + "debug"); + } + } + } + } + // ── End N卡截断修复 ────────────────────────────────────── + + // 未通过完整性校验,记录为最优截断值,继续尝试更大 readLen if (value.Length > bestPartial.Length) + { bestPartial = value; + frmISP.WriteLog( + $"[DDC] cmd=0x{subCmd:X2} readLen={readLen} attempt={attempt} " + + $"partial='{value}' (len={len}, nullFound={nullTerminatorFound})", + "debug"); + } + WinIOLib.DelayMs(20); } } - // Return longest partial result if we never got a complete one. + // 所有 readLen 都未能读到完整值,返回最长截断结果 + // TryExpandField 负责前缀补全 + if (!string.IsNullOrEmpty(bestPartial)) + { + frmISP.WriteLog( + $"[DDC] cmd=0x{subCmd:X2} all readLens exhausted, best='{bestPartial}'", + "debug"); + } + return bestPartial; } + /// + /// 滤除 DDC 字符串尾部的非标准字符。 + /// AMD ADL DDC 偶尔返回损坏的响应头(reportedDataLen 被放大), + /// 导致字符串尾部夹带垃圾字节(如 checksum + 下一字节)。 + /// 监视器字段名合法字符集:[A-Za-z0-9 _.\\-/] + /// + private static string TrimNonStandardDdcSuffix(string raw) + { + if (string.IsNullOrEmpty(raw)) + return raw; + int end = raw.Length; + while (end > 0) + { + char c = raw[end - 1]; + if (char.IsLetterOrDigit(c) || c == '_' || c == ' ' || c == '.' || c == '-' || c == '/') + break; + end--; + } + if (end < raw.Length) + { + frmISP.WriteLog( + $"[DDC-Sanitize] trimmed {raw.Length - end} trailing char(s): '{raw}' → '{raw.Substring(0, end)}'", + "debug"); + } + return raw.Substring(0, end); + } + private bool TryReadMonitorIdentity(int retryCount = 5, int delayMs = 80) { // Name commands are more stable in non-tool mode on some GPU/DDC paths. @@ -538,37 +657,77 @@ namespace WinISP /// /// AMD/Intel 友好的型号读取:带响应有效性校验 + 多轮重试。 /// AMD ADL DDC 读命中率低(约 4 错 1 对),需要足够重试次数才能读到完整字符串。 + /// 采用"众数策略"而非"取最长":AMD 偶尔返回损坏的响应头会拉长字符串, + /// 统计上正确的值出现频率最高,长度众数 + 值众数投票能过滤掉偶发损坏。 /// private string ReadMonitorFieldRobust(byte subCmd, int expectLen, int maxRounds = 30) { - string best = string.Empty; - int stableCount = 0; // 连续读到相同最长值的次数 + var candidates = new System.Collections.Generic.List(); + string bestEver = string.Empty; for (int round = 0; round < maxRounds; round++) { string v = DDC_GetMonitorName(subCmd, 300); if (!string.IsNullOrWhiteSpace(v)) { - if (v.Length > best.Length) + // ── AMD 响应头损坏防御 ─────────────────────────────── + // AMD ADL DDC 偶尔返回损坏的响应头(reportedDataLen 被放大), + // 导致字符串尾部夹带垃圾字节。虽然 DDC_GetMonitorName 已做 + // 尾部字符过滤,但仍需在调用方以 expectLen 作为硬上限: + // 任何超过预期最大长度的值一律拒绝,不作为有效候选。 + if (v.Length > expectLen) { - best = v; - stableCount = 0; // 读到更长的,重置稳定计数 - frmISP.WriteLog($"[AMD-Identity] cmd=0x{subCmd:X2} round#{round} got='{v}' (len={v.Length})", "debug"); - } - else if (v.Length == best.Length && v == best) - { - stableCount++; // 连续读到相同的完整值 + frmISP.WriteLog( + $"[AMD-Reject] cmd=0x{subCmd:X2} round#{round} value too long " + + $"({v.Length} > expectLen={expectLen}): '{v}'", + "debug"); + WinIOLib.DelayMs(50); + continue; } + // ── End AMD 响应头损坏防御 ────────────────────────── - // 完整性判断(二选一即退出): - // 1) 读到的长度已接近期望最大长度 → 认为读全 - // 2) 同一个完整值连续稳定读到 2 次 → 认为读全(应对实际长度 < expectLen 的字段) - if (best.Length >= expectLen - 1 || stableCount >= 2) - break; + candidates.Add(v); + frmISP.WriteLog($"[AMD-Identity] cmd=0x{subCmd:X2} round#{round} got='{v}' (len={v.Length})", "debug"); + + if (v.Length > bestEver.Length) + bestEver = v; + + // 提前退出优化: + // 1) 读到接近 expectLen 的值 → 大概率完整 + // 2) 同一值已稳定出现 3 次 → 确信正确 + if (v.Length >= expectLen - 1) + return v; + + int sameCount = 0; + for (int i = candidates.Count - 1; i >= 0 && sameCount < 3; i--) + if (candidates[i] == v) sameCount++; + if (sameCount >= 3) + return v; } WinIOLib.DelayMs(50); } - return best; + + if (candidates.Count == 0) + return bestEver; + + // ── 众数投票:选出现最频繁的长度,再取该长度下出现最频繁的值 ── + int modeLen = candidates.GroupBy(s => s.Length) + .OrderByDescending(g => g.Count()) + .First().Key; + + string modeValue = candidates.Where(s => s.Length == modeLen) + .GroupBy(s => s) + .OrderByDescending(g => g.Count()) + .First().Key; + + if (modeValue.Length < bestEver.Length) + { + frmISP.WriteLog( + $"[AMD-Vote] cmd=0x{subCmd:X2} mode='{modeValue}' (len={modeLen}) < bestEver='{bestEver}' (len={bestEver.Length})", + "debug"); + } + + return modeValue; } private void ClearConnectedMonitorInfo() @@ -1567,7 +1726,7 @@ namespace WinISP // NVIDIA DP AUX can truncate DDC reads, so the fw name may be a prefix of the full name. string fwTrim = (effModel ?? string.Empty).Trim(); string binTrim = (_u8ModelName ?? string.Empty).Trim(); - if (fwTrim.Length < 6 || !binTrim.StartsWith(fwTrim, StringComparison.Ordinal)) + if (fwTrim.Length < 6 || !binTrim.Equals(fwTrim, StringComparison.Ordinal)) { frmISP.WriteLog($"FW:{fwTrim},Bin:{binTrim}","debug"); errMsg = "Model not match."; @@ -1581,7 +1740,7 @@ namespace WinISP // Prefix-match fallback: BIN chip "MST9U6" should pass against legacy effChip "MST9U". string fwChipTrim = (effChip ?? string.Empty).Trim(); string binChipTrim = (_u8ChipName ?? string.Empty).Trim(); - if (!binChipTrim.StartsWith(fwChipTrim, StringComparison.OrdinalIgnoreCase)) + if (!binChipTrim.Equals(fwChipTrim, StringComparison.OrdinalIgnoreCase)) { frmISP.WriteLog($"FW:{fwChipTrim},Bin:{binChipTrim}", "debug"); errMsg = "Chip not match."; @@ -1603,7 +1762,7 @@ namespace WinISP // Same prefix-match fallback as Model: DDC truncation may deliver only a prefix. string fwPanelTrim = (effPanel ?? string.Empty).Trim(); string binPanelTrim = (_u8PanelName ?? string.Empty).Trim(); - if (fwPanelTrim.Length < 6 || !binPanelTrim.StartsWith(fwPanelTrim, StringComparison.Ordinal)) + if (fwPanelTrim.Length < 6 || !binPanelTrim.Equals(fwPanelTrim, StringComparison.Ordinal)) { frmISP.WriteLog($"FW:{fwPanelTrim},Bin:{binPanelTrim}", "debug"); errMsg = "Panel not match.";