- PVSM.RU - https://www.pvsm.ru -

Обзор одной российской RTOS, часть 5. Первое приложение

Готова очередная публикация обзора особенностей ОСРВ МАКС. В предыдущей статье мы разбирались с теорией, а сегодня наступило время практики.

Часть 1. Общие сведения [1]
Часть 2. Ядро ОСРВ МАКС [2]
Часть 3. Структура простейшей программы [3]
Часть 4. Полезная теория [4]
Часть 5. Первое приложение (настоящая статья)
Часть 6. Средства синхронизации потоков
Часть 7. Средства обмена данными между задачами
Часть 8. Работа с прерываниями

При начале работы с контроллерами, принято мигать светодиодами. Я нарушу эту традицию.

Во-первых, это банально надоело. Во-вторых, светодиоды потребляют слишком большой ток. Не спешите думать, что я экономлю каждый милливатт, просто по ходу работ, нам эта экономия будет крайне важна. Опять же, то, что мы увидим ниже — на светодиодах увидеть практически невозможно.

Итак, пока нет генератора проектов, берём проект по-умолчанию для своей макетной платы и своего любимого компилятора (я взял ...maksRTOSCompilersSTM32F4xxMDK-ARM 5ProjectsDefault) и копируем его под иным именем (у меня получилось ...maksRTOSCompilersSTM32F4xxMDK-ARM 5ProjectsTest1) . Также следует снять со всех файлов атрибут «Только для чтения».

image

Каталог файлов проекта весьма спартанский.

DefaultApp.cpp
DefaultApp.h
main.cpp
MaksConfig.h

Файл main.cpp относится к каноническому примеру, файлы DefaultApp.cpp и DefaultApp.h описывают пустой класс-наследник от Application. Файл MaksConfig.h мы будем использовать для изменения опций системы.

Если открыть проект, то окажется, что к нему подключено огромное количество файлов операционной системы.

image

В свойствах проекта также имеется бешеное количество настроек.

image

Так что не стоит даже надеяться создать проект «с нуля». Придётся смириться с тем, что его надо или копировать из пустого проекта по умолчанию, или создавать при помощи автоматических утилит.

Для дальнейшего изложения, я разрываюсь между «правильно» и «читаемо». Дело в том, что правильно — это начать создавать файлы для задач, причём — отдельно заголовочный файл, отдельно — файл с кодом. Однако, читатель запутается в том, что автор натворит. Такой подход хорош при создании видеоуроков. Поэтому я пойду другим путём — начну добавлять новые классы в файл DefaultApp.h. Это в корне неверно при практической работе, но зато код получится более-менее читаемым в документе.

Итак. Мы не будем мигать светодиодами. Мы будем изменять состояние пары выводов контроллера, а результаты наблюдать — на осциллографе.

Сделаем класс задачи, которая занимается этим шевелением. Драйверы мы использовать пока не умеем, поэтому будем обращаться к портам по-старинке. Выберем пару свободных портов на плате. Пусть это будут PE2 и PE3. Что они свободны, я вывел из следующей таблицы, содержащейся в описании платы STM32F429-DISCO:

image

Сначала сделаем класс, шевелящий ножкой 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"));
	
}

Шьём в в пла… Ой, а в проекте по умолчанию используется симулятор.

image

Хорошо, переключаемся на JTAG адаптер (в случае платы STM32F429-DISCO — на ST-Link).

image

Теперь всё можно залить в плату. Заливаем, подключаем осциллограф к линии PE2, наблюдаем…

image

Красота! Только быстродействие какое-то низковатое.

image

Заменяем оптимизацию с уровня 0 на уровень 3

image

И… Всё перестаёт работать вообще

image

Пытаемя трассировать — по шагам прекрасно работает. Что за чудо? Ну, это не проблемы ОС, это проблемы микроконтроллера, ему не нравятся рядом стоящие команды записи в порт. Разгадка этого эффекта – в настройках тока выходных транзисторов. Выше ток – больше звон, но выше быстродействие. По умолчанию, все выходы настроены на минимальном быстродействии. А у нас оптимизатор всё хорошо умял:

0x08004092 6182 STR r2,[r0,#0x18]
0x08004094 6181 STR r1,[r0,#0x18]
0x08004096 E7FC B 0x08004092

Можно, конечно, поднять быстродействие выхода, но там появится неправильная скважность (Вверх, затем – вниз, затем – задержка на переход), поэтому просто поправим код следующим образом:

Обзор одной российской RTOS, часть 5. Первое приложение - 11

то же самое текстом

virtual void Execute()
	{
		while (true)
		{
			GPIOE->BSRR = (1<<2);
			asm {nop}
			GPIOE->BSRR = (1<<(2+16));
		}
	}
};

Здесь получается вверх, затем – задержка на NOP, затем – вниз, затем – задержка на переход, что обеспечивает скважность 50% (возможно — не ровно 50%, а близко, но на имеющемся осциллографе этого не заметить). И быстродействия выхода уже хватает и в малошумящем режиме. Частота выходного сигнала стала 21 МГц.

image

Правда, на другом масштабе нет-нет, да и проскочат вот такие чёрные провалы

image

Это мы наблюдаем работу планировщика. Задача у нас одна, но периодически у неё отбирают управление, чтобы проверить, нельзя ли передать его кому-то другому. При наличии отсутствия других задач, управление возвращается той единственной, которая есть. Что ж, добавляем вторую задачу, которая будет дёргать PE3. Поместим номер бита в переменную-член класса, а настраивать его будем через конструктор

Обзор одной российской RTOS, часть 5. Первое приложение - 14

Текстом

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));
		}
	}
};

А добавление задач в планировщик — вот так:

Обзор одной российской RTOS, часть 5. Первое приложение - 15

Текстом

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. Теперь иногда идут импульсы на одном канале

image

Ой какая частота низкая… Нет, фальстарт. Перепишем задачу на шаблонах…

Обзор одной российской RTOS, часть 5. Первое приложение - 17

Текстом

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));
		}
	}
};

И её постановку на планирование — вот так:

Обзор одной российской RTOS, часть 5. Первое приложение - 18

Текстом

	Task::Add (new Blinker<2> ("Blink_PE2"));
	Task::Add (new Blinker<3> ("Blink_PE3"));

Итак. Теперь иногда импульсы (с правильной частотой) идут на одном канале:

image

А иногда — на другом

image

Можно выбрать масштаб, на котором видны кванты времени. Заодно произведём замер и убедимся, что один квант действительно равен одной миллисекунде (с точностью до масштаба экрана осциллографа)

image

Можно убедиться, что планировщику всё так же нужно время для переключения задач (причём больше, чем в те времена, когда задача была одна)

image

Теперь давайте рассмотрим работу потоков с разными приоритетами. Добавим забавную задачу, которая «то потухнет, то погаснет»

public:
	Funny (const char * name = nullptr) : Task (name){}
	virtual void Execute()
	{
		while (true)
		{
			Delay (5);
			CpuDelay (5);
		}
	}
};

И добавим её в планировщик с более высоким приоритетом

Обзор одной российской RTOS, часть 5. Первое приложение - 23

Текстом

	Task::Add (new Blinker<2> ("Blink_PE2"));
	Task::Add (new Blinker<3> ("Blink_PE3"));
	Task::Add (new Funny ("FunnyTask"),Task::PriorityHigh);

Эта задача половину времени выполняет задержку без переключения контекста. Так как её приоритет выше остальных, то управление не будет передано никому другому. Половину времени задача спит. То есть, находится в заблокированном состоянии. То есть, в это время будут работать потоки с нормальным приоритетом. Проверим?

image

Собственно, что и требовалось доказать. Пауза равна пяти миллисекундам (выделено курсорами), а во время работы нормальных задач, контекст успевает 5 раз переключиться между ними. Вот другой масштаб, чтобы было видно, что это не случайность, а статистика

image

Убираем работу этой ужасной задачи. Продолжать будем с двумя основными
Task::Add (new Blinker<2> («Blink_PE2»));
Task::Add (new Blinker<3> («Blink_PE3»));

Наконец, переведём дёрганье порта из режима «Совсем дёрганный» в более реальный. До светодиодного доводить не будем. Скажем, сделаем период величиной в 10 миллисекунд

Обзор одной российской RTOS, часть 5. Первое приложение - 26

Текстом

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 и включить прибор вместо неё, о чём сказано в документации:

image

Измеряем ток, потребляемый данным вариантом программы

image

Идём в файл MaksConfig.h и добавляем туда строку:
#define MAKS_SLEEP_ON_IDLE 1

image

Собираем проект, «прошиваем» результат в плату, смотрим на амперметр:

image

Таааак, ещё одну теоретическую вещь проверили на практике. Тоже работает. А вот если бы мы мигали светодиодом, то он бы то потреблял, то не потреблял 10 мА, что на фоне измеренных значений — вполне существенно.

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

Обзор одной российской RTOS, часть 5. Первое приложение - 31

Текстом

class DefaultApp : public Application
{
public:
	DefaultApp() : Application (false){}
private:
	virtual void Initialize();
};

И сделаем так, чтобы задачи после трёх импульсов в порт передавали друг другу управление. Также добавим задержки, чтобы на осциллографе задержка планировщика не уводила бы изображение другой задачи за экран.

Обзор одной российской RTOS, часть 5. Первое приложение - 32

Текстом

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();
		}
	}
};

Что там на осциллографе?

image

Собственно, мы хотели тройки импульсов — мы их получили.
Ну, и наконец, добавим виртуальную функцию

Обзор одной российской RTOS, часть 5. Первое приложение - 34

Текстом

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);
		}

Запускаем проект на отладку, ставим точку останова на следующую строку

Обзор одной российской RTOS, часть 5. Первое приложение - 35

после чего запускаем на исполнение (F5). Моментально получаем останов (если не сработало — щёлкаем по пиктограмме «Stop»).

Обзор одной российской RTOS, часть 5. Первое приложение - 36

В строке, на которой произошёл останов, наводим курсор на переменную reason. Получаем следующий результат:

Обзор одной российской RTOS, часть 5. Первое приложение - 37

Ну что же, проверку первых основных теоретических выкладок мы завершили, можно переходить к следующему большому сложному разделу.

Автор: EasyLy

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie-mikrokontrollerov/264213

Ссылки в тексте:

[1] Часть 1. Общие сведения: https://habrahabr.ru/post/336308/

[2] Часть 2. Ядро ОСРВ МАКС: https://habrahabr.ru/post/336696/

[3] Часть 3. Структура простейшей программы: https://habrahabr.ru/post/336944/

[4] Часть 4. Полезная теория: https://habrahabr.ru/post/337476/

[5] Источник: https://habrahabr.ru/post/337974/