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 toolStripStatusLabel1->Text = "连接成功"; //底部状态 this->_UART_ComboBox->Enabled = false; //串口连接后,禁用串口选择 this->tabControl1->Enabled = true; //连接成功了,允许配置 //按钮图片变为已经连接状态 this->toolStripButton1->Image = (cli::safe_cast(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(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 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 <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发生了同步的变化。
实时数据读取与波形显示,在实时数据读取的过程中可以同时读写配置,由于使用了异步操作,界面不会卡顿,并且多个操作可以一起顺序执行,不用担心连续读取实时数据的时候影响配置读写。