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

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

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

  • ОС должна работать на виртуальном уровне (полная независимость от железа).
  • Жесткое реальное время.
  • Вытесняющая многозадачность.
  • Динамическое добавление, изменение, удаление задач.

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

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

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

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

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

.DataSection
.Types
StackMap 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
.Program
RWCNTX 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 автоматически делает этот перевод:

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

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

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 регистре (его аналог R29 на виртуальном уровне) хранится граница ядра, отделяющая код ползователя от кода ядра планировщика. Так же в начале программы мы ставим в регистр cpu.R[31].ui - значение, с которой будем менять задачи, в данном случае - каждую миллисекунду.

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

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

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

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

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

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

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

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

  • вообще убрать ОС,
  • написать свою ОС без какого либо вмешательства в нативный код
  • обновить “ОС” по воздуху.

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

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

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

  • IO_Scan_: обновляет выходы ПЛК. А так же каждую секунду отправляет форматированную строку на внешнее устройство по UART.
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 портом (красный).
Разработка виртуальной RTOS для ПЛК на микроконтроллере, image #3

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

  • компилятора который точно просчитывает адреса/переходы сложных данных и извилистых путей инструкций. Так как упоминал это не LLVM который многое делает из коробки.
  • Самой физической виртуальной машины, которая может показаться простой - но легко запутаться в указателях на стеки, кадры стека и всякие смещения.
  • И конечно же среды IDE .

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

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

Источник: https://habr.com/ru/articles/1091398/

125 views·4 shares