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

Мда, я дико извиняюсь за большое количество опечаток в предыдщих постах. Руки не поспевают за мыслью, а при редактировании потеряется часть оформления регулярно. Не зналь(
Имея на руках пример того, с чем предлагается работать, впору поговорить об архитектуре, которая и будет обеспечивать корректное и стабильное исполнение кода, однако, перед этим сделаем ряд важных оговорок:
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 );
//выталкиватель трубы из
Из вкусного – вы можете создавать массивы и обрабатывать их пакетно, можете обрабатывать индивидуально, можете создавать со своим именем на каждый сигнал…но концепция не изменится. Они управляются одинаково, ведут себя одинаково, выглядят плюс-минус одинаково. Вы можете спокойно ковыряться внутри драйвера, подкручивая ему те или иные плюшки под конкретный проект, но вся остальная обвязка не сдвинется ни на бит. И это, йа щитаю, прекрасно!