最近帮朋友做一款工具,设计到对操作系统串口的操作,虽然这个东西已经是历史产物了,但是还有很多设备再用,索性从网络上找了一些代码最终完成这个小功能。下面资料将介绍串口在打开、关闭、读和写的时候一些注意事项以及参数的配置(代码中有详细注释。)

串口的开关

在串口打开的时候,我们要对串口做一些基础的初始化,比如波特率、数据位、校验位、停止位几个参数,他们分别被声明在 WinBase.h 头文件中。 打开串口的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
bool SerialPortManager::Open(ReceiveDataCallback cb/* = nullptr*/)
{
if (serial_handle_ != NULL)
{
return false;
}

if (cb != nullptr)
{
cb_ = cb;
}

serial_handle_ = CreateFile(com_.c_str(), GENERIC_READ GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL FILE_FLAG_OVERLAPPED, NULL);
if (serial_handle_ == INVALID_HANDLE_VALUE)
{
return false;
}

// 获取旧的 dcb 数据
DCB dcb;
dcb.DCBlength = sizeof(DCB);
if (!GetCommState(serial_handle_, &dcb))
{
Close();
return false;
}

// 修改 dcb 数据然后设置端口属性
// CBR_115200;
dcb.ByteSize = byte_size_;
dcb.BaudRate = baud_rate_;
dcb.StopBits = stop_bits_;
dcb.Parity = parity_;
dcb.fBinary = TRUE;
dcb.fParity = TRUE;
if (!SetCommState(serial_handle_, &dcb))
{
Close();
return false;
}

// 设置读写缓冲区大小
SetupComm(serial_handle_, 1024, 1024);

// 清空数据
PurgeComm(serial_handle_, PURGE_TXABORT PURGE_RXABORT PURGE_TXCLEAR PURGE_RXCLEAR);

// 设置超时 10 秒
COMMTIMEOUTS to;
memset(&to, 0, sizeof(to));
to.ReadIntervalTimeout = 1000;
to.ReadTotalTimeoutMultiplier = 500;
to.ReadTotalTimeoutConstant = 5000; //设定写超时
to.WriteTotalTimeoutMultiplier = 500;
to.WriteTotalTimeoutConstant = 2000;
SetCommTimeouts(serial_handle_, &to);

PostReadThread();

QLOG_APP(L"Serial port device is ready, serial: {0}, baud rate: {1}, byte size: {2}, parity: {3}, stop bits: {4}.")
<< com_ << baud_rate_ << byte_size_ << parity_ << stop_bits_;

return true;
}

其中除了打开串口时传递的参数外,还包含了一些串口处理数据超时、读写缓冲区大小等属性,需要用到的根据自己的环境来配置。 串口的关闭很简单,只需要关闭掉 CreateFile 返回的句柄就可以了,这里不多介绍。

串口读写

串口的读写可以同步也可以异步,但是同步方式会造成一个问题就是当你调用了 ReadFile 在等待串口数据时,再去调用 WriteFile 就会被阻塞,因为 ReadFile 一直没有返回。所以我还是推荐大家用异步方式来读写串口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void SerialPortManager::ReadSerialPortThread()
{
QLOG_APP(L"PostReadThread is running....");
while (TRUE)
{
if (!serial_handle_)
{
QLOG_ERR(L"Failed to read data from serial port, serial port handle is null.");
break;
}

// 计算最小需要读取的数据量
DWORD read_size = 1024;
// read_size = min(read_size, (DWORD)com_stat.cbInQue);

// 开始异步读取
OVERLAPPED over_lapped;
memset(&over_lapped, 0, sizeof(OVERLAPPED));
over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

std::shared_ptr<BYTE> buffer;
buffer.reset(new BYTE[read_size]);
BOOL bReadStatus = ReadFile(serial_handle_, buffer.get(), read_size, &read_size, &over_lapped);
if (!bReadStatus) // 如果 ReadFile 函数返回 FALSE
{
DWORD last_error = GetLastError();
if (last_error == ERROR_IO_PENDING)
{
QLOG_APP(L"Read file return ERROR_IO_PENDING..");
BOOL bRet = GetOverlappedResult(serial_handle_, &over_lapped, &read_size, TRUE);
if (bRet)
{
// 返回 true 代表读取到了数据
QLOG_APP(L"Read data {0}") << nbase::UTF8ToUTF16((char*)buffer.get());
cb_((char*)buffer.get());
PurgeComm(serial_handle_, PURGE_TXABORT PURGE_RXABORT PURGE_TXCLEAR PURGE_RXCLEAR);
}
else
{
// 返回 false 可能是句柄已经被 close 了
QLOG_APP(L"GetOverlappedResult returned false");
}

continue;
}

QLOG_ERR(L"Failed to read data from serial prot, error code = {0}") << last_error;
}
}

QLOG_APP(L"PostReadThread is quit....");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool SerialPortManager::WriteData(const std::string& data)
{
QLOG_APP(L"Begin to write data [{0}] to serial port.") << data;

DWORD bytes_written = data.size() + 1;
OVERLAPPED over_lapped;
memset(&over_lapped, 0, sizeof(OVERLAPPED));
over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
BOOL write_stat = WriteFile(serial_handle_, data.c_str(), bytes_written, &bytes_written, &over_lapped);
if (!write_stat)
{
DWORD last_error = GetLastError();
if (last_error == ERROR_IO_PENDING)
{
WaitForSingleObject(over_lapped.hEvent, 2000);
return true;
}

return false;
}

QLOG_APP(L"Finished to write data.");
return true;
}

读因为是异步操作,我们需要传一个 OVERLAPPED 结构体到 ReadFile 的最后一个参数。写也是一样,我们可以用 GetOverlappedResultWaitForSingleObject 来等待操作事件完成(记得要初始化 OVERLAPPED 否则会报错的)。两种方式读写我都做了演示,可以根据自己的需求改造。 读写操作的时候可以获取当前返回值判断是不是 ERROR_IO_PENDING 来确定是不是有数据还没有读取完成。

总结

串口的读写其实还是相对简单的,上面代码基本上把可能出现问题的点都体现出来了,最后再来罗列一下注意事项。

  • 打开串口时要根据硬件情况初始化串口参数(在 WinBase.h 中有声明)
  • 设置串口的缓冲区和超时
  • 异步去读写串口通过返回值判断是否读写成功
  • 不要忘记初始化 OVERLAPPAD 结构
  • 读取完成后 PurgeComm 串口