C++编写串口通信程序

C++ 专栏收录该内容
1 篇文章 0 订阅

声明:本博客的内容主要是本人学习其他串口通信博客之后的总结,主要参考的博客地址如下:

https://blog.csdn.net/wlk1229/article/details/52566701

http://wangbaiyuan.cn/c-serial-communication-write-reading.html#title-0

https://blog.csdn.net/superyang198608/article/details/54233200

https://blog.csdn.net/targusyoona/article/details/9624203

https://blog.csdn.net/wangshubo1989/article/details/47746617

串口通信一般分为同步和异步两种方式,本博客主要讲述异步通信程序的编写,其编程步骤主要分为四步骤:

一、打开串口 

Win32系统把文件的概念进行了扩展。无论是文件、通信设备、命名管道、邮件槽、磁盘、还是控制台,都是用API函数CreateFile来打开或创建的。本程序串口类中打开串口的函数定义如下:

bool My_Com::Open_Com(LPCTSTR  Port)
{
    hCom = CreateFile(
        Port, //将要打开的串口逻辑名
        GENERIC_READ | GENERIC_WRITE, //允许读和写
        0, //指定共享属性,由于串口不能共享,该参数必须置为0,独占方式
        NULL,//引用安全性属性结构,缺省值为NULL
        OPEN_EXISTING, /创建标志,对串口操作该参数必须置为OPEN_EXISTING
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //属性描述,此处指定该串口异步
        NULL //对串口而言该参数必须置为NULL);
    if (hCom == INVALID_HANDLE_VALUE)
    {
        printf ("打开串口失败!\n");
        return FALSE;
    }
    else
    {
        printf("打开串口成功!\n");
    }
    return TRUE;
}

注意:FILE_ATTRIBUTE_NORMAL 和 FILE_FLAG_OVERLAPPED 均代表异步通信。也可以借用CreateFile函数加上for循环实现对外设驱动的扫描以自动找出插入的串口号,其代码实现如下:

GetCom()
{
	MyComm.hCom = INVALID_HANDLE_VALUE;
	BOOL ret = false;
	int j = 0;
	//逐一进行COM   端口检测
	printf("正在搜寻当前可用端口...\n");
	for (int i = 1; i <= 10; i++)
	{
		string comname = "COM" + to_string(i);
		MyComm.hCom = CreateFile(stringToLPCWSTR(comname), // 打开串口
			GENERIC_READ | GENERIC_WRITE,  //读写方式
			0, //不能共享
			NULL, //安全属性,一般不用设为NULL
			OPEN_EXISTING,   //打开已存在的设备
			FILE_ATTRIBUTE_NORMAL,  //普通文件属性
			NULL);       //无模板
		if (MyComm.hCom != INVALID_HANDLE_VALUE)
		{
			j++;
			printf("端口 COM %d 可用\n", i);
			ret = CloseHandle(MyComm.hCom);
			if (!ret) printf("关闭串口失败!!");
		}
	}
	if (!j)
	{
		printf("无可用端口!!\n");
		return FALSE;
	}
	else return TRUE;
}

二、配置串口

本部分主要是一些初始化配置:

1、DCB结构相关参数的配置(波特率、数据位数、奇偶校验和停止位数等信息),调用GetCommState函数获取串口的初始配置,然后先修改DCB结构,再调用SetCommState函数设置串口。

2、COMMTIMEOUTS结构串口读写超时参数设置(单位:毫秒;若设置为0,表示该参数不起作用)
ReadIntervalTimeout:两字符之间最大的延时,当读取串口数据时,一旦两个字符传输的时间差超过该时间,读取函数将返回现有的数据。在ReadFile操作期间,时间周期从第一个字符接收到算起。如果收到的两个字符之间的间隔超过该值,ReadFile操作完毕并返回所有缓冲数据。如果值为MAXDWORD,并且ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier两个值都为0,   则指定读操作携带已经收到的字符立即返回,即使没有收到任何字符。如果ReadIntervalTimeout为0,则该值不起作用。
ReadTotalTimeoutMultiplier:读取每字符间的超时。 指定累积值,用于计算读操作时的超时总数。对于每次读操作,该值与所要读的字节数相乘。
ReadTotalTimeoutConstant:一次读取串口数据的固定超时。在一次读取串口的操作中,其超时为 ReadTotalTimeoutMultiplier乘以读取的字节数再加上 ReadTotalTimeoutConstant。将ReadIntervalTimeout设置为MAXDWORD,并将 ReadTotalTimeoutMultiplier 和ReadTotalTimeoutConstant设置为0,表示读取操作将立即返回存放在输入缓冲区的字符。
WriteTotalTimeoutMultiplier:写入每字符间的超时。
WriteTotalTimeoutConstant:一次写入串口数据的固定超时。在一次写入串口的操作中,其超时为WriteTotalTimeoutMultiplier乘以写入的字节数再加上 WriteTotalTimeoutConstant。
间隔超时=ReadIntervalTimeout
总超时   = ReadTotalTimeoutMultiplier  *  字节数   +   ReadTotalTimeoutConstant 
           这里以串口读取事件为例进行详细的分析,其过程分两个阶段:
           第一个阶段是:串口执行到ReadFile函数时,串口还没有开始传输数据,所以串口缓冲区的第一个字节是没有装数据的,这时候总超时起作用,如果在总超时时间内没有进行串口数据的传输,ReadFile函数就返回,当然 没有读取到任何数据。而且,间隔超时并没有起作用。
          第二阶段:假设总超时为20秒,程序运行到ReadFile,总超时开始从0 计时,如果在计时到达10秒时,串口开始了数据的传输,那么从接收的第一个字节开始,间隔超时就开始计时,假如间隔超时为1ms,那么在读取完第一个字节后,串口开始等待1ms,如果1ms之内接收到了第二个字节,就读取第二个字节,间隔超时重置为0并计时,等待第三个字节的到来,如果第三个字节到来的时间超过了1ms,那么ReadFile函数立即返回,这时候总超时计时是没到20秒的。如果在20秒总计时时间结束之前,所有的数据都遵守数据间隔为1ms的约定并陆陆续续的到达串口缓冲区,那么就成功进行了一次串口传输和读取;如果20秒总计时时间到,串口还陆陆续续的有数据到达,即使遵守字节间隔为1ms的约定,ReadFile函数也会立即返回,这时候总超时就起作用了。
          总结起来,总超时在两种情况下起作用:一是串口没进行数据传输,等待总超时时间那么长ReadFile才返回(即非正常数据传输);二是数据太长,总超时设置太短,数据还没读取完就返回了(读取的数据是不全的)。间隔超时触发的条件:在总超时时间内且串口进行了数据的传输。

3、利用SetUpComm、PurgeComm两个函数分别设置输出输出缓冲区的大小并进行清空处理
SetupComm参数解释:dwInQueue指定输入缓冲区的大小(字节);dwOutQueue指定输出缓冲区的大小(字节)。
PurgeComm参数解释:PURGE_TXABORT 终止所有正在进行的字符输出操作,PURGE_RXABORT 终止所有正在进行的字符。输入操作PURGE_TXCLEAR 设备驱动程序清除输出缓冲,PURGE_RXCLEAR 设备驱动程序清除输入缓冲区。

4、利用CreateEvent函数创建读写及等待的操作事件,用于读写函数中做判断使用。

5、利用SetCommMask函数设置要监控的事件,而WaitCommEvent函数是等待串口通信事件的发生放在读函数中。

6、创建读取线程。

bool My_Com::Config_Com()
{
	SetupComm(hCom, 1024, 1024); //输入缓冲区和输出缓冲区的大小都是1024
	
        DCB dcb;
	GetCommState(hCom, &dcb);
	dcb.BaudRate = 9600; //波特率为9600
	dcb.ByteSize = 8; //每个字节有8位
	dcb.Parity = NOPARITY; //无奇偶校验位
	dcb.StopBits = TWOSTOPBITS; //两个停止位
	SetCommState(hCom, &dcb);
	
        COMMTIMEOUTS TimeOuts; //设定读超时
	TimeOuts.ReadIntervalTimeout = MAXDWORD;
	TimeOuts.ReadTotalTimeoutMultiplier = 0;
	TimeOuts.ReadTotalTimeoutConstant = 0; //设定写超时
	TimeOuts.WriteTotalTimeoutMultiplier = 500;
	TimeOuts.WriteTotalTimeoutConstant = 2000;
	SetCommTimeouts(hCom, &TimeOuts); //设置超时

	PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);

	m_ovRead.hEvent = CreateEvent(NULL, false, false, NULL);
	m_ovWrite.hEvent = CreateEvent(NULL, false, false, NULL);
	m_ovWait.hEvent = CreateEvent(NULL, false, false, NULL);

	//SetCommMask设置要监控的事件 
	//EV_RXCHAR:输入缓冲区中已收到数据,即接收到一个字节并放入输入缓冲区。
	//EV_ERR:线路状态错误,包括了CE_FRAME / CE_OVERRUN / CE_RXPARITY 3种错误。
	SetCommMask(hCom, EV_ERR | EV_RXCHAR);

	//_beginThreadex创建读取线程  
	m_Thread = (HANDLE)_beginthreadex(NULL, 0, &My_Com::ComRecv, this, 0, NULL);
	m_IsOpen = true;

	return TRUE;
}

三、串口读写

写函数ComWrite(发送数据)中主要调用了WriteFile函数参数:HANDLE hFile文件句柄,LPCVOID lpBuffer数据缓存区指针DWORD  nNumberOfBytesToWrite你要写的字节数,LPDWORD lpNumberOfBytesWritten用于保存实际写入字节数的存储区域的指针LPOVERLAPPED lpOverlappedOVERLAPPED结构体指针

bool My_Com::ComWrite(LPBYTE buf, int &len)
{
	BOOL rtn = FALSE;
	DWORD WriteSize = 0;   //DWORD 代表 unsigned long
	PurgeComm(hCom, PURGE_TXCLEAR | PURGE_TXABORT);
	m_ovWait.Offset = 0;
	rtn = WriteFile(hCom, buf, len, &WriteSize, &m_ovWrite);
	if (FALSE == rtn && GetLastError() == ERROR_IO_PENDING)//后台读取
	{
		//等待数据写入完成
		printf("已发送 :");
		for (int i = 0; i < len; i++)
			printf("%d ", buf[i]);
		printf("\n");
	}
	return rtn != FALSE;
}

读函数ComRecv(接收数据)的解释已在代码区详细备注,其函数定义如下:

unsigned int __stdcall My_Com::ComRecv(void* LPParam)
{
	My_Com *obj = static_cast<My_Com*>(LPParam);
	DWORD WaitEvent = 0, Bytes = 0;
	BOOL Status = FALSE;
	BYTE ReadBuf[4096];
	DWORD Error;
	COMSTAT cs = { 0 };
	int i;
	while (obj->m_IsOpen)
	{
		WaitEvent = 0;
		obj->m_ovWait.Offset = 0;
		Status = WaitCommEvent(obj->hCom, &WaitEvent, &obj->m_ovWait);
		/*
		WaitCommEvent等待串口通信事件的发生
		用途:用来判断用SetCommMask()函数设置的串口通信事件是否已发生。
		原型:BOOL WaitCommEvent(HANDLE hFile,LPDWORD lpEvtMask,LPOVERLAPPED lpOverlapped);
		参数说明:
		-hFile:串口句柄
		-lpEvtMask:函数执行完后如果检测到串口通信事件的话就将其写入该参数中。
		-lpOverlapped:异步结构,用来保存异步操作结果。
		*/

		//GetLastError()函数返回ERROR_IO_PENDING,表明串口正在进行读操作
		if (FALSE == Status && GetLastError() == ERROR_IO_PENDING)
		{
// GetOverlappedResult函数的最后一个参数设为TRUE,函数会一直等待,直到读操作完成或由于错误而返回。
			Status = GetOverlappedResult(obj->hCom, &obj->m_ovWait, &Bytes, TRUE);
		}
		//在使用ReadFile 函数进行读操作前,应先使用ClearCommError函数清除错误。
		ClearCommError(obj->hCom, &Error, &cs);
		if (TRUE == Status //等待事件成功
			&& WaitEvent&EV_RXCHAR//缓存中有数据到达
			&& cs.cbInQue > 0)//有数据		
		{
			Bytes = 0;
			obj->m_ovRead.Offset = 0;
			memset(ReadBuf, 0, sizeof(ReadBuf));
			/*
			BOOL ReadFile( 
			HANDLE hFile, //串口的句柄
			LPVOID lpBuffer,// 读入的数据存储的地址,即读入的数据将存储在以该指针的值为首地址的一片内存区
			DWORD nNumberOfBytesToRead,// 要读入的数据的字节数
			LPDWORD lpNumberOfBytesRead,// 指向一个DWORD数值,该数值返回读操作实际读入的字节数
			LPOVERLAPPED lpOverlapped // 重叠操作时,该参数指向一个OVERLAPPED结构,同步操作时,该参数为NULL
			);
			*/
			Status = ReadFile(obj->hCom, &ReadBuf, 4096, &Bytes, &obj->m_ovRead);
			if (Status != FALSE)
			{
				printf("收到 :");
				for (i = 0; i < Bytes; i++)
				{
					printf("%d ", ReadBuf[i]);
				}
				printf("\n");
			}
			//PurgeComm函数清空串口的输入输出缓冲区
			PurgeComm(obj->hCom, PURGE_RXCLEAR | PURGE_RXABORT);
		}
	}
	return 0;
}

四、关闭串口

本部分主要编写了关闭串口的函数体,其被串口类的析构函数调用,实现各开启功能的关闭。若只是关闭开启的串口号,可直接调用底层代码提供的CloseHandle函数。

void My_Com::Close_Com()
{
	m_IsOpen = false;
	if (INVALID_HANDLE_VALUE != hCom)
	{
		CloseHandle(hCom);
		hCom = INVALID_HANDLE_VALUE;
	}
	if (NULL != m_ovRead.hEvent)
	{
		CloseHandle(m_ovRead.hEvent);
		m_ovRead.hEvent = NULL;
	}
	if (NULL != m_ovWrite.hEvent)
	{
		CloseHandle(m_ovWrite.hEvent);
		m_ovWrite.hEvent = NULL;
	}
	if (NULL != m_ovWait.hEvent)
	{
		CloseHandle(m_ovWait.hEvent);
		m_ovWait.hEvent = NULL;
	}
	if (NULL != m_Thread)
	{
		WaitForSingleObject(m_Thread, 5000);//等待线程结束  
		CloseHandle(m_Thread);
		m_Thread = NULL;
	}
}

本串口异步通信的程序组合可见:https://download.csdn.net/download/sinat_35728816/10599384

 

 

 

 

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值