基于RS485的双机(客户端)通信软件-MFC(C++)实现

双端口可以是双PC机,也可以是一台PC的两个串口。
可实现2端口间通信,其中有通信协议的设置。分为主从站。

功能+协议文档

一、 硬件环境

  1. 连接方式
    RS-485,一主四从模式
    各站点间通过“USB转485”相连,将各转接器的485并联到总线上。
  2. 网络拓扑

二、 报文格式概述

  1. 报文格式
    类型 帧头 源
    地址 目的
    地址 主
    功能码 读写
    功能码 有效
    数据 CRC 帧尾
    字节数 1 1 1 2 2 2 2 1
    ASCII ‘*’ 如‘0’ 如‘1’ 如‘Ms’ 如‘Wr’ 如‘12’ 如‘12’ ‘#’

  2. 主功能码:

    1. 定时通信: 主 –》 某从 Master Slave Ms
    2. 广播同步: 主 –》 任意从 BroadCast Bc
    3. 错误检测: 主 –》 从 Error Er
      从 –》 主
    4. 网络管理: 主 轮询 从 Net Control Nc
  3. 读写功能码:

  4. 读 : Write Wr
  5. 写 : Read Rd
  6. 无 : None No

三、各功能描述

  1. 定时通信
    1.1 主站主动向从站发, 读写功能码填Wr或Rd.
    2.1 读:从站“读写功能码”回复为读,有效数据为待读数据
    写:从站“读写功能码”回复为写,有效数据为原(主向从发送的)报文。

类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 CRC 帧尾
字节数 1 1 1 2 2 2 2 1
ASCII ‘*’ 如‘0’ 如‘1’ 如‘Ms’ 如‘Wr’ 如‘12’ 如‘12’ ‘#’

  1. 广播同步
    类型 帧头 源
    地址 目的
    地址 主
    功能码 读写
    功能码 有效
    数据 CRC 帧尾
    字节数 1 1 1 2 2 8 2 1
    ASCII ‘*’ ‘0’ ‘F’ ‘Br’ ‘No’ “12:00:00” 如‘12’ ‘#’

因所有从站均接收,故目的地址为’F’.
不需要读写功能码。
有效数据为时间(时:分:秒)
3. 错误检测
类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 CRC 帧尾
字节数 1 1 1 2 2 2 2 1
ASCII ‘*’ 如‘1’ 如‘0’ ‘Er’ 无 无 无 无
上图为错误帧”*10Er#”, 主功能码为错误功能码, 有效数据设为默认的“%%”。
以主-》从为例,当从收到CRC不一致时,向主发送此错误帧,主会重传原报文。

4. 网络管理

类型 帧头 源
地址 目的
地址 主
功能码 读写
功能码 有效
数据 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间任意从站上线均会认为上线。
从站若在线,回复相同报文。

5. 数据记录

存为txt文件, 绘制历史曲线, 可选择调取任意站点、时间的数据。
固定节点4为监控节点, 不断监控总线上的所有数据。

6. 闭环控制

以控制炉温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 <iostream>

#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<WPARAM>(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<HCURSOR>(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

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daska110/article/details/80323326
文章标签: RS485 桌面通信软件
个人分类: MFC BCB界面软件
上一篇大话设计模式笔记
下一篇C++,打开关闭与使用,剪切板ClipBoard
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭