Top.Mail.Ru

STM32 с нуля. FreeRTOS. Типы многозадачности, пример программы.

Как и обещал, сейчас попробуем реализовать что-нибудь посложнее мигания диодами на базе FreeRTOS. Но сначала немного теории, которая нам понадобится для понимания сути работы ОСРВ.

Помните, мы говорили о многозадачности операционных систем реального времени? Так вот, существуют три разных типа многозадачности. Первый из них мы использовали в предыдущей статье – это вытесняющая многозадачность. Что же это такое и кого она вытесняет?

Этот тип многозадачности означает, что готовая задача с высоким приоритетом перекрывает, а точнее вытесняет задачу с более низким. Время при вытесняющей многозадачности делится на равные промежутки – кванты, и вызов планировщика происходит по истечению кванта времени. Например, по умолчанию квант времени равен 1 мс, значит, планировщик будет вызываться каждую миллисекунду и передавать управление той или иной задаче (в зависимости от приоритета и готовности задачи). Соответственно, в настройках можно задать другое значение кванта времени. Фиксируем эти основные принципы вытесняющей многозадачности, на очереди следующий тип – многозадачность кооперативная.

Здесь уже планировщик не может вклиниться в выполнение задачи. Каждая задача должна сама передавать ему управление. То есть в конце кода задачи мы должны явно вызвать планировщик при помощи функции taskYIELD().

И, наконец, третий тип многозадачности – гибридная. Ну, тут по названию уже понятно, что она объединяет предыдущие два типа. Планировщик вызывается каждый квант времени (привет от вытесняющей многозадачности), но программист также может вызвать его принудительно, как в кооперативной многозадачности.

Вот так кратко и надеюсь понятно получилось ) Типы многозадачности рассмотрели, идем дальше...

В рамках данной статьи я также хочу рассказать о механизме обмена данными между задачами. Для этого в FreeRTOS используются очереди. Сразу же возникает вопрос, зачем какие-то очереди нужны, если можно просто объявить глобальную переменную и использовать ее как угодно и где угодно. Казалось бы, справедливое замечание, но использование такой переменной на деле оказывается небезопасным. Давайте посмотрим на примере.

Пусть есть глобальная переменная glVariable = 100. Задача 1 делает следующее:

glVariable = 0;
delay(100);
glVariable = 100;

То есть обнуляет переменную, немного ожидает и снова делает ее равной 100. Казалось бы, все хорошо, но что будет, если планировщик отдаст управление Задаче 2 в тот момент, когда Задача 1 еще не восстановила значение глобальной переменной. Получается, что задача 2 вместо значения 100 получила значение 0, а дальше уже последствия могут быть непредсказуемыми.

Чтобы вы оценили масштаб трагедии, вот еще пример ) На столе стоит стакан воды, вдруг бодренько заходит Задача 1 и выпивает его. Прежде чем заполнить его снова она решает посидеть, подождать. А в этот момент заходит Задача 2, берет стакан, а он пуст... Неприятно. Вот поэтому и применяется механизм очередей.

Очередь представляет собой набор элементов определенного размера. В качестве элемента может выступать любая переменная C (char, int и т. д.). При записи элемента в очередь создается его побайтовая копия. Аналогично и при чтении. Очередь в ОСРВ, как и в жизни, базируется на принципе "первым вошел - первым вышел". То есть последний записанный в очередь элемент последним будет и прочитан. Все справедливо.

В нашей программе мы будем создавать очередь и работать с ней, поэтому рассмотрим основные функции, использующиеся для этого.

Создается очередь функцией:

xQueueHandle xQueueCreate (unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxIemSize);

Функция принимает аргументы:

  • uxQueueLength – размер очереди
  • uxItemSize – размер элемента очереди

и возвращает дескриптор очереди:

  • Null – если очередь не создана
  • Не Null – если очередь создана

При удачном создании очереди возвращаемое значение должно быть сохранено в переменной типа xQueueHandle. Создали, а дальше то что?

А вот:

portBASE_TYPE xQueueSend(xQueueHandle xQueue, const void * pvIiemToQueue, portTickType xTicksToWait);

Функция записи в очередь и ее аргументы:

  • xQueue – дескриптор очереди
  • pvItemToQueue – указатель на элемент, который будет помещен в очередь
  • xTicksToWait - максимальное количество квантов времени, в течение которого задача может пребывать в блокированном состоянии, если очередь полна, и записать новый элемент невозможно

Если что-то непонятно - не страшно - в примере все протестируем наглядно. А теперь функция чтения из очереди:

portBASE_TYPE xQueueReceive(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);

Аргументы такие же, как и в функции записи, за исключением pvBuffer – это указатель на область памяти, куда будет считан элемент из очереди. Как функция записи, так и функция чтения могут возвращать два значения:

  • pdPASS – успешное завершение
  • errQUEUE_EMPTY – провал

Вот в принципе и все, что касается теории.

Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также статья на смежную тему из нового курса: STM32CubeMx. Быстрый старт с FreeRTOS для STM32.

Попробуем написать программу, в которой максимально используем то, о чем сейчас узнали. Создадим две задачи, одна будет запускать преобразование АЦП и сравнивать результат с порогом. Если напряжение на входе больше порога, задача кладет в очередь посылку "ОК", а если порог не превышен – посылку "NO". Вторая задача будет читать данные из очереди и отправлять их по USART.

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

Понадобится переменная для обращения к нашей очереди, ну и еще пара переменных:

GPIO_InitTypeDef port;
USART_InitTypeDef usart;
ADC_InitTypeDef adc;
xQueueHandle xDataQueue;
uint8_t usartData[2];
uint8_t sendData[2];
uint8_t usartCounter;
uint16_t data;

Далее включаем тактирование всего, что нам понадобится:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

Инициализируем ножки, которые будут нашими Rx и Tx:

GPIO_StructInit(&port);
port.GPIO_Mode = GPIO_Mode_AF_PP;
port.GPIO_Pin = GPIO_Pin_9;
port.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &port);

port.GPIO_Mode = GPIO_Mode_AF_PP;
port.GPIO_Pin = GPIO_Pin_10;
port.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &port);

Также нам понадобится первый канал модуля ADC1:

port.GPIO_Mode = GPIO_Mode_AF_PP;
port.GPIO_Pin = GPIO_Pin_0;
port.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &port);

Настраиваем режим работы АЦП:

ADC_StructInit(&adc);

// Нам нужен непрерывный режим
adc.ADC_ContinuousConvMode = ENABLE;

// Режим запуска – установка бита SWSTART
adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_Init(ADC1, &adc);

Не забываем настроить еще и USART:

USART_StructInit(&usart);
usart.USART_BaudRate = BAUDRATE;
USART_Init(USART1, &usart);

BAUDRATE объявлена у меня ранее:

#define BAUDRATE                                                 9600

Пришло время написать код для двух задач – напоминаю, одна работает с АЦП, вторая с USART:

void vADCTask (void *pvParameters)
{
	while(1)
	{
		// АЦП у нас фигачит непрерывно и постоянно, так что надо только забирать данные )
		data = ADC_GetConversionValue(ADC1);
		
		// Если напряжение больше порога – шлем "OK", иначе – "NO"
		if (data > 0x9B2)
		{
			sendData[0] = 'O';
			sendData[1] = 'K';
		}
		else
		{
			 sendData[0] = 'N';
			 sendData[1] = 'O';
		}
		
		// Отправляем данные в очередь.
		// xDataQueue – имя очереди
		xQueueSend(xDataQueue, &sendData[0], 0);
	}
	
	vTaskDelete(NULL);
}

Половина дела сделана! Теперь реализация второй функции:

void vUSARTTask(void *pvParameters)
{
	while(1)
	{
		// Забираем данные из очереди и сохраняем их 
		// в массив usartData[]
		xQueueReceive(xDataQueue, &usartData[0], 0);
		
		// Счетчик отправленных байт – в ноль
		usartCounter = 0;
		
		// Всего передаем два байта
		while(usartCounter < 2)
		{ 
			// Кладем данные в регистр данных USART, ждем, пока отправится, увеличиваем счетчик и шлем следующий байт
			USART_SendData(USART1, usartData[usartCounter]);
			while(!USART_GetFlagStatus(USART1, USART_FLAG_TC)); 
			usartCounter++;
		}
	}
	
	vTaskDelete(NULL);
}

Осталось связать все это:

int main()
{
	vFreeRTOSInitAll();
	
	// Включаем USART
	USART_Cmd(USART1, ENABLE);
	ADC_Cmd(ADC1, ENABLE);
	
	// Запускаем АЦП
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
	// Создаем очередь с дескриптором xDataQueue
	xDataQueue = xQueueCreate( 2, sizeof(char *));
	xTaskCreate(vADCTask, (signed char*)”ADCTask”, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
	xTaskCreate(vUSARTTask, (signed char*)”USARTTask”, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
	vTaskStartScheduler();
}

Ну вот и все, давайте попробуем скомпилировать и запустить программу в отладчике. Для эмуляции сигнала на входе АЦП в командной строке отладчика напишем (об этом можно почитать тут):

ADC1_IN0 = 2.5

Сейчас на входе будет 2.5 В. Пишем разные команды в командной строке и смотрим, что получается:

FreeRTOS и вытесняющая многозадачность.

В USART идут данные, меняющиеся в зависимости от значения аналогового напряжения на входе АЦП. На самом деле нам повезло, что результат получился таким. И скоро мы разберемся, в чем же проблема этой программы...

Но в традициях лучших сериалов – "об этом в следующей серии" - то есть в следующей статье )

Подписаться
Уведомить о
guest

7 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Алиса Алексеева
11 лет назад

а почему планировщик может отдать управление другой задаче? только ли из-за переменной? или есть еще что-то?

Алиса Алексеева
Ответ на комментарий  Aveal
11 лет назад

а как узнать причину сбоя? есть специальная диагностика? или что там?

Nemo
Nemo
9 лет назад

Написал так:
void prvSend( void *pvParameters )
{
for(;;)
{
xQueueSend(xDataQueue, &TestPacket[0], 0);
vTaskDelay (1000);
}
}
void vUSARTTask(void *pvParameters)
{
uint8_t usartData[18];
for(;;)
{
xQueueReceive(xDataQueue, &usartData[0], 0);
TransceiverSendPacket(200, usartData , 18);
}
vTaskDelete(NULL);
}

каждую секунду кладу дание в очередь, потом извлекаю.Осцилографом смотрю таск постоянно шлёт дание на уарт(очень много,а не 18 байт). Я думал что пока очередь пустая, таск vUSARTTask спит, и просипаеться только тогда когда появляються дание. Если таск спит, когда нет даных я должен видить на осцилографе байты с интервалом 1с.Так должно быть или у меня ошибка? Спасибо!

aa
aa
9 лет назад

не контролируется что данные забраны из очереди... Так что даже если она пуста вы шлете прошлые данные..

Вадим
Вадим
9 лет назад

У меня в терминал приходит мусор при попытки отсылать данные в FreeRTOS:
[00][00][00]n[00]n[00][00][00]n[00][00][00][00][00]n[00

Для теста делаю так:
void usart_task(void *pvParameters) {
while(1) {
vTaskDelay(500);
USART_SendData(USART1, 'n');
while(!USART_GetFlagStatus(USART1, USART_FLAG_TC));
}
}

7
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x