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

Kate

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

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

 
Сверху