UN NewsWHO releases first global guidelines on child obesity as cases surgeESPN📈 Ovechkin's retirement announcement sparks major ticket price hikePunchBarcelona hails Messi’s remarkable career with ArgentinaThe Jerusalem PostHamam al-Hammami hired by flydubai as part of airline's mass hiring push - reportBollywood HungamaGuneet Monga Kapoor-led Women in Film India and Google Flow join hands to enable AI-powered filmmaking for new creative possibilitiesInquirerDTI maps halal calamansi supply chain in Oriental MindoroCapital FMPharmacists told to be on alert as Kenya confirms imported Ebola caseZDF heuteAktuelle Pressemitteilungen des ZDFCollider‘Mistborn’ Movie Script Is Officially Finished as Brandon Sanderson Reveals Next Step [Exclusive]SDP EspectáculosReseña de Carrie: una buena actualización de Prime Video, pero ¿dónde quedó el horror?Daily MaverickDANCE REVIEW: Elysium — 4 dances of bliss and seduction, transcendence and joy from Cape Ballet AfricaGIGAZINEChatGPTが生成したマンガに「実在するマンガ家の署名」が含まれているとの指摘
The Daily Newsstand · Free, Always
Wednesday, October 7, 2026

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

Translate

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

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

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

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

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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.