Операционная система выходного дня

Дисклеймер. Автор не является сторонником использования многозадачных операционных систем для микроконтроллеров.

image
Жизнь нещадно заставляет применять операционные системы (ОС) для микроконтроллеров. На рынке существует немерянное количество подобных систем. Разработчики операционных систем, соревнуясь друг с другом, пытаются максимально увеличить функциональность своих продуктов. Это зачастую приводит к увеличению «тяжеловесности» системы, а также значительно повышает «порог вхождения» для программиста, разрабатывающего программное обеспечение встраиваемых систем.

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

Предлагаемая ОСька (именно ОСька, язык не поворачивается назвать ее ОС, а тем более ОСРВ) кооперативная со статическими задачами. Как было отмечено выше, я не являюсь сторонником использования ОС для микроконтроллеров, но еще больше я не являюсь сторонником использования вытесняющих операционных систем в микроконтроллерах. Вытесняющая многозадачность, по сравнению с кооперативной, это не только сложные процедуры переключения контекста, но и ресурсоемкая синхронизация потоков. Использование динамических задач также значительно утяжеляет операционную систему.

Операционная система разрабатывалась под процессор семейства 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


Файл 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);
  }

В заключении хочется искренне надеяться, что эта, по приколу, разработанная ОСька будет интересна и полезна разработчикам программного обеспечения для встраиваемых систем.

© Habrahabr.ru