Как я разрабатывал отказоустойчивый промышленный контроллер. Ч1

от автора

Я, автор , независимый исследователь, разработчик SCADA системы Gatherlog

А так же автор комплекса по разработке Промышленных Контроллеров под названием 3o|||sheet. Среда, IDE читается как Зошыт — тетрадь, но так как для компилятора и среды выполнения названия не придумал, пока все называю 3o|||sheet.

Это не коммерческие проекты, в будущем планирую их сделать открытыми.

В статье я делаю акцент на разработанном мной методе отказоустойчивости, и только коротко напишу что за проект.

3o|||sheet это комплекс для разработки ПЛК на подобии Codesys. Он состоит из:

1) Среда разработки: 3o|||sheet IDE. Кроссплатформенное GUI приложение с собственным графическим движком для отрисовки LD/FBD , а так же текстовых языков. Эти языки среда транслирует в универсальный — гибридный ассемблер для компилятора.

2) Компилятор: Так же собственной разработки , пример родного синатксиса:

Types:StackMap typedef{  R:     dword[16];  Size:      dword;}Thread typedef{  Stack:     StackMap;  Priority:     dword;  IsActive:     dword;  Time:         dword;}TasksManager typedef{    tasks:                Thread[32];    Item:                     dword;    ProgramSegment:           dword;    CurrentContext:           dword;    StackBorder:              dword;    TaskCount:         dword      0;}GPIO_Config typedef {  GPIOC_BSRR:    pointer    1073811472;  opcouner:      dword           10000;  flagsop:       dword               1;  outreg:          dword              0;  count32:         dword              0;  locconst:        dword              1;  dvf:             dword              1;  savvr:           dword              0;  savvrrel:        real;  paramrel:        real              0;  paramrel1:        real             1;}Declaration:  thread:                    TasksManager;  gpio:                       GPIO_Config;  floatparam:   real             8.141592;  bufferf:      real                    0;           Program:RWCNTX RDCD R0;JISBIT R0 0 SaveContext;CALL InitialManager;JMP24 Taskmanager;SaveContext:RWCNTX WRITE thread.CurrentContext;MOVI       R0                    0;RWCNTX     WRCD                 R0;JMP24 Taskmanager;InitialManager:CALL                      ProgramBorder;MOV   R15                             SP;ADDI  R15                             32;SDR20 R15             thread.StackBorder;MOVI  R14           @addr(BinaryOperaty);CALL AddTask;MOVI  R14             @addr(MathOperaty);CALL AddTask;MOVI  R14             @addr(RealOperaty);CALL AddTask;MOVI  R14               @addr(IOOperaty);CALL AddTask;MOVI  R14               @addr(ItemsDown);CALL AddTask;RET;AddTask:LDR20  R15                                    thread.TaskCount;MULI   R15                                       @size(Thread);ADDI   R15                                 @addr(thread.tasks);ADDI   R15                               @addr(Thread.Stack.R);SDR    R15               R14                         @addr(PC);LDR20  R0                                   thread.StackBorder;SDR    R15               R0                          @addr(FP);SDR    R15               R0                          @addr(SP);SUBI   R0                                                  128;SDR20  R0                                   thread.StackBorder;RET;/*-------------------------------------------OS--------------------------------*/Taskmanager:LDR20    R0                     thread.Item;MULI     R0                   @size(Thread);ADDI     R0             @addr(thread.tasks);ADDI     R0           @addr(Thread.Stack.R);SDR20    R0           thread.CurrentContext;LDR      R2           R0          @addr(PC);LDR20    R0          thread.CurrentContext;CALL                                ItemsUp;RWCNTX READ           thread.CurrentContext;JMP32    R2;ItemsDown:MOVI     R0                               0;SDR20    R0                     thread.Item;JMP24                           Taskmanager;ItemsUp:LDR20    R4                     thread.Item;ADDI     R4                               1;SDR20    R4                     thread.Item;RET;ProgramBorder:MOV  R0 PC;ADDI R0 16;SDR20 R0 16;RET;BinaryOperaty:MOVI R0 1;SHLI R0 15;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL Processbinary;MOVI R0 1;SHLI R0 31;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL Processbinary;JMP24 BinaryOperaty;MODE 0;MathOperaty:MOVI R0 1;SHLI R0 15;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL ProcessMath;MOVI R0 1;SHLI R0 31;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL ProcessMath;JMP24 MathOperaty;MODE 0;RealOperaty:MOVI R0 1;SHLI R0 13;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL ProcessReal;MOVI R0 1;SHLI R0 29;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL ProcessReal;JMP24 RealOperaty;MODE 0;IOOperaty:MOVI R0 1;SHLI R0 15;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL ProcessIO;MOVI R0 1;SHLI R0 31;LDR20 R1 gpio.GPIOC_BSRR;SDPHY R1 R0;CALL ProcessIO;JMP24 IOOperaty;MODE 0;

Код представляет — гибридный ассемблер:

Высокоуровневые данные, поддерживающий сложные структуры в глубину и многомерные массивы.

Инструкции — похожи на ассемблер.

В данном коде написан механизм — смены ( вытеснения) задачи: сохранение\извлечение контекста, либо инициализация если это первый запуск. Это происходит на — виртуальном уровне.

3) Рантайм, система выполнения байткода который создается компилятором, из кода представленного — выше. Написан на языке СИ.

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

Как все начиналось

Чтоб сделать ПЛК который поддерживает много языков и функций (например — замена участков кода на лету и прочее) нужен собственный рантайм и компилятор который будет переводить любые языки ( LD FBD ST) в собственные инструкции. Первая версия моего компилятора давала сбой — то одно не верно посчитает, то другое. Я столкнулся «паразитными» смещениями по адресам, а точнее — в сложных местах программа могла не туда перейти, не то прочитать или не туда записать. Программа слетала, и не понятно в каком месте была ошибка — так как при тихом повреждении данных (silent corruption), и физическим вылетом могло пройти много времени, — замучаешься пошагово следить в отладке.

Дивергентное, многоверсионное выполнении программы. DME.

Тут я подумал, а что если компилировать одну и ту же программу — два раза? Но вторую копию — перемешивать адреса функциям, блокам, чтоб каждая функция, блок, и переменная, в каждой копии была на другом месте. Логика была така — Если компилятор все верно считает, нет никаких паразитных смещений, то каждая инструкция будет совпадать во всех копиях. Адреса у них хоть будут разные, но ход программы будет логически идентичен . А если адрес не верно просчитан — то в каждой копии программа запишет/прочитает или перейдет в логически разные — места. Тем самым детекция ошибки произойдет сразу.

Так и было, — две компиляции но разные структурно в памяти — все паразитные смещения из за бага компилятора — всплывали сразу. Так называемые silent corruption (скрытые ошибки) — стали наблюдаемы. Это позволило точно отточить алгоритмы просчета адресов, и больше компилятор не ошибался.

Фрагмент, описание моей работы

Фрагмент, описание моей работы

Отказоустойчивые системы

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

Lockstep — две копии (одинаковые бинарники) программы на двух процессорах работают и сверяют друг друга на каждом шаге.

TMR — три копии (одинаковые бинарники) программы на трех процессорах. Если одна отличается, происходит голосование , кто не прав и работа выполняется дальше.

Lockstep и TMR — уязвимы к коррелированным сбоям. Методы эффективны только если система замечает разницу между копиями программы, сигналит об этом. Но если оба ядра ошибаются — система не заметит ошибки, и продолжит выполнять ошибочную программу.

И тут я заметил, мой метод с деккореляцией адресного пространства который я применял в отлаживании компилятора — решает эту задачу.

Если система Lockstep и TMR «одинаковость» воспринимают как ОК, мой метод наоборот — одинаковость воспринимает как как — осторожность. А разность — как нормальную работу. Но у моего метода есть два типа разности:

  • Допустимая разность — это адреса (адреса функций, счетчика команд, переменных и т.д).

  • Не допустимая разность — в последовательности инструкций ( семантика ).

То есть, в системах Lockstep и TMR сводится к 1/2 :

  • Либо одна из копий программы ведет себя по другому — это ошибка.

  • Либо ведут себя одинаково — значит все хорошо. Но тут и проблема одинаковых бинарников, если все копии выполняются одинаково не верно, система будет думать что все хорошо.

В моем методе DME, соотношение 1/3 . Где:

  • одинаковая логика/инструкции — это Хорошо.

  • одинаковость адресов -Плохо.

  • разная логика/инструкции — Плохо.

Я сделал более узким коридор для корректности , и расширил коридор для прохождения ошибок (усилил ошибку).

В нашем методе DME , при двойной компиляции и перемешиванием адресов, одинаковый баг программы, компилятора, или ошибки из за физики — обязательно приведет к расхождению (Дивергенции).

Практика

Создаем в моей среде — тестовый проект в языке LD. Моя среда поддерживает  FBD но так же пока только внутри LD. ST язык я не реализовал в полной мере, но на каком бы языке программа ни была, она переводится в единый виртуальный ассемблер (собственный синтаксис и инструкции, см. прошлые постыкоторый выполняется внутри на голом железе Микроконтроллера, FPGA  или любого CPU.

Интерфейс. Тяжелые функциональные блоки внутри LD подходят под тесты отказоустойчивости

Интерфейс. Тяжелые функциональные блоки внутри LD подходят под тесты отказоустойчивости

Переходим к настройкам компиляции:

— количество копий программы и количество соответственно компиляций. N Должно соответствовать количеству ядер, или ПЛК если они работают в режиме похожему на Lockstep\TMR. На STM32G030 эти ядра эмулируются, в чем смысл — дальше.

+1 -1 это команда разнесения адресов в разные по знаку стороны. Если в одной копии переход из функции А к функции Б изменяет программный счетчик например PC+=94, то в второй копии компилятор организует этот переход PC-=64. Усиливает декорреляцию адресного пространства, соответственно увеличивает область детектируемых программных багов и программных ошибок из-за физического влияния среды.

Canonical trace hash: Инкрементальный хеш выполнения программы (если есть аппаратный хеш как у Cortex M4) , но на слабом железе вместо хеша может подойти обычное сравнение каждой инструкции (опкода или результата) каждой копии программы простым «=», или хеш может быть банальным XOR с степенью ( за 2 такта, это + 10..20 наносекунд к выполнению инструкции, дешево ).

Тут принцип тот же что и Lockstep\TMR — N копий программы на ядрах, только в Lockstep\TMR сравниваются идентичные бинарники и состояние на каждом такте, у меня только логическая траектория, потому что адреса переменных и инструкций в копиях разные — они (адреса) исключаются из уравнений.

Любая программа, LD ST C++ не важно, состоит из блоков кода и функций. Мой компилятор их и перемешивает по разным адресам и разным знакам (+-) направлениям перехода. Если мы скомпилируем программу, то получим пространственные -разные версии одной программы:

N компиляций, и расположение программы в памяти. Один и тот же блок или функция - по разным адресам.

N компиляций, и расположение программы в памяти. Один и тот же блок или функция — по разным адресам.

Если мы присмотримся к адресам функций на скрине, то увидим в одной копии например Main будет по адресу — 0 (ноль) в другой копии она же будет по адресу — 144. Так и со всеми остальными. Выполняется главный принцип DME — усиление ошибки программы через структурную декорреляцию адресного пространства.

Теперь, предположим в системе произошел коррелированый сбой, то есть такой который задевает все ядра или все копии программы. Чаще это возможно  когда программа криво написана, затерла адрес возврата, или  другого участка программы. Проблемы с питанием — тоже может повредить все ядра одинаково. Радиация, или помехи. Если сбой повреждает ход только одной копии программы или ядра — то тут обычный способ известный в мире Lockstep ( и мой DME) его детектирует. Но если обе копии повреждены одновременно на одно значение, то Lockstep из-за своей одинаковости его просто не заметит, если счетчик команд попадет в валидное пространство.

Давайте предположим, что сбой произошел (неважно по какой причине) ,  адрес возврата или какого либо перехода ветки , установлен в случайное значение 148: В моем методе DME будет новый эффект:

Просмотр результата компиляции в 3o|||sheet. Одна программа - разные адреса ее компонентов.

Просмотр результата компиляции в 3o|||sheet. Одна программа — разные адреса ее компонентов.

Это вызовет — Дивергенцию. Система фиксирует — две разные инструкции одновременно: CALL и  SDPHY. Несовпадение. Компаратор вызовет программу X (типа обработчик ошибки) или  осуществит переход в безопасное состояние.

Детекция — сразу, что очень важно, так как обычный ПЛК — слетит с концами и не успеет передать на каком месте авария. Либо вообще может слететь через какое то время долго притворяясь что все хорошо.

Ошибка может быть более сложной, например радиация или электромагнитная помеха может не изменить весь адрес полностью, а только один бит (так называемый bit flip), тем самым добавив\убавив к адресу одинаковое — число. То есть, адреса продолжат быть разные, но смещенные на константу. Для этого мой компилятор и делает переходы с разными знаками между функциями и некоторые другие приемы для локальной асимметрии адресов.

Ошибка из за физических процессов: bit flip может привести к тому что в копиях, адрес изменится на константу. В методе DME это приведет к разным участкам кода, и ошибка будет детектирована. Для этого случая в копии реплик добавляются мелкие сдвиги которые в статье пока не рассматриваются.

Ошибка из за физических процессов: bit flip может привести к тому что в копиях, адрес изменится на константу. В методе DME это приведет к разным участкам кода, и ошибка будет детектирована. Для этого случая в копии реплик добавляются мелкие сдвиги которые в статье пока не рассматриваются.

Я только на пути развития своего метода. Протестировал на Микроконтроллерах и FPGA. В микроконтроллере параллельность — программная, две программы с перемешанными адресами поинструкционно выполняются по очереди. То есть с временной задержкой, что само по себе дает отказоустойчивый эффект известный как Time redundancy, EDDI/SWIFT . В такой комбинации мой метод DME хорошо справляется с программными багами и компиляции, а Time redundancy, EDDI/SWIFT дает устойчивость к физическим влияниям на систему против кратковременных (перемежающихся) сбоев, вызванных внешними помехами (космические лучи, электромагнитные шумы)

Если брать скорость работы в отказоустойчивом режиме, что технически — дублированном, небольшие данные моего теста:

Скорость выполнения операций в микросекундах. Первое значение это базовые логические операции (контакты\катушки) второе значение — математика. Для теста язык LD лучше всего подходит для сравнения так как время выполнения у большинства брендов четко описано в документации :

Mitsubishi (китайский клон) STM32F103--2.7---------math int 8.6

Allen Bradley Micro 810 ---------------2.5---------math int 8.5

Siemens LOGO!------------------------- 5.0

Schneider Electric Zelio Logic --------10.

3o|||sheet STM32G030 ---------------- 2.1 --------math int 6.5

===3o|||sheet отказоустойчивый режим N - реплик=====================

3o|||sheet STM32G030 (N=2) ---------- 4.5 --------math int 13.5

3o|||sheetSTM32F407(N=2)----------- 2.1 -------math int/float5.2

3o|||sheetSTM32F722(N=2) -----------0.9 -------math int/float2.3

3o|||sheetCyclone lV 50 MHz (N=2)-- 0.1 -------math int 0.1

Нашему прототипу ПЛК, название не придумал и записан как 3o|||sheet.

В режиме отказоустойчивости, с циклом 10 миллисекунд, 3o|||sheet STM32G030 отработает примерно 1100 операций: 550 — логические (контакты катушки) + 550 математические.

3o|||sheet STM32F722х — вытянет в быстрых (по меркам промышленности) ПИД регуляторах в режиме отказоустойчивости.

Без дублирования, в обычном режиме наши прототипы ПЛК больше чем в два раза быстрее отказоустойчивого режима. А если сравнивать с брендами, то ПЛК на STM32G030 64MHz- производительнее чем Allen Bradley Mirco 810 или китайский клон Mitsubishi FX3 на STM32F103 72 MHz.

Altera Cyclone lV EP4CE6E22C8 даже на 50 МНz , в отказоустойчивом режиме , показывает впечатляющий результат. В цикле 1 миллисекунды отработает 8000 логических или 2000 математических (целочисленных) операций. FPGA можно сказать работал в режиме двухъядерного lockstep, только в нашем случае отслеживается траектория программы, а не состояние процессоров. Свой рантайм я переработал в Soft Processor на Verilog , выполняющий тот же байткод что и микроконтроллеры.

Я не тестировал работу с физическими помехами, сбивал счетчик программы и тихо искажал данные — программно. Лабораторные Испытания еще впереди. Но старался сделать свой метод DME — математически доказуем. Тут я не буду публиковать изобилие формул, так как считаю что концепция должна быть интуитивно понятно и так.

Программные ошибки (сюда вношу и баги компиляторов) ведут в одно и тоже ошибочное место все реплики как повреждения адресов возврата и переходов. Такие сбои как раз не покрываются традиционными методами типа lockstep/TMR. Я и разрабатываю свой метод DME для подобных ошибок, расширяя возможности обычных lockstep/TMR.

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

В целом это лишь часть из моей 30 страничной в данный момент работы DME. Тут я не рассматривал более детально механизмы детектирования пропуска инструкций внутри критических функций или блоков, когда счетчик команд во всех репликах одновременно перескакивает на одинаковое небольшое значения в плоть до 1 инструкции точности. Так же метод DME охватывает темы — верификации компиляторов, канонизации инструкций разных архитектур к единому виду, для гетерогенных систем в отказоустойчивой работе, и другие некоторые направления где я вижу потенциал DME. Если будут вопросы в комментариях — отвечу следующей статьей.

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