ISOS

 

News

What's this?

Screenshots

Documentation

Downloads

License

Author

About this document:

The aim of this document is to give a quick overview about what ISOS is and to rapidly present the principles behind it. Some knowledge about operating systems principles is assumed. This document aims at presenting ISOS to people who already have some knowledge about operating systems, not to explain how operating systems work. It is however very simple for an operating system and it can be helpful for people interested in operating systems to read its source and understand it.

For the API documentation, a doxygen generated documentation is included in the package that can be found on the download page.

1. Technical overview:

1.1. Threads:

A thread is a block of executable code with its own stack. It is similar to a task although there is no separate memory space for each thread.

In ISOS, the kernel maintains a static array of 32 thread structures. Since one thread is automatically created and used by the kernel (the idle thread), there is a maximum of 31 threads left to be created by the user.

The multi-threading in ISOS is preemptive meaning that each thread is assigned an amount of time to run before it is interrupted to leave the CPU to another thread. The thread switching is done every 1/100th of a second. The scheduler called each time is responsible for choosing the next thread to run. If there is no other thread to run the scheduler will run the idle thread which does nothing.

Threads do not have priorities. All threads are run in a sequential order (round-robin) except the idle thread which is only run when there is no other thread to run.

The thread structure is defined as below:

struct thread_t {
const char *name;
uint32 *stack_top;
uint32 *stack_current;
uint32 *stack_bottom;
volatile thread_state_t state;
uint32 wakeup_time;
uint32 cpu_usage;
};

This structure contains the information necessary for the kernel to manage threads. In particular "stack_bottom" is a pointer to the thread's stack where its execution context and local variables are stored. The variable "state" defines the current state of the thread. If "state" is equal to DEAD, there is no thread assigned to this structure and the rest of the structure's data is meaningless.

The following diagram illustrates the different thread's states and the possible transitions between them:

Thread's states
Figure1: Thread's states

  • DEAD means that there is no thread associated to the thread_t structure.
  • When a thread is created, its stack is allocated by the kernel and the thread structure is initialized. The thread's initial state is always READY. A READY thread can be executed.
  • If a thread calls the function thread_sleep_self(uint32 centisecs) it is put into the SLEEPING state and will not be run until the specified amount of time has passed. When the time has come, the thread is put back into the READY state and is executed as the other threads.
  • A thread can be put into the BLOCKED state when waiting on a condition lock or trying to acquire a semaphore. It is put back into the READY state when the condition lock is signaled or the semaphore is released by another thread.
  • If a thread returns, its stack is freed and its state set to DEAD. The thread_t structure does no longer represent a valid thread and can be used if another thread is created.

Note: There is no "RUNNING" state. Only one of the READY threads is running at a time, it is identified in the kernel by the variable "s_running_thread" which is an index in the threads structures table.

1.2. Alarms:

Alarms are a mechanism by which a function execution can be programmed at a later point in time. The alarm_add() function must be called to program an alarm. It takes three arguments:
- the first one is the function that should be called (the alarm function must receive a void pointer an return nothing)
- the second argument is the time to wait before calling the function (the time must be given in 1/100th of seconds)
- the last argument is a void pointer that will be passed to the alarm function when called, it can be NULL or be used to pass some data to the alarm function.

Alarms are used internally by ISOS be program the de-allocation of threads stacks after they have returned.

1.3. Condition locks:

A condition lock is an object that can be used to synchronize threads executions. Several threads can be blocked on a condition and all unblocked by another thread.

A cond_lock_t structure must be declared and accessible for all the threads that will use the condition lock. This structure MUST be initialized before being used by calling cond_lock_create(cond_lock_t *cond) on it.

When a thread calls cond_lock_wait(cond_lock_t *cond), its execution is stopped here and its state changed to BLOCKED. It will no longer be executed by the kernel until it is unblocked. Several threads can be blocked at the same time on a condition lock. A thread can't be blocked on several condition lock at the same time. The only way for the blocked thread(s) to be unblocked is an other thread calling cond_lock_signal(cond_lock_t *cond) on the condition lock. Doing so will unblock ALL the threads blocked on the condition lock. Condition locks can be used by any thread, in ISOS they are internally used to manage messages queues.

1.4. Semaphores:

A semaphore is an object that can be used to protect the access to a resource. A semaphore_t structure needs to be declared and accessible by all the threads that will use the semaphore. This structure MUST be initialized before it can be used by calling semaphore_create(semaphore_t *sem, int32 initial_count). “initial_count” is the number of threads that can access the resource at any one time.

To access the protected resource, a thread has to call semaphore_acquire(semaphore_t *sem). If the resource is available, the thread's execution will continue. If the semaphore has already been acquired by the maximum number of threads specified at the semaphore's initialization, the thread's execution will be stopped here and its state changed to BLOCKED. The thread will no longer be executed until it is unblocked. This is done automatically when the resource becomes available, that is, when a thread which had previously acquired the semaphore releases it by calling semaphore_release(semaphore_t *sem).

If you don’t want your thread to be blocked when the semaphore is not available, you can use semaphore_acquire_try(semaphore_t *sem). This function will return true if the semaphore was successfully acquired, false if the resource was not available.

1.5. Message Queues:

Message queues provide a way for threads to communicate with each other by sending and receiving messages. This can be used to transfer information from one thread to another and also to synchronize threads. A message is defined like this:

struct msg_t {
uint32 num;
void *data;
};

The number can be used to send simple information while the void pointer can be used to send more complex information by sending a pointer to a data structure.

Message queues need to be created by calling msg_queue_create(msg_queue_t *queue, size_t size). The msg_queue_t structure must already being allocated; “size” memory space will be allocated to store the queue’s messages. Then, msg_queue_add(msg_queue_t *queue, msg_t msg, bool blocking = true) can be called to add a message to the queue and msg_queue_remove(msg_queue_t *queue, msg_t *msg, bool blocking = true) can be used to collect a message from the queue. Typically, the msg_queue_t structure will be visible from two thread functions; one will add messages to the queue while the other will collect them. Each of these two function is blocking by default, that is, the add function will wait until the message queue is not full to add its message and the remove function will wait that there is a message to collect to return it. If “blocking” is set to false, the function will not block and return false in the two previous cases. Otherwise, they return true.
When a queue is not needed anymore its allocated message storage space can be freed by calling msg_queue_delete(msg_queue_t *queue).

2. Development model:

In this section you will find the basics of application development under ISOS. Two application examples are provided. The first one show how to create a thread to run an application, the second one shows how to use interruptions in an application.

2.1. Simple “Hello world” application:

The thread function should have a prototype of this form:

void my_application(void *data);

To illustrate argument passing to a thread function, we will declare a “Hello world” string in the main function and pass a pointer to it to the thread that will display it.

In the “main.cpp” file:

#include “Hello.h”

[...]

// Declare the hello world string
char string[] = "Hello world!\r\n";

In the main() function:

// Create the thread that will display it on the serial interface
thread_create("HelloApp", hello_app, string);

“ Hello.h” file:

#ifndef HELLO_H
#define HELLO_H

// Declare the thread function
void hello_app(void *data);

#endif

“ Hello.cpp” file:

#include “Hello.h”
#include “Uart.h”

// Implement the thread function
void hello_app(void *data) {
uart_printf((char*)data); // Display the message received in argument
}
// When we return, the thread is destroyed

2.2. Application using interruptions:

ISOS differentiates two types of interruptions:
- the timer 0 interruption that is used for thread preemption
- other interruptions

Timer 0 interruptions are handled internally while the other interruptions sources can be controlled by the user. All other interruptions are handled in the function “irq_dispacher” in the file “Interrupts.cpp”. By default it contains handler for the serial interface and interrupt button interruptions. If you want to add an handler for example for the timer 1 interruption you can add the following to the “irq_dispacher” function:

if (TEST_BITS(INTPND, TIMER1_INT)) {
irq_timer1();
SET_BITS(INTPND, TIMER1_INT); // reset interrupt flag
}

Where “irq_timer1()” is the function in which you handle timer 1 interruptions.

 


© 2003 Wilhem Meignan.