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

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

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

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

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

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

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

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

Пусть есть глобальная переменная 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 – провал операции

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

Попробуем написать программу, в которой максимально используем то, о чем сейчас узнали. Создадим две задачи, одна будет запускать преобразование АЦП и сравнивать результат с порогом. Если напряжение на входе больше порога, задача пихает в очередь посылку «ОК», а если порог не превышен – посылку «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 В. Пишем разные команды в командной строке и смотрим, что получится =)

Видим примерно следующее:
Работа программы
В USART идут данные, меняющиеся в зависимости от значения аналогового напряжения на входе АЦП. На самом деле нам повезло, что результат получился таким. Но в традициях лучших сериалов – «об этом в следующей серии» — то есть в следующей статье )

Понравилась статья? Поделись с друзьями!

STM32 с нуля. FreeRTOS. Типы многозадачности, пример программы.: 7 комментариев
  1. Написал так:
    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с.Так должно быть или у меня ошибка? Спасибо!

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

  3. У меня в терминал приходит мусор при попытки отсылать данные в 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));
    }
    }

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *