Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2743
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать

Запуск FreeRTOS

631

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

23.5. Примитивы синхронизации

В многопоточных приложениях потокам рано или поздно необходим способ своей синхронизации, как при доступе к общим ресурсам, так и при передаче данных между несколькими потоками выполнения. В литературе о конкурентном программировании полно алгоритмов и структур данных, лучше всего подходящих в качестве примитивов синхронизации. API-интерфейс CMSIS-RTOS и лежащая в его основе ОС FreeRTOS определяют те примитивы, которые являются общими для всех операционных систем и потоковых библиотек. В этом параграфе кратко представлены наиболее значимые из них.

23.5.1. Очереди сообщений

Очередь (queue)29 – это скопление с логикой First-In-First-Out (FIFO), которое реализовано во FreeRTOS линейной структурой данных, где первый добавленный элемент будет удален первым. Когда элемент добавляется в очередь, говорят, что он помещен в очередь (enqueued), тогда как когда он удаляется – он снимается с очереди (dequeued).

Очереди широко используются в конкурентном программировании, особенно когда необходимо обмениваться данными между несколькими потоками, которые имеют разное время отклика на события. Например, у нас есть два потока из десяти, один действует в качестве поставщика, а другой – в качестве потребителя, совместно используя общий буфер. Задача поставщика – сгенерировать часть данных, поместить ее в буфер и начать заново. В то же время, задача потребителя состоит в том, чтобы удалять его из буфера по одной части за раз. Проблема заключается в том, чтобы убедиться, что поставщик не будет пытаться добавить данные в буфер, если он заполнен, и что потребитель не будет пытаться удалить данные из пустого буфера. В ОСРВ очереди спроектированы так, что, если поток пытается добавить данные в заполную очередь, он может быть переведен в режим блокировки до тех пор, пока хотя бы один элемент не будет удален из очереди. В то же время ядро ОС переводит потребителя в режим блокировки, если в очереди нет данных). Будучи обработанными из ОС, очереди спроектированы таким образом, чтобы не возникало условий гонки (race conditions) между разными потоками (если программист не вносит очевидные ошибки в свой код).

Очереди представляют собой необязательную структуру данных на уровне CMSIS-RTOS, которую необходимо включить, установив osFeature_MessageQ в 1 в файле cmsis_os.h. Очередь определена следующей структурой Си:

typedef struct os_messageQ_def {

 

uint32_t

queue_sz;

/* Количество элементов в очереди

*/

uint32_t

item_sz;

/* Размер элемента

*/

} osMessageQDef_t;

29 CMSIS-RTOS использует термин очереди сообщений (message queues) для обозначения того, что обычно называют очередями. Как мы увидим через некоторое время, это также закреплено в API-интерфейсе (все структуры и функции имеют префикс osMessage). Однако в оставшейся части данной главы мы будем просто называть их очередями.

Запуск FreeRTOS

632

Определить очередь можно с легкостью, воспользовавшись макросом osMessageQDef(). Очередь эффективно создается с помощью функции:

osMessageQId osMessageCreate(const osMessageQDef_t *queue_def, osThreadId thread_id);

которая принимает экземпляр структуры osMessageQDef_t, созданный с помощью макроса osMessageQDef(), и идентификатор потока, связанный с очередью. Однако APIинтерфейс FreeRTOS не позволяет ассоциировать поток с очередью, поэтому этот параметр просто игнорируется, и вы можете безопасно передавать значение NULL.

Чтобы поместить новый элемент в очередь, мы используем функцию

osStatus osMessagePut(osMessageQId queue_id, uint32_t info, uint32_t millisec);

где queue_id – идентификатор очереди, возвращаемый функцией osMessageCreate, в то время как info может быть как данными (целочисленный литерал типа unsigned long) для постановки в очередь, так и адресом ячейки памяти, содержащей более четко сформулированную структуру данных Си (например, блок из пула памяти). Наконец, параметр millisec представляет собой тайм-аут (время ожидания), то есть задает количество миллисекунд, которые мы готовы ждать, если очередь заполнена: если до истечения периода этого времени ожидания места недостаточно, то функция osMessagePut() возвращает значение osErrorTimeoutResource30. Передача osWaitForever приведет к тому, что osMessagePut() будет ждать бесконечно долго.

Для снятия данных с очереди мы используем функцию

osEvent osMessageGet(osMessageQId queue_id, uint32_t millisec);

которая возвращает экземпляр структуры Си osEvent, которая определена следующим образом:

typedef struct {

 

 

osStatus

status;

/* Код состояния: информация о событии или ошибке

*/

union {

 

 

 

uint32_t

v;

/* Сообщение в качестве 32-битного значения

*/

void

*p;

/* Сообщение или письмо в качестве указателя на void

*/

int32_t

signals;

/* Флаги сигналов

*/

} value;

 

/* Значение события

*/

...

 

 

 

} osEvent;

 

 

 

Как видите, экземпляр этой структуры может предоставить как код состояния (который равен osEventMessage, если элемент успешно снят с очереди, и osEventTimeout в случае тайм-аута), так и снятый с очереди элемент, который содержится внутри поля osEvent.value.v (или мы также можем использовать поле *p объединения, если значение

30 osMessagePut() и osMessageGet() могут возвращать другие коды состояний, если они вызваны из потока или из ISR. Для получения дополнительной информации обратитесь к официальной спецификации

CMSIS-RTOS (https://www.keil.com/pack/doc/CMSIS/RTOS/html/group__CMSIS__RTOS__Message.html).

Запуск FreeRTOS

633

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

Если мы хотим оставить элемент в очереди, не удаляя его физически, мы можем использовать функцию

osEvent osMessagePeek(osMessageQId queue_id, uint32_t millisec);

Примите во внимание, что FreeRTOS предоставляет два отдельных API-интер- фейса для управления очередями из потока и очередями из ISR. Например, функция xQueueReceive() используется для снятия элемента с очереди из потока, а функция xQueueReceiveFromISR() используется для безопасного снятия элементов с очереди из ISR. Уровень CMSIS-RTOS, разработанный ST, предназначен для абстрагирования от этого аспекта и автоматически проверяет, выполняем ли мы вызов из потока или из ISR. Как обычно, за счет потери в скорости.

В следующем примере показано, как можно использовать очередь для обмена данными между двумя потоками, один из которых выступает в качестве поставщика (UARTThread()), а другой – в качестве потребителя (blinkThread()), который может работать очень медленно, если указан достаточно большой тайм-аут.

Имя файла: src/main-ex3.c

14osMessageQDef(MsgBox, 5, uint16_t); // Определение очереди сообщений

15osMessageQId MsgBox;

16

17int main(void) {

18HAL_Init();

19

 

20

Nucleo_BSP_Init();

21

 

22

RetargetInit(&huart2);

23

 

24osThreadDef(blink, blinkThread, osPriorityNormal, 0, 100);

25osThreadCreate(osThread(blink), NULL);

26

27osThreadDef(uart, UARTThread, osPriorityNormal, 0, 300);

28osThreadCreate(osThread(uart), NULL);

29

30MsgBox = osMessageCreate(osMessageQ(MsgBox), NULL);

31osKernelStart();

32

33/* Бесконечный цикл */

34while (1);

35}

36

37void blinkThread(void const *argument) {

38uint16_t delay = 500; /* Задержка по умолчанию */

39osEvent evt;

40

Запуск FreeRTOS

634

41while(1) {

42evt = osMessageGet(MsgBox, 1);

43if(evt.status == osEventMessage)

44delay = evt.value.v;

45

46HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);

47osDelay(delay);

48}

49osThreadTerminate(NULL);

50}

51

52void UARTThread(void const *argument) {

53uint16_t delay = 0;

54

55while(1) {

56printf("Specify the LD2 LED blink period: ");

57scanf("%hu", &delay);

58printf("\r\nSpecified period: %hu\n\r", delay);

59osMessagePut(MsgBox, delay, osWaitForever);

60}

Поток UARTThread, определенный в строках [52:60], использует метод перенаправления ввода/вывода, описанный в Главе 8, что позволяет нам использовать классические процедуры printf()/scanf() стандартной библиотеки Си. Поток считывает значение типа uint16_t по UART и помещает его в очередь MsgBox. Поток blinkThread(), определенный в строках [37:50], берет эти значения из очереди и использует их в качестве значений задержки для функции osDelay(). Это простое приложение позволяет нам передавать желаемую частоту мигания светодиода LD2 из эмулятора терминала.

Если вы укажете большое значение задержки, вы можете легко увидеть, как могут использоваться очереди, когда поток поставщика работает быстрее, чем поток потребителя. Передав задержку, равную 10000, мы можем сразу же поместить другое значение задержки, равное 50, в очередь (поскольку в очереди достаточно места для хранения другого значения). Как вы увидите, нам нужно около 10 секунд, чтобы светодиод начал мигать с частотой 20 Гц, поскольку blinkThread() блокируется функцией osDelay().

API-интерфейс CMSIS-RTOS определяет очереди другого типа, называемые очередями писем. Очередь писем (mail queue) схожа с очередью сообщений, но ее передаваемые данные состоят из блоков памяти, которые должны быть выделены (до помещения данных) и освобождены (после принятия данных). Очередь писем использует пул памяти для создания форматированных блоков памяти и передает указатели на эти блоки в очередь сообщений. Это позволяет данным оставаться в выделенном блоке памяти, пока между отдельными потоками перемещается только указатель. Это является преимуществом перед сообщениями, которые могут передавать только 32-битное значение или указатель. Используя функции очереди писем, вы можете контролировать, отправлять, получать или ждать «письма». На самом деле очереди писем реализуются ST при помощи очередей сообщений и пулов памяти, и они доступны, если и только если мы включим модель динамического выделения памяти. Мы не будем вдаваться в подробности очередей писем.