Files
HW-Debug-Tools/DLPilot/src/hdmi_source.ps1
T

488 lines
19 KiB
PowerShell

<#
.SYNOPSIS
HDMI 源端测试工具 —— 枚举/切换输出分辨率 + 全屏测试图案
.DESCRIPTION
配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。
原理:电脑显卡通过 HDMI DDC 读取转接板 EDID 来枚举可用分辨率,
本脚本调用 Win32 API 完成模式枚举与切换,再用 WinForms 全屏窗口
输出测试图案。板载 EDID 只声明了 640x360@60(25.2MHz 像素时钟),
插上 HDMI 后显卡应只能发这一个模式;-List 可直接验证 EDID 是否生效。
测试图案用途:
Bars 100% 彩条 —— 肉眼/示波器验证 RGB 通道顺序与极性
GraySteps 16 级灰阶 —— 验证 RGB888 各 bit 连通性
Gradient R/G/B 三段渐变 —— 验证每通道 8bit 线性度
Checker 黑白棋盘格 —— 验证像素时钟 PCLK 与 DE 对位
Grid 网格+中心十字 —— 验证像素映射/偏移/缩放
Red/Green/Blue/White/Black/Gray50 纯色 —— 万用表/示波器测单通道电平
全屏窗口操作:空格或→下一图案,←上一图案,ESC 退出。
.EXAMPLE
tools\hdmi_source.bat -List
枚举所有活动显示器与显卡支持的分辨率(验证 EDID)
.EXAMPLE
tools\hdmi_source.bat -Monitor 1 -Mode 640x360
把显示器 1 切到 640x360(缺省 60Hz,可写 640x360@60)
.EXAMPLE
tools\hdmi_source.bat -Mode 640x360 -Pattern -Restore
切到 640x360 并全屏出图案,退出图案后恢复原分辨率
.EXAMPLE
tools\hdmi_source.bat -Pattern -PatternName Checker -Seconds 5
当前分辨率下全屏棋盘格,5 秒后自动退出(自动化测试用)
#>
param(
[switch]$List,
[int]$Monitor = 1,
[string]$Mode = '',
[switch]$Pattern,
[ValidateSet('', 'Bars', 'GraySteps', 'Gradient', 'Checker', 'Grid',
'Red', 'Green', 'Blue', 'White', 'Black', 'Gray50')]
[string]$PatternName = '',
[int]$Seconds = 0,
[switch]$Restore
)
$ErrorActionPreference = 'Stop'
# ---------------- Win32 显示 API ----------------
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
namespace HdmiSrc
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DISPLAY_DEVICE
{
public int cb;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceString;
public int StateFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceID;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DEVMODE
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public int dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
public static class Native
{
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
public static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum,
ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);
// PowerShell 会把 $null marshal 成空串而非 NULL,由 C# 侧保证传 NULL
public static bool EnumDisplayAdapters(uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice)
{
return EnumDisplayDevices(null, iDevNum, ref lpDisplayDevice, 0);
}
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
public static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum,
ref DEVMODE lpDevMode);
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
public static extern int ChangeDisplaySettingsEx(string lpszDeviceName,
ref DEVMODE lpDevMode, IntPtr hwnd, int dwflags, IntPtr lParam);
}
}
"@
# 板载 EDID 声明的目标模式,-List 时用于自检
$TargetW = 640
$TargetH = 360
function New-DevMode {
$dm = New-Object HdmiSrc.DEVMODE
$dm.dmDeviceName = ''
$dm.dmFormName = ''
$dm.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($dm)
return $dm
}
function New-DisplayDevice {
$dd = New-Object HdmiSrc.DISPLAY_DEVICE
$dd.DeviceName = ''
$dd.DeviceString = ''
$dd.DeviceID = ''
$dd.DeviceKey = ''
$dd.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($dd)
return $dd
}
# 枚举所有"已连接到桌面"的显示器(即插着 HDMI 的输出口)
function Get-Displays {
$list = @()
$i = 0
while ($true) {
$dd = New-DisplayDevice
if (-not [HdmiSrc.Native]::EnumDisplayAdapters($i, [ref]$dd)) { break }
# StateFlags bit0 = ATTACHED_TO_DESKTOP
if ($dd.StateFlags -band 0x1) {
$mon = New-DisplayDevice
[void][HdmiSrc.Native]::EnumDisplayDevices($dd.DeviceName, 0, [ref]$mon, 0)
$list += [pscustomobject]@{
Index = $list.Count + 1
Device = $dd.DeviceName
Adapter = $dd.DeviceString
MonName = $mon.DeviceString
}
}
$i++
}
return $list
}
# 显卡枚举到的全部显示模式(去重)
function Get-Modes {
param([string]$Device)
$modes = @()
$i = 0
while ($true) {
$dm = New-DevMode
if (-not [HdmiSrc.Native]::EnumDisplaySettings($Device, $i, [ref]$dm)) { break }
$modes += [pscustomobject]@{
W = $dm.dmPelsWidth
H = $dm.dmPelsHeight
Hz = $dm.dmDisplayFrequency
Bpp = $dm.dmBitsPerPel
}
$i++
}
return ($modes | Sort-Object W, H, Hz, Bpp -Unique)
}
function Get-CurrentDevMode {
param([string]$Device)
$dm = New-DevMode
[void][HdmiSrc.Native]::EnumDisplaySettings($Device, -1, [ref]$dm) # ENUM_CURRENT_SETTINGS
return $dm
}
# 应用一个 DEVMODE(先 CDS_TEST 再真正生效),返回 $true/$false
function Set-DevMode {
param([string]$Device, $dm)
$dm.dmFields = 0x00040000 -bor 0x00080000 -bor 0x00100000 -bor 0x00400000
# DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY
$rc = [HdmiSrc.Native]::ChangeDisplaySettingsEx($Device, [ref]$dm, [IntPtr]::Zero, 2, [IntPtr]::Zero)
if ($rc -ne 0) {
Write-Host (" 错误: 显卡拒绝该模式 (CDS_TEST 返回 {0})" -f $rc) -ForegroundColor Red
return $false
}
$rc = [HdmiSrc.Native]::ChangeDisplaySettingsEx($Device, [ref]$dm, [IntPtr]::Zero, 0, [IntPtr]::Zero)
if ($rc -ne 0) {
Write-Host (" 错误: 模式应用失败 (返回 {0})" -f $rc) -ForegroundColor Red
return $false
}
return $true
}
# 在显卡枚举的模式里找 W x H [@Hz] 并切换
function Switch-Mode {
param([string]$Device, [int]$W, [int]$H, [int]$Hz)
$all = @(Get-Modes $Device)
$hit = @($all | Where-Object { $_.W -eq $W -and $_.H -eq $H -and ($Hz -le 0 -or $_.Hz -eq $Hz) })
if (-not $hit) {
Write-Host (" 错误: 显卡未枚举到 {0}x{1} 模式" -f $W, $H) -ForegroundColor Red
Write-Host " 可能原因:" -ForegroundColor Yellow
Write-Host " 1. 转接板 EDID 未被读取(检查 HPD / DDC / LT8619C 供电)"
Write-Host " 2. EDID 中未包含该分辨率(当前板载 EDID 只有 640x360@60)"
Write-Host " 3. 显卡驱动拒收该 DTD(可用 CRU 手动添加自定义分辨率)"
Write-Host " 先运行 -List 查看显卡实际枚举到了哪些模式。"
return $false
}
$dm = New-DevMode
$dm.dmPelsWidth = $W
$dm.dmPelsHeight = $H
$dm.dmDisplayFrequency = $hit[0].Hz
$dm.dmBitsPerPel = 32
$ok = Set-DevMode $Device $dm
if ($ok) {
Write-Host (" 已切换 {0} -> {1}x{2}@{3}" -f $Device, $W, $H, $hit[0].Hz) -ForegroundColor Green
}
return $ok
}
# ---------------- 全屏测试图案 ----------------
function Show-PatternWindow {
param(
[string]$Device = '',
[string]$StartPattern = '',
[int]$AutoSeconds = 0
)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# 窗口放到目标设备名对应的屏幕上(如 \\.\DISPLAY2),找不到则回退主屏
$screen = [System.Windows.Forms.Screen]::AllScreens |
Where-Object { $_.DeviceName -eq $Device } |
Select-Object -First 1
if (-not $screen) { $screen = [System.Windows.Forms.Screen]::PrimaryScreen }
$script:patterns = @('Bars', 'GraySteps', 'Gradient', 'Checker', 'Grid',
'Red', 'Green', 'Blue', 'White', 'Black', 'Gray50')
$script:pi = 0
if ($StartPattern) {
$idx = [array]::IndexOf($script:patterns, $StartPattern)
if ($idx -ge 0) { $script:pi = $idx }
}
$script:form = New-Object System.Windows.Forms.Form
$script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
$script:form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
$script:form.Bounds = $screen.Bounds
$script:form.TopMost = $true
$script:form.KeyPreview = $true
# 双缓冲避免棋盘格/网格闪烁
$script:form.GetType().GetProperty('DoubleBuffered',
[System.Reflection.BindingFlags]'Instance,NonPublic').SetValue($script:form, $true, $null)
$osdFont = New-Object System.Drawing.Font('Consolas', 11)
$paintHandler = {
param($s, $e)
$g = $e.Graphics
$w = $e.ClipRectangle.Width
$h = $e.ClipRectangle.Height
$name = $script:patterns[$script:pi]
switch ($name) {
'Bars' {
# 100% 饱和度竖彩条:白黄青绿品红红蓝黑
$c = @(@(255,255,255), @(255,255,0), @(0,255,255), @(0,255,0),
@(255,0,255), @(255,0,0), @(0,0,255), @(0,0,0))
$n = $c.Count
for ($k = 0; $k -lt $n; $k++) {
$bw = [Math]::Ceiling($w / $n)
$brush = New-Object System.Drawing.SolidBrush (
[System.Drawing.Color]::FromArgb($c[$k][0], $c[$k][1], $c[$k][2]))
$g.FillRectangle($brush, [int]($k * $bw), 0, [int]$bw + 1, $h)
$brush.Dispose()
}
}
'GraySteps' {
# 16 级灰阶,验证 RGB888 各 bit
$n = 16
for ($k = 0; $k -lt $n; $k++) {
$v = [int]($k * 255 / ($n - 1))
$bw = [Math]::Ceiling($w / $n)
$brush = New-Object System.Drawing.SolidBrush (
[System.Drawing.Color]::FromArgb($v, $v, $v))
$g.FillRectangle($brush, [int]($k * $bw), 0, [int]$bw + 1, $h)
$brush.Dispose()
}
}
'Gradient' {
# R/G/B 三段水平渐变,验证每通道 8bit 线性度
$seg = [Math]::Ceiling($w / 3.0)
$rgbIdx = @(0, 1, 2)
foreach ($ch in $rgbIdx) {
for ($x = 0; $x -lt $seg; $x++) {
$v = [int](255 * $x / [Math]::Max(1.0, ($seg - 1)))
$r = 0; $gg = 0; $b = 0
if ($ch -eq 0) { $r = $v } elseif ($ch -eq 1) { $gg = $v } else { $b = $v }
$brush = New-Object System.Drawing.SolidBrush (
[System.Drawing.Color]::FromArgb($r, $gg, $b))
$g.FillRectangle($brush, [int]($ch * $seg + $x), 0, 1, $h)
$brush.Dispose()
}
}
}
'Checker' {
# 黑白棋盘格,验证 PCLK/DE 对位
$g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h)
$cell = 16
for ($y = 0; $y -lt $h; $y += $cell) {
for ($x = 0; $x -lt $w; $x += $cell) {
if ((([int]($x / $cell) + [int]($y / $cell)) % 2) -eq 0) {
$g.FillRectangle([System.Drawing.Brushes]::White, $x, $y, $cell, $cell)
}
}
}
}
'Grid' {
# 黑底 + 40px 网格 + 中心十字 + 白色边框,验证像素映射
$g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h)
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(0, 128, 0), 1)
for ($x = 40; $x -lt $w; $x += 40) { $g.DrawLine($pen, $x, 0, $x, $h) }
for ($y = 40; $y -lt $h; $y += 40) { $g.DrawLine($pen, 0, $y, $w, $y) }
$pen.Dispose()
$wp = New-Object System.Drawing.Pen ([System.Drawing.Color]::White, 2)
$cx = [int]($w / 2); $cy = [int]($h / 2)
$g.DrawLine($wp, $cx, 0, $cx, $h)
$g.DrawLine($wp, 0, $cy, $w, $cy)
$g.DrawRectangle($wp, 1, 1, $w - 3, $h - 3)
$wp.Dispose()
}
'Red' { $g.FillRectangle([System.Drawing.Brushes]::Red, 0, 0, $w, $h) }
'Green' { $g.FillRectangle([System.Drawing.Brushes]::Green, 0, 0, $w, $h) }
'Blue' { $g.FillRectangle([System.Drawing.Brushes]::Blue, 0, 0, $w, $h) }
'White' { $g.FillRectangle([System.Drawing.Brushes]::White, 0, 0, $w, $h) }
'Black' { $g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h) }
'Gray50' {
$brush = New-Object System.Drawing.SolidBrush (
[System.Drawing.Color]::FromArgb(128, 128, 128))
$g.FillRectangle($brush, 0, 0, $w, $h)
$brush.Dispose()
}
}
# 左上角 OSD:图案名 + 分辨率 + 按键提示
$text = "[{0}] {1}x{2} 空格/→:下一图案 ←:上一图案 ESC:退出" -f $name, $w, $h
$sz = $g.MeasureString($text, $osdFont)
$bg = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(150, 0, 0, 0))
$g.FillRectangle($bg, 6, 6, $sz.Width + 12, $sz.Height + 8)
$bg.Dispose()
$g.DrawString($text, $osdFont, [System.Drawing.Brushes]::White, 12, 10)
}
$script:form.Add_Paint($paintHandler)
$script:form.Add_KeyDown({
param($s, $e)
switch ($e.KeyCode) {
'Escape' { $s.Close() }
'Space' { $script:pi = ($script:pi + 1) % $script:patterns.Count; $s.Invalidate() }
'Right' { $script:pi = ($script:pi + 1) % $script:patterns.Count; $s.Invalidate() }
'Left' { $script:pi = ($script:pi - 1 + $script:patterns.Count) % $script:patterns.Count; $s.Invalidate() }
}
})
$script:form.Add_Activated({ [System.Windows.Forms.Cursor]::Hide() })
$script:form.Add_FormClosed({ [System.Windows.Forms.Cursor]::Show() })
if ($AutoSeconds -gt 0) {
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = $AutoSeconds * 1000
$timer.Add_Tick({ $script:form.Close() })
$timer.Start()
}
Write-Host ("全屏图案已启动于 {0} ({1}x{2})" -f $screen.DeviceName, $screen.Bounds.Width, $screen.Bounds.Height)
Write-Host "空格/→:下一图案 ←:上一图案 ESC:退出"
[void]$script:form.ShowDialog()
}
function Show-Usage {
Write-Host @"
用法:
hdmi_source.bat -List 枚举显示器与可用分辨率(验证 EDID)
hdmi_source.bat -Monitor <n> -Mode <WxH[@Hz]> 切换分辨率, 如 -Mode 640x360 或 640x360@60
hdmi_source.bat [-Monitor <n>] -Pattern [-PatternName <名>] [-Seconds <n>]
全屏测试图案(Bar/GraySteps/Gradient/Checker/Grid/纯色)
可选: -Restore 退出图案窗口后恢复切换前的分辨率
-Seconds <n> 图案 n 秒后自动退出(自动化测试)
典型调试流程:
1. HDMI 插上转接板 -> -List 确认出现 640x360(EDID 生效)
2. -Mode 640x360 -Pattern -> 全屏图案, 逐项验证 RGB 通道/位序/像素映射
"@
}
# ---------------- 主流程 ----------------
$displays = @(Get-Displays) # @() 保证单显示器时不退化为标量
if ($displays.Count -eq 0) {
Write-Host "错误: 未找到活动显示器" -ForegroundColor Red
exit 1
}
if ($List) {
foreach ($d in $displays) {
Write-Host ""
Write-Host ("== 显示器 {0} {1} 显卡[{2}] 监视器[{3}]" -f $d.Index, $d.Device, $d.Adapter, $d.MonName) -ForegroundColor Cyan
$cur = Get-CurrentDevMode $d.Device
Write-Host (" 当前模式: {0}x{1}@{2} {3}bpp" -f $cur.dmPelsWidth, $cur.dmPelsHeight, $cur.dmDisplayFrequency, $cur.dmBitsPerPel)
$modes = Get-Modes $d.Device
$modes | Format-Table W, H, Hz, Bpp -AutoSize
$hasTarget = $modes | Where-Object { $_.W -eq $TargetW -and $_.H -eq $TargetH }
if ($hasTarget) {
Write-Host (" [OK] 找到板载 EDID 目标模式 {0}x{1} -> EDID 已被显卡读取" -f $TargetW, $TargetH) -ForegroundColor Green
} else {
Write-Host (" [--] 未找到 {0}x{1} (若此口接的是转接板, 说明 EDID/HPD 未生效)" -f $TargetW, $TargetH) -ForegroundColor Yellow
}
}
exit 0
}
if (-not $Mode -and -not $Pattern) {
Show-Usage
exit 0
}
if ($Monitor -gt $displays.Count) {
Write-Host ("错误: -Monitor 超出范围(共 {0} 个活动显示器, 运行 -List 查看)" -f $displays.Count) -ForegroundColor Red
exit 1
}
$disp = $displays[$Monitor - 1]
Write-Host ("目标: 显示器 {0} = {1}" -f $disp.Index, $disp.Device)
$prevMode = $null
if ($Mode) {
if ($Mode -notmatch '^\s*(\d+)\s*[xX]\s*(\d+)\s*(?:@\s*(\d+)\s*)?$') {
Write-Host "错误: -Mode 格式应为 WxH 或 WxH@Hz, 例如 640x360 或 640x360@60" -ForegroundColor Red
exit 1
}
$w = [int]$Matches[1]; $h = [int]$Matches[2]
$hz = 0
if ($Matches[3]) { $hz = [int]$Matches[3] }
$prevMode = Get-CurrentDevMode $disp.Device
$ok = Switch-Mode $disp.Device $w $h $hz
if (-not $ok) { exit 1 }
Start-Sleep -Milliseconds 300 # 等显卡/显示器完成模式切换
}
if ($Pattern) {
if ($Restore -and -not $prevMode) {
Write-Host "提示: -Restore 仅在与 -Mode 同用时生效" -ForegroundColor Yellow
}
try {
Show-PatternWindow -Device $disp.Device -StartPattern $PatternName -AutoSeconds $Seconds
}
finally {
if ($Restore -and $prevMode) {
Write-Host ("恢复原分辨率 {0}x{1}@{2} ..." -f $prevMode.dmPelsWidth, $prevMode.dmPelsHeight, $prevMode.dmDisplayFrequency)
[void](Set-DevMode $disp.Device $prevMode)
}
}
}