Разработка виртуальной RTOS для ПЛК на микроконтроллере

—

от автора

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

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

Хотя я считаю свой проект серьезным — не являюсь узким специалистом по разработке ОС. Проект пока любительский, и могу позволить себе некоторую свободу по разработке. Тут ставил для себя такие задачи:

  • ОС должна работать на виртуальном уровне (полная независимость от железа).

  • Жесткое реальное время.

  • Вытесняющая многозадачность.

  • Динамическое  добавление, изменение, удаление задач.

Так как в примере используется код — виртуальный гибридный ассемблер (собственная ISA) под софт-процессор собственной разработки, опишу свой проект:

  • Собственная система команд: WIA32. Расшифровывается как — широкий непосредственный адрес. 32 битные инструкции.

  • Разрабатывал свой компилятор под WIA32. Не LLVM или что то подобное. Работа с памятью (размещение переменных, выравнивание и прочее), аллокация регистров, оптимизация — собственные алгоритмы. Не настолько продвинут как классические тяжелые компиляторы  — но математически корректный, и достаточен для задачи.

  • Регистровая виртуальная машина  на стороне микроконтроллера для выполнения инструкций WIA32.

Все это программируется или в текстовом редакторе, или в моей собственной графической среде разработки:

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

uint8_t memory[SIZE] ={код ядра- код пользователя -данные, … стек}

Родной синтаксис моего компилятора, код планировщика и код пользователя:

.DataSection.TypesStackMap typedef{  R:     dword[16];  Size:      dword;}Thread typedef{  Priority:     dword;  IsActive:     dword;  Stack:     StackMap;  Time:         dword;  Period:       dword;}TasksManager typedef{    tasks:               pointer[8];    Item:                     dword;    CurrentContext:           dword;    TaskCount:                dword;}EModule_ typedef{  IsInterrupt:    byte;  Ptr8t:          byte;  IsPwr:          byte;    Rwt:            byte;    Rdy_:           byte;    Piw:            byte;    DI_0:           byte;    DI_1:           byte;    DI_2:           byte;    DI_3:           byte;    DI_4:           byte;    DI_5:           byte;    DI_6:           byte;    DI_7:           byte;  DI_8:           byte;    DI_9:           byte;    DI_10:           byte;    DI_11:           byte;    DI_12:           byte;   DI_13:           byte;   DI_14:           byte;      DI_15:           byte;  }System typedef{  ID:                  word;  IO_phy:           pointer;  Interrupt:          dword;  IP:               byte[4];  Status:              byte;  IsActive:            byte;  emodule_:        EModule_;  Time:               dword;  AddrTime:           dword;}TG_16proj typedef{    NameProcess:     byte[32];    StartProg:           byte;    StopExtSrc:          byte;    CallBackCode:  byte[2][3];    MSG_IO:         byte[356];}.EndTypes.Declaration thread:                     TasksManager; system:                           System;  system.IO_phy     =           0x40020414; tproj:                         TG_16proj; tproj.NameProcess = "%s Task A. Parameters"; tproj.MSG_IO="%clear_   %s Ports State: \n 0] %b(system.emodule_.DI_0)  \n 1] %b(system.emodule_.DI_1)  \n 2] %b(system.emodule_.DI_2)    \n 3] %b(system.emodule_.DI_3)  \n 4] %b(system.emodule_.DI_4)  \n 5] %b(system.emodule_.DI_5)  \n 6] %b(system.emodule_.DI_6)   \n 7] %b(system.emodule_.DI_7) \n 14] %b(system.emodule_.DI_14)  \n ";count32:       dword;locconst:      dword;savvr:         dword;system.Interrupt=1000;system.emodule_.Ptr8t=1;.EndDeclaration.EndDataSection.ProgramRWCNTX RDCD                             R0;JISBIT R0 0                    Taskmanager;JISBIT R0 1                    SaveContext;CALL InitialManager;JMPI Taskmanager;NOP 0;SaveContext:RWCNTX     WRITE                thread.CurrentContext;RWCNTX     WRCD                                    2;JMPI                                      Taskmanager;NOP 0;InitialManager:MOV   R0                                 R28;SDRI  R0                     system.AddrTime;MOVI  R31                                  1;ADDRL R0           @LabelAddress(redrobmarg);MOV   R29                                 R0; MOV   R28                                 SP;SUBI  R28                                120;MOVI  R0                                   0;SDRI  R0                    thread.TaskCount;MOVI   R13                               5000;ADDRL  CNXT           @LabelAddress(IO_Scan_);CALL                                  AddTask;MOVI   R13                               7000;ADDRL  CNXT              @LabelAddress(TaskA);CALL                                  AddTask;MOVI   R13                               3000;ADDRL  CNXT              @LabelAddress(TaskB);CALL                                  AddTask;MOVI   R13                               4000;ADDRL  CNXT              @LabelAddress(TaskC);CALL                                  AddTask;LDRI    R0                   thread.TaskCount;SDRI    R0                        thread.Item;InitIO:NOP;RET;NOP 0;AddTask:LDRI    R7                     thread.TaskCount;MULI    R7                                    4;ADDRL   R6         @SymbolAddress(thread.tasks);ADD     R7         R7                        R6;SDR     R7                                  R28;PUSH                                        R28;SUBI    R28                       @size(Thread);ADDI    R28      @SymbolAddress(Thread.Stack.R);SDR     R28      CNXT        @SymbolAddress(PC);POP     R7;SDR     R28      R7          @SymbolAddress(FP);SUBI    R7                        @size(Thread);SDR     R28      R7          @SymbolAddress(LP);SDR     R28      R7          @SymbolAddress(SP);LDRI    R15                    thread.TaskCount;ADDI    R15                                   1;SDRI    R15                    thread.TaskCount;                              SUBI    R7                                   32;MOV     R28                                  R7;RET;Taskmanager:CALL                                              ItemsUp;LDRI     CNXT                                 thread.Item;MULI     CNXT                                           4;ADDRL    R13                 @SymbolAddress(thread.tasks);ADD      CNXT                R13                     CNXT;LDR      R15                                         CNXT;MOV      CNXT                                         R15;SUBI     R15                                @size(Thread);ADDI     R15               @SymbolAddress(Thread.Stack.R);SDRI     R15                        thread.CurrentContext;RWCNTX   READ                       thread.CurrentContext;NOP 0;ItemsUp:LDRI     R4                      thread.Item;LDRI     R5                 thread.TaskCount;JGEQ     R4          R5                    4;ADDI     R4                                1;SDRI     R4                      thread.Item;RET;MOVI     R4                                0;SDRI     R4                      thread.Item;RET;IO_Scan_:LDRI     R0                      system.Time; LDRI     R1                  system.AddrTime; LDPHY    R1          R1                    0;SUB      R0          R1                   R0; MOVI     R2                             1000;JGEQ     R2          R0                    4; SDRI     R1                      system.Time;ADDRL    R0    @SymbolAddress( tproj.MSG_IO);EXTFN    R0          R0      R0           32; MOVI  R15                      0;LDBI  R14   system.emodule_.DI_0;SHLI  R14        0             0;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_1;SHLI  R14        0             1;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_2;SHLI  R14        0             2;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_3;SHLI  R14        0             3;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_4;SHLI  R14        0             4;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_5;SHLI  R14        0             5;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_6;SHLI  R14       0              6;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0             7;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0             8;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0             9;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0            10;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0            11;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0            12;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0            13;OR    R15   R14              R15;LDBI  R14  system.emodule_.DI_14;SHLI  R14        0            14;OR    R15   R14              R15;LDBI  R14   system.emodule_.DI_7;SHLI  R14        0            15;OR    R15   R14              R15;LDRI  R14          system.IO_phy;SDPHY R14                    R15;JMPI                 Taskmanager;redrobmarg:NOP 0;TaskA:LDBI  R6 system.emodule_.DI_0;RBIT  R6                    0;SDBI  R6 system.emodule_.DI_0;CALL Ladder;JMPI TaskA;NOP 0;TaskB:LDBI  R6 system.emodule_.DI_7;RBIT  R6                    0;SDBI  R6 system.emodule_.DI_7;CALL Processbinary;JMPI TaskB;NOP 0;TaskC:LDBI  R6 system.emodule_.DI_14;RBIT  R6                     0;SDBI  R6 system.emodule_.DI_14;CALL ProcessMath;JMPI TaskC;NOP 0;Processbinary:LDRI R0  system.Interrupt;MULI R0                2;BinaryItem:SUBI R0 1;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;LDBI   R15 system.emodule_.Ptr8t;JFLSE R15 BinaryItem;JTRUE R0  BinaryItem;RET;Ladder:LDRI R0  system.Interrupt;MOVI R15 1;LadderItem:SUBI R0 1;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;JTRUE R0  LadderItem;RET;ProcessMath:LDRI R0   system.Interrupt;MathItem:SUBI R0 1;LDRI R15 count32;LDRI R14 locconst;ADD   R13 R14      R15;SDRI R13 savvr;LDRI R15 count32;LDRI R14 locconst;SUB   R13 R15      R14;SDRI R13 savvr;LDRI R15 count32;LDRI R14 locconst;MUL   R13 R15      R14;SDRI R13 savvr;LDRI R15 count32;LDRI R14 locconst;DIV   R13  R15      R14;SDRI R13  savvr;LDRI R15 count32;LDRI R14 locconst;ADD   R13 R14      R15;SDRI R13 savvr;LDRI R15 count32;LDRI R14 locconst;SUB   R13 R15      R14;SDRI R13 savvr;LDRI R15 count32;LDRI R14 locconst;MUL   R13 R15      R14;SDRI R13 savvr;LDRI R15  count32;LDRI R14 locconst;DIV   R13  R15      R14;SDRI R13  savvr;LDRI R15 count32;LDRI R14 locconst;MUL   R13 R15      R14;SDRI R13 savvr;LDRI R15  count32;LDRI R14 locconst;DIV   R13  R15      R14;SDRI R13  savvr;JTRUE R0 MathItem;RET;Exitprog:NOP 0;.EndProgram

StackMap — содержит сохраненный контекст.

Thread   — содержит поля с описанием задачи, и содержит внутри структуру StackMap .

TasksManager — это главная структура планировщика. в ней массив указателей на задачи (количество в зависимости от доступной памяти).

EModule_ — хранит цифровое представление входов выходов контроллера.

Так как любая задача независимо может менять поведение выводов, EModule_  — есть единой точкой работы с физическими выводами.

Программа начинается с нулевого адреса виртуального ОЗУ uint8_t memory[0].

RWCNTX RDCD            R0;// Читаем статус контекста/Проверяем статус*/JISBIT R0 0   Taskmanager;JISBIT R0 1   SaveContext;CALL InitialManager;JMPI Taskmanager;

События сбрасывают счетчик команд — в ноль, а там по коду события определяем что произошло. Если RDCD  — пуст, значит это первоначальный старт и переходим в инициализацию CALL InitialManager; Так же сюда вписываются адреса обработчиков других событий (ошибок и прочее).

При нативной инициализации ВМ,на аппаратном уровне,  в регистре R28 — хранится указатель на системный таймер 

cpu.R[28].ui = (unsigned int)(&cpu.Time);

Поэтому уже на виртуальном уровне важно не затереть указатель, и сразу сохранить в виртуальную переменную (Ну или не затирать значение R28).

Сохраняем указатель на аппаратный таймер, для использования на виртуальном уровне

MOV   R0                                 R28;

SDRI  R0                     system.AddrTime;

Добавление задач.

Несмотря на то мой компилятор поддерживает достаточно продвинутый виртуальный ассемблер с высокоуровневыми данными, проще всего работать через мою графическую среду которая автоматически транслирует LD\FBD в этот ассемблер и разносит задачи.

Но сама организация добавления задач выглядит так:

AddTask:LDRI    R7                     thread.TaskCount;MULI    R7                                    4;ADDRL   R6         @SymbolAddress(thread.tasks);ADD     R7         R7                        R6;SDR     R7                                  R28;PUSH                                        R28;SUBI    R28                       @size(Thread);ADDI    R28      @SymbolAddress(Thread.Stack.R);SDR     R28      CNXT        @SymbolAddress(PC);POP     R7;SDR     R28      R7          @SymbolAddress(FP);SUBI    R7                        @size(Thread);SDR     R28      R7          @SymbolAddress(LP);SDR     R28      R7          @SymbolAddress(SP);LDRI    R15                    thread.TaskCount;ADDI    R15                                   1;SDRI    R15                    thread.TaskCount;                              SUBI    R7                                   32;MOV     R28                                  R7;RET;

Потом каждая следующая задача добавляется так:

MOVI   R13                               7000;ADDRL  CNXT              @LabelAddress(TaskA);CALL                                  AddTask;

Под каждую задачу выделяется память на виртуальном стеке. В виртуальном стеке создаем переменную Thread и инициализируем регистры контекста (на каком адресе задача, где ее стек и прочее).

После того как добавили все задачи, переходим в Taskmanager который извлекает задачи хранящиеся thread.tasks

Taskmanager:CALL                                              ItemsUp;LDRI     CNXT                                 thread.Item;MULI     CNXT                                           4;ADDRL    R13                 @SymbolAddress(thread.tasks);ADD      CNXT                R13                     CNXT;LDR      R15                                         CNXT;MOV      CNXT                                         R15;SUBI     R15                                @size(Thread);ADDI     R15               @SymbolAddress(Thread.Stack.R);SDRI     R15                        thread.CurrentContext;RWCNTX   READ                       thread.CurrentContext;

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

Каждая задача зациклена (в вечном цикле) инвертирует свой вывод, чем моргает светодиодом.

TaskA:LDBI  R6 system.emodule_.DI_0;RBIT  R6                    0;SDBI  R6 system.emodule_.DI_0;CALL Ladder;JMPI TaskA;NOP 0;

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

ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;ANDM  R15 system.emodule_.Ptr8t;

что является аналогом контактов/катушек типа. Среда IDE автоматически делает этот перевод:

Каждая задача работает отведенный ей квант времени (если она сама себя не остановит), ее прерывает системный таймер.

void SysTick_Handler(void){cpu.Time++;cpu.TimeOut++;if(PC<=cpu.R[29].ui || cpu.State ||cpu.TimeOut<cpu.R[31].ui){return;}      cpu.State = MODETASKMANAGER_;      cpu.TimeOut = 0;}

В PC<=cpu.R[29].ui регистре хранится граница ядра, отделяющая код ползователя от кода ядра планировщика. Так же в начале программы мы ставим в регистр cpu.R[31].ui — значение, с которой будем менять задачи, в данном случае — каждую миллисекунду.

При иннициализации виртуальной ОС мы читаем адрес метки redrobmarg

ADDRL R0           @LabelAddress(redrobmarg);MOV   R29                                 R0;

Если этого не сделать то само ядро системы само будет прерываться на смену задачи, вызывая саму себя, и наступит хаос. Мы просто отделили код пользователя (который может прерываться) от кода системы (который не может быть прерван).

Но пользователь тоже может добавить задачу ниче метки redrobmarg, и тогда его задача так же сработает от начала до конца и не будет остановлена, как например наша IO_Scan_:  которая изменяет физические порты.

Такая архитектура выбрана потому что — ОС может вовсе удалена (для экономии памяти), Или посреди программы пользователь может запретить смену задачи установив

cpu.R[29].ui = 0xFFFFFFFF; 

тогда весь код от начала и до конца будет работать без прерываний, то есть станет привилегированным или однопоточным.

Таким образом сам пользователь сможет:

  • вообще убрать ОС, 

  • написать свою ОС без какого либо вмешательства в нативный код

  • обновить “ОС” по воздуху.

Возможно ОС — это пока громко сказано, драйверов работы с оборудованием тут не на работано, но сути не меняет.

Тестовые программы:

Так как наша ВМ запущена на STM32F722 Nucleo, у нас четыре задачи

  • IO_Scan_: обновляет выходы ПЛК. А так же каждую секунду отправляет форматированную строку на внешнее устройство.

tproj.MSG_IO="%clear_   %s Ports State: \n 0] %b(system.emodule_.DI_0)  \n 1] %b(system.emodule_.DI_1)  \n 2] %b(system.emodule_.DI_2)    \n 3] %b(system.emodule_.DI_3)  \n 4] %b(system.emodule_.DI_4)  \n 5] %b(system.emodule_.DI_5)  \n 6] %b(system.emodule_.DI_6)   \n 7] %b(system.emodule_.DI_7) \n 14] %b(system.emodule_.DI_14)  \n ";

На этапе компиляции , компилятор заменяет названия переменных типа %b(system.emodule_.DI_7) и подставляет — адрес переменных, а железо уже выводит каждую секунду форматированную строку о состоянии выводов. Мы можем например динамически создавать строки   в формате JSON и передавать по RS485 или TCP (если такой есть) на удаленные сервера или устройства. Без %s будет выводиться поток байт.

  • TaskA: Задача с LD цепями , и моргает 0 портом (зеленый).

  • TaskB Задача LD цепями, моргает 7 портом (синий).

  • TaskС Задача имитирующая математические операции внутри LD цепей. Моргает 14 портом (красный).

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

  • компилятора который точно просчитывает адреса/переходы сложных данных и извилистых путей инструкций. Так как упоминал это не LLVM который многое делает из коробки.

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

  • И конечно же среды IDE .

Так тяжело моргание светодиодом никогда не давалось.

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