串口通信一般分为四大步: 打开串口 ->配置串口 ->读写串口 ->关闭串口, 还可以在串口上监听读写等事件.
1, 打开和关闭串口
Windows 中串口是作为文件来处理的, 调用 CreateFile()函数可以打开串口, 函数执行成功返回串口句柄, 出错返回 INVALID_HANDLE_VALUE.
- HANDLE WINAPI CreateFile(
- _In_ LPCTSTR lpFileName,// 要打开或创建的文件名
- _In_ DWORD dwDesiredAccess,// 访问类型
- _In_ DWORD dwShareMode,// 共享方式
- _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,// 安全属性
- _In_ DWORD dwCreationDisposition,// 指定要打开的文件已存在或不存在的动作
- _In_ DWORD dwFlagsAndAttributes,// 文件属性和标志
- _In_opt_ HANDLE hTemplateFile// 一个指向模板文件的句柄
- );
lpFileName: 要打开或创建的文件名.
dwDesiredAccess: 访问方式. 0 为设备查询访问方式; GENERIC_READ 为读访问; GENERIC_WRITE 为写访问;
dwShareMode: 共享方式. 0 表示文件不能被共享, 其它打开文件的操作都会失败; FILE_SHARE_READ 表示允许其它读操作; FILE_SHARE_WRITE 表示允许其它写操作; FILE_SHARE_DELETE 表示允许其它删除操作.
lpSecurityAttributes: 安全属性. 一个指向 SECURITY_ATTRIBUTES 结构的指针.
dwCreationDisposition: 创建或打开文件时的动作. OPEN_ALWAYS: 打开文件, 如果文件不存在则创建它; TRUNCATE_EXISTING 打开文件, 且将文件清空(故需要 GENERIC_WRITE 权限), 如果文件不存在则会失败; OPEN_EXISTING 打开 文件, 文件若不存在则会失败; CREATE_ALWAYS 创建文件, 如果文件已存在则清空; CREATE_NEW 创建文件, 如文件存在则会失败;
dwFlagsAndAttributes: 文件标志属性. FILE_ATTRIBUTE_NORMAL 常规属性; FILE_FLAG_OVERLAPPED 异步 I/O 标志, 如果不指定此标志则默认为同步 IO;FILE_ATTRIBUTE_READONLY 文件为只读; FILE_ATTRIBUTE_HIDDEN 文件为隐藏. FILE_FLAG_DELETE_ON_CLOSE 所有文件句柄关闭后文件被删除; 其它标志和属性参考 MSDN.
hTemplateFile: 一个文件的句柄, 且该文件必须是以 GENERIC_READ 访问方式打开的. 如果此参数不是 NULL, 则会使用 hTemplateFile 关联的文件的属性和标志来创建文件. 如果是打开一个现有文件, 则该参数被忽略.
使用 CreateFile()打开串口时需要注意的是: lpFileName 文件名直接写串口号名, 如 "COM1",COM10 及以上的串口名格式应为:"\\\\.\\COM10";dwShareMode 共享方式应为 0, 即串口应为独占方式; dwCreationDisposition 打开时的动作应为 OPEN_EXISTING, 即串口必须存在.
调用 CloseHandle()函数来关闭串口, 函数参数为串口句柄.
BOOL WINAPI CloseHandle(HANDLE hObject);
2, 配置串口
设置超时
在调用 ReadFile()和 WriteFile()读写串口的时候, 如果没有指定异步操作的话, 读写都会一直等待指定大小的数据, 这时候我们可能想要设置一个读写的超时时间. 调用 SetCommTimeouts()可以设置串口读写超时时间, GetCommTimeouts()可以获得当前的超时设置, 一般先利用 GetCommTimeouts 获得当前超时信息到一个 COMMTIMEOUTS 结构, 然后对这个结构自定义, 再调用 SetCommTimeouts()进行设置.
- BOOL GetCommTimeouts(
- _In_ HANDLE hFile,
- _Out_ LPCOMMTIMEOUTS lpCommTimeouts
- );
- BOOL SetCommTimeouts(
- _In_ HANDLE hFile,
- _In_ LPCOMMTIMEOUTS lpCommTimeouts
- );
- typedef struct _COMMTIMEOUTS {
- DWORD ReadIntervalTimeout; /* Maximum time between read chars. */
- DWORD ReadTotalTimeoutMultiplier; /* Multiplier of characters. */
- DWORD ReadTotalTimeoutConstant; /* Constant in milliseconds. */
- DWORD WriteTotalTimeoutMultiplier; /* Multiplier of characters. */
- DWORD WriteTotalTimeoutConstant; /* Constant in milliseconds. */
- } COMMTIMEOUTS,*LPCOMMTIMEOUTS;
ReadIntervalTimeout 为读操作时两个字符间的间隔超时, 如果两个字符之间的间隔超过本限制则读操作立即返回.
ReadTotalTimeoutMultiplier 为读操作在读取每个字符时的超时.
ReadTotalTimeoutConstant 为读操作的固定超时.
WriteTotalTimeoutMultiplier 为写操作在写每个字符时的超时.
WriteTotalTimeoutConstant 为写操作的固定超时.
以上各个成员设为 0 表示未设置对应超时.
超时设置有两种: 间隔超时和总超时, 间隔超时就是 ReadIntervalTimeout, 总超时 = ReadTotalTimeoutConstant + ReadTotalTimeoutMultiplier * 要读写的字符数.
可以看出: 间隔超时和总超时的设置是不相关的, 写操作只支持总超时, 而读操作两种超时均支持.
比如:
ReadTotalTimeoutMultiplier 设为 1000, 其余成员为 0, 如果 ReadFile()想要读取 5 个字符, 则总的超时时间为 1*5=5 秒;
ReadTotalTimeoutConstant 设为 5000, 其余为 0, 则总的超时时间为 5 秒;
ReadTotalTimeoutMultiplier 设为 1000 并且 ReadTotalTimeoutConstant 设为 5000, 其余为 0, 如果 ReadFile()想要读取 5 个字符, 则总的超时间为 1*5+5 =10 秒.
ReadIntervalTimeout 设为 MAXDWORD,ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 都为 0, 则读操作会一次读入缓冲区的内容后立即返回, 不管是否读入了指定字符.
需要注意的是, 用重叠方式读写串口时, SetCommTimeouts()仍然是起作用的, 在这种情况下, 超时规定的是 I/O 操作的完成时间, 而不是 ReadFile 和 WriteFile 的返回时间.
设置发送和接收缓冲区大小
SetupComm()函数用来设置串口的发送 / 接受缓冲区的大小, 如果通信的速率较高, 则应该设置较大的缓冲区.
- BOOL WINAPI SetupComm(
- __in HANDLE hFile,// 串口句柄
- __in DWORD dwInQueue,// 输入缓冲区大小
- __in DWORD dwOutQueue// 输出缓冲区大小
- );
设置串口的配置信息
函数 GetCommState()和 SetCommState()分别用来获得和设置串口的配置信息, 如波特率, 校验方式, 数据位个数, 停止位个数等. 一般也是先调用 GetCommState()获得串口配置信息到一个 DCB 结构中去, 在对这个结构自定义后调用 SetCommState()进行设置.
- BOOL WINAPI GetCommState(
- __in HANDLE hFile,// 串口句柄
- __out LPDCB lpDCB// 保存的串口配置信息
- );
- BOOL WINAPI SetCommState(
- __in HANDLE hFile,// 串口句柄
- __in LPDCB lpDCB// 设置的串口配置信息
- );
DCB 结构中几个比较重要的成员有: BaudRate(波特率),fParity(指定奇偶校验使能),Parity(校验方式),ByteSize(数据位个数),StopBits(停止位个数).
BaudRate 波特率常用的有 CBR_9600,CBR_14400,CBR_19200,CBR_38400,CBR_56000,CBR_57600,CBR_115200, CBR_128000, CBR_256000.
fParity 指定奇偶校验使能, 若此成员为 1, 允许奇偶校验.
Parity 校验方式可以为 0~4, 对应宏为 NOPARITY,ODDPARITY,EVENPARITY,MARKPARITY,SPACEPARITY, 分别表示无校验, 奇校验, 偶校验, 校验置位(标记校验), 校验清零.
ByteSize 数据位个数可以为 5~8 位.
StopBits 停止位可以为 0~2, 对应宏为 ONESTOPBIT,ONE5STOPBITS,TWOSTOPBITS, 分别表示 1 位停止位, 1.5 位停止位, 2 位停止位.
读写串口
清空缓冲
PurgeComm()函数用来停止读写操作, 清空读写缓冲区, 第一次读取串口数据, 写串口数据之前, 串口长时间未使用, 串口出现错误等情况下, 应先清空读或写缓冲区.
BOOL PurgeComm(HANDLE hFile, DWORD dwFlags );
第二个参数 dwFlags 指定串口执行的动作, 可以是以下值的组合:
-PURGE_TXABORT: 停止目前所有的传输工作立即返回不管是否完成传输动作.
-PURGE_RXABORT: 停止目前所有的读取工作立即返回不管是否完成读取动作.
-PURGE_TXCLEAR: 清除发送缓冲区的所有数据.
-PURGE_RXCLEAR: 清除接收缓冲区的所有数据.
如清除串口的所有操作和缓冲: PurgeComm(hComm, PURGE_RXCLEAR|PURGE_TXCLEAR|PURGE_RXABORT|PURGE_TXABORT);
清除错误
ClearCommError()用来清除通信中的错误及获得当前通信状态. 在读写操作之前, 可以调用 ClearCommError 来清除错误和获得缓冲区内数据大小.
- BOOL WINAPI ClearCommError(
- _In_ HANDLE hFile,// 串口句柄
- _Out_opt_ LPDWORD lpErrors,// 返回的错误码
- _Out_opt_ LPCOMSTAT lpStat// 返回的通讯状态
- );
lpErrors 用来保存错误码, 具体对应的什么错误为:
1-CE_BREAK: 检测到中断信号. 意思是说检测到某个字节数据缺少合法的停止位.
2-CE_FRAME: 硬件检测到帧错误.
3-CE_IOE: 通信设备发生输入 / 输出错误.
4-CE_MODE: 设置模式错误, 或是 hFile 值错误.
5-CE_OVERRUN: 溢出错误, 缓冲区容量不足, 数据将丢失.
6-CE_RXOVER: 溢出错误.
7-CE_RXPARITY: 硬件检查到校验位错误.
8-CE_TXFULL: 发送缓冲区已满.
lpStat 为指向_COMSTAT 结构的指针, 保存通讯状态. 一般我们只关心这个结构中的两个成员: cbInQue,cbOutQue, 分别表示输入缓冲区中的字节数, 输出缓冲区中的字节数.
读写串口数据
调用 WriteFile()向串口中写数据, ReadFile()从串口读数据, 函数执行成功返回 TRUE, 失败返回 FALSE.
需要注意的有两点:
如果想要异步读写操作, 则 lpOverlappen 参数不能为 NULL, 而且在 CreateFile()打开文件时应指定 FILE_FLAG_OVERLAPPEN 标记. 在异步读写操作的时候, ReadFile()和 WriteFile()返回 FALSE 时应调用 GetLastError 函数分析返回的结果, 如果是 ERROR_IO_PENDING, 这说明异步 I/O 操作正在进行.
在用 ReadFile()读文件时, 如果想要读取的数据大小比文件内容大, 则只会读取文件大小的数据. 而读串口时, 如果想要读取的数据比缓冲区中数据大, 则 ReadFile()会阻塞, 直到数据到达或者超时.
函数 WriteFileEx()与 ReadFileEx()只能用于异步读写操作, 而且可以设置一个读写完成后自动调用的回调函数, 函数执行成功返回 TRUE, 表示异步 I/O 操作开始, 出错返回 FALSE.
- BOOL WINAPI ReadFile(
- _In_ HANDLE hFile,// 文件句柄
- _Out_ LPVOID lpBuffer,// 指向一个缓冲区, 保存读取的数据
- _In_ DWORD nNumberOfBytesToRead,// 要读取数据的字节数, 如果实际读取的字节数小于这个数的话函数会一直等待直到超时
- _Out_opt_ LPDWORD lpNumberOfBytesRead,// 实际读取的字节数
- _Inout_opt_ LPOVERLAPPED lpOverlapped// 指向一个 OVERLAPPED 结构, 用于异步操作
- );
- BOOL WINAPI WriteFile(
- _In_ HANDLE hFile,// 文件句柄
- _In_ LPCVOID lpBuffer,// 指向一个缓冲区, 包含要写入的数据
- _In_ DWORD nNumberOfBytesToWrite,// 要写入数据的字节数
- _Out_opt_ LPDWORD lpNumberOfBytesWritten,// 实际写入的字节数
- _Inout_opt_ LPOVERLAPPED lpOverlapped// 指向一个 OVERLAPPEN 结构体, 用于异步操作
- );
监听串口事件和异步读写串口
在串口编程中, 可以先设置好串口所关注的事件, 然后启动一个辅助线程来监听该事件是否已经发生, 如果没有发生的话该线程就一直等待, 当事件发生后, 如读缓冲区中收到数据, 该线程可以向主线程窗体发送对应事件消息提示进行读串口处理, 或者在辅助线程中直接进行异步读写串口处理. SetCommMask()函数用来设置串口监听事件, GetCommMask()函数获得通信设备上的事件掩码.
BOOL SetCommMask(HANDLE hFile, DWORD dwEvtMask);
参数 hFile 为串口句柄, dwEvtMask 为要监视的串口事件掩码, 可以有以下位值:
EV_RXCHAR: 输入缓冲区中收到数据
EV_TXEMPTY: 输出缓冲区中的数据已被完全送出
EV_RXFLAG: 使用 SetCommState()函数设置的 DCB 结构中的事件字符已被传入输入缓冲区中
......
串口事件设置好以后可以使用 WaitCommEvent()来判断事件是否已经发生.
- BOOL WINAPI WaitCommEvent(
- _In_ HANDLE hFile,
- _Out_ LPDWORD lpEvtMask,
- _In_ LPOVERLAPPED lpOverlapped
- );
-hFile: 串口句柄
-lpEvtMask: 检测到串口通信事件的话就将其写入该参数中.
-lpOverlapped: 指向一个重叠结构, 如果串口打开时指定了 FILE_FLAG_OVERLAPPED 标志 , 则改参数不能为 NULL, 且重叠结构中 应该包含一个人工重置对象句柄 (通过 CreateEvent() 创建).
如果不是异步读写的话, WaitCommEvent()会一直等待事件的发生, 如果异步读写没有立即完成的话函数会直接返回 FALSE, 调用 GetLastError()会返回 ERROR_IO_PENDING.
目前发现了一个 BUG: 如果 CloseHandle()关闭串口的时候, WaitCommEvent()还在等待事件, 那么程序就会出现卡死现象, 而且在同步读写下很容易发生这种情况. MSDN 上说如果是重叠操作的话再次调用 SetCommMask()改变事件掩码将会使 WaitCommEvent()立即返回, 但我试了下在同步读写情况下这种方法不管用, 不知道重叠操作的情况是否真的管用!
5, 异步读写串口
重叠模型是异步 I/O 方式中一种, 所以可以使用重叠操作来实现异步读写串口. 前面说过, 如果重叠操作不能立即完成, 则 WaitCommEvent()返回 FALSE,GetLastError()会返回 ERROR_IO_PENDING, 表示操作正在后台进行, 在 WaitCommEvent 返回之前, 参数重叠结构中的 hEvent 成员会被设置为无信号状态, 如果当事件发生或错误发生时, 其被设置为有信号状态, 应用程序可以调用 wait functions(WaitForSingleObject,WaitForSingleObjectEx 等)来判断事件对象的状态, 而 WaitCommEvent()的参数 lpEvtMask 会保存具体发生的事件.
有两种方法可以等待或者判断重叠操作是否完成, 一种是使用 WaitForSingleObject()来等待读写函数中 OVERLAPPED 类型的参数的 hEvent 成员: 当调用 ReadFile, WriteFile 函数的时候, 该成员会自动被置为无信号状态; 当重叠操作完成后, 该成员变量会自动被置为有信号状态.
另一种方法是调用 GetOverlappedResult()获得重叠操作的状态, 来判断重叠操作是否完成, 函数原型:
- BOOL WINAPI GetOverlappedResult(
- _In_ HANDLE hFile,// 文件句柄
- _In_ LPOVERLAPPED lpOverlapped,// 指向欲检查的重叠结构
- _Out_ LPDWORD lpNumberOfBytesTransferred,// 返回重叠操作 (读或写) 的字节数
- _In_ BOOL bWait
- );
如果参数 bWait 为 TRUE 则函数会一直等待直到重叠结构中的 hEvent 变成有信号, 即一直等到重叠操作完成; FALSE 为如果检测到 pending 状态则立即返回, 此时函数返回 FALSE,GetLastError()返回值为 ERROR_IO_INCOMPLETE.
下面为一个异步读写串口的示例:
- /****************** 主线程 *********************/
- // 以重叠方式打开串口
- g_hCom = CreateFile(_T("COM7"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
- if (g_hCom == INVALID_HANDLE_VALUE)
- {
- int a = GetLastError();
- CString str;
- str.Format(_T("%d"), a);
- AfxMessageBox(str);
- return false;
- }
- // 设置读超时
- COMMTIMEOUTS timeouts;
- GetCommTimeouts(g_hCom, &timeouts);
- timeouts.ReadIntervalTimeout = 0;
- timeouts.ReadTotalTimeoutMultiplier = 0;
- timeouts.ReadTotalTimeoutConstant = 60000;
- timeouts.WriteTotalTimeoutMultiplier = 0;
- timeouts.WriteTotalTimeoutConstant = 0;
- SetCommTimeouts(g_hCom, &timeouts);
- // 设置读写缓冲区大小
- static const int g_nZhenMax = 32768;
- if (!SetupComm(g_hCom, g_nZhenMax, g_nZhenMax))
- {
- AfxMessageBox(_T("SetupComm() failed"));
- CloseHandle(g_hCom);
- return false;
- }
- // 设置串口配置信息
- DCB dcb;
- if (!GetCommState(g_hCom, &dcb))
- {
- AfxMessageBox(_T("GetCommState() failed"));
- CloseHandle(g_hCom);
- return false;
- }
- int nBaud = 115200;
- dcb.DCBlength = sizeof(DCB);
- dcb.BaudRate = nBaud;// 波特率为 115200
- dcb.Parity = 0;// 校验方式为无校验
- dcb.ByteSize = 8;// 数据位为 8 位
- dcb.StopBits = ONESTOPBIT;// 停止位为 1 位
- if (!SetCommState(g_hCom, &dcb))
- {
- AfxMessageBox(_T("SetCommState() failed"));
- CloseHandle(g_hCom);
- return false;
- }
- // 清空缓冲
- PurgeComm(g_hCom, PURGE_RXCLEAR|PURGE_TXCLEAR);
- // 清除错误
- DWORD dwError;
- COMSTAT cs;
- if (!ClearCommError(g_hCom, &dwError, &cs))
- {
- AfxMessageBox(_T("ClearCommError() failed"));
- CloseHandle(g_hCom);
- return false;
- }
- // 设置串口监听事件
- SetCommMask(g_hCom, EV_RXCHAR);
- HANDLE hThread1 = CreateThread(NULL, 0, ThreadSendMsg, NULL, 0, NULL);
- CloseHandle(hThread1);
- /****************** 辅助线程 ********************/
- DWORD WINAPI ThreadSendMsg(LPVOID lpParameter)
- {
- while(1)
- {
- OVERLAPPED osWait;
- memset(&osWait,0,sizeof(OVERLAPPED));
- osWait.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
- DWORD dwEvtMask;
- if (WaitCommEvent(g_hCom, &dwEvtMask, &osWait))
- {
- if (dwEvtMask & EV_RXCHAR)
- {
- DWORD dwError;
- COMSTAT cs;
- if (!ClearCommError(g_hCom, &dwError, &cs))
- {
- AfxMessageBox(_T("ClearCommError() failed"));
- CloseHandle(g_hCom);
- return false;
- }
- char buf[101] = {0};
- DWORD nLenOut = 0;
- DWORD dwTrans;
- OVERLAPPED osRead;
- memset(&osRead,0,sizeof(OVERLAPPED));
- osRead.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
- BOOL bReadStatus = ReadFile(g_hCom, buf, cs.cbInQue, &nLenOut,&osRead);
- if(!bReadStatus)
- {
- if(GetLastError()==ERROR_IO_PENDING)// 重叠操作正在进行
- {
- //GetOverlappedResult(g_hCom,&osRead2,&dwTrans,true); 判断重叠操作是否完成
- //To do
- }
- }
- else// 操作已完成
- {
- //To do
- }
- }
- }
- else
- {
- if(GetLastError()==ERROR_IO_PENDING)
- {
- WaitForSingleObject(osWait.hEvent, INFINITE);
- if (dwEvtMask & EV_RXCHAR)
- {
- DWORD dwError;
- COMSTAT cs;
- if (!ClearCommError(g_hCom, &dwError, &cs))
- {
- AfxMessageBox(_T("ClearCommError() failed"));
- CloseHandle(g_hCom);
- return false;
- }
- char buf[101] = {0};
- DWORD nLenOut = 0;
- DWORD dwTrans;
- OVERLAPPED osRead;
- memset(&osRead,0,sizeof(OVERLAPPED));
- osRead.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
- BOOL bReadStatus = ReadFile(g_hCom, buf, cs.cbInQue, &nLenOut,&osRead);
- if(!bReadStatus)
- {
- if(GetLastError()==ERROR_IO_PENDING)// 重叠操作正在进行
- {
- //GetOverlappedResult(g_hCom,&osRead2,&dwTrans,true); 判断重叠操作是否完成
- //To do
- }
- }
- else// 操作已完成
- {
- //To do
- }
- }
- }
- }
- }
- return 1;
- }
在异步编程中我发现在读事件发生后, 利用 ClearCommError()获得的缓冲区内数据大小有时会比对方 WriteFile()指定发送的数据大小小, 我猜是因为这个时候数据还没有全部发送到缓冲区内, 这点需要注意.
来源: http://www.bubuko.com/infodetail-2854120.html