Хотя я считаю свой проект серьезным — не являюсь узким специалистом по разработке ОС. Проект пока любительский, и могу позволить себе некоторую свободу по разработке. Тут ставил для себя такие задачи:
-
ОС должна работать на виртуальном уровне (полная независимость от железа).
-
Жесткое реальное время.
-
Вытесняющая многозадачность.
-
Динамическое добавление, изменение, удаление задач.
Так как в примере используется код — виртуальный гибридный ассемблер (собственная 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/