A(rduino) OS
Simple Task Scheduler for Arduino Development Board
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
Introduction

A(rduino)OS is a simple task scheduler. It has been written mostly for education purposes but it is a fully functional system. It is dedicated to run on Arduino UNO Rev. 3 board equipped with Atmega 328p MCU, but the code has been written in a fashion allowing to easily extend it and port to other MCU.

Features

AOS does not have an aspirations to conquer with any other RTOS available for embedded devices like ChibiOS/FreeRTOS. In fact, most of the schedulers currently available are far more superior. AOS is a playground really. It has been written to be easily comprehended, with plain internal architecture and easy to extend. The main purpose though is to learn about the hardware and all the little details related with scheduling and context switching.

AOS Features:

  • Completely free, with an intension to be an educational platform
  • Pre-emptive scheduling model
  • 4 priority levels, multiple tasks at the same priority level allowed.
  • Priority scheduling with two queues for each priority (active + expired).
  • Implements basic semaphores
  • Tasks can be suspended, put to sleep
  • Basic timers implementation

Requirements

AOS depends on libpca (pure C arduino library). libpca can be cloned from here:

git clone git@g.nosp@m.ithu.nosp@m.b.com:dagon666/avr_Libpca pca

It uses timer configuration routines from the library during the initialization. By default AOS expects that the library is present on the same directory level as the OS itself. This of course can be customized by modifying the provided Makefile. Example directory layout:

      .
      |
      +-- pca  
      |
      +-- aos
      |      
      +-- your_project_dir

Tasks

AOS is a task scheduler. It breaks the original nomenclature by calling tasks tasks (when they indeed are threads). It doesn't implement any other levels of distinction thus it causes no harm. Each task, it's control block and it's stack reside in a dedicated dynamically allocated memory block. Task creation is the only place where a dynamic memory allocation happens ! The picture bellow depicts tasks memory map.

* 
*    --+--    +--------------------+
*      |      |                    |                   
*      |      |  initial context   |                        
*      |      +--------------------+  struct ctx *
*      |      |                    |  
*      |      |    stack grows     |  
*      |      |     downwards      |  
*      |      |         |          |
*      |      |         v          |
*      |      |                    |  
*      |      |      private       |  
*             |       stack        |  
*   wa_size   |        for         |  
*             |     variables      |  
*      |      |        and         |
*      |      |   function calls   |
*      |      |                    |
*    --+--    +--------------------+ 0x00 + sizeof(task_cb) = wa *
*             |                    |
*             |      task_cb       |
*             |                    |
*             +--------------------+ 0x00 = task_cb *
* 
*  

Naming Conventions

  • aos_ - AOS prefix

Rule of thumb is that every

  • file
  • function
  • type definition
  • variable

should begin with this prefix. AOS API's conform to this rule.

Data structures and functions naming conventions.

System Context

Each memory block is independent and non overlapping with any other block belonging to a different task. Global system context is initialized out side the tasks stacks and is a global static variable. The system context stores current runlist, system status, currently active task and a lot more other internal settings. Please have a look at the struct aos_sys for more details.

System Hooks

AOS implements system hooks - you can attach custom routines to various events happening in the system. Please, have a look on the hooks_01.c example for details how to use them aos_constants.h ( aos_hook_t ) for a list of available hooks.

Scheduler

Aos tries to implement a simple priority based scheduler. Each task has a priority assigned to it. There are four possible priority levels (aos_task_priority_t)

  • AOS_TASK_PRIORITY_IDLE
  • AOS_TASK_PRIORITY_LOW
  • AOS_TASK_PRIORITY_NORMAL
  • AOS_TASK_PRIORITY_HIGH

The priority defines how much of an execution time slice a task will get once rescheduled. The relation between the priority and the assigned time slice is the following (aos_config.h):

  • AOS_SCHED_TASK_QUANTA_PRIORITY_HIGH 196
  • AOS_SCHED_TASK_QUANTA_PRIORITY_NORMAL 32
  • AOS_SCHED_TASK_QUANTA_PRIORITY_LOW 8
  • AOS_SCHED_TASK_QUANTA_PRIORITY_IDLE 2

That means, that the process with AOS_TASK_PRIORITY_NORMAL, when scheduled to run will own the CPU for 32 ticks. If a tick happens every 10 ms, that means this particular task will own the CPU for 320 ms, before any other task will take it's place. The priority defines the order in which the tasks are scheduled as well. AOS tries to mimic linux CFS scheduler - of course, it's nowhere near, since it's far less complex but it's based on the same assumptions. First of all task context blocks form a list, if you look at the task_cb declaration, the very beginning looks the following way:

  struct task_cb {

      /// task list pointers must be on top
      struct task_cb *prv, *nxt;
      ...

The scheduler operates on them and moves them around in so called "run lists" - those are formed out of two task queues (aos_run_list) - those will be described later on. Each task has it's own state. It defines what actually happens with it at the moment. The state can be one of the following (aos_task_state_t):

  /// task has been explicitly suspended from execution - it will not be scheduled
  AOS_TASK_SUSPENDED = 0,

  /// task is ready to be executed, waiting for it's time slice
  AOS_TASK_READY,

  /// task is currently running
  AOS_TASK_RUNNING,

  /// task is pauzed, it either wait's for hardware or for semaphore
  AOS_TASK_PAUZED,

  /// task has ended up, it won't be scheduled any more. 
  AOS_TASK_STOPPED,

AOS_TASK_SUSPENDED and AOS_TASK_PAUZED are very similar to each other. Task is in AOS_TASK_SUSPENDED state if it has been explicitly suspended (by itself or other task). Tash is in AOS_TASK_PAUZED if it has been suspended by the AOS itself (i.e. it's waiting for semaphore). When task is currently scheduled (it's time slice has not expired yet - it's greater than zero) then it's in the AOS_TASK_RUNNING state. If it's time slice has expired it's in the AOS_TASK_READY state waiting to be scheduled again. Ok let's get back to the scheduler again. The run list is contains two lists for each task priority (struct aos_run_list).

    struct task_cb *prio_tl[AOS_TASK_PRIORITY_IDLE][2];

Why two ? One is "expired", the other one is "active". There are two run-lists. The active and expired one. The active list is selected by the active field in the struct aos_run_list. The scheduler works in a way that it schedules the task in priority order from the active list and moves them to the expired list once they're time quanta goes down to zero. Once there are no tasks available in the active list the active variable is switched to select the expired list as the active one (active = (active + 1) & 0x01).

System timers

Please refer to the provided examples to learn the details about the AOS timer implementation.

Semaphores

Please refer to the provided examples to learn the details about the AOS semaphores implementation.

Lists

AOS implements lists data structers in a general way. This implementation is used by the scheduler itself but is flexible and generic enough to be used anywhere else. List nodes are implemented around aos_list_entity structure. The functions manipulating lists (aos_list_prepend, aos_list_append etc) only take care about the list pointers. You can define any custom struct and use the lists API to manipulate it as long as it has the nxt and prv pointers declared at the beginning. You can also the AOS_LIST_ALLOCATE macro to declare a memory area for your custom structure (it will automatically take into consideration the size of your data structure and the aos_list_entity size and allocate memory needed to hold the requested number of elements).

Let's consider that you have a datatype : struct my_struct { uint8_t x,y,z; };

If you want to have a list of those (and use aos API to manage it) you can either declare it using AOS_LIST_ALLOCATE:

  AOS_LIST_ALLOCATE(struct my_struct, my_list_elements, 32);

This will declare 32 elements for my_struct called my_list. At this stage it's not a list yet, it's just a declaration of 32 elements which can be linked together to form a list. You can use AOS list API to operate on them (bellow I'll declare an empty list and add a first element to it:

  struct aos_list_entity *my_list = NULL;

  aos_list_prepend(&my_list, (struct aos_list_entity *)&my_list_elements[0]));

At this stage my_list contains one element (my_list_element[0]). You can cast back to access the original type, using the AOS_LIST_CAST macro:

  AOS_LIST_CAST(struct my_struct, &my_list_elements[4])->x = 123;
  AOS_LIST_CAST(struct my_struct, &my_list_elements[4])->y = 124;
  AOS_LIST_CAST(struct my_struct, &my_list_elements[4])->z = 125;

or simply:

  ((struct my_struct *)my_list->data)->x = 123;

Another, simpler way to use AOS list API would be to simply add the required fields in the beginning of your declaration, and use it directly:

  struct my_struct2 {
      // list specific data - MUST BE on TOP
      struct my_struct2 *prv, *nxt;

      uint8_t x,y,z;
  };

  struct my_struct2 my_struct_elements[32];

That's it. You can use AOS list API with data type - since it's an enhanced type of aos_list_entity:

  aos_list_prepend(&my_list, (struct aos_list_entity *)&my_struct_elements[0] );

Preparing AOS project

AOS compiles itself into a static library (libaos.a). In order to build itself it needs the libpca.a as well (it is build along). So, you need both libpca and AOS itself to build your AOS powered application. A typical project looks the following way:

* 
*   -+-
*    |
*    +-- pca
*    |
*    +-- aos
*    |
*    +-+ your_application
*      |
*      +-- Makefile
*      |
*      +-- ...
* 
* 

Let's prepare a simple environment for the "blinking led" AOS powered application.

  1. Obtain libpca:

    git clone git@g.nosp@m.ithu.nosp@m.b.com:dagon666/avr_Libpca pca

  2. Obtain AOS:

    git clone git@g.nosp@m.ithu.nosp@m.b.com:dagon666/avr_Aos aos

  3. Create your application directory:

    mkdir blink && cd blink

  4. Now it's time to create the application itself. Let's start from the Makefile:
     TARGET=blink
     SOURCES= \
            main.c
    
     DEPS=
     COBJ=$(SOURCES:.c=.o)
     ASRC=$(SOURCES:.c=.s)
    
     PCA_PREFIX=../pca
     AOS_PREFIX=../aos
    
     CC=avr-gcc
     AS=avr-as
     STRIP=avr-strip
     OBJC=avr-objcopy
     MCU=atmega328p
     CFLAGS=-I. -I$(PCA_PREFIX)/include/ -I$(AOS_PREFIX) -Wall -Wno-strict-aliasing -Os -DF_CPU=16000000UL -std=gnu99 \
          -ffunction-sections \
          -fdata-sections \
          -ffreestanding \
          -fno-tree-scev-cprop \
          -fpack-struct
     LDFLAGS=-laos -lpca -L$(AOS_PREFIX) -L$(PCA_PREFIX) -Wl,--gc-sections,--relax,-u,vfprintf -lprintf_flt -lm
    
     ISPPORT=/dev/ttyACM0
     ISPDIR=/usr/share/arduino/hardware/tools
     ISP=$(ISPDIR)/avrdude
     ISPFLAGS=-c arduino -p $(MCU) -P $(ISPPORT) -b 115200 -C $(ISPDIR)/avrdude.conf 
    
     all: $(TARGET)
    
    
     %.o: %.c $(DEPS)
       @echo -e "\tCC" $<
       @$(CC) -mmcu=$(MCU) -c -o $@ $< $(CFLAGS)
    
    
     libpca.a:
       @echo -e "\tBUILDING PURE C ARDUINO LIB"
       $(MAKE) -C $(PCA_PREFIX)
    
    
     libaos.a:
       @echo -e "\tBUILDING AOS"
       $(MAKE) -C $(AOS_PREFIX)
    
    
     $(TARGET): $(COBJ) libpca.a libaos.a
       @echo -e "\tLINKING CC" $<
       @$(CC) -mmcu=$(MCU) -o $(TARGET) $(COBJ) $(LDFLAGS)
       $(OBJC) -O ihex -R .eeprom $(TARGET) $(TARGET).hex
       $(OBJC) -O binary -R .eeprom $(TARGET) $(TARGET).bin
    
    
     clean:
       $(MAKE) -C $(PCA_PREFIX) clean
       $(MAKE) -C $(AOS_PREFIX) clean
       @echo ========== cleanup ========== 
       rm -rf *.s *.o *.bin *.hex a.out $(TARGET)  
    
    
     read:
       $(ISP) $(ISPFLAGS) -U flash:r:$(TARGET)_backup.hex:i
    
    
     install:
       $(ISP) $(ISPFLAGS) -U flash:w:$(TARGET).hex
    

This is a very generic Makefile which compiles libpca libaos and links them with the application code. It also contains some flashing targets. You can use make install to flash the code to the arduino (or atmega - as long as it has the bootloader running).

AOS uses a hardware timer and it's interrupt as a tick source. You can tell which timer to use by editing the aos_config.h file (AOS_SCHED_TIMER). By default it's configured to timer 2. Remember when changing this value to any other to reconfigure libpca as well. If you choose to use timer 0 as a tick interrupt source you must remember to disable any interrupt routines implemented for that timer in libpca (TDELAY_IMPLEMENT_T0_INT), you may observe problems otherwise.

I'll use one of the examples delivered with AOS as an application code:

  cp ../aos/examples/generic_01.c main.c

That's it. The project should now build successfully.