VC++ CLR 串口读写上位机例程

使用VC++ .net4.0编写的串口读写上位机,实现基本的配置读取,写入,以及连续的实时数据读取显示,波形显示(采用异步操作,连续读取实时数据的过程中,可以读写配置)。

1.总体界面


功能:系统串口选择,串口连接,通信地址设置,采集周期设置功能,读取配置,写入配置。


功能:实时数据读取并显示,同步显示波形数据。

2.串口获取

在 toolStripComboBox1 控件的 DropDown事件中,获取系统的串口,并显示。

	//刷新串口
	private: System::Void toolStripComboBox1_DropDown(System::Object^  sender, System::EventArgs^  e) {
		this->UI_RefreshCom();	//刷新串口
	}
//刷新串口
void CLASS_NAME::UI_RefreshCom(void)
{
	String ^SelectUartName;
	bool isDefault = true;

	try
	{
		SelectUartName = this->_UART_ComboBox->SelectedItem->ToString();//获取上次的串口号
		this->UI_comboBoxGetCom();										//重新刷新串口
		
		//查找刷新前的串口是否存在,如果存在则选择之前的串口
		for (int i = 0; i < this->_UART_ComboBox->Items->Count; i++)
		{
			if (this->_UART_ComboBox->Items[i]->ToString() == SelectUartName)//找到了之前的串口
			{
				this->_UART_ComboBox->SelectedIndex = i;
				isDefault = false;
				break;
			}
		}
		if (isDefault == true) //需要选择默认的
		{
			if (this->_UART_ComboBox->Items->Count != 0)					//如果串口数量不为0,则选中第一个
			{
				this->_UART_ComboBox->SelectedIndex = 0;					//默认选择第一个串口
			}
		}

	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
	}
}

3.连接或者关闭串口,按钮事件

			 //连接或关闭串口
private: System::Void toolStripButton1_Click(System::Object^  sender, System::EventArgs^  e) {
	this->UI_OpenAndCloseUart_Button_Click();//连接或关闭串口
}
//连接或关闭串口
void CLASS_NAME::UI_OpenAndCloseUart_Button_Click(void)
{
	String ^SelectUartName;
	bool isDefault = true;
	DWORD Status;
	WCHAR ComName[8];
	char *pComName;

	try
	{
		System::ComponentModel::ComponentResourceManager^  resources = (gcnew System::ComponentModel::ComponentResourceManager(MainForm::typeid));

		if (g_mUartHandle == 0)			//当前串口没有连接,开始连接串口
		{
			this->toolStripStatusLabel1->Text = "未连接";		//底部状态
			if (g_mUART.UartNum == 0)	//没有串口,无法连接
			{
				System::Windows::Forms::MessageBox::Show("没有串口,无法连接!", "错误", System::Windows::Forms::MessageBoxButtons::OK,
					System::Windows::Forms::MessageBoxIcon::Error);
				return;
			}
			pComName = USER_LIB.StringToChar(this->_UART_ComboBox->SelectedItem->ToString());	//获取当前选择的串口名称
			if (strlen(pComName) > 6) pComName[6] = 0;	//限制串口名称长度
			USER_LIB.CharToWchar(pComName, ComName);
			g_mUartHandle = g_mUART.UART_Init(ComName, 9600, 4096, &Status);
			if (g_mUartHandle <= 0)
			{
				g_mUartHandle = 0;						//句柄设置为0,代表串口没有连接
				System::Windows::Forms::MessageBox::Show("连接串口失败,错误:"+Status, "错误", System::Windows::Forms::MessageBoxButtons::OK,
					System::Windows::Forms::MessageBoxIcon::Error);
				return;
			}
			this->toolStripStatusLabel1->Text = "连接成功";		//底部状态
			this->_UART_ComboBox->Enabled = false;				//串口连接后,禁用串口选择
			this->tabControl1->Enabled = true;					//连接成功了,允许配置
			//按钮图片变为已经连接状态
			this->toolStripButton1->Image = (cli::safe_cast<System::Drawing::Image^>(resources->GetObject(L"toolStripButton2.Image")));
		}
		else //断开连接
		{
			g_mUART.UART_Close(g_mUartHandle);	//断开连接
			g_mUartHandle = 0;					//句柄清零
			this->toolStripStatusLabel1->Text = "未连接";		//底部状态
			this->_UART_ComboBox->Enabled = true;				//串口关闭后,启用串口选择
			//显示关闭图标
			this->toolStripButton1->Image = (cli::safe_cast<System::Drawing::Image^>(resources->GetObject(L"toolStripButton1.Image")));
			this->tabControl1->Enabled = false;					//连接断开,不允许配置
		}
	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
	}
}

4.读取配置 按钮事件

		 //读取配置
private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) {
	this->UI_ReadConfig_Button_Click();	//读取配置
}
//读取配置
void CLASS_NAME::UI_ReadConfig_Button_Click(void)
{
	try
	{
		//禁用界面,并弹出读取中窗口提示
		this->toolStrip1->Enabled = false;
		this->tabControl1->Enabled = false;
		this->mMessageControl->Visible = true;			//显示读取中提示窗口
		this->isReadConfig = true;						//异步命令,需要读取配置
	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
		System::Windows::Forms::MessageBox::Show(e->Message, "错误", System::Windows::Forms::MessageBoxButtons::OK,
			System::Windows::Forms::MessageBoxIcon::Error);
	}
}

读取配置采用异步操作,异步线程中不停的判断 this->isReadConfig 是否有效,如果有效将会进行异步的读取操作。

5.写入配置 按钮事件

		 //写入配置
private: System::Void button2_Click(System::Object^  sender, System::EventArgs^  e) {
	this->UI_WriteConfig_Button_Click();//写入配置
}
//写入配置
void CLASS_NAME::UI_WriteConfig_Button_Click(void)
{
	try
	{
		//先从界面获取配置到全局缓冲区中
		this->UI_GetConfig(this->pWriteConfig);
		//如果没有读取过配置,则提示用户,应该先读取配置
		if (this->isNotReadConfig == true)
		{
			System::Windows::Forms::MessageBox::Show("请先读取配置,再写入!", "警告", System::Windows::Forms::MessageBoxButtons::OK,
				System::Windows::Forms::MessageBoxIcon::Warning);
			return;
		}
		//检查配置
		if (this->CheckConfig(this->pWriteConfig) == false)//检查配置
		{
			System::Windows::Forms::MessageBox::Show("无效的配置", "错误", System::Windows::Forms::MessageBoxButtons::OK,
				System::Windows::Forms::MessageBoxIcon::Error);
			return;
		}

		//禁用界面,并弹出读取中窗口提示
		this->toolStrip1->Enabled = false;
		this->tabControl1->Enabled = false;
		this->mMessageControl->Visible = true;			//显示操作中提示窗口
		this->isWriteConfig = true;						//异步命令,需要写入配置
	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
		System::Windows::Forms::MessageBox::Show(e->Message, "错误", System::Windows::Forms::MessageBoxButtons::OK,
			System::Windows::Forms::MessageBoxIcon::Error);
	}
}

同读取配置一样,采用异步操作。


6.实时数据读取 按钮事件

		 //实时数据读取开关
private: System::Void button3_Click(System::Object^  sender, System::EventArgs^  e) {
	this->UI_ReadRealData_Button_Click();				//读取实时数据
}
//读取实时数据
void CLASS_NAME::UI_ReadRealData_Button_Click(void)
{
	try
	{
		if (this->isReadRealData == false) //没有读取-开始读取
		{
			this->isReadRealData = true;
			this->button3->Text = "读取中...";
		}
		else //已经开启了,关闭读取
		{
			this->isReadRealData = false;
			this->button3->Text = "读取关闭";
		}
	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
		System::Windows::Forms::MessageBox::Show(e->Message, "错误", System::Windows::Forms::MessageBoxButtons::OK,
			System::Windows::Forms::MessageBoxIcon::Error);
	}
}

同读取配置一样,采用异步操作。

7.异步操作介绍

异步操作采用的 System::ComponentModel::BackgroundWorker^ mBackgroundWorker; 异步工作线程实现的,工作中线程属于后台线程,在主线程关闭后会自动停止。

//异步线程初始化
void CLASS_NAME::BackgroundWorker_Init(void)
{
	this->mBackgroundWorker = (gcnew System::ComponentModel::BackgroundWorker());				//异步线程初始化
	this->mBackgroundWorker->WorkerReportsProgress = true;										//运行更新状态
	this->mBackgroundWorker->WorkerSupportsCancellation = true;									//允许异步结束
	this->mBackgroundWorker->DoWork += gcnew System::ComponentModel::DoWorkEventHandler(this, &CLASS_NAME::BackgroundWorker_DoWork);
	this->mBackgroundWorker->ProgressChanged += gcnew System::ComponentModel::ProgressChangedEventHandler(this, &CLASS_NAME::BackgroundWorker_ProgressChanged);
	this->mBackgroundWorker->RunWorkerCompleted += gcnew System::ComponentModel::RunWorkerCompletedEventHandler(this, &CLASS_NAME::BackgroundWorker_RunWorkerCompleted);


	this->mBackgroundWorker->RunWorkerAsync();	//开始执行
}

异步线程的初始化主要是添加一些事件,比如线程核心函数,线程状态更新回调函数,线程结束后回调函数,是否允许更新状态,此处必须允许更新状态,在异步线程中是不能直接访问UI的,但是使用状态更新可以实现异步刷新的目的,比如在异步线程中读取配置,读取车成功后触发一个状态,在BackgroundWorker_ProgressChanged中刷新界面。

//线程-运行核心
System::Void CLASS_NAME::BackgroundWorker_DoWork(System::Object^  sender, System::ComponentModel::DoWorkEventArgs^  e)
{
	char *pError;
	CONFIG_TYPE TempConfig;	//临时配置缓冲区

	try
	{
		while (1)
		{
			try
			{
				this->dt = System::DateTime::Now;						//更新系统时间

				if (g_mUartHandle <= 0)	//串口如果没有连接,则延时等待
				{
					Sleep(500);
				}
				else //串口连接成功了,等待读写数据
				{
					if (this->isReadConfig == true)	//需要读取配置
					{
						this->isReadConfig = false;	//清除状态
						if (ReadConfig(&TempConfig, &pError) == true)	//读取成功了
						{
							memcpy(this->pReadConfig, &TempConfig, sizeof(CONFIG_TYPE));	//配置读取成功了
							this->mBackgroundWorker->ReportProgress(0);	//读取配置成功
						}
						else //读取失败了
						{
							this->mStringBuilderError->Clear();			//清空字符
							this->mStringBuilderError->Append("读取配置失败,错误:");
							this->mStringBuilderError->Append(CharToString(pError));
							this->mBackgroundWorker->ReportProgress(1);	//读取配置失败
						}
					}
					else if (this->isWriteConfig == true) //写配置
					{
						this->isWriteConfig = false;		//清除写命令
						if (this->CheckConfig(this->pWriteConfig) == false)//配置有误,不能写入
						{
							this->mStringBuilderError->Clear();			//清空字符
							this->mStringBuilderError->Append("配置有误,不允许写入");
							this->mBackgroundWorker->ReportProgress(3);	//写配置失败
						}
						else //配置无误,写入
						{
							if (this->WriteConfig(this->pWriteConfig, &pError) == true)
							{
								this->mBackgroundWorker->ReportProgress(2);	//写配置成功
							}
							else //写入失败
							{
								this->mStringBuilderError->Clear();			//清空字符
								this->mStringBuilderError->Append("写入配置失败,错误:");
								this->mStringBuilderError->Append(CharToString(pError));
								this->mBackgroundWorker->ReportProgress(3);	//写入配置失败
							}
						}
					}
					else if (this->isReadRealData == true)	//需要读取实时数据
					{
						if (this->ReadRealData(this->pRealData, &pError) == true)	//读取成功了
						{
							this->mBackgroundWorker->ReportProgress(4);	//读取配置成功
						}
						else //读取失败了
						{
							this->mStringBuilderError->Clear();			//清空字符
							this->mStringBuilderError->Append("读取实时数据,错误:");
							this->mStringBuilderError->Append(CharToString(pError));
							this->mBackgroundWorker->ReportProgress(5);	//读取配置失败
						}

						Sleep(500);
					}



					Sleep(100);
				}
			}
			catch (Exception^ e)
			{
				SYS_LOG.Write(__FILE__ + __LINE__ + "\t异步线程崩溃:" + e->Message + e->StackTrace);
				Sleep(3000);
			}
			
		}

	}
	catch (Exception ^e1)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + "\t:" + e1->Message + e1->StackTrace);
	}
}

//线程-状态改变
System::Void CLASS_NAME::BackgroundWorker_ProgressChanged(System::Object^  sender, System::ComponentModel::ProgressChangedEventArgs^  e)
{
	char buff[24];

	try
	{
		switch (e->ProgressPercentage)
		{
			case 0:	//读取成功了
			{
				this->toolStrip1->Enabled = true;
				this->tabControl1->Enabled = true;
				this->mMessageControl->Visible = false;			//影藏读取中提示窗口

				this->UI_ShowConfig(this->pReadConfig);			//显示配置到界面
				this->isNotReadConfig = false;					//配置读取过,标志清零
				this->toolStripStatusLabel1->Text = "读取配置成功";
				System::Windows::Forms::MessageBox::Show("读取配置成功!", "提示", System::Windows::Forms::MessageBoxButtons::OK,
					System::Windows::Forms::MessageBoxIcon::Information);

			}break;
			case 1:	//读取失败了
			{
				this->toolStrip1->Enabled = true;
				this->tabControl1->Enabled = true;
				this->mMessageControl->Visible = false;			//影藏读取中提示窗口

				this->toolStripStatusLabel1->Text = this->mStringBuilderError->ToString();
				System::Windows::Forms::MessageBox::Show(this->mStringBuilderError->ToString(), "错误", System::Windows::Forms::MessageBoxButtons::OK,
					System::Windows::Forms::MessageBoxIcon::Error);
			}break;

			case 2://写配置成功
			{
				this->toolStrip1->Enabled = true;
				this->tabControl1->Enabled = true;
				this->mMessageControl->Visible = false;			//影藏读取中提示窗口

				this->toolStripStatusLabel1->Text = "写入配置成功";
				System::Windows::Forms::MessageBox::Show("写入配置成功!", "提示", System::Windows::Forms::MessageBoxButtons::OK,
					System::Windows::Forms::MessageBoxIcon::Information);
			}break;
			case 3:	//写入配置失败
			{
				this->toolStrip1->Enabled = true;
				this->tabControl1->Enabled = true;
				this->mMessageControl->Visible = false;			//影藏读取中提示窗口

				this->toolStripStatusLabel1->Text = this->mStringBuilderError->ToString();
				System::Windows::Forms::MessageBox::Show(this->mStringBuilderError->ToString(), "错误", System::Windows::Forms::MessageBoxButtons::OK,
					System::Windows::Forms::MessageBoxIcon::Error);
			}break;
			case 4:	//读取成功了,显示实时数据
			{
				this->UI_ShowRealData(this->pRealData);	//显示读取到的实时数据到界面
				this->toolStripStatusLabel1->Text = "读取实时数据成功";	//底部状态提示
			}break;
			case 5:	//读取配置失败了
			{
				this->toolStripStatusLabel1->Text = this->mStringBuilderError->ToString();
			}break;
		}
	}
	catch (Exception^ e1)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + "\t:" + e1->Message + e1->StackTrace);
	}
}

//线程-结束
System::Void CLASS_NAME::BackgroundWorker_RunWorkerCompleted(System::Object^  sender, System::ComponentModel::RunWorkerCompletedEventArgs^  e)
{
	try
	{


	}
	catch (Exception^ e1)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + "\t:" + e1->Message + e1->StackTrace);
	}
}

8.modbus-RTU

modbus-RTU协议使用了回调函数,跟单片机中类似的,此处我只需要实现底层的串口收发函数,并初始化回调即可,注意在CLR程序中,托管的代码必须使用类,但是托管的函数中不允许直接使用函数指针,此处我使用的C代码(非类)来实现modbus所需的收发函数(函数是全局的,无需像类需要先实例化)。

CommInterface.c

#include "StdAfx.h"
#include "CommInterface.h"
#include "UART.h"
#include "SystemLog.h"
#include "modbus_rtu.h"

UART_TYPE g_mUART;						//串口类
HANDLE g_mUartHandle = 0;				//串口句柄
MODBUS_RTU g_mModbus;					//MODBUS-RTU 通信接口类

#define  BAUD_RATE		9600			//串口波特率


//串口发送函数
bool UART_SendData(BYTE *pData, DWORD DataLen)
{
	try
	{
		g_mUART.MYUART_ClearTxBuff(g_mUartHandle);								//清空发送缓冲区 
		if (g_mUART.MYUART_SendData(g_mUartHandle, pData, DataLen) == false)	//调用串口发送数据
		{
			return false;	//串口错误
		}
		Sleep(DataLen * 8 * 1000 / BAUD_RATE);
		g_mUART.MYUART_ClearRxBuff(g_mUartHandle);								//清除接收缓冲区
		return true;
	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
	}

	return false;
}


//清除接收缓冲区
void UART_ClearRxBuff(void)
{
	g_mUART.MYUART_ClearRxBuff(g_mUartHandle);								//清除接收缓冲区
}

//串口接收数据
bool UART_ReadData(BYTE *pData, DWORD *DataLen)
{
	DWORD cnt = 0;
	DWORD TimeOut = 500 / 50;										//超时时间
	DWORD DelayCnt = 0;												//延时计数器,最大等待5秒
	DWORD PackDelay = (32 * 10 * 1000 * 2) / BAUD_RATE;				//包延时间隔

	try
	{
		//等待数据返回
		do
		{
			cnt = g_mUART.MYUART_GetRxCnt(g_mUartHandle);			//获取接收到的数据长度
			Sleep(50);												//延时10ms	
			if (cnt == g_mUART.MYUART_GetRxCnt(g_mUartHandle))		//完成接收数据了,退出等待
			{
				TimeOut--;
				if ((cnt > 0) && (TimeOut != 0))
				{
					if (cnt > 30)
					{

						Sleep(PackDelay);									//收完后再等待200ms防止CH340这类串口分包导致数据丢失,串口波特率不一样时等待的实际会不一样,大数据包等待的时间会更长
						DelayCnt += PackDelay;
					}
					Sleep(20);										//收完后再等待20ms防止PL2303这类串口分包导致数据丢失
					TimeOut = 1;									//数据接收完毕,退出
					DelayCnt += 20;
				}
			}
			DelayCnt += 50;
			if (DelayCnt > 5000) break;								//强制退出,5秒
		} while (TimeOut);

		//等待完毕
		if (cnt == 0) 												//没有接收到数据
		{
			*DataLen = 0;											//返回接收数据长度
			g_mUART.MYUART_ClearRxBuff(g_mUartHandle);		//清除接收缓冲区
			return true;											//返回超时
		}
		//读取数据
		if (g_mUART.MYUART_ReadData(g_mUartHandle, pData, cnt) == -1)//读取串口接收到的数据
		{
			*DataLen = 0;											//返回接收数据长度
			g_mUART.MYUART_ClearRxBuff(g_mUartHandle);		//清除接收缓冲区
			return false;											//串口错误
		}
		*DataLen = cnt;												//返回接收数据长度
		g_mUART.MYUART_ClearRxBuff(g_mUartHandle);			//清除接收缓冲区

		return true;												//读取数据成功
	}
	catch (Exception^ e)
	{
		SYS_LOG.Write(__FILE__ + __LINE__ + " \t:" + e->Message + e->StackTrace);
	}
	*DataLen = 0;
	return false;
}



//MODBUS通讯接口初始化
void MODBUS_InterfaceInit(void)
{
	//初始化Modbus-rtu的回调函数指针  
	g_mModbus.InterfaceInit(UART_SendData, UART_ReadData);
}

CommInterface.h

#pragma once

#include "UserLib.h"
#include "windows.h"
#include "UART.h"
#include "modbus_rtu.h"

extern UART_TYPE g_mUART;							//串口类
extern HANDLE g_mUartHandle;						//串口句柄
extern MODBUS_RTU g_mModbus;						//MODBUS-RTU 通信接口类

bool UART_SendData(BYTE *pData, DWORD DataLen);		//串口发送函数
bool UART_ReadData(BYTE *pData, DWORD *DataLen);	//串口接收数据
void UART_ClearRxBuff(void);						//清除接收缓冲区

void MODBUS_Int

9.使用modbus-RTU协议读取配置与数据

//读取配置-通信过程
bool CLASS_NAME::ReadConfig(CONFIG_TYPE *pConfig, char **pError)
{
	int Retry;
	MRTU_ERROR Status;
	WORD RegBuff[5];


	try
	{
		//调用modbus读取数据,失败重试3次
		for (Retry = 0; Retry < 3; Retry ++)
		{
			Status = g_mModbus.ReadMultReg(HOLD_REG, 1, 0, 2, RegBuff, pError);	//读取保持寄存器0,1
			if (Status == MRTU_OK) //读取成功
			{
				pConfig->Addr = RegBuff[0];	//寄存器0,通信地址
				pConfig->Time = RegBuff[1];	//寄存器1,采集间隔
				return true;		//返回成功
			}
			Sleep(200);				//失败了,延时200ms并重试
			
		}
	}
	catch (Exception^ e)
	{
		*pError = USER_LIB.StringToChar(e->Message);
	}

	return false;
}



//写入配置-通信过程
bool CLASS_NAME::WriteConfig(CONFIG_TYPE *pConfig, char **pError)
{
	int Retry;
	MRTU_ERROR Status;
	WORD RegBuff[5];


	try
	{
		//调用modbus写入数据,失败重试3次
		for (Retry = 0; Retry < 3; Retry++)
		{
			RegBuff[0] = pConfig->Addr;	//寄存器0,通信地址
			RegBuff[1] = pConfig->Time;	//寄存器1,采集间隔

			Status = g_mModbus.WriteMultReg(1, 0,RegBuff, 2,  pError);	//读取保持寄存器0,1
			if (Status == MRTU_OK) //读取成功
			{
				return true;		//返回成功
			}
			Sleep(200);				//失败了,延时200ms并重试

		}
	}
	catch (Exception^ e)
	{
		*pError = USER_LIB.StringToChar(e->Message);
	}

	return false;
}



//读取实时数据-通信过程
bool CLASS_NAME::ReadRealData(REAL_DATA_TYPE *pData, char **pError)
{
	int Retry;
	MRTU_ERROR Status;
	WORD RegBuff[5];

	//寄存器3,4:水位,寄存器5:电压
	try
	{
		//调用modbus读取数据,失败重试3次
		for (Retry = 0; Retry < 3; Retry++)
		{
			Status = g_mModbus.ReadMultReg(HOLD_REG, 1, 3, 3, RegBuff, pError);	//读取保持寄存器0,1
			if (Status == MRTU_OK) //读取成功
			{
				pData->WaterLevel = RegBuff[0];		//寄存器3,水位高16位
				pData->WaterLevel <<= 16;
				pData->WaterLevel |= RegBuff[1];	//寄存器4,水位低16位

				pData->Vol = RegBuff[2];			//寄存器5,电压值
				return true;		//返回成功
			}
			Sleep(200);				//失败了,延时200ms并重试

		}
	}
	catch (Exception^ e)
	{
		*pError = USER_LIB.StringToChar(e->Message);
	}

	return false;
}

10.工程目录说明


UserLib文件夹中都是我自己实现的一些工具类

GetConfigFromUI:用于从NumericUpDown获取或显示数据,增加了异常与范围限制功能。

MODBUS_RTU:MODBUS_RTU通信协议层

SystemLog:简单的日志。

UART:串口操作相关类。

UserLib:常用的工具类。


11.测试效果

测试寄存器说明

寄存器0,通信地址

寄存器1,采集间隔

寄存器3,水位高16位

寄存器4,水位低16位

寄存器5,电压值


可以获取到系统串口,COM30 COM31为一对虚拟串口,用于测试。


读取配置测试效果,使用了Modbus Slave虚拟的modbus从机进行测试。


写入配置测试,从机的寄存器0与1发生了同步的变化。


实时数据读取与波形显示,在实时数据读取的过程中可以同时读写配置,由于使用了异步操作,界面不会卡顿,并且多个操作可以一起顺序执行,不用担心连续读取实时数据的时候影响配置读写。


相关文章链接

Modbus-RTU实现:https://blog.csdn.net/cp1300/article/details/53036478

串口操作:https://blog.csdn.net/cp1300/article/details/40591699

完整工程实例(VS2013 .NET4.0 VC++2010):https://download.csdn.net/download/cp1300/10395424





猜你喜欢

转载自blog.csdn.net/cp1300/article/details/80213417
CLR
今日推荐