Готова очередная публикация обзора особенностей ОСРВ МАКС. В предыдущей статье мы разбирались с теорией, а сегодня наступило время практики.
Часть 1. Общие сведения
Часть 2. Ядро ОСРВ МАКС
Часть 3. Структура простейшей программы
Часть 4. Полезная теория
Часть 5. Первое приложение (настоящая статья)
Часть 6. Средства синхронизации потоков
Часть 7. Средства обмена данными между задачами
Часть 8. Работа с прерываниями
При начале работы с контроллерами, принято мигать светодиодами. Я нарушу эту традицию.
Во-первых, это банально надоело. Во-вторых, светодиоды потребляют слишком большой ток. Не спешите думать, что я экономлю каждый милливатт, просто по ходу работ, нам эта экономия будет крайне важна. Опять же, то, что мы увидим ниже — на светодиодах увидеть практически невозможно.
Итак, пока нет генератора проектов, берём проект по-умолчанию для своей макетной платы и своего любимого компилятора (я взял ...\maksRTOS\Compilers\STM32F4xx\MDK-ARM 5\Projects\Default) и копируем его под иным именем (у меня получилось ...\maksRTOS\Compilers\STM32F4xx\MDK-ARM 5\Projects\Test1) . Также следует снять со всех файлов атрибут «Только для чтения».
Каталог файлов проекта весьма спартанский.
DefaultApp.cpp
DefaultApp.h
main.cpp
MaksConfig.h
Файл main.cpp относится к каноническому примеру, файлы DefaultApp.cpp и DefaultApp.h описывают пустой класс-наследник от Application. Файл MaksConfig.h мы будем использовать для изменения опций системы.
Если открыть проект, то окажется, что к нему подключено огромное количество файлов операционной системы.
В свойствах проекта также имеется бешеное количество настроек.
Так что не стоит даже надеяться создать проект «с нуля». Придётся смириться с тем, что его надо или копировать из пустого проекта по умолчанию, или создавать при помощи автоматических утилит.
Для дальнейшего изложения, я разрываюсь между «правильно» и «читаемо». Дело в том, что правильно — это начать создавать файлы для задач, причём — отдельно заголовочный файл, отдельно — файл с кодом. Однако, читатель запутается в том, что автор натворит. Такой подход хорош при создании видеоуроков. Поэтому я пойду другим путём — начну добавлять новые классы в файл DefaultApp.h. Это в корне неверно при практической работе, но зато код получится более-менее читаемым в документе.
Итак. Мы не будем мигать светодиодами. Мы будем изменять состояние пары выводов контроллера, а результаты наблюдать — на осциллографе.
Сделаем класс задачи, которая занимается этим шевелением. Драйверы мы использовать пока не умеем, поэтому будем обращаться к портам по-старинке. Выберем пару свободных портов на плате. Пусть это будут PE2 и PE3. Что они свободны, я вывел из следующей таблицы, содержащейся в описании платы STM32F429-DISCO:
Сначала сделаем класс, шевелящий ножкой PE2, потом — переделаем его на шаблонный вид.
Идём в файл DefaultApp.h (как мы помним, это неправильно для реальной работы, но зато наглядно для текста) и создаём класс-наследник от Task. Что туда нужно добавить? Правильно, конструктор и функцию Execute(). Прекрасно, пишем (первая и последняя строки оставлены, как реперные, чтобы было ясно, куда именно пишем):
Задача, дёргающая PE2 готов. Но теперь надо
Где это удобнее всего делать? Правильно, мы уже знаем, что это удобнее всего делать в функции
благо заготовка уже имеется. Пишем что-то, вроде этого:
Шьём в в пла… Ой, а в проекте по умолчанию используется симулятор.
Хорошо, переключаемся на JTAG адаптер (в случае платы STM32F429-DISCO — на ST-Link).
Теперь всё можно залить в плату. Заливаем, подключаем осциллограф к линии PE2, наблюдаем…
Красота! Только быстродействие какое-то низковатое.
Заменяем оптимизацию с уровня 0 на уровень 3
И… Всё перестаёт работать вообще
Пытаемя трассировать — по шагам прекрасно работает. Что за чудо? Ну, это не проблемы ОС, это проблемы микроконтроллера, ему не нравятся рядом стоящие команды записи в порт. Разгадка этого эффекта – в настройках тока выходных транзисторов. Выше ток – больше звон, но выше быстродействие. По умолчанию, все выходы настроены на минимальном быстродействии. А у нас оптимизатор всё хорошо умял:
Можно, конечно, поднять быстродействие выхода на этапе настройки порта
Но у сигнала будет явно неправильная скважность (Вверх, затем – вниз, затем – задержка на переход). Для улучшения сигнала поправим код основного цикла следующим образом:
Здесь получается вверх, затем – задержка на NOP, затем – вниз, затем – задержка на переход, что обеспечивает скважность, близкую к 50% (в комментариях ниже было точно вычислено, что реально 3 такта в единице и 5 тактов в нуле, но это ближе к 50%, чем 1 к 5, да и на имеющемся осциллографе разницу в верхней и нижней частях импульсов всё равно практически невозможно заметить). И быстродействия выхода уже хватает даже в малошумящем режиме. Частота выходного сигнала стала 168/(3+5) = 21 МГц.
Правда, на другом масштабе нет-нет, да и проскочат вот такие чёрные провалы
Это мы наблюдаем работу планировщика. Задача у нас одна, но периодически у неё отбирают управление, чтобы проверить, нельзя ли передать его кому-то другому. При наличии отсутствия других задач, управление возвращается той единственной, которая есть. Что ж, добавляем вторую задачу, которая будет дёргать PE3. Поместим номер бита в переменную-член класса, а настраивать его будем через конструктор
А добавление задач в планировщик — вот так:
Подключаем второй канал осциллографа к выводу PE3. Теперь иногда идут импульсы на одном канале
Ой какая частота низкая… Нет, фальстарт. Перепишем задачу на шаблонах…
И её постановку на планирование — вот так:
Итак. Теперь иногда импульсы (с правильной частотой) идут на одном канале:
А иногда — на другом
Можно выбрать масштаб, на котором видны кванты времени. Заодно произведём замер и убедимся, что один квант действительно равен одной миллисекунде (с точностью до масштаба экрана осциллографа)
Можно убедиться, что планировщику всё так же нужно время для переключения задач (причём больше, чем в те времена, когда задача была одна)
Теперь давайте рассмотрим работу потоков с разными приоритетами. Добавим забавную задачу, которая «то потухнет, то погаснет»
И добавим её в планировщик с более высоким приоритетом
Эта задача половину времени выполняет задержку без переключения контекста. Так как её приоритет выше остальных, то управление не будет передано никому другому. Половину времени задача спит. То есть, находится в заблокированном состоянии. То есть, в это время будут работать потоки с нормальным приоритетом. Проверим?
Собственно, что и требовалось доказать. Пауза равна пяти миллисекундам (выделено курсорами), а во время работы нормальных задач, контекст успевает 5 раз переключиться между ними. Вот другой масштаб, чтобы было видно, что это не случайность, а статистика
Убираем работу этой ужасной задачи. Продолжать будем с двумя основными
Task::Add (new Blinker<2> («Blink_PE2»));
Task::Add (new Blinker<3> («Blink_PE3»));
Наконец, переведём дёрганье порта из режима «Совсем дёрганный» в более реальный. До светодиодного доводить не будем. Скажем, сделаем период величиной в 10 миллисекунд
Тепрерь подключаем амперметр. Для платы STM32F429-DISCO надо снять перемычку JP3 и включить прибор вместо неё, о чём сказано в документации:
Измеряем ток, потребляемый данным вариантом программы
Идём в файл MaksConfig.h и добавляем туда строку:
#define MAKS_SLEEP_ON_IDLE 1
Собираем проект, «прошиваем» результат в плату, смотрим на амперметр:
Таааак, ещё одну теоретическую вещь проверили на практике. Тоже работает. А вот если бы мы мигали светодиодом, то он бы то потреблял, то не потреблял 10 мА, что на фоне измеренных значений — вполне существенно.
Ну, и напоследок заменим многозадачность на кооперативную. Для этого добавим конструктор к классу приложения
И сделаем так, чтобы задачи после трёх импульсов в порт передавали друг другу управление. Также добавим задержки, чтобы на осциллографе задержка планировщика не уводила бы изображение другой задачи за экран.
Что там на осциллографе?
Собственно, мы хотели тройки импульсов — мы их получили.
Ну, и наконец, добавим виртуальную функцию
и попробуем вызвать какую-либо проблему. Например, создадим критическую секцию в задаче с обычным уровнем привилегий.
Запускаем проект на отладку, ставим точку останова на следующую строку
после чего запускаем на исполнение (F5). Моментально получаем останов (если не сработало — щёлкаем по пиктограмме «Stop»).
В строке, на которой произошёл останов, наводим курсор на переменную reason. Получаем следующий результат:
Ну что же, проверку первых основных теоретических выкладок мы завершили, можно переходить к следующему большому сложному разделу.
Часть 1. Общие сведения
Часть 2. Ядро ОСРВ МАКС
Часть 3. Структура простейшей программы
Часть 4. Полезная теория
Часть 5. Первое приложение (настоящая статья)
Часть 6. Средства синхронизации потоков
Часть 7. Средства обмена данными между задачами
Часть 8. Работа с прерываниями
При начале работы с контроллерами, принято мигать светодиодами. Я нарушу эту традицию.
Во-первых, это банально надоело. Во-вторых, светодиоды потребляют слишком большой ток. Не спешите думать, что я экономлю каждый милливатт, просто по ходу работ, нам эта экономия будет крайне важна. Опять же, то, что мы увидим ниже — на светодиодах увидеть практически невозможно.
Итак, пока нет генератора проектов, берём проект по-умолчанию для своей макетной платы и своего любимого компилятора (я взял ...\maksRTOS\Compilers\STM32F4xx\MDK-ARM 5\Projects\Default) и копируем его под иным именем (у меня получилось ...\maksRTOS\Compilers\STM32F4xx\MDK-ARM 5\Projects\Test1) . Также следует снять со всех файлов атрибут «Только для чтения».
Каталог файлов проекта весьма спартанский.
DefaultApp.cpp
DefaultApp.h
main.cpp
MaksConfig.h
Файл main.cpp относится к каноническому примеру, файлы DefaultApp.cpp и DefaultApp.h описывают пустой класс-наследник от Application. Файл MaksConfig.h мы будем использовать для изменения опций системы.
Если открыть проект, то окажется, что к нему подключено огромное количество файлов операционной системы.
В свойствах проекта также имеется бешеное количество настроек.
Так что не стоит даже надеяться создать проект «с нуля». Придётся смириться с тем, что его надо или копировать из пустого проекта по умолчанию, или создавать при помощи автоматических утилит.
Для дальнейшего изложения, я разрываюсь между «правильно» и «читаемо». Дело в том, что правильно — это начать создавать файлы для задач, причём — отдельно заголовочный файл, отдельно — файл с кодом. Однако, читатель запутается в том, что автор натворит. Такой подход хорош при создании видеоуроков. Поэтому я пойду другим путём — начну добавлять новые классы в файл DefaultApp.h. Это в корне неверно при практической работе, но зато код получится более-менее читаемым в документе.
Итак. Мы не будем мигать светодиодами. Мы будем изменять состояние пары выводов контроллера, а результаты наблюдать — на осциллографе.
Сделаем класс задачи, которая занимается этим шевелением. Драйверы мы использовать пока не умеем, поэтому будем обращаться к портам по-старинке. Выберем пару свободных портов на плате. Пусть это будут PE2 и PE3. Что они свободны, я вывел из следующей таблицы, содержащейся в описании платы STM32F429-DISCO:
Сначала сделаем класс, шевелящий ножкой PE2, потом — переделаем его на шаблонный вид.
Идём в файл DefaultApp.h (как мы помним, это неправильно для реальной работы, но зато наглядно для текста) и создаём класс-наследник от Task. Что туда нужно добавить? Правильно, конструктор и функцию Execute(). Прекрасно, пишем (первая и последняя строки оставлены, как реперные, чтобы было ясно, куда именно пишем):
#include "maksRTOS.h"
class Blinker : public Task
{
public:
Blinker (const char * name = nullptr) : Task (name){}
virtual void Execute()
{
while (true)
{
GPIOE->BSRR = (1<<2);
GPIOE->BSRR = (1<<(2+16));
}
}
};
class DefaultApp : public Application
Задача, дёргающая PE2 готов. Но теперь надо
- Включить тактирование порта E;
- Подключить задачу к планировщику.
Где это удобнее всего делать? Правильно, мы уже знаем, что это удобнее всего делать в функции
void DefaultApp::Initialize()
благо заготовка уже имеется. Пишем что-то, вроде этого:
void DefaultApp::Initialize()
{
/* Начните код приложения здесь */
// Включили тактирование порта E
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN;
// Линии PE2 и PE3 сделали выходами
GPIOE->MODER = GPIO_MODER_MODER2_0 | GPIO_MODER_MODER3_0;
// Подключили поток к планировщику
Task::Add (new Blinker ("Blink_PE2"));
}
Шьём в в пла… Ой, а в проекте по умолчанию используется симулятор.
Хорошо, переключаемся на JTAG адаптер (в случае платы STM32F429-DISCO — на ST-Link).
Теперь всё можно залить в плату. Заливаем, подключаем осциллограф к линии PE2, наблюдаем…
Красота! Только быстродействие какое-то низковатое.
Заменяем оптимизацию с уровня 0 на уровень 3
И… Всё перестаёт работать вообще
Пытаемя трассировать — по шагам прекрасно работает. Что за чудо? Ну, это не проблемы ОС, это проблемы микроконтроллера, ему не нравятся рядом стоящие команды записи в порт. Разгадка этого эффекта – в настройках тока выходных транзисторов. Выше ток – больше звон, но выше быстродействие. По умолчанию, все выходы настроены на минимальном быстродействии. А у нас оптимизатор всё хорошо умял:
0x08004092 6182 STR r2,[r0,#0x18]
0x08004094 6181 STR r1,[r0,#0x18]
0x08004096 E7FC B 0x08004092
Можно, конечно, поднять быстродействие выхода на этапе настройки порта
// Максимальный ток выходных транзисторов
GPIOE->OSPEEDR |= (3<<(2*2))|(3<<(3*2));
Но у сигнала будет явно неправильная скважность (Вверх, затем – вниз, затем – задержка на переход). Для улучшения сигнала поправим код основного цикла следующим образом:
то же самое текстом
virtual void Execute()
{
while (true)
{
GPIOE->BSRR = (1<<2);
asm {nop}
GPIOE->BSRR = (1<<(2+16));
}
}
};
Здесь получается вверх, затем – задержка на NOP, затем – вниз, затем – задержка на переход, что обеспечивает скважность, близкую к 50% (в комментариях ниже было точно вычислено, что реально 3 такта в единице и 5 тактов в нуле, но это ближе к 50%, чем 1 к 5, да и на имеющемся осциллографе разницу в верхней и нижней частях импульсов всё равно практически невозможно заметить). И быстродействия выхода уже хватает даже в малошумящем режиме. Частота выходного сигнала стала 168/(3+5) = 21 МГц.
Правда, на другом масштабе нет-нет, да и проскочат вот такие чёрные провалы
Это мы наблюдаем работу планировщика. Задача у нас одна, но периодически у неё отбирают управление, чтобы проверить, нельзя ли передать его кому-то другому. При наличии отсутствия других задач, управление возвращается той единственной, которая есть. Что ж, добавляем вторую задачу, которая будет дёргать PE3. Поместим номер бита в переменную-член класса, а настраивать его будем через конструктор
Текстом
class Blinker : public Task
{
int m_nBit;
public:
Blinker (int nBit,const char * name = nullptr) : Task (name),m_nBit(nBit){}
virtual void Execute()
{
while (true)
{
GPIOE->BSRR = (1<<m_nBit);
GPIOE->BSRR = (1<<(m_nBit+16));
}
}
};
А добавление задач в планировщик — вот так:
Текстом
void DefaultApp::Initialize()
{
/* Начните код приложения здесь */
// Включили тактирование порта E
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN;
// Линии PE2 и PE3 сделали выходами
GPIOE->MODER = GPIO_MODER_MODER2_0 | GPIO_MODER_MODER3_0;
// Подключили поток к планировщику
Task::Add (new Blinker (2,"Blink_PE2"));
Task::Add (new Blinker (3,"Blink_PE3"));
}
Подключаем второй канал осциллографа к выводу PE3. Теперь иногда идут импульсы на одном канале
Ой какая частота низкая… Нет, фальстарт. Перепишем задачу на шаблонах…
Текстом
template <int nBit>
class Blinker : public Task
{
public:
Blinker (const char * name = nullptr) : Task (name){}
virtual void Execute()
{
while (true)
{
GPIOE->BSRR = (1<<nBit);
asm {nop}
GPIOE->BSRR = (1<<(nBit+16));
}
}
};
И её постановку на планирование — вот так:
Текстом
Task::Add (new Blinker<2> ("Blink_PE2"));
Task::Add (new Blinker<3> ("Blink_PE3"));
Итак. Теперь иногда импульсы (с правильной частотой) идут на одном канале:
А иногда — на другом
Можно выбрать масштаб, на котором видны кванты времени. Заодно произведём замер и убедимся, что один квант действительно равен одной миллисекунде (с точностью до масштаба экрана осциллографа)
Можно убедиться, что планировщику всё так же нужно время для переключения задач (причём больше, чем в те времена, когда задача была одна)
Теперь давайте рассмотрим работу потоков с разными приоритетами. Добавим забавную задачу, которая «то потухнет, то погаснет»
public:
Funny (const char * name = nullptr) : Task (name){}
virtual void Execute()
{
while (true)
{
Delay (5);
CpuDelay (5);
}
}
};
И добавим её в планировщик с более высоким приоритетом
Текстом
Task::Add (new Blinker<2> ("Blink_PE2"));
Task::Add (new Blinker<3> ("Blink_PE3"));
Task::Add (new Funny ("FunnyTask"),Task::PriorityHigh);
Эта задача половину времени выполняет задержку без переключения контекста. Так как её приоритет выше остальных, то управление не будет передано никому другому. Половину времени задача спит. То есть, находится в заблокированном состоянии. То есть, в это время будут работать потоки с нормальным приоритетом. Проверим?
Собственно, что и требовалось доказать. Пауза равна пяти миллисекундам (выделено курсорами), а во время работы нормальных задач, контекст успевает 5 раз переключиться между ними. Вот другой масштаб, чтобы было видно, что это не случайность, а статистика
Убираем работу этой ужасной задачи. Продолжать будем с двумя основными
Task::Add (new Blinker<2> («Blink_PE2»));
Task::Add (new Blinker<3> («Blink_PE3»));
Наконец, переведём дёрганье порта из режима «Совсем дёрганный» в более реальный. До светодиодного доводить не будем. Скажем, сделаем период величиной в 10 миллисекунд
Текстом
class Blinker : public Task
{
public:
Blinker (const char * name = nullptr) : Task (name){}
virtual void Execute()
{
while (true)
{
GPIOE->BSRR = (1<<nBit);
Delay (5);
GPIOE->BSRR = (1<<(nBit+16));
Delay (5);
}
}
};
Тепрерь подключаем амперметр. Для платы STM32F429-DISCO надо снять перемычку JP3 и включить прибор вместо неё, о чём сказано в документации:
Измеряем ток, потребляемый данным вариантом программы
Идём в файл MaksConfig.h и добавляем туда строку:
#define MAKS_SLEEP_ON_IDLE 1
Собираем проект, «прошиваем» результат в плату, смотрим на амперметр:
Таааак, ещё одну теоретическую вещь проверили на практике. Тоже работает. А вот если бы мы мигали светодиодом, то он бы то потреблял, то не потреблял 10 мА, что на фоне измеренных значений — вполне существенно.
Ну, и напоследок заменим многозадачность на кооперативную. Для этого добавим конструктор к классу приложения
Текстом
class DefaultApp : public Application
{
public:
DefaultApp() : Application (false){}
private:
virtual void Initialize();
};
И сделаем так, чтобы задачи после трёх импульсов в порт передавали друг другу управление. Также добавим задержки, чтобы на осциллографе задержка планировщика не уводила бы изображение другой задачи за экран.
Текстом
template <int nBit>
class Blinker : public Task
{
public:
Blinker (const char * name = nullptr) : Task (name){}
virtual void Execute()
{
while (true)
{
for (int i=0;i<3;i++)
{
GPIOE->BSRR = (1<<nBit);
CpuDelay (1);
GPIOE->BSRR = (1<<(nBit+16));
CpuDelay (1);
}
Yield();
}
}
};
Что там на осциллографе?
Собственно, мы хотели тройки импульсов — мы их получили.
Ну, и наконец, добавим виртуальную функцию
Текстом
class DefaultApp : public Application
{
public:
DefaultApp() : Application (true){}
virtual ALARM_ACTION OnAlarm(ALARM_REASON reason)
{
while (true)
{
volatile ALARM_REASON r = reason;
}
}
private:
virtual void Initialize();
};
и попробуем вызвать какую-либо проблему. Например, создадим критическую секцию в задаче с обычным уровнем привилегий.
Blinker (const char * name = nullptr) : Task (name){}
virtual void Execute()
{
CriticalSection cs;
while (true)
{
GPIOE->BSRR = (1<<nBit);
Delay (5);
GPIOE->BSRR = (1<<(nBit+16));
Delay (5);
}
Запускаем проект на отладку, ставим точку останова на следующую строку
после чего запускаем на исполнение (F5). Моментально получаем останов (если не сработало — щёлкаем по пиктограмме «Stop»).
В строке, на которой произошёл останов, наводим курсор на переменную reason. Получаем следующий результат:
Ну что же, проверку первых основных теоретических выкладок мы завершили, можно переходить к следующему большому сложному разделу.