双端口可以是双PC机,也可以是一台PC的两个串口。
可实现2端口间通信,其中有通信协议的设置。分为主从站。
报文格式
类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 CRC 帧尾
字节数 1 1 1 2 2 2 2 1
ASCII ‘*’ 如‘0’ 如‘1’ 如‘Ms’ 如‘Wr’ 如‘12’ 如‘12’ ‘#’
主功能码:
读写功能码:
类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 CRC 帧尾
字节数 1 1 1 2 2 2 2 1
ASCII ‘*’ 如‘0’ 如‘1’ 如‘Ms’ 如‘Wr’ 如‘12’ 如‘12’ ‘#’
因所有从站均接收,故目的地址为’F’.
不需要读写功能码。
有效数据为时间(时:分:秒)
3. 错误检测
类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 CRC 帧尾
字节数 1 1 1 2 2 2 2 1
ASCII ‘*’ 如‘1’ 如‘0’ ‘Er’ 无 无 无 无
上图为错误帧”*10Er#”, 主功能码为错误功能码, 有效数据设为默认的“%%”。
以主-》从为例,当从收到CRC不一致时,向主发送此错误帧,主会重传原报文。
类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 CRC 帧尾
字节数 1 1 1 2 2 2 2 1
ASCII ‘*’ 如‘0’ 如‘1’ ‘Nc’ 如‘Wr’ 如‘12’ 如‘12’ ‘#’
4.1 掉线检测:
主轮询,某从若3次未回复
4.2 上线检测:
主轮询(冗余), 若某从第一次回复,认为上线。假设目前为1主4从, 但主会冗余轮询1-9共9个从站,故1-9间任意从站上线均会认为上线。
从站若在线,回复相同报文。
存为txt文件, 绘制历史曲线, 可选择调取任意站点、时间的数据。
固定节点4为监控节点, 不断监控总线上的所有数据。
以控制炉温100度为例:
传感器(节点1)测得90度,将90度发送给控制器(节点2) 节点2算得100-90 = 10度偏差, 将”10度偏差对应的控制动作“送给执行器(节点3) 执行器(节点3)收到后,使炉温由90度上升到100度 节点4一直在记录总线数据。
2个节点的简化如下:
以控制炉温100度为例:
传感器(节点1)测得90度,将90度发送给控制器(节点2) 节点2算得100-90 = 10度偏差, 将”10度偏差对应的控制动作“送给执行器(节点3) 执行器(节点3)收到后,使炉温由90度上升到100度 节点4一直在记录总线数据。
//----------------------------------------------------------
// 串口控件
void CTestDlg::OnCommMscomm1()
{
// TODO: 在此处添加消息处理程序代码
if (m_mscom.get_CommEvent() == 2)
{
char str[1024] = { 0 };
long k;
VARIANT InputData = m_mscom.get_Input();
COleSafeArray fs;
fs = InputData;
for (k = 0; k < fs.GetOneDimSize(); k++)
fs.GetElement(&k, str + k);
m_EditReceive += str; // 显示到接收框内
// 根据报头报尾区分报文,并计算CRC
int start, end;
int crc = 0;
for (start = 0; start < strlen(str); ++start) // 找start
{
if (str[start] == '*')
break;
}
for (end = start; end < strlen(str); ++end) // 找end
{
if (str[end] == '#')
break;
}
for (int i = start; i < end - 3; ++i)
{
crc += str[i];
}
crc = crc % 100;
if (crc != str[end - 2] * 10 + str[end - 1]) // 如果crc不相等的话
{
WrongFlag = true; // 主向从发送固定的错误帧为*10Er#
}
// str里有很多段报文,这只是截取了一段
UpdateData(false);
}
}
//----------------------------------------------------------
// 发送数据
void CTestDlg::OnBnClickedButtonSend()
{
// TODO: 在此添加控件通知处理程序代码
UpdateData(true);
if (WrongFlag == true)
{
char ErrChar[] = "*10Er#";
CString ErrStr;
ErrStr = ErrChar;
// ErrStr.Format("%s", ErrChar);
m_mscom.put_Output(COleVariant(ErrStr)); // 主向从发送固定的错误帧为*10Er#
}
else
m_mscom.put_Output(COleVariant(m_EditSend));
}
// TestDlg.cpp : 实现文件
//
#include "stdafx.h"
#include "Test.h"
#include "TestDlg.h"
#include "afxdialogex.h"
#include
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 用于应用程序“关于”菜单项的 CAboutDlg 对话框
class CAboutDlg : public CDialogEx
{
public:
CAboutDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_ABOUTBOX };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()
// CTestDlg 对话框
CTestDlg::CTestDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(IDD_TEST_DIALOG, pParent)
, m_EditReceive(_T(""))
, m_EditSend(_T(""))
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
busAllData = "总线所有数据如下: ";
}
void CTestDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT1, m_EditReceive);
DDX_Text(pDX, IDC_EDIT2, m_EditSend);
DDX_Control(pDX, IDC_COMBO1, m_comb1);
DDX_Control(pDX, IDC_COMBO2, m_comb2);
DDX_Control(pDX, IDC_MSCOMM1, m_mscom);
DDX_Control(pDX, IDC_EDIT1, m_EditReceiveShow);
DDX_Control(pDX, IDC_CHECK1, m_CheckBoxErrCheck);
DDX_Control(pDX, IDC_CHECK2, m_CheckBoxTempControl);
}
BEGIN_MESSAGE_MAP(CTestDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON_OPEN, &CTestDlg::OnBnClickedButtonOpen)
ON_BN_CLICKED(IDC_BUTTON_SEND, &CTestDlg::OnBnClickedButtonSend)
ON_BN_CLICKED(IDC_BUTTON_CLEAN, &CTestDlg::OnBnClickedButtonClean)
ON_BN_CLICKED(IDC_BUTTON_CLOSE, &CTestDlg::OnBnClickedButtonClose)
END_MESSAGE_MAP()
// CTestDlg 消息处理程序
BOOL CTestDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
CString str;
int i;
for (i = 0; i < 15; i++)
{
str.Format(_T("com %d"), i + 1);
m_comb1.InsertString(i, str);
}
m_comb1.SetCurSel(0);
CString str1[] = { _T("300"), _T("600"), _T("1200"), _T("2400"), _T("4800"), _T("9600"),
_T("19200"), _T("38400"), _T("43000"), _T("56000"), _T("57600"), _T("115200") };
for (int i = 0; i < 12; i++)
{
int judge_tf = m_comb2.AddString(str1[i]);
if ((judge_tf == CB_ERR) || (judge_tf == CB_ERRSPACE))
{
MessageBox(_T("build baud error!"));
}
}
m_comb2.SetCurSel(5);
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
void CTestDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialogEx::OnSysCommand(nID, lParam);
}
}
// 如果向对话框添加最小化按钮,则需要下面的代码
// 来绘制该图标。 对于使用文档/视图模型的 MFC 应用程序,
// 这将由框架自动完成。
void CTestDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // 用于绘制的设备上下文
SendMessage(WM_ICONERASEBKGND, reinterpret_cast(dc.GetSafeHdc()), 0);
// 使图标在工作区矩形中居中
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// 绘制图标
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CTestDlg::OnQueryDragIcon()
{
return static_cast(m_hIcon);
}
void CTestDlg::OnBnClickedButtonOpen()
{
// TODO: 在此添加控件通知处理程序代码
CString str, str1, n;
GetDlgItemText(IDC_BUTTON_OPEN, str);
CWnd *h1;
h1 = GetDlgItem(IDC_BUTTON_OPEN);
if (!m_mscom.get_PortOpen())
{
m_comb2.GetLBText(m_comb2.GetCurSel(), str1);
str1 = str1 + ',' + 'n' + ',' + '8' + ',' + '1';
m_mscom.put_CommPort((m_comb1.GetCurSel() + 1));
m_mscom.put_InputMode(1);
m_mscom.put_Settings(str1);
m_mscom.put_InputLen(1024);
m_mscom.put_RThreshold(1);
m_mscom.put_RTSEnable(1);
m_mscom.put_PortOpen(true);
if (m_mscom.get_PortOpen())
{
str = _T("关闭串口");
UpdateData(true);
h1->SetWindowTextW(str);
}
}
else
{
m_mscom.put_PortOpen(false);
if (str != _T("打开串口"))
{
str = _T("打开串口");
UpdateData(true);
h1->SetWindowTextW(str);
}
}
// 网络管理-从站1-上线
slaver1OnlineFlag = true;
}
//----------------------------------------------------------
// 发送数据
void CTestDlg::OnBnClickedButtonSend()
{
// TODO: 在此添加控件通知处理程序代码
UpdateData(true);
// 网络管理-从站1-上线
if(slaver1OnlineFlag == true)
{
m_EditSend = "*10Online#";
busAllData = busAllData + m_EditSend; // 因为要记录总线上所有数据,故把从站发送的数据也算上
m_mscom.put_Output(COleVariant(m_EditSend));
}
// 错误检测-从站1-发送
if (((CButton*)GetDlgItem(IDC_CHECK1))->GetCheck()) // 如果从站界面上选中错误检测,则启动下面的代码
{
// 错误检测-从站1-返回错误帧
if (WrongFlag == true)
{
char ErrChar[] = "*10Er#";
CString ErrStr;
ErrStr = ErrChar;
m_mscom.put_Output(COleVariant(ErrStr)); // 主向从发送固定的错误帧为*10Er#
}
else
m_mscom.put_Output(COleVariant(m_EditSend));
}
}
void CTestDlg::OnBnClickedButtonClean()
{
// TODO: 在此添加控件通知处理程序代码
m_EditReceive = _T("");
UpdateData(false);
}
void CTestDlg::OnBnClickedButtonClose()
{
// TODO: 在此添加控件通知处理程序代码
if (m_mscom.get_PortOpen())
m_mscom.put_PortOpen(false);
CDialogEx::OnCancel();
}
BEGIN_EVENTSINK_MAP(CTestDlg, CDialogEx)
ON_EVENT(CTestDlg, IDC_MSCOMM1, 1, CTestDlg::OnCommMscomm1, VTS_NONE)
END_EVENTSINK_MAP()
//----------------------------------------------------------
// 串口控件
void CTestDlg::OnCommMscomm1()
{
using namespace std;
// TODO: 在此处添加消息处理程序代码
if (m_mscom.get_CommEvent() == 2)
{
char str[1024] = { 0 };
long k;
VARIANT InputData = m_mscom.get_Input();
COleSafeArray fs;
fs = InputData;
for (k = 0; k < fs.GetOneDimSize(); k++)
fs.GetElement(&k, str + k);
m_EditReceive += str; // 显示到接收框内
CString m_EditReceiveCopy;
m_EditReceiveCopy = str; // 因为从站的m_EditReceive已经并不只是一个*0xMsNc#, 而是
// 一连串*0xMsNc#*0xMsNc#*0xMsNc#*0xMsNc#,所以要想办法清空,但界面上还要把所有的记录都写下来。
busAllData = busAllData + m_EditReceiveCopy; // 因为要记录总线上所有数据,故把从站接收的数据也算上
// 错误检测-从站1-接收
if (((CButton*)GetDlgItem(IDC_CHECK1))->GetCheck()) // 如果从站界面上选中错误检测,则启动下面的代码
{
// 根据报头报尾区分报文,并计算CRC
int start, end;
char crc = 0;
for (start = 0; start < strlen(str); ++start) // 找start
{
if (str[start] == '*')
break;
}
for (end = start; end < strlen(str); ++end) // 找end
{
if (str[end] == '#')
break;
}
for (int i = start; i < end - 2; ++i)
{
crc += (str[i] % 10);
}
crc = crc % 100;
if (crc != (str[end - 2] - '0') * 10 + (str[end - 1] - '0')) // 如果crc不相等的话
{
WrongFlag = true; // 主向从发送固定的错误帧为*10Er#
}
}
// 炉温闭环控制-从站1-接收
if (((CButton*)GetDlgItem(IDC_CHECK2))->GetCheck())
{
if (m_EditReceiveCopy == "*01Temp4850#") // (人手动)将”实测48度”和”标准50度”发送给控制器(节点2)
{
m_EditSend = "*10TempChange2#"; // 将”2度偏差对应的控制动作“送给执行器(节点1)*10TempChange2#
m_mscom.put_Output(COleVariant(m_EditSend));
}
}
if (m_EditReceiveCopy == "*0xMsNc#")
slave1StillOnlineFlag = true; // 主向从询问是否已下线,从需回复, 代表从还未下线
// 网络管理-从站1-下线
if (slave1StillOnlineFlag == true) // 主 向 从1 询问是否已下线,从1 需回复, 代表从1还未下线
{
m_EditSend = "*10MsNc#";
m_mscom.put_Output(COleVariant(m_EditSend));
slave1StillOnlineFlag = false; // 收到主站的“下线询问”后复位, 因为下次收到询问后还要回复
}
// 数据记录
CStdioFile file;
try
{
file.Open(_T("D:\\总线所有数据.txt"), CFile::modeCreate | CFile::modeWrite | CFile::typeText);
file.WriteString(busAllData);
file.Close();
}
catch (CFileException* e)
{
e->ReportError();
e->Delete();
}
UpdateData(false);
m_EditReceiveShow.SetSel(-1, -1); // 定位到最后一行
this->SetDlgItemTextW(IDC_EDIT1, m_EditReceive);
m_EditReceiveShow.LineScroll(m_EditReceiveShow.GetLineCount() - 1, 0);
}
}
// Test.cpp : 定义应用程序的类行为。
//
#include "stdafx.h"
#include "Test.h"
#include "TestDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CTestApp
BEGIN_MESSAGE_MAP(CTestApp, CWinApp)
ON_COMMAND(ID_HELP, &CWinApp::OnHelp)
END_MESSAGE_MAP()
// CTestApp 构造
CTestApp::CTestApp()
{
// 支持重新启动管理器
m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART;
// TODO: 在此处添加构造代码,
// 将所有重要的初始化放置在 InitInstance 中
}
// 唯一的一个 CTestApp 对象
CTestApp theApp;
// CTestApp 初始化
BOOL CTestApp::InitInstance()
{
//TODO: call AfxInitRichEdit2() to initialize richedit2 library.
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。 否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
CWinApp::InitInstance();
AfxEnableControlContainer();
// 创建 shell 管理器,以防对话框包含
// 任何 shell 树视图控件或 shell 列表视图控件。
CShellManager *pShellManager = new CShellManager;
// 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));
// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
CTestDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
else if (nResponse == -1)
{
TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n");
TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n");
}
// 删除上面创建的 shell 管理器。
if (pShellManager != NULL)
{
delete pShellManager;
}
// 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序,
// 而不是启动应用程序的消息泵。
return FALSE;
}
完整程序下载link