+7 (812) 494-9090
Обратная связьEnglish
Главная → Статьи → Системное ПО → Книга знаний ОСРВ МАКС. Статья 5. Первое приложение
Версия для печати

Книга знаний ОСРВ МАКС. Статья 5. Первое приложение

26 сентября 2017

Один из разработчиков нашей операционной системы реального времени МАКС написал серию статей о ее создании и особенностях. Получилась своеобразная «Книга знаний», неформальное руководство программиста.

В первой статье мы рассказали об отличии ОС от ОСРВ, представили архитектуру ОСРВ МАКС и ее специфику. Вторая статья была посвящена ядру системы и приоритету задач, третья — структуре простейшей программы, в четвертой мы представили необходимую теорию, а в настоящей статье мы приступим к практике. Так же статья размещена на Хабре.




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

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

Итак, пока нет генератора проектов, берём проект по умолчанию для своей макетной платы и своего любимого компилятора (я взял ...\maksRTOS\Compilers\STM32F4xx\MDK-ARM 5\Projects\Default) и копируем его под иным именем (у меня получилось ...\maksRTOS\Compilers\STM32F4xx\MDK-ARM 5\Projects\Test1) . Также следует снять со всех файлов атрибут «Только для чтения».


Книга знаний ОСРВ МАКС. Статья 5.


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

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


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

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


Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.


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

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

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

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



Книга знаний ОСРВ МАКС. Статья 5.



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

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

Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.

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

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


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

Книга знаний ОСРВ МАКС. Статья 5.


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


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


Можно, конечно, поднять быстродействие выхода на этапе настройки порта

// Максимальный ток выходных транзисторов
GPIOE->OSPEEDR |= (3<<(2*2))|(3<<(3*2));

Но у сигнала будет явно неправильная скважность (Вверх, затем – вниз, затем – задержка на переход). Для улучшения сигнала поправим код основного цикла следующим образом:


Книга знаний ОСРВ МАКС. Статья 5


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 МГц.


Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.


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

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

Книга знаний ОСРВ МАКС. Статья 5.

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

Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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

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

Книга знаний ОСРВ МАКС. Статья 5.

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

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

Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.

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


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

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

Книга знаний ОСРВ МАКС. Статья 5.
	Task::Add (new Blinker<2> ("Blink_PE2"));
	Task::Add (new Blinker<3> ("Blink_PE3"));
	Task::Add (new Funny ("FunnyTask"),Task::PriorityHigh);

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


Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.


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

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


Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.

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

Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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

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


Книга знаний ОСРВ МАКС. Статья 5.


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


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


Книга знаний ОСРВ МАКС. Статья 5.


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

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

Книга знаний ОСРВ МАКС. Статья 5.


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

Книга знаний ОСРВ МАКС. Статья 5.


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

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

Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.


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


Книга знаний ОСРВ МАКС. Статья 5.

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


Теги: ОСРВ, rtos, осрв, микроконтроллеры