среда, 16 марта 2016 г.

CMSIS RTOS - операционная система для контроллеров Cortex

Пишу операционную систему со всеми объектами типа: задач, тредов, мьютексов, семафоров и очередей. В качестве основы выбрал спецификацию CMSIS RTOS 1.02 и при необходимости расширяю ее стандартными вызовами POSIX. Уже написал рабочую версию. Работает. Хочу немного описать или попробовать описать ее достоинства.
Моя операцинка нужна для проекта автоматики здания. От нее не требуется существенное быстродействие, меня вполне устраивает наличие задержек в 1мс. Однако, делаю ее пригодной для задач реального времени. Меня стесняет спецификация CMSIS, она ориентирована на квант времени 1 мс, а мне привычнее ориентироваться на микросекунды, потому что нужно иногда управлять моторами и другими процессами, которые требуют квантования до 100кГц и точности соблюдения интервалов времени до 1мкс.

DeviceTree Есть что-то такое, что не укладывается в понятие операционка, живет до нее и внутри нее,  API, от которого зависит эффективность реализации. Это начальная инициализация. В общем случае инициализация оборудования может выполняться до запуска операционки, в загрузчике. Как инициализацию оборудования сделать эффективной? Есть две концепции - конфигурация аппаратуры на этапе компиляции и в процессе работы. Думаю про концепцию дерева устройств DeviceTree, которая живет в Linuх и других операционках. Однако до реализации структурированного представления конфигурации пока не дошел.
Первая концепция - конфигурация на этапе компиляции. Есть понимание, если бы не человек, то количество операций необходимое для выполнения конфигурации N регистров аппаратуры равно N. А на деле, если брать HAL от производителя, то получается мы сначала заполняем тучу переменных удобных для заполнения и понимания, но крайне не удобных для копирования в регистры аппаратуры. Затем разбираем, проверяем и пересортировываем эти переменные в функциях HAL. На это расходуется и место в памяти и время. Есть то что удобно копировать в аппаратуру, а есть то что удобно человеку. Концепция состоит в том чтобы преобразование из человеческого языка в упакованные данные для аппаратуры производилось на этапе компиляции, и в регистры аппаратуры попадали бы уже подготовленные данные. Как этого добиться: чаще всего я применяю представление конфигурации в виде файла заголовка (board.h) с набором определений для условной компиляции. Почти весь HAL я переписываю в форме inline функций. При оптимизации inline функции содержащие ветвления по статическим переменным (константам или переменным значение которых вычисляется на этапе компиляции) упрощаются до команд копирования упакованных данных. Тут конечно важно понимать, какая операция будет оптимизирована, а какая нет. В этом и есть искусство.
Вторая концепция - хранение данных конфигурации в специальном сегменте данных (для контроллеров - в специальном сегменте флеш памяти). Сегмент во флеш (файл) имеет отображение на сегмент данных и может сохраняться в процессе работы программы обратно во флеш. При включении контроллера содержимое сегмента автоматически накладывается на начальную (статическую) конфигурацию и мы получаем ноль команд на необходимость чтения конфигурации с носителя. Таким образом мы получаем, что при запуске функции конфигурирования аппаратуры и настройки драйверов, в памяти лежит именно та конфигурация, которая была до выключения устройства. Переменные которые требуют сохранения обозначаются специальным атрибутом, например:
int param CONFIG = 1;// на языке С
Это значит, что начальное значение, после заливки прошивки в контроллер будет 1, мы можем менять содержимое  параметра, например запишем туда 2. При следующем включении питания параметр будет содержать значение 2. Таким образом не тратится код и время на загрузку и обработку параметров конфигурации.

osTimer. Таймерные процессы. Быстрые. Их задача с квантом времени системного таймера проверять и пересчитывать время, время пересчитывается с ошибкой округления до заданного кванта системного таймера SysTick, ошибка округления компенсируется на следующем такте. Алгоритм округления времени - это и есть ноу хау. Другое ноу хау - задача типа таймер запускается с высоким приоритетом и выполняется с заданной задержкой (фазой). В итоге можно запустить несколько таймеров которые будут разнесены по времени и обеспечивать одновременную синхронную работу нескольких задач, фактически не мешая друг-другу и не вызывая задержек выполнения реал-таймовых задач. Относительно спецификации CMSIS вводим расширенную функцию конфигурации, которая определяет задержку времени и период таймера в микросекундах.

Есть конечно и проблемы, но таких чтобы не обойти и не пройти нет.

PendSV_Handler Планировщик задач. Я не могу сказать, что изобрел что-то новое. Идеи списал. Немного оптимизировал. Вкратце, для тех кто не в теме, логика планировщика такова - он запускается, по специальному прерыванию PendSV. Прерывание планировщика может запускаться на хвосте любого другого прерывания, если в системном регистре выставить флажок "пора". Флажок "пора" выставляется по разным поводам, есть важный повод - переключение задач по времени, другой повод - задача сама может решить отдохнуть, osThreadYield. Концептуально важный повод - по готовности данных. Например, в прерывании по готовности данных может выставляться признак "пора переключаться на конкретную задачу". Чтобы дать возможность обработчику прерывания переключить контекст на процесс-владельца ресурса ввел функцию osThreadNotify.
Конфигурация системы включает три принципа, реализован пока только один из них. Можно использовать два регистра стека MSP и PSP. Можно использовать два уровня привилегий доступа к памяти и ресурсам. Можно использовать аппаратно контролируемый приоритет задачи. Можно углублять стек при использовании FPU для ленивых, когда регистры FPU сохраняются по необходимости. До регистров FPU пока дело не дошло. В текущей реализации сделал один указатель стека PSP и один уровень привилегий. Уровень привилегий хочу разделить для системных и прикладных задач, а также не плохо бы разграничить сегмент памяти прикладной и системной задачи, чтобы можно было сохранять работоспособность системы при сбоях и анализировать и устранять сбои уже выпущенного оборудования.

SVC - это системный вызов, программное прерывание. Идея в том, что мы можем усилить безопасность и отказоустойчивость нашей системы если ограничим доступ из треда (прикладной задачи) к ресурсам. Некоторые ресурсы требуют раздельного доступа. С помощью вызовов SVC можно разделить привилегии доступа к ресурсам и обеспечить одновременную работу с разделением по времени. Технически переключение задач может выполнятся через вызов SVC, на хвосте прерывания. Таким образом сделана реализация функции osThreadYield, переключение задач по инициативе приложения.

MPU. Раз уж затронули тему, в операционке поддержан MPU - разграничение доступа к памяти, для системных и прикладных программ. Пока что в данной реализации не разделяется привилегии доступа к регистрам и сегментам, однако механизмы разделения доступа присутствуют и реализованы. Сначала я хочу убедиться насколько эффективно можно использовать эту технику, а потом уже разграничивать. Внедрение функционала MPU позволяет отлавливать ошибки в коде в процессе разработки программы - это важно. Ошибки - это прерывания типа MemManage и HardFalt, которые обрабатываются системой. В случае ошибки можно выполнить рестарт системы, выкинуть или перезапустить задачу. Система может сохранить работоспособность. То что у меня появилась возможность отладки глюков работы с памятью -- это уже большое достижение. Тут же следует упомянуть про функционал отладки. Всегда была проблема выяснить почему у меня повисла программа. Сейчас у меня есть механизм вывода системных сообщений типа syslog, реализованный через ITM -- SWO интерфейс контроллера. Например, сообщения из прерывания HardFalt выводятся через этот интерфейс. Удивительно то, что если ошибка непоправимая произошла и вывод сообщения об ошибке происходит с информацией об адресе возникновения ошибки, можно выяснить причину, исправить эту ошибку и двигаться дальше.

Модули. Модульный принцип построения ядра системы. Идея в том чтобы не было единого файла, который отвечает за включение функционала: есть такая-та функциональность или такой-то драйвер в составе сборки системы. Достигается это путем введения таблицы регистрации модулей в специальном сегменте памяти. Весь сегмент - это и есть таблица регистрации. По факту получилось две такие таблицы. Одна таблица - это программные модули, которые требуют периодического запуска для выполнения функций операционной системы, например сбор мусора при выделении динамической памяти. Вторая таблица - инициализация. Инициализация получилась стандартной, на конструкторах/деструкторах, например.
static void __attribute__((constructor)) init(){...} //
эту нотацию понимает GCC и формирует запись в специальном сегменте. Записи представляют собой таблицу вызовов, которая выполняется до запуска main().

Atomic. Атомарные переменные. Неблокирующий доступ к разделяемым ресурсам без ожидания. Это не часть системы, это метод работы с разделяемыми ресурсами. Архитектура Cortex M3/4/7 поддерживает команды LDREX/STREX, на которых можно реализовать атомарный неблокирующий доступ к переменным для операций типа чтение-модификация-запись. Что очень важно, мы не можем выключать прерывания, чтобы не тормозить и не сбивать реал-таймовые процессы! Важным дополнением к атомарным операциям с целыми числами и указателями является реализация списков и очередей без блокировок доступа.

Все механизмы разделения доступа к ресурсам типа семафоров osSemaphore и мьютексов osMutex выполнены на базе неблокирующих атомарных операций. Все подобные вызовы не должны вызывать простоя процессора.

osSignal. Сигналы - это битовые флаги, которыми обмениваются задачи, чтобы синхронизировать свою работу. Задача может ожидать выставления сигнала. Сигналы поддержаны в планировщике задач. Механизм сигналов применяю, по собственному желанию. Чтобы обработать событие аппаратное мы запускаем обратный вызов (callback) в обработчике или выставляем флаг (сигнал) готовности, по которому происходит переключение задач. В общем случае делаем и то и другое. Для задач, которые не требуют четкой привязки по времени к событию и допускают задержку в обработке на 1мс предпочтительным является использование механизма синхронизации через сигналы. Операции чтения и модификации сигналов сделаны на атомарных неблокирующих операциях.

osMutex osSemaphore. Мьютексы - Блокировки, я бы про них вообще не упомянал, я их не использую фактически. Т.е. нет в наших задачах условий при которых использование блокировок оправдано, нет необходимости. При разработке программ мы всегда знаем что происходит, всегда допускается обработка данных без ожидания. Никакие системные алгоритмы не попадают в дистрибутив, если они требуют блокировок. В операционке поддерживается нетоторый тип ожидания для работы с разделяемыми ресурсами, можно ожидать семафор. Семафор- это системный объект, который используется для посдчета ресурсов: число свободных блоко в очереди, размер свободного места и т.п. Семафор - счетчик. Когда рессурс освобождается, счетчик увеличивается. В системе можно ожидать освобождения ресурса. Частный случай - ожидание бинарного семафора -мьютека.

osPool Динамическая память, выделение блоков памяти из массива. Концепция такова, пулы памяти используются для выделения буферов обмена, пакетов данных и организации обмена между процессами. Количество блоков памяти в пуле фиксировано. Механизм выделения памяти из пула и возвращения памяти в пул сделан на атомарных неблокирующих операциях. Операции выделения и освобождения памяти не циклические, задержка на выделение и освобождение памяти - не зависит от времени и условий работы.

osMessage osMail отправка сообщений через очереди. Идея очереди состоит в том, что отправители не используют взаимные блокировки типа мьютексы, очереди неблокирующие. Тут есть ноу-хау, таких идей не встречал, реализация оригинатная. Концепция строится на выделении блоков из массива и атомарном неблокирующем способе работы с очередями.

(напишу продолжение)
----

Иногда я думаю, я такой странный человек, который познает мир методом написать про это программу. Я могу четко описать, почему я не могу использовать другие операционки и чужие библиотеки. Они мне не нравятся. Я их не понимаю, пока не посмотрю, как они устроены. Кода смотрю чужие исходники - ужасаюсь и не хочу использовать, в большинстве случаев качество кода, в смысле эффективности оказывается неприемлемо. Иногда мне проще написать код самому, чем подстраиваться под чужую логику.

Чем больше описываю, тем больше сам понимаю, как это эффективно использовать.



Комментариев нет:

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