АСУ ТП

Подписчиков: 10     Сообщений: 4     Рейтинг постов: 45.9

песочница АСУ ТП программирование geek реактор образовательный длиннопост 

Часть 3, каркас архитектуры

Часть 1, введение
Часть 2, приоритеты и базовые сигналы

Мда, я дико извиняюсь за большое количество опечаток в предыдщих постах. Руки не поспевают за мыслью, а при редактировании потеряется часть оформления регулярно. Не зналь(

Имея на руках пример того, с чем предлагается работать, впору поговорить об архитектуре, которая и будет обеспечивать корректное и стабильное исполнение кода, однако, перед этим сделаем ряд важных оговорок:
1) концепт ориентирован на работу с жёстким распределением памяти (Siemens, OMRON CP/CJ series)
2) ввиду пункта 1 внутри одной структуры могут быть переменные которые И читаются, И пишутся, что существенно усложняет их проброс в коммуникацию(особенно тех, которые пишутся И с панели оператора, И из программы, например запуск в ручном режиме и сброс аварий в структуре управления)
3) объём памяти оперативность и для хранения кода, а также мощность CPU–считаются достаточно большими, чтобы не обращать на них внимания, в иных случаях требуется оптимизация кода
4) при работе с контроллерами, имеющими «классическое» распределение памяти, для обеспечения корректного управления моим кодом по ModBus RTU/TCP требуется дополнительная прослойка, которая будет пересобирать структуры в WORD и обратно, когда-нибудь я это исправлю, но явно не сейчас
5) вся концепция рассчитана на применение в обще-промышленной области, где потерять 10-20-30 мс роли не сыграет никакой, если вам нужна более высокая точность – добро пожаловать в чудный мир оптимизации и распределения кусков кода по разным циклам

Ещё одно лирическое отступление

Я осознанно не буду вам рассказывать как работает Промышленный Логический Контроллер (ПЛК), распределение его памяти, времени CPU по задачам системным и пользовательским, типы данных и прочую лабуду – это вы можете узнать на любых вводных курсах от любого производителя железа. Моя задача – показать пример прикладной реализации тех или иных задач.
Кстати, на ардуине в своё время использовал Union, который прекрасно решал проблему компоновки памяти, однако подавляющее большинство сред разработки для промышленного оборудования его или не поддерживают, или имеют кастрированную реализацию.

Генерация импульсов

Начнём с терминологии:
- импульс – это нечто, существующее предельно короткое время, в нашем случае сие ровно 1 цикл контроллера.

Большинство контроллеров предлагает свои системные генераторы импульсов в том или ином виде, но выглядят они следующим образом на примере 1 секунды: 

0.5 сек <-----►
<---->
0.5 сек
------>
время,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,реактор образовательный,длиннопост

Что это значит на практике? Предположим, время цикла ПЛК = 10 мс, тогда:

50 циклов <----->
<--->
50 циклов
<----------->
100 циклов
----->
циклы,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,реактор образовательный,длиннопост

Ёбушки воробушки, наш самописный таймер то проверяет когда сигнал = 1, а он равен 1 в течении 50 циклов, а за это время он насчитает 50 секунд, вместо 1! Лажа какая-то. Мы должны получить следующее:

о
1 цикл
циклы
>,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,реактор образовательный,длиннопост

Для этого есть несколько вариантов, и сначала мы отметём в сторону самый очевидный: мы НЕ будем в каждом таймере проверять фронт системного генератора импульсов, потому что каждая проверка фронта это +1 переменная в памяти ПЛК, а мы не хотим засирать его бесполезным хламом.
Итого остаётся:
1) вызывать системный импульс, проверять его передний или задний фронт и закидывать в pulse_1s;
2) вызвать стандартный таймер TON с длительностью 1 секунда и зациклить на самого себя, при срабатывании таймера взводить pulse_1s;
3) читать системное время и по внутренним часам ПЛК и при смене секунды однократно взводить pulse_1s, потребуется буфер для хранения и сравнения предыдущего времени;
4) прочитать из недр контроллера время предыдущего цикла, насуммировать несчастные наносекунды до 1 секунды и тоже записать в pulse_1s единичку, после чего очистить буфер.
5) ещё что-нибудь на ваше усмотрение.

Мне глубоко фиолетово, каким способом вы получаете в итоге pulse_1s, но вам достаточно получить его корректно 1 раз и все таймеры во всей программе сразу же заработают, причём корректно и, вау, синхронно. Т.е. если у вас в двух местах с разбегом менее 1 секунды начался счёт до 5, то закончится он тоже одновременно. В этом и плюс, и минус. Мы теряем точность. Но так ли она нужна?
Хотите точность до 100 мс? Пишем таймер Time_R для дробных значений генерируем pulse_01s.
Хотите точность до 10 мс? Ну… сделайте отдельный цикл ПЛК с такой частотой и там считайте. Или убедитесь, что ваше время цикла менее 10 мс на всю программу.

1
2
3
4
5
€
7
3
S
10
11
12
TYPE Time_R :	
STRUCT	
Start	:BOOL.
Q	:BOOL.
Pause	:BOOL.
Reset	:BOOL.
SP	:REÀL,
AP	:REAL,
One	:REÀL,
Percent	:REAL,
END_STRUCT	
END TYPE	
//вес импульса //прогресс выполнения,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный,


FUNCTION drv_Time_R : bool VAR_IN_OUT
T	:Time_R;
END_VAR VAR
END VAR
// Таймер с произвольной кратностью счёта T.One := 0.1;
IF T.Start AND NOT T.Reset THEN // Работа таймера IF T.AP < T.SP THEN
IF NOT T.Pause THEN
IF sys.pulse_01s THEN
T.AP := T.AP + T.One; END_IF;
END_IF;
T.Q :=

Обратите внимание, что этот таймер сложнее – мы считаем прогресс времени от 0 до 1,который можно использовать для вспомогательных операций (типа на 30% всего времени моргнуть правой пяткой) и красивого прогресс-бара на экране.

Архитектура

Перейдём к тому, ради чего был затеян данный раздел. Следите за руками…

1 цикл ПЛК,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,реактор образовательный,длиннопост

Где-то там приютилось ещё регулирование, типа ПИДов, но о них как-нибудь в другой раз.

На повестке дня вопрос распределения переменных по областям памяти. Что будет глобальным, а что локальным?

К глобальным относится всё, что ранее было названо глобальным, а также туда настоятельно рекомендуется выносить:
- все структуры управления драйверами
- все структуры параметров драйверами
- все структуры состояний драйверами
- общие параметры техпроцесса
- общее управление техпроцессом, типа включить/выключить установку

Из них в раздел энергонезависимой памяти попадают только структуры параметров. 

1
2
3
4
5
€
7
3
з
10
11
12
13
14
15
1€
17
13
13
20
21
22
23
24
25
2 €
27
23
23
30
31
32
VAR GLOBAL RETAIN
//Дискретные входа
DIx_cfg	¡ARRAY
//Дискретные вы:-:ода	
D0x_cfg	¡ARRAY
//Приводы с ЧП	
MDx_cfg	¡ARRAY
//Пневмоцилиндр	>ы
VDx_cfg	¡ARRAY
[0..10] OF

REQ – Request, запрос запуска. Сюда мы из авторежима будем писать команду для устройства, а затем скармливать драйверам в CMD.Start_A.
DONE – состояние устройств, если оно выключено или в аварии то соответствующий флаг =0, если успешно запущен = 1. Это хорошо заходит для минимизации алгоритма авторежима и всяких вспомогательных операций.

А что же тогда попадает в call’ы?Нерадивые любители звонков с автонабором? Увы, их неплохо бы там запереть, но места маловато. Там будут вызваны непосредственно экземпляры драйверов на исполнение со всей обвязкой. На примере уже рассмотренных дискретных входов это выглядит так… опс, в этом проекте их нет, вот вам выхода:


FUNCTION_BLOCK call_DO
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
DOx ¡ARRAY [0..10] OF drv_DO; END VAR
//Пневматика
//выталкиватель трубы из захвата dev := 0;
DOx[dev](
Signal := REQ.KC[0],
CHD := DOx_CHD[dev], cfg := DOx_cfg[dev],
Result => KC_0_CMD );
//выталкиватель трубы из

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

песочница geek АСУ ТП программирование реактор образовательный длиннопост 

Часть 2, приоритеты и базовые сигналы

Часть 1,введение (отправил в политоту из-за двусмысленных высказываний, ибо было сложно удержаться)


Приоритеты команды
Итак, вы решили взяться за благое дело и сэкономить пару дефицитных рупей на найме программиста и запилить всё своими руками. Что же для этого надо?
1) скачайте среду разработки
2) откройте среду разработки

3) закройте среду разработки и идите искать программиста

Для начала, давайте проясним одну вещь. То что я рассказываю не является аксиомой. Это концепция архитектуры, позволяющая сделать максимально гибкое и устойчивое решение, позволяющее безболезненно изменять различные прослойки кода внутри проекта, не роняя завод.

Итак, помните, я говорил что всё есть сигнал? Я наврал. Всё есть объект.
Вход - это объект.
Выход - это объект.
Бутылка - тоже объект, но более высокого порядка.

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

Вот всё что лежит до троеточия и будем рассматривать. Начнём с базиса, но перед этим - а кто здесь главный? Ты, я? оператор? директор? да пошёл ты в жопу, директор! Главная здесь ОНА:

песочница,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,АСУ ТП,программирование,реактор образовательный,длиннопост


Мне лень рисовать пирамидки маслоу, поэтому обойдёмся блок-схемами. ГОСТы - для слабаков! Главное, чтобы было понятно...

ооооо
Глобальные переменные
Внутренние переменные Управление из алгоритма
Управление с НМ1 Внешние сигналы
Ограничение уровней доступа на НМ1
[а] Администратор 0 Оператор [п~] Пользователь Без ограничений
Приоритетность команд
Приоритет управляющих сигналов и режимов работы от наивысшего


УХ, ебать! Но это лёгкий вариант, извините.

Глобальные переменные
Обратите внимание на "Глобальные переменные". Что в них входит? А всё, что проходит красной нитью через весь код и существует в единственном экземпляре:
- Аварийный останов
- Общий сброс аварий
- Общий режим эмуляции
- Общий перевод в автоматический режим всех устройств (очень полезно, если оператор любит пошарить ручками где надо и забывает что делал в недрах нашей системы)

Как оно выглядит в объявлении переменных? Вот так:

All_Block:BOOL; //аварийная остановка
All_Reset :BOOL; //сброс всех аварий
All_Sim :BOOL; //общий режим эмуляции
All_Auto :BOOL; //перевести всё в автоматический режим
sys :system; //системные переменные
sec :REAL; //длительность одной секунды

Обратите внимание, на две последние переменные. Это наша опора и поддержка, которую мы будем гонять вместе с первой четвёркой по всем проектам.
sec - длительность секунды в часах, да, я знаю что считать в REAL повышает нагрузку на CPU, но пардон, у вас мощности позволяют то в 2к2+ году
sys - структура системных переменных, которая собирает в себя базовые компоненты, реализация которых отличает у разных производителей:
- флаг TRUE
- флаг FALSE
- импульсы с разным весом, которые живут ровно 1цикл программы
- прочая мелочёвка

Опс, опять что-то новенькое. 1 цикл программы. Да-да, ваш код всегда исполняется от начала и до конца сверху вниз, слевана право. За всякие go_to я лично вырываю руки, ибо нехер.

Итого, на текущий момент у меня в sys лежит:

1
2
3
4
5
€
7
3
3
10
11
12
13
14
15
16
17
13
13
20
21
22
23
24
TYPE system :
STRUCT
pulse_01s	:BOOL,
pulse_ls	:BCDL,
xON	:BOOL,
xOFF	:BCXDL,
Hand_DI	:BOOL,
Hand_D0	:BCDL,
Hand_AI	:BOOL,
Hand_A0	:BOOL,
Hand_M	:BOOL,
Hand_V	:BOOL,
Hand_Axis	:BOOL,
Hand_Any	:BOOL,

Дану ёб твою мать, скажете вы, что это за херня? И будете правы!
- x - не потому что хуй, а потому что ON и OFF зарезервированы системой и их нельзя просто так применять;
- Hand - флаги, что хотя бы одно устройство данного типа переведено в ручной режим;
- Alm - флаги, что хоть одно устройство данного типа находится в аварии;
- Any - сборный флаг для всех флагов, привязанных к типам устройств;
- V - Valve, а не то что вы подумали. Клапан, заслонка, задвижка;
- M - не мудак, к сожалению, а Motor. Вентилятор, транспортёр, всё что вращается - всё Motor. Я иногда делю на MD и MDA, дикрестное и дискретно-аналоговое управления, но это личные заморочки, не более;
- DTL - DataTimeL.. хз что за L, типовая структура формата год-месяц-дата-час-минута-секунда, которую мы будем использовать при работе с расписанием и всем, что требует проверки системного времени.

Ну наконец-то! А где код-то? Хрена вам, а не код. Рано.
На очереди ещё одна структура, которая будет всегда и везде, обеспечивая работу вон той страшной картинки наверху, обычно я называю её CMD:

1
2
3
4
S
€
7
3
s
10
11
12
TYPE CMD : STRUCT

	Start_A	:BOOL,
	Start_M	:BOOL.
	Hand	:BOOL.
	Lock	:BOOL.
	Force	:BOOL.
	Vector	:BOOL.
	ResetOp	:BOOL.
	Reset	:BOOL,
END_	STRUCT	
END	TYPE	
//управление в автоматическом режиме //управление в ручном режиме
//режим управления, 0


УправлениеУра! Наконец-то! Мы добрались! И теперь...

FUNCTION drv_CMD : BOOL VAR_INPUT
Alarm :BOOL; END_VAR VAR_IN_OUT
CHD :CHD; END_VAR VAR
END VAR
// Опредление режима работы драйвера IF NOT All_Block THEN
//He принудительный режим IF NOT CMD.Force THEN
IF NOT CHD.Lock AND NOT Alarm THEN //Автоматический режим IF NOT CHD.Hand THEN

песочница,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,АСУ ТП,программирование,реактор образовательный,длиннопост

Видите вон там странный Alarm, которого раньше не было? Видите? И он есть!
А всё почему? Да потому что...
1) мы делаем универсальный код, который будет применим для всего и вся
2) программа - это не только программа, это ещё функциональный блоки функции

В чём же разница? Если кратко - функциональный блок может иметь свои внутренние, приватные, переменные, а функции - работает только с внешними и временными.
СтопЭ. временные и внутренние - какая в пень разница?
Внутренняя - сохраняет своё значения при переходе на новый цикл программы.
Временная - теряет своё значение может содержать случайную величину при переходе на новый цикл программы.
Таким образом, если вы внутри функции делаете А+Б=С и по значению С принимаете решение КАЖДЫЙ раз вызывая функцию, то можно использовать временную. А если делаете А+Б = С один раз и больше к этому не возвращаетесь, только читая С, то нужна внутренняя переменная. И опять таки... если вы хотите сохранить значение С при перезапуске контроллера - будьте добрый, сделать её энергонезависимой, т.е. Retain.

Вернёмся к Alarm. Для функциональных блоков (FB) и функций (FC) есть следующие виды переменных:
- входные - можно только читать
- выходные - можно только записывать
- входные-выходные - как тугая попка трапа, можно и читать и изменять значение
Это очень важно, когда у вас одна и та же внешняя переменная передаётся разныеFC/FB а вы сидите и хлопаете глазками, почему значение теряется посреди программы. Да потому что вы проебались и привязали не к тому типу. Наиболее наглядно сие видно в графических языках:

"«•оссл/и^рв1* "SseLCChjOt f eFE " n9Zjr*Lbu*m wrCAetualJ5p««dr
—	1ч» a Lu ii On
—Switch OÍÍ “
—	P9ÜUT« .Actuad Sp«M»
“Pebiule
"Eag¿.ae*
Huyauc_Or.
— rK Oa'
Prc5C5_3í>eed_?.síic "pl!_Pr**et_Sp*ed ? bsd	" -eftohftd*
SBC,песочница,geek,Прикольные гаджеты. Научный, инженерный и  айтишный

Слева- входные (input) и входно-выходные (input-output) переменные, справа -выходные.

Фух, разобрались, теперь вызовем нашу страшную функцию:

2S
30
31
// Управлявшее слово
drv_CMD(Alarm := stare.Alarm, CMD := CMD); IF CMD.Hand THEN sys.Hand_V := TRUE; END IF,песочница,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,АСУ ТП,программирование,реактор образовательный,длиннопост


Драйвера, устройства и состояния
Расширим очко овертона терминологию: драйвер. Нет, это не виндовый драйвер. Но близко. Это некий алгоритм и набор переменных, описывающий работу базового или типового устройства. Входа, выхода, насоса, клапана. Оно беспечивает автомномный контроль:
- безопасности
- аварий
- ручного и автоматического управления
- наработки
Включает в себя все необходимые для жизни параметры и настройки, коих дофига и больше. Но это мы рассмотрим потом. Или не рассмотрим. Если микроскоп вдруг сломается.

Выглядит управление дискретным входом следующим образом:

ооооо
Глобальные переменные	Ограничение уровней доступа на НМ1	Сигнал	
Внутренние переменные	[а] Администратор	0 У	BOOL = неопределён
Управление из алгоритма	0 Оператор	0 У	BOOL = TRUE
Управление с НМ1	(ГГ) Пользователь	0 У	BOOL = FALSE
Внешние сигналы	Без ограничений	0 У	Значение
Дискретный


Итак, для работы драйвера нам необходимы:
- CMD - структура команд, рассмотрена выше
- cfg - структура параметров
- state - структура состояний

Дискретный вход слишком прост и отдельный набор состояний ему не нужен. Состояния включают в себя коды аварий, подсчёт наработки, вспомогательные плюшки для удобства отображения оператору "что тут происходит, мамочки, почему оно встало?!"

TYPE cfg_DI :
STRUCT
use_NC	:BOOL;	//выбор	типа сигнала, 0 - нормально открытый, 1 -	нормально закрытый
T_ON	:INT;	//время	фильтрации дребезга на включение сигнала
T_OFF	:INT;	//время	фильтрации дребезга на выключение сигнала
END_STRUCT END TYPE,песочница,geek,Прикольные гаджеты. Научный,

use_NC...
NC - нормальной закрытый сигнал
NO - нормально открытый сигнал
Например, у вас есть дверь. Стоит датчик, контролирующий что "дверь закрыта". 

Есликогда она закрыта сигнал = 0, а при открытии двери сигнал = 1, это нормально открытый сигнал.
Если когда она закрыта сигнал = 1, а при закрытии двери сигнал = 0, то это нормально закрытый сигнал.

Применение того или иного типа сигнала должно быть продиктовано соображениями безопасности. Например, аварийный стоп - должен быть нормально закрытым сигналом. Есть сигнал - всё хорошо, пропал - всё плохо. Почему так? Потому что Вася уронил топор на кабель и сигнал пропал - вы должны отключить всю систему, чтобы бедного Васю не намотало, превратив в инвалида пожизненно. Да-да, безопасность Васи - Ваша ответственность!

А если это не критичный сигнал, требующий контроля целостности сигнальной линии, то можно смело ставить нормально открытый датчик. Но это теория, а на практике ныне - что найдёте, то и поставите. Сорян.

УУУ, сука, сколько буков то! А это только начало! И я не ответил на главный вопрос - на кой хер нам вообще различать на уровне драйвера тип сигнала? В алгоритме основном поправим, да и делов то. НЕТ! Хватит! Не усложняйте себе жизнь!

Вам, как программисту, должно быть глубочайше насрать какой там тип датчика -сработал это всегда = 1, не сработал это всегда = 0. И именно эту задачу унификации решает драйвер дискретного входа. УНИ-ФИ-КА-ЦИИ, а не загрузки процессорного времени бесполезным хламом. Так менеджеру и ответите, что вы ускоряете дальнейший цикл разработки, ага.

ТаймерыАвтор, ты заебал, где код драйвера? Да вот он, только работать он у вас не будет:

1
2
3
4
5
€
7
3
5
10
11
12
13
14
15
FUNCTION BLOCK drv DI
VAR_INPÜT Signal ENDJVAR VÄR_IN_OUT	:BOOL;
CMD cfg END_VAR VÄR_OUTPUT	:CHD; :cfg_DI;
Result END_VÄR VAR	:BOOL;
Clock	:ARRAY [0..1] OF Time_S;
END VAR,песочница,geek,Прикольные гаджеты. Научный, инженерный и  айтишный

1
2
3
4
5
€
7
3
5
10
11
12
13
14
15
16
17
13
13
20
21
22
23
24
25
2€
27
23
23
30
31
32
33
34
35
3€
37
33
35
40
41
42
43
44
45
4€
47
43
45
50
51
52
53
54
55
5€
57
// ДИСКРЕТНЫЙ ВХОД
// Сигналы от датчиков, например:
//	- уровень
//	-

А почему - угадаете? Не, вы не тупые, вы умные. Просто я хитрожопый и вместо системных таймеров использую самописные. Нахуа-хуа? Да потому что системные кривые. И их мало. И каждый системный таймер можно использовать только один раз(если они аппаратные, как в старых ПЛК).
Почему использовать системный таймеры, это грех:
- если вы измените во время работы таймера уставку времени и она окажется меньше, чем уже прошедшее время с момента запуска таймера, то он встанет раком
- нет паузы
- нет сброса
- нет контроля % отсчитанного времени
- и, самое главное, время в формате Time, которое очень неудобно выводить на панель оператора
Самописный таймер решает все эти задачи. Как вы уже догадались, там потребуется структура переменных для него и FC, вот они для самого простого таймера, который используется в драйвере выше:

1	ТУРЕ Т1те_3	:		
2	БТЮТСТ			
3	Бгагг	:ВООЬ;	//управление таймером	
4	0	:ВООЬ;	//выход таймера	
8	Раизе	:ВООЬ;	//пауза	
€	Иезег	:ВООЬ;	//сброс текущего времени	
7	БР	:ШГ;	//ЗебРотпб - уставка, время, которое надо отсчитать	запуска таймер^
8	АР	:ЮТ;	//ActualPoint - аутальное время, которое


FUNCTION drv_Time_S : bool VAR_IN_OUT
T	:|liine_S;
END_VAR VAR
END VAR
// Таймер с краткостью 1 секунда IF T.Start AND NOT T.Reset THEN // Работа таймера IF T.AP < T.SP THEN
IF NOT T.Pause AND sys.pulse_ls THEN T.AP := T.AP + 1;
END_IF;
T.Q := FALSE;
ELSE
// Заданное время достигнуто


И весь этот охреневший объём кода вам нужен в промышленном контроллере только для того, что проверить нажатие кнопочки или срабатывание маленького геркончика. Очень мило, не правда ли?)

Благодарю за терпение, забыл важную картинку и не смог отредактировать пост. В прошлом варианте поста были вопрос на тему, почему pulse_01s и pulse_1s имеют тип BOOL и как вообще эта мерзость должна работать. Не зря в начале было сказано об архитектуре, именно её мы затронем третьей части историй после кружечки кофе и предложим своё видение "как оно должно работать", что, естественно, не претендует на истину и даже не носит рекомендательного характера, потому что код каждого программиста индивидуален и уникален, но если мы сможем писать хотя бы так, чтобы сосед слева мог его прочитать - это уже хорошо.

Развернуть

песочница АСУ ТП программирование geek автоматика реактор образовательный удалённое 

Часть 2, приоритеты и базовые сигналы

Часть 1, введение (улетел в политоту из-за двусмысленных высказываний, было сложно удержаться)

Приоритеты команды
Итак, вы решили взяться за благое дело и сэкономить пару дефицитных рупей на найме программиста и запилить всё своими руками. Что же для этого надо?
1) скачайте среду разработки
2) откройте среду разработки
3) закройте среду разработки и идите искать программиста

Для начала, давайте проясним одну вещь. То что я рассказываю не является аксиомой. Это концепция архитектуры, позволяющая сделать максимально гибкое и устойчивое решение, позволяющее безболезненно изменять различные прослойки кода внутри проекта, не роняя завод.

Итак, помните, я говорил что всё есть сигнал? Я наврал. Всё есть объект.
Вход - это объект.
Выход - это объект.
Бутылка - тоже объект, но более высокого порядка.

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

Вот всё что лежит до троеточия и будем рассматривать.
Начнём с базиса, но перед этим - а кто здесь главный? Ты, я? оператор? директор? да пошёл ты в жопу, директор! Главная здесь ОНА:

песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,реактор образовательный,удалённое



Мне лень рисовать пирамидки маслоу, поэтому обойдёмся блок-схемами. ГОСТы - для слабаков! Главное, чтобы было понятно...

ооооо
Глобальные переменные
Внутренние переменные Управление из алгоритма
Управление с НМ1 Внешние сигналы
Ограничение уровней доступа на НМ1
[а] Администратор 0 Оператор [п~] Пользователь Без ограничений
Приоритетность команд
Приоритет управляющих сигналов и режимов работы от наивысшего


УХ, ебать! Но это лёгкий вариант, извините.

Глобальные переменные
Обратите внимание на "Глобальные переменные". Что в них входит? А всё, что проходит красной нитью через весь код и существует в единственном экземпляре:
- Аварийный останов
- Общий сброс аварий
- Общий режим эмуляции
- Общий перевод в автоматический режим всех устройств (очень полезно, если оператор любит пошарить ручками где надо и забывает что делал в недрах нашей системы)


Как оно выглядит в объявлении переменных? Вот так:

All_Block :BOOL; //аварийная остановка
All_Reset :BOOL; //сброс всех аварий
All_Sim :BOOL; //общий режим эмуляции
All_Auto :BOOL; //перевести всё в автоматический режим
sys :system; //системные переменные
sec :REAL; //длительность одной секунды

Обратите внимание, на две последние переменные. Это наша опора и поддержка, которую мы будем гонять вместе с первой четвёркой по всем проектам.
sec - длительность секунды в часах, да, я знаю что считать в REAL повышает нагрузку на CPU, но пардон, у вас мощности позволяют то в 2к2+ году
sys - структура системных переменных, которая собирает в себя базовые компоненты, реализация которых отличает у разных производителей:
- флаг TRUE
- флаг FALSE
- импульсы с разным весом, которые живут ровно 1 цикл программы
- прочая мелочёвка

Опс, опять что-то новенькое. 1 цикл программы. Да-да, ваш код всегда исполняется от начала и до конца сверху вниз, слева направо. За всякие go_to я лично вырываю руки, ибо нехер.

Итого, на текущий момент у меня в sys лежит:

1
2
3
4
5
€
7
3
3
10
11
12
13
14
15
16
17
13
13
20
21
22
23
24
TYPE system :
STRUCT
pulse_01s	:BOOL,
pulse_ls	:BCDL,
xON	:BOOL,
xOFF	:BCXDL,
Hand_DI	:BOOL,
Hand_D0	:BCDL,
Hand_AI	:BOOL,
Hand_A0	:BOOL,
Hand_M	:BOOL,
Hand_V	:BOOL,
Hand_Axis	:BOOL,
Hand_Any	:BOOL,


Да ну ёб твою мать, скажете вы, что это за херня? И будете правы!
- x - не потому что хуй, а потому что ON и OFF зарезервированы системой и их нельзя просто так применять;
- Hand - флаги, что хотя бы одно устройство данного типа переведено в ручной режим;
- Alm - флаги, что хоть одно устройство данного типа находится в аварии;
- Any - сборный флаг для всех флагов, привязанных к типам устройств;
- V - Valve, а не то что вы подумали. Клапан, заслонка, задвижка;
- M - не мудак, к сожалению, а Motor. Вентилятор, транспортёр, всё что вращается - всё Motor. Я иногда делю на MD и MDA, дикрестное и дискретно-аналоговое управления, но это личные заморочки, не более;
- DTL - DataTimeL.. хз что за L, типовая структура формата год-месяц-дата-час-минута-секунда, которую мы будем использовать при работе с расписанием и всем, что требует проверки системного времени.

Ну наконец-то! А где код-то? Хрена вам, а не код. Рано.
На очереди ещё одна структура, которая будет всегда и везде, обеспечивая работу вон той страшной картинки наверху, обычно я называю её CMD:

1
2
3
4
S
€
7
3
s
10
11
12
TYPE CMD : STRUCT

	Start_A	:BOOL,
	Start_M	:BOOL.
	Hand	:BOOL.
	Lock	:BOOL.
	Force	:BOOL.
	Vector	:BOOL.
	ResetOp	:BOOL.
	Reset	:BOOL,
END_	STRUCT	
END	TYPE	
//управление в автоматическом режиме //управление в ручном режиме
//режим управления, 0



Управление
Ура! Наконец-то! Мы добрались! И теперь...


FUNCTION drvjCMD : BOOL VAR_INPUT
Alarm :BOOL; END_VAR VAR_IN_OUT
CHD :mt_CHD; END_VAR VAR
END VAR
// Опредление режима работы драйвера IF NOT All_Block THEN
//He принудительный режим IF NOT CMD.Force THEN
IF NOT CHD.Lock AND NOT Alarm THEN //Автоматический режим IF NOT CHD.Hand THEN
CHD.


песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,реактор образовательный,удалённое

Видите вон там странный Alarm, которого раньше не было? Видите? И он есть!
А всё почему? Да потому что...
1) мы делаем универсальный код, который будет применим для всего и вся
2) программа - это не только программа, это ещё функциональный блоки функции

В чём же разница? Если кратко - функциональный блок может иметь свои внутренние, приватные, переменные, а функции - работает только с внешними и временными.
СтопЭ. временные и внутренние - какая в пень разница?
Внутренняя - сохраняет своё значения при переходе на новый цикл программы.
Временная - теряет своё значение может содержать случайную величину при переходе на новый цикл программы.
Таким образом, если вы внутри функции делате А+Б=С и по значению С принимаете решение КАЖДЫЙ раз вызывая функцию, то можно использовать временную. А если делаете А+Б = С один раз и больше к этому не возвращаетесь, только читая С, то нужна внутренняя переменная. И опять таки... если вы хотите сохранить значение С при перезапуске контроллера - будьте добрый, сделать её энергонезависимой, т.е. Retain.

Вернёмся к Alarm. Для функциональных блоков (FB) и функций (FC) есть следующие виды переменных:
- входные - можно только читать
- выходные - можно только записывать
- входные-выходные - как тугая попка трапа, можно и читать и изменять значение
Это очень важно, когда у вас одна и та же внешняя переменная передаётся разные FC/FB а вы сидите и хлопаете глазками, почему значение теряется посреди программы. Да потому что вы проебались и привязали не к тому типу. Наиболее наглядно сие видно в графических языках:

"«•оссл/и^рв1* "SseLCChjOt f eFE " n9Zjr*Lbu*m wrCAetualJ5p««dr
—	1ч» a Lu ii On
—Switch OÍÍ “
—	P9ÜUT« .Actuad Sp«M»
“Pebiule
"Eag¿.ae*
Huyauc_Or.
— rK Oa'
Prc5C5_3í>eed_?.síic "pl!_Pr**et_Sp*ed ? bsd	" -eftohftd*
SBC,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный,

Слева - входные (input) и входно-выходные (input-output) переменные, справа - выходные.

Фух, разобрались, теперь вызовем нашу страшную функцию:

2S
30
31
// Управлявшее слово
drv_CMD(Alarm := stare.Alarm, CMD := CMD); IF CMD.Hand THEN sys.Hand_V := TRUE; END IF,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,реактор образовательный,удалённое


Драйвера, устройства и состояния
Расширим очко овертона терминологию: драйвер. Нет, это не виндовый драйвер. Но близко. Это некий алгоритм и набор переменных, описывающий работу базового или типового устройства. Входа, выхода, насоса, клапана. Он обеспечивает автомномный контроль:
- безопасности
- аварий
- ручного и автоматического управления
- наработки
Включает в себя все необходимые для жизни параметры и настройки, коих дофига и больше. Но это мы рассмотрим потом. Или не рассмотрим. Если микроскоп вдруг сломается.

Итак, для работы драйвера нам необходимы:
- CMD - структура команд, рассмотрена выше
- cfg - структура параметров
- state - структура состояний

Дискретный вход слишком прост и отдельный набор состояний ему не нужен. Состояния включают в себя коды аварий, подсчёт наработки, вспомогательные плюшки для удобства отображения оператору "что тут происходит, мамочки, почему оно встало?!"

TYPE cfg_DI :
STRUCT
use_NC	:BOOL;	//выбор	типа сигнала, 0 - нормально открытый, 1 -	нормально закрытый
T_ON	:INT;	//время	фильтрации дребезга на включение сигнала
T_OFF	:INT;	//время	фильтрации дребезга на выключение сигнала
END_STRUCT END TYPE,песочница,АСУ

use_NC...
NC - нормальной закрытый сигнал
NO - нормально открытый сигнал
Например, у вас есть дверь. Стоит датчик, контролирующий что "дверь закрыта". 

Если когда она закрыта сигнал = 0, а при открытии двери сигнал = 1, это нормально открытый сигнал.
Если когда она закрыта сигнал = 1, а при закрытии двери сигнал = 0, то это нормально закрытый сигнал.

Применение того или иного типа сигнала должно быть продиктовано соображениями безопасности. Например, аварийный стоп - должен быть нормально закрытым сигналом. Есть сигнал - всё хорошо, пропал - всё плохо. Почему так? Потому что Вася уровнил топор на кабель и сигнал пропал - вы должны отключить всю систему, чтобы бедного Васю не намотало, превратив в инвалида пожизненно. Да-да, безопасность Васи - Ваша ответственность!

А если это не критичный сигнал, требующий контроля целостности сигнальной линии, то можно смело ставить нормально открытый датчик. Но это теория, а на практике ныне - что найдёте, то и поставите. Сорян.

УУУ, сука, сколько буков то! А это только начало! И я не ответил на главный вопрос - на кой хер нам вообще различать на уровне драйвера тип сигнала? В алгоритме основном поправим, да и делов то. НЕТ! Хватит! Не усложняйте себе жизнь!

Вам, как программисту, должно быть глубочайше настрать какой там тип датчика - сработал это всегда = 1, не сработал это всегда = 0. И именно эту задачу унификации решает драйвер дискретного входа. УНИ-ФИ-КА-ЦИИ, а не загрузки процессорного времени бесполезным хламом. Так менеджеру и ответите, что вы ускоряете дальнейший цикл разработки, ага.

Таймеры
Автор, ты заебал, где код драйвера? Да вот он, только работать он у вас не будет:

FUNCTION BLOCK drv DI
VAR_INPÜT Signal ENDJVAR VÄR_IN_OUT	:BOOL;
CMD cfg END_VAR VÄR_OÜTPUT	:mt_CHD; :cfg_DI;
Result END_VÄR VAR	:BOOL;
Clock	:ARRAY [ 0.. 1] OF mt_Time_S
END VAR,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,реактор

1
2
3
4
5
€
7
3
5
10
11
12
13
14
15
16
17
13
13
20
21
22
23
24
25
2€
27
23
23
30
31
32
33
34
35
3€
37
33
35
40
41
42
43
44
45
4€
47
43
45
50
51
52
53
54
55
5€
57
// ДИСКРЕТНЫЙ ВХОД
// Сигналы от датчиков, например:
//	- уровень
//	-

А почему - угадаете? Не, вы не тупые, вы умные. Просто я хитрожопый и вместо системных таймеров использую самописные. Нахуа-хуа? Да потому что системные кривые. И их мало. И каждый системный таймер можно использовать только один раз (если они аппаратные, как в старых ПЛК).
Почему использовать системный таймеры, это грех:
- если вы измените во время работы таймера уставку времени и она окажется меньше, чем уже прошедшее время с момента запуска таймера, то он встанет раком
- нет паузы
- нет сброса
- нет контроля % отсчитанного времени
- и, самое главное, время в формате Time, которое очень неудобно выводить на панель оператора

Самописный таймер решает все эти задачи. Как вы уже догадались, там потребуется структура переменных для него и FC, вот они для самого простого таймера, который используется в драйвере выше:

1	ТУРЕ Т1те_3	:		
2	БТЮТСТ			
3	Бгагг	:ВООЬ;	//управление таймером	
4	0	:ВООЬ;	//выход таймера	
8	Раизе	:ВООЬ;	//пауза	
€	Иезег	:ВООЬ;	//сброс текущего времени	
7	БР	:ШГ;	//ЗебРотпб - уставка, время, которое надо отсчитать	запуска таймер^
8	АР	:ЮТ;	//ActualPoint - аутальное время, которое


FUNCTION drv_Time_S : bool VAR_IN_OUT
T	:|liine_S;
END_VAR VAR
END VAR
// Таймер с краткостью 1 секунда IF T.Start AND NOT T.Reset THEN // Работа таймера IF T.AP < T.SP THEN
IF NOT T.Pause AND sys.pulse_ls THEN T.AP := T.AP + 1;
END_IF;
T.Q := FALSE;
ELSE
// Заданное время достигнуто


И весь этот охреневший объём кода вам нужен в промышленном контроллере только для того, что проверить нажатие кнопочки или срабатывание маленького геркончика. Очень мило, не правда ли?)

Развернуть

песочница АСУ ТП программирование geek автоматика напитки завод политика длиннопост реактор образовательный 

Часть 1, введение

Часть 2, приоритеты и базовые сигналы
Часть 3, каркас архитектуры

Последнее время свербит в одном месте сформулировать мысли, но цэ не очень уместно порою. Посему отвлечёмся на то, как работает маленький, но важный, кусочек нашего мира.


Почему здесь? 
Да просто Хабросообщество асушников это унылые токсики, сующие минусы в карму по поводу и без, любящие брать мануалы и копировать как статью или ноу-хау... меня бесит, когда на профильном хабе тебе с умным видом изливают написанное в F1 да ещё обижаются, когда указываешь на сей момент.

О чём речь?
Итак, вот перед вами бутылка. Нет, пластиковая. Хотя любая сойдёт. Но посмотрим на пластик... ну, это могла бы быть кока-кола, но "почему-то" оказался дюшес. Прежде чем попасть к вам на стол, она была в магазине, на складе, в фуре, на складе, в контейнере, фуре... о, вот она, вышла со склада завода. Однако, чтобы туда попасть - её должны были сперва изготовить и налить продукт-с. О том, как она родилась и пойдёт речь. Технология будет чисто для примера и тайны никакой не представляет из себя уже дофига лет (состав напитка не в счёт).

Заебись, всем спасибо, пока.
Шутка.

Когда эти ампулы изготовлены, они отправляются на завод производства напитков. Например, вот рекламный ролик производителя линий розлив с внятным описанием технологии и что для чего используется.



Итак, мы ознакомились с тем как оно выглядит со стороны и пора заглянуть под капот.

Шкафы управления

Шкаф, сука, управления. Управления чем? А вон всей той хернёй, которая двигалась на роликах выше. Всё что шевелится - управляется из ШУ, а что не шевелится - жалкими смертными, вроде нас с вами. Их ещё "операторами" называют. А над ними стоят "технологи", которые должны разбираться в куче страшных циферок на тех маленьких экранчиках, что мелькали в видосиках, и правильно их настраивать. Одна ошибка - и ты ошибся. Спалил движок, погнул валы, испортил партию продукта.

Что живёт в шкафу?
Кратко:


Подробно:

) -m NEU ТО OI
(t. T ?
V
»«•»и,песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,напитки,завод,политика,политические новости, шутки и мемы,длиннопост,реактор образовательный


Контроллер
Поскольку я не проектировщик, хоть и могу разобраться в схеме и даже от руки нарисовать, а сраный неИТ-шный программист, то и интересует меня лишь одна часть: МОООЗГИИИ... оу, у кого там от зомбицида лекарство? Нет, давайте без топора обойдёмся, пожалуйста.

Мозги бывают разные: серые, белые, красные...

песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,напитки,завод,политика,политические новости, шутки и мемы,длиннопост,реактор образовательный

песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,напитки,завод,политика,политические новости, шутки и мемы,длиннопост,реактор образовательный

песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,напитки,завод,политика,политические новости, шутки и мемы,длиннопост,реактор образовательный

Но главнее всех - желтые:

песочница,АСУ ТП,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,автоматика,напитки,завод,политика,политические новости, шутки и мемы,длиннопост,реактор образовательный


Жёлтые - это, мать её, безопасность. А знаете сколько раз за мои 12 лет работы я видел их российских ШУ? Угадаете? Правильно. 0 ёбаных раз. Клали в эрафийском общепроме на ёбаную безопасность. Иногда ставят некое реле безопасности, но подключают его так, что лучше бы не ставили вообще, инвалиды безрукие... Может кто-то где-то и использует их, может даже правильно подключает, я за них искренне рад. Но когда в следующей схеме я увижу это дерьмо на текущей работе - мата будет много. Мат - двигатель процесса.

Пробежимся слегка по производителям железа, их особенностям и нюансам.

Siemens - божественный нектар, услада глаз моих...
# Имеет понтовые линейки с кодом "400", которые ставят атомщики и нефтяники. Горячий аппаратный резерв - это к ним.
# Линейка "300" - ...у тракториста. Уверенный середнячок, который уже лет пять пытаются снять с производства, но выходит как-то не очень. Слишком много их продали в 2000-х годах по всему миру.
# Линейка 1500 - пришла на смену 300 и 400, имеет монстров, способных в одну харю утянуть хороших размеров завод, но, сами понимаете - одна ошибка и весь завод ошибся. Не надо так.
# Линейка 1200 - самый ходовой продукт... был. Их вы видели в видео про шкафы.
=> языки: LAD, SFC, FBD, STL, SCL, Graph, ещё какая-то новая херня для очень тупых вышла в 2021 году, но я её не запомнил
=> среда разработки: закрытый проприентарный пакет Step7, WinCC, объединённый в новый TIA Portal
+ единственный в своём роде с полностью свободным доступом ко всей памяти внутри ПЛК, что позволяет творить офигенные вещи и очень сильно оптимизировать код
+ огромный форум со всеми вопросами и ответами, отличная документация, стабильный как кирпич, пока не ёбнешь молотком на 220
+ единственный в своём роде, кто позволяет загружать программу большими кусками без остановки контроллера (у остальных есть "нюансы" или ограничения)
- закрытая среда разработки, специфичный синтаксис с куче # и ", из-за чего прямой перенос кода на другие системы невозможен, всё ручками, будьте добры
$$$ дорого, но кря кря, 30 дней триала

ОВЕН - для диспетчеризации и садомазохистов
# Имеет широкое распространение среди любителей сэкономить
=> языки: LAD, FDB, CFC, ST
=> среда разработки базируется на CoDeSYS, что обеспечивает хорошую совместимость с большой кодовой базой, пока вам не нужно что-то специфичное
+ . . .
- их тоже больше не будет
- за его цену вы можете взять siemens 1200 и не знать горя
- теряет программу, слабый проц, мало памяти, нет внутренней шины для модулей ввода-вывода
- документация, техподдержка? не, не слышали
$$$ бесплатно, как сыр в мышеловке

OMRON - японцы, которым запретили хентай, решили поебаться сами с собой
# Старая линейка CJ/CP, которая что-то может, но лучше бы вам говнокодить, иначе не вывезет
# Новые линейки, в которых чёрт ногу сломит, много решений заточенных под координатное управление и перемещение
=> языки: LAD, FDB, ST
=> среда разработки: закрытый проприентарный пакет CX-Programmer и SYSMAC



+ оно шевелится и шевелится хорошо, только надо сперва придрочиться
- слабая документация
$$$ дорого, но кря

B&R - когда-то я считал, что хуже овна ничего быть не может...
# Большой спектр решений, много ОЕМ продукции
=> языки: LAD, ST, C
=> среда разработки: закрытый проприентарный пакет B&R Automation
+ хорошо умеют себя продавать
+ поддерживают С
- ошибки компилятора памяти
- программа управления может повредить ОС контроллера
- нельзя сохранить исходник в контроллере или скачать обратно загруженный код и отредактировать, нет проекта - нет проекта
$$$ 30 дней и плати

Shneider Electric - ваша головная боль и ваш бич, когда вы ловите ошибку, которой нет в документации
# Дичайший зоопарк железа и сред разработки, намешана прорва Legacy в одну кучу
=> языки: LAD, FDB, ST
=> среда разработки базируется на CoDeSYS, что обеспечивает хорошую совместимость с большой кодовой базой, пока вам не нужно что-то специфичные
+ большое количество фирменных библиотек для всей линейки дополнительного оборудования собственного производства
+ документация на уровне Siemens, но без форума техподдержки
- есть много мелких нюансов, которые никто нигде не раскроет, а вы разобьёте себе голову о стену пока в них разбираетесь
$$$ 30 дней и плати, привязка к аккаунту

Carel - для вентиляции и отопления самый торт, много готовых программ и решений. ST, LAD, FDB. Бесплатно на 30 дней. Считается импортозамещением, лол.


Beckhoff - Win10+виртуальный контроллер сверху. Хорошо работает, но дела с ними не имел.

Rockwell, Honeywell - что-то слышал, пару раз видел, дорого-богато.

В целом на этом список ходовых прошлогодних решений заканчивается. Наступает 2022...

Со временем, когда остынет мой пукан, я внятно расскажу о китайских и новых импортозамещённых решениях, но не сейчас.

Разработка
Итак, как программиста, нас интересует раздел "языки":
LADFBDSFC/CFCST
Как мы видим, все кроме ST - графические языки. Ах да, все они - группа языков стандарта IEC 61131-3. Перенести графические языки между средами разработки - адская, невыносимая боль, потому каждый гад считает нужным делать собственный визуальный редактор с извращённым функционалом и заморочками. К слову, самый крутой редактор LAD - у Seiemens. Для FDB мне больше всего понравился Carel cSuite.

В связи с этим, мы обратим свой взор на ST и будем дальше ковыряться исключительно в нём. Для Siemens это будет SCL (а STL у них - мерзотный древний язык, который вроде как может дофига всего, но только для сименса и вы без глаз останетесь во время его отладки, когда одна переменная / команда - одна строка, а ещё они зеброй подсвечены, фу, чур меня чур).

И теперь, с этого момента мы перейдём к сути...
Всё есть дискрета и аналог. Всё. Без исключений.
Дискретный вход.
Дискретный выход.
Аналоговый вход.
Аналоговый выход.

Дискретный - он либо есть, либо нет. Как секс.
Аналоговость - определяется качество, условно говоря.
И то, и другое нужно обработать, перед тем как использовать. Те кто пытается в user-friendly, как овно, берёт самостоятельно на себя первичную обработку сигнала, что вызывает адские муки когда надо что-то изменить, потому что это нельзя вывести на ту кнопочную панельку и сказать: "Чел, зайди туда, нажми это и отъебись." Нееет, нихуя, вы должны собирать монатки, закупать билеты и пиздюхать в жопу мира ради 5 минут правок и 100 минут поиска "где этот ебаный бэкап". Ну вот надо ли оно вам, а?

Из всех этих сигналов, как из кубиков и складывается управление всем процессом и его контроль. Кажется, я слегка разбежался, а дальше на рассмотрении недра "под капотом", которые будут интересны не всем. Нырнём в них в следующей части.)

Развернуть
В этом разделе мы собираем самые смешные приколы (комиксы и картинки) по теме АСУ ТП (+4 картинки, рейтинг 45.9 - АСУ ТП)