Как и обещал, сейчас попробуем реализовать что-нибудь посложнее мигания диодами на базе 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 В. Пишем разные команды в командной строке и смотрим, что получается:
В USART идут данные, меняющиеся в зависимости от значения аналогового напряжения на входе АЦП. На самом деле нам повезло, что результат получился таким. И скоро мы разберемся, в чем же проблема этой программы...
Но в традициях лучших сериалов – "об этом в следующей серии" - то есть в следующей статье )
а почему планировщик может отдать управление другой задаче? только ли из-за переменной? или есть еще что-то?
Там много всяких моментов, например, разный приоритет задач и разная готовность их к выполнению
а как узнать причину сбоя? есть специальная диагностика? или что там?
Есть отладчик, там можно посмотреть по шагам как выполняется программа
Написал так:
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с.Так должно быть или у меня ошибка? Спасибо!
не контролируется что данные забраны из очереди... Так что даже если она пуста вы шлете прошлые данные..
У меня в терминал приходит мусор при попытки отсылать данные в 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));
}
}