Reputation: 23759
I'm having some problems with serial ports in a cross-platform application (with Linux embedded and actual embedded targets), which also works on Windows to make development easier. This is about the Windows implementation.
The implementation of the serial protocol is, therefore, targetted at a mixture of OS- and non-OS systems and I won't touch the implementation itself. I'd like to make it compatible with the existing implementation. If that fails within reasonable time, I'll just make a separate thread for serial reading.
OK, basically the implementation opens the serial port, registers the file descriptor in our IO system (which uses epoll
on Linux and WaitForMultipleObjects
on Windows) and then, basically, just waits for all handles and does whatever required. So we want to read from the serial port when the handle is signaled for reading. Unfortunately on Windows, you can't specify if you're waiting for read or write, so I thought I'd use the following solution:
CreateFile
with FILE_FLAG_OVERLAPPED
SetCommMask
with EV_RXCHAR
OVERLAPPED
structure with a manual reset eventWaitCommEvent
with said OVERLAPPED
structure, which usually returns ERROR_IO_PENDING
That's the basic setup. I register the event handle instead of the file handle to wait on. When the handle is signalled, I do the following:
ReadFile
ResetEvent
and call WaitCommEvent
againIt seems, however, that if you specify FILE_FLAG_OVERLAPPED
, you must use overlapped IO also for reading and writing. So I thought that whenever ReadFile
or WriteFile
return ERROR_IO_PENDING
, I'll just wait for the IO with WaitForSingleObject
and GetOverlappedResult
. It seems that I don't get into that though. It seems to work basically, but sometimes it crashes on one of the ResetEvent
calls, as if the overlapped was still active (though I guess it still shouldn't crash).
So, the actual question. Can this be done as I want it? Is there a problem with the approach in general, or should it work? Or is using yet another thread the only good solution? The communication is already in a separate thread, so it would be at least three threads then.
I'll try to post as much code as needed, though it is reduced from the actual code which contains a lot of things not directly related to serial reading.
SerialPort::SerialPort(const std::string &filename)
{
fd = INVALID_HANDLE_VALUE;
m_ov = new OVERLAPPED(); // Pointer because header shouldn't include Windows.h.
memset(m_ov, 0, sizeof(OVERLAPPED));
m_waitHandle = m_ov->hEvent = CreateEvent(0, true, 0, 0);
}
SerialPort::~SerialPort(void)
{
Close();
CloseHandle(m_ov->hEvent);
delete m_ov;
}
The constructor is called in a separate thread, which later calls Open:
bool SerialPort::Open(void)
{
if (fd != INVALID_HANDLE_VALUE)
return true;
fd = CreateFile(filename.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (fd != INVALID_HANDLE_VALUE) {
DCB dcb;
ZeroMemory(&dcb, sizeof(DCB));
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = TimeOut();
timeouts.ReadTotalTimeoutConstant = TimeOut();
timeouts.ReadTotalTimeoutMultiplier = TimeOut() / 5;
if (timeouts.ReadTotalTimeoutMultiplier == 0) {
timeouts.ReadTotalTimeoutMultiplier = 1;
}
if (!SetCommTimeouts(fd, &timeouts)) {
DebugBreak();
}
SetCommMask(fd, EV_RXCHAR);
InitWait();
return true;
}
return false;
}
void SerialPort::InitWait()
{
if (WaitForSingleObject(m_ov->hEvent, 0) == WAIT_OBJECT_0) {
return; // Still signaled
}
DWORD dwEventMask;
if (!WaitCommEvent(fd, &dwEventMask, m_ov)) {
// For testing, I have some prints here for the different cases.
}
}
Via a rather long chain, the thread then calls WaitForMultipleObjects
on m_waitHandle
, which is the same as the hEvent
member of the OVERLAPPED
structure. This is done in a loop, and there are several other handles in the list, that's why this is different from the typical solution where you have a thread exclusively reading from the serial port. I have, basically, no control about the loop, that's why I try to do the WaitCommEvent
(within InitWait
) at just the right time.
When the handle is signaled, the ReadData method is called by the thread:
int SerialPort::ReadData(void *buffer, int size)
{
if (fd != INVALID_HANDLE_VALUE) {
// Timeouts are reset here to MAXDWORD/0/0, not sure if necessary.
DWORD dwBytesRead;
OVERLAPPED ovRead = {0};
ovRead.hEvent = CreateEvent(0, true, 0, 0);
if (ReadFile(fd, buffer, size, &dwBytesRead, &ovRead)) {
if (WaitForSingleObject(m_ov->hEvent, 0) == WAIT_OBJECT_0) {
// Only reset if signaled, because we might get here because of a timer.
ResetEvent(m_waitHandle);
InitWait();
}
CloseHandle(ovRead.hEvent);
return dwBytesRead;
} else {
if (GetLastError() == ERROR_IO_PENDING) {
WaitForSingleObject(ovRead.hEvent, INFINITE);
GetOverlappedResult(fd, &ovRead, &dwBytesRead, true);
InitWait();
CloseHandle(ovRead.hEvent);
return dwBytesRead;
}
}
InitWait();
CloseHandle(ovRead.hEvent);
return -1;
} else {
return 0;
}
}
The write is done as follows, without syncing:
int SerialPort::WriteData(const void *buffer, int size)
{
if (fd != INVALID_HANDLE_VALUE) {
DWORD dwBytesWritten;
OVERLAPPED ovWrite = {0};
ovWrite.hEvent = CreateEvent(0, true, 0, 0);
if (!WriteFile(fd, buffer, size, &dwBytesWritten, &ovWrite)) {
if (GetLastError() == ERROR_IO_PENDING) {
WaitForSingleObject(ovWrite.hEvent, INFINITE);
GetOverlappedResult(fd, &ovWrite, &dwBytesWritten, true);
CloseHandle(ovWrite.hEvent);
return dwBytesWritten;
} else {
CloseHandle(ovWrite.hEvent);
return -1;
}
}
CloseHandle(ovWrite.hEvent);
}
return 0;
}
It seems that it does work now. There are no crashes anymore, at least I can't reproduce them. So as it works now, I'm just asking if what I do is sane, or if I should do things differently.
Upvotes: 1
Views: 4135
Reputation: 597166
Offhand, I don't see any errors in the code you have shown, but I would like to suggest alternative code to clean up your error handling in ReadData()
and WriteData()
in general:
int SerialPort::ReadData(void *buffer, int size)
{
if (fd == INVALID_HANDLE_VALUE)
return 0;
OVERLAPPED ovRead = {0};
ovRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!ovRead.hEvent)
return -1;
DWORD dwBytesRead;
if (!ReadFile(fd, buffer, size, &dwBytesRead, &ovRead))
{
if (GetLastError() != ERROR_IO_PENDING)
{
CloseHandle(ovRead.hEvent);
return -1;
}
if (!GetOverlappedResult(fd, &ovRead, &dwBytesRead, TRUE))
{
CloseHandle(ovRead.hEvent);
return -1;
}
}
if (WaitForSingleObject(m_waitHandle, 0) == WAIT_OBJECT_0)
{
ResetEvent(m_waitHandle);
InitWait();
}
CloseHandle(ovRead.hEvent);
return dwBytesRead;
}
int SerialPort::WriteData(const void *buffer, int size)
{
if (fd == INVALID_HANDLE_VALUE)
return 0;
OVERLAPPED ovWrite = {0};
ovWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!ovWrite.hEvent)
return -1;
DWORD dwBytesWritten;
if (!WriteFile(fd, buffer, size, &dwBytesWritten, &ovWrite))
{
if (GetLastError() != ERROR_IO_PENDING)
{
CloseHandle(ovWrite.hEvent);
return -1;
}
if (!GetOverlappedResult(fd, &ovWrite, &dwBytesWritten, TRUE))
{
CloseHandle(ovWrite.hEvent);
return -1;
}
}
CloseHandle(ovWrite.hEvent);
return dwBytesWritten;
}
Upvotes: 1