ПОСЛЕ ИЗУЧЕНИЯ ГЛАВЫ ВЫ СМОЖЕТЕ:
• Подробно описать понятия и термины, касающиеся операционных систем реального времени, ОСРВ (RTOS — Real Time Operating System).
• Понять принципы работы ОСРВ.
• Различать жесткие, твердые и мягкие ОСРВ.
• Описать основные свойства записи, списков связей, стеков и очередей.
• Дать определение динамическому распределению памяти и описать связанные с ним преимущества и недостатки.
• Описать методы управления задачами с помощью блока управления задачами.
• Объяснить важность системных таблиц, блоков управления устройствами и диспетчера при реализации ОСРВ.
• Объяснить различия между алгоритмами планирования ОСРВ.
• Рассказать о проблемах, связанных с ОСРВ.
Операционные системы реального времени достаточно давно и успешно используются в промышленных программируемых контроллерах и во встраиваемых приложениях на основе 32-разрядных МК. Однако в настоящее время, в связи со значительного роста функциональности и быстро действии 16-разрядных МК наблюдается тенденция внедрения ОСРВ в системы на основе этой элементной базы. Поэтому авторы предлагают читателю познакомится с основными идеями, на основе которого строится ОСРВ.
В этой главе мы представим вам понятия, связанные с операционными системами реального времени. Начнем с притчи, чтобы показать некоторые ключевые моменты и концепции ОСРВ.
Я (Стивен Ф.Барретт) испытываю глубокое и подлинное уважение к профессии официанта. Я восхищаюсь умениями, связанными с этим трудным, часто неблагодарным ремеслом. Я был непосредственным свидетелем всех сложностей этой профессии, когда был старшеклассником средней школы. По вечерам и выходным я подрабатывал в качестве уборщика/посудомойки/помощника повара в офицерском обеденном клубе на авиабазе Минот, в Северной Дакоте. Вот тогда я с трепетом наблюдал за работой официантов. Так или иначе они были способны с успехом запоминать и выполнять требования людей за множеством столиков одновременно. Порой казалось, что они способны делать все сразу. Если случалось что-то необычное, например на обеде появлялся генерал со своей свитой, или ребенок опрокидывал на стол свой стакан молока, они сходу реагировали на эти непредвиденные события. И снова, даже при этих дополнительных покушениях на свое драгоценное время, они умудрялись каким-то образом следить за всеми событиями сразу. Они должны были одновременно следить за состоянием множества заказов на различных столиках и наличием блюд на кухне (например, знать, сколько еще осталось ежедневных удешевленных обедов). Для этого они должны были постоянно держать тесную связь с поварами.
А когда случались небольшие паузы, официанты успевали готовиться к работе следующего дня, протирая серебро или сворачивая салфетки и т.д. Да, при такой интенсивной работе эти талантливые люди, должно быть, очень крепко спали по ночам.
Через несколько лет мне пришлось работать в пиццерии в Беллевю, штат Небраска, где мне была предоставлена возможность сменить мое умение в приготовлении пиццы, на работу официанта в ресторане. Я продержался официантом два дня. А потом попросил администратора, разрешить мне возвратиться к моей кулинарной деятельности. Мне было гораздо легче работать поваром, чем официантом.
Так вот, официант — это классический пример операционной системы, реального времени (ОСРВ) — системы, которая должна реагировать на множество событий, пользуясь ограниченными ресурсами. Во встроенной системе управления, основанной на ОСРВ, мы имеем только одно устройство, последовательный процессор, которое должно обнаруживать множество событий, сортировать их по приоритетам, и реагировать на них соответствующим образом. При этом процессор не может обслуживать до полного завершения только событие с самым высоким приоритетом, игнорируя все остальные. Он должен каким-то образом реагировать на событие с самым высоким приоритетом в течение некоторого времени, а затем перейти к обработке другого события со следующим уровнем приоритета. Нетрудно представить себе, что ждет официанта, который всю свою энергию и внимание будет уделять только одному столику, игнорируя все остальные.
Процессоры, переключаясь с одной задачи на другую, должны помнить такие ключевые детали происходящих событий, как значения основных регистров, программных счетчиков, и так далее. Назовем эту информацию контекстом задачи. Точно так же, как официант переключает свое внимание «от столика к столику», так и процессоры должны помнить состояние каждого своего «столика».
Как мы уже сказали, когда происходит незапланированное событие с более высоким приоритетом, процессор (или официант) должен своевременно ответить на это новое событие, не игнорируя при этом полностью все остальные. Кроме того, должна иметься связь между отдельными задачами, которая называется межзадачной связью.
Наша цель в этой главе состоит в том, чтобы познакомить вас с понятиями и сложностями разработки ОСРВ системы. Будем считать, что вы не имеете никакой предварительной подготовки в этой области. Мы не предлагаем вам написать собственную ОСРВ, отказавшись от покупки готовой версии. Наша цель состоит в том, чтобы рассказать о понятиях и проблемах ОСРВ. А выбор между разработкой собственной системы и покупкой коммерческой версии мы оставляем за вами.
В этом разделе мы обсуждаем основные концепции ОСРВ. Как только вы получите общее представление о том, что же такое ОСРВ, мы начнем обзор основных концепций, касающихся их разработки.
Так что же такое операционная система реального времени? Согласно большинству фундаментальных определений, ОСРВ — это операционная система процессора, которая должна своевременно обрабатывать несколько событий при ограниченных ресурсах. Во встроенной системе управления целый ряд событий обрабатывается одним устройством — последовательным процессором. Операционная система должна обрабатывать многие, часто одновременные события, каждое из которых требует для своей для этого драгоценного времени. Так как мы используем одно устройство, последовательный процессор, он может выполнять в каждый момент только один шаг программы. Мы исследуем методы, которые позволяют так определить систему приоритетов событий, чтобы сначала были обработаны события с самым высоким приоритетом, но при этом другие события с более низким приоритетом не игнорировались.
Все события обрабатываются как бы одновременно (хотя мы должны снова подчеркнуть, что это не соответствует действительности, так как мы используем для обработки только одно устройство — последовательный процессор).
События, которыми управляет ОСРВ могут быть периодическими, асинхронными или возникающими в произвольное время. Например, если мы разрабатываем ОСРВ для управления всей вашей домашней работой, некоторые события, такие, например, как корректировка температуры в помещениях, будут происходить периодически, в то время как другие, такие как срабатывание охранной или пожарной сигнализации, не могут быть запланированы и происходят в любое время.
Мы исследуем здесь широкий диапазон систем ОСРВ — от простых систем опроса (поллинга) до сложных, с несколькими различными прерываниями и рассмотрим также смешанные системы, которые являются комбинацией обоих типов систем. При простом опросе, операционная система регулярно проходит через ряд задач. Например, операционная система, контролируя местную защиту, могла бы последовательно циклически опрашивать состояние каждого датчика защиты в помещениях вашей фирмы. При этом система защиты как бы задает каждому датчику вопрос «А здесь все в порядке?» Система же, использующая несколько прерываний, вместо этого реагировала бы на события в тот момент, когда они случаются. Например, при активации датчика защиты, он вызывал бы тревогу в операционной системе через прерывание, показывая, что произошло некоторое важное событие, требующее реакции. Различие между двумя операционными системами заключается в том, что при поллинге, операционная система постоянно опрашивает состояния, а система, управляемая прерыванием, приводится в готовность, когда происходит некоторое ключевое событие. Важно подчеркнуть, что ни одна методика не лучше другой. Каждая имеет собственные преимущества и недостатки. В действительности важно, чтобы особенности каждой операционной системы соответствовали ее конкретному применению.
Пример: Имеется пример, иллюстрирующий, что операционная система может выполнять критическую функцию, но при этом использовать метод поллинга. Первый автор книги работал в старших классах и во время обучения в колледже в электронной фирме. Фирма разрабатывала систему защиты от хищения телевизоров для мотелей или гостиниц. Эта система должна была опрашивать телевизоры в каждом номере, чтобы установить их наличие. Система непрерывно и последовательно опрашивала все телевизоры в гостинице или мотеле. Если телевизор не отвечал, в главном офисе гостиницы звучал сигнал тревоги и высвечивался номер помещения на семисегментном дисплее. И снова, продолжался опрос, поскольку все действия имели равный приоритет. Это была простая методика, которую к тому же довольно легко было реализовать.
А теперь вернемся к теории ОСРВ. Любое действие операционной системы в ОСРВ называется задачей. Так как система предназначена для обработки целого ряда задач, она называется многозадачной системой. Считается, что все задачи выполняются одновременно. То есть отдельная задача выполняется до определенной точки перехода внутри задачи или до определенного момента, связанного с функциями задачи. Затем операционная система передает управление другой задаче, ожидающей выполнения. Управление возвращается первой задаче спустя некоторое время, и она продолжает выполнять соответствующие действия с того места, где была прервана. Так как операционная система передает управление от задачи к задаче, важно, чтобы состояние всех ключевых регистров, связанных с задачей сохранялось во время выполнения других задач. Ключевые значения регистров называются контекстом задачи. Такая работа ничем не отличается от деятельности официанта в приведенном ранее рассказе. Когда официант переключает внимание с одного столика на другой, ему, или ей необходимо запомнить состояние предыдущего столика — его контекст.
Операционные системы реального времени могут быть классифицированы по степени риска, связанного с невыполнением задачи за установленное ограниченное время обработки. Если система не завершает назначенную задачу за ограниченное время, то происходит сбой в работе ОСРВ. Даже удачное завершение задачи с запаздыванием должно считаться сбоем. Например, если мы рассматриваем ОСРВ, корректирующую направление полета ракеты, то будет считаться сбоем, если вычисления не будут закончены достаточно быстро, чтобы воздействовать на ход ракеты. По критичности ограничений, налагаемых на время отклика, системы ОСРВ можно разделить на следующие категории [8]:
• Жесткая система реального времени: система, где выход момента окончания вычислений за установленные временные рамки ведет к сбою системы;
• Твердая система реального времени: система, в которой небольшой выход за установленный предельный интервала может допускаться;
• Мягкая система реального времени: система, эффективность которой ухудшается при выходе за пределы временных ограничений, но система продолжает функционировать.
ОСРВ выполняет все действия, связанные с задачей, включая следующее:
• Управление задачей, включая планирование и диспетчеризацию;
• Связь между задачами (межзадачная связь);
• Управление системой памяти;
• Управление системой ввода–вывода (I/O);
• Синхронизация;
• Управление обработкой ошибок;
• Управление cообщениями.
При рассмотрении встроенных систем ОСРВ, мы обычно касаемся только небольшой части операционной системы, которая называется ядром и обеспечивает ключевые функции планирования задачи, диспетчеризации и межзадачной связи.
Далее в настоящей главе мы рассмотрим эти понятия.
Вопрос: Что такое задача?
Ответ: Каждое действие ОСРВ определено как задача.
Вопрос: Что называется контекстом?
Ответ: Текущее значение ключевых регистров, связанных с задачей.
Вопрос: Что называется межзадачной связью?
Ответ: Связь между различными задачами.
Вопрос: Каково различие между жесткой, твердой и мягкой ОСРВ?
Ответ: В жесткой системе, выход времени выполнения задачи за установленный предел ведет к сбою системы; в твердой системе может допускаться незначительный выход за установленные временные рамки; в мягкой системе, выход за установленные временные рамки снижает эффективность системы, но приводит к сбоям.
Вопрос: Что такое ядро?
Ответ: Ядро — очень небольшая часть операционной системы, которая обеспечивает ключевые функции планирования задачи, диспетчеризации, и межзадачной связи.
Прежде чем перейти к дальнейшему изучению ОСРВ, мы должны сделать дать некоторых понятия, касающиеся ОСРВ.
В этом разделе мы проведем обзор некоторых важных тем, касающихся ОСРВ. Некоторые авторы определенно (и безапелляционно) заявляют, что программы ОСРВ должны быть написаны на ассемблере, чтобы обеспечить максимальное быстродействие системы. Не оспаривая такой точки зрения, мы тем не менее, представляем наши примеры на языке С, чтобы яснее осветить понятия и важные детали ОСРВ. Рассмотрим связь некоторых понятий языка С с основными принципами структур данных. ОСРВ состоит из этих совместно работающих основных структур данных, позволяющих выполнить требования к системе.
Мы советуем вам возвратиться к предыдущим разделам и вспомнить о следующих понятиях: об указателях (глава 3), глобальных и локальных переменных (глава 3) и свойствах стеков и в прерываний 68HC12 (глава 4). Повторив эти разделы, вы можете вернуться к рассмотрению динамического распределения памяти.
В главе 4 мы обсуждали систему памяти, встроенную в отладочную плату (EVB) контроллера B32, который предназначен прежде всего для однокристальных применений. Он включает в себя 32 Кб ПЗУ типа FLASH, 1 Кб статической оперативной памяти RAM и 768 байт стираемого по байту ПЗУ типа EEPROM для сохранения данных системы. Карта памяти для B32 EVB показана на рис. 8.1. Обратите внимание, что большая часть объема принадлежит флеш-памяти EEPROM, в то время как оперативная память (RAM) занимает только 1 Кб. Фактически же только 512 байтов, доступны для использования (от $0800 до $9FFF).
$0000 $01FF | Регистры ЦП |
$0800 $0BFF | 1 Кб RAM «на чипе»; • Код/данные пользователя; • Резервирована для D-Bug12 ($0A00-$0BFF) |
$0D00 $0FFF | 768 байт EEPROM «на чипе»; • Код/данные пользователя |
$8000 $FFFF | FLASH EEPROM 32 Кб «на чипе»; • код D-Bug12 ($8000-$F67F); • Область, доступная пользователю ($F6C0–$F6FF); • Настройка функций D-Bug12 ($F680–$F6BF); • Код запуска D-Bug12 ($F700–$F77F); • Таблица вектора прерывания ($F780–$F7FF); • Расширение загрузчика ($F800-$FBFF); • EEPROM загрузчика ($FC00–$FFBF); • Векторы сброса и прерывания ($FFC0–$FFFF) |
Рис. 8.1. Карта памяти микроконтроллера B32
Для чего же эта RAM используется? Прежде всего она используется для локальных переменных каждой функции. Переменные, помещенные в стек, доступны только при вызове функции. При выходе из функции переменные удаляются из стека, освобождая пространство памяти. Ясно, что можно достаточно быстро исчерпать объем стека, если ваш встроенный контроллер использует рекурсивную подпрограмму (вызываемую, например, для получения ряда Фибоначчи или операции вычисления факториала) или функцию с высокой степенью вложения. То есть при обращении к функции, которая снова вызывает функцию, и т.д. В этих ситуациях активны как все функции, так и связанные с ними локальные переменные.
Ключевой инструмент ОСРВ — использование таких абстрактных типов данных, как записи, списки с указателями и очереди. Обсудим их вкратце. Эти типы данных обычно используют динамическое распределение памяти RAM. Где мы можем взять RAM для этих типов данных? Если мы используем 512 байт как для абстрактных типов данных, так и для стека мы можем потенциально сталкиваться с «зависанием» структуры данных при переполнении стека или наоборот. Это может привести к катастрофическому сбою системы. Мы должны предотвратить это любой ценой. В идеале, мы должны выделить дополнительную память RAM в карте памяти B32. Было бы также полезно физически отделить эту память от памяти RAM на плате B32. Это позволило бы нам иметь отдельные пространства RAM для стека и динамической памяти. Но это не всегда возможно. Если же и стек и динамическая память постоянно находятся в одном и том же пространстве памяти, необходимо гарантировать, что не происходит подмены информации в процессе выполнения программы. Контроллер 68HC12 не обеспечивает автоматического контроля этой ситуации и исключение ее при программировании системы входит в вашу задачу.
В разделе 4.7.1 мы уже обсуждали систему памяти, размещенной на плате B32. На рис. 8.2 приведено схемное решение системы, с учетом которого мы можем использовать дополнительное пространство RAM. Настоятельно рекомендуем вам ознакомится с концепций расширения памяти у Pack и Barrett [2002, Ch. 8]. Здесь же мы только приводим схемное решение, не обсуждая проблем подключения памяти, электрической связи с помощью интерфейса и синхронизации. Хотя все они чрезвычайно важны, но при обсуждении динамического распределения памяти, без их рассмотрения можно обойтись.
Рис. 8.2. Подключение внешней памяти к микроконтроллеру при программировании
Прежде чем определить конкретные границы этого дополнительного пространства RAM, обеспечим их удобный расчет. Мы хотим определить область 16 Кб для статической оперативной памяти (SRAM) в карте памяти B32 EVB. Разместим эту память в ячейках от $4000 до $7FFF. Если бы это удалось, то все, к чему мы имели доступ на участке памяти — это 32 Кб×8 SRAM. Если мы заземлим старшую линию адреса этой памяти A[14], то старшие 16 Кб чипа памяти будут недоступны. Как мы уже упоминали в главе 4, PORTA обеспечивает мультиплексную линию данных D[7:0], и старшие разряды адреса A[15:8] в расширенных режимах работы МК. PORTB обеспечивает младшие разряды адреса A[7:0]. Мы используем адресную линию A[15:14] со схемой И-НЕ, чтобы создать активный низкий сигнал для SRAM памяти чипа.
Альтернативой расширению памяти является использование варианта HCS12 с большей дополнительной областью RAM. Обратитесь к различным доступным вариантам, описанным в разделах 1.3, 1.4 и 4.8. Эти варианты включают в себя RAM объемом от 4 до 12 Кб.
Перед продолжением нашего обсуждения, относящегося к динамическому распределению памяти, рассмотрим еще несколько проблем, воспользовавшись рис. 8.2.
Вопрос: Какова цель применения схемы И-НЕ?
Ответ: Логический элемент И-НЕ переходит на низкий уровень и генерирует сигнал разрешения (CE) для SRAM памяти, когда A15 находится на низком , а A14 — на высоком уровне.
Вопрос: Какова цель применения микросхемы 74HC573?
Ответ: Эта микросхема действует как защелка при демультиплексировании линий адреса/данных от порта РA.
Вопрос: Каков промежуток адресов памяти RAM?
Ответ: От $4000 до $7FFF или 16 Кб RAM.
Вопрос: Каков размер SRAM памяти?
Ответ: Этот размер составляет 215 или 32К адресов с размещением одного байта в каждом адресе. Нам удается использовать только нижние 16 Кб памяти.
Вопрос: Какой вид будет иметь карта памяти после введения этих новых компонентов памяти.
Ответ: Карта памяти приведена на рис. 8.3.
$0000 $01FF | Регистры ЦП |
$0800 $0BFF | 1 Кб RAM «на чипе»; • Код/данные пользователя ($0800-$09FF); • Резервирована для D-Bug12 ($0A00-$0BFF) |
$0D00 $0FFF | 768 байт EEPROM «на чипе»; • Код/данные пользователя |
$8000 $FFFF | FLASH EEPROM 32 Кб «на чипе»; • код D-Bug12 ($8000–$F67F); • Область, доступная пользователю ($F6C0–$F6FF); • Настройка функций D–Bug12 ($F680-$F6BF); • Код запуска D-Bug12 ($F700–$F77F); • Таблица вектора прерывания ($F780–$F7FF); • Расширение загрузчика ($F800–$FBFF); • EEPROM загрузчика ($FC00–$FFBF); • Векторы сброса и прерывания ($FFC0–$FFFF) |
Рис. 8.3. Карта памяти B32, расширенная внешней флеш-памятью и RAM
В компиляторе С для эффективного управления динамической памятью используются указатели. Они позволяют объявить большое число переменных различных типов и размеров. Когда память динамически распределена для переменных в RAM, указатель может привести вас к ресурсам памяти для переменной.
Динамическое распределение памяти осуществляется с помощью команды распределения памяти
malloc()
. Эта команда содержится в файле заголовка stdlib.h, который является частью любого С компилятора. Команда malloc()
обычно используется вместе с функцией sizeof()
. Эта комбинация функций чрезвычайно полезна при динамическом распределении памяти. Общая форма этой комбинации функций:
Ptr = (variable_type)*malloc(sizeof(variable_type));
Большинство структур данных объявляется и распределяется с помощью этой методики. Когда переменная больше не нужна, пространство памяти используемое для нее, возвращается системе с помощью функции
free()
. Это наилучший способ динамического распределения памяти. Мы создаем переменные на ходу (в процессе выполнения программы), когда мы нуждаемся в них, и избавляемся от переменных, когда они больше не нужны.
При таком распределении и освобождении памяти, происходящем в процессе выполнения программы, необходима эффективная система управления памятью. Динамическая память — это часть памяти RAM, используемая для динамического распределения памяти. Как мы упомянули прежде, важно сохранять стек и динамическую память, отделенными от друг друга.
Теперь, когда мы имеем два отдельных пространства RAM в нашей карте памяти, мы можем легко выделить одну из них для стека, а другую — для динамической памяти. Мы рекомендуем использование меньшего пространства для стека и большего для динамической памяти. В данном компиляторе имеются параметры настройки, конфигурируемые пользователем и позволяющие установить адрес начала для стека и области динамической памяти.
Получив способ динамического распределения памяти, мы можем теперь создавать структуры данных для использования в наших ОСРВ. В следующем разделе мы проведем общий обзор структур данных, используемых в ОСРВ: структур (или записей), списков с указателями, очередей, и круговых очередей. Как только мы познакомимся с каждым из этих основных типов, мы сможем объединять их в более сложные структуры данных, используемые для работы ОСРВ.
В этом разделе мы проведем общий обзор структур данных, используемых в операционных системах, работающих в режиме реального времени. Мы выполним обзор каждой структуры данных отдельно и затем объединим их, чтобы выполнять различные операции внутри ОСРВ.
Структура. Мы будем использовать взаимозаменяемые термины запись и структура. Структуры позволяют программистам при разработке совокупности данных для спецификации, использовать другие основные типы данных. Это позволяет им следить за сложной информацией, которая может содержать различные типы данных. Например, если вы разрабатываете систему описи для автомобильного торгового агента, вы могли бы разработать следующую структуру основных данных для конкретного автомобиля, таких, как год выпуска, изготовитель, модель, номер идентификации транспортного средства (VIN), и прогон, как показано на рис. 8.4.
Рис. 8.4. Запись для автомобиля. Запись содержит поля данных, совокупность которых описывает автомобиль
Каждый из типов данных в записи называется полем. Мы объявляем запись или структуру, используя следующий синтаксис:
struct car {
int year; /* год производства* /
char make[10]; /*BWM, Hummer, Saturn */
char model[12]; /*купе, обратимый, SUV, пикап */
char VIN[10]; /* комбинация цифр, букв */
float mileage; /*показания счетчика: от 0 до 500,000+ */
struct car *next; /*указатель на следующий автомобиль в списке*/
};
/*typedef обеспечивает компилятор an alternate???*/
typedef struct car ELEMENT; /*для типа переменной */
typedef ELEMENT *car_temp_ptr; /*определяет указатель на автомобиль*/
Как можно видеть, все поля записи описывают различные параметры данного автомобиля. Мы привели сокращенный список параметров. Структура может содержать столько полей, сколько необходимо. Мы выбрали для каждого поля тот тип данных, который наиболее подходит для описания каждого параметра автомобиля.
Структуры создаются с использованием динамических методов распределения памяти. Как мы уже упоминали, команда
malloc()
— предоставляет методику динамического распределения памяти, определяя соответствующий объем памяти для структуры данных. Например, чтобы создать новую запись для автомобиля, можно использовать следующий код:
сar_temp_ptr new_car_entry;
new_car_entry = (car_temp_ptr)malloc(sizeof(ELEMENT));
В главе 3, мы описали, как обращаться к различным полям внутри записи. В этом примере, мы инициализируем недавно созданную автомобильную запись с информацией о конкретном автомобиле. Обратите внимание, что мы использовали оператор
–>
, чтобы обратиться к специфическому полю внутри структуры.
/* инициализация новых полей для структуры car */
new_car_entry–>year = 1981; /* год производства */
strcpy(new_car_entry–>make, "Chevy"); /*BWM, Hummer, Saturn */
strcpy(new_car_entry–>model, "Camaro"); /*купе, обратимый, SUV, пикап */
strcpy(new_car_entry–>VIN, " 12Z3 67"); /* комбинация цифр, букв*/
new_car_entry–>mileage = 37456; /*показания спидометра: от 0 до 500,000+ */
new_car_entry–>next = NULL; /* определяет указатель на автомобиль */
Чтобы распечатать различные поля записи, мы могли бы использовать следующий код:
printf("\nyear: %4d ", new_car_entry–>year); / *year mfg */
printf("\nmake: %s ", new_car_entry–>make); /*car делает */
printf("\nmodel: %s ", new_car_entry–>model); /*model*/
printf("\nVIN: является ", new_car_entry–>VIN); /*VIN */
printf("\nMileage: %6.of ", new_car_entry –> mileage); /*odometer reading*/
Поместим все эти части вместе с примером. Мы убедительно просим вас компилировать и выполнить этот код.
#include /*стандартные входные/выходные функции*/
#include /*библиотека и распределение памяти */
void main(void) {
/*определение структуры */
struct car;
int year; /*год выпуска */
char make[10]; /*BWM, Hummer, Saturn*/
char model[12]; /*купе, обратимый, SUV, пикап */
char VIN[10]; /*комбинация цифр и букв*/
float mileage; /*показания одометра: от 0 до 500 000+ */
struct car *next; /*указатель на следующий автомобиль в списке */
typedef struct car ELEMENT;
typedef ELEMENT *car_temp_ptr;
car_temp_ptr new_car_entry; /*ввод записи для автомобиля*/
new_car_entry = (car_temp_ptr)malloc(sizeof(ELEMENT));
/*инициализация новых полей для автомобиля */
new_car_entry->year = 1981; /*год изготовления*/
strcpy(new_car_entry–>make, "Chevy"); /*BWM, Hummer, Saturn */
strcpy(new_car_entry–>model,"Camaro");
/* купе, обратимый, SUV, пикап */
strcpy(new_car_entry–>VIN, "12Z3 67");
/* комбинация цифр и букв */
new_car_entry–>mileage - 37456;/*показания одометра: 0 to 500,000+*/
new_car_entry–>next - NULL; /*указатель на следующий автомобиль в списке*/
printf("\nyear: %4d", new_car_entry–>year); /*год выпуска */
printf("\nmake: %s", new_car_entry–>make) ; /*производитель */
printf("\nmodel: %s", new_car_entry–>model); /*модель */
printf("\nVIN: %s ", new_car_entry –>VIN); /*номер */
printf("\nMileage: %6. Из ", new_car_entry->mileage); /*показания одометра*/
}
Когда эта программа будет откомпилирована и выполнена, на экране вашего ПК появится следующее сообщение:
year: 1981
make: Chevy
model: Camaro
VIN: 12Z367
Mileage: 37456
Пока, наверное, использование динамического распределения памяти не кажется вам очень мощным инструментом. В следующем разделе вы сможете оценить силу динамического распределения памяти, когда мы скомпонуем записи и создадим список, что позволит нам осуществлять изменения в процессе выполнения программы. То есть мы сможем добавлять новые элементы в список, переносить элементы из одного списка в другой, удалять элементы из списка, и т.д. Но не будем здесь углубляться в лес деталей, чтобы сразу в нем не заблудиться. Лучше сосредоточим свое внимание на дороге через этот лес. Не будем забывать, что мы изучаем здесь основные структуры данных, чтобы узнать, как они могут использоваться в построении и реализации операционной системы в режиме реального времени.
Список с указателями. Список с указателями является мощной структурой данных, которая может быть создана, вставлена в программу или удалена из нее динамически в процессе выполнения программы. Список связей состоит из узлов, содержащих две части: часть данных и часть поля связи. Часть данных хранится информация об узле (или пункте) списка. Например, если мы должны были создать список автомобилей, готовых к продаже, частью данных для узла будет структура (или запись) car, которую мы разработали в предыдущем разделе. Полем связи был бы указатель (адрес памяти) следующую запись в списке. Начало списка названо головой (head). Конец списка называется хвостом (tail) и обозначается символом нуля (?) в поле связи. Это построение списка иллюстрируется на рис. 8.5. Здесь приведено объявление различных списков, позволяющих автомобильным дилерам проследить за состоянием парка автомобилей.
car_temp_ptr head_ptr; /*начало списка состояния автомобилей */
car_temp_ptr in_stock_list; / *автомобили в наличии */
car_temp_ptr repair_list; /*автомобили в ремонтных мастерских - не предназначены для продажи */
car_temp_ptr paint_shop_list; /*автомобили в мастерских покраски - не предназначены для продажи */
car_temp_ptr sold_list; /*проданные автомобили - не предназначены для продажи */
Мир автомобильных продаж очень динамичен. Автомобильные дилеры постоянно продают автомобили, занимаются их обменом, размещают автомобили в мастерских для ремонта, или даже производят перекрашивание автомобилей. Если нам необходимо создать список для каждого из этих действий, мы будем постоянно добавлять и удалять элементы в каждом из списков. Эти действия иллюстрируются на рис. 8.5в и 8.5г.
Рис. 8.5. Операции со списком a) список состоит из узла с данными и указателя, мента в список, г) удаление элемента
Чтобы вставить новый автомобиль в список, мы были бы должны найти соответствующее место для автомобиля в существующем списке. Например, если мы размещаем автомобили в алфавитном порядок, мы должны просматривать список, пока мы не найдем, соответствующее место чтобы вставить автомобиль в список, как показано на рис. 8.6. Как только соответствующая точка ввода найдена, указатель предшествующего узла должен быть изменен, чтобы указать на новый автомобиль, и указатель нового автомобиля должен быть изменен, чтобы указать на следующий автомобиль в списке.
Рис. 8.6. Список с указателями для текущего учета автомобилей
Когда автомобиль продан, он больше не доступен для продажи. Он должно быть затем быть удален из списка «для продажи». Для этого связь предшествующего автомобиля должна теперь указывать на последующий автомобиль. Автомобиль, который был продан, будет теперь действительно исключен из списка «для продажи» и может быть добавлен в список «проданные». Если бы у нас не было списка «проданные», мы могли бы освободить динамическую память, выделенную для этого автомобиля. Это осуществляется с помощью команды
free(argument)
.
Если вы ищите в списке определенный автомобиль, вы должны будете следовать по цепочке указателей, пока желательный автомобиль не будет найден. Мы выполнили бы поиск, исследуя содержание определенных полей каждой записи в списке с указателями.
Рис. 8.7. Общие функции, связанные со списками с указателями
На рис. 8.7 показаны общие функции, связанные со списком с указателями. Код для каждой из этих функций приведен ниже:
/*********************************************************************
/*имя файла: linklist.с */
/*********************************************************************
/*включенные файлы*/
*include /*стандартная библиотека ввода - вывода */
*include /*стандартная библиотека динамического */
/*распределения*/
/*global variables - глобальные переменные. Объявление этих переменных */
/*помещают в файл заголовка (header file). Они приведены здесь, */
/* чтобы иллюстрировать последовательность построения программы. */
/*определение структуры "автомобиль"*/
struct car {
int year; /* год производства */
char make[10]; /*BWM, Hummer, Saturn */
char model[12]; /*купе, обратимый, SUV, пикап */
char VTN[10]; /*комбинация цифр, букв */
float mileage; /*показания спидометра: от 0 до 500,000+*/
struct car *next; /*указатель на следующий автомобиль в списке */
};
/*определение указателя на автомобиль */
typedef struct car ELEMENT;
typedef ELEMENT *car_temp_ptr;
/*функции прототипов*/
void initialize_link_list(void);
void print_link_list(car_temp_ptr);
void insert_link_list(car_temp_ptr);
void delete_link_list(car_temp_ptr);
void search_link_list(car_temp_ptr);
/*переменные*/
/ " Создают списки, чтобы следить за состоянием автомобильного сервиса*/
car_temp_ptr in_stock_list; /* автомобили в продаже */
car_temp_ptr repair_list; /* автомобили в ремонт - не подлежат продаже*/
car_temp_ptr paint_shop_list;/*автомобили в покраске - не подлежат продаже*/
car_temp_ptr sold_list; /*проданные автомобили -- не подлежат продаже*/
car_temp_ptr new_car_entry; /*новый автомобиль для введения в список*/
int TRUE=1, FALSE=0; /*логические флаги */
void main(void) {
/*заполняет пустой список переменными NULL */
in_stock_list = NULL; /* автомобили в продаже */
repair_list = NULL; /* автомобили в ремонте - не подлежат продаже */
paint_shop_list = NULL; /* автомобили в покраске - не подлежат продаже*/
sold_list = NULL; /*проданные автомобили -- не подлежат продаже * /
new_car_entry = NULL;
initialize_link_list(); /*составление списка для продажи */
print_link_list(in_stock_list); /*print the list */
insert_link_list(in_stock_list); /*вставить новый автомобиль в список*/
print_link_list(in_stock_list); /*распечатать список */
delete_link_list(in_stock_list); /*удалить автомобиль из списка */
print_link_list(in_stock_list); /*распечатать список */
search_link_list(in_stock_list); /*поиск определенного пункта в списке */
}
/********************************************************************/
/*void initialize_link_list (car_temp_ptr): инициализирует автомобиль */
/* для списка продаж используя список.Отметим, что этот список */
/* с указателями был объявлен как глобальная переменная. */
/*********************************************************************
void initialize_link_list(void) {
car_temp_ptr new_car_entry1, new_car_entry2;
/*создает вход в список автомобилей */
new_car_entry = (car_temp_ptr)malloc(sizeof(ELEMENT));
/*инициализирует новые поля для ввода автомобиля в список*/
new_car_entry->year = 1981; /*год выпуска */
strcpy(new_car_entry->make, "Chevy"); /*BWM, Hummer, Saturn */
strcpy(new_car_entry->model, "Camaro"); /*купе, обратимый, SUV, пикап*/
strcpy(new_car_entry->VIN, "12Z3 67"); /*комбинация цифр и букв */
new_car_entry->mileage = 37456; /*показания одометра: от 0 до 500 000+*/
new_car_entry->next = NULL; /*указатель на следующий автомобиль в списке*/
in_stock_list = new_car_entry;
new_car_entry1 = (car_temp_ptr) malloc(sizeof(ELEMENT));
/*инициализирует новые поля для ввода автомобиля в список*/
new_car_entry1->year = 1974; /*год выпуска*/
strcpy(new_car_entry1->make,"Ford"); /*BWM, Hummer, Saturn */
strcpy(new_car_entry1->model,"Mustang11")/*купе, обратимый, SUV, пикап*/
strcpy(new_car_entry1->VIN, "3L265ST" ) ; /*комбинация цифр и букв */
new_car_entry1->mileage = 122456; /*показания одометра: от 0 до 500 000+ */
new_car_entry1->next = NULL; /*указатель на следующий автомобиль в списке */
new_car_entry2 = (car_temp_ptr)malloc(sizeof(ELEMENT));
/*инициализирует новые поля для ввода автомобиля в список*/
new_car_entry2->year = 1997; /*год выпуска*/
strcpy(new_car_entry2->make, "Saturn"); /*BWM, Hummer, Saturn */
strcpy(new_car_entry2->model,"SL1"); /*купе, обратимый, SUV, пикап */
strcpy(new_car_entry2->VIN, "234TH67"); /*комбинация цифр и букв */
new_car_entry2->mileage = 140512;/*показания одометра: от 0 до 500 000+ */
new_car_entry2->next = NULL; /*указатель на следующий автомобиль в списке*/
new_car_entry1->next = new_car_entry2;
}
/********************************************************************/
/*print_link_list: печатает поля выделенного списка с указателями */
/********************************************************************/
void print_link_list(car_temp_ptr print_list) {
car_temp_ptr temp_ptr; /*объявляет текущий указатель */
printf("\nCars available in stock for sale:");
/*продвижение по списку */
for (temp_ptr=print_list; temp_ptr != NULL; temp_ptr-temp_ptr->next) {
printf("\n\nyear: %4d, temp_ptr->year); /*год выпуска*/
printf("\nmake: %s", temp_ptr->make); /*изготовитель*/
printf("\nmodel: %s", temp_ptr->model); /*модель*/
printf("\nVIN: %S", temp_ptr->VIN); /*номер*/
printf("\nMileage: %6.0f", temp_ptr->mileage); /*показания одометра*/
}
}
/********************************************************************/
/*insert_link_list (in_stock_list) - вставляют новый автомобиль в */
/* отмеченный список в алфавитном порядке */
/********************************************************************/
void insert_link_list(car_temp_ptr in_stock_list) {
car_temp_ptr new_car_entry, list, ptr;
int place_found;
list = in_stock_list;
/*создает ввод автомобиля */
new_car_entry = (car_temp_ptr) malloc(sizeof(ELEMENT));
/*инициализирует новые поля для ввода автомобиля в список */
new_car_entry->year = 2002; /*год выпуска */
strcpy(new_car_entry->make,"Hummer"); /*BWM, Hummer, Saturn*/
strcpy(new_car_entry->model, "H2"); /*купе, обратимый, SUV, пикап */
strcpy(new_car_entry->VTIM, "73H2L7");/*комбинация цифр и букв*/
new_car_entry->mileage = 13; /*показания одометра: от 0 до 500 000+ */
new_car_entry->next = NULL; /*указатель на следующий автомобиль в списке */
if (list==NULL) { /*вставка в пустой список */
list=new_car_entry;
} else {
/* вставка в первый элемент списка */
if (strcmp(new_car_entry->make, list->make) < 1) {
new_car_entry->next=list;
list = new_car_entry;
} else /*вставка в непустой список */
{
ptr = list; /*определение позиции вставки */
place_found = FALSE;
while((ptr->next != NULL) && (!place_found)) {
if (strcmp (new_car_entry->make, ptr->next->make) > = 1) /*сравнение */
{
ptr=ptr->next; /*продвижение по списку */
} else /*вставка после указателя */
{
place_found = TRUE;
}
}/*конец цикла while*/
/*переадресует указатель, чтобы */
/*закончить ввод в список */
new_car_entry->next = ptr->next;
ptr->next - new_car_entry;
}/*конец else*/
}/*конец else*/
}/*конец insert_link_list*/
/********************************************************************/
/*delete_link_list (car_temp_ptr): */удаление отмеченных элементов */
/*из списка */
/********************************************************************/
void delete_link_list(car_temp_ptr in_stock_list) {
car_temp_ptr current,backup,temp; /*текущий указатель списка */
char delete_make[10];
/*определить поле make для удаления */
printf("\n\nDelete car from for sale list.");
printf("\nEnter make of car for deletion from list.");
scanf("%s", delete_make);
/*инициировать указатели для поиска */
current = in_stock_list;
backup=NULL;
/*поиск записи, содержащих заданное значение make */
while (strcmp(current->make, delete_make) !=0) {
backup = current;
current = current->next;
}
/*Был удален автомобиль из первого элемента? */
if (backup == NULL){ /*удалить автомобиль из первого элемента */
in_stock_list = in_stock_list->next;
} else { /*удалить элемент из списка */
backup->next = current -> next;
}
free(current); /*перераспределить динамическую память*/
}
/********************************************************************/
/********************************************************************/
/*void search_link_list (car_temp_ptr) - найти запись с определенным */
/* значением поля make. Распечатать автомобили этого изготовителя. */
/********************************************************************/
void search_link_list(car_temp_ptr search_list) {
char search_make[10];
car_temp_ptr temp_ptr; /*объявить текущий указатель */
/*определить изготовителя для поиска */
printf("\n\nSearch for car in stock.");
printf("\nEnter make of car to search for in list. ");
scanf("%s", search_make);
/*движение по списку */
for(temp_ptr-search_list; temp_ptr!=NULL; temp_ptr=temp_ptr->next) {
if (strcmp(temp_ptr->make, search_make) == 0) {
printf("\n\nyear: %4d", temp_ptr->year); /*год изготовления */
printf("\nmake: %s", temp_ptr->make); /*изготовитель */
printf("\nmodel: %s", temp_ptr->model); /*модель */
printf("\nVIN: %s", temp_ptr->VIN); /*номер */
printf("\nMileage: %6.0f ", temp_ptr->mileage);
/*считать показания одометра*/
}
}
}
/********************************************************************/
/********************************************************************/
После выполнения программы на дисплее появится следующее сообщение.
year: 1981
make: Chevy
model: Camaro
VIN: 12Z367
mileage: 37456
year: 1974
make: Ford
model: Mustang11
VIN: 3L265ST
mileage: 122456
year: 1997
make: Saturn
model: SL1
VIN: 234TH67
mileage: 140512
year: 1981
make: Chevy
model: Camaro
VIN: 12Z367
mileage: 37456
year: 1974
make:Ford
model: Mustang11
VIN: 3L265ST
mileage: 122456
year: 2002
make: Hummer
model: H2
VIN: 73H2L7
mileage: 13
year: 1997
make: Saturn
model: SL1
VIN: 234TH67
mileage: 140512
Удалены следующие автомобили из списка для продаж.
Введено поле make для удаления из списка – Hummer
year: 1981
make: Chevy
model: Camaro
VIN: 12Z367
mileage: 37456
year: 1974
make: Ford
model: Mustang11
VIN: 3L265ST
mileage: 122456
year: 1997
make: Saturn
model: SL1
VIN: 234TH67
mileage: 140512
Найден автомобиль в списке продаж.
Введенное поле make для поиска – Saturn
year: 1997
make: Saturn
model: SL1
VIN: 234TH67
mileage: 140512
Этот очень простой пример был придуман, чтобы показать основные действия, связанные обработкой списка с указателями. Мы намеренно выбрали скелетную программу, чтобы концентрироваться на работе списков с указателями. Если бы мы разрабатывали фактическую программу описи, основанную на списках с указателями, мы разработали бы дружественное меню, которое позволило бы вызывать функции в любом порядке. Кроме того, мы сформировали бы исходный список загрузив данные из файла. Мы обеспечили бы также возможность сохранять данные списка в файле.
Очередь. Очередь представляет собой особым образом сконфигурированный список с указателями. Она называется также буфером первым-вошел-первым-вышел (first-in first out — FIFO). Элементы добавляются в хвост очереди и извлекаются с головы, как показано на рис. 8.8. Вернемся к нашей автомобильной коммерческой аналогии. Как только вы приобретаете автомобиль, вы должны зарегистрировать его в департаменте транспортных средств. Я был там недавно с моим сыном, которому нужны было заменить водительские права. После нашего прихода мы встали в хвост очереди и стали ждать, пока нас обслужат. После короткого ожидания, мы достигли головы очереди и смогли получить требуемые права. Нас обслуживали в порядке прохождения очереди. Во время пика занятости, например, в утренние часы, в конце месяца — очередь в департаменте может быть очень длинной. В более свободное время она может быть совсем короткой. В обоих случаях мы видим, что очередь не имеет фиксированного числа элементов. Длина очереди или число элементов в ней, является переменной величиной и изменяется, в зависимости от действия программы.
Рис. 8.8. Структура данных очередь — «первым вошел–первым вышел»
Круговая очередь. В круговой очереди, которая имеет ту же самую базисную структуру, что и простая очередь, указатель последней записи в очереди не пустой, он указывает на первую запись. Круговая очередь показана на рис. 8.9. Как мы скоро увидим, это эффективная структура данных для переключения с задачи на задачу.
Рис. 8.9. Круговая очередь
Стек. Стек — это структура данных последним вошел — первым вышел (last-in-first-out — LIFO), показанная на рис. 8.10. Она также может быть создана с помощью методов списка с указателями. Во встроенных контроллерных системах на базе 68HC12, стек — определенная пользователем часть RAM, в которой в течение нормального выполнения программы временно хранятся переменные, например содержимое какого-либо регистра. Верхняя часть стека в 68HC12 обычно определяется последней позицией RAM плюс один. Указатель вершины стека для 68HC12 содержит адрес последнего используемого расположения стека. Когда элемент помещают в стек, указатель вершины стека уменьшается на 1 (проводится операция декремента). Когда элемент извлекается из стека, указатель вершины стека увеличивается на 1 (операция инкремента). Когда программирование проводится на языке C, положение вершины стека является опцией компилятора. Если встроенная контроллерная система содержит только один стек, лучше всего просто использовать свойства, встроенные в процессор. Однако, как мы скоро увидим, в ОСРВ можем потребоваться несколько стеков, по одному для каждой задачи. В этом случае разработчику системы необходимо, чтобы обеспечить работу нескольких стеков. Если используются динамические методы распределения памяти, то несколько стеков создаются и используются в динамической памяти. Мы оставим разработку структуры данных стека, использующей динамические методы распределения памяти в качестве вашей домашней работы (задача 1 для самостоятельного исследования). Вместо этого, мы разработаем структуру стека с фиксированный размером массива. Стек с фиксированным массивом используется, когда размер стека известен и неизменен. Как мы скоро увидим, стеки с успехом могут используются в блоках управления задачами.
Рис. 8.10. Стек
Структура данных стека, разработаны ли они с использованием методов динамического распределения или фиксированного массива, имеет следующие функции:
•
initialize_stack
: инициализация стека;
•
push
: поместить элемент в стек;
•
pull
: извлечь элемент из стека;
•
stack_empty
: выяснить, не пуст ли стек. Это необходимо сделать до использования функции pull
;
•
stack_full
: выяснить, не полон ли стек. Это необходимо сделать до использования функции push
;
•
print_stack
: распечатать содержимое стека.
Основные операции стека показаны на рис. 8.11.
Рис. 8.11. Операции со стеком
Следующий программный код реализует стек с фиксированным массивом.
/********************************************************************/
/* имя файла: stack.с */
/********************************************************************/
/*включенные файлы*/
#include /*стандартная библиотека I/O */
#include /*стандартная библиотека динамического распределения*/
/*global variables - объявляет глобальные переменные в header file. */
/* Они приведены здесь, чтобы иллюстрировать общее построение программы.*/
/*определение структуры*/
struct stack_struct {
int stack_top; /*следующая используемая позиция в стеке*/
int stack_item[10]; /*элемент, сохраненный в стеке с фиксированным размером */
}
typedef struct stack_struct stack; /*массив для распознавания имен различных переменных*/
typedef stack *stack_ptr; /*определение указателей на стек */
/*функции-прототипы*/
void initialize_stack(stack);
void push(stack*, int); /*Используется метод передачи параметра по ссылке*/
int pull(stack *); /* когда содержимое стека должно измениться */
int stack_empty(stack);
int stack_full(stack);
void print_stack(stack);
/*переменные*/
int YES=1,NO=0; /*логические флаги */
stack stack1; /*объявление стека */
char *filename;
FILE *outputfile;
void main(void) {
int response;
int stack_item;
/*печать результата в файл/
printf("\n\nEnter a filename for output.");
scanf("%s", filename);
outputfile = fopen(filename, "a");
initialize_stack(stack1);
response = stack_empty(stack1); /*вызов по значению */
response = stack_full(stack1);
print_stack(stack1);
push(&stack1, 11); /*вызов по ссылке */
push(&stack1, 12);
push(&stack1, 13);
push{&stack1, 14);
print_stack(stack1);
pull(&stack1);
pull(&stack1);
pull(&stack1);
pull(&stack1);
pull(&stack1);
fclose(outputfile); /*закрыть выходной файл */
}
/********************************************************************/
/*initialize_stack: установить указатель вершины стека в 0 */
/********************************************************************/
void initialize_stack(stack a_stack) {
a_stack.stack_top=0; /*установить указатель стека в 0*/
}
/********************************************************************/
/*stack_empty: возвращает ДА если стек пуст, и НЕТ в противном случае */
/********************************************************************/
int stack_empty(stack a_stack)
{
fprintf(outputfile, "\n\nStack top: %d", a_stack.stack_top);
if (a_stack.stack_top == 0) /*проверить не пуст ли стек*/
{
fprintf(outputfile, "\nStack Empty!");
return YES;
} else {
fprintf(outputfile, "\nStack is not empty.");
return NO;
}
}
/********************************************************************/
/*stack_full: возвращает ДА если стек полон, и НЕТ в противном случае */
/********************************************************************/
int stack_full(stack a_stack) {
if (a_stack.stack_top == 10) /*проверить не заполнен ли стек */
{ /*произвольный выбор предела стека */
fprintf(outputfile, "\n\nStack Full!");
return YES;
} else {
fprintf(outputfile, "\n\nStack is not full.");
return NO;
}
}
/********************************************************************/
/*print_stack: печать текущего элемента (на вершине стека) */
/********************************************************************/
void print_stack(stack a_stack) {
int i;
if (!(stack_empty(a_stack)))/*проверить не пуст ли стек перед печатью*/
{ /*перейти к основанию стека перед печатью */
for(i = a_stack.stack_top; i>=0; i=i-1)
fprintf(outputfile, "\nStack item: %d", a_stack.stack_item[i]);
} else fprintf(outputfile,"\nCannot print - stack is empty!");
}
/********************************************************************/
/*push(stack *, int): запись элемента в стек */
/********************************************************************/
void push(stack *a_stack, int item) {
fprintf(outputfile, "\n\nBefore push - stack pointer: %d",
a_stack->stack_top);
if (!(stack_full(*a_stack))) /*проверка заполнения стека*/
/* перед записью элемента*/
{
a_stack->stack_item[a_stack->stack_top] = item;
fprintf(outputfile, "\nstack item after push: %d",
a_stack->stack_item[a_stack->stack_top]);
a_stack->stack_top = a_stack->stack_top + 1;
fprintf(outputfile, "\nstacktop after push: %d",
a_stack->stack_top);
} else fprintf(outputfile, "\nCannot push - stack is full!");
}
/********************************************************************/
/*pull(stack *): извлечение элемента из стека */
/********************************************************************/
int pull(stack *a_stack) {
int item;
fprintf(outputfile,"\n\nBefore pull - stack pointer: %d",
a_stack->stack_top);
if (!(stack_empty(*a_stack))) /*проверка не пуст ли стек */
/*перед извлечением элемента*/
{
item = a_stack->stack_item[a_stack->stack_top-1];
fprintf(outputfile, "\nstack item pulled: %d", item);
a_stack->stack_top = a_stack->stack_top - 1;
fprintf(outputfile,"\nstacktop after pull: %d",
a_stack->stack_top); return item;
} else fprintf(outputfile, "\nCannot pull - stack is empty!");
}
/********************************************************************/
Мы показали работу этого примера на рис. 8.12. После выполнения этой программы будет выдан следующий код:
Рис. 8.12. Запись в стек и извлечение из стека
Stack top: 0
Stack Empty!
Stack is not full.
Stack top: 0
Stack Empty!
Cannot print - stack is empty!
Before push - stack pointer: 0
Stack is not full.
stack item after push: 11
stacktop after push: 1
Before push - stack pointer: 1
Stack is not full.
stack item after push: 12
stacktop after push: 2
Before push - stack pointer: 2
Stack is not full.
stack item after push: 13
stacktop after push: 3
Before push - stack pointer: 3
Stack is not full.
stack item after push: 14
stacktop after push: 4
Stack top: 4
Stack is not empty.
Stack item: 0
Stack item: 14
Stack item: 13
Stack item: 12
Stack item: 11
Before pull - stack pointer: 4
Stack top: 4
Stack is not empty
stack item pulled: 14
stacktop after pull: 3
Before pull - stack pointer: 3
Stack top: 3
Stack is not empty.
stack item pulled: 13
stacktop after pull: 2
Before pull - stack pointer: 2
Stack top: 2
Stack is not empty,
stack item pulled: 12
stacktop after pull: 1
Before pull - stack pointer: 1
Stack top: 1
Stack is not empty.
stack item pulled: 11
stacktop after pull: 0
Before pull - stack pointer: 0
Stack top: 0
Stack Empty!
Cannot pull - stack is empty!
Несколько стеков. Обычно система микропроцессора содержит один стек. Этот стек объявляется внутри RAM, и процессор имеет несколько функций для объявления положения стека (LDS), записи данных (PUSH), извлечение данных из стека (PULL) и т.д. Кроме того, как мы уже рассказывали в главе 4, в процессор встроен целый ряд аппаратных функций, связанных стеком, таких, как сохранение данных программного счетчика и ключевых регистров. В операционной системе реального времени нам нужен будет стек для каждой задачи, в котором мы будем сохранять контекст. Следовательно, мы должны иметь несколько стеков для работы с системами ОСРВ. В этих случаях, мы используем понятия о стеке, рассмотренные в этом разделе. Мы могли бы легко объявлять дополнительные стеки, использовав приведенный выше код. Кроме того, таким же образом может работать любой из стеков, которые мы объявим.
На этом мы завершаем обзор основных конструкций, которые используются для реализации операционной системы в режиме реального времени. Мы теперь собираемся сместить акценты и обсудить дополнительные концепции ОСРВ в следующем разделе. Мы расстаемся с конструкциями и концепциями, чтобы описать, как программировать различные ОСРВ.
Ранее в этой главе мы сказали, что ОСРВ — компьютерная операционная система, которая должна своевременно обрабатывать несколько событий при ограниченных ресурсах процессора. Наше исследование ОСРВ начинается с определения понятия задачи. Это потребует радикального изменения нашего понимания программ (сдвига парадигмы). При наличии в системе только одного последовательного процессора, мы можем рассматривать программу как последовательность шагов, которые процессор выполняет один за другим по определенному алгоритму. В ОСРВ, наша программа состоит из независимых, асинхронных (могущих появиться в любое время) взаимодействующих задач. И все они будут конкурировать за драгоценное (и ограниченное) время обработки. Наша программа состоит из механизмов, позволяющих следить за состоянием каждой задачи, планировать задачи для выполнения, и удостовериться, что каждая задача получает необходимую долю процессорного времени.
Мы начнем этот раздел, с получения хорошего описания того, что мы понимаем под задачей и как мы представляем ее в программе. Затем мы исследуем, как следить за состоянием каждой задачи и модифицировать его, используя блок управления задачами (task control block — TCB). Мы исследуем также, как отслеживается состояние другой системой информации, с помощью управляющих блоков устройства. Мы увидим, как диспетчер следит за состоянием всех задач и определяет, какая из задач является очередной. В заключение, мы также исследуем различные алгоритмы планирования, которые могут использоваться в ОСРВ.
Задача — это независимое, асинхронное действие, которое выполняется системой ОСРВ. Поскольку задачи асинхронны, мы не можем точно предугадать момент, когда они будут выполняться программой. Каждая задача может рассматриваться как маленькая, независимая программа, которая выполняет специфическое действие. Так как мы имеем несколько задач, конкурирующих за использование одного и того же процессора, задача должна иметь возможность сохранить контекст (ключевые значения регистров, счетчик программы, и т.д.). Эта информация резервируется на интервале выполнения другой задачи. Следовательно, каждая задача должна иметь свой стек для сохранения контекста. Даже если выполнение задачи прервано другой задачей, в конечном счете, его планируется завершить позднее.
В нескольких следующих разделах мы исследуем возможные состояния задач и способы, с помощью которых вся информация о задачах обрабатывается блоком управления задачами. До перехода к этому материалу рассмотрим предварительно задачи, связанные со знакомым уже нам роботом, проходящим через лабиринт.
Пример: В главе 7 был рассмотрен проект автономного робота, проходящего через неизвестный лабиринт. Этот робот, обнаруживая границы лабиринта с помощью инфракрасных локаторов, принимал решения, двигаться ли вперед или повернуть в необходимом направлении, чтобы пройти через лабиринт. При проходе через лабиринт робот должен был избежать земляных мин (магнитов в полу лабиринта). Как мы говорили, робот должен находить мины с помощью датчика Холла. Если робот обнаруживал магнит, он должен был остановиться, отъехать назад, и объехать мину. Робот был также оборудован ЖК дисплея (ЖКД), показывающим его текущее состояние в процессе выполнения программы.
Создадим список функций, которые должна выполнять операционная система робота, чтобы успешно выполнять все перечисленные задачи:
• Функции инициализации ЖКД, ATD-преобразователя и системы широтно-импульсной модуляции (ШИМ);
• ATD-преобразование выходных сигналов ИК датчиков;
• Сравнение выходных сигналов ИК датчика с пороговыми уровнями обнаружения стенки;
• Алгоритм поворотов робота, позволяющий правильно изменить движение робота в ответ на выходные сигналы ИК датчиков;
• Функции, позволяющие осуществить поворот робота направо, налево и продолжить движение вперед;
• Метод обработки выходного сигнала датчика Холла;
• Функции, необходимые для выполнения объезда мины — остановка, задний ход, и объезд;
• Функции, обеспечивающие работу ЖКД.
Эти функции показаны в структуре программы (рис.8.13).
Рис. 8.13. Структура программы, управляющей роботом, проходящим лабиринт
При реализации ОСРВ робота эти функции становятся задачами. Как мы уже сказали, задачами называются независимые, асинхронные и взаимодействующие процессы, соревнующиеся за предоставление процессорного времени. Исследуем сценарий работы системы управления роботом, чтобы иллюстрировать это.
Сценарий. Робот помещается в начальную точку неизвестного лабиринта, содержащего магнитные мины. Операционная система инициализирует ЖКД, ATD-конвертер и систему ШИМ. В главе 4, мы обсуждали требования инициализации для каждой из этих систем робота.
Как только инициализация закончена, робот начинает проход через неизвестный лабиринт, обрабатывая сигналы ИК датчиков и датчика Холла. Что получается, если робот получит сигналы о приближении к стенке одновременно с сигналом об обнаружении мины? Робот не сможет обрабатывать оба эти два события одновременно, поскольку располагает только одним процессором. Оба события являются критическими, хотя и не в равной степени. Если мы обрабатываем сначала информацию о стенках, робот избегает столкновения, но рискует подорваться на мине, если обработка информации о стенках не закончится достаточно быстро. С другой стороны, если мы сначала обрабатываем информацию о минах, считая эту задачу более приоритетной, мы подвергаемся риску возможного столкновения со стенками. К тому же оба события взаимосвязаны. Мы не хотим подорваться на мине, обрабатывая информацию о стенках, но и не хотим наткнуться на стенки объезжая мины.
В следующих разделах мы обсудим, как решить рассматриваемые проблемы, используя ОСРВ и, прежде всего, познакомимся с концепциями управления задачами.
В этом разделе мы рассмотрим, как ОСРВ взаимодействует с отдельной задачей. Сначала мы рассмотрим различные состояния, в которых может находиться задача. Затем исследуем, как задачи переходят из одного состояния в другое, и рассмотрим программную функцию, позволяющую моделировать состояния задач и их связь друг с другом. Это обсуждение будет частью полного обсуждения блока управления задачами.
Состояния задачи. В любой момент времени задача может быть только в одном определенном состоянии. Различные состояния задачи показаны на рис. 8.14. ОСРВ должна отслеживать и изменять состояние каждой задачи. Не забудем при этом, что все задачи независимы, асинхронны и претендуют на процессорное время. ОСРВ должна обеспечить эффективное планирование этого дефицитного ресурса.
Рис. 8.14. Состояния задач
Задача находится в одном из следующих состояний:
• Бездействия (Dormant — D): задача не нуждается в обработке и не требует процессорного времени. Она рассматривается как удаленная или неактивная задача, и по сигналу операционной системы переходит в состояние готовности.
• Готовности (Ready — R): задача полностью готова к переходу в активное состояние; однако, в настоящее время процессор занят другой задачей. Задача может переходить в состояние готовности из состояний бездействия или активности. Она переходит от активного состояния в состояние готовности, когда процессор обрабатывает другую более приоритетную задачу. Зарезервированная задача с более низким приоритетом (находящаяся в состоянии готовности) повторно переходит в активное состояние, как только становится доступным процессорное время и операционной системой дается разрешение на выполнение.
• Активности (Executing/active/running — A): задача управляется процессором, выполняя часть своей программы. Так как наша система содержит только один процессор, только одна задача может быть в активном состоянии в любое данное время. Задача остается в активном состоянии, пока не происходит одно из трех событий:
1) завершаются необходимые для выполнения задачи действия;
2) она выгружается задачей с более высоким приоритетом;
3) она возвращает управление операционной системе.
Во всех этих случаях задача переходит от активного состояния в состояние готовности. Эти варианты станут более ясными, когда мы обсудим различные типы систем ОСРВ в разделе 8.5. Из активного состояния задача может также переходить в состояния ожидания.
• Ожидание (Wait — W): выполнение задачи было отсрочено. Она остается в ждущем состоянии на заданном отрезке времени и затем переходит в состояние готовности, ожидая обработки. Задача переводится в состояние ожидания временно, чтобы выделить время для обработки задач с более низким приоритетом.
• Приостановка (suspended — S): задача ждет некоторого ресурса. Как только ресурс становится доступным, задача переходит в состояние готовности и ждет процессорного времени.
• Восстановление (rescheduling — X): Это состояние вводится каждый раз, когда задача выполнена, но не может сразу же перейти в состояние готовности. В этом случае, задача остается в состоянии восстановления, пока не закончится необходимый интервал восстановления (RSI). Как только это происходит, задача снова переходит в состояние готовности.
Приведенная диаграмма состояний задач может быть выполнена с помощью программного кода, использующего команду
switch
, которая позволяет нам точно управлять переходами в зависимости от состояния.
/********************************************************************/
char task_state_diagram(char present_state, char action) {
char next_state;
switch (present_state) {
case 'D': /*если состояние бездействия (D) и выбрана опция create(c), то */
/* задача переходит в состояние готовности, в противном */
/* случае она остается в состоянии бездействия (D) */
if (action == 'с') next_state = 'R';
else next_state = 'D';
break;
case 'R': /*если задача в состоянии готовности (R), процессор доступен */
/* и задача имеет наивысший приоритет,то она переходит в */
/* активное состояние (A). Это состояние обозначается */
/*присвоением переменной action значения (g). */
if (action == 'g') next_state = 'A';
else next_state = 'R';
break;
case 'A': /*из состояния активности(A)задача переходит в состояние */
/*определяемое значением переменной action. */
if (action == 'p') /*время ожидания или приостановка задачи*/
next_state = 'R'; /*возврат в состояние готовности*/
else if (action == 'w')
/*переход в состояние ожидания*/
next_state = 'W'; /*состояние ожидания */
else if (action == 'n') /*ресурс недоступен */
next_state = 'S';
/*задача переходит в состояние приостановки*/
else if (action == 'd')
/*завершается выполнение задачи*/
next_state = "X';
/*если требуется время для */
/* восстановления */
else if (action == 'x') /*задача исключается */
next_state = 'D'; /*возврат в состояние бездействия */
else next_state = 'A'; /*остается в состоянии активности */
break;
case 'X': /*из состояния восстановления (X) переход в состояние */
/*готовности (R)по сигналу таймера восстановления (t). */
if (action=='t') /*ожидается сигнал таймера восстановления*/
next_state='R'; /*задача переходит в состояние готовности*/
else next_state='X';
break;
case 'W': /*из состояния ожидания (W) переход в состояние готовности (R)*/
/*по сигналу таймера ожидания (e). */
if (action == 'e') /*ожидание сигнала таймера*/
next_state = 'R'; /*возврат в состояние готовности*/
else next_state = 'W';
break;
case 'S': /*из состояния приостановки (S) переход в состояние готовности (R)*/
/*при появлении ожидаемого ресурса (a) */
if (action == 'a') /*необходимый ресурс доступен*/
next_state = 'R'; /*переход в состояние готовности*/
else next_state = 'S';
break;
return next_state;
}
}
/********************************************************************/
Блок управления задачами. Как мы уже упоминали, задачи представляют собой независимые, асинхронные и взаимодействующие процессы, для взаимосвязанного выполнения которых необходим один и тот же процессор. Давайте вспомним о нашем бесстрашном официанте из предшествующего раздела. Официант (процессор) должен удовлетворять запросы нескольких заказчиков (задач), используя при этом ограниченные ресурсы. Так как официант (процессор) переключается с обслуживания одного столика заказчиков (от одной задачи) к другому, ему, или ей необходимо помнить состояние (контекст) каждого из столиков (задач), чтобы обеспечить качественное обслуживание.
В этом разделе мы рассмотрим, как ОСРВ «запоминает» и отслеживает состояние каждой задачи, чтобы позволить каждой задаче выполнить свой процесс. Вы, наверное, думаете: «А в чем сложности? Позвольте процессу, который стал активным завершиться». Но как мы иллюстрировали в примере с роботом, это невозможно. Так, в системе с большим количеством конкурирующих задач, некоторые задачи низким приоритетом никогда не выполнялись бы процессором. Вообразите, что случилось бы, если наш официант (процессор) посвятил все свое время одному столику (процесс) до полного его обслуживания, не обращая внимания на все остальные столики (задачи).
Мы не знаем точно, как хороший официант следит за многими столиками одновременно. А вот ОСРВ для отслеживания состояния каждой задачи обычно использует блок управления задачами (TCB). Каждая задача в ОСРВ имеет собственную связь с TCB, которая обеспечивает текущую информацию о задаче. Эта информация используется и модифицируется ядром ОСРВ, чтобы эффективно отслеживать, планировать и выполнять весь набор задач данной системы. Мы должны подчеркнуть, что TCB изменяется только операционной системой. Задача не имеет прямого контакта с TCB, хотя этот блок содержит наиболее обновленную информацию о задаче.
Структура TCB показана на рис. 8.15. Когда ОСРВ переключается с одной задачи на другую, вся ключевая информация о текущей задаче должна быть надежно сохранена перед переключением к следующей задаче. Таким образом, когда задача позже получает процессорное время, она может продолжить процесс с того места, где он был остановлен, при выгрузке. Как мы говорили ранее, эта ключевая информация задачи называется контекстом.
Рис. 8.15. Блок управления задачами
Прервемся на минуту, и подумаем, какая информация была бы важна для задачи. Рассмотрим также, как мы могли бы выполнить программное обеспечение TCB. Как минимум TCB должен содержать имя задачи, текущее состояние, приоритет, текущий контекст (ключевые значения регистров), и адрес, по которому можно найти этот контекст для обработки задачи.
Сначала, попробуем справится с разработкой программы для TCB. Данные, отражающие отдельные свойства каждой задачи имеют различные типы. Ранее в этой главе мы обсуждали запись или структуру — абстрактный тип данных, хорошо подходящий для программной реализации TCB. Как было упомянуто, структура представляет собой определяемую пользователем совокупность различных, но связанных между собой типов данных. Мы можем объединить эти различные типы данных в структуру и использовать ее для создания программы TCB. В главе мы уже разработали структуру для автомобиля и следили за самой разнообразной информацией о конкретных автомобилях, заключенной в полях структуры. Мы сделаем теперь то же самое для TCB. (На самом деле, мы попросим, чтобы это сделали вы в качестве задания 4 для самостоятельной работы). Используйте автомобильную структуру в качестве примера, и разработайте подобную структуру для TCB.
Рассмотрим подробнее отдельные поля для TCB и определим типы данных для каждого поля.
• Имя задачи состоит из символьного массива. Для примера робота мы ограничим имя задачи 20 символами. Это позволит нам сохранить полное имя для каждой задачи, не пользуясь маловразумительными сокращениями. Мы позволяем себе такую расточительность в использовании памяти ради удобочитаемости.
• Текущее состояние задачи: как следует из предыдущего раздела, задача в каждый момент времени может находиться в одном из шести состояний бездействия (D), готовности (R), активности (A), ожидания (W), приостановки (S) и восстановления (X). Мы обозначили каждое состояние задачи одним символом. Мы можем, следовательно, сохранять текущее состояние задачи в символьной переменной внутри нашей структуры TCB.
• Приоритет задачи: Для относительного ранжирования задач внутри системы, разработчиком системы назначается приоритет задачи. В нашем примере с роботом, мы использует фиксированные приоритетные значения. Но при необходимости можно выполнить и ОСРВ системы, в которых приоритет задачи может изменяться в процессе выполнения программы. Приоритет задачи представляется натуральными числами. В следующем примере мы покажем, как назначать приоритет задачи.
• Контекст задачи: Содержимое всех ключевых регистров связанных с задачей, составляет контекст задачи. Система прерывания, встроенная в микропроцессор 68HC12 использует стек, чтобы сохранить весь контекст, когда прерывание происходит. Мы также используем стек, чтобы сохранить контекст задачи. Каждая задача имеет собственный стек, следовательно, ОСРВ имеет целый ряд одновременно существующих стеков. Как программист системы, вы должны гарантировать, что эти стеки не будут смешиваться друг с другом в пространстве памяти. Для простоты, мы используем фиксированную структуру стека, разработанную ранее в этой главе для контекстной памяти TCB. Так как все ключевые регистры и ячейки памяти в микроконтроллере 68HC12 имеют объем в 8 или 16 бит, мы используем фиксированный массив целых чисел для стеков TCB.
• Состояние активности задачи: состояние активности задачи представляет собой следующий шаг программы, который должен выполняться, как только задача станет активной. После инициализации, задача начинается с начала соответствующего ей программного кода. Однако до своего полного завершения задача может неоднократно выгружаться задачами с более высоким приоритетом. Следовательно, TCB должен помнить следующий шаг, с которого должно продолжиться выполнение задачи.
• Указатель задачи: Так как мы будем связывать эти структуры TCB с помощью указателей, нам необходим указатель на следующий в списке TCB.
Вы уже, наверное, усвоили понятиям задачи и блока управления задачей. Однако мы еще не касались ряда сложных проблем. Вам, вероятно, не очень ясна идея выхода из программы до ее завершения, даже если она обладает наивысшим приоритетом. Мы исследуем эту тему в следующем разделе. Вы можете воображать осложнения, которые получатся, если мы начнем ATD-преобразование, а оно выгрузится событием с более высоким приоритетным прежде, чем будет закончено. И представьте себе, какая путаница возникнет, если это событие с более высоким приоритетом также должно будет использовать ATD-преобразование. Мы видим, что разработчик операционной системы должен определить, когда можно безопасно прервать управление задачи. Перед исследованием этих проблем, возвратимся к нашему примеру робота и посмотрим, как назначаются приоритеты задач.
Сценарий: В нашем примере с роботом у нас было много задач по обеспечению его работы. Теперь мы должны назначить численное значение приоритета для каждой задачи. Мы будем использовать более низкое численное значение для задач с более высоким приоритетом. Например, задаче с самым высоким приоритетом сопоставим значение 1. Мы используем наше понимание сценария работы робота, чтобы назначить приоритеты задач. Так как наш робот дезактивируется, когда приближается к мине, мы присваиваем задаче обнаружения мин приоритет 1. Следующий самый высокий приоритет, равный 2 присвоим операции объезда мины. Задаче ATD-преобразования присвоим приоритет 3, так как она обеспечивает информацию о близости стенок лабиринта. Задаче выбора поворота присвоим следующий приоритет 4, так как она обрабатывает информацию, необходимую, чтобы избежать столкновения со стенками. Наконец, задаче модификации ЖКД назначаем приоритет 5. Это самый низкий приоритет для задач, рассмотренных к настоящему времени; однако, приоритет всех прочих задач еще ниже. Остающиеся задачи имеют еще более низкий приоритет. Они активны на начальных этапах работы нашей операционной системы, а затем входят в состояние бездействия. Мы, следовательно, назначаем им самый низкий приоритет, давая им приоритеты 6 (инициализация ЖКД), 7 (инициализация ATD), и 8 (инициализация ШИМ).
Фрагментация задач. Как мы уже выяснили в этом разделе, желательно разделить программный код задачи на непрерываемые части или фазы, определив удобные точки выхода. Это важно, поскольку в ОСРВ с несколькими задачами с одинаковым приоритетом, задача редко будет способна завершить связанные с ней действия от начала до конца. Чаще всего задача завершит некоторую часть действий и затем будет приостановлена задачей или задачами с более высоким приоритетом. Факт перехода задачи с более высоким приоритетом в состояние готовности еще не означает, что процессорное время немедленно передается ей. Мы должны организовать переход от одной задачи к другой, прервав выполнение кода в заранее определенном удобном для этого месте. Поскольку мы записываем контекст прерываемой задачи, мы должны сохранить его в памяти. Например, мы можем подразделять связанную с задачей функцию на три части. Первый раз, когда задача становится активной, процессор выполняет первую треть кода до удобной отметки прерывания (определенной вами, как программистом). Когда код достигает этой отметки прерывания, контекст сохраняется в TCB, и процессор прерывает управление задачей. Затем выполняется задача с более высоким приоритетом. Когда прерванная задача снова получает для своего выполнения драгоценное процессорное время, она начинается с того места, где была прервана и продолжает обработку второй части своего кода. Этот процесс продолжается, пока задача не завершает все предусмотренные действия.
Проиллюстрируем такую работу примером.
Пример: Предположим, что робот, имеющий пять ИК локаторов (рис. 8.16) выполняет функцию названную
process_turn
, которая инициализирует систему ATD контроллера 68HC12, начиная последовательность преобразований, необходимую, чтобы записать аналоговые сигналы от пяти датчиков (с номерами от 0 до 4), которые связаны с каналами ATD от 7 до 3, соответственно. Выход датчика Холла, установленного в нижней части робота, чтобы обнаруживать магнитные мины, связан с каналом 2 ATD. Обратите внимание: этот пример придуман, чтобы показать, как следует подразделять код, чтобы обеспечить удобные точки прерывания.
Рис. 8.16. Робот c пятью ИК локаторами и датчиком Холла. ИК-датчик обнаруживает присутствие стенок лабиринта, в то время как датчик Холла обнаруживает присутствие магнитных мин.
Код
process_turn
, обеспечивающий процесс поворота, приведен ниже.
void process_turn() {
/*Инициализация системы ATD */
ATDCTL2 = 0x80; /*установка флага ADPU, чтобы подать питание на систему ATD*/
ATDCTL3 = 0x00; /*игнорировать «замораживание» системы */
ATDCTL4 = 0x7F; /*Снижение частоты таймера P до 125 кГц */
/*выборка, время преобразования = 32 ATD цикла */
/* 1 выборка за каждые 256 мкс */
for (i=0; i<67; i++) { /* ожидание 100 мкс при 8 МГц ECLK*/
;
}
/*Инициализация ATD-преобразования */
ATDCTL5 = 0x50; /*Начать многоканальное ATD-преобразование */
/* для 8 каналов */
while((ATDSTAT & 0x8000) == 0) { /* проверить окончание преобразования по*/
/*состоянию флага SCF */
;
}
/* сохранить результаты ATD-преобразования*/
/* в глобальном массиве char*/
sens[0] = ADR7H; /*крайний левый датчик */
sens[1] = ADR6H; /*средний левый датчик */
sens[2] = ADR5H; /*центральный датчик */
sens[3] = ADR4H; /*средний правый датчик */
sens[4] = ADR3H; /*крайний правый датчик */
sens[5] = ADR2H; /*Датчик Холла*/
/*анализ информации датчиков для решения о повороте. Примечание: пороги для*/
/*датчика Холла(hes_threshold) и для ИК-датчиков (opto_threshold)являются*/
/* глобальными переменными и определены экспериментально*/
if (sens[5] < hes_threshold) { /*сигнал с датчика Холла, объезд*/
pwm_motors(back_up); /* робот дает задний ход*/
/*действия, следующие после того */
/* как робот отъехал назад */
if(sens[0] > opto_threshold) pwm_motors(right_turn);
else pwm_motors(left_turn);
for(i=0; i<0xFFFF; i++) { /*задержка перед вращением двигателя */
for(j=0; j<15; j++){
;
}
}
}
/*если обнаружен тупик - задний ход*/
else if((sens[2]>opto_threshold) && (sens[0]>opto_threshold) && (sens[4]>opto_threshold)) {
pwm_motors(back_up);
}
/*если стенки спереди и слева, */
/*поворот робота направо */
else if((sens[0]>opto_threshold) && (sens[2]>opto_threshold)) {
pwm_motors(right_turn);
}
/*если стенки спереди и справа, */
/*поворот робота налево */
else if((sens[2]>opto_threshold) && (sens[4]>opto_threshold)) {
pwm_motors(left_turn);
}
/*если стенка перед средним правым */
/* датчиком, то полуповорот направо */
else if (sens[1] > opto_threshold) {
pwm_motors(half_right);
}
/*если стенка перед средним левым */
/* датчиком, то полуповорот налево */
else if (sens[3]>opto_threshold) {
pwm_motors(half_left);
}
/*если сигналов от датчиков нет, продолжить движение вперед*/
else {
pwm_motors(forward);
}
}
Если мы хотим подразделить этот код на три части обрабатываемые ОСРВ без прерывания, мы можем вставить точки прерывания после последовательности инициализации ATD и после последовательности записи данных с ATD. Это позволит функции без проблем прерывать и восстанавливать управление процессором. Чтобы выполнять эти изменения, мы должны ввести переменную, которую мы назовем code_section. Эта переменная позволит нам проследить, какая из трех частей кода должна быть выполнена при очередной активности задачи.
int process_turn(int code_section) {
switch(code_section) {
case 0:
/*Инициализация системы ATD */
ATDCTL2 = 0x80; /*включение ATD */
ATDCTL3 = 0x00; /*игнорировать доступ при отладке системы */
ATDCTL4 = 0x7F; /*Снижение частоты таймера P до 125 кГц */
/*выборка, время преобразования = 32 ATD цикла */
/* 1 выборка за каждые 256 мкс */
for (i=0; i<67; i++) {
/* ожидание 100 мкс при 8 МГц ECLK*/
;
}
code_section = 1; /*update code_section variable */
break;
case 1:
/*Инициализация ATD-преобразования */
ATDCTL5 = 0x50; /*Начать многоканальное ATD-преобразование*/
/* для 8 каналов */
while ((ATDSTAT & 0x8000) == 0) {
/* проверить окончание преобразования по*/
/*состоянию флага SCF */
;
}
/* сохранить результаты ATD-преобразования*/
/* в глобальном массиве char */
sens[0] = ADR7H; /*крайний левый датчик */
sens[1] = ADR6H; /*средний левый датчик */
sens[2] = ADR5H; /*центральный датчик */
sens[3] = ADR4H; /*средний правый датчик */
sens[4] = ADR3H; /*крайний правый датчик */
sens[5] = ADR2H; /*Датчик Холла */
code_section = 2; /*update code_section variable */
break;
case 2:
/*анализ информации датчиков для решения о повороте. Примечание: пороги для*/
/*датчика Холла(hes_threshold) и для ИК-датчиков (opto_threshold)являются*/
/* глобальными переменными и определены экспериментально*/
if (sens[5] < hes_threshold) { /*сигнал с датчика Холла, объезд*/
pwm_motors(back_up); /* робот дает задний ход*/
/*действия, следующие после того */
/* как робот отъехал назад */
if (sens[0] > opto_threshold) pwm_motors(right_turn);
else pwm_motors(left_turn);
for (i=0; i<0xFFFF; i++) { /*задержка перед вращением двигателя */
for(j=0; j<15; j++) {
;
}
}
}
/*если обнаружен тупик - задний ход*/
else if ((sens[2]>opto_threshold) && (sens[0]>opto_threshold) && (sens[4]>opto_threshold)) {
pwm_motors(back_up);
}
/*если стенки спереди и слева, */
/*поворот робота направо */
else if((sens[0]>opto_threshold) && (sens[2]>opto_threshold)) {
pwm_motors(right_turn);
}
/*если стенки спереди и справа, */
/*поворот робота налево */
else if((sens[2]>opto_threshold) && (sens[4]>opto_threshold)) {
pwm_motors(left_turn);
}
/*если стенка перед средним правым */
/* датчиком, то полуповорот направо */
else if (sens[1] > opto_threshold) {
pwm_motors(half_right);
}
/*если стенка перед средним левым */
/* датчиком, то полуповорот налево */
else if (sens[3]>opto_threshold) {
pwm_motors(half_left);
}
/*если сигналов от датчиков нет */
/*продолжить движение вперед */
else {
pwm_motors(forward);
}
code_section = 0; /* изменить переменную code_section */
break;
}/*конец switch*/
return code_section;
}
Когда задача, связанная с функцией
process_turn
, переходит из состояния готовности в активное состояние, ОСРВ вызывает функцию с параметром 0. Функция process_turn
затем выполняется до первой отметки прерывания в коде. Достигнув этой отметки, функция возвращает управление ОСРВ, которая модифицирует TCB, связанный с процессом и продолжает выполнение второй части кода, когда задача в очередной раз переходит в активное состояние. Затем задача снова возвращается в состояние готовности и ждет, когда ОСРВ выделит ей процессорное время. Повторим снова, что причина, по которой мы делим код на логические части, состоит в том, чтобы позволить задаче работать до завершения определенной части и затем позволить другой задаче выполнить часть связанного с ней кода, и т.д. Это дает возможность выполнять несколько появившихся задач практически одновременно, хотя в любой момент времени процессор выполняет только одну задачу.
В этом примере мы использовали глобальные переменные, чтобы сохранять информацию между последовательными обращениями к функции
process_turn
. Использование глобальных переменных может быть не самое лучшее использование дефицитных ресурсов памяти RAM. Далее в этой главе мы рассмотрим альтернативные методы движения информации между задачами.
Как можно предположить из этого примера, ядро ОСРВ должно быть довольно сложным, чтобы позволить следить за множеством задач, эффективно планировать использование процессорного времени и полностью выполнять функции, необходимые для конкретного применения. В следующем разделе мы исследуем составляющие части ядра ОСРВ и их взаимодействие друг с другом, позволяющие удовлетворить требования применения.
Чтобы выполнить многозадачную операционную систему, разработчик системы (которым являетесь вы) должен рассматривать управление прикладным устройством как группу независимых, но взаимодействующих задач. Каждая задача выполняет ряд связанных с ней действий. Цель многозадачной системы состоит в том, чтобы выполнить требования к прикладному устройству, изменяя задачи, планируя и реализуя их осуществление и решая конфликты, возникающие между различными задачами, из-за использования только одного процессора. Как вы могли уже себе представить, многозадачная система может быть очень сложной. Однако мы разбиваем систему на составные части и исследуем каждую из них отдельно.
Операционная система должна следить за состоянием каждой из управляемых ей задач. Обратимся снова к примеру с автомобильными дилерами, в котором мы использовали списки с указателями, чтобы учесть текущее состояние каждого автомобиля: включение его в партию, предназначенную для продажи, продажу, ремонт, перекраску и т.д.; мы удаляли его из одного списка и добавляли в другой при изменении состояния. ОСРВ также использует подобные методы, чтобы проследить за состоянием каждой задачи.
Обычно ОСРВ позволяет частично выполнять задачу на некотором ограниченном временном промежутке. Когда время, выделенное для задачи истекает, ОСРВ сохраняет текущий контекст задачи в связанном с ней блоке управления TCB. Затем ОСРВ решает, какой из задач предоставить очередную часть процессорного времени. Для принятия этого решения может использоваться ряд алгоритмов планирования. Но какой бы алгоритм планирования не был выбран, он должен удовлетворять основному требованию: каждой задаче должна быть выделена оптимальная доля процессорного времени.
Прежде, чем задача сможет перейти в состояние готовности, ОСРВ должна гарантировать, что ей будут доступны все необходимые ресурсы. Под ресурсами понимаются специфические данные, аппаратные подсистемы и т. д. Например, если задача должна использовать одну из подсистем микроконтроллера 68HC12, ОСРВ должна гарантировать, что этот ресурс доступен для задачи, переходящей в состояние готовности. Задача же, которая ожидает необходимого ресурса, должна быть переведена в состояние ожидания.
Процессор должен также иметь возможность приостановить задачу на некоторое время, чтобы предоставить процессорное время задачам с более низким приоритетом. Если мы не сделаем этого, выполняя все время задачи с самым высоким приоритетом, то ряд задач просто никогда не получит процессорного времени. Чтобы предотвратить эту ситуацию, активные задачи с высоким приоритетом должны временно переводиться в ждущее состояние, чтобы позволить частично выполнить низкоприоритетные задачи, находящиеся в состоянии готовности. Операционная система должна также иметь механизмы, позволяющие выполнять критические задачи с наивысшим приоритетом по мере их появления. Это подразумевает использование прерываний в обработке ОСРВ.
Для выполнения этих действий ядро ОСРВ использует ряд инструментов, включая системные таблицы, отслеживающие состояние задач и диспетчеры/планировщики, исследующие данные системы, чтобы определить, какая из задач должна выполняться в любой момент. ОСРВ также обеспечивает возможность осуществления межзадачных связей, которую мы рассмотрим далее в этой главе.
Системные таблицы. Операционная система используют ряд таблиц/блоков, чтобы проследить состояние задач, устройств ввода–вывода и услуг системы. Мы уже обсудили в общих чертах блок управления задачами TCB в разделе 8.4. Кроме TCB, операционная система также поддерживает управляющий блок устройства (DCB), чтобы отслеживать состояние связанных c системой устройств. Это позволяет операционной системе гарантировать, что все требуемые задачей ресурсы доступны для использования, до того как будет дано разрешение на переход задачи в состояние готовности. В зависимости от числа устройств и ресурсов в системе, DCB может быть введен с помощью простого двумерного массива, который может динамически изменяться при изменении состояния устройства.
Пример: Когда мы наблюдали по телевидению феноменальную игру в гольф Лесного Тигра (Matt Christopher, прим. переводчика), то были заинтригованы тем, как же обслуживающий персонал поля для гольфа способен проследить за состоянием большого числа игроков в гольф (задач) на турнире, чтобы предоставлять им лунки (ресурсы). В процессе передачи, мы увидели большое табло, на котором отслеживалось состояние игроков и лунок. Обслуживающий персонал поля для гольфа мог сообщить состояние данного игрока или лунки. Табло состояния постоянно изменялось в течение турнира. ОСРВ использует тот те же методы (TCB и DCB) чтобы проследить состояние задач, ресурсов и услуг в процессе выполнения программы.
Диспетчер/планировщик. Диспетчер/планировщик — другая ключевая часть ядра ОСРВ. Первичная его функция состоит в том, чтобы определить, какая из задач является очередной. Планировщик может использовать ряд алгоритмов, чтобы принять это решение. В следующем разделе обсуждаются различные алгоритмы и свойственные им недостатки и преимущества.
Обычно, операционные системы реального времени характеризуются типом алгоритма, используемого звеном диспетчера/планировщика, имеющимся в ядре ОСРВ. Уже название каждого типа ОСРВ дает достаточно хорошее описание работы его алгоритма. Проведем сравнительный обзор различных типов ОСРВ. Мы должны подчеркнуть, что ни один из рассматриваемых алгоритмов планирования ОСРВ не лучше любого другого. Выявление алгоритма, наиболее подходящего для конкретного применения является задачей программиста, создающего систему ОСРВ.
Система с циклическим опросом является наиболее простым ядром планирования ОСРВ, как для разработки, так и для исполнения. Как подразумевается самим названием, оно использует механизм последовательного опроса, чтобы определить, требует ли данная задача процессорного времени. Когда задача его требует, оно предоставляется ей от начала до конца выполнения. Как только задача заканчивается, операционная система продолжает опрос других задач, требующих процессорного времени.
Системы с циклическим опросом используются при управлении многозадачными системами с равным приоритетом для всех задач. Она должна также использоваться для систем с задачами, которые вряд ли потребуют процессорного времени одновременно. Так как одиночная задача выполняется до завершения, межзадачная связь в таких операционных системах обычно не требуется.
Основное преимущество системы с циклическим опросом — простота. С этим типом системы просто определить время срабатывания специфической задачи, систему просто написать и отладить. Основной недостаток такой системы — также простота. Система с циклическим опросом не может обрабатывать пакет событий — то есть несколько задач, требующих процессорного времени одновременно.
Следует подчеркнуть, что система с циклическим опросом — не лучший выбор сценария работы операционной системы реального времени. Она имеет существенные ограничения, однако идеально подходит для некоторых приложений, в чем мы убедимся на следующем примере.
Пример: В главе 2 мы обсуждали встроенную систему управления, предназначенную для обработки входных сигналов дистанционного управления или группового ввода для стерео усилителей. В этом специфическом примере, операционная система инициализировала систему усилителя и затем непрерывно опрашивала входные сигналы от пульта дистанционного управления и переключателей на лицевой панели. Интерфейс дистанционного управления связан с портом А, а переключатели на лицевой панели — портом B. Для этого применения, опрос был наилучшим метод для реализации операционной системы. Он был выбран в качестве предпочтительного, потому что 1) как только усилитель инициализирован, операционная система, выполняет только опрос сигналов на входах, 2) входы от дистанционного управления и переключателей лицевой панели имеют равный приоритет и 3) время реакции на изменение входных сигналов не критично.
Что же будет, если наше прикладная программа в основном хорошо соответствует системе с опросом, но содержит несколько задач, которые требуют непосредственного доступа к процессору? Необходимо ли отказываться от простой системы с опросом в пользу намного более сложного алгоритма планирования ОСРВ? Ответ будет отрицательным. Как мы указывали в главе 4, микроконтроллер 68HC12 имеет мощную, гибкую систему прерывания с приоритетным управлением. Чтобы обрабатывать простые текущие задачи может применяться алгоритм опроса, в то же время для внеочередной обработки задач требующих немедленного вмешательства может использоваться система прерываний.
Эти типы алгоритмов планирования известны также как системы с передним планом и фоном. Алгоритм опроса рассматривается как фоновая часть операционной системы, в то время как прерывание отвечает за приоритетную часть системы. Обычно, эти системы и разрабатываются по частям. Сначала разрабатывается, выполняется и отлаживается фоновая часть, а затем к ней добавляется часть приоритетного прерывания.
Пример: Обратимся опять к нашему контроллеру стереоусилителя. Транзисторные усилители, используемые в нашем стереопроекте достаточно дороги. В хорошем проекте транзисторы не перегреваются при обычной работе усилителя, чтобы предотвратить их повреждение. Предположим, что для защиты от перегрева проектировщик предусмотрел датчики температуры для каждого транзистора, чтобы постоянно контролировать их текущую температуру. Если температура любого транзистора достигает недопустимого уровня, проектировщик хотел бы, чтобы операционная система включала дополнительные вентиляторы охлаждения. Эта модификация проекта не требует, чтобы мы вообще отказались от схемы опроса. Лучше всего использовать схему опроса в фоновом режиме, чтобы обработать обычное рабочее управление стереоусилителем. В приоритетной части системы, датчики, контролирующие температуру транзисторов, могли бы быть подключены с помощью интерфейса к системе прерывания. Прерывание было бы активизировано при достижении температурой транзистора(ов) неприемлемо высокого уровня. В этом случае процессор должен был бы формировать сигнал, на включение дополнительного вентилятора. Мы исследуем этот проект в применениях, рассмотренных в разделе 8.9 и задании для самостоятельного исследования 2, приведенного в конце главы.
В карусельных ОСРВ системах, ядро последовательно переходит от обработки одной активной задачи к другой. Когда оно достигает последней задачи в системе, то начинает снова с первой задачи. В циклической системе, задачам часто позволяют работать до завершения прежде перейти к следующей задаче. Если это недопустимо для каждого прохода через все задачи, может использоваться метод квантования времени. При квантовании времени она каждую задачу цикла выделяется фиксированным временем доступа к процессору. Если за этот период задача не заканчивается, ее контекст сохраняется в связанном с ней TCB, она переводится в состояние готовности, а активной становится следующая задача в циклической последовательности. Карусельная операционная система может быть легко выполнена при использовании структуры данных с круговой очередью. В этом случае, каждый TCB мог бы быть связан с позицией в очереди. Операционная система последовательно переходила бы от одной задачи к другой внутри очереди. А если желательно было бы использовать квантование времени, то это легко было бы реализовать с помощью прерываний в режиме реального времени (RTI) имеющихся в 68HC12.
Пример: В предыдущей профессиональной жизни я (Стивен Ф. Барретт) был наводчиком в военно-воздушных силах. Я вместе с моим партнером отвечал за текущий контроль состояния своих ракет. Мы находились на расстоянии нескольких километров от ракет. Состояние ракеты проверялось по компьютеру в нашем центре управления. Компьютер проводил последовательный циклический опрос состояния каждой из наших ракет. Я не участвовал в проектировании компьютерной операционной системы центра управления; однако, я думаю, что использовалась карусельная система опроса, поскольку состояние каждой ракеты имело равную важность.
Как мы уже видели, карусельная система опроса может также быть дополнена возможностями приоритетного прерывания. Такой тип циклической системы с прерываниями называется смешанной системой. В этом случае, циклический алгоритм формирует фоновую часть из операционной системы, в то время как приоритетная часть системы формируется программой прерываний.
Пример: В сценарии управления ракетами, мы были бы заинтересованы в том, чтобы получить немедленное сообщение о катастрофических событиях, угрожающих одной из управляемых нами ракет, таких как затопление пусковой шахты, пожар в бункере ракеты, нарушение защиты, и т.д. В этих случаях операционная система могла бы быть дополнена частью программы с прерываниями, чтобы мгновенно перейти от обычного циклического опроса, к обработке событий с высоким приоритетом, а после ее окончания вернуться к обычному карусельному опросу.
В главе 4 мы обсуждали мощную и гибкую систему прерывания микроконтроллера 68HC12 с приоритетным управлением. Эту систему прерывания мы можем использовать теперь при разработке ОСРВ. При этом сначала основная программа проводит инициализацию, позволяющую конфигурировать систему, а затем система переходит к бесконечному циклу. Внутри цикла, процессор просто ждет событий, вызывающих прерывание. Когда приходят отдельные запросы на прерывания, процессор выполняет программу обработки прерывания, связанную с каждой из задач (с каждым прерыванием). Если несколько запросов на прерывания приходят одновременно, используются механизмы приоритетов, встроенные в 68HC12, и определяющие, какая из задач должна быть выполнена сначала. Механизмы стеков в системах прерывания 68HC12 гарантирует, что контексты задач будут правильно сохраняться и восстанавливаться. Операционную систему с управлением по прерыванию такого типа относительно просто написать. К числу ее преимуществ относится малое время реакции на событие, вызывающее прерывание. Систему необходимо спроектировать таким образом, чтобы она точно оценивала приоритет каждой задачи, от которой приходит запрос на прерывание.
Пример: В этой главе мы уже обсуждали робот, движущийся в лабиринте и обнаруживающий магнитные мины. Операционная система для этого робота могла бы быть выполнена, используя методы прерывания. В этом случае задачи инициализации (для ЖКД, ATD и ШИМ) были бы реализованы при инициировании операционной системы и затем переведены в бездействующее состояние. Операционная система затем перешла бы к непрерывному циклу, ожидая запросов на прерывание от различных задач. Каждая из задач, связанных с обнаружением мин, с обнаружением стенок лабиринта и управлением соответствующими поворотами, с модификацией информации, выводимой на ЖКД, посылала бы отдельный запрос на прерывание и вызывала связанную с ним программу обработки прерывания (ISR). Если, например, робот обнаруживает мину, это событие вызовет прерывание, и затем соответствующую ISR, связанную с обходом мины. Если несколько запросов на прерывания приходят одновременно; аппаратные средства приоритета прерывания 68HC12 обеспечивают первоочередное обслуживание прерывания с самым высоким приоритетом. Если запрос на прерывание с более высоким приоритетом приходит во время обслуживания низкоприоритетного прерывания, то последнее прерывается, контроллер 68HC12 конфигурируется для обслуживания прерывания с высоким приоритетом и лишь затем возвращается к обслуживанию низкоприоритетного прерывания.
При кооперативном многозадачном режиме ОСРВ для задачи с самым высоким приоритетом из находящихся в состоянии готовности выделяется определенное количество процессорного времени. Затем она возвращает управление операционной системе в отметке, удобной для прерывания связанных с ней действий. До передачи управления, эта задача модифицирует связанный с ней TCB в соответствии с текущим контекстом. Затем задача снова переводится в состояние готовности. Операционная система затем позволяет следующей задаче с самым высоким приоритетом, находящейся в состоянии готовности перейти в активное состояние. Задача, вводящая активное состояние восстанавливает контекст, извлекая информацию из своего TCB. Эта задача затем выполняет свою часть программы, пока не достигает точки прерывания. Затем она, в свою очередь, модифицирует свой TCB в соответствии с текущим контекстом, возвращает управление операционной системе и процесс продолжается. Важно подчеркнуть, что в кооперативной многозадачной системе, задача возвращает управление операционной системе. Если задача не будет передавать управление, то задачи с более низким приоритетом никогда не смогут получить процессорного времени. Вы можете заметить, насколько важна межзадачная связь при таком типе операционной системы. Она обычно осуществляется путем использования глобальных переменных.
Операционные системы этого типа можно выполнить, используя для задач системы ряд списков с указателями. При этом списки с указателями существуют для каждого из различных состояний, в которых могут находиться задачи. Эта концепция иллюстрируется структурой, показанной на рис. 8.17. В процессе работы системы, задачи перемещаются из списка в список. Ранее в этой главе мы перемещали из одного списка с указателями в другой запись, идентифицирующую автомобиль, в ответ на его обслуживание различными автомобильными дилерами. Точно так же в зависимости от действий нашей прикладной программы перемещается из списка в список и задача, состояние которой отражается в форме записи с указателем.
Рис. 8.17. Система списков с указателями для задач
Многозадачная система с преимущественным приоритетом (PPMS) очень похожа на кооперативную многозадачную систему, главное различие между ними заключается в том, какой из компонентов системы отвечает за предоставление процессорного времени. В кооперативной многозадачной системе, задача возвращает управление процессором операционной системе. В PPMS, сам процессор решает, какая из задач станет активной и будет ли выгружаться задача с низким приоритетом. С PPMS, каждой задаче внутри системы назначен приоритет. Операционная система исследует список с указателями, содержащий все задачи, находящиеся в состоянии готовности и переводит в активное состояние задачу с самым высоким приоритетом. Когда готова задача с более высоким приоритетом, операционная система затем выгружает низкоприоритетную задачу. Последняя изменяет свой TCB и затем переходит в состояние готовности. Активной становится задача с высоким приоритетом. При этом типе ОСРВ, приоритеты задач могут быть постоянными или динамически изменяться в процессе выполнения программы.
Завершая наше обсуждение различных типов ОСРВ, мы должны подчеркнуть, что ни один из способов построения ОСРВ не лучше, чем другие. Проектировщик системы, должен выбрать наиболее подходящую для конкретной разработки конфигурацию ОСРВ в соответствии с требованиями прикладного устройства. В следующем разделе мы обсудим методы, применяемые при разработке ОСРВ.
После обсуждения теории и принципов работы ОСРВ в предыдущих разделах, вы, наверное, оценили эти системы по достоинству. Система ОСРВ решает много проблем, но создает также ряд трудностей. В этом разделе мы исследуем некоторые из проблем, с которыми вы можете столкнуться во встроенных системах ОСРВ. Мы также обсуждаем методы, с помощью которых можно избежать этих ситуаций, либо смягчить или исправить их. Мы исследуем проблемы конкуренции, повторной входимости, межзадачной связи и безопасности, и коснемся, наконец, главного вопроса: создавать ли вам собственную ОСРВ или покупать стандартную.
Я провожу много времени в поездках. Однажды я проезжал от Ларами до Форта Коллинз, где должен был подготовить конференцию. На прекрасном и живописном шоссе U.S. 287, мне встретился протяженный участок ремонтируемой дороги. Эта магистраль — асфальтированная дорога с двумя встречными полосами.
Одна полоса магистрали была закрыта на ремонт, по другой можно было проехать. Регулировщики с флажками стояли на обоих концах свободной полосы, чтобы поочередно пропускать группу автомобилей, едущих с юга, а затем — группу, едущих с севера. Регулировщики поддерживали связь друг с другом по радио. Чтобы поддерживать движение и избежать столкновений, один регулировщик должен был дать знак остановки приближающемуся потоку, в то время как другой должен был дать знак встречному потоку «осторожно продолжайте движение».
Этот рассказ иллюстрирует проблему конкуренции, которая присутствует в некоторых типах ОСРВ. В этом случае мы имеем критический ресурс (одну дорогу для встречных потоков), который одновременно требуется для использования двумя различными задачами (пропуском северного и южного потоков). Задача двух регулировщиков состоит в том, чтобы предотвратить одновременный доступ двух задач к критическому ресурсу, не допустив столкновения. В этом разделе мы исследуем, как распределяется критический ресурс в ОСРВ и как можно при этом избежать столкновений.
Как мы уже установили, ОСРВ должна позволить многим задачам, конкурирующим за один и тот же ресурс — процессорное время, завершить свои действия. Мы исследовали достаточно много алгоритмов планирования, позволяющих реализовать эту задачу. Некоторые алгоритмы разрешали межзадачные переключения, только в четко определенные моменты (карусельный и кооперативный многозадачные режимы), другие же — в частности, использующие механизмы прерывания, или приоритетного планирование — могли осуществлять межзадачное переключение без предупреждения.
Проблема конкуренции появляется, когда задача с низким приоритетом выполняет некоторую критическую часть своего кода или использует критический ресурс. Если она будет прервана, то не сможет корректно завершить свою программу, даже если впоследствии и получит необходимое процессорное время. Например, представим себе, что задача использует ATD микроконтроллера 68HC12, выполняя аналого-цифровое преобразование. Если задача начнет преобразование и будет прервана до его окончания, то произойдут ошибки. Эта ситуация может стать еще более сложной, если ставшая активной высокоприоритетная задача также должна использовать модуль ATD.
Обычно, мы называем такую часть программного кода или ресурса критической. Правильное решение проблемы конкуренции должно в первую очередь предотвратить подобные случаи. Чтобы предотвратить одновременный доступ к критическим ресурсам может использоваться ряд методов, включая следующие:
• Запрет на прерывания: Прерывания блокируются, когда задача выполняет критическую часть программного кода. В микроконтроллере 68HC12 это выполняется с помощью команды SEI (установить маскирование прерывания). Как только критическая часть кода закончена, прерывания могут быть снова разрешены командой CLI (очистить маскирование прерывания).
• Использование семафоров или блокировок; семафор или блокировка может использоваться, чтобы указать, что критический ресурс не доступен для использования, потому что это используется в настоящее время другой задачей. Регулировщики с флажками из нашего рассказа как раз и использовали семафоры (знаки «остановиться» и «продолжать осторожное движение») чтобы запрещать или разрешать доступ к критическому ресурсу. Прежде, чем часть программы сможет использовать критическую часть кода, она должна выяснить, доступен ли ресурс.
Тесно связана с концепцией конкуренции проблема повторных входов. Функция считается повторно используемой, если она всегда работает правильно и сохраняет данные даже после прерывания и перезапуска. И снова возникают проблемы, когда задача с низким приоритетом прерывается прежде, чем завершает свою работу функция. Если более высокоприоритетная задача использует ту же самую функцию, функция перезапустится прежде, чем она закончит задачу с более низким приоритетом.
Следующие методы могут использоваться, чтобы создать повторно используемую функцию:
• Запрет на прерывания: Прерывания заблокированы при выполнении критических частей некоторой функции.
• Использование локальных переменных: Вспомним, что локальные переменные хранятся в стеках. Если высокоприоритетная задача выгружает задачу с низким приоритетом, переменные безопасно сохранены в стеке.
• Использование регистров микропроцессора: регистры микропроцессора могут использоваться, чтобы сохранить критические переменные внутри повторно используемой функции. Если функция прервана, переменные автоматически сохраняются в стеке микропроцессора.
• Комбинация методов: чтобы создать повторно используемую функцию, может использоваться комбинация этих методов.
Межзадачная связь — ключевое требование для ОСРВ, использующих прерывания. Эта проблема не критична для кооперативных многозадачных ОСРВ поскольку сами задачи определяют, когда можно вернуть управление операционной системе. Следовательно, действия задачи могут гарантировать, что все данные были правильно модифицированы перед отказом задачи от управления.
Проще всего использовать для пересылки данных между задачами глобальные переменные. Как мы видели в предыдущем обсуждении, эта простая методика может нарушаться, если низкоприоритетная задача выгружается задачей с высоким приоритетом прежде, чем получит возможность, чтобы модифицировать эти переменные.
Чтобы предотвращать такие случаи, мы можем использовать почтовый ящик — взаимно согласованное расположение в памяти, которую задачи могут совместно использовать. Почтовый ящик может содержать одну переменную, группу переменных или даже структуру данных. Чтобы гарантировать, что, данные в почтовом ящике являются текущими, задача, может выполнять почтовую операцию — операцию, которая может записывать в почтовый ящик информацию, защищенную шифром, и другая задача может затем считать содержимое почтового ящика (выполнить операцию pend), если имеет доступ к шифру. Шифром в каждый момент обладает только одна задача, и только она может работать с почтовым ящиком в это время. Это очень похоже на обсужденную ранее методику семафоров.
Одной из наиболее критичных проблем, усложняющих работу ОСРВ, является проблема безопасности. Мы уже обсудили часть стандартов, касающихся измерений и безопасности. В зависимости от того, где используется ОСРВ (связь, медицинское изделие, коммерческое изделие, авиация, и т.д.), имеются различные требования безопасности, которые должны быть выполнены. Безопасность операционной системы должна быть подтверждена документацией и испытаниями. Кроме того, в случае сбоя, система должна переходить в безопасный режим — предотвращающий травматизм персонала и повреждение оборудования.
Пример: Если мы должны были использовать ОСРВ, чтобы управлять светофором на оживленном перекрестке, безопасным режимом при любых сбоях системы будет красный свет для обоих направлений движения. Нетрудно представить, что выбор для безопасного режима зеленого света сразу приведет к катастрофическим последствиям.
Проблема безопасности — ключевой фактор при решении вопроса: написать ли вам собственную операционную систему или приобрести стандартную.
При нашем обсуждении операционных систем в режиме реального времени, мы обходили ключевой вопрос, который вы могли бы задать: следует написать свою собственную систему или приобрести стандартную? Мы полагаем, что это решение этой проблемы остается за вами — проектировщиком системы. Чтобы помочь вам принять это решение, мы рекомендуем учесть следующие три фактора:
• Применение: Для чего система будет использоваться? Если это используется в системе, где безопасность — критичная проблема, разумно использовать систему, сертифицированную для безопасного использования. Это не подразумевает высокой стоимости. Пакеты ОСРВ для безопасной работы могут быть и дешевыми [Labrosse 2002].
• Критичность: уважаемый коллега по вычислительной технике сознательно выбрал собственную разработку, а не применение стандартного пакета ОСРВ при разработке критичного применения ОСРВ, усложненного требованиями к ядерной безопасности. Он мотивировал это тем, что он хотел написать каждую строку этой программы и полностью понимать работу системы. Он чувствовал, что в коммерческом пакете некоторые подробности системы будут от него скрыты.
• Стоимость: Не следует думать, что разработка собственного ОСРВ может принести вам большую экономию, поскольку стоимость программ в 5 000 долларов может показаться слишком высокой. Однако при создании собственной программы ваши трудозатраты могут стоить не меньше.
Какой бы путь вы не выбрали, вам будут полезны некоторые рекомендации следующего раздела, которым необходимо следовать при разработке ОСРВ.
Мы приводим здесь краткое описание шагов, необходимых для создания реализации выполнения операционной системы в режиме реального времени ОСРВ:
1. Полностью понять все системные требования. Определить, действительно ли вам требуется ОСРВ. Если вы разрабатываете простой алгоритм управления, то вас может удовлетворить обычная последовательная программа; ОСРВ необходима только тогда, когда ваша прикладная программа должна управлять целым рядом событий.
2. Определить соответствующий алгоритм планирования, который целесообразно использовать для управления задачами. Не забудьте, каждый из них не лучше другого. Вы, как разработчик системы, задание должны найти алгоритм планирования, наиболее удовлетворяющий применению.
3. Разделить вашу систему на независимые задачи.
4. Выполнить операционную систему, основная функция которой заключается в планировании и управлении задачами.
5. Написать код задач. Убедитесь, что задачи не конкурируют за одни и те же участки памяти и что они не зависят друг от друга. Убедитесь также, что реализация задачи соответствует принятому алгоритму планирования.
6. Если вы разрабатываете систему с передним планом и фоновыми задачами, разработайте сначала фоновую, а затем приоритетную часть.
7. И что важнее всего, проверяйте, проверяйте и проверяйте! Используйте звуковые методики испытаний, которые были обсуждены в главе 2.
В этом разделе мы обсудим различные операционные системы в режиме реального времени. Мы начнем с базовой системы циклического опроса, а затем рассмотрим систему циклического опроса с прерываниями. Затем мы опишем аппаратный имитатор, предназначенный для разработки и проверки более сложных алгоритмов планирования.
Мы исследуем здесь базовую систему циклического опроса, используемую для управления стереоусилителем. Мы рассматривали уже такой усилитель в главе 2, когда обсуждали проектирование систем и затем в данной главе в качестве примера системы циклического опроса. На рис. 8.18 представлен общий вид системы усилителя. Шесть переключателей на передней панели блока или на пульте дистанционного управления используются, чтобы выбрать один из шести источников звукового сигнала. На усилитель в любой момент времени подается сигнал только с одного источника.
Рис. 8.18. Краткий обзор усилителя
Приведенный код, который используется, чтобы управлять усилителем, состоит из кода инициализации и цикла опроса. Цикл опроса непрерывно проверяет изменение в состоянии переключателей на лицевой панели блока (PORTB) или дистанционном управлении (PORTA). Алгоритм управления UML приведен на рис. 8.19.
Рис. 8.19. Алгоритм программы UML для усилителя
//file name: ampl2.с
//function: program provides control of amplifier
//target controller: Motorola 68HC912B32 evaluation board (EVB)
// - 32K Flash EEPROM available at $8000
// - Compiler options:
// - Program Memory: 0x8000
// - DataMemory: 0x0800
// - Stack Pointer: 0x09FF
//
// Эта программа обеспечивает управление звуковым усилителем.
// Усилитель может принимать звуковой сигнал от ряда
// источников. Пользователь может выбирать источник сигнала
// для усиления с помощью переключателей на лицевой панели
// (связанных с портом B), либо переключателей на пульте
// дистанционного управления (связанных с портом A). Процессор
// управляет светодиодами на передней панели(связанными с портом P)
// и показывающими активный источник сигнала и включает реле(связанные
// с портом T), подсоединяющие один из источников сигнала к усилителю
//
// Функции портов ввода
//
// Порт А, входной - вводит сигналы от пульта дистанционного управления,
// требует импульсов высокого логического уровня длительностью в 100 мс
// PA7 выкл. звука от пульта дист. управления высокий - импульс 100 мс
// PA6 Дополнительный канал (ДК) от пульта дист. управления высокий
// импульс 100 мс
// PA5 магнитофон # 2 от пульта дист. управления высокий - импульс 100 мс
// PA4 магнитофон # 1 от пульта дист. управления высокий - импульс 100 мс
// PA3 тюнер от пульта дист. управления высокий - импульс 100 мс
// PA2 CD от пульта дист. управления высокий - импульс 100 мс
// PA1 пианино от пульта дист. управления высокий - импульс 100 мс
// PA0 предусилитель от пульта дист. управления высокий - импульс 100 мс
// Порт В входной - от переключателей на лицевой панели блока
// PB0 предусилитель от переключателя на лицевой панели, вжатый перек-
// лючатель = вкл
// PB1 пианино от переключателя на лицевой панели, вжатый переключатель = вкл
// PB2 CD от переключателя на лицевой панели, вжатый переключатель = вкл
// PB3 тюнер от переключателя на лицевой панели, вжатый переключатель = вкл
// PB4 магнитофон # 1 от переключателя на лицевой панели, вжатый пе-
// реключатель = вкл
// PB5 магнитофон # 2 от переключателя на лицевой панели, вжатый пе-
// реключатель = вкл
// PB6 ДК от переключателя на лицевой панели, вжатый переключатель = вкл
// PB7 выкл. звука от переключателя на лицевой панели, вжатый перек-
// лючатель = вкл
//
//Порт P выходной - светодиоды на лицевой панели
//PP0 сигнал на силовое реле и на светодиоды и сигнал низкого уровня
//для //светодиодов в буфер
//PP1 светодиод пианино выходной низкопотенциальный сигнал - 10 мА
//PP2 светодиод CD выходной низкопотенциальный сигнал - 10 мА
//PP3 светодиод тюнер выходной низкопотенциальный сигнал - 10 мА
//PP4 светодиод магнитофон # 1 выходной низкопотенциальный сигнал - 10 мА
//PP5 светодиод магнитофон # 2 выходной низкопотенциальный сигнал - 10 мА
//PP6 светодиод ДК выходной низкопотенциальный сигнал - 10 мА
//PP7 светодиод выкл. звука, сигнал на силовое реле
//
//Порт T выходной - драйверы реле
//PT0 реле RESET выход на реле RESET высокий уровень - импульс 5 мс
//PT1 реле пианино выход на реле пианино высокий уровень - импульс 5 мс
//PT2 реле CD выход на реле CD высокий уровень - импульс 5 мс
//PT3 реле тюнера выход на реле тюнера высокий уровень - импульс 5 мс
//PT4 реле магнитофон # 1 выход на реле магнитофон # 1 высокий уро-
//вень - импульс 5 мс
//PT4 реле магнитофон # 2 выход на реле магнитофон # 2 высокий уро-
//вень - импульс 5 мс
//PT6 реле ДК выход на реле ДК высокий уровень //- импульс 5 мс
//PT7 высокий уровень - импульс 10 мс для подачи питания на оптроны
//светодиодов и усилитель
//Подача питания (от сети или от источника 5 В):
//Конфигурация портов:
//1. Порт A: конфигурирован как входной, отжатый переключатель - запрет
//2. Порт B: конфигурирован как входной, отжатый переключатель - разрешение
//3. Порт P: конфигурирован как выходной, все линии в 1
//4. Порт T: конфигурирован как выходной, все линии в 0
//5. Установка "RELAY-RESET" (PTO) импульсом высокого состояния 5 мс
//6. Установка "RELAY-CD" (PT2) импульсом высокого состояния 5 мс
//7. Установка "WHICH-INPUT" позиция сохранения = "CD"
//8. Цикл PP1-РР6 (устанавливаются в низкое состояние) светодиоды
// показывают , что контроллер работает
//9. Переход к последовательности "PREAMP ON"
//
//Логика работы :
//Последовательность "PREAMP ON"
//1. Ожидание установки "S-PREAMP-PWR" (PB0) или "R-PREAMP-PWR" (РАО)
//2. Установка "LED-MUTE-RELAY" (PP7)
//3. Установка "LED-PWR-RELAY" (PP0)
//4. Считывание позиции в "WHICH-INPUT"
//5. Установка "LED-xxxxx" = позиция "WHICH-INPUT"
//6. Установка PT7(1) импульсом 10 мс
//7. DE-Assert "LED-MUTE-RELAY" (PP7) через ~3 с.
//8. переход к режиму "SCAN"
//
//Последовательность "SCAN"
//1.Ожидание входного сигнала от (PB0-PB7) или от (PA0-PA7)
//2. IF = "x-PREAMP-PWR" - переход к последовательности "PREAMP OFF"
//3. IF = "x-MUTE" GOTO - переход к последовательности "MUTE"
//4. IF = любой входной сигнал от (PB1-PB6) или (PA1-PA6)- переход к
"CHANGE"
// последовательность "INPUT"
//
// последовательность "CHANGE INPUT":
//1. Включить "LED-MUTE-RELAY" (PP7)
//2. Включить "RELAY-RESET" (PT0) импульсом высокого уровня 5 мс
//3. Включить "RELAY-xxxxx" (PT1-PT6) (в соответствии с выбором
// "WHICH-INPUT" импульсом высокого уровня 5 мс)
//4. Включить "LED-xxxxx" (PP1-PP6) (в соответствии с выбором
// "WHICH-INPUT")
//5. Очистить Old/Input сохранить новое значение "WHICH-INPUT"
//6. DE-переключение "MUTE-RELAY" (PP7) примерно через 3 с.
//7. Перейти к последовательности "SCAN"
//
// последовательность "MUTE":
//1. Переключить "LED-MUTE-RELAY" (PP7)
//2. Перейти к последовательности "SCAN"
//
//Последовательность "PREAMP OFF":
//1. Включить "LED-MUTE-RELAY" (PP7)
//2. DE-переключение "LED-PWR-RELAY" (PP0)
//3. DE-переключение всех светодиодов (PP1-РР6)
//4. Включить PT7(1) импульсом 10 мс
//5. DE-переключение "LED-MUTE-RELAY" (PP7) примерно через 3 с.
//6. Перейти к последовательности "PREAMP ON"
//
//авторы: Steven Barrett и Daniel Pack
//Дата разработки: 19 июня 2004
//Последняя редакция: 20 июня 2004
//*******************************************************************
//*******************************************************************
//включенные файлы
#include <912b32.h> //B32 EVB header file
#include "func_def.h" //функции-прототипы, глобальные переменные
//main program*******************************************************
// глобальные переменные
int which_input; //вход усилителя
int keep_going; //ввод переменных
int mute; //флаг управления выключением звука
unsigned char old_PORTB = 0xff; //текущие значения PORTB
unsigned char old_PORTA = 0x00; //текущие значения PORTA
unsigned char new_PORTB, new_PORTA; //новые значения PORTA, PORTB
void main(void) {
asm(" .area vectors(abs)\n"
" .org 0xFFF8\n" //инициализация вектора сброса для 68HC12 B32
" .word 0x8000, 0x8000, 0x8000, 0x8000\n"
" .text");
initialize_task();
//главный цикл
while(1) { //ожидается сигнал на включение питания
if ((PORTB==0xFE)||(PORTA==0X01))
//PORTB переключается в низкое, PORTA - в
// высокое состояние
{ //вы забыли включить питание! Запрос на операцию включения
keep_going = 1; //цикл считывания переменных
PORTP=0x7E; //включение LED-MUTE-RELAY PP7(0)
//LED-PWR-RELAY PP0(0) (0111_1110)
which_input_task();
activate_power_relay_task();
delay_3s(); //задержка 3 с.
PORTP = 0x80; // DE-переключение PD7(1) - включение звука
while(keep_going) //прохождение меню - главный цикл опроса
{
process_PORTB_input_task();
process_PORTA_input_task();
}
}//end if - ожидание включения питания - питание не подано!
}//end while(1)
}//конец главного цикла
//*******************************************************************
// определение функций
//*******************************************************************
initialize_task: начальные установки усилителя
//*******************************************************************
void initialize_task(void) {
mute = on; //turn mute on
initialize_timer(); // инициализация таймера
initialize_ports(); // инициализация портов
initialize_pins(); // инициализация состояния отдельных выводов
which_input = 2 ; //по умолчанию включается вход CD(2)
//включение светодиодов на лицевой панели
PORTP = 0x81; //включение всех светодиодов PD1-PD6 низким активным
// уровнем (1000_0001)
delay_3s(); //задержка 3 с
PORTP = 0xff; //выключение светодиодов
}
//*******************************************************************
//which_input_task: опрос входов, установка текущего состояния
//*******************************************************************
void which_input_task(void) {
switch(which_input) { // подсвечивается светодиод для используемого
// входа (по умолчанию вход 2 - CD)
case 1: //Пианино
phono_task();
break;
case 2: //CD
CD_task();
break;
case 3: //Тюнер
tuner_task();
break;
case 4: //Магнитофон 1
tape1_task();
break;
case 5: //Магнитофон 2
tape2_task();
break;
case 6: //Дополнительный канал (ДК)
aux_task();
break;
default:;
}//конец switch
}
//*******************************************************************
//phono_task: конфигурируется вход от Радио
//*******************************************************************
void phono_task(void) {
PORTT |= 0x02; //устанавливается PT1(1) (0000_0010)
delay_5ms();
PORTT &= ~0x02; // выключается PT1(0)
PORTP = 0x7E; //гасятся все светодиоды
PORTP &= ~0x02; //включается светодиод 1 (0)
}
//******************************************************************
//CD_task: конфигурируется вход от CD
//******************************************************************
void CD_task(void) {
//CD
PORTT |= 0x04; // устанавливается PT2(1) (0000_0100)
delay_5ms();
PORTT &= ~0x04; // выключается PT2(0)
PORTP |= 0x7E; //гасятся все светодиоды
PORTP &= ~0x04; // включается светодиод 2 (0)
}
//******************************************************************
//tuner_task: конфигурируется вход от тюнера
//******************************************************************
void tuner_task(void) {
//TUNER PORTT |= 0x08; // устанавливается PT3(1) (0000_1000)
delay_5ms();
PORTT & = 0x08; // выключается PT3(0
PORTP |= 0x7E; //гасятся все светодиоды
PORTP &= ~0x08; // включается светодиод 3 (0)
}
//******************************************************************
//tape1_task: конфигурируется вход от магнитофона 1
//******************************************************************
void tape1_task(void) {
//TAPE#1
PORTT |= 0x10; //assert PT4(1) (0001_0000)
delay_5ms();
PORTT &= ~0x10; // выключается PT4(0)
PORTP |= 0x7E; //гасятся все светодиоды
PORTP &= ~0x10; // включается светодиод 4 (0)
}
//******************************************************************
//tape2_task: конфигурируется вход от магнитофона 2
//******************************************************************
void tape2_task(void) {
//TAPE#2
PORTT |= 0x20; // устанавливается PT5(1) (0010_0000)
delay_5ms();
PORTT &= ~0x20; // выключается PT5(0)
PORTP |= 0x7E; //гасятся все светодиоды
PORTP & = ~0x20; // включается светодиод 5 (0)
}
//******************************************************************
//aux_task: конфигурируется вход от дополнительного канала
//******************************************************************
void aux_task(void) {
//ДК
PORTT |= 0x40; // устанавливается PT6(1) (0100_0000)
delay_5ms();
PORTT &= ~0x40; // выключается PT6(0)
PORTP |= 0x7E; //гасятся все светодиоды
PORTP &= ~0x40; // включается светодиод 6(0)
}
//******************************************************************
//activate_power_relay_task(): включается реле силового питания
//******************************************************************
void activate_power_relay_task(void) {
PORTT |= 0x80; // устанавливается PT7(1) импульсом 10 мс
delay_5ms();
delay_5ms();
PORTT &= ~0x80; // выключается PT7
}
//******************************************************************
//process_PORTB_input_task(): определяется выбранный вход от PORTB
//******************************************************************
void process_PORTB_input_task(void) {
new_PORTB = PORTB; //read PORTB
if (new_PORTB != old_PORTB) { //считывание состояния порта PORTB
switch(new_PORTB) { //PORTB устанавливается на низкий уровень
case 0xFE: //PB0 "S-PREAMP-PWR" (1111_1110)
if (process_valid_input_PORTB(new_PORTB)) {
preamp_off();
keep_going=0;
}
break;
case 0xFD: //PB1 "S-PHONO" (1111_1101)
if (which_input !=1) {
if (process_valid_input_PORTB(new_PORTB) {
which_input = 1;
change_input();
}
}
break;
case 0xFB: //PB2 "S-CD" (1111_1011)
if (which_input!=2) {
if (process_valid_input_PORTB(new_PORTB)) {
which_input = 2;
change_input();
}
}
break;
case 0xF7: //PB3 "S-TUNER" (1111_0111)
if (which_input != 3) {
if (process_valid_input_PORTB(new_PORTB)) {
which_input = 3;
change_input();
}
}
break;
case 0xEF: //PB4 "S-TAPE#1" (1110_1111)
if (which_input != 4) {
if (process_valid_input_PORTB(new_PORTB)) {
which_input = 4;
change_input();
}
}
break;
case 0xDF: //PB5 "S-TAPE#2" (1101_1111)
if (which_input != 5) {
if (process_valid_input_PORTB(new_PORTB)) {
which_input = 5;
change_input();
}
}
break;
case 0xBF: //PB6 "S-AUX" (1011_1111)
if (which_input != 6) {
if (process_valid_input_PORTB(new_PORTB)) {
which_input = 6;
change_input();
}
}
break;
case 0x7F: //PB7 "S-MUTE" (0111_1111)
if (process_valid_input_PORTB(new_PORTB)) {
mute_toggle();
}
break;
default:; //all other cases
} //конец switch(new_PORTB)
} //конец if new_PORTB
old_PORTB=new_PORTB; //update PORTB
}
//******************************************************************
//process_PORTA_input_task():определяется выбранный вход от PORTA
//******************************************************************
void process_PORTA_input_task(void) {
new_PORTA = PORTA; //Читать PORTA
if (new_PORTA != old_PORTA) { //выбор входа по состоянию порта PORTA
switch (new_PORTA) { //PORTA переводится в высокое состояние
case 0x01: //РАО "R-PREAMP-PWR" (0000_0001)
if (process_valid_input_PORTA(new_PORTA)) {
preamp_off();
keep_going=0;
}
break;
case 0x02: //PA1 R-PHONO" (0000_0010)
if (which_input != 1) {
if (process_valid_input_PORTA(new_PORTA)) {
which_input = 1;
change_input();
}
}
break;
case 0x04: //PA2 "R-CD" (0000_0100)
if (which_input != 2) {
if (process_valid_input_PORTA(new_PORTA)) {
which_input = 2;
change_input();
}
}
break;
case 0x08: //РАЗ "R-TUNER" (0000_1000)
if (which_input != 3) {
if (process_valid_input_PORTA(new_PORTA)) {
which_input = 3;
change_input();
}
}
break;
case 0x10: //PA4 "R-TAPE#1" (0001_0000)
if (which_input != 4) {
if (process_valid_input_PORTA(new_PORTA)) {
which_input = 4;
change_input();
}
}
break;
case 0x20: //PA5 "R-TAPE#2M (0010_0000)
if (which_input != 5) {
if (process_valid_input_PORTA(new_PORTA)) {
which_input = 5;
change_input();
}
}
break;
case 0x40: //PA6 "R-ДОПОЛНИТЕЛЬНЫЙ КАНАЛ" (0100_0000)
if (which_input != 6) {
if (process_valid_input_PORTA(new_PORTA)) {
which_input = 6;
change_input();
}
}
break;
case 0x80: //PA7 "R-MUTE" (1000_0000)
if (process_valid_input_PORTA(new_PORTA)) {
mute_toggle();
}
break;
default:; //all other cases
} //конец switch(new_PORTA)
}//конец if new_PORTA
old_PORTA = new_PORTA; //изменяется состояние PORTA
}
//******************************************************************
//initialize_timer:установка частоты таймера обслуживающего счетчик
//******************************************************************
void initialize_timer(void) {
TMSK2 = 0x05; //установка на 250 КГц
TSCR = 0x80; //разрешение работы таймера
}
//******************************************************************
//initialize_ports: начальная конфигурация портов
//******************************************************************
void initialize_ports(void) {
DDRA=0x00; //конфигурация PORTA в качестве входного
PORTA=0x00; //запрет на подключение подтягивающих резисторов в PORTA
DDRB=0x00; //конфигурация PORTB в качестве входного
PORTB=0xff; //разрешение подключения подтягивающих резисторов в PORTB
DDRT=0xff; // конфигурация PORTT в качестве выходного
PORTT=0x00; // установка на низкий уровень
DDRP=0xff; // конфигурация PORTD в качестве выходного
PORTP=0xff // установка на высокий уровень
}
//******************************************************************
//******************************************************************
//initialize_pins: установка отдельных выводов
//******************************************************************
void initialize_pins(void) {
PORTT=0x01; //сброс реле PT0(1) 5 мс импульс с
// активным уровнем (0000_0001)
//delay_5ms():
PORTT=0x00;
}
//******************************************************************
//delay_5ms: Задержка на 5 мс сформированная из базе частоты таймера
//в 250 кГц
//******************************************************************
void delay_5ms(void) {
int i;
for(i=0; i<1250; i++)
asm("nop"); //требуется только один импульс таймера
}
//******************************************************************
//delay_3s: Задержка на 3 с
//******************************************************************
void delay_3s(void) {
int i;
for(i=0;i<600;i++) delay_5ms();
}
//******************************************************************
//change_input: изменение активного входа
//******************************************************************
void change_input(void) {
PORTP &= ~0x80; //установка LED-MUTE-RELAY PP7(0) 1000_0000
PORTT |= 0x01; //установка сброса реле PT0(l) 5 мс
delay_5ms();
PORTT &= ~0x01; //turn off PT0
switch(which_input) {
case 1: //PHONO
phono_task();
break;
case 2: //CD
CD_task();
break;
case 3: //TUNER
tuner_task();
break;
case 4: //TAPE#1
tape1_task();
break;
case 5: //TAPE#2
tape2_task();
break;
case 6: //AUX
aux_task();
break;
default:;//все другие входы
}//конец switch
delay _3s();
PORTP |= 0x80; //сброс LED-MUTE-RELAY PP7(1)
}
//******************************************************************
//mute_toggle: включение и выключение звука
//******************************************************************
void mute_toggle(void) {
if (mute == off) {
PORTP &= ~0x80; //установка LED-MUTE-RELAY PP7(0)
mute = on;
} else {
PORTP |= 0x80; // сброс LED-MUTE-RELAY PP7(1)
mute = off;
}
}//end mute_toggle
//******************************************************************
//preamp_off: turn amplifier off
//******************************************************************
void preamp_off(void) {
PORTP &= ~0x80; //установка LED-MUTE-RELAY PP7(0)
PORTP |= 0x01; //сброс LED-PWR-RELAY PP0(1)
PORTP |= 0x7e; //сброс светодиодов PP1-PP6(1)(0111_1110)
//установка PT7 импульсом 10 мс
PORTT |= 0x80; //установка PT7(1) импульсом 10 мс
delay_5ms();
delay_5ms();
PORTT &= ~0x80; //сброс PT7
delay_3s();
PORTP = 0x80; //сброс PP7(1) LED-MUTE-RELAY
keep_going=0;
}
//******************************************************************
//process_valid_input_PORTA: проверка состояния пульта дистанционного
//управления, длительностью не менее 50 мс
//******************************************************************
int process_valid_input_PORTA(unsigned char portx) {
int valid_input; //установить флаг ошибочного входа
unsigned int current_count;
valid_input = TRUE;
current_count = TCNT; // задать текущее состояние
while (TCNT < (current_count+12500)) { //отследить активный вход за 50 мс
if (portx==PORTA) valid_input = TRUE;
else valid_input = FALSE;
if (!valid_input) break; //цикл while
}//end while
return valid_input;
}
//******************************************************************
//process_valid_input_PORTB: проверка состояния переключателей на
//лицевой панели,длительностью не менее 50 мс
//******************************************************************
int process_valid_input_PORTB(unsigned char portx) {
int valid_input; //установить флаг ошибочного входа
unsigned int current_count;
valid_input = TRUE;
current_count = TCNT; // задать текущее состояние
while (TCNT < (current_count+12500)){ //отследить активный вход за 50 мс
if (portx==PORTB) valid_input = TRUE;
else valid_input = FALSE;
if (!valid_input) break; //цикл while
}//конец while
return valid_input;
}
//******************************************************************
//******************************************************************
Проверять программное обеспечение целесообразно при работе с имитатором усилителя, как мы уже упоминали в главе 2. Воспроизведем снова схему имитатора усилителя для удобства на рис. 8.20. На дешевом имитаторе можно провести полное испытание алгоритма управления, проверив его работу при мыслимых сценариях.
Рис. 8.20. Имитатор усилителя
В этой программе мы разработаем приоритетную часть системы опроса с передним планом и фоном, позволяющей предотвратить перегрев транзисторов в стереоусилителе. Система приведена на рис. 8.21. Температура транзистора постоянно контролируется датчиком температуры LM34 (в пластмассовом корпусе), приклеенным к металлическом корпусу ТО K-220 мощного транзистора. Напряжение на выходе датчика линейно связано с его температурой (коэффициент 10 мВ/С).
Рис. 8.21. Транзисторные системы обнаружения перегрева
Выход LM34 подан к один из входов аналогового компаратора, построенного на ОУ. На другой вход подается опорное напряжение, задающее порог температуры. Когда температура мощного транзистора достигает этого порога, на входе системы прерывания микроконтроллера появляется активный сигнал низкого уровня формирующий запрос на прерывание IRQ.
При обработке запроса на прерывание, PORTX[0] формируется импульс с активным высоким уровнем, подключающий дополнительный вентилятор через мощный МОП-транзистор IRF530. Можно предусмотреть определенную задержку в программе обработки прерывания и затем и затем выключить вентилятор. Когда температура транзистора упадет ниже установленного предела, вывод запроса на прерывание перейдет на высокий логический уровень, возвращая процессор к выполнению основной программы.
Если температура транзистора упадет до безопасного уровня процессор продолжит нормальную работу, (при этом на входе запроса на прерывание установится сигнал высокого уровня), а при превышении температуры снова инициализирует прерывание (установится непрерывный низкий уровень на входе запроса на прерывание). Мы предлагаем вам самостоятельно разработать программу для системы, использующей прерывания, в качестве задания для самостоятельной работы (задание 2).
Некоторые из продвинутых алгоритмов планирования в режиме реального времени достаточно сложно выполнить. Сложность частично заключается в том, что внутренняя операция этих программ не визуализирована. Имитатор ОСРВ, показанный на рис. 8.22 может использоваться как помощь в разработка ОСРВ.
Рис. 8.22. Имитатор ОСРВ
Имитатор состоит из набора блоков для 16 задач: пронумерованных в шестнадцатеричной системе от задачи 0 до задачи F. Каждый из таких блоков выполнен на микросхеме регистра с защелкой 74HC573. Состояние пересылается в регистр, где оно сохраняется вплоть до изменения. Состояние каждой задачи отображается набором светодиодов. Только один светодиод высвечивается в любой момент для каждой задачи. Мы использовали схему драйвера для светодиодов, рассмотренную в главе 5 для каждого из регистров 74HC573.
Схема симулятора приведена на рис. 8.23. Демультиплексор 4 в 16 (74HC154) используется, чтобы выбрать отдельную задачу. Двоичный «адрес» задачи задается микропроцессором на входных линиях демультиплексора. В любой момент времени только один его выход находится в активном низком состоянии и, следовательно, выбирается только одна задача.
Рис. 8.23. Аппаратное обеспечение симулятора ОСРВ
Обычно после инициировании все задачи находятся в состоянии бездействия. Чтобы выбрать различные состояния системы используется вспомогательная клавиатура, моделирующая работу системы ОСРВ. С вспомогательной клавиатуры набирается сначала конкретное состояние, отображающее набор типов активности различных задач. Затем с той же клавиатуры набирается информация о том, активность какой из задач будет изменена.
Чтобы облегчить понимании концепций ОСРВ, обратимся снова к относительно гибким, быстро изменяющимся и просто визуализируемым сценариям с дилерским обслуживанием автомобиля, обслуживанием многих заказчиков официантом или даже к системе защиты в большой гостинице. Любые из этих примеров могут использоваться, чтобы иллюстрировать различные алгоритмы планирования.
Например, используемый автомобиль (задача) может иметь свойственный контекст, состоящий из года изготовления, модели, номера и показаний одометра. Автомобиль (задача) может находится в различных состояниях, идентичных состояниям задач (готовности, бездействия и т.д). После изготовления автомобиля, он может быть готов для продажи (готовность), проходить испытания (активность), недоступен из-за обслуживания (ожидание), продан (бездействие), и т.д. Состояние нескольких автомобилей (задач) может быть визуально отображено на имитаторе ОСРВ (до 16 задач). Новое состояние сценария может вводиться пользователем с шестнадцатеричной вспомогательной клавиатуры. Новое состояние можно показывать на ЖКД, а изменение списков с указателями получаемых при изменении состояния может быть отображено на экране ПК [Barrett 20041].
Детальное описание имитатора ОСРВ выходит за рамки данной книги. В этом разделе мы просто приведем код, позволяющий отображать и изменять состояние каждой из задач.
Функция update_task_status требует в качестве параметров идентификационного номера задачи и кода желательного изменения состояния задачи. Специфическая задача используется, чтобы конфигурировать
PORTA[3:0]
правильным четырехразрядным двоичным кодом, который подается на микросхему 74HC154 декодера 4–16. Когда на демультиплексор подается код через выходной порта PORTA[7]
, активируется триггер-защелка, соответствующий выбранной задаче. Это позволяет передать модифицированное состояние задачи, представленное на выходах порта T контроллера 68HC12 на светодиоды и, таким образом, показать состояние задачи на дисплее.
Алгоритм программы на UML для реализации этой функции представлен на рис. 8.24.
Рис. 8.24. Алгоритм UML для функции update_task_status
//******************************************************************
//имя файла: realtime.с
//авторы: Steve Barrett and Daniel Pack
//разработан: 1 июля, 2004
//последняя редакция: 1 июля, 2004
//******************************************************************
//включенные файлы ****************************************
#include <912b32.h>
//функции-прототипы****************************************
void initialize_ports(void); //инициализация портов
void initialize_LCD(void); //инициализация ЖКД
void putchars(unsigned char); //функция поддержки ЖКД - ввести символ
void putcommands(unsigned char); // функция поддержки ЖКД - ввести команду
void delay_5ms(void); //задержка на 5 мс
void delay_100us(void); //задержка на 100 мкс
void update_task_status(unsigned char task, char task_status);
//изменить состояние задачи
//глобальные переменные************************************************
//главная программа****************************************************
void main(void) {
asm(" .area vectors(abs)\n" //inline assembly statement
" .org 0xFFF8\n" //initialize 68HC12 B32 reset vector
" .word 0x8000, 0x8000, 0x8000, 0x8000\n"
" .text");
initialize_ports(); //инициализировать порты
initialize_LCD(); // инициализировать ЖКД
update_task_status(0x00, 'R') ; //изменить состояние задачи
}
//******************************************************************
//initialize_ports: provides initial configuration for I/O ports
//******************************************************************
void initialize_ports(void) {
DDRA = 0xFF; //установить PORTA как выходной - управляющие линии
// демультиплексора
DDRT = 0xFF; // установить PORTT как выходной - состояние задачи
DDRB = 0xFF; // установить PORTB как выходной - порт данных для ЖКД
CTDRDLC = 0xFF; // установить PORT DLC как выходной - сигналы
//управления для ЖКД
DDRP = 0x0F; // установить PORTP[3:0] как выходной, PORT[7:4] как
//входной - для клавиатуры
PORTA = 0xFF; //установить для PORTA все линии декодера высокоомными (Hi-Z)
}
/****************************************************************/
/****************************************************************/
/*update_task_status: изменить состояние конкретной задачи */
/*в соответствии с допустимым переходом (рис. 8.14) */
/****************************************************************/
void update_task_status(unsigned char task, char task_status) {
//установить состояние задачи на выходе порта T
switch(task_status) {
case 'D': //бездействие (D)
PORTT = 0x01;
break;
case 'R': //готовность (R)
PORTT = 0x02;
break;
case 'A': //активность (A)
PORTT = 0x04;
break;
case'W': //ожидание (W)
PORTT = 0x08;
break;
case 'S': // приостановка(S)
PORTT = 0x10;
break;
case 'X': //восстановление (X)
PORTT = 0x20;
break;
}
PORTA = task & 0x7F; /*выбор задачи, активизируйте декодер */
PORTA = 0xFF /*Высокоомный выход (Hi-Z) декодера */
}
/****************************************************************/
/****************************************************************/
В этой главе мы познакомили вас с концепциями операционных систем реального времени. Мы не хотели выбирать конкретные ОСРВ и затем рассматривать их работу. Вместо этого, мы сосредоточились на концепциях, связанных с самими ОСРВ и на ключевых вопросах их применения. Мы рассмотрели терминологию ОСРВ, структуры данных, алгоритмы планирования и затруднения, встречающиеся при разработке этих систем.
1. Barrett S. F, D. J. Pack, С Straley, L. Sircin, and G. Janack. «Real-Time Operating Systems: A Visual Simulator.» Paper presented at the annual meeting of the American Society for Engineering Educations, June 2004.
2. Ganssle, J. «Writing a Real-Time Operating System-Part I: A Multitasking Event Scheduler for the HD64180.» Circuit Cellar Ink (January/February 1989): 41–51.
3. Ganssle, J. «Writing a Real-Time Operating System-Part II: Memory Management and Applications for the HD64180.» Circuit Cellar Ink (March/April 1989): 30–33.
4. Ganssle, J. «An OS in a CAN.» Embedded Systems Programming (January 1994): 1–6. ImageCraft Creations, Inc. «ICC12, ImageCraft С Compiler and Development Environment for Motorola HC12.» 2001.
5. Korsch, J. F., and L. J. Garrett. Data Structures, Algorithms, and Program Style Using С. Boston: PWS-Kent Publishing Company, 1988.
6. Labrosse, J. J. Micro C/OS-II The Real-Time Kernel, 2nd ed. Lawrence, KS: CMP Books, 2002.
7. Lafore, R. The Waite Group's Microsoft С Programming for the PC, 2nd ed. Carmel, IN, Howard W. Sams and Company, 1990.
8. Laplante, P. Real-Time Systems Design and Analysis: An Engineer's Handbook. New York: IEEE Computer Society Press, 1993.
9. Miller, G. H. Microcomputer Engineering, 2nd ed. Englewood Cliffs, NJ: Pearson Education, 1998.
10. Moore, R. How to Use a Real-time Multitasking Kernels in Embedded Systems, Costa Mesa, CA: Micro Digital Associates, 2001.
11. Motorola Inc. «68HC12 M68EVB912B32 Evaluation Board User's Manual.» Motorola Document 68EVB912B32 UM/D, 1997.
12. Motorola Inc. «HC12 M68HC12B Family Advance Information.» Motorola Document M68HC12B/D, 2000.
13. Pack, D. J., and S. F. Barrett. 68HC12 Microcontroller: Theory and Applications. Upper Saddle River, NJ: Prentice Hall, 2002.
1. Что такое — ОСРВ?
2. Что называется задачей в системах реального времени?
3. Что такое контекст задачи? Приведите конкретные примеры содержимого контекста задачи.
4. Опишите действия, которые ОСРВ должен выполнить относительно задачи.
5. Что такое — ядро ОСРВ? Какими основными свойствами оно должно обладать?
6. Каковы различия между глобальной и локальной переменной?
7. Что понимается под динамическим распределением памяти?
8. Какая память (RAM, ROM, и т.д.) используется при динамическом распределении памяти? Объясните почему.
9. Опишите следующие структуры данных. Где они обычно используются:
• Структура/запись;
• Список с указателями;
• Очередь;
• Круговая очередь;
• Стек.
1. Объясните различие между жесткой, твердой, и мягкой системами в режиме реального времени. Приведите пример для каждой из них.
2. Сравните динамическую память со стеком. Где они обычно размещаются в системе памяти? Почему?
3. Каких методов программирования нужно избегать, чтобы сохранить память RAM? Почему?
4. Определите каждое из различных состояний, в которых может находиться задача. Во скольких состояниях задача может находиться одновременно?
5. Что такое — управляющий блок задачи (TCB)? Из чего он должен состоять? Какая структура данных была бы хорошим выбором для TCB? Почему?
6. Какова функция диспетчера/планировщика в ядре ОСРВ? Определите каждый из различных алгоритмов планирования и их свойственные им преимущества и недостатки.
7. Что такое конкуренция? Как это происходит? Как этого избежать?
8. Что такое повторная входимость? Как это происходит? Как это предотвратить?
9. Что понимается под отказоустойчивой работой ОСРВ? Почему — это проблема является критичной при разработке ОСРВ?
10. Управляемая прерыванием ОСРВ должна быть выполнена на 68HC12. Вы решили, что система всегда должна отвечать на прерывания с более высоким приоритетом, когда они происходят. Как это может быть выполнено? Вспомните, что 68HC12 автоматически отключает систему прерывания при ответе на прерывание, Подсказка: Посмотрите описание команд CLI и SEI ассемблера 68HC12.
1. Разработайте стек и связанные с ним функции, использовав список с указателями для динамического распределения памяти.
2. Разработайте приоритетную часть системы фонового опроса с передним планом, для защиты от перегрева транзисторов, описанной в применениях раздела 8.9.
На рис. 8.25 (совпадающим с рис. 8.21 и повторенном здесь для удобства) показана система защиты от транзистора от перегрева. Температура транзистора постоянно контролируется датчиком температуры LM34 (в пластмассовое корпусе) приклеенным к металлическому корпусу мощного транзистора K-220. Напряжение на на выходе датчика линейно связано с его температурой (коэффициент 10 мВ/°С). Выход LM34 подан на один из входов аналогового компаратора, построенного на ОУ. На другой вход подается опорное напряжение, задающее порог температуры. Когда температура мощного транзистора достигает этого порога, на входе системы прерывания микроконтроллера появляется активный сигнал низкого уровня формирующий запрос на прерывание IRQ.
Рис. 8.25. Система защиты транзистора от перегрева
3. Разработайте и проверьте функцию, позволяющую модифицировать состояние задачи.
4. Выполните управляющий блок задачи (TCB) при помощи соответствующей структуры данных. Обеспечьте функции поддержки, чтобы обращаться к различным информационным полям TCB и модифицировать их.
5. Опишите различные методы выполнения межзадачной связи в ОСРВ.
6. Напишите короткую статью на две страницы, рассмотрев все за и против для двух вариантов: создание собственной ОСРВ и приобретение готовой системы.