使用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