重构串口驱动
前言:
最近调试串口获取机芯日志,要做个上位机工具能随时查看机芯的本地日志。起初想参考合泰源码做一份,但是考虑到通用性。如果未来要做其他项目,又得改串口类的代码,很麻烦。所以想整一份自己的专属串口类,方便后续移植使用。
网上参考了很多资料,基本上都是大同小异。打算自己重构一份,使用双线程,实现读写分离,环形数组收发数据。应用层可以向指定的环形数组,随时随地写入不定长度的数据。或者随时随地从指定的环形数组,读取不定长度的数据。
参考资料如下:
- 《MFC图文并茂详解.doc》
- 《SCOM_MFC2.rar》
- 《serial_src.zip》
- 《SerialCom_YiHai_TEST-master.zip》
- 《SerialComSoftware_自写详细注释.rar》
- 《VC_MFC串口通信编程详解.pdf》
- 《VC++串口通信20个经典源码合集.rar》
- 《串口通信处女作.zip》
- 《用MFC实现串口编程.doc》
关键API接口和结构体描述:
CreateFile 函数打开串口。
HANDLE CreateFile(
LPCTSTR lpFileName, // 指向文件名的指针
DWORD dwDesiredAccess, // 访问模式(写 / 读)
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性的指针
DWORD dwCreationDisposition, // 如何创建
DWORD dwFlagsAndAttributes, // 文件属性
HANDLE hTemplateFile // 用于复制文件句柄
);
COMMTIMEOUTS 结构体,用来设置串口读写的超时时间
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; //读---间隔超时 ms 两字符之间的 间隔超时
DWORD ReadTotalTimeoutMultiplier; //读---时间系数 ms ReadTotalTimeoutMultiplier * 字节数 = 总超时
DWORD ReadTotalTimeoutConstant; //读---时间常量 ms 一次读取串口数据的固定超时
DWORD WriteTotalTimeoutMultiplier; //写---时间系数 ms WriteTotalTimeoutMultiplier * 字节数 = 总超时
DWORD WriteTotalTimeoutConstant; //写---时间常量 ms 一次写入串口数据的固定超时
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
DCB 结构体有28个参数,一般用来配置一些串口的主要参数
(1)DWORD BaudRate; 波特率设置。
(2)BYTE ByteSize; 数据位设置。
(3)DWORD fParity: 1; TRUE时, 支持奇偶检验。
(4)BYTE Parity; 奇偶检验位的设置
(5)BYTE StopBits; 停止位的设置
StopBits指定端口当前使用的停止位数,可取值:
取值 意义
ONESTOPBIT 1停止位
ONE5STOPBITS 1.5停止位
TWOSTOPBITS 2停止位
参考资料: https://blog.csdn.net/weixin_42639646/article/details/120319833
ReadFile 函数从串口读取数据
BOOL ReadFile(
HANDLE hFile, //文件的句柄
LPVOID lpBuffer, //用于保存读入数据的一个缓冲区
DWORD nNumberOfBytesToRead, //要读入的字节数
LPDWORD lpNumberOfBytesRead, //指向实际读取字节数的指针
LPOVERLAPPED lpOverlapped //如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
);
WriteFile 函数写数据到串口
BOOL WriteFile(
HANDLE hFile, //文件句柄
LPCVOID lpBuffer, //数据缓存区指针
DWORD nNumberOfBytesToWrite, //你要写的字节数
LPDWORD lpNumberOfBytesWritten, //用于保存实际写入字节数的存储区域的指针
LPOVERLAPPED lpOverlapped //如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
);
CloseHandle 函数用来关闭串口或者线程
BOOL CloseHandle(HANDLE hObject);
参数
hObject :代表一个已打开对象handle。
返回值
TRUE:执行成功;
FALSE:执行失败,可以调用GetLastError()获知失败原因。
CloseHandel(ThreadHandle ); 只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对应的线程做任何干预了。并没有结束线程。
CloseHandel(CreateThread(NULL,0,.....)); 在线程创建后马上调用CloseHandle()是个良好的做法,这里不会影响线程的执行
参考资料: https://blog.csdn.net/imJaron/article/details/78812726
AfxBeginThread 函数用来创建线程,一般填前两个参数即可
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc, //线程函数地址
LPVOID pParam, //线程参数
int nPriority = THREAD_PRIORITY_NORMAL, //线程优先级
UINT nStackSize = 0, //线程堆栈大小,默认为1M
DWORD dwCreateFlags = 0, //线程创建标志
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //指向安全描述符的指针
);
ResumeThread 函数用来重启线程
SuspendThread 函数用来挂起线程
OVERLAPPED 结构体,主要是用来做异步调用的时候,事件通知对象。
typedef struct _OVERLAPPED {
DWORD Internal; //预留给操做系统使用
DWORD InternalHigh; //预留给操做系统使用
DWORD Offset; //该文件的位置是从文件起始处的字节偏移量。
DWORD OffsetHigh; //指定文件传送的字节偏移量的高位字
HANDLE hEvent; //在转移完成时处理一个事件设置为有信号状态
} OVERLAPPED
参考资料:
https://blog.csdn.net/xbmoxia/article/details/22306317?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-22306317-blog-120159802.pc_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3
COMSTAT 结构体,主要是用来串口通信的状态,一般用的比较多的是 cbInQue 变量
typedef struct _COMSTAT { // cst
DWORD fCtsHold : 1; // Tx waiting for CTS signal
DWORD fDsrHold : 1; // Tx waiting for DSR signal
DWORD fRlsdHold : 1; // Tx waiting for RLSD signal
DWORD fXoffHold : 1; // Tx waiting, XOFF char rec''d
DWORD fXoffSent : 1; // Tx waiting, XOFF char sent
DWORD fEof : 1; // EOF character sent
DWORD fTxim : 1; // character waiting for Tx
DWORD fReserved : 25; // reserved 保留
DWORD cbInQue; //该成员变量的值表明输入缓冲区的字节数
DWORD cbOutQue; //记录着输出缓冲区中字节数
} COMSTAT, *LPCOMSTAT;
ClearCommError 函数用来清除硬件的通讯错误,以及获取通讯设备的当前状态
BOOL ClearCommError(
HANDLE hFile, //由CreateFile函数返回指向已打开串行口的句柄
LPDWORD lpErrors, //指向定义了错误类型的32位变量
LPCOMSTAT lpStat //指向一个返回设备状态的控制块COMSTAT
);
WaitCommEvent 函数主要是用来监听串口中断,比如设置掩码 EV_RXCHAR ,则代表串口只要收到任何字符就会触发事件。
BOOL WaitComrnEvent(
HANDLE hFi1e, //通信设备的句柄
LPWORD lpEvtMask,//发生的事件变量的地址
LPOVERLAPFED lpoverlapped //Overlapped结构的地址
)
GetLastError 函数主要是在操作出错后,查询错误结果。比如读写操作失败的时候,调用改函数,发现错误是 ERROR_IO_PENDING 则表明串口当前正在执行接收或者发送数据
DWORD GetLastError(VOID);
GetOverlappedResult 函数主要是用来读取重叠操作结果
BOOL GetOverlappedResult(
HANDLE hFile, // 串口的句柄
LPOVERLAPPED lpOverlapped, // 指向重叠操做开始时指定的OVERLAPPED结构
LPDWORD lpNumberOfBytesTransferred, // 指向一个32位变量,该变量的值返回实际读写操做传输的字节数。
BOOL bWait // 该参数用于指定函数是否一直等到重叠操做结束。
// 若是该参数为TRUE,函数直到操做结束才返回。
// 若是该参数为FALSE,函数直接返回,这时若是操做没有完成,
// 经过调用GetLastError()函数会返回ERROR_IO_INCOMPLETE。
);
EnterCriticalSection 和 LeaveCriticalSection 函数主要是用来做资源保护,避免多线程操作的时候,资源被破坏,保证资源的完整性。
WaitForSingleObject 函数主要用来等待事件发生,避免CPU资源被无效浪费在查询上面。
DWORD WaitForSingleObject(
HANDLE hHandle, // 串口的句柄
DWORD dwMilliseconds // 等待时间,单位 ms,永久等待使用 INFINITE
);
SetEvent 函数用来设置事件
ResetEvent 函数用来清除事件
PurgeComm() 函数--清空缓冲区
BOOL PurgeComm(HANDLE hFile,DWORD dwFlags )
HANDLE hFile //串口句柄
DWORD dwFlags // 需要完成的操作
};
参数dwFlags指定要完成的操作,可以是下列值的组合:
PURGE_TXABORT 终止所有正在进行的 字符输出操作,完成一个正处于等待状态的重叠 i/o操作,他将产生一个事件,指明完成了写操作
PURGE_RXABORT 终止所有正在进行的字符输入操作,完成一个正在进行中的重叠i/o操作,并带有已设置得适当事件
PURGE_TXCLEAR 这个命令指导设备驱动程序清除输出缓冲区,经常与PURGE_TXABORT 命令标志一起使用
PURGE_RXCLEAR 这个命令用于设备驱动程序清除输入缓冲区,经常与PURGE_RXABORT 命令标志一起使用
代码调试部分:
同步操作(阻塞)
先定义串口类 CSerialPort
再声明串口类 CSerialPort
类的初始化操作,主要是打开串口,创建读写线程
写线程操作。主要是判断头尾指针是否同步,以启动发送数据。这里需要将类里面的头尾指针(port->m_nWriteHead 和 port->m_nWriteTail),转换成线程里面的局部变量(m_nWriteHead 和 m_nWriteTail)来操作。避免在读取数据的时候,由于其他线程操作了该变量导致数据错误。
封装各种输出函数
EnterCriticalSection(&m_csWriteSync); //进入临界区
LeaveCriticalSection(&m_csWriteSync); //退出临界区
主要是防止多线程同时调用发送函数的时候,破坏数据的完整性
实测每次都能正常输出内容。而且中间是没有任何停顿,连续发送不同的数据,直接叠加在输出内容后面。
读线程操作。每间隔5ms固定获取一次接收缓存状态,然后根据接收的缓存取数据,再转存到port->m_szReadBuffer给其他线程使用。这里需要将类里面的头尾指针(port->m_nReadHead 和 port->m_nReadTail),转换成线程里面的局部变量(m_nReadHead 和 m_nReadTail)来操作。避免在读取数据的时候,由于其他线程操作了该变量导致数据错误。
实测数据无论什么时候写入,都会保存在m_szReadBuffer里面,供其他线程使用。
异步操作(重叠)
CreateFile 函数打开串口。
HANDLE CreateFile(
LPCTSTR lpFileName, // 指向文件名的指针
DWORD dwDesiredAccess, // 访问模式(写 / 读)
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性的指针
DWORD dwCreationDisposition, // 如何创建
DWORD dwFlagsAndAttributes, // 文件属性
HANDLE hTemplateFile // 用于复制文件句柄
);
首先打开串口的时候,需要制定文件属性为重叠操作
发送数据的线程,就不在是间隔5ms去轮询是否需要发送数据,而是根据事件触发。
发送数据失败后,通过 GetOverlappedResult函数等待重叠操作完成。
接收数据的时候,一直查询缓存是否有数据,若是读取失败了,通过GetOverlappedResult函数等待重叠操作完成。
实测数据都能接收到指定缓存,供其他线程使用。
资料下载地址:
Hankin
2022.06.19