最近有客户报告:当插入 HDMI 从外接的显示器显示时,有一定概率出现显示正常但是 HDMI Audio 失效的情况。这个问题的概率不高,但是对客户影响严重。为了复制现象,需要不断插拔HDMI来验证,经常是插拔50次之后抓到一次现象,随之而来的是再来50次,更可怕的是据说某些修改之后会出现插拔100次才会出现一次现象。这对于我这样只愿意动脑不愿动手的人来说是一个极大的考验。
这种情况下,必须考虑用机器来实现自动化的测试。思路是淘宝上买一个带有遥控器的HDMI切换器,通过 Arduino 模拟遥控的操作来实现切换到 HDMI 上然后进行检测。于是,选择了下面这货14元+8元的运费。这个切换器有三个输入口,可以对应三台HDMI输出设备;有一个输出口,可以连接一台显示器

接下来要解决的问题如下:
1. 如何进行遥控?实现遥控之后如何保证切换成功(红外线遥控容易受到光线的影响)。
2. 如何检测当前是Audio 是否正常。
经过研究,第一个问题可以使用下面这个模块来解决:
红外解码模块 编码模块 红外无线通信 接收发射串口通信模块

因为这是发射和接收一体的模块,刚好可以用来解码HDMI切换器发射器的数据,解码之后发射同样的内容就能实现遥控的操作。遥控板上有3个按键,分别对应HDMI 的三个端口。经过实验遥控板按键发送数据如下:
HDMI IN1: 0xF1 0x80 0x7F 0x02 0xA1
HDMI IN2: 0xA1 0xF1 0x80 0x7F 0x04
HDMI IN3: 0xA1 0xF1 0x80 0x7F 0x06
接下来还有一个问题,如何保证切换正常。比如,当前是 HDMI1 输出,我切花发送切换HDMI2命令之后需要确认切换是否成功(红外遥控受到环境影响较大)。经过研究,可以使用切换器上的LED来判断,每一个输入端口都有一个对应的LED用来表示当前选中的输入端口。只需要读取LED电平即可得知。此外,我们再使用额外的LED来标记当前的输入 HDMI Port。
最终,硬件连接如下:
const int LED1=14;
const int LED2=16;
const int LED3=10;
void setup()
{
pinMode(LED1,OUTPUT);
pinMode(LED2,OUTPUT);
pinMode(LED3,OUTPUT);
digitalWrite(LED1,LOW);
digitalWrite(LED2,LOW);
digitalWrite(LED3,LOW);
pinMode(A1,INPUT_PULLUP);
pinMode(A2,INPUT_PULLUP);
pinMode(A3,INPUT_PULLUP);
Serial.begin(9600);
Serial1.begin(9600);
}
void loop()
{
char c=' ';
int retry;
while (Serial.available() > 0)
{
c=Serial.read();
if (c=='1') { //Command for Switch to HDMI1
retry=3;
while (retry>0) {
Serial1.write(0xF1);
Serial1.write(0x80);
Serial1.write(0x7F);
Serial1.write(0x02);
Serial1.write(0xA1);
delay(200);
if (analogRead(A1)>20) {retry--;}
else {retry=0;}
Serial.println("Go1");
}
}
if (c=='2') {//Command for Switch to HDMI2
retry=3;
while (retry>0) {
Serial1.write(0xA1);
Serial1.write(0xF1);
Serial1.write(0x80);
Serial1.write(0x7F);
Serial1.write(0x04);
delay(200);
if (analogRead(A2)>20) {retry--;}
else {retry=0;}
Serial.println("Go2");
}
}
if (c=='3') {//Command for Switch to HDMI3
retry=3;
while (retry>0) {
Serial1.write(0xA1);
Serial1.write(0xF1);
Serial1.write(0x80);
Serial1.write(0x7F);
Serial1.write(0x06);
delay(200);
if (analogRead(A3)>20) {retry--;}
else {retry=0;}
Serial.println("Go3");
}
} //if (c=='3')
}
if (analogRead(A1)<20) {digitalWrite(LED1,HIGH);}
else {digitalWrite(LED1,LOW);}
if (analogRead(A2)<20) {digitalWrite(LED2,HIGH);}
else {digitalWrite(LED2,LOW);}
if (analogRead(A3)<20) {digitalWrite(LED3,HIGH);}
else {digitalWrite(LED3,LOW);}
}
为了配合这个设备,我们需要编写上位机的代码, 上位机是运行在被测试机器上。主要的逻辑就是:发送切换命令切换到其他HDMI Port,之后再发送命令切换回来,然后检查音频是否正常,如果不正常就停止。如果正常就继续运行。软件界面如下:

代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Management;
using System.Runtime.InteropServices;
namespace WindowsFormsApplication8
{
public partial class Form1 : Form
{ //当前切换到 HDMI[x]
int current=1;
//切换次数
int loops = 0;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//如果当前测试还没有开始,那么开始测试,否则停止
if (button1.Text == "Start") { button1.Text = "Stop"; }
else { button1.Text = "Start"; timer1.Enabled = false; return; }
//检查选中的 HDMI 数量
int counter = 0;
for (int i= 0; i < 3; i++) {
if (checkedListBox1.GetItemChecked(i)) { counter++; }
}
//如果只有1个HDMI 被选中,提示出错
if (counter < 2) { System.Windows.Forms.MessageBox.Show("Please choose at least 2 HDMI"); return; }
if (serialPort1.IsOpen) { serialPort1.Close(); }
//打开选中的 COMx
serialPort1.PortName = comboBox1.SelectedItem.ToString();
serialPort1.Open();
textBox1.AppendText("Open " + comboBox1.SelectedItem.ToString()+"\n");
//1秒之内开始测试
timer1.Interval = 1000;
timer1.Enabled = true;
}
private void Form1_Load(object sender, EventArgs e)
{
//设置可以选择的串口号
comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
comboBox1.SelectedIndex = 0;
for (int i = 0; i < checkedListBox1.Items.Count; i++) {
checkedListBox1.SetItemChecked(i,true);
}
//输出当前的串口设备名称
ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_PnPEntity");
foreach (ManagementObject queryObj in searcher.Get())
{
if (queryObj["Caption"] != null)
if (queryObj["Caption"].ToString().Contains("(COM"))
{
//Console.WriteLine(queryObj["Caption"]);
textBox1.AppendText(queryObj["Caption"].ToString()+"\n");
}
}
searcher.Dispose();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (serialPort1.IsOpen) {
textBox1.AppendText("close " + comboBox1.SelectedItem.ToString());
serialPort1.Close();
};
}
// native calls
[DllImport("winmm.dll")]
public static extern int waveOutGetNumDevs();
private void timer1_Tick(object sender, EventArgs e)
{
//如果当前是 HDMI2
if (current == 2) {
//检查设备是否有 Audio
int inum = waveOutGetNumDevs();
if (inum == int.Parse(maskedTextBox1.Text)) //发现错误
{
textBox1.AppendText("Audio failed");
timer1.Enabled = false;
return;
}
}
//20秒后触发下一次切换
if (timer1.Interval == 1000) { timer1.Interval = 20000; }
//尝试下一个HDMI
current = (current + 1) % 3+1;
//检查下一个是否被选中
while (checkedListBox1.GetItemChecked(current-1)==false) {
current = (current + 1) % 3+1;
}
if (current == 2) { loops++; }
//切换到下一个可用的HDMI设备
textBox1.AppendText("Switch to " + current.ToString()+"\n");
textBox1.AppendText("Loops " + loops.ToString() + "\n");
serialPort1.Write(current.ToString());
}
private void label1_Click(object sender, EventArgs e)
{
}
}
}

