Cornell University
Electrical Engineering 476
Experiments with a preemptive, multitasking OS
for Atmel Mega32 microcontrollers

Introduction

This page is a revision history of the modifications I have made to aOS.

The operating system

One operating system I am experimenting with, called aOS, was written by Anssi Ylätalo. aOS seems to be based on uC/OS-II written by Jean Labrosse. Labrosse wrote a very nice book on the subject. This operating system has several characteristics which make it interesting for ECE476:

Example 1 -- Blinking LEDs, message passing, and default UART communication (with semaphore protection)

A test program uses all of the aOS user function calls. The program is based on a slightly modifed version of the user task code written by Ylätalo. The aOS versions I used are:

Four tasks are defined in the test program:

  1. The first task sets up semaphores and message pipe, toggles an LED and sends its stack pointer to the UART. Since another task is also using the UART, the tranmission is protected by a semaphore.
  2. The second task toggles an LED, reads the global aOS_uptime variable, formats it to a string, and sends the string in a message to the third task.
  3. The third task waits for the message, then waits for the UART and sends the uptime string to the UART.
  4. The forth task executes a couple of long loops and causes a wave activity across 4 LEDs.

The NULL TASK blinks an LED, but in a real application would probably do nothing. Be sure to set the stack size to 200 in the Project..Configure..Compile dialog. On the STK500, you will need to attach portB to the LEDs and attach a serial connection to a PC.

Example 2 -- Improved UART handler using interrupts with packaged semaphore protection

This test program uses a short additional set of routines to handle the UART. There is a UART initialization routine, two macros, and two ISRs in the aos_uart.c module. The macros implement the setup for put-string and get-string functions, including semaphore protection. The ISRs execute respectively when an incoming chararter is ready or when the UART is ready for another outgoing character. The aos_core module was modified slightly to move UART initialization to the aos_uart module. Be sure to set the stack size to 200 in the Project..Configure..Compile dialog. On the STK500, you will need to attach portB to the LEDs and attach a serial connection to a PC. The new UART functions are:

void aos_init_uart(void)
Sets up the UART speed and turns on receive and transmit interrupts. The UART defaults to 9600 baud, 1 stop bit, no flow control, no parity. Two semaphores are defined, one for transmit and one for receive.
aos_gets(str)
The task which calls this macro will wait until the ENTER key is pressed. The typed string (minus the ENTER) will be returned to the task in str. Another task which requests a string will wait for UART access.
aos_puts(str)
The task which calls this macro will send a string, str, to the UART. Another task which tries to send a string will wait for UART access.

The modified aos_core and aos_uart modules are linked below.

Four tasks are defined in the test program:

  1. The first task sets calls aos_init_uart(), toggles an LED and sends aOS_uptime to the UART. Note that all semaphore locking of the UART is done by the aos_uart code.
  2. The second task toggles an LED, reads a string from the UART, and sends the string in a message to the third task.
  3. The third task waits for the message, then waits for the UART and sends the string to the UART.
  4. The forth task executes a couple of long loops and causes a wave activity across 4 LEDs.

With the Project..Configure..Compile parameter set to Compile for speed, this example runs with a 'heart-beat' interrupt interval of 1mSec (this is set by the value of OCR0 in aos_core), but fails with an interval of 0.70 mSec. I am guessing that there is an interaction between the timer0 ISR and the UART ISRs, but I have not explored this. See Example 5 for more detailed performance measurements.

Example 3 -- Memory sharing with semaphore protection, and a new routine aos_sleep_task

This test program uses a small function to allow one task to force another to sleep. To do this, you need to find a pointer to the task control block you want to change. This example shows how to do that. The program also allows two tasks to write to one memory location, protected by semaphores, so that the memory is not corrupted.

Four tasks are defined in the test program:

  1. The first task sets calls aos_init_uart(), gets its own control block pointer, then loops while toggling an LED and sending aOS_uptime to the UART. Note that all semaphore locking of the UART is done by the aos_uart code, as in the last example.
  2. The second task toggles an LED, reads a string from the UART, and sends the string in a message to task 3.
  3. The third task waits for the message, then waits for the UART and sends the string to the UART. It also checks a couple of buttons in order to set the global variable which changes the sleep time of task 1. Note that the buttons won't respond until a string is received.
  4. The forth task executes a couple of long loops and causes a wave activity across 4 LEDs. It checks three buttons. Two affect the global variable which changes the task 1 sleep time. The third button causes task 4 to put task 1 to sleep directly.

Example 4 -- Every file has changed! There is more flexible task handling, easier specification of header files, and a couple of modification to the core routines.

This new version of all the files has several improvments.

Download:

Four tasks are defined in the test program:

  1. The first task sets calls aos_init_uart(), gets its own control block pointer, then loops while toggling an LED and sending aOS_uptime to the UART. If button 4 is pushed, the task suspends itself. Also, the task sleep time can be set by two other tasks.
  2. The second task toggles an LED, reads a string from the UART, and sends the string in a message to task 3.
  3. The third task waits for the message, then waits for the UART and sends the string to the UART. It also checks a couple of buttons in order to set the global variable which changes the sleep time of task 1. Note that the buttons won't respond until a string is received.
  4. The forth task executes a couple of long loops and causes a wave activity across 4 LEDs. It checks five buttons. Two affect the global variable which changes the task 1 sleep time. The third button causes task 4 to put task 1 to sleep directly. Two other buttons suspend/resume task 1.

Steps to configuring aOS.

  1. In aos.h you may want to modify:
  2.  #define AOS_TICK_TIME			10  // In mSec
    #define AOS_SEM_MAX			5
    #define AOS_TASK_MAX			5
    #define AOS_MBOX_MAX			2
    #define AOS_TASK_HSTK_SIZE		128
    #define AOS_TASK_DSTK_SIZE		128
      
    Each mailbox requires two semaphores. I would not set the tick time below about 2 mSec. You can experiment with the task stack sizes to determine the minimum which will allow your application to run, but in any case, it will take at least 40 bytes or so to store the task context.
  3. In user_task.h you will need to modify the names of the tasks and the defines which identify the first task to aos_core.c.
  4. Write a file with your tasks in it with the includes at the beginning:
    #include < mega32.h >   
    #include < stdio.h >                                            
    #include "aos.h"  
    #include "user_tasks.h"
    #include "aos_core.c" 
    #include "aos_task.c"                   
    #include "semaphore.c"
    #include "mbox.c" 
    #include "aos_uart.c"
      
    Each task should be written as a void function with void parameters. See example above. This example also shows how to define a mailbox and a semaphore, how to obtain a task control block and how to use the new functions.
  5. Be sure to set the stack size as small as possible in the Project..Configure..Compile dialog. The compiler will complain if you set it too low, but try for 200 or less. It may be useful to optimize for speed.

Example 5 -- Performance of aOS.

Using the aOS routines from Example 4, but with new tasks defined, we can estimate task switching performance. Measurements:

If we run the time-tick at 10 mSec (160,000 cpu cycles), then the time-tick overhead is
1200/160000x100 = 0.75%
assuming the worst case of a scheduler operation at each tick. As a cross-check on timing, I reduced the tick-time to determine when the scheduler failed. The UART ISR competes with the timer0 ISR, so when the UART routines are used the minimum tick-time is 0.7 mSec. I removed all UART calls, and only blinked lights. With four tasks, all blinking lights, the minimum reliable tick-time was around 0.13 mSec, which is consistent with the timing estimates above. Below that time, the scheduler runs, but there is a lot of jitter due to the details of the specific schedule operation.

Example 6-- Using task 1 as a round-robin scheduler.

Using the aOS routines from Example 4, but with new tasks defined, we can force aOS to be a round-robin scheduler. Task 1 (highest priority) sequentially suspends all but one task at a time. The time slice is determined by the amount of time task 1 sleeps between sequential task suspensions. Note that round-robin scheduling should be used with care, particularly when there is event-driven communication between tasks.

Example 7 -- Measuring the cpu time used by each task.

By slightly modifying aos.h and aos_core.c we can:

Example 8 -- Measuring the stack space used by each task.

We can put in hooks to measure the maximum stack useage by adding a couple of fields to the task control block and adding two functions. Load modified versions:

The two new functions are:

int aos_task_dstk_chck(aos_tcb *task)
Rreturns the number of bytes on the data stack which have not been used since RESET. It is the data stack high-water mark
int aos_task_hstk_chck(aos_tcb *task)
Returns the number of bytes on the hardware stack which have not been used since RESET. It is the hardware stack high-water mark

If either of these functions returns zero, your application will crash because one task will start to overwrite another task's stack. You can use the numbers returned by these two functions to tune the stack size required for your tasks. The stacks are checked by starting at the bottom of the allocated stack memory and scanning upward, checking each byte, until a nonzero byte is detected. A nonzero byte indicates that the stack reached that point at least once. Notice that it is extremely unlikely that the hardware stack would ever have a zero pushed on it, but the data stack check could give misleading results occasionally because of pushed zeros. The example program has three short tasks defined, plus a fourth task which prints out statistics. As in Example 7, if you set AOS_CHCK to 1 in aos.h, then you cannot use timer1 for anything else. You can always get the stack use for each task and check the number of dispatches which have occured. Only task timing is affected by AOS_CHCK. Performance of aOS is slightly affected by the dispatch rate code, since a counter has to be incremented on each dispatch.

A version of the Example 4 task code (but with stack checking using the Example 8 core routines) will be used as an example in class.

Example 9 -- This is the 5March2005 version. There is one major bug fix in the system tmer ISR plus a few new routines.

aos_tcb *aos_task_create( void(*task)(void), UBYTE *hwstk, UBYTE *swstk, UBYTE pri )

Indentifies a function as a task in aOS. The input parameters are:
1.Function pointer to task.
2. Pointer to task’s hardware stack.
3. Pointer to task’s software stack.
4. Task’s priority.
Returns either a pointer to task’s TCB, or a null pointer.

void aos_sleep( UWORD timerticks ) Puts the current task to sleep for a number of timer ticks
(typically 10 mSec/tick)
ULONG aos_uptime( void ) Returns the number of ticks since aOS started as an unsigned long.
aos_scb *aos_sem_create( UWORD count ) Creates a semaphore with initial state equal to count. Returns either the semaphore control block or null.
void aos_wait( aos_scb *semaphore ) Causes the task to wait until the specified semaphore has a nonzero value. If the semaphore has a nonzero value, the value is decremented and the current task proceeds.
void aos_signal( aos_scb *semaphore ) Increments the specified semaphore by one and triggers a check to see of a context switch is nesessary to a signaled task.
aos_mcb *aos_mbox_create( void ) Creates a mailbox. Returns either the mailbox control block or null.
void aos_mbox_send( aos_mcb *mailbox, void *message ) Send a message to mailbox. If there is no room in the mailbox, the calling task is suspended until the message is read.
void *aos_mbox_recv( aos_mcb *mailbox ) Read a message from mailbox. If there the mailbox is empty, the calling task is suspended until a new message is sent to mailbox. Returns a void pointer to the message.

 

There are three additional routines to handle the UART. There is a UART initialization routine, two macros, and two ISRs in the aos_uart.c module. The macros implement put-string and get-string functions, including semaphore protection. The ISRs execute respectively when an incoming chararter is ready or when the UART is ready for another outgoing character. The aos_core module was modified slightly to move UART initialization to the aos_uart module. The new UART functions are:

void aos_init_uart(void)
Sets up the UART speed and turns on receive and transmit interrupts. The UART defaults to 9600 baud, 1 stop bit, no flow control, no parity. Two semaphores are defined, one for transmit and one for receive.
aos_gets(str)
The task which calls this macro will wait until the ENTER key is pressed. The typed string (minus the ENTER) will be returned to the task in str. Another task which requests a string will wait for UART access.
aos_puts(str)
The task which calls this macro will send a string, str, to the UART. Another task which tries to send a string will wait for UART access.

Routines were added to help control task execution. There are routines to suspend/resume tasks and to allow one task to sleep another.

void aos_sleep_task(*task,ticks)
Allows one task to force another to sleep for a number of clock ticks
void aos_suspend_task(*task)
Makes a task inactive. If this function is called from the same task which is being suspended, the scheduler is called. If a suspended task is signaled by a semaphore, it is resumed. The sleep time counter for a task is frozen when it is suspended.
void aos_resume_task(*task)
Reactivates a task (only if the task has previously been suspended). If the task to be reactivated has priority over the current task, the scheduler called. If a suspended task is signaled by a semaphore, it is resumed. Resumed tasks do not restart, but continue from the point at which they were suspended.

We have put in hooks in aOS to measure the maximum stack useage by adding a couple of fields to the task control block and adding two functions. The two new functions are:

int aos_task_dstk_chck(aos_tcb *task)
Rreturns the number of bytes on the data stack which have not been used since RESET. It is the data stack high-water mark
int aos_task_hstk_chck(aos_tcb *task)
Returns the number of bytes on the hardware stack which have not been used since RESET. It is the hardware stack high-water mark

If either of these functions returns zero, your application will crash because one task will start to overwrite another task's stack. You can use the numbers returned by these two functions to tune the stack size required for your tasks. The stacks are checked by starting at the bottom of the allocated stack memory and scanning upward, checking each byte, until a nonzero byte is detected. A nonzero byte indicates that the stack reached that point at least once. Notice that it is extremely unlikely that the hardware stack would ever have a zero pushed on it, but the data stack check could give misleading results occasionally because of pushed zeros. An example program has three short tasks defined, plus a fourth task which prints out statistics. If you set AOS_CHCK to 1 in aos.h, then you cannot use timer1 for anything else. You can always get the stack use for each task and check the number of dispatches which have occured. Only task timing is affected by AOS_CHCK. Performance of aOS is slightly affected by the dispatch rate code, since a counter has to be incremented on each dispatch.

There are also hooks to:

Configuring aOS

  1. Download the following files and put them on one directory. Configure a project which points to demoTasks.c as the source file. Be sure to minimize the size of the stack and to turn on the printf option to print long integers. The version is 5March2005.
    1. aos.h contains general definitions. Stack lengths may need to be tuned for a given application.
    2. user_tasks.h contains the names and stack definitions for the actual tasks that you will write.
    3. aos_core.c contains the OS. The first task is created in this file based on defines in user_task.h.
    4. aos_task.c contains the code to create a task, plus some other task handling functions.
    5. semaphore.c contains the semaphore code. This can be eliminated if you do not use semaphores or mailboxes
    6. mbox.c contains mailbox code.
    7. aos_uart.c contains the UART initialization and string send/receive functions.
    8. demoTasks.c which is a demo code. The code assumes buttons on PINC, LEDs on PORTB, and UART on D0 and D1. The demo prints the system uptime, the task1 stack use, takes user typing and echos it via a message, prints aOS dispatches/sec, prints the cpu time of each task, and uses buttons to modify the execution of task 1.
  2. In aos.h you may want to modify:
  3.  #define AOS_TICK_TIME			10  // APPROXIMATE tick time in mSec
    #define AOS_SEM_MAX			5
    #define AOS_TASK_MAX			5
    #define AOS_MBOX_MAX			2
    #define AOS_TASK_HSTK_SIZE		128
    #define AOS_TASK_DSTK_SIZE		128
    #define AOS_CHCK		   	1
      
    Each mailbox requires two semaphores. I would not set the tick time below about 2 mSec. You can experiment with the task stack sizes to determine the minimum which will allow your application to run, but in any case, it will take at least 40 bytes or so to store the task context. AOS_CHCK turns on timer1 in the kernel to time tasks. Set this flag to zero for better performance, or if you need to use timer1 for any other function.

  4. In user_task.h you will need to modify the names of the tasks and the defines which identify the first task to aos_core.c.
  5. Write a file with your tasks in it with the includes at the beginning:
    #include < mega32.h >   
    #include < stdio.h >                                            
    #include "aos.h"  
    #include "user_tasks.h"
    #include "aos_core.c" 
    #include "aos_task.c"                   
    #include "semaphore.c"
    #include "mbox.c" 
    #include "aos_uart.c"
      
    Each task should be written as a void function with void parameters. See example above. This example also shows how to define a mailbox and a semaphore, how to obtain a task control block and how to use all aOS functions.
  6. Be sure to set the stack size as small as possible in the Project..Configure..Compile dialog. The compiler will complain if you set it too low, but try for 200 or less. It may be useful to optimize for speed. Do NOT modify any registers affecting timer0. If the tick time is critical for some aspect of your code (e.g. realtime clock), you will need to set it carefully, or use another timer for accurate time keeping.
  7. When defining shared, global variables, be sure to use the #pragma regalloc- option to force variables into RAM. Variables in registers will not be shared between tasks. For example you can use
    AOS_GLOBAL_VAR
    UWORD sleep_time  ;  		
    AOS_GLOBAL_END  
    
    To force a variable into RAM.

Example 10 The following two sets of code compile with the 1.23.8 version of the compiler. This is the 21march2005 version.

The two sets of code are for a generic example which uses every user-available system call, and for a command line interface to aOS. The main changes in the compiler which made this version obsolete are:

  1. Download the following files and put them on one directory. Configure a project which points to demoTasks.c as the source file. Be sure to minimize the size of the stack and to turn on the printf option to print long integers. The version is 21March2005.
    1. aos.h contains general definitions. Stack lengths may need to be tuned for a given application.
    2. user_tasks.h contains the names and stack definitions for the actual tasks that you will write.
    3. aos_core.c contains the OS. The first task is created in this file based on defines in user_task.h.
    4. aos_task.c contains the code to create a task, plus some other task handling functions.
    5. aos_semaphore.c contains the semaphore code. This can be eliminated if you do not use semaphores or mailboxes
    6. aos_mbox.c contains mailbox code.
    7. aos_uart.c contains the UART initialization and string send/receive functions.
    8. demotasks.c which is a demo code. The code assumes buttons on PINC, LEDs on PORTB, and UART on D0 and D1. The demo prints the system uptime (if button 2 is pushed), the task4 stack use, takes user typing and echos it via a message, prints aOS dispatches/sec, prints the cpu time of each task (except the NULLTASK which prints time/sec), and uses buttons to modify task execution
  2. To use the command shell, you need to download new files for the following

    1. aos.h contains general definitions. Stack lengths may need to be tuned for a given application.
    2. user_tasks.h contains the names and stack definitions for the actual tasks that you will write.
    3. aos_core.c contains the OS. The command shell task is created in this file based on defines in user_task.h.
    4. aos_task.c contains the code to create a task, plus some other task handling functions.
    5. aos_semaphore.c contains the semaphore code. This can be eliminated if you do not use semaphores or mailboxes
    6. aos_mbox.c contains mailbox code.
    7. aos_uart.c contains the UART initialization and string send/receive functions. Modified to support <control-c> to shell
    8. demotasks.c which is a demo code. The code assumes buttons on PINC, LEDs on PORTB, and UART on D0 and D1. The first task is the command shell. Feel free to modify it to your specifications. The second and third tasks exercise the UART interface as above. Task four just blinks a LED, unless you press button zero, which exits to the shell.


Copyright Cornell University Sept 2005