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

от автора

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

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

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

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

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

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

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

Я нашел контроллер Icp-Das LP-8×21 на 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 %MEMTIME+  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 для обмена между приложениями на ПЛК и мнемосхемой.


ссылка на оригинал статьи https://habr.com/ru/articles/762576/