The Rockbox kernel
General
The Rockbox kernel is an advanced priority aware cooperative kernel. It consists of thin threads with some means of passing messages and locking data structures.
Threads
A thread is simply a normal C function that never returns. It has its own stack, and is executed until it allows the kernel to run another thread. Threads are executed in the order they were created.
The kernel related functions are declared in
thread.h
and
kernel.h
.
When the kernel switches the execution to another thread, it is called a
context switch. You can force a context switch by calling the
yield()
function, therefore a forced context switch is often called
yielding. A function that may cause a context switch is often called a
blocking function, or a function that
blocks.
The thread context
The thread context consists of the CPU registers that are normally preserved in normal C calls, plus the stack pointer. The "scratch registers" are not included. Also, no extra registers, such as MAC accumulators or status registers are preserved.
Thread functions
unsigned int create_thread(void (function)(void), void stack, size_t stack_size, unsigned flags, const char *name IF_PRIO(, int priority) IF_COP(, unsigned int core))
Creates a thread. It is immediately inserted in the list of threads, ready to execute. If a thread function returns, it is automatically removed from the thread list.
You can use the remove_thread()
function to remove a thread externally.
priority is the priority at which the thread is scheduled, as defined in thread.h. Possible priority levels are (ordered from highest to lowest):
PRIORITY_REALTIME
PRIORITY_PLAYBACK_MAX
PRIORITY_BUFFERING
PRIORITY_USER_INTERFACE
PRIORITY_RECORDING
PRIORITY_PLAYBACK
PRIORITY_SYSTEM
PRIORITY_BACKGROUND
core is the core which you want the thread to run on on dual processor targets (e.g. iPod, iriver H10, Sansa e200.) This will generally be "CPU" (the main CPU), but in certain cases may be "COP" (the coprocessor).
flags is a bit mask of thread properties. Possible bits are:
CREATE_THREAD_FROZEN which will create the thread as frozen. It will not run until
thread_thaw()
is called for it.
A handle id is returned, which should be passed to other thread functions for referencing the created thread.
void remove_thread(unsigned int thread_id)
remove_thread was removed from the kernel on Aug, 8 2014
Removes a thread from the kernel. The thread_id parameter is the thread handle id returned by create_thread()
.
void thread_thaw(unsigned int thread_id)
Wake up a thread that has been created with CREATE_THREAD_FROZEN
void thread_wait(unsigned int thread_id)
Blocks the current thread until the thread identified by
thread_id terminated (similarly to join() in other threading APIs).
void thread_exit(void)
Immediately exits the current thread.
void yield(void)
Forces a context switch. Which particular thread is executed next cannot be determined. The scheduler will choose it depending on the priority.
void sleep(int ticks)
Sleeps for
ticks number of system ticks, allowing other threads to execute. Due to the cooperative nature, it can last longer, so do not rely on the exact ticks too much.
Complete thread example
You create a thread in three steps:
- Write a C function. The thread function should not take any arguments, and not return anything (i.e. it should be of type
void (*)(void)
)
- Reserve some stack. A normal thread should not use that much stack. If in doubt, use the DEFAULT_STACK_SIZE macro, which should be large enough for simple threads. Most CPU's want the stack long-aligned, so it might be a good idea to use the 'long' type.
- Call
create_thread()
#include "thread.h"
/* My thread function */
void my_thread(void)
{
while(1) {
/* Do something fun */
yield(); /* I don't want to make rockbox freeze, so I let other threads run also */
}
}
static long my_stack[DEFAULT_STACK_SIZE/sizeof(long)];
static unsigned int my_thread_id = -1;
void init_my_thread(void)
{
my_thread_id = create_thread(my_thread, my_stack, sizeof(my_stack), "my thread", IF_PRIO(, PRIORITY_BACKGROUND) IF_COP(, CPU));
}
The system tick
There is a general timing mechanism called the
system tick. It is a 32-bit counter that is increased HZ times per second. You can read the current tick count in the
current_tick
global variable.
Tick helper functions
TIME_AFTER(a, b)
Returns
true if the
a count is later than
b.
Example:
unsigned long my_delay = current_tick + HZ;
if(TIME_AFTER(current_tick, my_delay)) {
splash(HZ, true, "1 second has passed");
}
TIME_BEFORE(a, b)
Returns
true if the
a count is earlier than
b.
Example:
unsigned long my_delay = current_tick + HZ;
while(TIME_BEFORE(current_tick, my_delay)) {
yield();
}
splash(HZ, true, "1 second has passed");
Note: This would be a very simple sleep()
implementation.
Tick tasks
You can declare a function to be called on every kernel tick. It will be executed in interrupt context and has to be very short and simple.
int tick_add_task(void (*f)(void))
Adds a tick task to the kernel. Returns 0 on success or -1 on failure.
Example:
void my_tick_task(void)
{
/* Do something fun */
}
void my_init(void)
{
tick_add_task(my_tick_task);
}
int tick_remove_task(void (*f)(void))
Removes a tick task from the kernel. Returns 0 if it was found and removed or -1 if the task wasn't found in the kernel list.
Event queues
Threads can communicate using
event queues. An event queue is a circular buffer of events. A thread can wait for messages to arrive in the queue while other threads are executing. When an event arrives, the thread will receive the event the next time it is scheduled.
An event looks like this:
struct event
{
long id;
void *data;
};
The
id is what identifies this particular event. It can be any 32-bit positive number. Negative numbers are reserved for system events. The
data can be anything you want to pass along with the event.
If you want to receive events in your thread, declare a global event queue and you're set.
struct event_queue my_queue;
Before using the queue, you have to initialize it.
queue_init(&my_queue);
Queue functions
Here are the functions for using event queues.
void queue_init(struct event_queue *queue)
Initializes a queue. This must be done before using it.
Example:
struct event_queue my_queue;
queue_init(&my_queue)
void queue_delete(struct event_queue *q)
Removes
q from the queue list. You should use this function if the queue could be destroyed (like in plugins), otherwise calling of queue_broadcast() or sending an event to this queue could/will do a very bad thing (tm).
void queue_wait(struct event_queue *q, struct event *ev)
Waits for an event to arrive, removes the event from the queue and copies it to the event structure pointed to by
ev.
Example:
struct event ev;
queue_wait(&my_queue, &ev);
if(ev.id == 1) {
splash(HZ, true, "Got event 1");
}
void queue_wait_w_tmo(struct event_queue *q, struct event *ev, int ticks)
Waits for an event to arrive, removes the event from the queue and copies it to the event structure pointed to by
ev. If
ticks system ticks have passed, it returns anyway with ev.id set to SYS_TIMEOUT.
Example:
struct event ev;
queue_wait_w_tmo(&my_queue, &ev, HZ);
switch(ev.id) {
case 1:
splash(HZ, true, "Got event 1");
break;
case SYS_TIMEOUT:
splash(HZ, true, "Timeout");
break;
}
bool queue_empty(const struct event_queue* q)
Returns
true if a queue is empty
Example:
while(queue_empty(&my_queue)) {
calculate();
yield();
}
queue_wait(&my_queue, &ev);
if(ev.id == 1) {
splash(HZ, true, "Got event 1");
}
void queue_clear(struct event_queue* q)
Empties a queue.
void queue_post(struct event_queue *q, long id, void *data)
Sends an event to a queue. This can be called from another thread or from an interrupt handler.
Example:
extern struct event_queue your_queue;
queue_post(&your_queue, 1, NULL);
int queue_broadcast(long id, void *data)
Sends the event to all threads in the system, including the sending thread.
Protecting your shared data
To protect your shared data, there is a mechanism called a
mutex
. It is a simple locking mechanism to allow exclusive access to a resource. Mutexes can only be used in a thread context and not in an interrupt handler.
Mutex functions
void mutex_init(struct mutex *m)
Initializes a mutex before it can be used.
void mutex_lock(struct mutex *m)
Attempts to lock a mutex. If it is already locked, it blocks until the mutex is unlocked.
void mutex_unlock(struct mutex *m)
Unlocks a mutex.
Mutex example
struct mutex my_mutex;
void my_protected_function(void)
{
mutex_lock(&my_mutex);
/* Do the stuff that requires exclusive access */
mutex_unlock(&my_mutex);
}
void init_my_protected_function(void)
{
mutex_init(&my_mutex);
}
Guidelines for developers
When you develop Rockbox code, you have to be nice to the other threads in the system.
- Don't spend time in an interrupt handler (or tick task)
- Yield or sleep every so often. Many threads in the system rely on being executed regularly. If your code performs lengthy operations, like Mandelbrot calculations, you must call yield() once in a while, so the other threads can run.
- Sleeping is often better than yielding, because the kernel can execute a SLEEP instruction (architecture specific) if all threads are asleep, which will reduce the power consumption.
Copyright © by the contributing authors.