В проекте созданы две задачи —
Uart1 и
Uart2.
Обе они используют один и тот же физический интерфейс
USART2.
Первая задача будет постоянно передавать строку:
UART1 start and work_UART1 start and work_UART1 start and work
Вторая задача будет передавать:
UART2 start and work_UART2 start and work_UART2 start and work
Для того чтобы хорошо увидеть переключение между задачами,
данные будем передавать не всей строкой сразу, а по одному символу.
После передачи каждого символа вызывается функция
osThreadYield().
void Uart1(void *argument)
{
const char msg[] =
"UART1 start and work_UART1 start and work_UART1 start and work\r\n";
for (;;)
{
for (uint32_t i = 0; i < sizeof(msg) - 1; i++)
{
HAL_UART_Transmit(&huart2,
(uint8_t *)&msg[i],
1,
HAL_MAX_DELAY);
osThreadYield();
}
}
}
void Uart2(void *argument)
{
const char msg[] =
"UART2 start and work_UART2 start and work_UART2 start and work\r\n";
for (;;)
{
for (uint32_t i = 0; i < sizeof(msg) - 1; i++)
{
HAL_UART_Transmit(&huart2,
(uint8_t *)&msg[i],
1,
HAL_MAX_DELAY);
osThreadYield();
}
}
}
Предположим, первой начала выполняться задача
Uart1.
Она передала один символ и выполнила
osThreadYield().
Планировщик FreeRTOS может передать управление задаче
Uart2.
Она также передаст один символ в тот же UART и снова уступит процессор.
Uart1 → передала один символ
↓
osThreadYield()
↓
Uart2 → передала один символ
↓
osThreadYield()
↓
Uart1 → передала следующий символ
↓
...
Таким образом, одна задача может начать вывод своей строки, затем управление будет передано второй задаче и она начнёт передавать свои данные в тот же UART.
В терминале вместо двух нормальных сообщений можно получить, например:
UUAARRTT12 ssttaarrtt aanndd wwoorrkk...
Конкретный порядок символов может быть другим. Он зависит от того, в какой момент планировщик FreeRTOS переключит выполнение с одной задачи на другую.
В данном примере UART является общим ресурсом. Обе задачи имеют доступ к одному и тому же периферийному модулю, но никак не согласовывают между собой момент начала передачи.
FreeRTOS не знает, что USART2 должен использоваться только одной задачей в определённый момент времени. Для операционной системы это просто две независимые задачи.
Поэтому защиту общего ресурса должен организовать программист.
Такая проблема может возникать не только при работе с UART. Общим ресурсом может быть:
Для решения этой проблемы будем использовать Mutex.
В нашем проекте его дескриптор называется
nextHandle.
Работу Mutex удобно представить как обычный светофор. Если ресурс свободен, задача получает разрешение работать с ним. Если ресурс уже занят другой задачей, новая задача должна ждать.
Алгоритм получается следующим:
Uart1 → получает Mutex → передаёт всю строку → освобождает Mutex
↑
│
UART занят
Uart2 → ждёт освобождения Mutex
Важно понимать, что Mutex не запрещает FreeRTOS переключать задачи. Планировщик по-прежнему может передать процессор другой задаче.
Но если вторая задача попытается получить тот же Mutex, она не сможет продолжить работу с UART до тех пор, пока первая задача его не освободит.
void Uart1(void *argument)
{
const char msg[] =
"UART1 start and work_UART1 start and work_UART1 start and work\r\n";
for (;;)
{
osMutexAcquire(nextHandle, osWaitForever);
for (uint32_t i = 0; i < sizeof(msg) - 1; i++)
{
HAL_UART_Transmit(&huart2,
(uint8_t *)&msg[i],
1,
HAL_MAX_DELAY);
osThreadYield();
}
osMutexRelease(nextHandle);
osThreadYield();
}
}
void Uart2(void *argument)
{
const char msg[] =
"UART2 start and work_UART2 start and work_UART2 start and work\r\n";
for (;;)
{
osMutexAcquire(nextHandle, osWaitForever);
for (uint32_t i = 0; i < sizeof(msg) - 1; i++)
{
HAL_UART_Transmit(&huart2,
(uint8_t *)&msg[i],
1,
HAL_MAX_DELAY);
osThreadYield();
}
osMutexRelease(nextHandle);
osThreadYield();
}
}
osMutexAcquire(nextHandle, osWaitForever);
Функция osMutexAcquire()
пытается захватить Mutex.
Первый параметр:
nextHandle
Это дескриптор нашего Mutex.
Второй параметр:
osWaitForever
означает, что если Mutex уже занят другой задачей, текущая задача будет ждать его освобождения без ограничения по времени.
Если Mutex свободен, задача сразу его получает и продолжает работу.
HAL_UART_Transmit(&huart2,
(uint8_t *)&msg[i],
1,
HAL_MAX_DELAY);
Первый параметр:
&huart2
указывает, через какой UART будут передаваться данные. В нашем случае используется USART2.
Следующий параметр:
(uint8_t *)&msg[i]
указывает адрес текущего символа в строке.
Значение:
1
означает, что за один вызов функции передаётся один байт.
Передача по одному символу здесь используется специально. В нормальной программе строку обычно передают целиком. В нашем уроке такой вариант позволяет хорошо увидеть проблему переключения между задачами.
osThreadYield();
Эта команда не создаёт задержку.
Текущая задача добровольно сообщает планировщику FreeRTOS, что готова уступить процессор другой готовой задаче с таким же приоритетом.
Без Mutex после передачи одного символа задача выполняет
osThreadYield(),
и управление может сразу перейти второй задаче.
Поэтому без защиты символы двух сообщений могут перемешиваться.
После добавления Mutex переключение задач всё ещё возможно, но вторая задача не сможет начать работу с UART, пока первая задача удерживает Mutex.
osMutexRelease(nextHandle);
После того как задача закончила передачу всей строки, она освобождает Mutex.
Теперь Mutex может получить другая задача и начать свою работу с UART.
osMutexRelease()
должен находиться после цикла передачи всей строки.
Если освобождать Mutex после каждого символа,
то защита всей операции пропадёт.
Теперь последовательность работы выглядит следующим образом:
1. Uart1 получает Mutex 2. Uart1 полностью передаёт: UART1 start and work_UART1 start and work_UART1 start and work 3. Uart1 освобождает Mutex 4. Uart2 получает Mutex 5. Uart2 полностью передаёт: UART2 start and work_UART2 start and work_UART2 start and work 6. Uart2 освобождает Mutex
В этом уроке мы увидели, к чему может привести одновременная работа нескольких задач FreeRTOS с одним общим ресурсом.
Без Mutex обе задачи независимо передавали данные через один UART, в результате чего символы могли перемешиваться.
После добавления Mutex одна задача получает исключительный доступ к UART на время выполнения всей операции. Вторая задача может продолжить работу с этим ресурсом только после его освобождения.
Без Mutex:
Task 1 ─────┐
├────→ UART
Task 2 ─────┘
С Mutex:
Task 1 ───→ [ Mutex ] ───→ UART
↑
│
Task 2 ───── ждёт
В реальной программе строку обычно передают целиком,
а osThreadYield()
после каждого символа не используют.
В данном примере это сделано специально,
чтобы наглядно показать проблему совместного доступа
к одному ресурсу и принцип работы Mutex.