SerialPort的DataReceived事件会在一个或多个字节的数据可用时被触发,具体触发的精确时间由OS和驱动来决定,同时接收到数据的时间与在.NET时事件的触发时间会有一个短暂的延迟。
在DataReceived事件中要尽量减少对单线程模型对象的操作,如[STA]的WinForm、Console,因为串口监听线程和UI线程极易造成并发冲突而死锁:
其中IdentifyDevice是在UI主线程中调用。在STA这样的编程模型几乎不会有任何实际的效果。因为Send之后WaitOne,造成UI主线程阻塞等待OnSerialPort_DataReceived中去释放Event。而由于OnSerialPort_DataReceived中要对UI主线程进行操作,所以会阻塞来等待UI主线程释放。这样只有WaitOne的超时之后才会解除UI主线程的阻塞,但这时timeout已经为true了。这时OnSerialPort_DataReceived中会向ListBox中插入一行信息。如果将WaitOne下的ClosePort注释去掉,则连ListBox中都不会插入一行信息,因为打开串口的时候,SerialPort会创建一个监听线程ListenThread,在这个线程中,等待注册的串口中断,当收到中断后,会调用DataReceived事件。调用完成后,继续进入循环等待,直到串口被关闭退出线程。
ClosePort直接导致关闭DataReceived线程,从而没机会操纵UI了。
解决方法就是在OnSerialPort_DataReceived中用BeginInvoke来操纵UI,因为它不会阻塞当前线程。
另外,在大量发送命令给串口设备时可能会使DataReceived无法触发,这时需要在每个命令后加一个停顿。
在DataReceived事件中要尽量减少对单线程模型对象的操作,如[STA]的WinForm、Console,因为串口监听线程和UI线程极易造成并发冲突而死锁:
如:(项目代码摘录)
private void ReceivedData(string line)
{
m_ReceivedTextList.Items.Insert(0,
string.Format(
"[{0}]\t{1} ===> {2}",
DateTime.Now.ToShortTimeString(),
m_commandComboBox.Text.Trim(),
line
)
);
}
void OnSerialPort_DataReceived(object sender, StringEventArgs e)
{
if (this.InvokeRequired)
{//这里不能用Invoke
this.BeginInvoke((MethodInvoker)delegate
{
ReceivedData(e.Message);
});
}
else
{
ReceivedData(e.Message);
}
m_lastReceivedLine = e.Message;
m_autoResetEvent.Set();
}
private bool IdentifyDevice(string id, string portName)
{
if (!m_serialPortHelper.IsOpened)
{
Parameter.PortName = portName;
m_serialPortHelper.SetParameter(Parameter);
m_serialPortHelper.OpenPort();
}
m_serialPortHelper.Send("*IDN?");
var timeout = ! m_autoResetEvent.WaitOne(1000);
//ClosePort();
if (timeout)
{
return false;
}
else
{
return m_lastReceivedLine.StartsWith(id);
}
}
private void OnAutoFindPortButton_Click(object sender, EventArgs e)
{
var oldPort = Parameter.PortName;
bool foundPort = false;
this.Enabled = false;
if (!string.IsNullOrEmpty(DeviceID))
{
if (!IdentifyDevice(DeviceID, Parameter.PortName))
{
//ClosePort();
int i = m_allPortsComboBox.Items.Count - 1;
for (; i >= 0 ; i--)
{
if (m_allPortsComboBox.Items[i].ToString() != oldPort)
{
m_allPortsComboBox.SelectedIndex = i;
if (IdentifyDevice(DeviceID, m_allPortsComboBox.Text))
{
break;
}
//ClosePort();
}
}
foundPort = i >= 0;
}
else
{
foundPort = true;
}
if (foundPort)
{
MessageBox.Show(string.Format("Find port with specified device ID: {0}", DeviceID));
}
else
{
MessageBox.Show(string.Format("Can not find port with specified device ID: {0}", DeviceID));
m_allPortsComboBox.SelectedItem = oldPort;
}
this.Enabled = true;
}
else
{
MessageBox.Show("Can not find port without device ID.");
}
}
其中IdentifyDevice是在UI主线程中调用。在STA这样的编程模型几乎不会有任何实际的效果。因为Send之后WaitOne,造成UI主线程阻塞等待OnSerialPort_DataReceived中去释放Event。而由于OnSerialPort_DataReceived中要对UI主线程进行操作,所以会阻塞来等待UI主线程释放。这样只有WaitOne的超时之后才会解除UI主线程的阻塞,但这时timeout已经为true了。这时OnSerialPort_DataReceived中会向ListBox中插入一行信息。如果将WaitOne下的ClosePort注释去掉,则连ListBox中都不会插入一行信息,因为打开串口的时候,SerialPort会创建一个监听线程ListenThread,在这个线程中,等待注册的串口中断,当收到中断后,会调用DataReceived事件。调用完成后,继续进入循环等待,直到串口被关闭退出线程。
ClosePort直接导致关闭DataReceived线程,从而没机会操纵UI了。
解决方法就是在OnSerialPort_DataReceived中用BeginInvoke来操纵UI,因为它不会阻塞当前线程。
另外,在大量发送命令给串口设备时可能会使DataReceived无法触发,这时需要在每个命令后加一个停顿。