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:
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: |
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. |
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:
aOS_uptime
variable, formats it to a string, and sends the string in a message to the
third task.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:
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.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:
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.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.
aos.h
user_task.h
make modification
of aos_core.c
unnecessary.aos_tasks.c
:
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. |
Download:
Four tasks are defined in the test program:
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.Steps to configuring aOS.
#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 128Each 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.
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.
#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.
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:
aos_sleep
is called
is about 620 cycles. Task code.aos_wait
is called
is about 680 cycles. Task code.aos_signal
)
is about 710 cycles. Task code. _aos_schedule
is needed, and about 1200 cycles if scheduling
occurs. Note that these numbers will depend on number of tasks and the relative
synchronization of sleep times. To get these numbers, timer1 start/stop commands
were inserted into the timer0 compare-match ISR. Timer1 is then read and printed
in the highest priority task. The printing task should be considered a critical
section. 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:
aos_uptime
is in
scheduler ticks). The divide by 1024 was necessary to keep timer1 from possibly
overflowing on any task at a clock rate of 16 MHz. To turn on timing, set
AOS_CHCK
to 1
in the modified aos.h
.
An example with 4 tasks (plus the null
task) shows how to use the timer. The lowest priority task (task 4) reads
the task control blocks, formats the times, and sends them to the UART. Don't
use these modifed versions of aOS (with AOS_CHCK==1
) for routine
programming unless you are very sure you will not use timer1 for any other
purpose.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: |
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:
aos_uptime
is in
scheduler ticks). The divide by 1024 was necessary to keep timer1 from possibly
overflowing on any task at a clock rate of 16 MHz. To turn on timing, set
AOS_CHCK
to 1
in the modified aos.h
.
An example given below with 4 tasks (plus
the null task) shows how to use the timer. The lowest priority task (task
4) reads the task control blocks, formats the times, and sends them to the
UART. Don't use aOS with AOS_CHCK==1
for routine programming
unless you are very sure you will not use timer1 for any other purpose.Configuring aOS
#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 1Each 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.
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.
#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.
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.
#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_ENDTo 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:
AOS_GOLBAL_VAR
macro found in aos.h
below.
To use the command shell, you need to download new files for the following
Copyright Cornell University Sept 2005