Разработка виртуальной RTOS для ПЛК на микроконтроллере — АСУ ТП: сообщество инженеров

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

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

Статья описывает любительский проект виртуальной RTOS для ПЛК на микроконтроллере STM32F722 Nucleo, включая собственную систему команд WIA32, компилятор и регистровую виртуальную машину. Из неё можно узнать о принципах построения планировщика задач, работе с контекстом и организации прерываний.

Главное
  • ОС работает на виртуальном уровне, обеспечивая полную независимость от аппаратного обеспечения.
  • Реализованы жёсткое реальное время, вытесняющая многозадачность и динамическое управление задачами.
  • Используется собственная система команд WIA32 с 32-битными инструкциями и собственный компилятор.
  • Вся программа и данные хранятся в массиве uint8_t memory [SIZE] внутри микроконтроллера.
  • Планировщик использует структуры TasksManager, Thread и StackMap для управления задачами и контекстом.
  • Прерывания обрабатываются через SysTick_Handler, который переключает задачи по системному таймеру.
Термины и технологии
  • ПЛК
  • WIA32
  • STM32F722 Nucleo
  • SysTick_Handler
  • TasksManager
  • Thread
  • StackMap

Кому полезно: Инженеры АСУ ТП, интересующиеся разработкой ОС для ПЛК и виртуальных машин.

Первоисточник: habr.com

Выжимка составлена редакцией сайта с помощью ИИ по тексту статьи.

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

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

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

Добавление задач.

Несмотря на то мой компилятор поддерживает достаточно продвинутый виртуальный ассемблер с высокоуровневыми данными, проще всего работать через мою графическую среду которая автоматически транслирует LDFBD в этот ассемблер и разносит задачи.

Но сама организация добавления задач выглядит так:

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 регистре (его аналог 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 портом (красный).

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

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

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

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

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

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

Оставьте комментарий

Войти

Зарегистрироваться

Сбросить пароль

Пожалуйста, введите ваше имя пользователя или эл. адрес, вы получите письмо со ссылкой для сброса пароля.

Прокрутить вверх