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

Каждая задача работает отведенный ей квант времени (если она сама себя не остановит), ее прерывает системный таймер.
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 .
Так тяжело моргание светодиодом никогда не давалось.