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

в 13:03, , рубрики: ОСРВ МАКС RTOS, программирование микроконтроллеров

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

Часть 1. Общие сведения
Часть 2. Ядро ОСРВ МАКС
Часть 3. Структура простейшей программы
Часть 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js