Всем доброго времени суток. Я работаю на большом предприятии, занимаюсь автоматизацией производства. Очень захотелось с Вами поделиться наработками, которые мы используем в повседневной трудовой жизни. Не судите строго, это моя первая статья на большую аудиторию...
Если мы говорим о промышленной автоматизации, то понимаем, что программирование будет связано с языками стандарта МЭК 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 для обмена между приложениями на ПЛК и мнемосхемой.
