BMSTU/01-embedded-systems-softwar...

1275 lines
157 KiB
TeX
Raw Normal View History

\documentclass{article}
2023-10-24 15:47:15 +03:00
\input{settings/common-preamble}
\input{settings/bmstu-preamble}
\input{settings/fancy-listings-preamble}
\author{Фёдоров Сергей Владимирович (svf\_@mail.ru)}
\title{Проектирование программного обеспечения встраиваемых (встроенных) систем}
\date{2021-09-07}
\def\makeyear{2021}
\begin{document}
\maketitle
\newpage
\tableofcontents
\newpage
\section{Структура курса}
\begin{itemize}
\item встроенные системы и их особенности: особенности архитектур процессоров и периферии, а также их влияние на программное обеспечение с точки зрения программиста
\item простые системы на микроконтроллере без операционной системы
\item оптимизация программ (ручные, компилятора): рассматриваем как пользователи (программисты). Литература: DragonBook
\item операционные системы реального времени и их механизмы на примере μС/OS-II. Будем пользоваться платами альтера (ядро, обвязка, ввод-вывод, интерфейсы внешней памяти), без погружения в ПЛИС
\item стандарты и методика разработки программного обеспечения: как стандарты регламентируют кодирование и как они усложняют процесс выстрела себе в ногу. Стандарты, регламентирующие разработку систем высокой надёжности
\end{itemize}
\section{Введение}
\subsection{Встраиваемая система}
(!) не работает без окружения, не бывает абстрактной, поэтому вернее говорить "встроенная система". Это цифровая или цифро-аналоговая микропроцессорная система, использующаяся для управления техническим объектом, и функционирующая без управления оператором. Оператор может выполнять настройку, но главное, что реакция на события определяется системой. То есть это практически любая компьютерная система, не являющаяся персональным компьютером общего назначения.
Применение:
\begin{itemize}
\item бытовое,
\item транспорт,
\item индустриальное.
\end{itemize}
Жизненно важные применения:
\begin{itemize}
\item медицинские,
\item транспортные,
\item промышленные объекты.
\end{itemize}
\subsection{Особенности встраиваемых систем}
\begin{itemize}
\item размещение непосредственно на объекте, хотя ведутся работы по написанию таких систем, которые можно размещать удалённо
\item непосредственное подключение к датчикам и исполнительным устройствам объекта
\item выполнение огрниченного набора отностительно простых функций:
\begin{itemize}
\item общее программное обеспечение обычно гарантирует работу при любой последовательности действий (например, текстовый редактор: по большому счёту, без разницы в каком порядке писать текст, оформлять текст или выбирать стили)
\item в отличие от привычных программ общего назначения, для встраиваемой системы желательно составить чёткий последовательностный алгоритм работы
\item с объектами реального мира тоже желательно работать по строгой последовательности (например, нет смысла выгружать из элеватора зерно, если под зоной загрузки нет трансопртного средства)
\item чаще всего попытка рассмотреть все возможные варианты действий во встраиваемой системе либо бесконечна, либо приводит к непоправимым ошибкам, часто с материальными последствиями
\end{itemize}
\item работа в условиях реального масштаба времени
\begin{itemize}
\item чаще всего подразумевает использование операционных систем реального времени, возможно, линукс с расширением реального времени, но желательно использовать специализированные (например, если (условная) винда начнёт выполнять задачи связанные с файлом подкачки, система может перестать откликаться на 1-2 секунды, что неприемлемо в системах реального масштаба времени).
\item хотя можно попробовать использовать pcap, как это делает вайршарк, но это чаще всего значительно усложняет систему и выводит за пределы требований по потреблению аппаратных ресурсов и надёжности
\end{itemize}
\item требования высокой аппаратной надёжности
\item требования высокой программной надёжности
\end{itemize}
\subsection{Требования к системе}
\begin{itemize}
\item минимизация энергопотребления
\item минимизация массы и габаритов
\item устойчивость ко внешним воздействиям (температура, влажность, радиация, и т.д.)
\item снижение стоимости производства и эксплуатации
\item дополнительные требования - ЭМС, взрывозащита, и т.д.
\end{itemize}
\subsection{Экономические критерии}
\begin{itemize}
\item стоимость разработки устройства (сюда входят стоимость офиса, бухгалтера, и прочие накладные расходы) то есть человекомесяц * 3-4
\item стоимость производства устройства
\item поддержка
\item эксплуатация
\end{itemize}
Часто взять готовое аппаратное решение дороже в разработке, но дешевле в изготовлении, например:
\begin{figure}[H]
\centering
\includesvg[scale=.59]{pics/01-ess-00-economics.svg}
\caption{Отношение затрат на разработку и тиражирование к объёму выпуска }
\label{pic:economics}
\end{figure}
Где:
\begin{itemize}
\item стоимость изделия S
\item расходы на разработку NRE
\item объём выпуска N
\item стоимость изготовления P
\end{itemize}
\[ S = P + \frac{NRE}{N} \]
(зелён) случай а: разработка = 100к, изготовление = 2к
(оранж) случай б: разработка = 1кк, изготовление = 1,5к
точка пересечения графиков по этим входным данным где-то в районе 1700 изделий, то есть, если нужно в рамкаж полного жизненного цикла создать меньше 1700 изделий нет смысла делать "собственную плату".
На открытом рынке, чем быстрее спроектировали, тем больше продали. Так используются готовые платформы и средства быстрой разработки. При этом жизненный цикл разработанных как раньше, так и позже устройств чаще всего завершается в одной точке, то есть долгий выход на рынок - это финансовые потери. Для отработки опытных образцов нужно стараться отрабатывать на трёх и более тестовых стендах, чтобы иметь возможность отлавливать ошибки. Всегда надо считать, сколько я потратил денег работодателя, пока бился головой о стену. При возникновении ошибок, по деньгам обычно выгоднее, например, переделать плату, чем вымучивать что-то из головы инженера.
\subsection{Технические критерии}
\begin{itemize}
\item энергопотребление
\item производительность
\item ...
\end{itemize}
\subsection{Инженерные требования}
\begin{itemize}
\item гибкость (снижение невозвратных потерь при изменении изделия или разработке нового)
\item переностимость (использование частей при возможном переходе на другую платформу). Здесь главное не перейти в категорию разработки гибких решений ради разработки гибких решений, то есть переносимость вообще всего, когда она не очень нужна - вредит разработке. Преждевременная оптимизация - корень всех бед.
\item технологические ограничения при производстве и ремонтопригодность
\end{itemize}
\section{Классификация процессоров}
\subsection{Общего назначения}
\begin{itemize}
\item Intel, AMD, хотя они становятся системами на кристалле
\item широкий диапазон применения, требует периферии, памяти, итд,
\end{itemize}
Примеры: ARM Cortex-A, x86
\subsection{Специализированные и системы на кристалле}
\begin{itemize}
\item разрабатывается для конкретного применения: например, чипсет для телефона
\item включает ядра и логику которая позволяет решать задачи системы целиком
\item представляет из себя систему, которая создержит процессорное ядро и аналоговые модули для аппаратной обработки информации
\item чаще всего создаются на основе программно реализованных ядер (на ПЛИС)
\end{itemize}
Примеры: altera nios, xilinx microblaze, cypress PSoC
\subsection{Микроконтроллеры}
\begin{itemize}
\item процессорное ядро и набор универсальной периферии для реализации автономной системы
\item при использовании микроконтроллеров достигается максимальная оптимизация по энергопотреблению
\item интегрированная память программ и данных
\end{itemize}
Примеры: ARM, MIPS, intel x51, ...
\subsection{Цифровой обработки сигналов}
\begin{itemize}
\item в современной технике - редкое явление в чистом виде, заменяются, например, графическими процессорами и специализированными устройствами.
\item специализированные, предназначены для чрезвычайно узких задач, таких как, например, преобразование фурье и других обработок звука, видео
\item в процессорах цифровой обработки сигналов реализована собственная оптимизированная система команд, то есть одна команда ЦОС - это обычно 3-4 команды процессора общего назначения
\end{itemize}
Примеры: Analog Devices ADSP, Texas Instruments TMS
\section{Характеристики процессоров}
\begin{itemize}
\item архитектура ядра и система команд
\begin{itemize}
\item скалярные, суперскалярные
\begin{itemize}
\item скалярные - выполняет в один момент времени только одну команду, в современных используется конвейеризация, то есть команда будет выполняться за 3-5 стадий. На рисунке ниже показано, что блок С1 обрабатывает команду 1, вызывая её из памяти, на втором такте С2 декодирует команду, а С1 уже берёт следующую, на третьем такте С3 вызывает операнды первой команды, на четвёртом С4 выполняет команду 1, а на пятом такте С5 записывает результат выполнения команды, в это время блоки С1, С2 продолжают выборку и декодирование следующих команд
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{01-ess-00-conv.png}
\caption{Принцип работы конвейеризации в процессорах}
\label{pic:conv}
\end{figure}
\item суперскалярные - может выполнять более, чем одну инструкцию на одном этапе конвейера - все современные процессоры для персональных компьютеров и мобильных устройств. Суперскалярный процессор сам может определять сколько и чего выполнять в зависимости от ситуации.
Cortex-A
\item out of order execution (OoOE) - суперскаляры с внеочередным исполнением, сейчас почти все современные процессоры
Intel, AMD
\end{itemize}
\item VLIW (very long instruction word) C6x DSP (TI), Эльбрус - загружает сразу несколько инструкций подряд по количеству исполнительных программных модулей в одно длинное слово, в С6х это 8 слов. обычно в среднем 2-3, обычно не удаётся уложить настолько эффективно. В отличие от ооое не может предсказывать некоторые ветки интерпретации, поэтому сложнее оптимизировать, хотя сам процессор судет проще в архитектуре
\item CISC, RISC
\end{itemize}
\item конвейеризация
\begin{itemize}
\item глубокая не всегда хорошо для микроконтроллера или процессора общего назначения, потому что если следующий операнд зависит от предыдущего результата - конвейер встанет.
\item часто используется предсказание переходов, но бывают ошибки предсказания перехода, тогда конвейер опустошается и всё, что было декодировано - перестаёт быть актуальным.
\item TigerSharc 3-хстадийный конвейер (1 jmp A(db) 2 add... 3 mov...) отложенный переход delayed branch
\item в МК и встраиваемых процессорах обычно короткие этапы конвейера, а в настольных длинные, в связи с тем, что там более быстро и более сложная система предсказания конвейера (уменьшается шанс ложного предсказания и сбрасывания конвейера)
\end{itemize}
\item архитектура прерываний (приложить информацию из раздела: Семинар от 13.09.2021)
\begin{itemize}
\item уровни
\item векторизация
\item вложенность
\end{itemize}
\item DMA и периферийные FIFO
\item Механизм доступа к памяти программ и кэш
\item наличие MMU для реализации сложных систем на основе ОС
\end{itemize}
\subsection{CISC-RISC}
complex, reduced instruction set computer. Сложный, полный и сокращённый набор инструкций. хотя речь больше не в сложности, а в самом наборе. RISC придумали как минимальный набор простейших инструкций для наиболее быстрого декодирования. со сложными инструкциями - одна может реализовывать сразу несколько действий. типичная черта в рисках разделены все операции загрузки-сохранения и для обработки. лоад-стор архитектура. это легче конвейеризуется, легче делать внеочередное исполнение, возможна более простая логика АЛУ. Практические все современные - это RISC. недостаток: код получается более громоздким.
\subsubsection{CISC-Style MSP430}
Сброс двух битов в регистре управления таймером для его остановки
мнемоника, разрядность (слово), константа - значение за решёткой, адрес
\begin{verbatim}
bic.w #MC0|MC1, &TACTL //3сл, 5т
\end{verbatim}
читаем значение в регистре, смотрим на значение которое нужно записать, пишем в регистр, сохраняем в регистр - это всё одна инструкция (3 слова (сама инструкция и две константы), 5 тактов).
\subsubsection{RISC-Style}
\begin{verbatim}
load.w #TACTL, R4 //2сл, 2т
load.w @R4, R5 //1сл, 2т
load.w #MC0|MC1 //2сл, 2т
bic.x R6, R5 // 1сл, 1т
store.w R5, @R4 // 1сл, 2т
\end{verbatim}
\begin{enumerate}
\item загрузили текущее значение адреса TACTL,
\item положили в R5 значение по адресу R4
\item в R6 загружаем маску
\item сбрасываем регистр по R5 по маске R6
\item сохраняем обратно
\end{enumerate}
7 слов 9 тактов. работает дольше, но поскольку сам мк работает на низких частотах и с низким энергопотреблением. CISC инструкция в итоге работает дольше, потому что нужно больше действий сделать и инструкция сложнее. То есть если нужна скорость - используем RISC. При использовании внешней памяти, например, SDRAM можно получить значительные задержки до тысячи тактов на обращение, особенно если память решит обновиться.
\section{Аппаратные интерфейсы}
Классификация интерфейсов:
\begin{itemize}
\item по назначению
\begin{itemize}
\item внутрисистемные RapidO, UTOPIA, I2C, SPI
\item системные PCI, PCIe
\item Периферийные UART, RS232
\item межсистемные Ethernet, Bluetooth
\end{itemize}
\item по организации
\begin{itemize}
\item физическая
\item логическая, есть особенности: арбитраж, состояние, адресация, система команд, форматы данных
\end{itemize}
\item по степени синхронизации
\begin{itemize}
\item синхронные - привязаны к синхронизирующему сигналу (как правило малые расстояния, высокая скорость)
\item асинхронные - данные отвязаны от синхронизирующего сигнала, но синхронизация заложена в сами данные, от RS-232 до PCIe. Есть необходимость закладывать в протокол рукопожатие, квитирование.
\item изохронные - привязаны ко времени, но также используются принципы асинхронности в части сохранения целостности данных.
\end{itemize}
\item по топологии
\begin{itemize}
\item точка-точка - только два устройства, но нет необходимости в арбитраже
\item общая шина - экономия ресурсов, возможность расширения, необходимость адресации и арбитража (I2C).
\item цепочная - возможность построения сложных систем, но сложная логическая организация.
\end{itemize}
\end{itemize}
Тенденции к последовательным интерфейсам (ATA вытеснен SATA, PCI вытеснен PCIe, итд). Потому что скорость больше. TTL-CMOS (<100MHz), LVDS (<10GHz). Физически протоколы усложнились, то есть просто байтик передать стало сложнее, но объёмы передаваемых данных также значительно увеличились, стали нужны библиотеки для передачи данных.
\section{Проектирование ПО}
Встроенные системы делятся на:
\begin{itemize}
\item те, которым не предъявляются требования к реальному времени, например, обработки информации, измерительные без учёта частоты дискретизации, интернет вещей
\item те, которым предъявляются требования к реальному времени - время реакции на внешние события нормируется, например, весы.
\end{itemize}
Системы реального времени - это аппаратно-программный комплекс реагирующий в предсказуемые времена на непредсказуемый поток внешних событий. Система должна сформировать реакцию в течение заданного промежутка времени. Чаще всего нормируется только максимальное время, но бывает, задаётся и минимум. Система должна выполнять временн\'{ы}е требования при любой комбинации внешних событий. Отсутствие реакции в заданный срок - это ошибка, но разной степени тяжести.
Системы реального времени делятся на системы жесткого реального и мягкого реального времени.
\begin{itemize}
\item Системы жёсткого реального времени: если опоздали - уже и не надо ничего делать, как деление на ноль (например бортовые системы, системы аварийной защиты, и так далее)
\item Системы мягкого реального времни: несоблюдение требований ошибкой не является, но крайне нежелательно (задержка допустима, но снижает качество системы, ухудшает user experience)
\end{itemize}
Внутри системы может происходить деление на процессы и уже каждому процессу может быть предъявляться требование жёсткого или мягкого реального времени.
Самая простая организация встроенного программного обеспечения - это суперпетля (по сути, это вайлтру с прерываниями)
\section{Реализация простых систем}
\subsection{Суперпетля}
\begin{lstlisting}[language=C,style=CCodeStyle]
void main() {
init_vars();
init peripherals();
while (1) {
process_key();
process_sensors();
//...
}
}
\end{lstlisting}
то есть обычно это простой бесконечный цикл, который иногда прерывается на аппаратные прерывания. Иногда всё выводится в прерывания, тогда в мейне идём в режим сна или исполняем микроконтроллерную инструкцию для сна. События будут обрабатываться обработчиками прерывания, а фоновые события - в строгом порядке, как их перечислили. то есть если нажали кнопку два раза - второй раз обработаем только когда пройдём весь цикл. Главные проблемы
\begin{itemize}
\item это то, что максимальное время отклика уровня задач - это максимальное время выполнения всех задач (может быть достаточно большое, если надо, например, отформатировать флешку, или принять-отправить что-то по эзернету).
\item без операционной системы нет способа передачи информации между задачами. Нужно придумывать собственные механики вроде общих переменных итд. Чем сложнее система - тем сложнее велосипеды передачи сведений.
\end{itemize}
Обычно есть обработчики прерываний от периферии, но отдельно можно выделить прерывания от аппаратного таймера. То есть можем сделать прерывания по синхронным событиям, раз-в-сколько-то-сек.
\begin{figure}[H]
\centering
\input{pics/01-ess-00-longinterrupt}
\caption{Проблема длинного обработчика прерываний}
\label{pic:longinterrupt}
\end{figure}
Когда мы дробим обработчик таймера на несколько фаз мы решаем проблему, показанную на рисунке (пропущенные обработчики прерываний от serial). Поэтому важно понимать карту времени, и стараться не блокировать микроконтроллер надолго. с уровнем задач обычно проблем нет, а с другими прерываниями вполне могут быть. Также важно не забывать про пролог и эпилог прерывания (установка контекста, и так далее). Поэтому всегда предпочтительно - короткие прерывания
\begin{figure}[H]
\centering
\input{pics/01-ess-00-shortinterrupt}
\caption{Проблема длинного обработчика прерываний}
\label{pic:shortinterrupt}
\end{figure}
Надо заметить, что время выполнения цикла не постоянно, О(цикла) это выполнение каждой задачи, $\Omega(\text{цикла})$, когда пустые обработчики - считанные такты и инструкции. Использовать \code{volatile} для переменных разных задач - плохой стиль. То есть если например, включить оптимизации компилятора и объявить переменные для условий внутри вайлтру нулями и не сделать \code{volatile} - компилятор решит, что это недостижимый код и его можно убрать. Так оптимизатор скомпилирует нам пустой вайлтру. То есть аппаратные прерывания компилятором не особо учитываются. volatile не оптимизируется компилятором никогда, потому что это всегда явное обращение к памяти или регистрам ввода-вывода.
Резюме:
\begin{itemize}
\item [-] Не требует памяти на операционную систему
\item [-] не требует памяти для сохранения контекста
\item Значительно усложняется с повышением сложности системы
\item время отклика растёт
\item Сложно переписать (обычно в случае внесения правок, затрагиваются другие сегменты системы)
\item [+] Нужно понимать карту времени и точно её расчитывать
\item [+] Возможны модификации по организации очереди на уровне задач
\item [+] применяется для простых систем
\end{itemize}
Рекомендации по дисциплинированному подходу при работе с периферией (и к программированию архитектуры, алгоритмов)
\begin{enumerate}
\item Понимать (сформулировать) требования к периферийному модулю на естественном языке;
\item Изучить документацию и понять, насколько требование выполнимо \footnote{включая документ errata - документированные ошибки в кремнии}
\begin{itemize}
\item определяем режим работы периферийного модуля
\item задокументировать выбранные решения
\item определить конкретные значения для регистров
\begin{itemize}
\item для инициализационной настройки
\item для управления периферийным модулем в режиме работы (функционирования)
\begin{lstlisting}[language=C,style=CCodeStyle]
init_peripherals() {
init_timers()
init_uart();
init_ADC();
//...
}
\end{lstlisting}
\end{itemize}
\item при обращении к регистрам оставлять комментарии, чтобы не лазить в документацию для каждого изменения
\item определить последовательность инициализации периферийных модулей (драйвер)
\begin{itemize}
\item функции инициализации
\item функции управления
\end{itemize}
\item разделить интерфейс аппаратуры и интерфейс кода, что позволит перенести код в операционную систему реального времени
\item определить и реализовать механизмы обмена данными исходя из требований конкретной задачи.
\begin{itemize}
\item с уровнем приложения
\item с аппаратурой (по опросу, по прерыванию, с использованием ПДП (прямого доступа к памяти)).
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-burstwrite.svg}
По опросу обычно делают короткие события не требующие быстрой реакции, вроде проверки ножки, изменения направления ветра или для приёма бёрстов данных.
ПДП не осуществляет анализ данных по приёму, поэтому не факт что хорошая идея так принимать ethernet пакеты.
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-burstanswer.svg}
Если есть кэш и план по использованию прямого доступа к памяти - нельзя забывать о кэше. При использовании ПДП мы работаем как будто напрямую с подсистемой ПДП, но фактически работаем с кэшем, поэтому обязательно когда мы собрались записать данные - нужно выполнять cache flush.
\begin{lstlisting}[language=C,style=CCodeStyle]
write_data(a);
cache_flush(a, sizeof(a));
start_data_tx(a, sizeof(a));
\end{lstlisting}
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-cacheflush.svg}
второй случай - подсистема прямого доступа к памяти данные модифицировала, а для процессора они остались старые, потому что кэш обычно надо сделать cache invalidate
\begin{lstlisting}[language=C,style=CCodeStyle]
if (dma_finished()) {
cache_invalidate(a, sizeof(a));
process_data(a);
}
\end{lstlisting}
\end{itemize}
\end{itemize}
\end{enumerate}
\section{Оптимизация кода}
Желательно не делать руками то, что должен делать компилятор. Чаще всего используются готовые наборы оптимизаций, например, для gcc - это о0, о1, о2, о3. Также есть оптимизация по размеру, она используется гораздо реже - это оптимизация с точки зрения быстродействия. В о3, например, включена оптимизация type-based alias analysis и она может значительно повлиять на работоспособность нехорошо написанного кода. Обычно ограничиваются оптимизацией о2, но о3 неплохо тоже прогнать, чтобы понять, что код написан хорошо. Если нужно рассматривать на уровне ассемблера - пошаговую отладку нужно вести на о0 или о1 - код компактнее, а читаемость сохраняется. Если отлаживается без оптимизаций совсем - разница по быстродействию может очень сильно отличаться. Между о0 и о2 быстродействие не будет значительно отличаться, но на ассемблере код будет читаемее. Когда приступаем к тестированию релиза, его нужно проводить на том уровне, на котором будет сделан релиз. (TOREAD: gcc optimize options). Важно, что есть оптимизации, не входящие в наборы.
\begin{itemize}
\item оптимизация размещения данных (+ данные из раздела \hyperref[sec:sem2]{\ref{sec:sem2}}), например, выравнивание переменных в памяти.
\begin{verbatim}
unsigned char a;
unsigned int b;
\end{verbatim}
если \code{int} 32 бита, а \code{char} 8 бит, получится, что в памяти будет пустое место. Произойдёт выравнивание по разрядности. Так если объявлять структуру вперемежку, может получиться значительно дефрагментированное пространство памяти. Поэтому, желательно объявлять по уменьшению размера, тогда переменные будут укладываться по границе своей размерности. По умолчанию, структуры не упакованы. Это происходит для оптимизации, чтобы слова не располагались в полусловах. Например, IP-пакеты, если их объявить <<как есть>> - будет много дырок, поэтому нужно подумать об упаковке пакета. Стандарты кодирования не рекомендуют использовать \code{int}, \code{char}, и так далее, а рекомендуется использовать с фиксированными типами, так, например, в $\mu$C/OSII: \code{INT8U}, \code{INT8S}.
\item оптимизация кода
\begin{enumerate}
\item ручная (чаще всего не благо, а зло). Может осуществляться на уровне функций или compilation units.
\begin{itemize}
\item \code{inline} фукнции - заставляет компилятор не вызывать функцию, а вставить её в код <<как есть>>, чтобы не тратить время на вызов и подстановку параметров, а вызвать на месте. На уровнях о2-о3 компилятор сам решает, что инлайнить, а что нет, и обычно делает это лучше разработчика.
\item оптимизация операторов выбора: \code{switch} - это не самая оптимальная конструкция, компилятор последовательно должен сравнить кейсы.
\begin{lstlisting}[language=C,style=CCodeStyle]
switch(a) {
case 0: // code 1
case 1: // code 2
case 2: // code 3
}
\end{lstlisting}
Если вариантов пять - не страшно, но если 105 - дорого, потому что до 105-го варианта мы дойдём только через 105 сравнений. Вручную можно это оптимизировать хотя бы написанием более частых вариантов в начале. Ещё лучше заменить на массив функций и вызывать по индексу. В кейсах желательно сделать выборку вариантов последовательно, а не вразнобой, так компилятор может дополнительно оптимизировать выборку.
\item использование предвычисленных таблиц (например, значений синуса-косинуса), вместо библиотечных вычислений и использование интерполяции, если точность позволяет.
\item ассемблерные вставки (обычно компилятор делает это лучше, чем программист). Желательно не использовать, но если используются, обязательно вынести такой код в отдельную функцию, а не инлайнить его, или даже в отдельный файл \code{asm}. Также обязательно нужно ознакомиться с тем, как передаются параметры, и как возвращаются результаты (\textit{calling convetion}). Такой код точно не будет портируемый. Если у функции мало параметров, то обычно значения передаются через регистры процессора, а если много - часть через регистры, а остальные через стек, и такой вариант будет вызываться дольше, поскольку будет затрачено время на сохранение стека, это поведение нужно учитывать при написании ассемблерных вставок. Ассемблерные вставки могут помешать оптимизации компилятора. Желательно не экспериментировать с ассемблерными вставками внутри высоконагруженных циклов.
\item ознакоиться с \textbf{calling convetion} процессора и компилятора и понять сколько максимально параметров можно передавать в функцию, чтобы они передавались в регистрах.
\end{itemize}
\item оптимизация компилятора (+ данные из раздела \hyperref[sec:sem3]{\ref{sec:sem3}})
\begin{itemize}
\item локальные. Работают со скомпилированным небольшим куском кода, например, peephole
\begin{verbatim}
mov R2, R5
....
mov R4, R2
\end{verbatim}
оптимизация сразу скопирует в \code{R4} из \code{R5}, минуя \code{R2}.
\item уровня функций:
\begin{itemize}
\item оптимизация циклов,
\item назначения регистров,
\item и так далее.
\end{itemize}
Например:
\begin{enumerate}
\item регистровые (register) оптимизации включаются с уровня о1, поэтому не нужно мешать компилятору использованием ключевого слова \code{register}. Компилятор обычно использует от $\frac{1}{2}$ до $\frac{2}{3}$ регистров для хранения результатов промежуточных вычислений, оценивая время жизни переменной и частоту обращения к ней.
\item удаление общих подвыражений (common subexpression elimination): если есть код
\begin{verbatim}
a = c + k * 5;
b = e + k * 5;
\end{verbatim}
то умножение будет вычислено и ляжет во временную переменную
\item снижение стоимости операций (strength reduction), например умножения и деления заменяются на сдвиги. Но деление знаковых - не факт, потому что знак результата деления не регламентируется.
\item
\end{enumerate}
\item глобальные: на уровне единиц компиляций, то есть, файла и/или заголовка.
\begin{itemize}
\item автоматический inline
\item автоматическое назначение регистров (auto register allocation)
\item cross call - оптимизация на уровне файла. Выделение общих частей кода (иногда даже совсем не похожих с точки зрения программиста) в отдельную функцию. Эта оптимизация бывает многоуровневая, но это значительно ухудшает возможности отладки в ассемблере. Обычно жто оптимизация по объёму, но иногда это оптимизации и по быстрожействию.
\item constant folding (оптимизации констант (как стринг пул в джаве))
\item удаление избыточных переходов (сокращает джампы в ассемблере)
\item оптимизация исходя из дальности перехода (часто переход jmp это не прямой переход по адресу, а смещение адреса инструкций)
\item dead code elimination удаление кода, который не влияет на результат выполнения программы (с точки зрения компилятора) например посчитали сумму массива но нигде не использовали
\item недостижимый код (например, код после бесконечного цикла)
\begin{lstlisting}[language=C,style=CCodeStyle]
int flag;
void IRQ_Handler() {
//..
flag = 1;
//..
}
int main() {
flag = 0;
while (1) {
if(flag){
//...
flag = 0;
}
}
}
\end{lstlisting}
на уровне кода внутри цикла изменение флага недостижимо, надо делать волатайл флаг при инициализации. При этом волатайл надо использовать ТОЛЬКО в обработке прерываний, потому что нужно использовать механизмы межпроцессного обмена.
\end{itemize}
\end{itemize}
\end{enumerate}
\item как не надо делать (программа на частоте 1.2КГц)
\includegraphics[width=5cm]{01-ess-00-code.png}
\item Измерение быстродействия кода
\begin{enumerate}
\item замерить осциллографом (минимальное вмешательство в код)
\begin{verbatim}
\#define DEBUGON PORTD |= 0x02
\#define DEBUGOFF PORTD &= 0x02
\end{verbatim}
\item использовать таймер (а=таймстэмп, б=таймстэмп, дельта = б-а)
\item расширение функциональности таймера (таймер-с-плюсом perfomance counter) счётчик производительности. Фактически, это серия счётчиков. Модуль в каждой секции реализует два счётчика: первый считает такты процессора, второй считает количество запусков секции (time и event). Добавляя этот компонент в пакет поддержки платы добавляется заголовок с пятью макросами и функцией. Позволяет узнать точное количество тактов для секции кода. Как работать:
\begin{itemize}
\item сбросить все счётчкик
\item запускаем нулевую секцию (сколько всего времени производили измерения)
\item обрамляем код макросами \code{PERF\_BEGIN и PERF\_END}
\item вызываем завершение измерений \code{PERF\_STOP\_MEASURING}
\item выводим результаты (perf\_print\_formatted\_report)
\end{itemize}
\item профайлер (памяти, быстродействия и другие). Нас интересует профайлер быстродействия. Работает обычно несоколько иначе, чем performance counter, минимально влияет на код. Периодически считывает значение счётчика команд (инструкций) и в момент вызова счётчика можно понять в какой функции мы находились. В каждой функции добавляется служебный вызов (запишем кто и сколько раз вызывал функцию) чтобы узнать сколько раз и откуда вызывалась функция. В результате будет отчёт компилятора (плоский профиль и граф вызовов) сколько раз мы попали в функцию при прерывании таймера. чем больше вызовов но меньше времени - тем лучше.
\end{enumerate}
\end{itemize}
\section{ОСРВ}
Система реального времени - это система которая способна обеспечить требуемый уровень сервиса в определённом промежутке времени.
Системы реального времени - это аппаратно-программный комплекс реагирующий в предсказуемые времена на непредсказуемый поток внешних событий. Система должна сформировать реакцию в течение заданного промежутка времени. Чаще всего нормируется только максимальное время, но бывает, задаётся и минимум. Система должна выполнять временн\'{ы}е требования при любой комбинации внешних событий. Отсутствие реакции в заданный срок - это ошибка, но разной степени тяжести.
Системы реального времени делятся на системы жесткого реального и мягкого реального времени.
\begin{itemize}
\item Системы жёсткого реального времени: если опоздали - уже и не надо ничего делать, как деление на ноль (например бортовые системы, системы аварийной защиты, и так далее);
\item Системы мягкого реального времни: несоблюдение требований ошибкой не является, но крайне нежелательно (задержка допустима, но снижает качество системы, ухудшает user experience).
\end{itemize}
Внутри системы может происходить деление на процессы и уже каждому процессу может быть предъявляться требование жёсткого или мягкого реального времени.
Операционные системы реального времени бывают как общего назначения, так и специальные. Как таковые Windows, Linux не являются операционными системами реального времени, но в линукс есть расширения. Мы будем изучать $\mu$C/OSII потому что портирована в ниос. достаточно распространённая и по ней есть неплохая книга с исходниками. Лицензирование операционных систем реального времени происходит по-разному. Конкретно $\mu$C/OSII для образовательных целей бесплатна.
Операционные системы реального времени предоставляют следующие функции:
\begin{itemize}
\item управления задачами;
\item диспетчеризация задач;
\item выделения памяти;
\item межпроцессного взаимодействия.
\end{itemize}
С точки зрения теории таким образом (модульно) строятся многие системы, не только реального времени. Такой набор базовых функций называется микроядром, а остальные компоненты добавляются по необходимости. Часто в микроконтроллерах используется именно такой подход, в отличие от операционных систем для персональных компьютеров где реализован монолит. Микроядерная архитектура часто обеспечивает не только более высокую скорость, но и лучшую защищённость всей системы, поскольку вся обвязка работает не на нулевом уровне системы, то есть как задачи операционной системы, несмотря на то что внутри ядра часто передача данных происходит быстрее.
Параметры вызова функции, создающей задачу
\begin{verbatim}
OSTaskCreateExt();
// указатель на функцию-задачу
// указатель который передаётся на вход параметр-функции
// указатель на последний элемент стека задачи (на конец массива)
// приоритет (ставим дважды для идентификации задачи)
// стек задачи
// размер стека
// дополнительная память
// опции вызова
\end{verbatim}
При выполнении функции \code{OSStart();} мы не выходим из неё. В коде задач бесконечные циклы, которые выводят в консоль приветствия и спят по три секунды. В $\mu$C/OSII всегда есть задача фона, которая выполняется, когда никто больше не хочет взять процессорное время. Стек в этой системе растёт вниз. $\mu$C/OSII требует уникального значения приоритета для каждой отдельной задачи, то есть мы можем иметь до 256 приоритетов, меньший для фйдл, высший для таймеров.
Иногда удобно запустить всего одну задачу, которая будет полностью управлять остальными задачами в контролируемом порядке и с дополнительными структурами, флагами. Также функция создания задач может быть при необходимости удалена, а удаление и выход из созданных категорически не рекомендуется.
Приоритеты задач (вытесняющая диспетчеризация) высший - меньшая цифра. Если задача высшего приоритета не уходит в сон - менее приоритетные никогда не выполнятся. Если в обработчике вызываются функции ОС, обязательно нужно написать \code{OSIntEnter();} и \code{OSIntExit();}
\subsection{Состояния задач в uC/OSII}
\includegraphics[width=15cm]{01-ess-01-tasks.png}
Основные переходы всегда между \textbf{ready} \textbf{running} и \textbf{waiting}. При возникновении прерывания текущую задачу переводим в состояние \textbf{ISR} но при этом при возврате мы можем быть вытеснены более приоритетными задачами.
ОС бывают:
\begin{itemize}
\item невытесняющего типа (кооперативные). Задачи не вытесняются, а работают пока самостоятельно не освободят процессор. Фактически, это аналог суперпетли.
\item вытесняющего типа (диспетчеризующие). Планировщик решает какие задачи когда выполнять
\begin{enumerate}
\item с приоритетной диспетчеризацией - выполняется текущая готовая к выполнению задача. Как только будет готова более приоритетная - она будет запущена. Задачи вытесняются без спроса, поэтому они не знают, что их сменяли.
\begin{itemize}
\item с фиксированными приоритетами ($\mu$C/OSII) приоритет может менять только программист. существует проблема инверсии приоритетов. Допустим, есть три задачи с разными приоритетами. Задача 1 самая приоритетная, она не выполняется. Задача 3 самая не приоритетная захватывает ресурс А. Приходит запрос прерывания, которого ждёт задача 1, диспетчер снимает задачу 3. Если задаче 1 нужен будет ресурс А, диспетчер отправляет 1 в ожидание и продолжает выполнять 3. Но тут случается плохое - прерывание для задачи 2. диспетчер снимает 3 и выполняет 2. Получается, что задача 1 ждёт менее приоритетную задачу 2, которая даже не задействовала ресурс А.
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-tasks.svg}
Чтобы этого избежать в $\mu$C/OSII введён специальный тип семафоров (мьютексы) которые временно повышают приоритет задач, ожидающих ресурсы.
\item с динамическими приоритетами - приоритет может меняться самой ОС. В операционных системах реального времени жёсткого типа это часто плохо, поскольку наши временн\'{ы}е характеристики могут быть нарушены
\end{itemize}
\item с круговой диспетчеризацией - с выделением временн\'{ы}х квантов.
\end{enumerate}
\end{itemize}
Применяемые в микроконтроллерах операционные системы реального времени обычно используют статическую вытесняющую диспетчеризацию.
\subsection{Характеристики операционных систем реального времени}
Операционные системы реального времени обычно кроссплатформенны гораздо больше, чем обычные операционные системы. Сравнивать характеристики имеет смысл только на одной платформе.
\subsubsection{ Временн\'{ы}е характеристики:}
\begin{itemize}
\item interrupt latency. задержка прерывания - это время от возникновения запроса прерывания до выполнения первой инструкции обработчика. В сложных системах, где прерывания могут проходить несколько этапов - система может влиять, но в основном влияет архитектура процессора;
\item scheduling latency. запаздывание диспетчеризации - это задержка от последней инструкции обработчика до первой инструкции процесса, который был запущен на выполнение как средствие выполнения обработчика. характеристика больше связана с уровнем задач, а не прерываниями
\item context switch time. Время переключения контекста - общее. Время от последней инструкции вытесняемого процесса до первой инструкции запускаемого. Чем сложнее способ и методика диспетчеризации, тем эти времена будут больше.
\item task response time. время отклика уровня задач. также объединяется сразу несколько - от запроса прерывания до первой инструкции процеса, который был запущен прерыванием.
\end{itemize}
Если в коде надолго запретить прерывания - это будет ухудшать вышеупомянутые характеристики.
\subsubsection{Выделение памяти: (2)}
В $\mu$C/OSII применяется непрерывное динамическое выделение памяти. Проблема фрагментации кучи. нет страничной адресации, контроллеров виртуальной памяти. Даже на процессорах общего назначения должны использоваться процессы сборки мусора и дефрагментации кучи. Если ПК застынет на секунду - не страшно, а если это произойдёт в системе реального времени (жёсткого) - мы не выполним требования времени. В системах жёсткого реального времени эта задача часто перекладывается на программиста. Выделяется область памяти равного размера и ОС выдаёт свободный. ни о какой фрагментации речи быть не может, но на программиста ложится задача сборки таких кусков и укладывания туда больших объёмов данных, которые формально могут выходить за пределы выделенной области. Основные функции для работы с памятью - это \code{OSMemCreate();} \code{OSMemGet();}
\subsection{Механизмы операционных систем реального времени}
\subsubsection{Механизмы синхронизации и межпроцессного взаимодействия}
\label{subsubsection:mutex}
\begin{itemize}
\item $\mu$C/OSII ebook
\item $\mu$C/OSII cfgMan. здесь определено множество констант ядра, которыми управляется операционная система.
\item $\mu$C/OSII refMan
\end{itemize}
Средства синхронизации и взаимного исключения между процессами (стр 17-19 из 20 в cfgMan). Все функции названы обдинаково по смыслу, удобно запоминать, но это также недостаток. Бывают ОС POSIX (портируемый стандарт ОС) совместимые и не совместимые. $\mu$C/OSII не POSIX-совместимая, поскольку интерфейсы не кореллируют. Перенос между POSIX-совместимыми системами осуществляется проще из-за совместимого интрефейса.
\begin{itemize}
\item \textbf{Семафоры.} Были предложены Дейкстрой для синхронизации и взаимной блокировки процессов. При разделении ресурсов процессорами нужно обеспечить целостность. Критические секции запрещают всё, в то время как семафор позволяет таргетированно контролировать ресурсы. Семафоры - это некоторая взаимная договорённость, то есть семафоры задача может и не использовать, получив доступ, например к консоли, в обход семафора, но это логическая ошибка. Задача пытается получить доступ к семафору, но диспетчер не позволяет доступ к ресурсу, задача, сама того не зная, отправляется в ожидание. Как только задача 1 освобождает ресурс, диспетчер забирает семафор себе и распределяет ресурс между задачами, которые находились в ожидании. Такой семафор называется двоичным.
Если мы используем семафор для осуществления доступа к ресурсу, обычно двоичный семафор инициализируется как единица, то есть он свободен. Когда ресурс захватывается, семафор декрементируется и ресурс занят. При освобождении ресурса семафор инкрементируется. Семафор можно использовать также для синхронизации задач, в таком случае семафор инициализируется как ноль, то есть сначала занят, и задача, которую мы хотим запустить в нужный момент времени - ждёт этого ресурса. Естественно, так как ресурс занят, \textbf{задача 1}, которую мы хотим запустить, отправляется в ожидание. \textbf{Задача 2}, которая запускает (это может быть, например, обработчик прерываний) в нужный момент освобождает семафор. С этого момента \textbf{задача 1} получает доступ к ресурсу и запускается. Когда первая задача отработала, она снова начинает ждать семафора и пока \textbf{задача 2} снова не освободит семафор.
\item \textbf{Счётные семафоры.} Используются только для контроля доступа. Инициализирован больше, чем единицей, например, 4. При получении доступа к семафору задача декрементирует семафор и семафор уже равен 3. Задачи 2, 3, 4 успешно получают доступ к ресурсу, а задача 5 при попытке получения доступа ждёт пока какая-то из задач не освободит ресурс. Как только ресурс освободится, задача 5 захватывает его и начинает владеть семафором. Это может использоваться для балансировки нагрузки (много источников копирования в папку, или много задач, которые могут работать с Ethernet).
Функции $\mu$C/OSII, которые позволяют работать с семафорами.
\begin{itemize}
\item \code{OSSemCreate()} \textit{создание семафора.} Принимает один параметр - начальное значение семафора. Возвращает указатель на семафор. Например, чтобы удалить семафор, нужно передать именно указатель на семафор.
\item \code{OSSemDel()} \textit{удаление} (обычно в операционных системах реального времени не удаляются ресурсы, поэтому и семафор удалять не нужно). В большинстве случаев эта функция не нужна, внимательно нужно отслеживать необходимость использования. Принимает также опции удаления, как именно должны себя повести задачи, которые заняты семафором.
\item \code{OSSemPend()} \textit{получение доступа к ресурсу} (попытка захвата семафора). Принимает семафор, таймаут (в тактах системного таймера, тиках), указатель на код ошибки. Если тиков 0 - ждём бесконечно. Код ошибки (по нему мы узнаём, дождались мы или нет). Хорошим стилем является обязательный анализ ошибок (как минимум ифчиком проверить указатель на ошибку на \code{OS\_ERR\_NONE}).
\item \code{OSSemPost()} \textit{освобождение ресурса семафора.} Принимает семафор, вытесняет вызвавшую задачу и даёт доступ ожидавшей
\item \code{OSSemAccept()} \textit{неблокирующая попытка получения доступа.} Принимает только указатель на семафор, возвращает значение, которое имел семафор, в момент предшествующий вызову этой функции. Если там не 0, мы получаем доступ и значение декрементируется.
\end{itemize}
\item \textbf{Mutex.} (mutual exclusion) взаимное исключение. В большинстве операционных систем мьютекс это двоичный семафор, кроме $\mu$C/OSII. В $\mu$C/OSII это двоичный семафор, который решает проблему инверсии приоритетов. Каким образом решает? При создании мьютекса ему передаётся приоритет. Так как $\mu$C/OSII это операционная система с фиксированными приоритетами, для мьютекса нужно выделить отдельный приоритет, который не используется в других задачах и обязательно он должен быть выше, чем у любой задачи, которая будет использовать этот мьютекс. Как только какая-то задача пытается получить доступ к мьютексу, который занят, у задачи, которая владеет мьютексом становится приоритет мьютекса (становится выше, для задачи со средним приоритетом становится невозможно захватить ресурс).
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-mutex.svg}
Проблема, например, может быть, если есть больше задач с разными приоритетами. Нужно разруливать вручную, внимательно расставляя приоритеты задач. Функции такие же как с семаформаи. В создании есть больше опций ошибок. Важная ошибка - ошибка о существовании такого-же приоритета или при задании недопустимого приоритета. Остальные функции идентичны.
\item \textbf{Флаги.} Используется для синхронизации и в некотором смысле для передачи информации. Флаги реализуются с помощью регистра (обычно переменная соответствующая разрядности процессора). Функции установки/сброса позволяют по маске в выбранные биты записать 0 или 1. Процесс, который ожидает флаг формирует свою маску и может ждать чтобы в этой маске флаги были установлены или сброшены. Есть 4 варианта ожидания: все установлены, все сброшены, хотя бы один установлен, хотя бы один сброшен. Позволяет создавать разные синхронизации:
\begin{itemize}
\item от многих к одному, тогда каждый записывает по флажку, а один смотрит по маске от всех;
\item от одного ко многим, тогда один пишет серию флагов, а разные ждут их. Получается в некотором роде менеджмент;
\end{itemize}
Флаги можно использовать как состояние системы (биты инициализации разных подсистем). Функции для работы с флагами (у флагов отдельная структура, она немного больше):
\begin{itemize}
\item \code{OSFlagCreate()} указываем начальное значение флагов, возвращает указатель на \code{OS\_FLAG\_GRP};
\item \code{OSFlagPost()} принимает флаги, \code{OS\_FLAGS} маску, \code{opt} что нужно сделать - установить или сбросить флаги;
\item \code{OSFlagPend()} режимов побольше. Принимает указатель на флаги, таймаут (тоже самое, что в семафоре), код ошибки. \code{flags} тоже маска, \code{wait\_type} - это опция ожидания (константы по 4м обозначенным выше вариантам ожидания). Есть ещё одна опция - сбрасывание флагов установленного режима. Для этого нужно использовать \code{WAIT\_SET\_ANY} и использовать \code{OS\_FLAG\_CONSUME};
\item \code{OSFlagAccept()} используется гораздо реже, просто смотрим возвращаемое значение, если 0 значит не получили доступ по сочетанию флагов, а если не равно нулю, то надо смотреть вернувшееся значение.
\end{itemize}
\item \textbf{Почтовые ящики.} \textit{Mailboxes.} В основном это межанизм обмена данными \textbf{interprocess communication} и только во вторую очередь механизм синхронизации. Задача может положить в почтовый ящик сообщение, и сообщение может быть считано другой задачей, после чего почтовый ящик становится свободен и в него снова можно положить письмо. В данной концепции почтовый ящик может хранить только одно сообщение. При попытке положить второе - возникает ошибка. Функции для работы с почтовыми ящиками:
\begin{itemize}
\item \code{OSMboxCreate()} возвращается \code{OS\_EVENT}. Параметр - нетипизированный указатель, то есть должно быть согласование трактовки хранения. Можем положить в ящик какое-то инициализационное значение.
\item \code{OSMboxPost()} указываем на почтовый ящик и передаём значение, которое нужно поместить. Если в ящике уже есть сообщение - дадут ошибку \code{OS\_ERR\_MBOX\_FULL};
\item \code{OSMboxPostOpt()} помимо события и сообщения есть поле опций - определяет как поместить сообщение в почтовый ящик. Если делаем без опций то опцией по умолчанию считается \code{POST\_OPT\_NONE}. Может быть бродкаст (\textbf{Задача 1} помещает в почтовый ящик, из него хотят читать \textbf{задачи 2, 3, 4}. Предположим, \textbf{задача 2} более приоритетная. Если при публикации сообщения был использован обычный \code{OSMboxPost()} то сообщение всегда получит задача 2 остальные остаются ждать. если же указана опци я
\code{POST\_OPT\_BROADCAST}, то сообщение получат все задачи, которые прямо сейчас ждут сообщений в ящике) или, например, \code{POST\_OPT\_NO\_SHED}, Задача 1 поместила в почтовый ящик сообщение, но диспетчер задач не вызывается вплоть до последнего сообщения, и все читатели замораживаются, чтобы не вытеснить того, кто кладёт сообщения в почтовый ящик, этот флаг позволяет обеспечить более-менее одновременную доставку сообщений).
\end{itemize}
\item \textbf{Очереди.} \textit{\setword{Queue}{Word:Queue}}. Если остальные средства могут использоваться как для синхронизации, так и для передачи данных, то очереди это в чистом виде механизм передачи данных. Ожидаемо, это FIFO. Помимо стандартных функций \code{Create}, \code{Post}, \code{Pend}, \code{Accept} есть ещё \code{PostFront}. При создании обязательно указываем массив из указателей - собственно очередь, в котором будет храниться информация. \code{Pend} посылает сообщение в конец массива. \code{PostFront} помещает сообщение в начало очереди, то есть первый кто будет читать очередь - получит это сообщение.
Опции отправки сообщения примерно те же самые. Так, например, \code{OS\_POST\_NO\_SCHED} временно блокирует диспетчер задач, поэтому задача, передающая сообщения в очередь должна с этой опцией класть все сообщения кроме последнего. У \code{Pend} есть таймаут - это механизм похожий на связку \code{Accept()}+\code{OSTimeDly()}, но таймаут более асинхронный, то есть событие сразу срабатывает, в то время как \code{Accept}+\code{OSTimeDly} позволяет более точно разделить работу по времени. Функция \code{OSQFlush} очищает очередь.
\end{itemize}
\subsubsection{Управление временем и задачами}
Основные функции:
\begin{itemize}
\item \code{OSTimeDly} - понятно просто задержка;
\item \code{OSTimeDlyHMSM} - привязан к тикам системы на кристале это важно;
\item \code{OSTimeDlyResume} - силовой вывод из задержки таймера;
\item \code{OSEventPendMulti} - ожидание множества задач, создаёт не портируемый код, мало какие ОС поддерживают такое ожидание.
\end{itemize}
Таймеры:
OSTmrCreate, Del, NameGet, RemainGet
Позволяет зарегистрировать и однократно или периодически вызывать callback (функцию обратного вызова). У таймера при создании есть параметры: колбэк, аргументы колбэка, имя код ошибки, делей, период и опт. опт может принимать значение периодик или ваншот. Если ваншот то используется только параметр дилэй и колбэк вызывается через дилей тиков таймера. У таймера может быть настроен делитель системного таймера. если периодик - сначала ждём делей а потом срабатываем через период. Колбэк это всегда войд функция с двумя войд* аргументами - таймер и аргументы. Создав таймер мы с ним работаем функциями старт и стоп. У стопа есть дополнитеьлный параметр опт и колбэк арг. Когда мы останавливаем таймер - это нарушение порядка работы и это значит КБ больше не вызовется, и следовательно опции остмроптнон - не вызывать колбэк, остмроптколбэк - при остановке вызов колбэка вне периода, остмроптколбэкарг - чтобы отличить обычный вызов и этот, завершающий.
колбэк вызывается не в контексте таймера. работать лучше через ПЯ - колбэк пишет в ПЯ, а задача которая работает по таймеру (создала таймер) - должна из этого ПЯ читать. внутри колбэка лучше не делять никаких тяжеловесных действий.
Функция OSTmrSignal(). обычно не вызывается пользователем. Для использования таймеров первое что надо сделать - проверить включены ли они в БСП. открываем БСП эдитор - мейн - адвансд - укос2 - ос-тмр-ен и внизу появляется настройка таймеров. приоритет лучше оставить высший, указываем размер стека, указываем сколько всего может быть таймеров, сколько тиков в секунду, указание сколько таймеров надо распараллелить если таймеры срабатывают одновременно.
OSTmrSignal() находится в файле остмр.с. фактически он просто постит семафор с параметром(ОСТмрСемСигнал). Это семафор, который создаётся со значением 0, то есть это семафор, созданный для того, чтобы кто-то его ждал (синхронизация). постит этот семафор ОСТаймТикХук (в котором ещё и происходит деление системного тикера на значение таймера). тайм тик хук - это пользовательская функция, которая может вызываться при создании задачи, но если мы создали таймер - она будет занята. ОСТаймТикХук вызывается в функции ОСТаймТик(), которая внутри себя инкрементирует время и содержит часть диспетчера ОС (одна из самых главных функций в этой цепочке). ОСТаймТик в заголовке с хуками переименовывается и вызывается из папки HAL, то есть из папки поддержки ПЛИС, из функции альтТик. функция альтТик вызывается альтАвалонТаймерСцИрку. чистая ниос функция - обработчик прерывания таймера, который мы настроили в кусисе. эта функция запускается критической секцией с запрещением прерываний, поэтому запретив прерывания где-то внутри прерывания по таймеру мы с большой вероятностью обрушим систему.
пендит семафор таймера функция ОСТмрТаск. Эта функция считает в какой спице оказался таймер и вызывает зарегистрированный колбэк таймера. Колбэк вызывается в контекте таймера (не должна модифицировать никакие переменные других задач) должны совершаться минимальные действия и отправлять какие-то сообщения или флаги.
\subsubsection{Управление задачами в uC/OSII}
Важно, что удаление задач, пауза и возобновлени - нетипично, поскольку это должно по идее быть работой ОС, а не программиста. Задача в $\mu$C/OSII это всегда бесконечный цикл, в котором опрашиваются флажки мьютексов, почтовых ящиков, и так далее.
\begin{enumerate}
\item Создание задач. Функция, без которой ничего не получится - \code{OSTaskCreate()}. Принимает на вход нетипизированный контекст (собственно задача, ссылка на функцию), начальные параметры, вершину стека, приоритет.
\item Создание задач с параметрами. \code{OSTaskCreate()}. Параметры (см выше) + неиспользуемый параметр, дублируем приоритет. Указатель на стек, размер стека, дополнительное расширение для блока контроля задач, опции (разрешить проверку стека, обнулить стек, сохранять при переключении контекста состояние АЛУ с плавающей точкой).
\item Удаление задач (довольно редкая ситуация). Можно сделать сразу или по запросу. Принимает на вход только приоритет. Любой процесс может удалить любой другой процесс или даже себя. Обычно, это не хорошая практика. По запросу - мы сами просим задачу удалиться. То есть, когда задача A хочет удалить задачу B, она вызывает \code{OSTaskDelReq(B)}, которая в свою очередь иногда проверяет флаг (\code{OS\_ERR\_TASK\_DEL\_REQ}), не попросил ли кто её удалиться, и вызывает такой же \code{OSTaskDel(OS\_PRIO\_SELF)}. По запросу - безопасный способ удалять задачи, поскольку задача может сохранить ресурсы, и так далее.
\item Hooks. Задачи можно расширять функциями, описанными в хуках. Можно, например, привязаться к процессу переключения задач и в момент переключения что-то выполнять. \code{OSTimeTickHook()}, например, это занятый таймерами хук.
\end{enumerate}
Самый низкий приоритет у задачи IDLE, чуть выше у Stat. Если используются таймеры - самый высокий приоритет у задач таймеров. Всё, что между ними - пользовательские задачи. То есть если мы ничего не делаем - мы делаем задачу IDLE.
\subsubsection{Обработчики прерываний в uC/OSII}
Если вызываем в обработчике прерываний какие-то функции операционной системы, то обязательно в начале надо вызвать \code{OSIntEnter}, а в конце \code{OSIntExit}. Это отключит диспетчеризацию, то есть мы кладём задачу в очередь, но не вызываем диспетчер.
\subsubsection{Критические секции, блокировка диспетчера и атомарные операции}
Фактически, критическая секция - это участок кода, который выполняется неразрывно, гарантированно без переключения процессов, без выполнения обработчика прерываний. В $\mu$C/OSII реализованы макросы \code{OS\_ENTER\_CRITICAL} и \code{OS\_EXIT\_CRITICAL}. Почти всегда критические секции реализуются запрещением прерываний. Для AVR, например
\begin{verbatim}
asm("cli"); // запрет прерываний (сбросом бита разрешения)
...
asm("sti"); // разрешение прерываний
\end{verbatim}
Но лучше сделать
\begin{verbatim}
UINT8 k;
k = SREG;
asm("cli"); // запрет прерываний (сбросом бита разрешения)
...
SREG = k;
\end{verbatim}
Мы таким образом гаранированно восстанавливаем значение регистра настроек процессора на момент запрета прерывания. Так мы можем создавать вложенные прерывания и получается более надёжный код.
Используя макросы, надо обязательно написать
\begin{verbatim}
#if OS_CRITICAL_METHOD == 3 /*allocate storage for CPU status reg*/
OS_CPU_SP cpu_sr;
#endif
\end{verbatim}
Критическая секция по определению является атомарной, хотя в основном этот термин применяется к инструкциям. Многие операции, которые должны выполняться неразрывно кладутся в критические секции. Критические секции - это жёсткий подход, но часто бывают ситуации, когда мы хотим работать одновременно с другими задачами, или делим с ними ресурсы, поэтому лучше не делать критическую секцию, а блокировать диспетчер (запрещать только переключение задач). Например, может сложиться ситуация, когда мы проверяем поля структуры, а другая задача туда что-то пишет. Это ошибка, поэтому надо применять механизмы распределения задач.
Варианты контроля разделения доступа к ресурсам:
\begin{itemize}
\item семафор - уставной способ, но это очень тяжеловесно.
\item критическая секция - гораздо быстрее, но более жёстко.
\item отключение диспетчеризации - мягкий вариант критической секции (OSSchedLock, OSSchedUnlock), при этом задачи блокируются, а прерывания не блокируются. Можно сказать, что это <<мягкая критическая секция>>.
\end{itemize}
Использование семафора хорошо, когда мы используем тяжеловесные операции, например эзернет. Для разделения доступа к каждой переменной это слишком сложно. При работе с семафором возможен дедлок.
\begin{verbatim}
OSSemPend();
a = a + 1;
OSSempost();
\end{verbatim}
можно объявить критическую секцию
\begin{verbatim}
OS_ENTER_CRITICAL();
a = a + 1;
OS_EXIT_CRITICAL();
\end{verbatim}
в этом случае важно ввести понятие атомарности. Например, операция инкремента вряд-ли может считаться атомарной. Атомарной может быть, например операция чтения или записи в регистр процессора
\begin{verbatim}
cur_status = SR;
\end{verbatim}
но тогда и критическая секция не нужна. Важно, что процессоры становятся многоядерными. В многоядерной системе запрещение прерываний одного ядра совсем не означает запрет прерываний в другом ядре. Для микроконтроллеров это пока что не актуально, но развитие в эту сторону идёт. Несмотря на амеханизмы контроля когерентности кэшей.
Базовые чтения-записи в разрядности процессора обычно осуществляются атомарно, но ели инкремент или обмен значений - то это не атомарно, это несколько инструкций.
\begin{verbatim}
ld R4, @addr
sv @addr, R4
\end{verbatim}
Запрещать ради этого прерывания накладно и не всегда помогает ситуации, поэтому есть классические инструкции для доступа к памяти. Низкоуровневые аналоги семафора. Данные через них передавать не надо
\begin{enumerate}
\item \textbf{Test-and-set} (проба и установка). осуществляет запись в ячейку памяти и возвращает старое значение перед записью
\begin{verbatim}
ячейка памяти = 0
пишем 1
вернулся 0
ячейка памяти = 1
кто-то пишет 1
получит 1
бесконечно, пока наш процессор не запишет туда 0
и тогда кто-то записав 1 получит 0 и пойм§т что это его единица
\end{verbatim}
договорённость в том, что если я записал 1 и не получил 0 то я не получил доступ. Если я записал 1 и получил 0 это мой процесс. аналог семафора. Работает даже для многопроцессных системах, на уровне ядра вложено во многие ОС, где требуется. Во всех процессах которые используют этот метод пишем
\begin{verbatim}
while (test_and_set(*addr) == 1);
//got access
*addr = 0;
\end{verbatim}
\item \textbf{compare-and-swap} (Сравнение с обменом). x86:CMPXCHG техника следующая:
\begin{verbatim}
ячейка = А
чтобы его модифицировать надо подать туда А и Б
если значение в ячейке == тому которое я заявляю
будет записано моё новое значение
функция вернёт true
если не равно - новое не пишется, возвращается false
\end{verbatim}
Подменить значение может только тот, кто владеет старым значением в ячейке. Но есть маленькая проблема: мы можем считать значение памяти атомарно, но изменение уже не атомарно. Считали А, хотим подменить на Б, считая операцию атомарной, а в это время какой-то процесс сменил на Ц и обратно в А. у процесса есть иллюзия, что всегда был А, но была фактическая подмена. Многие процессоры предоставляют инструкцию LoadLinked/StoreConditional. Если значение LoadLinked считали и хотим записать в ячейку, но не получится, на уровне ядра процессора инструкцией StoreConditional подсчитывается количество обращений к ячейке.
\item \textbf{Fetch-and-add} (считывание со сложением или считывание с инкрементом) - Процесс, вставая в очередь получает номер. процесс который хочет получить доступ должен дождаться чтобы в контрольной ячейке памяти появился его номер. Обслуживающий процесс раздаёт доступ к ресурсу подряд, инкрементируя счётчик обслуженных клиентов.
\begin{lstlisting}[language=C,style=CCodeStyle]
struct lock {
int tickNumber; // ==0
int turn; // ==0
}
lock(locktype* lock) {
int myturn = fetch_and_increment(&lock.tickNumber); //+1
while (&lock.turn != myturn);
}
unlock(locktype* lock) {
fetch_and_increment(&lock.turn); //+1
}
\end{lstlisting}
Первый процесс, который получает доступ инкрементирует до единицы, следующие будут инкрементить tickNumber, а процесс, который готов освободить ресурс инкрементит turn.
\end{enumerate}
\section{Стандарты проектирования}
Основной стандарт, регламентирующий проектирование встраиваемых систем: ГОСТ Р 51904-2002. Фактически, это калька с ISO авиационной техники (DO-178B). Рассматривает разные процессы и регламентирует ЖЦ ПО.
ISO/IEC 12207:2008 (ГОСТ 12207-2010) - про разработку ПО, жизненный цикл программных средств. покрывает от идеи до уничтожения
В стандарте ЖЦ разделён на процессы разработки и интегральные процессы. Разработку надо осуществлять последовательно с модульностью документированием и тестированием, чтобыбыло проще локализовать ошибки.
\section{Стандарты кодирования (как частный случай проектирования)}
Определяет правила кодирования. Условно, дополнительные стандарты отвечают на вопрос: как сделать так, чтобы табуретка не падала? Согласно MISRA - прибить табуретку к полу. Есть несколько версий стандарта один из них портирован в качестве плагина к IAR и даже перевод на русский. Реализуются такие стандарты как статический анализатор кода. При отступлении от стандартов необходимо обязательно задокументировать принятое решение и объяснить почему произошло отступление, так, например, поступает $\mu$C/OSII.
Нежелательно использовать слово register, компилятор сам решит, что класть в регистры (до С++17). При инициализации массивов соответствовать размерностям скобками. тоже самое касается структур. Важно учитывать семантику операторов и следить за отсутствием побочных эффектов. приоритеты можно забыть, ставьте скобки. в одном выражении нужно с осторожностью совмещать логические и побитовые операторы. чаще всего знак остатка равен знаку остатка, но лучше деления избегать из-за разницы реализаций для разных архитектур и компиляторов. Вообще надо писать код более явно.
\newpage
\appendix
\addcontentsline{toc}{section}{Приложения}
\section{Семинар (1) 2021-09-13}
\textbf{Прерывание, выбор микроконтроллера с точки зрения особенностей работы прерываний}
\textbf{Прерывание} - это частный случай исключения (interrupt, но в документации к некоторым микроконтроллерам можно встретить exception). Если возникает исключение - вызываются инструкции обработки, нас интересуют внешние прерывания и прерывания аппаратуры. Процессорный 16тиричный адрес инструкции - это вектор прерывания (инструкции). Располагается в разных микроконтроллерах в разных местах.
Прерывания бывают:
\begin{enumerate}
\item программные и прерывания ядра
\begin{itemize}
\item например, неверные инструкции,
\item срабатывающие для защиты памяти
\item SWINT, явные программные прерывания
\item TRAP, инструкция-ловушка (ставится туда, куда мы не должны попасть, например, можем в отладчике анализировать откуда мы попали в ловушку и почему)
\end{itemize}
\item маскИруемые и нет
\begin{itemize}
\item маскируемый можно игнорировать
\item NMI немаскируемые прерывания, их нельзя пропустить
\end{itemize}
\item прерывания от периферии, аппаратные
\begin{itemize}
\item это просто прерывания, которые IRQ
\item обычно маскируется, кроме сторожевого (вочдог) таймера, иначе не проснёмся, если код не работает.
\end{itemize}
\end{enumerate}
Если нет цели энергопотребления - мы делаем в обработчике минимальное количество действий, чтобы соответствовать реальному времени. Когда система делает много обработок без использования вложенных прерываний - нельзя долго быть в одном обработчике, потому что не успеем опросить другие векторы прерываний.
На примере микроконтроллера TI MSP430, на что обратить внимание:
\begin{enumerate}
\item порядок обработки запроса. Желательно читать перед началом работы с микроконтроллером. Этим занимается программируемый контроллер прерываний (пункт документации 1.3.4.1.).
\begin{enumerate}
\item задержка 6 тактов (в AVR (ардуино) 4 такта, в более сложных архитектурах обычно больше), в это время принимается запрос, но ничего видимого не происходит, завершается исполнение текущей инструкции.
\item на стек сохраняется счётчик инструкций, сохраняется регистр указатель стека
\item выбирается запрос с наивысшим приоритетом
\item сбрасывается автоматически, в отличие от NIOS, где нужно записать нужный бит в нужный регистр конкретного периферийного модуля, чтобы сбросить, если не сбросить руками, получаем бесконечный цикл. Но это работает только для запросов прерываний с одним источником. если несколько источников - делаем руками. Если канал поступления информации один - логично использовать один обработчик на любые ошибки, например в UART. если же источников данных несколько - нужно разрешить вложенные прерывания или обрабатывать порознь. то есть важно принимать решение об обработки всех сразу или по-очереди
\item сбросили биты, которые отвечают за пониженное энергопотребление
\item сбрасывается бит глобального разрешения прерываний (автоматически, все остальные прерывания запрещаются), если хотим разрешить вложенные прерывания, нужно поставить единицу в бит глобальных прерываний.
\item в програм каунтер загружается вектор прерывания (адрес команда) и программа начинает выполнение обработчика прерываний.
\end{enumerate}
\item систему приоритетов прерываний, если есть
\begin{enumerate}
\item если \textbf{одновременно} возникают два или более запросов - обрабатывается (обслуживается) тот у которого приоритет выше
\item если в обработчике прерывания разрешены (во всех архитектурах по-умолчанию запрещены) вложенные прерывани, специально будет выставлен бит глобального обработчика прерываний. будет вызвано несколько, но снова по приоритету как в 2.1, то есть произойдёт вытеснение более приоритетным. важно, что максимально быстро нужно сбрасывать аппаратные прерывания, то есть обслужить аппаратуру. прочитали аппаратуру, разрешили вложенность, а дальше уже начали математику или складывание в очереди итд.
недостатки:
\begin{enumerate}
\item в современных микроконтроллерах много памяти, поэтому, сохраняется не только указатель на стек, но и текущий контекст исполнения, поэтому при использовании вложенных обработчиков, требования к размеру стека ещё больше увеличиваются. в операционных системах реального времени обычно контекст сохраняется в стек операционной системы.
\item всё равно ждать пока текущий низкоприоритетный обработчик закончит свои важные дела (недетерминированность во времени обработки уменьшается, но всё равно сохраняется) то есть для всех низкоприоритетных надо чётко знать сколько времени займут обязательные действия низкоприоритетных обработчиков
\item возможные логические ошибки для связанных процессов
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-irq.svg}
\caption{Проблемы разрешения вложенных прерываний}
\label{pic:irq}
\end{figure}
так будет сначала событие1 потом событие2 которые вызовут обработчики о1 и о2, но в очереди ОС будут сначала данные из о2 потом о1
\item во многих микроконтроллерах и процессорах возможности работы с прерываниями значительно ограничены
\begin{enumerate}
\item то есть приходится изобретать как делать приоритеты
\item обычно приоритеты фиксированы. AVR, NiosII, MSP430
\item ARM - Cortex (STM) приоритеты программируемые, прям в таблице есть type of priority.
\item сигнальные микроконтроллеры C28, c24 TI, микропроцессор C55? несколько слотов для каждого запроса. у каждого запроса есть два слота с большим и маленьким приоритетом, то есть можно расставить в фикс слоты приоритетов разные запросы.
\item несколько уровней (С51) два уровня по каждому обработчику, можно выставить 0 или 1
\end{enumerate}
\end{enumerate}
\item обычно максимальные приоритеты назначаются событиям возникающим с большей частотой.
\item при выборе архитектуры нужно смотреть не только на частоту процессора и объём памяти, но и на буферизацию периферии
\end{enumerate}
\item механизм ускорения обработчика
Помимо задержки вызова обработчика происходит сохранение контекста и восстановление(то есть состояние процессора и регистров, которые задействуются обработчиком)
\begin{itemize}
\item может быть программным (AVR, MSP)
\begin{verbatim}
ST -Y, R31
ST -Y, R30
ST -Y, R16
ST -Y, R17
обработчик
ST R17, Y+
ST R16, Y+
ST R30, Y+
ST R31, Y+
\end{verbatim}
\item аппараратное (Cortex) быстрее программного
\item теневые регистры или теневые банки регистров (ускоренный) банков ограниченное кол-во, а задач и обработчиков неограниченно. ниос не умеет работать с банками. у CortexR(M?) есть fast interrupt query: это всего один запрос, всегда последний, и при его вызове автоматически вызывается теневой банк регистров R8-R13, в которых мы можем работать без задержки. там даже нет инструкций перехода.
\end{itemize}
\item как назначать векторы в коде ПО
\end{enumerate}
\section{Семинар (2) 2021-09-27}
\label{sec:sem2}
\textbf{Архитектура микропроцессорных систем: память}
\textbf{Изучить:} What every programmer should know about memory: Ulrich Drepper November 21, 2007
Основных моделей распределения памяти две:
\begin{itemize}
\item фон-Нейманова модель - общая память для команд и данных.
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-fon-neyman.svg}
\label{pic:fon-neyman}
википедия: Архитектура фон Неймана (модель фон Неймана, Принстонская архитектура) — широко известный принцип совместного хранения команд и данных в памяти компьютера. Вычислительные машины такого рода часто обозначают термином <<машина фон Неймана>>, однако соответствие этих понятий не всегда однозначно. В общем случае, когда говорят об архитектуре фон Неймана, подразумевают принцип хранения данных и инструкций в одной памяти.
\item гарвардская модель - разделение памяти на данные и команды физически по устройствам. Часто именно так устроены микроконтроллеры.
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-harvard.svg}
\label{pic:harvard}
википедия: Гарвардская архитектура — архитектура ЭВМ, отличительными признаками которой являются: хранилище инструкций и хранилище данных представляют собой разные физические устройства; канал инструкций и канал данных также физически разделены.
\end{itemize}
Может возникнуть ситуация, когда программно в процессоре или микроконтроллере реализована модель фон-Неймана, а аппаратно - гарвардская
При использовании гарвардской архитектуры нет узкого места использования общих шин. Но узким местом становится память, к которой мы всё равно не можем обратиться одновременно. Также иногда выделяют отдельное адресное пространство в памяти для информации от устройств ввода-вывода, но иногда это объединяют с обычной шиной данных. Часто при проектировании в карту памяти вставляют пробелы (рис. \hyperref[pic:memory]{\ref{pic:memory}}).
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-memory.svg}
\caption{Обычное устройство микроконтроллера}
\label{pic:memory}
\end{figure}
Например, \textbf{Figure 1.1 msp430f}: MAB, MDB - шина адреса и шина данных, видим, что архитектура скорее фон-неймановская (любая инструкция может обращаться к любой части адресного пространства), хотя скорее всего на физическом уровне эти памяти разделены. \textbf{Table 6-8} показывает, что периферия отображается в младшие адреса, далее идёт RAM 2Кб, которая зеркально отражена в старшие адреса, и память загрузчика, адреса 0х2100 и далее уже память SRAM.
Так в архитектуре AVR полностью реализована фон-нейманова модель, видно, что адресация RAM идёт по той-же шине, что прерывания и другие программные адресации. И в ранних AVR память без пробелов. Интересный ход, что можно обратиться к регистрам ввода-вывода как к памяти, например.
Во многих микроконтроллерах конфигурации памяти могут быть различны. Например наборы адресов могут изменяться в зависимости от использования загрузчика. Также, например, в микроконтроллере \textbf{sprs645e} есть четыре аппаратных чип-селекта, от таких решений значительно будет зависеть программирование, потому что регулировать память будет трассировщик печатной платы, а не программист. Так следующий код
\begin{figure}[H]
\begin{lstlisting}[language=C,style=CCodeStyle]
#define MY_SPI_BASE 0x40008050
uint32 *pr;
pr = (uint32*) MY_SPI_BASE;
data = *pr; //(1) LD r6, r5
data = IORD(MY_SPI_BASE) //(2) LDIO
\end{lstlisting}
\end{figure}
будет понят и скомпилирован, но код (1, 4я строка) будет кэшировать значения регистров, поэтому дальше будем читать неверные данные и зря дублировать информацию, поэтому микроконтроллер часто предоставляет макросы (2, 5я строка) для чтения именно из пространства ввода-вывода. В данной ситуации не поможет даже \code{volatile} потому что всё равно будет читаться из кэша, и даже несмотря на то, что регистр будет перечитываться, будет перечитываться неверный, кэшированный.
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
% \includesvg[scale=1.01]{pics/01-ess-00-cache.svg}
\caption{Способ доступа к SDRAM}
\label{pic:cache}
\end{figure}
Доступ к СДРАМ (на рис. \hyperref[pic:cache]{\ref{pic:cache}}) без кэша может занимать сотни тактов. Кэш может быть многоуровневый. Кэш нужен потому что во многих процессорах используется внешняя память (SDRAM). Может использоваться статическая ОЗУ (SRAM), но она дорогая, поэтому часто выбираются микроконтроллеры со встроенной статической ОЗУ. Иногда используется QDR SRAM - это дорого и редко, но хочется использовать потому что гораздо быстрее читать данные по адресу. SDRAM плотнее и эффективнее, но чтение по адресу не такое быстрое. Бывает ещё память RLDRAM (reduced latency) но технология вроде не развивается.
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-sdram.svg}
\caption{Обычно SDRAM выглядит так}
\label{pic:SDRAM}
\end{figure}
Адрес конкретной ячейки формируется как \textbf{row-bank-column}, а не \textbf{bank-row-column}, так быстрее, потому что задержка выборки банка почти нулевая. Поэтому если есть массив, последовательно хранящийся по строкам, он будет храниться в первой строке первого банка, второго, третьего, и так далее. Соответственно, банков часто бывает 8-16 и далее в степени двойки, поэтому алгоритмы нужно строить таким образом, чтобы адресация памяти работала локальными отрезками и загружалась в кэш память процессора полностью, иначе полностью теряется смысл кэширования, если мы всё равно каждый локальный сегмент переспрашиваем у медленной памяти.
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-arrayrow.svg}
\caption{Хранение данных из массива в памяти}
\label{pic:arrayrows}
\end{figure}
Рисунок (\hyperref[pic:arrayrows]{\ref{pic:arrayrows}}) показывает, что две строки массива улягутся ровно в одну строку всех восьми банков памяти SDRAM. В документации к памяти часто пишутся цифры наподобие 11-11 или 17-17 - это обычно такты задержки выборки.
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-readmem.svg}
\caption{Чтение данных из памяти SDRAM}
\label{pic:readmem}
\end{figure}
RAS, CAS: после выборки адреса чтение происходит в два раза быстрее, бёрстами (burst) по несколько (обычно 64) байт, поэтому читать надо начинать по степеням двойки. Это порождает проблему: алгоритмист может принести плохой алгоритм, который будет читать рандомно по памяти, а это очень медленно (на порядки медленнее), потому что не используются кэшируемые сегменты. То есть при запросе данных от памяти по конкретному адресу скорее всего будет загружаться строка банка полностью, поэтому желательно и данные использовать не совсем случайные. Идеальный алгоритм ходит по строчкам памяти или влезает в кэш (желательно) первого уровня. Некоторые умные процессоры могут сами понять какое значение следующее грузить. \textbf{Важно сохранять локальность кэша и чтение данных по строкам.}
\begin{itemize}
\item SARAM single access random access memory
\item DARAM dual access random access memory
\end{itemize}
Отличаются обращениями к блокам памяти одиночные или двойные
Наша задача планирования размещения памяти расположить данные в SARAM и DARAM так, чтобы не было конфликтов обращений от разных модулей. Обычно это сводится к тому, что надо прагмами указать куда именно надо класть массивы и другие объёмные данные.
\begin{figure}[H]
\centering
2023-10-24 15:47:15 +03:00
\includesvg[scale=1.01]{pics/01-ess-00-s-d-aram.svg}
\caption{Планирование памяти}
\label{pic:sdaram}
\end{figure}
\section{Семинар (3) 2021-10-11}
\label{sec:sem3}
\textbf{Оптимизации циклов}
\begin{enumerate}
\item Развёртывание циклов (unrolling)
\begin{lstlisting}[language=C,style=CCodeStyle]
int sum = 0;
for (int i = 0; i < N; a++) {sum += a[i]}
\end{lstlisting}
нужно будет выбрать элементы по адресу a[i] будем загружать в регистр
\begin{verbatim}
ld R2, @R5 //где R5 - это адрес a[i] 2такта
add R4, R2, R4 - простой сумматор 1 такт
inc R5 - инкремент чтобы перейти к следующему адресу
inc R3 - оценка выхода из цикла 1т
cmp R3, R7 - 1т
jge out - 1т
jmp start 2т
\end{verbatim}
Получается 4 такта на вычисление и 5 тактов на управление (накладных расходов (overhead) больше, чем полезного действия).
\begin{lstlisting}[language=C,style=CCodeStyle]
int lenlong, lenshort
lenlong = N / 4; // глубина развёртывания
lenshort = N % 4; // N >> 2; N & 0x3;
sum = 0;
for (int i - 0; i < lenlong; i++){ // 5 тактов
sum += a[i * 4]; // 4 тактов
sum += a[i * 4 + 1]; // 4 тактов
sum += a[i * 4 + 2]; // 4 тактов
sum += a[i * 4 + 3]; // 4 тактов
}
for (int i - 0; i < lenshort; i++){// пренебрегаем при подсчёте
sum += a[lenlong * 4 + i];
}
\end{lstlisting}
работает для больших массивов. так получаем оверхед меньше четверти - около 23\% и на одну итерацию будет затрачено около 5,25такта, против 9. Такие оптимизации, в том числе явная замена деления на сдвиги скорее всего будут выглядеть естественно и не будут фактором <<преждевременной оптимизации>>. Развёртывание обычно осуществляется на степени двойки. Важна проверка компилятора на предмет оптимизаций. во 2 и 3 строке нужно проверить происходит ли деление или это оптимизируется в сдвиги. Также важно проверить насколько оптимизация хорошо работает с кэшем и строками памяти (может потребоваться выравнивание массива по границам памяти). Нужно читать документацию на тему прагм, определяющих помочь компилятору. Такие развёртывания имеет смысл применять даже для простых проходов по структурам данных. Во многих библиотеках такие развёртывания реализованы в коде.
\item Конвейеризация циклов (loop pipelining)
важна для VLIW и суперскалярных процессоров.
(картинка на рабочем столе)
\begin{lstlisting}[language=C,style=CCodeStyle]
for (int i - 0; i < N; i++){
A[i] = ...; //зависимость по данным
B[i] = A[i] + ...;
}
\end{lstlisting}
хорошо, если компилятор осуществляет деление такого цикла на части:
\begin{lstlisting}[language=C,style=CCodeStyle]
A[0] = ...; //пролог
for (int i = 0; i < (N - 1); i++) {
A[i+1] = ...;
B[i] = A[i] + ...;
}
B[N-1] = A[N-1] + ...; //эпилог
\end{lstlisting}
\item Вынос инвариантного кода из тела цикла (loop-invariant code motion)
Если в теле цикла есть операции, которые занимают время, но не зависят от индекса цикла, их можно вынести за пределы цикла.
\begin{lstlisting}[language=C,style=CCodeStyle]
for () {
a[i] = coeff/det * b[i];
}
\end{lstlisting}
Если компилятор не поддерживает такую простую оптимизацию, даже при явном включении какого-то набора оптимизаций, возможно, имеет смысл подобрать другой компилятор.
\item обращение цикла (loop reversal)
for(i=0; i<N-1; i++) {
//...
}
такой прямой ццикл компилятор часто заменяет на
for(i=N; i!=0; i--)
это связано с тем, что сравнение с нулём часто проще, чем сравнение с \code{N}. так, например, есть инструкция \code{djnz} - decrement and jump if not zero, которая одной инструкцией уменьшит, проверит и перейдёт по условию. В RISC часто \code{R0} это не просто регистр, а генератор константы 0. Обычно компилятор использует регистры для разделения данных по смыслу (scratchpad будет хранить текущие данные для манипуляций) и это знаительно облегчает манипуляции с часто используемыми данными, такими как значение 0.
\item переменные индукции (induction variables)
это переменные, значения которых при последовательных итерациях меняется на константу. Фактически, они не меняются
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i) {
k = i * 5;
}
\end{lstlisting}
компилятор может заменить это не расчётом \code{K} от \code{i}, а просто прибавлять 5. часто операция умножения дороже, поэтому такая оптимизация будет возможна. То есть, например, без них даже вычисление адреса i-ого элемента будет со значительными потерями по производительности.
\item Вынос оператора выбора и условных операторов из тела цикла.
Предположим условие не зависит от счётчика цикла (loop invariant)
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i){
if(){...}
else{...}
}
\end{lstlisting}
читается лучше, чем
\begin{lstlisting}[language=C,style=CCodeStyle]
if()
for{i}
else
for{i}
\end{lstlisting}
Поэтому любой оптимизирующий компилятор должен преобразовать первый вариант во второй автоматически, и если этого не происходит - лучше выбрать другой компилятор.
\item Инверсия цикла (не путать с обращением)
фактически перестановка контроля из начала цикла в конец (например, превращение вайла в дувайл).
\begin{lstlisting}[language=C,style=CCodeStyle]
while (i < N) {
}
// превратится в
if (i != 0) {
do {
} while (i < N);
}
\end{lstlisting}
первый вариант:
\begin{enumerate}
\item сравнение i и N
\item тело
\item переход в начало
\end{enumerate}
будет продолжаться пока i меньше N и будет лишний переход.
\begin{itemize}
\item [K-1] сравнение i и N
\item [K] выход из цикла (отдельная инструкция перехода)
\item [K+1] следующий код
\end{itemize}
второй вариант
\begin{enumerate}
\item сравнение i т 0
\item тело цикла
\item сравнение i и N
\item переход в 2.
\end{enumerate}
\begin{itemize}
\item [K-1] сравнение i и N
\item [K] следующий код
\end{itemize}
это хорошая оптимизация для коротких циклов, но такая оптимизация делается не всегда, это зависит от архитектуры. Если есть предсказание ветвлений - эта оптимизация должна работать с ним в паре. если поддерживается \code{\#pragma min\_iterate} то во втором варианте не будет первого сравнения.
\label{sec:semFour}
\item Разбиение и объединение циклов (стр. \hyperref[sec:semFourCont]{\pageref{sec:semFourCont}} семинар от 2021-10-25)
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i) {
A[i] = ...;
B[i] = ...;
}
\end{lstlisting}
преобразуется в
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i) {
A[i] = ...;
}
for(i) {
B[i] = ...;
}
\end{lstlisting}
это разбиение цикла (fission), а обратный процесс - это объединение цикла (fusion). обычно делается фьюжн, но и фишн применим, чаще всего в оптимизациях о3, например, когда действий так много, что они не помещаются в кэш (в этом случае накладные расходы на цикл перекрывают то, что было загружено в кэш для обработки а-итого)
\item перестановка циклов (interchange/permutation)
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i) {
for(j) {
...
}
}
\end{lstlisting}
может преобразоваться в \code{forj\{fori\{\}\}} это бывает нужно, когда работаем, например, с массивами, которые располагаются по строкам, а мы бежим сначала по столбщам. становится принципиально, когда мы работаем с памятью SDRAM потому что мы сразу будем обращаться к памяти бёрстами построчно. Не получится с задачами транспонирования матрицы (cache efficient matrix transpose надо поискать, если не найдётся, значит задача эффективно не решается). Это связано с тем, что из памяти данные всегда загружаются в кэш целиком строкой и внутри кэша есть флаг валижности данных, то есть если грузить частично - кэш будет частично валидный. Выборка строки - самая длинная операция, поэтому при выходе за её пределы мы выбираем следующий банк СДРАМа, а не следующую строку этого же банка.
\item разбиение циклана блоки (loop tiling/blocking)
\includegraphics[height=4cm]{01-ess-00-rows1.png}
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i) {
for(j) {
...
}
}
\end{lstlisting}
становится циклом со вложенностью 4
\begin{lstlisting}[language=C,style=CCodeStyle]
for(i/2) {
for(j/4) {
for(ii) {
for(jj) {
...
}
}
}
}
\end{lstlisting}
также напрямую связано с кэшированием данных. Если идём по длинной строке, строка может быть настолько длинной (например, больше 256 слов, а кэш 8Кб), то данных в строке больше, чем в кэше. то есть например при обработке изображений мы идём матрицей 5х5,
\includegraphics[height=4cm]{01-ess-00-rows2.png}
загружаем кэш в 5 раз быстрее, так можно поделить на блоки, которые будут и грузиться из кэша и быстрее обрабатываться. Такие обработки можно делать и вручную, чтобы обеспечить локальность данных без учёта оптимизаций компилятора.
\item использование синонимов (псевдонимов, alias)
фактически - это указатели, указывающие на одну и ту же область памяти (https://habr.com/ru/post/114117)
\begin{lstlisting}[language=C,style=CCodeStyle]
int k;
int *A = &k;
int *B = &k;
char *C = (char*) &k;
\end{lstlisting}
если есть псевдонимы разных типов - это повод ощутить лёгкую тревогу. если AB это не тоже самое с точки зрения оптимизаций что BC.
\begin{lstlisting}[language=C,style=CCodeStyle]
void sum(int* out, const int* in, int count) {
for (int i = 0; i < count; i++) {
...
}
}
\end{lstlisting}
очень плохо если ин внутри аут, тогда мы не сможем использовать регистр, а будем вынуждены всегда писать в ОЗУ. Можем указать ключевое слово \code{restrict}. это явно укажет, что указатель не является псевдонимом. Компилятор не должен делать допущение, что ин внутри аут. тогда мы почти не будем выполнять обращения к памяти. Ключевое слово \code{restrict} помогает оптимизировать код в циклах.
В случае использования указателей разного типа мы должны бояться возможного появления ошибок. проверки внутри оптимизаций type-based-alias-analysis, strict aliasing говорят, что указатели разных типов не могут указывать на одну область памяти. Единственный законный способ сделать псевдонимы разных типов - это использовать \code{union}.
\end{enumerate}
\section{Семинар (4) 2021-10-25}
\label{sec:semFourCont}
Начало в пункте 8 (раздел на стр. \hyperref[sec:semFour]{\pageref{sec:semFour}})
\section{Семинар (5) 2021-11-22}
\textbf{Разработка ПО}
\subsection{Настройка компоновщика}
Как правило К работает по скрипту (линкер скрипт). в ниосе работает на основе гсс и находится в файле линкер.икс. находится в папке БСП. в реальных проектах это обычно пишется руками, для ниоса это сахар. создаёт стандартные секции елф. есть два понятия
\begin{itemize}
\item регионы (бывает один два или три в одном физическом устройстве) например на сдрам и код. разделяют большие области данных
\item секции - разделение логических областей, куда помещаются разнотипные код и данные.
\end{itemize}
Настройка осуществляется в BSP-editor. Разработчик сам решает где в памяти будет лежать стек, куча, и так далее.
\begin{itemize}
\item .entry - из этой секции запускается код, обычно 0;
\item .exceptions - устанавливается секция обработчика исключений;
\item .text - основной код хранится в этой секции, после entry и exception;
\item .rodata это данные только для чтения (например заранее инициализированные значения);
\item .rwdata это заранее инициализированные объекты, значения которых могут меняться;
\item .bss все остальные данные, инициализируются нулём.
\end{itemize}
Остальное место выделено под кучу и стек. В нашей системе стек растёт вниз. В целом всегда нужно знать на каком устройстве работаем, какая память и сколько её, где что в карте памяти находится. Б\'{о}льшая часть секций настраивается в секции BSP. Есть средства контроля переполнения стека и кучи: если обнулить всю свободную память - это будет не очень наглядно, но перед выполнением можно заполнить память каким-то значением вроде \code{0xDEADBEEF}, и если они исчезли вообще все из памяти - стек и куча могут встретиться и использовать память совместно.
Для функций объявляется прототип и прописывается атрибут, для переменной тоже
\begin{verbatim}
int foo_var __attribute__ ((section ("on_chip_memory"))) = 0;
int bar_func(void* ptr) __attribute__ ((section (".ext_ram")));
\end{verbatim}
Тесно связанная память (tightly coupled instruction/data memory): работает параллельно с обычными кэшами данных и инструкций. Есть \textbf{instruction TCM}, \textbf{data TCM}. Нужно, например, когда есть какой-то код, который выполняется редко, но должен выполняться быстро. Обычным кодом будет из кэша вытеснен, увеличит латентность чтения инструкций или данных, чтобы этого избежать такой код кладут в \textbf{TCM}, получая быстродействие кэша первого уровня.
\subsection{Прерывания}
И исключения.
\begin{frm}
Используйте внешний контроллер прерываний - смешной совет, потому что внешние обработчики - это десятки и сотни тактов только на вход и выход. Но может обрабатывать больше, чем 32 прерывания. Хотя сами выполняются сами обработчики в разы быстрее.
\end{frm}
Реализуется как функция, передаются контекст и идентификатор. Регистрируем обработчик (\code{alt\_ic\_irq\_register()}), можно маск\'{и}ровать конкретные обработчики аппаратных прерываний, разрешать или запрещать их выполнение. Перед регистрацией желательно обнулить регистр запроса прерывания, потому что если там не ноль, обработчик вызовется при регистрации и непонятно, что там было и размаск\'{и}ровать периферию. В коде обработчика сбрасываем запрос, и важно, что если процессор в несколько раз быстрее - можем успеть отправить подтверждение, выйти из обработчика, и зайти обратно. поэтому нужно сделать dummy-чтение из медленного регистра. Подробнее на первой лабе.
\subsection{Разработка драйверов устройств}
Драйвер интегрируется в BSP, работает концепция как в линукс - всё это файл. Всегда лучше разделять код работы с аппаратурой и код API. Также лучше разделять код работы с периферией и пользовательский код. Для стандартных компонентов уже реализовано множество драйверов (в папке квартса) физически копируются в папку BSP.
\subsubsection{Типы драйверов}
Драйвер может быть просто заголовочным файлом. Например, для PIO как такового драйвера нет, а есть только один заголовочный файл с адресами регистров. Для, например, юарт, это полноценные драйверы. Бывают двух типов: синхронные (small) и асинхронные (fast). Синхронный более простой дожидается выполнения операции, а асинхронный не блокирует выполнение, буферизует данные и работает <<параллельно>> по факту заполнения буфера или прерыванию передачи. Например, для чтения, если работаем синхронно - или вернётся ошибка, либо будет <<зависание>> до появления данных. Асинхронный занимает больше места.
\subsubsection{Разработка}
Выбираем сложность и тип, используем HAL, понять, на каком уровне интегриовать драйвер. Пишем функции для прямой работы с регистрами, используем макросы периферии, выбираем тип классы модели (символьные устройства, таймеры, устройства ПДП). Если устройство подходит под класс - лучше реализовывать не с нуля, а от класса, чтобы было легче использовать в системе. Например, символьные устройства, используются стандартизированные функции работы с символами и имеет префикс \code{alt\_dev}. Реализованы функции открытия, закрытия, чтения, записи, и так далее. То есть некоторый шаблонный интерфейс. Указав в любой функции \code{NULL} мы запрещаем использование интерфейсной функции.
\includegraphics[width=15cm]{01-ess-00-drv.png}
в мейне создаём структуру и можем использовать её как файл при вызове. с точки зрения кода будем писать как в файл, а с точки зрения аппаратуры драйвер (alt\_dev) получает доступ между HAL API и аппаратурой.
Для регистрации надо каждому драйверу добавить Makefile, создать файлы \code{tcl} и \code{sv} и расположить в правильных папках. В качестве примера можно использовать UART. Так можно драйверы использовать при создании BSP в QSys. И затем инициализировать вызовом \code{alt\_sys\_init()}. Всё происходит по имени и должны присутствовать функции \code{x\_INIT()} \code{x\_INSTANCE()}. Дополнительная информация - в материалах к первой лабораторной работе.
\section{Семинар (6) 2021-12-06}
Конспект в подразделе \hyperref[subsubsection:mutex]{\ref{subsubsection:mutex}}
\section{Семинар (7) 2021-12-20}
Информация об очередях на странице \hyperref[Word:Queue]{\pageref{Word:Queue}}.
\section{XILINX practice}
https://alexbmstu.github.io/2021/
ssh-copy-id iu3001@195.19.32.95
\section{Источники}
\subsection{Список литературы}
\begin{enumerate}
\item Таненбаум: стоит прочитать всё, можно начать со стр.60-108 и гл.4 до с.343.
\item Харрис, Харрис
\begin{itemize}
\item глава 6:
\begin{itemize}
\item базовая информация: 6.1-6.3, 6.5-6.6
\item к л.р. №2 и при оформлении отчета: 6.4
\item дополнительно: 6.7-6.8
\end{itemize}
\item глава 7: продвинутый уровень, необязательно
\item глава 8:
\begin{itemize}
\item память: 8.1-8.3
\item дополнительно: 8.4 (полезно, но не относится к нашему курсу)
\item ввод-вывод и периферия: 8.5 и далее (8.6.7 необязательно)
\end{itemize}
\end{itemize}
\item По микроконтроллерам
\begin{enumerate}
\item Тревор Мартин ARM7 (LPC2000). устаревшая архитектура, но сама книга хорошая - не представляет собой, в отличие от многих других, перевод документации и примеров производителя, рассмотрение достаточно краткое и понятное.
\item MSP430. книга по микроконтроллерам, неплохо структурированный перевод документации
\end{enumerate}
\item Любой программист хотя бы раз в жизни должен просмотреть "Компиляторы" Ахо, Сети, Ульман, Лам и "Совершенный код" Макконела. Для программистов встроенных систем после Макконела стоит прочитать "The Art of Designing Embedded Systems" Гансла (кроме слишком затянутого рассмотрения дребезга в главе 5, где он открыл для себя электронику :) )
\end{enumerate}
\subsection{Организационные вопросы}
\begin{itemize}
\item 3 модуля и 4 лабораторные, зачёт
\item при удалёнке можно, и скорее всего будет нужно, использовать vpn+anydesk
\item lab1. система на кристалле (будем писать на ниосе)
\item lab2. оптимизация существующего программного решения
\item lab3. работа с операционной системой реального времени
\item lab4. телекоммуникационное программное обеспечение
\end{itemize}
Лабораторные работы:
Quartus Lite 20.1, Cyclone 4E, NiosII EDS
с 22 октября
\subsection{Ссылки}
https://cloud.mail.ru/public/ShVK/V4DzAeJPp/
https://cloud.mail.ru/public/4CXS/MeYv1D9i7/
\end{document}