C#/C++ 上位机监控PLC流水灯状态启动和暂停功能

[复制链接]
查看80530 | 回复0 | 2024-12-24 15:27:23 | 显示全部楼层 |阅读模式
看过之前教程的朋友可能知道,我们是用威纶通触摸屏的 IDE 去监控 PLC 输出状态。现在我们尝试用高级语言,自己实现一个上位机来读取 PLC 的 Y 端口的实时输出状态。这里依然用的是三菱 PLC,编程语言用 VC++ MFC,通信使用的是三菱 PLC 编程口通信方式。

实现功能效果如下:

好的,我们现在开始。
界面设计


▲ 8 位流水灯上位界面
界面很简单,拖了 8 个 picturebox 控件,并且 ID 依次设置为 IDC_STATIC_Y0 ~ IDC_STATIC_Y7,分别对应 PLC 输出点 Y0~Y7 的输出状态;红色为有信号输出,灰色为无信号输出。
控件状态绘制

为了后续遍历方便,先把 8 个控件的 ID 放如 vector 容器里面。
private:
    std::vector<int> m_Y;
    CWnd* GetLEDCtr(int y_index);

GetLEDCtr() 函数实现:
CWnd* CMFCApplication5Dlg::GetLEDCtr(int y_index)
{
    if (y_index < 0 || y_index > 7)
    {
        return NULL;
    }
    return GetDlgItem(m_Y[y_index]);
}

在 BOOL CMFCApplication5Dlg::OnInitDialog() 中对数据初始化。
// 初始化控件样式
m_Y.push_back(IDC_STATIC_Y0);
m_Y.push_back(IDC_STATIC_Y1);
m_Y.push_back(IDC_STATIC_Y2);
m_Y.push_back(IDC_STATIC_Y3);
m_Y.push_back(IDC_STATIC_Y4);
m_Y.push_back(IDC_STATIC_Y5);
m_Y.push_back(IDC_STATIC_Y6);
m_Y.push_back(IDC_STATIC_Y7);
for (int i = 0; i < m_Y.size(); ++i)
{
    SetLED(m_Y, OFF);
}

上面的 ONOFF 宏对应控件红色和灰色的 RGB 值:
#define ON RGB(255, 0, 0)
#define OFF RGB(128,128, 128)

LED 控件颜色绘制:
void CMFCApplication5Dlg::SetLED(int ctrID, int color)
{
    CRect rect;
    CWnd* pWnd = GetDlgItem(ctrID);
    CDC* pDC = pWnd->GetDC();
    ::GetClientRect(pWnd->m_hWnd, &rect);

    CBrush brush(color);
    pDC->SelectObject(brush);
    pDC->Ellipse(0, 0, rect.Width(), rect.Height());
    pWnd->ReleaseDC(pDC);
}
串口初始化

在 BOOL CMFCApplication5Dlg::OnInitDialog() 中对串口进行初始化。
// 串口初始化
using namespace HuAutoLib;
COMParam com;
com.TimeOut = 1000;
com.Name = "COM4";
com.BaudRate = 9600;
com.ByteSize = 7;
com.StopBits = ONESTOPBIT;
com.Parity = EVENPARITY;

bool res = m_PlcCOM.Open(&com);
ASSERT(res);

我这里是 COM4 口,根据硬件连接的实际情况定义。这里波特率9600,停止位1位,偶校验,这些好像是固定的,出厂默认的;是否可以自定义设置,暂未做研究。
通信协议初始化

这个协议是固定了的,可以参考文档或者互联网上查询,然后自己验证一下就好了,这里直接给出。

读取 Y0-Y7 的状态:
0x02, 0x30, 0x30, 0x30, 0x41, 0x30, 0x30, 0x31, 0x03, 0x36, 0x35

它这里的协议,0~9 的话 + 30H对应 0x30~0x39, A~F 的话对应 0x41 ~ 0x46。

这里简单说下这个协议的意思,第一个字节0x02是固定的开始字节,第二个字节0x30是读的意思,第三个字节起的0x30, 0x30, 0x41, 0x30是读取的其实地址(这里是Y0~Y7的起始地址),后面跟随的0x30, 0x31 代表读取一个字节;倒数第三个字节0x03是结束符,最后两个字节0x36 0x35是校验字节(0x30, 0x30, 0x30, 0x41, 0x30, 0x30, 0x31, 0x03相加,取后两个字节)。

后面的所有其他指令,大概都遵循这个协议规定。

X置位和复位指令:
private:   
    void SetLED(int ctrID, int color);
    CWnd* GetLEDCtr(int y_index);
    std::vector<int> m_Y;
    HuAutoLib::SerialCOMPort m_PlcCOM;
    const char m_CMD_SET_X[8][9] = {
         {0x02,0x37,0x30,0x30,0x30,0x34,0x03,0x46,0x45},
         {0x02,0x37,0x30,0x31,0x30,0x34,0x03,0x46,0x46},
         {0x02,0x37,0x30,0x32,0x30,0x34,0x03,0x36,0x36},
         {0x02,0x37,0x30,0x33,0x30,0x34,0x03,0x36,0x37},
         {0x02,0x37,0x30,0x34,0x30,0x34,0x03,0x36,0x38},
         {0x02,0x37,0x30,0x35,0x30,0x34,0x03,0x36,0x39},
         {0x02,0x37,0x30,0x36,0x30,0x34,0x03,0x37,0x41},
         {0x02,0x37,0x30,0x37,0x30,0x34,0x03,0x37,0x42}
    };
    const char m_CMD_RST_X[8][9] = {
       {0x02,0x38,0x30,0x30,0x30,0x34,0x03,0x46,0x46},
       {0x02,0x38,0x30,0x31,0x30,0x34,0x03,0x36,0x36},
       {0x02,0x38,0x30,0x32,0x30,0x34,0x03,0x36,0x37},
       {0x02,0x38,0x30,0x33,0x30,0x34,0x03,0x36,0x38},
       {0x02,0x38,0x30,0x34,0x30,0x34,0x03,0x36,0x39},
       {0x02,0x38,0x30,0x35,0x30,0x34,0x03,0x37,0x41},
       {0x02,0x38,0x30,0x36,0x30,0x34,0x03,0x37,0x42},
       {0x02,0x38,0x30,0x37,0x30,0x34,0x03,0x37,0x43}
    };
    const char NAK_ERR = 0x15;
    const char ACK_SUCCESS = 0x06;
    std::recursive_mutex m_Mutex;
定时器

1号定时器,间隔100毫秒:
#define LIGHT_ID 1
#define LIGHT_BREATHE 100

Y 状态刷新:

int CMFCApplication5Dlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CDialogEx::OnCreate(lpCreateStruct) == -1)
        return -1;

    // TODO:  在此添加您专用的创建代码
    SetWindowText(_T("三菱PLC流水灯上位机"));
    SetTimer(LIGHT_ID, LIGHT_BREATHE, NULL);

    return 0;
}

void CMFCApplication5Dlg::OnTimer(UINT_PTR nIDEvent)
{
    // TODO: 在此添加消息处理程序代码和/或调用默认值
    switch (nIDEvent)
    {
    case LIGHT_ID:
    {
        std::lock_guard<std::recursive_mutex> lock(m_Mutex);
        char cmd_y0_y7[11] = { 0x02, 0x30, 0x30, 0x30, 0x41, 0x30, 0x30, 0x31, 0x03, 0x36, 0x35 };
        if (m_PlcCOM.Send(cmd_y0_y7, 11))
        {
            // 应答 02 30 30 03 36 33    // size 6
            int size = -1;
            const char* res = m_PlcCOM.Read(size, 0);
            if (res && size == 6)
            {
                char val[2] = { 0 };
                val[0] = res[1];
                val[1] = res[2];
                if (val[0] > 0x40)
                {
                    val[0] = val[0] - 0x40 - 1 + 10;
                }
                else if (val[0] >= 0x30)
                {
                    val[0] = val[0] - 0x30;
                }

                if (val[1] > 0x40)
                {
                    val[1] = val[1] - 0x40 - 1 + 10;
                }
                else if (val[1] >= 0x30)
                {
                    val[1] = val[1] - 0x30;
                }

                char y0_y7 = 16 * val[0] + val[1];
                char mask = 1;
                for (int i = 0; i < 8; ++i)
                {
                    if (y0_y7 & (mask << i))
                    {
                        SetLED(m_Y, ON);
                    }
                    else
                    {
                        SetLED(m_Y, OFF);
                    }
                }
            }
        }
    }
    break;
    default:
        break;
    }

    CDialogEx::OnTimer(nIDEvent);
}

开始和停止按钮:

void CMFCApplication5Dlg::OnBnClickedButtonStart()
{
    std::lock_guard<std::recursive_mutex> lock(m_Mutex);
    m_PlcCOM.Send(m_CMD_SET_X[0], 9);
    int len = -1;
    m_PlcCOM.Read(len, 0);
}

void CMFCApplication5Dlg::OnBnClickedButtonStop()
{
    std::lock_guard<std::recursive_mutex> lock(m_Mutex);
    m_PlcCOM.Send(m_CMD_SET_X[1], 9);
    int len = -1;
    m_PlcCOM.Read(len, 0);
}

到此,整体的功能已经实现了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册哦

x
您需要登录后才可以回帖 登录 | 注册哦

本版积分规则