Дисклеймер. Автор не является сторонником использования многозадачных операционных систем для микроконтроллеров.
Жизнь нещадно заставляет применять операционные системы (ОС) для микроконтроллеров. На рынке существует немерянное количество подобных систем. Разработчики операционных систем, соревнуясь друг с другом, пытаются максимально увеличить функциональность своих продуктов. Это зачастую приводит к увеличению «тяжеловесности» системы, а также значительно повышает «порог вхождения» для программиста, разрабатывающего программное обеспечение встраиваемых систем.
Чтобы не мучатся с выбором ОС для своих проектов, а также не затуманить сознание изучением чужого продукта, а также освоить технику написания встраиваемых приложений под операционные системы, а также разобраться, что это вообще такое, я решил написать свою ОСьку. Свое не пахнет.
Предлагаемая ОСька (именно ОСька, язык не поворачивается назвать ее ОС, а тем более ОСРВ) кооперативная со статическими задачами. Как было отмечено выше, я не являюсь сторонником использования ОС для микроконтроллеров, но еще больше я не являюсь сторонником использования вытесняющих операционных систем в микроконтроллерах. Вытесняющая многозадачность, по сравнению с кооперативной, это не только сложные процедуры переключения контекста, но и ресурсоемкая синхронизация потоков. Использование динамических задач также значительно утяжеляет операционную систему.
Операционная система разрабатывалась под процессор семейства Cortex-M0. При небольших изменениях, касающихся правил сохранения-восстановления контекста, ее можно использовать для других типов процессоров.
По религиозным причинам я не могу использовать динамическое выделение памяти, поэтому объем требуемой памяти необходимо указывать на этапе компиляции.
Инициализация задачи. Задача оформляется в виде функции, указатель на функцию передается процедуре инициализации. При инициализации необходимо указать размер стека, выделяемый задаче. Порядок инициализации задач определяет их идентификаторы. Задача, инициализируемая первой, имеет идентификатор 0. Если указать суммарный размер стека больше зарезервированного произойдет ошибка. При инициализации задачи настраивается указатель стека задачи, стек загружается контекстом задачи.
Старт операционной системы. В качестве аргумента функции передается идентификатор задачи, с которой необходимо начать выполнение. При старте операционной системы настраивается системный таймер на квант одна миллисекунда. Со стека запускаемой задачи списывается контекст и вызывается задача.
Планировщик. При вызове этой функции из задачи, управление передается операционной системе. Операционная система из списка выбирает задачу готовую для исполнения и передает ей управление. Аргумент функции – время в миллисекундах, через которое необходимо вернуть управление текущей задаче. При вызове функции с аргументом 0xFFFFFFFF возврат управления не произойдет никогда.
Данную функцию невозможно написать на языке Си, так алгоритм ее работы полностью разрушает логику языка. В исходных кодах приведены тесты программ на языке ассемблера для систем программирования IAR и GCC. Для страждущих приведен код на языке Си. Но хотелось бы отметить, что правильно скомпилироваться он способен только при определенных «фазах луны». В моем случае это произошло только при использовании среднего уровня оптимизации, на низком и на высоком уровне код компилировался ошибочно.
Завершение задачи. Как отмечалось выше, задачи являются статическими, выгрузка задачи невозможна. Если необходимо задачу завершить можно воспользоваться этой функцией. Задача при этом остается в списке, но управление ей не передается.
Остановить или запустить задачу. Аргумент – идентификатор задачи. Эти функции позволяют реализовать диспетчер задач. Стоить отметить, что запустить можно только ранее остановленную задачу, время до запуска которой равно 0xFFFFFFFF.
Для примера традиционный микроконтроллерный «хелворд» под разработанную операционную систему.
В заключении хочется искренне надеяться, что эта, по приколу, разработанная ОСька будет интересна и полезна разработчикам программного обеспечения для встраиваемых систем.
Жизнь нещадно заставляет применять операционные системы (ОС) для микроконтроллеров. На рынке существует немерянное количество подобных систем. Разработчики операционных систем, соревнуясь друг с другом, пытаются максимально увеличить функциональность своих продуктов. Это зачастую приводит к увеличению «тяжеловесности» системы, а также значительно повышает «порог вхождения» для программиста, разрабатывающего программное обеспечение встраиваемых систем.
Чтобы не мучатся с выбором ОС для своих проектов, а также не затуманить сознание изучением чужого продукта, а также освоить технику написания встраиваемых приложений под операционные системы, а также разобраться, что это вообще такое, я решил написать свою ОСьку. Свое не пахнет.
Предлагаемая ОСька (именно ОСька, язык не поворачивается назвать ее ОС, а тем более ОСРВ) кооперативная со статическими задачами. Как было отмечено выше, я не являюсь сторонником использования ОС для микроконтроллеров, но еще больше я не являюсь сторонником использования вытесняющих операционных систем в микроконтроллерах. Вытесняющая многозадачность, по сравнению с кооперативной, это не только сложные процедуры переключения контекста, но и ресурсоемкая синхронизация потоков. Использование динамических задач также значительно утяжеляет операционную систему.
Операционная система разрабатывалась под процессор семейства Cortex-M0. При небольших изменениях, касающихся правил сохранения-восстановления контекста, ее можно использовать для других типов процессоров.
Исходный код
Файл IntorOS.h
#ifndef __INTOROS_H
#define __INTOROS_H
//Операционная система IntorOS
//Константы
#define IntorOSMaxKolvoZadach (2) //максимальное количество задач (резервирование памяти)
#define IntorOSRazmerSteka (1024) //размер стека под все задачи [байты] (резервирование памяти)
#define IntorOSError (0) //Обработка ошибок 0-зависнуть !0- Сброс МК
//Функции
//инициализация задачи
//аргументы TaskPointer - указатель на задачу, точка входа
//аргументы Stek - размер стека задачи в байтах
void InitTask(void (*TaskPointer)(void), unsigned long Stek);
//запуск операционной системы
//аргумент номер стартовой задачи
void StartOS(unsigned long Num);
//передать управление операционной системе, усыпить поток
//аргумент ms - время в миллисекундах
void Sleep(unsigned long ms);
//завершение задачи
static inline void EndTask(void){while(1)Sleep(0xFFFFFFFF);}
//остановить задачу
//аргумент - номер задачи
void StopTask(unsigned long Num);
//запустить ранее остановленную задачу
//аргумент - номер задачи
void StartTask(unsigned long Num);
#endif
Файл IntorOS.c
#define _INTOROS_C
#include "stm32l0xx.h"
#include "IntorOS.h"
//тип данных Параметры Задачи
typedef struct
{
unsigned long TaskSleep;//время до запуска задачи в мС
unsigned long* SP; //указатель стека задачи
}
Task_t;
unsigned long KolvoTask;//количество задач
unsigned long KolvoTaskStek;//использование стека
unsigned long TaskNum;//номер текущей исполняемой задачи
Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
unsigned long TaskStek[IntorOSRazmerSteka/4];//резервирование пямяти под стеки задач
//инициализация задачи
//аргументы TaskPointer - указатель на задачу, точка входа
//аргументы Stek - размер стека задачи
void InitTask(void (*TaskPointer)(void), unsigned long Stek)
{
//инициализация параметров задачи
TaskList[KolvoTask].TaskSleep=0;//время через которое произойдет возврат управления в задачу
TaskList[KolvoTask].SP=&(TaskStek[IntorOSRazmerSteka/4-1-KolvoTaskStek]);//указатель стека задачи
//инициализация стека задачи
//записать в стек точку входа в задачу (регистр LR)
TaskList[KolvoTask].SP--;
(*(TaskList[KolvoTask].SP))=(unsigned long)(TaskPointer);
TaskList[KolvoTask].SP--;//записать в стек R4
TaskList[KolvoTask].SP--;//записать в стек R5
TaskList[KolvoTask].SP--;//записать в стек R6
TaskList[KolvoTask].SP--;//записать в стек R7
TaskList[KolvoTask].SP--;//записать в стек R8
TaskList[KolvoTask].SP--;//записать в стек R9
TaskList[KolvoTask].SP--;//записать в стек R10
TaskList[KolvoTask].SP--;//записать в стек R11
TaskList[KolvoTask].SP--;//записать в стек R12
KolvoTask++;//инкремент количества задач (для следующего вызова)
KolvoTaskStek=KolvoTaskStek+Stek/4;//инкремент использование стека
//Проверяем распределение стека
if(KolvoTaskStek>(IntorOSRazmerSteka/4))
#if IntorOSError==0
while(1);//если ошибка в указании размера стека - зависнуть
#else
NVIC_SystemReset();//если ошибка в указании размера стека - Сброс МК
#endif
return;
}
//запуск операционной системы
//аргумент номер стартовой задачи
void StartOS(unsigned long Num)
{
SysTick_Config(SystemCoreClock/1000);//запускаем таймер задержки вызова задач квант 1мС
TaskNum=Num;//номер стартовой задачи
//Деинициализация стека стартовой задачи
TaskList[TaskNum].SP++;//списать со стека R12
TaskList[TaskNum].SP++;//списать со стека R11
TaskList[TaskNum].SP++;//списать со стека R10
TaskList[TaskNum].SP++;//списать со стека R9
TaskList[TaskNum].SP++;//списать со стека R8
TaskList[TaskNum].SP++;//списать со стека R7
TaskList[TaskNum].SP++;//списать со стека R6
TaskList[TaskNum].SP++;//списать со стека R5
TaskList[TaskNum].SP++;//списать со стека R4
TaskList[TaskNum].SP++;//списать со стека LR
__set_SP((unsigned long)TaskList[TaskNum].SP);//установить указатель стека запускаемой задачи
(*((void (*)(void))(*(TaskList[TaskNum].SP-1))))();//передаем управление в задачу
//если произошло завершение задачи
#if IntorOSError==0
while(1);//зависнуть
#else
NVIC_SystemReset();//Сброс МК
#endif
}
//остановить задачу
//аргумент - номер задачи
void StopTask(unsigned long Num)
{
TaskList[Num].TaskSleep=0xFFFFFFFF;
return;
}
//запустить ранее остановленную задачу
//аргумент - номер задачи
void StartTask(unsigned long Num)
{
if((~(TaskList[Num].TaskSleep))==0)
{//если задача была остановлена, запустить
TaskList[Num].TaskSleep=0x00000000;
}
return;
}
//прерывание системного таймера
void SysTick_Handler(void);
void SysTick_Handler(void)
{
TimingDelay++;//инкремент переменной системного таймера
for(int i=0;i<KolvoTask;i++)
{//перебираем задачи
if(((TaskList[i].TaskSleep)!=0) && ((~(TaskList[i].TaskSleep))!=0))
{//если время до запуска не 0 и не 0xFFFFFFFF
(TaskList[i].TaskSleep)--;//уменьшаем время до запуска
}
}
return;
}
Файл IntorOSSleepIAR.s
#define SHT_PROGBITS 0x1
EXTERN KolvoTask
EXTERN TaskList
EXTERN TaskNum
PUBLIC Sleep
SECTION `.text`:CODE:NOROOT(2)
THUMB
// 8 //передать управление операционной системе
// 9 //аргумент время в лимлисекундах
// 10 void Sleep(unsigned long ms)
Sleep:
// 11 {
// 12 //сохраняем контекст
// 13 __asm("PUSH {R4-R7,LR}");
PUSH {R4-R7,LR}
// 14 __asm("MOV R4,R8");
MOV R4,R8
// 15 __asm("MOV R5,R9");
MOV R5,R9
// 16 __asm("MOV R6,R10");
MOV R6,R10
// 17 __asm("MOV R7,R11");
MOV R7,R11
// 18 __asm("PUSH {R4-R7}");
PUSH {R4-R7}
// 19 __asm("MOV R4,R12");
MOV R4,R12
// 20 __asm("PUSH {R4}");
PUSH {R4}
// 21 TaskList[TaskNum].TaskSleep=ms;//сохраняем время через которое произойдет возврат управления
LDR R1,Sleep_0
LDR R2,Sleep_0+0x4
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
STR R0,[R2, R3]
// 22 TaskList[TaskNum].SP =__get_SP();//сохраняем SP
MOV R0,SP
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
ADDS R3,R2,R3
STR R0,[R3, #+4]
// 23 //выбор задачи для исполнения
// 24 while(1)
// 25 {
// 26 TaskNum++;if(TaskNum==KolvoTask)TaskNum=0;//инкрементируем номер текущей задачи
Sleep_1:
LDR R0,[R1, #+0]
ADDS R0,R0,#+1
LDR R3,Sleep_0+0x8
LDR R3,[R3, #+0]
CMP R0,R3
BNE Sleep_2
MOVS R0,#+0
Sleep_2:
STR R0,[R1, #+0]
LSLS R0,R0,#+3
ADDS R0,R2,R0
LDR R3,[R0, #+0]
CMP R3,#+0
BNE Sleep_1
// 27 //проверяем готовность задачи к выполнению
// 28 if(TaskList[TaskNum].TaskSleep==0)
// 29 {//задача готова к выполнению
// 30 //востанавливаем контекст
// 31 __set_SP(TaskList[TaskNum].SP);//востанавливаем SP
LDR R0,[R0, #+4]
MOV SP,R0
// 32 __asm("POP {R4}");
POP {R4}
// 33 __asm("MOV R12,R4");
MOV R12,R4
// 34 __asm("POP {R4-R7}");
POP {R4-R7}
// 35 __asm("MOV R11,R7");
MOV R11,R7
// 36 __asm("MOV R10,R6");
MOV R10,R6
// 37 __asm("MOV R9,R5");
MOV R9,R5
// 38 __asm("MOV R8,R4");
MOV R8,R4
// 39 __asm("POP {R4-R7,PC}");
POP {R4-R7,PC}
// 40
// 41 //The End
// 42 return;
NOP
// 43 }
// 44 }
// 45 }
DATA
Sleep_0:
// extern unsigned long TaskNum;//номер текущей задачи
DC32 TaskNum
// extern Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
DC32 TaskList
// extern unsigned long KolvoTask;//количество задач
DC32 KolvoTask
SECTION `.iar_vfe_header`:DATA:NOALLOC:NOROOT(2)
SECTION_TYPE SHT_PROGBITS, 0
DATA
DC32 0
SECTION __DLIB_PERTHREAD:DATA:REORDER:NOROOT(0)
SECTION_TYPE SHT_PROGBITS, 0
SECTION __DLIB_PERTHREAD_init:DATA:REORDER:NOROOT(0)
SECTION_TYPE SHT_PROGBITS, 0
END
Файл IntorOSSleepGCC.s
.cpu cortex-m0
.text
.cfi_sections .debug_frame
.section .text.Sleep,"ax",%progbits
.align 1
.global Sleep
.syntax unified
.thumb
.thumb_func
.type Sleep, %function
.extern KolvoTask
.extern TaskList
.extern TaskNum
.cfi_startproc
// 8 //передать управление операционной системе
// 9 //аргумент время в лимлисекундах
// 10 void Sleep(unsigned long ms)
Sleep:
// 11 {
// 12 //сохраняем контекст
// 13 __asm("PUSH {R4-R7,LR}");
PUSH {R4-R7,LR}
// 14 __asm("MOV R4,R8");
MOV R4,R8
// 15 __asm("MOV R5,R9");
MOV R5,R9
// 16 __asm("MOV R6,R10");
MOV R6,R10
// 17 __asm("MOV R7,R11");
MOV R7,R11
// 18 __asm("PUSH {R4-R7}");
PUSH {R4-R7}
// 19 __asm("MOV R4,R12");
MOV R4,R12
// 20 __asm("PUSH {R4}");
PUSH {R4}
// 21 TaskList[TaskNum].TaskSleep=ms;//сохраняем время через которое произойдет возврат управления
LDR R1,Sleep_0
LDR R2,Sleep_0+0x4
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
STR R0,[R2, R3]
// 22 TaskList[TaskNum].SP =__get_SP();//сохраняем SP
MOV R0,SP
LDR R3,[R1, #+0]
LSLS R3,R3,#+3
ADDS R3,R2,R3
STR R0,[R3, #+4]
// 23 //выбор задачи для исполнения
// 24 while(1)
// 25 {
// 26 TaskNum++;if(TaskNum==KolvoTask)TaskNum=0;//инкрементируем номер текущей задачи
Sleep_1:
LDR R0,[R1, #+0]
ADDS R0,R0,#+1
LDR R3,Sleep_0+0x8
LDR R3,[R3, #+0]
CMP R0,R3
BNE Sleep_2
MOVS R0,#+0
Sleep_2:
STR R0,[R1, #+0]
LSLS R0,R0,#+3
ADDS R0,R2,R0
LDR R3,[R0, #+0]
CMP R3,#+0
BNE Sleep_1
// 27 //проверяем готовность задачи к выполнению
// 28 if(TaskList[TaskNum].TaskSleep==0)
// 29 {//задача готова к выполнению
// 30 //востанавливаем контекст
// 31 __set_SP(TaskList[TaskNum].SP);//востанавливаем SP
LDR R0,[R0, #+4]
MOV SP,R0
// 32 __asm("POP {R4}");
POP {R4}
// 33 __asm("MOV R12,R4");
MOV R12,R4
// 34 __asm("POP {R4-R7}");
POP {R4-R7}
// 35 __asm("MOV R11,R7");
MOV R11,R7
// 36 __asm("MOV R10,R6");
MOV R10,R6
// 37 __asm("MOV R9,R5");
MOV R9,R5
// 38 __asm("MOV R8,R4");
MOV R8,R4
// 39 __asm("POP {R4-R7,PC}");
POP {R4-R7,PC}
// 40
// 41 //The End
// 42 return;
NOP
// 43 }
// 44 }
// 45 }
.align 2
Sleep_0:
// extern unsigned long TaskNum;//номер текущей задачи
.word TaskNum
// extern Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
.word TaskList
// extern unsigned long KolvoTask;//количество задач
.word KolvoTask
.cfi_endproc
Константы компиляции ОСьки
#define IntorOSMaxKolvoZadach (2) //максимальное количество задач (резервирование памяти)
#define IntorOSRazmerSteka (1024) //размер стека под все задачи [байты] (резервирование памяти)
По религиозным причинам я не могу использовать динамическое выделение памяти, поэтому объем требуемой памяти необходимо указывать на этапе компиляции.
Сервисы ОСьки
void InitTask(void (*TaskPointer)(void), unsigned long Stek);
Инициализация задачи. Задача оформляется в виде функции, указатель на функцию передается процедуре инициализации. При инициализации необходимо указать размер стека, выделяемый задаче. Порядок инициализации задач определяет их идентификаторы. Задача, инициализируемая первой, имеет идентификатор 0. Если указать суммарный размер стека больше зарезервированного произойдет ошибка. При инициализации задачи настраивается указатель стека задачи, стек загружается контекстом задачи.
void StartOS(unsigned long Num);
Старт операционной системы. В качестве аргумента функции передается идентификатор задачи, с которой необходимо начать выполнение. При старте операционной системы настраивается системный таймер на квант одна миллисекунда. Со стека запускаемой задачи списывается контекст и вызывается задача.
void Sleep(unsigned long ms);
Планировщик. При вызове этой функции из задачи, управление передается операционной системе. Операционная система из списка выбирает задачу готовую для исполнения и передает ей управление. Аргумент функции – время в миллисекундах, через которое необходимо вернуть управление текущей задаче. При вызове функции с аргументом 0xFFFFFFFF возврат управления не произойдет никогда.
Данную функцию невозможно написать на языке Си, так алгоритм ее работы полностью разрушает логику языка. В исходных кодах приведены тесты программ на языке ассемблера для систем программирования IAR и GCC. Для страждущих приведен код на языке Си. Но хотелось бы отметить, что правильно скомпилироваться он способен только при определенных «фазах луны». В моем случае это произошло только при использовании среднего уровня оптимизации, на низком и на высоком уровне код компилировался ошибочно.
Файл Sleep.c
extern Task_t TaskList[IntorOSMaxKolvoZadach];//список задач
extern unsigned long TaskNum;//номер текущей задачи
extern unsigned long KolvoTask;//количество задач
//передать управление операционной системе
//аргумент время в лимлисекундах
#pragma optimize=medium
void Sleep(unsigned long ms)
{
//сохраняем контекст
__asm("PUSH {R4-R7,LR}");
__asm("MOV R4,R8");
__asm("MOV R5,R9");
__asm("MOV R6,R10");
__asm("MOV R7,R11");
__asm("PUSH {R4-R7}");
__asm("MOV R4,R12");
__asm("PUSH {R4}");
TaskList[TaskNum].TaskSleep=ms;//сохраняем время через которое произойдет возврат управления
TaskList[TaskNum].SP =(unsigned long*)__get_SP();//сохраняем SP
//выбор задачи для исполнения
while(1)
{
TaskNum++;if(TaskNum==KolvoTask)TaskNum=0;//инкрементируем номер текущей задачи
//проверяем готовность задачи к выполнению
if(TaskList[TaskNum].TaskSleep==0)
{//задача готова к выполнению
//востанавливаем контекст
__set_SP((unsigned long)TaskList[TaskNum].SP);//востанавливаем SP
__asm("POP {R4}");
__asm("MOV R12,R4");
__asm("POP {R4-R7}");
__asm("MOV R11,R7");
__asm("MOV R10,R6");
__asm("MOV R9,R5");
__asm("MOV R8,R4");
__asm("POP {R4-R7,PC}"); //return
}
}
}
void EndTask(void);
Завершение задачи. Как отмечалось выше, задачи являются статическими, выгрузка задачи невозможна. Если необходимо задачу завершить можно воспользоваться этой функцией. Задача при этом остается в списке, но управление ей не передается.
void StopTask(unsigned long Num);
void StartTask(unsigned long Num);
Остановить или запустить задачу. Аргумент – идентификатор задачи. Эти функции позволяют реализовать диспетчер задач. Стоить отметить, что запустить можно только ранее остановленную задачу, время до запуска которой равно 0xFFFFFFFF.
Использование ОСьки
Для примера традиционный микроконтроллерный «хелворд» под разработанную операционную систему.
#include "stm32l0xx.h"
#include "stm32l0xx_ll_gpio.h"
#include "IntorOS.h"
//Задача 0
void Task0(void)
{
LL_GPIO_InitTypeDef GPIO_InitStruct;
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1)
{
GPIOB->BRR=LL_GPIO_PIN_0;
Sleep(1000);
GPIOB->BSRR=LL_GPIO_PIN_0;
Sleep(1000);
}
}
//Задача 1
void Task1(void)
{
LL_GPIO_InitTypeDef GPIO_InitStruct;
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1)
{
GPIOB->BRR=LL_GPIO_PIN_1;
Sleep(500);
GPIOB->BSRR=LL_GPIO_PIN_1;
Sleep(500);
}
}
void main(void)
{
// MCU Configuration
SystemClock_Config();
//Инициализация задач
InitTask(Task0, 512);
InitTask(Task1, 256);
//Запуск ОС
StartOS(0);
}
В заключении хочется искренне надеяться, что эта, по приколу, разработанная ОСька будет интересна и полезна разработчикам программного обеспечения для встраиваемых систем.