修复N卡DDC通道限制导致不能识别超过15字节,导致可能漏判问题

This commit is contained in:
chenjiangqun
2026-07-01 18:58:36 +08:00
parent fc09f64873
commit 65a3117d10
2 changed files with 220 additions and 30 deletions

View File

@@ -1,4 +1,6 @@
using System; using System;
using System.Linq;
using WinISP;
namespace MTK namespace MTK
{ {
@@ -72,6 +74,12 @@ namespace MTK
strData = ""; strData = "";
if (_i2cCtrl.ReadData(recvBuf, wrCmds) == true) 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) for (int i = 0; i < recvBuf.Length; ++i)
strData += recvBuf[i].ToString("X2") + " "; strData += recvBuf[i].ToString("X2") + " ";
@@ -102,6 +110,29 @@ namespace MTK
return null; return null;
} }
/// <summary>
/// 从 DDC 0x6F 继续读取数据(不重新发送命令)。
/// 用于 N卡 DP AUX 通道 16 字节限制导致截断时的分段续读。
/// 在 DDC_Read 成功返回后立即调用,尝试读取响应缓冲区中尚未消费的剩余字节。
/// </summary>
/// <param name="recvLen">要读取的字节数</param>
/// <returns>读取到的字节数组,失败返回 null</returns>
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) private static Byte CalCheckSum(Byte[] data, int startIdx = 0)
{ {
Byte chkSum = 0; Byte chkSum = 0;

View File

@@ -275,7 +275,7 @@ namespace WinISP
if (display) MessageBox.Show(msg, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); if (display) MessageBox.Show(msg, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
} }
/// <summary> /// <summary>
/// Send a 2-byte DDC command and parse the ASCII string returned by the monitor firmware. /// Send a 2-byte DDC command and parse the ASCII string returned by the monitor firmware.
/// recv layout: [hdr0][hdr1][char0][char1]...[\0] /// recv layout: [hdr0][hdr1][char0][char1]...[\0]
/// </summary> /// </summary>
@@ -283,11 +283,15 @@ namespace WinISP
{ {
Byte[] cmd = { 0xCC, subCmd }; Byte[] cmd = { 0xCC, subCmd };
int[] readLens = { 36, 32, 24, 20, 16, 12, 8 }; 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) 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++) for (int attempt = 0; attempt < 3; attempt++)
{ {
Byte[] recv = MTKDebugCmd.DDC_Read(cmd, readLen, delayTime); Byte[] recv = MTKDebugCmd.DDC_Read(cmd, readLen, delayTime);
@@ -297,16 +301,22 @@ namespace WinISP
continue; continue;
} }
// Error response pattern in logs: 6E 80 BE ... // Error response: 6E 80 BE ...
if (recv[1] == 0x80) if (recv[1] == 0x80)
{ {
WinIOLib.DelayMs(20); WinIOLib.DelayMs(20);
continue; continue;
} }
int len = 0; int reportedDataLen = recv[1] & 0x7F; // DDC响应头中声明的数据长度
while ((2 + len) < recv.Length && recv[2 + len] != 0)
len++; // 先按原始方式扫描整个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) if (len <= 0)
{ {
@@ -315,28 +325,137 @@ namespace WinISP
} }
string value = System.Text.Encoding.ASCII.GetString(recv, 2, len).Trim(); string value = System.Text.Encoding.ASCII.GetString(recv, 2, len).Trim();
value = TrimNonStandardDdcSuffix(value);
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
WinIOLib.DelayMs(20); WinIOLib.DelayMs(20);
continue; continue;
} }
// Only return when the null terminator was found inside the buffer (complete string). // nullTerminatorFound: 基于原始扫描长度判断rawLen 可能超过 reportedDataLen
bool isComplete = (2 + len) < recv.Length; // 因为扫过了 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) if (isComplete)
return value; 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] 同时匹配才算监视器重置了指针。
// 单独一个 0x6EASCII '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) if (value.Length > bestPartial.Length)
{
bestPartial = value; bestPartial = value;
frmISP.WriteLog(
$"[DDC] cmd=0x{subCmd:X2} readLen={readLen} attempt={attempt} " +
$"partial='{value}' (len={len}, nullFound={nullTerminatorFound})",
"debug");
}
WinIOLib.DelayMs(20); 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; return bestPartial;
} }
/// <summary>
/// 滤除 DDC 字符串尾部的非标准字符。
/// AMD ADL DDC 偶尔返回损坏的响应头reportedDataLen 被放大),
/// 导致字符串尾部夹带垃圾字节(如 checksum + 下一字节)。
/// 监视器字段名合法字符集:[A-Za-z0-9 _.\\-/]
/// </summary>
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) private bool TryReadMonitorIdentity(int retryCount = 5, int delayMs = 80)
{ {
// Name commands are more stable in non-tool mode on some GPU/DDC paths. // Name commands are more stable in non-tool mode on some GPU/DDC paths.
@@ -538,37 +657,77 @@ namespace WinISP
/// <summary> /// <summary>
/// AMD/Intel 友好的型号读取:带响应有效性校验 + 多轮重试。 /// AMD/Intel 友好的型号读取:带响应有效性校验 + 多轮重试。
/// AMD ADL DDC 读命中率低(约 4 错 1 对),需要足够重试次数才能读到完整字符串。 /// AMD ADL DDC 读命中率低(约 4 错 1 对),需要足够重试次数才能读到完整字符串。
/// 采用"众数策略"而非"取最长"AMD 偶尔返回损坏的响应头会拉长字符串,
/// 统计上正确的值出现频率最高,长度众数 + 值众数投票能过滤掉偶发损坏。
/// </summary> /// </summary>
private string ReadMonitorFieldRobust(byte subCmd, int expectLen, int maxRounds = 30) private string ReadMonitorFieldRobust(byte subCmd, int expectLen, int maxRounds = 30)
{ {
string best = string.Empty; var candidates = new System.Collections.Generic.List<string>();
int stableCount = 0; // 连续读到相同最长值的次数 string bestEver = string.Empty;
for (int round = 0; round < maxRounds; round++) for (int round = 0; round < maxRounds; round++)
{ {
string v = DDC_GetMonitorName(subCmd, 300); string v = DDC_GetMonitorName(subCmd, 300);
if (!string.IsNullOrWhiteSpace(v)) if (!string.IsNullOrWhiteSpace(v))
{ {
if (v.Length > best.Length) // ── AMD 响应头损坏防御 ───────────────────────────────
// AMD ADL DDC 偶尔返回损坏的响应头reportedDataLen 被放大),
// 导致字符串尾部夹带垃圾字节。虽然 DDC_GetMonitorName 已做
// 尾部字符过滤,但仍需在调用方以 expectLen 作为硬上限:
// 任何超过预期最大长度的值一律拒绝,不作为有效候选。
if (v.Length > expectLen)
{ {
best = v; frmISP.WriteLog(
stableCount = 0; // 读到更长的,重置稳定计数 $"[AMD-Reject] cmd=0x{subCmd:X2} round#{round} value too long " +
frmISP.WriteLog($"[AMD-Identity] cmd=0x{subCmd:X2} round#{round} got='{v}' (len={v.Length})", "debug"); $"({v.Length} > expectLen={expectLen}): '{v}'",
} "debug");
else if (v.Length == best.Length && v == best) WinIOLib.DelayMs(50);
{ continue;
stableCount++; // 连续读到相同的完整值
} }
// ── End AMD 响应头损坏防御 ──────────────────────────
// 完整性判断(二选一即退出): candidates.Add(v);
// 1) 读到的长度已接近期望最大长度 → 认为读全 frmISP.WriteLog($"[AMD-Identity] cmd=0x{subCmd:X2} round#{round} got='{v}' (len={v.Length})", "debug");
// 2) 同一个完整值连续稳定读到 2 次 → 认为读全(应对实际长度 < expectLen 的字段)
if (best.Length >= expectLen - 1 || stableCount >= 2) if (v.Length > bestEver.Length)
break; 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); 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() 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. // 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 fwTrim = (effModel ?? string.Empty).Trim();
string binTrim = (_u8ModelName ?? 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"); frmISP.WriteLog($"FW:{fwTrim},Bin:{binTrim}","debug");
errMsg = "Model not match."; errMsg = "Model not match.";
@@ -1581,7 +1740,7 @@ namespace WinISP
// Prefix-match fallback: BIN chip "MST9U6" should pass against legacy effChip "MST9U". // Prefix-match fallback: BIN chip "MST9U6" should pass against legacy effChip "MST9U".
string fwChipTrim = (effChip ?? string.Empty).Trim(); string fwChipTrim = (effChip ?? string.Empty).Trim();
string binChipTrim = (_u8ChipName ?? 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"); frmISP.WriteLog($"FW:{fwChipTrim},Bin:{binChipTrim}", "debug");
errMsg = "Chip not match."; errMsg = "Chip not match.";
@@ -1603,7 +1762,7 @@ namespace WinISP
// Same prefix-match fallback as Model: DDC truncation may deliver only a prefix. // Same prefix-match fallback as Model: DDC truncation may deliver only a prefix.
string fwPanelTrim = (effPanel ?? string.Empty).Trim(); string fwPanelTrim = (effPanel ?? string.Empty).Trim();
string binPanelTrim = (_u8PanelName ?? 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"); frmISP.WriteLog($"FW:{fwPanelTrim},Bin:{binPanelTrim}", "debug");
errMsg = "Panel not match."; errMsg = "Panel not match.";