В ответе на отклик на вакансию сайта hh.ru «Кандидат на должность Инженера программиста контроллеров» мне прислали тестовое задание. Поскольку оно не содержит персональных данных я привожу во вложении полный текст.
Не то, чтобы я никогда ничего не измерял АЦП stm32f051R8, просто использование FreeRTOS, да еще код на С++, как-то сразу напомнили детский анекдот: «Гланды,.. автогеном!»
Причем, если нет готового кода, то задача явно не на вечер! «Автоген» оставим в стороне, а вот поковыряться с FreeRTOS было самое время!
Совсем коротко:
– FreeRTOS не загружается предварительно с неиндустриальной флешки».
– При включении MCU не нужно ждать пока загрузится FreeRTOS.
– У FreeRTOS нет мелодии приветствия.
Переписывать анатомическое определение я не буду.
Достаточно подробно и интересно здесь:
https://habr.com/ru/post/415429/
FreeRTOS без CubeMX не плохо рассказан в видео уроках:
за что Илье Галкину мой респект и уважуха!
Когда-то давно STMicroelectronics предлагала файл MsExcel с макросами для формирования файла stm32f0xx_rcc.c, используемого для тактирования периферии. Потом я этот файл xls потерял, а CubeMX решающий в том числе и эту задачу занял приоритетную линию производителя. Однако в маленьких MCU не всегда удобно использовать CubeMX.
На эту тему полно примеров в сети, в том числе и в упомянутых выше видео уроках. Мне тоже пришлось коснуться этой темы в проектах, причем сразу для двух MCU stm32f051R8 & stm32L162RE. Когда рядом, видны отличия.
Что же я вынес из знакомства с FreeRTOS?
Есть общепринятая конструкция.
Понятно, что флаги, как и остальные глобальные переменные, изменяемые в прерываниях должны быть volatile.
Те, кто программировал на ассемблере, понимают, что все работает так и только так. Процессор не останавливается пока ждет изменения флага и ему без разницы крутиться в цикле или что-то считать. И уж точно не передает значение флага прям внутрь цикла while (1). Флаги можно сделать счетными и отслеживать, сколько прерываний произошло за время выполнения цикла.
Однако если «functions» будут длинными, то не всегда можно оперативно реагировать на прерывания. Для этого в том числе и придумали FreeRTOS.
Цикл while (1) будет теперь называться vTaskStartScheduler().
«Если все хорошо, то управление в main() никогда не дойдет до этой точки (
while (1)),
и теперь шедулер будет управлять задачами. Если main() довела управление до этого места (
while (1)), то это может означать, что не хватает памяти кучи (heap) для создания специальной задачи ожидания (idle task, об этой задаче далее)
»Здесь и далее выделено цветом с огромной благодарностью:
http://microsin.net/programming/arm/index/Page-4.html
Functions с картинки, теперь будет называться xTaskCreate (vTaskLed1, «LED1», 32, NULL, 0, NULL), сама задача – это обычная функция, которая выглядит ну как-то так:
void vTaskLed2 (void *argument){ while(1) { // Pin 9 Зелененький светодиод Дискавери GPIOC->ODR ^= GPIO_ODR_9; vTaskDelay(1000); } }
Задача (function с картинки) может создаваться и удаляться, ей можно менять приоритет. Планировщик, кстати, тоже можно блокировать до определенных событий. Создание большого количества задач жрет heap, а удаление и создание, ее фрагментирует.
Механизм передачи параметров (void *argument) меня особо не впечатлил, дабы параметры эти передаются только в момент создания задачи. Их можно использовать для создания копий задачи (создания одной задачи) с разными параметрами.
И… по задачам все!
Самая круть – это очереди! (Буду скромнее, осознанная мной круть).
Очередь – это не только смерть глобальных переменных (не всегда уместная). Очередь – это возможность блокировать задачу, до того события, пока в этой очереди че-нить не появится.
Опять же, возвращаясь к моей картинке с алгоритмом. (if (flag_1 == 1)). Блокировать и разблокировать задачу с большим приоритетом – это влезть прям внутрь цикла while (1) не проходя его целиком.
Итак,
USARTRMSPrint = xQueueCreate (4, sizeof (float)), где 4 – длина очереди. Классный параметр, потому что существует условие с функцией
if (uxQueueMessagesWaiting (USARTRMSPrint) == 0) {
…равно нулю, не равно нулю или равно тому, чего мы хотим. Но это еще не фантастика. Настоящая фантастика
xQueueReceive (USARTRMSPrint, &A_RMS, portMAX_DELAY);
// portMAX_DELAY — уводит задачу в блокировку, точно так же как в ожидание vTaskDelay(100);
// Что дает раздышаться другим задачам. Однако при появлении данных в очереди (всех данных см. выше)
// управление сразу передается сюда по программному прерыванию приоритета нашей этой задачи.
В моем примере наиграться с параметром очереди можно в задаче
void vTaskUSART1Print (void *argument){…portBASE_TYPE xQueueReceive (xQueueHandle xQueue,
const void * pvBuffer,
portTickType xTicksToWait);
где xTicksToWait
Максимальное количество времени, в течение которого задача должна оставаться в состоянии Blocked в ожидании, пока не появится в очереди доступный для чтения элемент данных (если очередь уже пуста).
Обе функции и xQueueReceive(), и xQueuePeek() сделают возврат немедленно, если xTicksToWait указан 0 и если очередь уже пуста.
Время блокировки указывается в периодах тика, поэтому абсолютное время ожидания зависит от частоты следования тиков. Для преобразования времени в миллисекундах во время в тиках может использоваться константа portTICK_RATE_MS.
Установка xTicksToWait в значение portMAX_DELAY приведет к тому, что задача будет ждать бесконечно (таймаута разблокировки не будет), что обеспечивается установкой в 1 значения INCLUDE_vTaskSuspend в файле FreeRTOSConfig.h.
Так вот. Если убрать в задаче vTaskDelay(1000), а поставить параметр xTicksToWait
равным
portMAX_DELAY, то задача будет выводить максимальное количество насчитанных измерений по мере их поступления, если оставить все как есть, то измерения будут усредняться и выводиться раз в секунду.
Я не использовал в проекте мьютексы, поскольку автор в зеленой ссылке рассказывает о них в части 4 (управление ресурсами), описывает недостатки решений, а затем переходит к задаче привратника с обычными очередями, что я и попытался реализовать. Возможно к мьютексам вернемся в каком-нибудь следующем проекте.
Функция xQueueSend (USARTRMSPrint, &A_RMS, 0) работает аналогично с точностью до наоборот.
Подробно все команды, параметры и примеры даны по зеленой ссылке. Все в удобной куче и тоже с примерами, правда уже по-английски лежит у разработчика:
https://www.freertos.org/a00018.html
Однако изначально в статье все же речь шла о прерываниях, а в них очереди имеют свою специфику и не только в названиях функций.
Вот пример с сайта разработчика:
void vBufferISR( void ) { char cIn; BaseType_t xHigherPriorityTaskWoken; /* We have not woken a task at the start of the ISR. */ /* Мы не разбудили задачу при запуске ISR. */ xHigherPriorityTaskWoken = pdFALSE; /* Loop until the buffer is empty. */ /* Цикл, пока буфер не станет пустым. */ do { /* Obtain a byte from the buffer. */ /* Получить байт из буфера. */ cIn = portINPUT_BYTE (RX_REGISTER_ADDRESS); /* Post the byte. */ /* Отправьте байт. */ xQueueSendFromISR (xRxQueue, &cIn, &xHigherPriorityTaskWoken); } While (portINPUT_BYTE (BUFFER_COUNT)); /* Now the buffer is empty we can switch context if necessary. */ /* Теперь буфер пуст, мы можем переключить контекст при необходимости. */ If (xHigherPriorityTaskWoken) { /* Actual macro used here is port specific. */ /* Фактический макрос, используемый здесь, зависит от порта. */ taskYIELD_FROM_ISR (); } }
Почему то вызвало вопросы две вещи.
Первая – мудреное название xHigherPriorityTaskWoken (Пробуждена задача с более высоким приоритетом). — Какая задача? Почему во всех примерах одно и тоже имя? Не имеет ли это имя какого-нибудь тайного смысла?
— Не имеет!
static portBASE_TYPE PoFig;
где
#define portBASE_TYPE long
Вторая –
/* ВНИМАНИЕ: макрос, реально используемый для переключения контекста
из ISR, зависит от конкретного порта FreeRTOS. Здесь указано имя макроса, корректное для порта Open Watcom DOS. Другие порты FreeRTOS могут использовать другой синтаксис. Для определения используемого синтаксиса обратитесь к примерам, предоставленнымвместе с портом FreeRTOS. */
portSWITCH_CONTEXT();
И там и там ссылаются на конкретный порт FreeRTOS. При условии, что нет никакого понимания куда из этой команды передается управление.
— Для STM32 правильный макрос taskYIELD_FROM_ISR ();
Во всей этой конструкции главную роль играет команда:
xQueueSendFromISR (xRxQueue, &cIn, &xHigherPriorityTaskWoken);
в которой указано имя очереди, которая держит в блокировке псевдообработчик прерывания.
Приоритет здесь в том смысле, что данные из очереди с таким именем могут ждать несколько задач. А если ждет одна задача, то она немедленно будет разблокирована, ну если не решается задача с более высоким приоритетом без этой очереди. В проекте я передаю управление и приоритетной задаче и не приоритетной.
Псевдообработчик… Забавная лексика. Видимо пошла от всеместного использования функций обратного вызова. В одном из примеров использования протокола TCP/IP глубоко уважаемый мною гуру, вызывает из функции обратного вызова функцию, которая обрабатывает все события, заголовки и проч.
Само по себе событие вызова функции из обработчика – не весело. Но если инженеры STM так сделали, то это продиктовано кучей удобств. Однако вызывая из функции обратного вызова обработчика прерывания следующую функцию, мы все равно остаемся в обработчике прерывания и приоритетом этого обработчика. Ну то есть содержимое РОНов все равно возвращается из стека сначала функции, а потом обработчика, как и счетчик адреса.
Отсюда, наверное, переключение контекста в псевдообработчик, он и не обработчик во все, но может иметь свой приоритет.
Уместно добавить, что с приоритетами у stm32f051R8 беда полная, поскольку под значение приоритета отводится всего два бита и все мои попытки изменить приоритет прерывания к значимым результатам в задачах без FreeRTOS не привели. При уменьшении (увеличении) приоритета либо переставали работать прерывания ядра и все висло, либо переставали работать сами прерывания. Хотя приоритет самой таблицы прерываний соблюдается.
В моем проекте из прерывания счета периода сигнала передаются данные значения этого периода. А в другом обработчике прерывания канала DMA по концу измерения регулярной группы каналов АЦП используется глобальный массив из 32 переменных, которые я не увидел смысла пихать в очередь. Однако управление все равно передавать нужно.
Сначала я грузил в очередь какой-нибудь ноль и все работало. Но как-то не изящно… Для этого есть двоичные семафоры. Это та же очередь, только без значений.
Двоичные семафоры после того, как разобрался с очередями из прерываний прошли как-то буднично и на них я останавливаться не хочу.
В тестовой задаче есть пункт обработки ошибок: «программа должна корректно отрабатывать ошибки». В терминологии STMicroelectronics есть такое понятие «Manage an error for robust application (Управление ошибкой для надежного приложения)». Подозреваю, что с этих позиций отдельный проект можно сосредоточить на главной задаче – обработке ошибок. С позиций «надежного приложения» уже само по себе можно выделить задачу просто посчитать возможные ошибки, а не то чтобы их обработать.
Однако в проекте можно обработать интересную ошибку с позиций FreeRTOS. Дело в том, что время преобразования каналов АЦП можно менять в зависимости от качества сигнала (видимо тока, поскольку заряжаются конденсаторы). Мы делим период на 24 отрезка, меряем амплитудное значение на отрезке (у меня 8 раз подряд), и считаем сумму квадратов всех отрезков.
В stm32f051R8 не корректно работает прерывание по половине буфера.
Экспериментам было уделено достаточное время. Грешил в том числе и на отладчик. В конце концов поставил в каждом флаге по cr1++; & cr2++; И кто больше, скажем 1000, останавливаемся. Значения переменных всегда значительно отличались. Где-то читал, что тоже не удалось решить данную проблему. Если кто-то подскажет решение, буду благодарен.
Этот факт накладывает некоторые ограничения на данную задачу.
В проекте целостность данных при прерывании DMA1->ISR & DMA_ISR_TCIF1 контролируется в псевдообработчике vTaskDMA1_Channel1_IRQ, куда мы уходим из DMA1_Channel1_IRQHandler по двоичному семафору xBinarySemaphoreDMA1_Channel1.
За отрезок времени счета TIM14 до переполнения, ADC измеряет амплитудное значение четырех каналов регулярной группы по 8 раз каждый.
Время выборки взято максимальным, но, чтобы успеть считать частоту 166 Гц. Это частота двигателя на 10 000 оборотах в минуту. Были раньше такие винчестеры. Шустрее движка я не нашел. Массив ADC1_Simpl [32] я оставил глобальным, поскольку его обработка критична по времени.
Псевдообработчик прерывания vTaskDMA1_Channel1_IRQ — задача с максимальным приоритетом, висит в блокировке до появления данных по каналу DMA и выходит из блокировки по семафору xBinarySemaphoreDMA1_Channel1. После чего данные накопительно суммируются отдельно по каждому каналу. Затем проверяем корректность данных значением счетного семафора xCountingSemaphoreADCSpeedNormal, предварительно уменьшив его на единицу. Если до этой операции не произошло еще одного прерывания по переполнению TIM14, данные корректны, иначе обрабатываем ошибку.
Слишком детальное отступление, потому что со счетным семафором возникли проблемы. У счетного семафора можно узнать до скольки он досчитал, можно увеличить, можно уменьшить.
Мне, при возникновении ошибки нужно было его сбросить, поскольку задача после выдачи сообщения об ошибке должна адаптироваться к уменьшению пользователем частоты сигнала до приемлемой.
Сматывать его не получилось, поскольку в прерывании он продолжал наматываться. Причем при сматывании в цикле, он принимал непонятно-большие значения. Возможно что-то не так делал, возможно из-за не атомарности операции.
Выходом из этой ситуации послужило его удаление и создание вновь при очередном счете периода сигнала.
Да, о «взять генератор…» Взять было не где, пришлось генератор написать.
В самом stm32f051R8 проекте вольтметра уже есть как минимум 4 генератора. Это DAC и три выхода PWM с таймера TIM1.
Однако не есть правильно что-то подавать и измерять, если все это тактируется от одного источника.
Поэтому все-таки генератор пришлось взять и написать на такой же STM32F0DISCOVERY, неоднократно горевшей в угли с перепаянным микроконтроллером, который в нужное время оказался в ящике стола. Это был stm32L162RE. STMicroelectronics продвигает соответствие выводов разных серий в одинаковых корпусах. При внимательном сравнении оказалось, что выводы порта PF stm32f051R8 используются в stm32L162RE как питание и земля, что я и обозначил сразу на плате.
Отличий в CMSYS предостаточно. System_HSE_init.c, System_HSI_init.c —
другие, тактовые частоты разные, регистры USRT разные…
С FreeRTOS я разбирался параллельно на обоих камнях.
В отличие от F0, у L1 общепринятые два канала ЦАПа. Второй канал было как-то скучно пользовать через DMA, аналогично первому. До кучи мы обязаны мерять RMS периодического сигнала любой формы. Я решил попробовать аппаратный треугольник. Я получил желаемый результат, однако он идет в разрез с «внутренний треугольный счетчик увеличивается на три такта APB1 после каждого события запуска (RM0038)». У меня получилось, что на один такт.
В задаче предписано измерять вокруг оси 1,65 В. Возникает вопрос: «Как научить осциллограф измерять вокруг этой оси?» Или чем можно проверить намерянное программой? Поэтому возможность такого измерения предусмотрена, но отключена.
Основная цель проекта достигнута. Получилось посчитать RMS с использованием FreeRTOS с условием, что без FreeRTOS у меня уже был готовый код.
Файлы проекта выложены в репозиторий
https://github.com/t654rk/FreeRTOS_REG/
https://github.com/t654rk/Generator_L1/
В заключении я хотел бы заметить, буду рад, если мои Новогодние каникулы помогут кому-то в вопросе решения таких тестовых задач! )))
С уважением Петр.
Реализуем коммерческие проекты.
Возможна работа по договору подряда.
t654rk@mail.ru