Cornell University ECE476
A preemptive, multitasking OS
for Atmel Mega32 microcontrollers
Introduction
It is useful to be able to write several small tasks running on a microcontroller, as if each one were the only program running. For instance, if you were running several communications protocols, it would be nice if each could run without worrying too much about the operation and timing of the other protocols. It is also handy if you have several state machines running at different rates. Obviously, only one task at a time can actually execute on the MCU. To make each task think it owns the whole machine, the state of the task has to be saved while other tasks execute. The swapping process, as well as other functions (e.g. intertask communication) are handled by an operating system (OS). This page describes a very small, fairly fast OS for the Mega32. While an OS makes some aspects of programming easier, there are many times that an MCU cannot run an OS and meet requirements for timing or memory use. For instance, if you are doing one or two very fast operations, like generating video, don't use an operating system.
An operating system This code tested in codevision 1.24.6 and 1.25.2. See the experiments page for older versions.
The operating system I will describe, 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:
There are several requirements and limitations to apparent task independence on one cpu:
The API supplied by Ylätalo includes core realtime routines. We have written some extensions to aOS to make it more general. See the experiments page for the development history and some performance measurements. There is a separate page describing a simple debugger for a single user-written task.
Task-related functions:
aos_tcb *aos_task_create( void(*task)(void),
UBYTE *hwstk, UBYTE *swstk, UBYTE pri ) |
Identifies 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) and transfers control to another task. |
void aos_sleep_task(*task,ticks) |
Allows one task to force another to sleep for a number of clock ticks |
void aos_unsleep_task(aos_tcb *task) |
Allows a task to wake up another task by forcing its
tick count to zero.
|
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. |
UBYTE aos_task_getstatus(aos_tcb *task) |
Returns an integer which indicates the exectuion state
of the task. By refering to aos.h for the definitions, you
can determine if the task is running, delayed, waiting, runnable, or suspended. |
void aos_sched_lock() |
Locks the scheduler so that the current task cannot be prempted. Use this with care. Locking the scheduler, then calling any of the functions which might cause a context switch will crash the system! |
void aos_sched_unlock() |
Unlocks the scheduler. This is the normal condition. |
Intertask communication functions:
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. Do not use this function in an ISR, use aos_sem_accept instead. Otherwise the ISR will hang if the semaphore is not available (count=zero). |
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. |
UWORD aos_sem_accept( aos_scb *semaphore ) |
Returns the count of the specified semaphore. If the semaphore has a nonzero value, the value is decremented. Your task must check the return value. This function does not block. You can use this function in an ISR. |
aos_mcb *aos_mbox_create( void ) |
Creates a mailbox. Returns either the mailbox control block or null. Each mailbox creates two semaphores for message send/receive control. The receive semaphore is created first. |
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. Do not use this function in an ISR, use aos_mbox_accept instead. Otherwise the ISR will hang if there is no message available. |
void *aos_mbox_accept( aos_mcb *mcb ) |
Read a message from mailbox without blocking. If there the mailbox is empty, the function returns a NULL. If there is a message, it returns a void pointer to the message. Your task must check the return value. This function does not block. You can use this function in an ISR. |
UART-related functions:
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(long baud) |
Sets up the UART speed and turns on receive and transmit enable
bits. The UART baud rate is set by the parameter. The calculation of UBRRL
value in the routine depends on setting the correct crystal speed in the
Project...Config dialog. Other parameters default to 1 stop
bit, no flow control, no parity. Two semaphores are defined, one for transmit
and one for receive. The transmit semaphore is defined first. Be sure to set your terminal emulator to not send linefeed at the end of a line. |
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. If the UART is in use by another task, this macro
will cause a context change. |
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. If the UART is in use, this macro will cause a context change. |
Diagnostic functions for aOS:
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. A dispatch counter was added, and the ability to time tasks.
ULONG aos_uptime( void ) |
Returns the number of ticks since aOS started as an unsigned long. |
long aos_getdispatch(void) |
Returns the number of times the dispatcher has run. |
void aos_setdispatch(ULONG count) |
Sets the dispatch counter. |
long aos_task_gettime(aos_tcb *task) |
If the AOS_CHCK flag is set in aos.h ,
then task execution times are accumulated. This function returns the time.
Note that timer1 is used. |
void aos_task_settime(aos_tcb *task, ULONG time) |
If the AOS_CHCK flag is set in aos.h,
then task execution times are accumulated. This function sets the time.
Note that timer1 is used. |
int aos_task_dstk_chck(aos_tcb *task) |
Rreturns the number of bytes on the task 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 task hardware stack which have not been used since RESET. It is the hardware stack high-water mark |
Usage notes for diagnostic functions.
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.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. The UART also 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 "aos_semaphore.c" #include "aos_mbox.c" #include "aos_uart.c"Each task should be written as a void function with void parameters. The 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), DO NOT USE the aOS timer
tick as a reference. It is not accurate enough.
#pragma
regalloc-
option to force variables into RAM. Variables in registers
will not be shared between tasks. For example you can use
#pragma regalloc- UWORD sleep_time ; #pragma regalloc+To force a variable into RAM.
Command Shell for aOS
We wrote an elementary command shell for aOS as a high-priority task. At reset, the main application runs by default. You can enter the command shell from a running task or from a UART attached terminal as shown in the table below. In the shell you can manipuate tasks, I/O registers and data registers. Note that the command shell task is created in the aos_core.c file. The version is 18Sept05.
To use the command shell, you need to download new files for the following
Shell commands:
h |
Prints a short version of this table | ||||||||||||||
g |
Exit command shell and run other tasks | ||||||||||||||
<control-c> |
Stop other tasks and enters command shell with BreakID=255 | ||||||||||||||
break(char BreakID) |
When used in other tasks, this macro enters the command shell
with the specified BreakID. Example: break(4) |
||||||||||||||
x |
Forces a RESET of the MCU and trashes the state of your program. | ||||||||||||||
i ioregAddress |
Read an i/o register. The register address is entered in hexadecimal.
The result displayed in in hex. Example: i 13 Reads the state of the pushbuttons attached to PINC. |
||||||||||||||
I ioregAddress iodata |
Write to an i/o register. The register address and data are entered in
hexadecimal |
||||||||||||||
t tasknumber query |
Perform an operation on a task. The task number is an index which is
assigned when new tasks are created by the command shell at RESET. The first task (command shell) has index 1. The
query parameter can take the values:
|
||||||||||||||
r tasknumber datareg |
Read task data register. The register number is in decimal.
The result displayed is in hex. The task number is an index which is
assigned when new tasks are created by the command shell at RESET. The first task (command shell) has index 1. Remember that C uses register 0 and registers 22-31 for intermediate results. Registers 16-21 are used to store local variables in functions. You have to look at the map file to see where golbal variables are stored. By default, Codevision stores the first few variables you
define into registers.Example: r 4 1 Displays the value 55 if the demo program is running and button
zero is pressed to enter the command shell. This is because task 4 writes
a 0x55 to data register one just before it calls the break(4)
macro |
||||||||||||||
R tasknumber datareg data |
Write task data register. The register number is in decimal.
The data is in hex. The task number is an index which is
assigned when new tasks are created by the command shell at RESET. The first task (command shell) has index 1.
Writes 0xaa to task 4 register 0 |
||||||||||||||
m address |
Read memory. Address is given in hex. You have to look at the map file to see where golbal variables are stored.
By default, Codevision stores the first few variables you
define into registers.Example: m 7d0 Displays the value 55 if the demo program is running and button
zero is pressed to enter the command shell. This is because task 4 writes
a 0x55 to 7d0 just before it calls the break(4)
macro |
||||||||||||||
M address data |
Write data to memory. Address and data are in hex. Writes 0xaa to address 7d0 . Remember that
there is NO memory protection, so you can easily trash the system with this
command. |
||||||||||||||
s semaphorenumber query |
Gets information about semaphores. This command orders the semaphores
with semaphore zero being the last semaphore defined in the code.
Remember that initialziing the UART creates two semaphores and each mailbox
creates two more semaphores. You can use this command to query the status
of a mailbox by looking at its semaphores. The query options are c and
q.
|
Copyright Cornell University sept 2005