Данная серия статей посвящена описанию работы RTOS– написанной на основе TNKernel. Исходники вы можете скачать по этой ссылке.
Если вы хотите написать свою RTOS или разобраться в TNKernel читайте так-же:
- Задачи
- Работа планировщика
- Последовательность действий при инициализации
- Assembler, переключение контекста
- Чередование задач при помощи отправки задачи в конец очереди для данного приоритета
- Отправление задачи в режим ожидания на определенное время
- Простой семафор
Работа планировщика
Всю работу RTOS можно разделить на два цикла:
- Цикл выполнения функции задачи
- Цикл переключения контекста
Переключение контекста происходит при выполнении задачи RTOS timer_task. Поэтому ее необходимо вызывать, каждый раз при необходимости переключать задачи пользователя(планирование).
Цикл переключения — цикл прерывания по системному таймеру SysTick. Прерывание по системному таймеру настраивается в main. В обработчике прерывания SysTick_Handler как следующая задача указывается timer_task (см. функцию tick_myrtos).
void tick_myrtos(void)
{
next_task_to_run = &timer_task;
}
После чего вызывается переключение контекста задач int_exit (непосредственное переключение контекста задачи в int_exit написано на ассемблере и будет рассмотрено в др. статье).
void SysTick_Handler()
{
tick_myrtos();
int_exit();
}
Контекст переключается на задачу планирования timer_task. Данная задача дожна выполниться без прерываний т.к. она является основой планирования RTOS, поэтому в начале функции timer_task_func отключаются прерывания, а в ее конце включаются.
// планировщик
void timer_task_func(void *par)
{
uint8_t priority;
while(1)
{
arm_disable_interrupts();
// приоритет для сл. готовой задачи
priority = queue_find_priority_next_task();
// выбираем сл. задачу из списка задач по приоритетам
next_task_to_run = task_param_from_task_queue(task_queue_priority[priority].next);
// переключаем контекст на сл. задачу
switch_context();
arm_enable_interrupts();
}
}
queue_find_priority_next_task производит поиск задач в списке приоритетов. Данная функция возвращает номер самого высокого приоритета у которого есть задачи в списке. Очередь задач по приоритетам представляет собой связанный список для каждого приоритета:
// 1 приоритет
task_queue_priority[1].next
task_queue_priority[1].prev
Для того, чтобы узнать, есть ли задачи с тем или иным приоритетом в очереди, проверяется следующий элемент в списке — если указывает сам на себя, то очередь для данного приоритета пуста и происходит переход к следующему приоритету, где проверяется тоже самое.
// очередь задач для каждого приоритета
link_init task_queue_priority[NUM_PRIORITY];
// связанные списки
typedef struct _link_init
{
struct _link_init * prev;
struct _link_init * next;
} link_init;
uint8_t queue_find_priority_next_task(void)
{
uint8_t i;
// проход по всем приоритетам и проверка есть ли задача в их списке (поиск сл. задачи)
// есть - если 1й связанный список в поле next не указывает сам на себя
for(i = 1; i < NUM_PRIORITY; i++)
{
if(task_queue_priority[i].next != &(task_queue_priority[i]))
{
break;
}
}
return(i);
}
После того как мы узнали какой приоритет будет использоваться — указываем следующую задачу с данным приоритетом из списка приоритетов. Это происходит в функции task_param_from_task_queue.
// получение из очереди(связанного списка) - структуры к которой она относятся - задача
task_param_init * task_param_from_task_queue(link_init *queue)
{
uint8_t *byte_offset;
task_param_init *task;
byte_offset = (uint8_t*)(&((task_param_init*)0)->task_queue);
task = (task_param_init*)((uint8_t*)(queue) - byte_offset);
return(task);
}
NOTE: Более подробно о работе данного кода, можно узнать из этой статьи.
Последний этап это переключение контекста на следующую задачу — задачу пользователя, которую мы определили выше, происходит в int_exit.