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:
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) 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.

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 16Sept2005.
    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. Be sure to set your terminal emulator to not send linefeed at the end of a line.
    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. 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. 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.

  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 "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.
  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), DO NOT USE the aOS timer tick as a reference. It is not accurate enough.
  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
    #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

  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. Be sure to set your terminal emulator to not send linefeed at the end of a line.
  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.

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
Example: I 18 f0
Turns on LEDs 0 to 3 attached to PORTB.

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: t, s, x, d, h, w.
Example: t 4 x
Suspends task 4

t
total task execution time since RESET. Note that if you set AOS_CHCK to zero in aos.h then this operation will not return a value
s
task status: see aos.h for task status values definitions.
x
suspend a task. Freezes a task until it is resumed by the command shell or by another task.
r
resume a suspended task.
d
data stack free bytes for this task. The portion of the task data stack which has never been used.
u
hardware stack free bytes for this task. The portion of the task hardware stack which has never been used.
w
wait time remaining from a requested aos_sleep for the specified task

 

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.
Example: R 4 0 aa
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.
Example: M 7d0 aa
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.
Example: s 3 q

c
Display semaphore count.
q
Display next pending task number. Only semaphores with count=0 will have pending tasks. Semaphores with count>0 return 0xff.

 

 


Copyright Cornell University sept 2005