Разработка виртуальной 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;
.EndProgramStackMap - содержит сохраненный контекст.
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 .
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.