Pull to refresh

Промышленные контроллеры, Linux и только C++. Часть 1

Level of difficultyMedium
Reading time11 min
Views14K

Всем доброго времени суток. Я работаю на большом предприятии, занимаюсь автоматизацией производства. Очень захотелось с Вами поделиться наработками, которые мы используем в повседневной трудовой жизни. Не судите строго, это моя первая статья на большую аудиторию...

Если мы говорим о промышленной автоматизации, то понимаем, что программирование будет связано с языками стандарта МЭК 61131-3.

На основе данного стандарта в промышленные контроллеры устанавливают программное обеспечение от:

  • ISaGRAF, Rockwell Automation (Монреаль, Канада)

  • CoDeSYS, CODESYS GmbH (Кемптен, Германия)

  • ISPSoft, Delta Electronics, Inc (Тайбэй, Тайвань)

А как же быть, когда тебе не хочется зависеть от стороннего программного обеспечения и иметь переносимый код, который с минимальными трудозатратами можно развернуть на множестве устройств, и даже поставить на персональном компьютере?

Я нашел контроллер Icp-Das LP-8x21 на Ubuntu 12, пару модулей ввода/вывода MDS DIO-16BD и AIO-4 от КонтрАвт для тестов. Вот уже и есть с чем экспериментировать.

Тестовая сборка
Тестовая сборка

Для связи контроллера и модулей ввода/вывода будем использовать один из наиболее распространенных стандартов физического уровня связи - RS-485. Данные модули используют протокол Modbus.

Для создания исполняемых файлов под данный контроллер необходимо для его процессора установить компилятор:

sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf

Устанавливаем файлы для сборки и исходники libmodbus-dev:

sudo apt install libmodbus-dev
apt source libmodbus-dev

Переходим в созданную папку с исходниками и собираем библиотеку под компилятор для arm-linux-gnueabihf:

./autogen.sh
./configure --host=arm-linux-gnueabihf --enable-shared --enable-static
make

В папке ./src/.libs/  вы найдете все собранные библиотеки чтобы подложить в проект. Нам потребуется файл libmodbus.a.

Из документации для модулей ввода/вывода находим регистры для чтения. Для DIO-16BD это регистр 258 для входа, а для AIO-4 мы прочитаем 4 аналоговых входа с адреса 207. Т.к. там располагаются 4 значения типа float, то будем читать сразу 8 регистров.  Для пробы соберем на C++ небольшой проект:

Файл main.cpp:

#include <modbus/modbus.h>
#include <iostream>
#include <errno.h>
#include "util.h"

using namespace std;

bool _stop=false;

void handle_signal(int i) {
   printf("[MAIN] Terminating\n"); 
   _stop=true;
}

int main() {
   signal(SIGINT, handle_signal);
   signal(SIGTERM, handle_signal);
   cout<<"Start..."<<endl;
   uint16_t buffer[8];
   modbus_t *ctx=modbus_new_rtu("/dev/ttyS2",115200,'N',8,1);
   if (modbus_connect(ctx) == -1)
       cout<<"Connection failed: "<<modbus_strerror(errno)<<endl;
   modbus_set_response_timeout(ctx, 1,0);
   while(!_stop) {
       modbus_set_slave(ctx,1);
       int rc=modbus_read_registers(ctx,258,1,buffer);
       if (rc == -1)
           cout<<"#1 MDS_DIO_16BD::Read() "<<modbus_strerror(errno)<<endl;
       else
           cout<<"#1 "<<buffer[0]<<endl;
       modbus_set_slave(ctx,2);
       nsleep(10);
       rc=modbus_read_registers(ctx,258,1,buffer);
       if (rc == -1)
           cout<<"#2 MDS_DIO_16BD::Read() "<<modbus_strerror(errno)<<endl;
       else
           cout<<"#2 "<<buffer[0]<<endl;
       modbus_set_slave(ctx,3);
       nsleep(10);
       rc = modbus_read_input_registers(ctx, 207, 8, buffer);
       if (rc == -1)
           cout<<"#3 MDS_AIO_4::Read() "<<modbus_strerror(errno)<<endl;
       else
           cout<<"#3 "<<modbus_get_float_badc(buffer)<<" "<<modbus_get_float_badc(buffer+2)<<" "<<modbus_get_float_badc(buffer+4)<<" "<<modbus_get_float_badc(buffer+6)<<endl;
       nsleep(10);
   }  
   modbus_close(ctx);
   modbus_free(ctx);
   cout<<"Stop..."<<endl;
}

Файл util.cpp:

int nsleep(long miliseconds) {
   struct timespec req, rem;
   if(miliseconds > 999) {
       req.tv_sec = (int)(miliseconds / 1000);
       req.tv_nsec = (miliseconds - ((long)req.tv_sec * 1000)) * 1000000;
   }
   else {
       req.tv_sec = 0;
       req.tv_nsec = miliseconds * 1000000;
   }
   return nanosleep(&req , &rem);
}

Собираем и отправляем исполняемый файл при помощи следующих команд:

arm-linux-gnueabihf-g++ -std=c++11 -I ./libmodbus -L. -o ./main ./*.cpp -lmodbus
sshpass -p "icpdas" scp main root@192.168.0.2:/root/

Все собралось и отправилось на ПЛК. Если все у нас собрано и сконфигурировано правильно, то в терминале мы получим такой результат:

#1 0
#2 0
#3 4.000000 4.000001 3.999998 4.000000

Отлично все работает, в цикле отображаются нужные данные. Вот мы и научились получать данные с “поля”.

Если сейчас при таких таймингах посмотреть загрузку процессора на ПЛК, то мы увидим не очень красивую картину, что 3 модуля загружают контроллер на 10%. В боевых условиях это очень и очень много, т.к. сейчас есть рабочие проекты где более десятка линий с количеством модулей 7-10 штук.

Посмотрим данные о загрузке системы. Вызовем команду top:

top - 10:47:15 up  5:28,  2 users,  load average: 1.00, 1.01, 1.03
Tasks: 108 total,   1 running, 107 sleeping,   0 stopped,   0 zombie
Cpu(s):  1.3%us, 14.4%sy,  0.0%ni, 84.0%id,  0.0%wa,  0.0%hi,  0.3%si,  0.0%st
Mem:	506968k total,	95152k used,   411816k free,    	0k buffers
Swap:    	0k total,    	0k used,    	0k free,	41316k cached

  PID USER  	PR  NI  VIRT  RES  SHR S %CPU %MEM	TIME+  COMMAND
 2079 root  	20   0  2660 1004  860 S  9.5  0.2 0:09.09 main…

Для уменьшения нагрузки на систему, можно увеличить задержку после опроса всех модулей ввода/вывода. Если установить задержку в 300 мс., то загрузка процессора составить 1.6% на данную задачу.

Как же нам улучшить и унифицировать работу с большим количеством разных устройств? Надо пойти в сторону паттерна строителя. Создадим базовый класс для Modbus устройств:

Файл ModbusModule.h:

class ModbusModule
{
public:
    ModbusModule(modbus_t *ctx, int addr);
    virtual ~ModbusModule();
    virtual void Read() {};
    virtual void Write() {};
    bool isError=true,isChanged=true;
    int error=0;
protected:
    bool oldIsError=false;
    int rc,olderror;
protected:
    void Set();
    modbus_t *ctx;
    int address;
};

Файл ModbusModule.cpp:

ModbusModule::ModbusModule(modbus_t *ctx, int addr) :
    ctx{ctx},address{addr} { }

ModbusModule::~ModbusModule() { }

void ModbusModule::Set() {
    modbus_flush(ctx);
    modbus_set_slave(ctx,address);
    nsleep(5);
}

А теперь давайте опишем само устройство на примере MDS DIO-16BD. Концепция заключается в том, чтобы передавать адреса переменных в которые нам надо получать значения или записывать данные в устройство. И другие потоки должны иметь доступ к этим переменным.

Файл MDS-DIO-16BD.h:

class MDS_DIO_16BD : public ModbusModule
{
public:
    MDS_DIO_16BD(modbus_t *ctx, int addr,
        volatile bool *rB1=NULL,volatile bool *rB2=NULL,volatile bool *rB3=NULL,volatile bool *rB4=NULL,
        volatile bool *rB5=NULL,volatile bool *rB6=NULL,volatile bool *rB7=NULL,volatile bool *rB8=NULL,
        volatile bool *rB9=NULL,volatile bool *rB10=NULL,volatile bool *rB11=NULL,volatile bool *rB12=NULL,
        volatile bool *rB13=NULL,volatile bool *rB14=NULL,volatile bool *rB15=NULL,volatile bool *rB16=NULL,
        volatile bool *wB1=NULL,volatile bool *wB2=NULL,volatile bool *wB3=NULL,volatile bool *wB4=NULL,
        volatile bool *wB5=NULL,volatile bool *wB6=NULL,volatile bool *wB7=NULL,volatile bool *wB8=NULL,
        volatile bool *wB9=NULL,volatile bool *wB10=NULL,volatile bool *wB11=NULL,volatile bool *wB12=NULL,
        volatile bool *wB13=NULL,volatile bool *wB14=NULL,volatile bool *wB15=NULL,volatile bool *wB16=NULL,
        volatile uint16_t *rC1=NULL,volatile uint16_t *rC2=NULL,volatile uint16_t *rC3=NULL,volatile uint16_t *rC4=NULL,
        volatile bool *wReset1=NULL,volatile bool *wReset2=NULL,volatile bool *wReset3=NULL,volatile bool *wReset4=NULL);
    ~MDS_DIO_16BD() override;
    void Write() override;
    void Read() override;
    void setRev(int index);


private:
    uint16_t *buffer;
    volatile bool *rB1,*rB2,*rB3,*rB4,*rB5,*rB6,*rB7,*rB8,*rB9,*rB10,*rB11,*rB12,*rB13,*rB14,*rB15,*rB16,*wB1,*wB2,*wB3,*wB4,*wB5,*wB6,*wB7,*wB8,*wB9,*wB10,*wB11,*wB12,*wB13,*wB14,*wB15,*wB16;
    volatile uint16_t *rC1,*rC2,*rC3,*rC4;
    volatile bool *wReset1,*wReset2,*wReset3,*wReset4;
    bool rev[16];
};

Файл MDS-DIO-16BD.cpp:

MDS_DIO_16BD::MDS_DIO_16BD(modbus_t *ctx, int addr,
    volatile bool *rB1,volatile bool *rB2,volatile bool *rB3,volatile bool *rB4,
    volatile bool *rB5,volatile bool *rB6,volatile bool *rB7,volatile bool *rB8,
    volatile bool *rB9,volatile bool *rB10,volatile bool *rB11,volatile bool *rB12,
    volatile bool *rB13,volatile bool *rB14,volatile bool *rB15,volatile bool *rB16,
    volatile bool *wB1,volatile bool *wB2,volatile bool *wB3,volatile bool *wB4,
    volatile bool *wB5,volatile bool *wB6,volatile bool *wB7,volatile bool *wB8,
    volatile bool *wB9,volatile bool *wB10,volatile bool *wB11,volatile bool *wB12,
    volatile bool *wB13,volatile bool *wB14,volatile bool *wB15,volatile bool *wB16,
    volatile uint16_t *rC1,volatile uint16_t *rC2,volatile uint16_t *rC3,volatile uint16_t *rC4,
    volatile bool *wReset1,volatile bool *wReset2,volatile bool *wReset3,volatile bool *wReset4)
    : rB1{rB1},rB2{rB2},rB3{rB3},rB4{rB4},rB5{rB5},rB6{rB6},rB7{rB7},rB8{rB8},
    rB9{rB9},rB10{rB10},rB11{rB11},rB12{rB12},rB13{rB13},rB14{rB14},rB15{rB15},rB16{rB16},
    wB1{wB1},wB2{wB2},wB3{wB3},wB4{wB4},wB5{wB5},wB6{wB6},wB7{wB7},wB8{wB8},
    wB9{wB9},wB10{wB10},wB11{wB11},wB12{wB12},wB13{wB13},wB14{wB14},wB15{wB15},wB16{wB16},
    rC1{rC1},rC2{rC2},rC3{rC3},rC4{rC4},
    wReset1{wReset1},wReset2{wReset2},wReset3{wReset3},wReset4{wReset4},ModbusModule(ctx,addr) {
    buffer = new uint16_t[16];
    memset(buffer, 0, 16 * sizeof(uint16_t));
    for(int i=0;i<16;i++) rev[i]=false;
}
MDS_DIO_16BD::~MDS_DIO_16BD() {
    delete[] buffer;
}

void MDS_DIO_16BD::Write() {
    uint16_t temp=0;
    if(wB1!=NULL||wB2!=NULL||wB3!=NULL||wB4!=NULL||wB5!=NULL||wB6!=NULL||wB7!=NULL||wB8!=NULL||
        wB9!=NULL||wB10!=NULL||wB11!=NULL||wB12!=NULL||wB13!=NULL||wB14!=NULL||wB15!=NULL||wB16!=NULL) {
        temp=((wB1==NULL)?0:rev[0]?!(*wB1<<0):(*wB1<<0))|
            ((wB2==NULL)?0:rev[1]?!(*wB2<<1):(*wB2<<1))|
            ((wB3==NULL)?0:rev[2]?!(*wB3<<2):(*wB3<<2))|
            ((wB4==NULL)?0:rev[3]?!(*wB4<<3):(*wB4<<3))|
            ((wB5==NULL)?0:rev[4]?!(*wB5<<4):(*wB5<<4))|
            ((wB6==NULL)?0:rev[5]?!(*wB6<<5):(*wB6<<5))|
            ((wB7==NULL)?0:rev[6]?!(*wB7<<6):(*wB7<<6))|
            ((wB8==NULL)?0:rev[7]?!(*wB8<<7):(*wB8<<7))|
            ((wB9==NULL)?0:rev[8]?!(*wB9<<8):(*wB9<<8))|
            ((wB10==NULL)?0:rev[9]?!(*wB10<<9):(*wB10<<9))|
            ((wB11==NULL)?0:rev[10]?!(*wB11<<10):(*wB11<<10))|
            ((wB12==NULL)?0:rev[11]?!(*wB12<<11):(*wB12<<11))|
            ((wB13==NULL)?0:rev[12]?!(*wB13<<12):(*wB13<<12))|
            ((wB14==NULL)?0:rev[13]?!(*wB14<<13):(*wB14<<13))|
            ((wB15==NULL)?0:rev[14]?!(*wB15<<14):(*wB15<<14))|
            ((wB16==NULL)?0:rev[15]?!(*wB16<<15):(*wB16<<15));
        buffer[1]=temp;
        Set();
        rc = modbus_write_registers(ctx, 267, 1, buffer+1);
        isError=(rc==-1);
        if (rc == -1)
            fprintf(stderr, "#%d MDS_DIO_16BD::Write(... %s\n", address, modbus_strerror(errno));
    }
    if(wReset1!=NULL||wReset2!=NULL||wReset3!=NULL||wReset4!=NULL) {
        temp=((wReset1==NULL)?0:(*wReset1<<0))|
            ((wReset2==NULL)?0:(*wReset2<<1))|
            ((wReset3==NULL)?0:(*wReset3<<2))|
            ((wReset4==NULL)?0:(*wReset4<<3));
        rc = modbus_write_register(ctx, 276, temp);
        isError&=(rc==-1);
        if (rc == -1)
            fprintf(stderr, "#%d MDS_DIO_16BD::Write(... %s\n",address, modbus_strerror(errno));
    }
}

void MDS_DIO_16BD::Read() {
    if(rB1!=NULL||rB2!=NULL||rB3!=NULL||rB4!=NULL||rB5!=NULL||rB6!=NULL||rB7!=NULL||rB8!=NULL||
        rB9!=NULL||rB10!=NULL||rB11!=NULL||rB12!=NULL||rB13!=NULL||rB14!=NULL||rB15!=NULL||rB16!=NULL) {
        Set();
        rc = modbus_read_registers(ctx, 258, 1, buffer);
        isError=(rc==-1);
        if (rc == -1)
            fprintf(stderr, "#%d MDS_DIO_16BD::Read() %s\n",address, modbus_strerror(errno));
        else {
            if(rB1!=NULL) *rB1=(buffer[0]>>0)&1;
            if(rB2!=NULL) *rB2=(buffer[0]>>1)&1;
            if(rB3!=NULL) *rB3=(buffer[0]>>2)&1;
            if(rB4!=NULL) *rB4=(buffer[0]>>3)&1;
            if(rB5!=NULL) *rB5=(buffer[0]>>4)&1;
            if(rB6!=NULL) *rB6=(buffer[0]>>5)&1;
            if(rB7!=NULL) *rB7=(buffer[0]>>6)&1;
            if(rB8!=NULL) *rB8=(buffer[0]>>7)&1;
            if(rB9!=NULL) *rB9=(buffer[0]>>8)&1;
            if(rB10!=NULL) *rB10=(buffer[0]>>9)&1;
            if(rB11!=NULL) *rB11=(buffer[0]>>10)&1;
            if(rB12!=NULL) *rB12=(buffer[0]>>11)&1;
            if(rB13!=NULL) *rB13=(buffer[0]>>12)&1;
            if(rB14!=NULL) *rB14=(buffer[0]>>13)&1;
            if(rB15!=NULL) *rB15=(buffer[0]>>14)&1;
            if(rB16!=NULL) *rB16=(buffer[0]>>15)&1;
        }
    }
    if(rC1!=NULL||rC2!=NULL||rC3!=NULL||rC4!=NULL) {
        rc = modbus_read_registers(ctx, 278, 4, buffer);
        isError&=(rc==-1);
        if (rc == -1)
            fprintf(stderr, "#%d MDS_DIO_16BD::Read() %s\n",address, modbus_strerror(errno));
        else {
            if(rC1!=NULL) *rC1=buffer[0];
            if(rC2!=NULL) *rC2=buffer[1];
            if(rC3!=NULL) *rC3=buffer[2];
            if(rC4!=NULL) *rC4=buffer[3];
        }
    }
}

void MDS_DIO_16BD::setRev(int index) {
    if(index<0 || index>15) return;
    rev[index]=!rev[index];
}

Порождающий паттерн будем использовать при создании класса опроса устройств на одной шине RS-485.

Файл ModbusLine.h:

class ModbusLine {
public:
    ModbusLine(modbus_t *ctx, long sleepTime=300);
    ~ModbusLine();
    MDS_DIO_16BD* addMDS_DIO_16BD(int address,
        volatile bool *rB1=NULL,volatile bool *rB2=NULL,volatile bool *rB3=NULL,volatile bool *rB4=NULL,
        volatile bool *rB5=NULL,volatile bool *rB6=NULL,volatile bool *rB7=NULL,volatile bool *rB8=NULL,
        volatile bool *rB9=NULL,volatile bool *rB10=NULL,volatile bool *rB11=NULL,volatile bool *rB12=NULL,
        volatile bool *rB13=NULL,volatile bool *rB14=NULL,volatile bool *rB15=NULL,volatile bool *rB16=NULL,
        volatile bool *wB1=NULL,volatile bool *wB2=NULL,volatile bool *wB3=NULL,volatile bool *wB4=NULL,
        volatile bool *wB5=NULL,volatile bool *wB6=NULL,volatile bool *wB7=NULL,volatile bool *wB8=NULL,
        volatile bool *wB9=NULL,volatile bool *wB10=NULL,volatile bool *wB11=NULL,volatile bool *wB12=NULL,
        volatile bool *wB13=NULL,volatile bool *wB14=NULL,volatile bool *wB15=NULL,volatile bool *wB16=NULL,
        volatile uint16_t *rC1=NULL,volatile uint16_t *rC2=NULL,volatile uint16_t *rC3=NULL,volatile uint16_t *rC4=NULL,
        volatile bool *wReset1=NULL,volatile bool *wReset2=NULL,volatile bool *wReset3=NULL,volatile bool *wReset4=NULL);
protected:
    void Thread();
    modbus_t *ctx;
    long sleepTime;
    std::thread _t;
    bool _stop=false;
    std::vector<ModbusModule*> devices;
};

Файл ModbusLine.cpp:

ModbusLine::ModbusLine(modbus_t *ctx, long sleepTime) : ctx{ctx},sleepTime{sleepTime} {
    _t=std::thread(&ModbusLine::Thread,this);
    printf("ModbusLine create\n");
}
ModbusLine::~ModbusLine() {
    _stop=true;
    _t.join();
    for(auto& dev:devices) delete dev;
    devices.clear();
    modbus_close(ctx);
    modbus_free(ctx);
    printf("ModbusLine delete\n");
}

void ModbusLine::Thread() {
    int isOpen=-1;
    auto olderrno=errno;
    while(!_stop) {
        if (isOpen==-1) {
            cout<<"Try open port..."<<endl;
            if ((isOpen = modbus_connect(ctx)) == -1) {
                if(errno!=olderrno) {
                    fprintf(stderr, "Connection failed: %s\n",modbus_strerror(errno));
                }
            }
            modbus_set_error_recovery(ctx,  (modbus_error_recovery_mode)6);
            modbus_set_response_timeout(ctx, 0,500);
            olderrno=errno;
            nsleep(1000);
            continue;
        }
        bool err=true;
        for(auto& dev:devices) {
            dev->Read();
            if(dev->isError==false) err=false;
            nsleep(10);
            dev->Write();
            if(dev->isError==false) err=false;
            nsleep(10);
        }
        if(err) {
            isOpen=-1;
        }
        nsleep(sleepTime);
    }
}

MDS_DIO_16BD* ModbusLine::addMDS_DIO_16BD(int address,
    volatile bool *rB1,volatile bool *rB2,volatile bool *rB3,volatile bool *rB4,
    volatile bool *rB5,volatile bool *rB6,volatile bool *rB7,volatile bool *rB8,
    volatile bool *rB9,volatile bool *rB10,volatile bool *rB11,volatile bool *rB12,
    volatile bool *rB13,volatile bool *rB14,volatile bool *rB15,volatile bool *rB16,
    volatile bool *wB1,volatile bool *wB2,volatile bool *wB3,volatile bool *wB4,
    volatile bool *wB5,volatile bool *wB6,volatile bool *wB7,volatile bool *wB8,
    volatile bool *wB9,volatile bool *wB10,volatile bool *wB11,volatile bool *wB12,
    volatile bool *wB13,volatile bool *wB14,volatile bool *wB15,volatile bool *wB16,
    volatile uint16_t *rC1,volatile uint16_t *rC2,volatile uint16_t *rC3,volatile uint16_t *rC4,
    volatile bool *wReset1,volatile bool *wReset2,volatile bool *wReset3,volatile bool *wReset4) {
    auto out = new MDS_DIO_16BD(ctx,address,rB1,rB2,rB3,rB4,
        rB5,rB6,rB7,rB8,rB9,rB10,rB11,rB12,rB13,rB14,rB15,rB16,
        wB1,wB2,wB3,wB4,wB5,wB6,wB7,wB8,wB9,wB10,wB11,wB12,wB13,wB14,wB15,wB16,
        rC1,rC2,rC3,rC4,wReset1,wReset2,wReset3,wReset4);
    devices.push_back((ModbusModule*)out);
    return out;
}

Получается, что описание шины RS-485 будет выглядеть следующим образом:

volatile bool b1=false,b2=false,b3=false;
ModbusLine line1(modbus_new_rtu("/dev/ttyS2", 115200, 'N', 8, 1));
line1.addMDS_DIO_16BD(1,&b1,&b2,&b3);

У нас на каждую линию будет создаваться отдельный поток и основное приложение не будет зависеть от опроса устройств. В переменных всегда будут находится актуальные параметры, за это у нас отвечает ключевое слово volatile которое информирует компилятор о том, что значение переменной может меняться извне и компилятор не будет кэшировать эту переменную.

В следующей статье мы посмотрим использование MQTT для обмена между приложениями на ПЛК и мнемосхемой.

Tags:
Hubs:
Total votes 11: ↑10 and ↓1+13
Comments36

Articles