release
dev builds
extras
themes manual
wiki
device status forums
mailing lists
IRC bugs
patches
dev guide



Search | Go
Wiki > Main > PortingApplication

Porting Application

Work in progress

This page tries to describe steps and requirements for porting Rockbox as an application (RaaA), as well as common pitfalls.

Overview

Rockbox is historically an operating system for embedded systems. Yet, it has many audio and playback related features that you cannot find on a single media player application. But it lacks features of OSes shipped on devices (such as telephony or networking) which makes it illogical to run Rockbox as an OS on those devices. Additionally, Rockbox as an OS needs to be tailored for every specific device. Therefore it's desirable to run Rockbox as an application, within a hosted environment.

As part of SummerOfCode2010, Rockbox has been successfully ported to two platforms, SDL and Android.

Platform can mean an specific OS or a (cross platform) user space library. Rockbox is ported to the platforms then, not to the devices.

Speaking of platforms. We currently differentiate between platform ports and application ports. A platform port ports the existing application to a new platform. The application port has a defined feature set and should be similar across all platforms it has been ported too. A new application port is needed where the feature set differs significantly from existing application ports.

In practice: if apps/ code doesn't notice the difference between two platform ports, then it should be a single application port. But if apps/ notices it, because the one platform supports FM radio (which makes themes behave differently) and the other doesn't, then there should be two separate application ports.

This differentiation could possibly change in the future, it hasn't been set in stone at the time of writing.

This document will largely cover platform ports.

Requirements

Because Rockbox has always been an OS for embedded systems, it has virtually no dependencies. But to reach the best degree of integration and to reduce Rockbox' memory footprint the goal is to use supporting OS/library routines where possible.
  • Standard C library: Every platform should come with a C library. But sometimes, platforms only have a reduced and limited C library (e.g. Android's bionic). If the host's C library is insufficient, it is possible to partly or completely fall back to the C library that comes with Rockbox (in firmware/libc). Rockbox also has only a very small subset implemented, but it has everything it needs. Other libraries, like libm on Unix are not needed.
  • Memory allocation: Rockbox doesn't use malloc() at all. All memory the core needs is allocated statically (the max. size can be adjusted tools/configure). The default is 8MB.
  • File I/O: Rockbox only has a FAT(32) driver, which exposes a Unix-like API (file descriptor based, creat/open/close). So, you are (a) limited to fat32, (b) the platform offers a Unix-like File I/O API (see uisimulator/common/io.c), or (c) you can wrap around the platforms native API in a platform specific I/O driver. Of course, (a) is very much discouraged, (b) and (c) enable you to read/write any file system.
  • Loading codecs(and plugins): The application ports currently use libdl for codecs. You need a compatible API (again, see uisimulator/common/io.c) or you implement loading them in a native way. Loading them in the classic manner (loading the binary blob into memory and jump to its main funcion) is probably not possible, due to CPU cache problems.

Platform Drivers

There's a set of drivers mandatory for each port, I'll refer to them in the OS terms.

Functions marked with extern are already implemented in Rockbox, but you need to call them somehow from the platform drivers.

LCD driver

Rockbox draws into a flat memory region, the LCD framebuffer. It then uses lcd_update() and lcd_update_rect() to display the framebuffer. The following bits are interesting for the lcd driver.

typedef unsigned short fb_data;

#define LCD_FBWIDTH ((LCD_WIDTH+7)/8)

#define LCD_FBHEIGHT LCD_HEIGHT

fb_data lcd_framebuffer[LCD_FBHEIGHT][LCD_FBWIDTH];

void lcd_init_device(void);

void lcd_update(void);

void lcd_update_rect(int x, int y, int width, int height);

The above shows the RGB565 (so, 16bit) case (i.e. each pixel has 5 bits of red, 6 bits of green and 5 bits of blue). Currently there's no driver for more bits, so you need to write one if you want to use it. But that'd be a lot of effort, because you'd also need to properly convert the compiled-in bitmaps. So it's recommended you stick to the 16bit driver, then all compiled-in bitmaps will also work as is.

What the driver needs to implement is the 3 functions shown.
  • lcd_init_device() is the initializing functions, which is called early at startup. You can use it to set up static data, or create objects or something, and generally connect to the host's update mechanism.
  • lcd_update() draws the complete framebuffer, full screen. It's expected to either draw directly or copy it to a back buffer from which the platform can update.
  • lcd_update_rect() draws a fraction of the framebuffer. It's meant to safe resources because it only updates what's needed, but you can call lcd_update() in it if you need to.
It is important that lcd_update[_rect] schedules the update completely, otherwise you might see artifacts on the display.

Example: The Android port creates an RockboxFramebuffer object in lcd_init_device. The object holds a back buffer. The OS updates from this back buffer. The back buffer is needed because you cannot determine when Android does the actual update. lcd_update() also emits a postInvalidate() call which notifies the OS that we're ready to draw.

Input driver

Rockbox reads the button state in each tick. The input driver is responsible for mapping the states to appropriate buttons (as used in apps/keymap-*.c). Rockbox currently has no complete keyboard support, so don't waste time on trying to implement it. Instead, focus on the touchscreen and button interface.

The Touchscreen interface can be used for touchscreen input, obviously. But it can also re-purposed to mouse navigation.

With button interface you can send arbitrary buttons to the core. You need to have them defined (so they can be bitwise OR'ed) in the button-target.h file and you need to give them a function in the apps/keymap-*.c file.

void button_init_device(void);

int button_read_device(int *data);

extern int touchscreen_to_pixels(int x, int y, int *data);
  • button_init_device() is much like the aforementioned lcd_init_device() functions, i.e. you use it to initialize the driver and connect it with the host.
  • button_read_device() is what's being called periodically, every tick. It should be fast to execute. It only has the data parameter if you implement the touchscreen interface. It's expected to return the appropriate bit for each pressed button (e.g. BUTTON_UP|BUTTON_LEFT), or BUTTON_NONE if nothing was pressed. You should return the value of touchscreen_to_pixels() if you received a button press with the touchscreen interface. Pass through the data parameter if you call it. It'll handle the current touchscreen mode for you and return the correct press on its own.
Example: The Android port has some functions which the OS calls whenever a touchscreen press is detected. Those function write the state into variables. button_read_device() only reads that state, calls touchscreen_to_pixels() and returns the result.

Kernel and Timer driver

Rockbox uses "tick tasks" for a variety of purposes. tick tasks are small C functions that are called in a fixed intervall HZ times per second (HZ is usually 100).


bool timer_register(int reg_prio, void (*unregister_callback)(void),<br /> long cycles, void (*timer_callback)(void))

bool timer_set_period(long cycles);

void timer_unregister(void);

#define HZ 100

void tick_start(unsigned int interval_in_ms);

extern void call_tick_tasks(void); 
  • timer_* need to be stubbed, for now, since it's only used in (some) plugins which the application doesn't build right now.
  • tick_start() is important. It acts as an initializing function, meaning it needs to initializes the driver and connects to the host. This function needs to set up a mechanism which enables the tick tasks to be called. In most cases this means setting up a timer task, which runs periodically at a HZ rate.
The timer task function needs to call call_tick_tasks().

Example: The SDL port creates a SDL_Timer, sets it to run each each interval_in_ms, and calls call_tick_tasks() from it.

PCM driver

Rockbox continuously writes the decoded audio data into the pcm buffer, which acts as a ring buffer. Since that can wrap, the pcm driver needs to ask for a chunk raw pcm data. Then, that chunk is to be fed to host, who doesn't need to do any more than writing it to the hardware.

Rockbox usually decodes at 44100Hz, and the result is 16bit signed interleaved pcm data.

void pcm_play_lock(void);

void pcm_play_unlock(void);

void pcm_dma_apply_settings(void);

void pcm_play_dma_start(const void *addr, size_t size);

void pcm_play_dma_stop(void);

void pcm_play_dma_pause(bool pause);

size_t pcm_get_bytes_waiting(void);

const void * pcm_play_dma_get_peak_buffer(int *count);

void pcm_play_dma_init(void);

void pcm_postinit(void);

extern void pcm_play_get_more_callback(void **start, size_t *size);

These are quite a lot of functions, but it's not actually complicated.
  • pcm_play_dma_init() initializes the driver and connects to the host's pcm playback mechanism.
  • pcm_post_init() can most probably be stubbed, unless you need initializing stuff that can't be done in pcm_play_dma_init().
  • pcm_play_dma_pause and _stop should be self-explanatory.
  • pcm_dma_apply_settings() should configure the host for a 44.1Khz signed 16bit interleaved pcm stream (but that's usually the default anyway).
  • pcm_play_dma_get_peak_buffer() shall return the start and size of the current pcm data that has not been fed to the host yet.
  • pcm_play_lock() shall lock out the host (i.e. preventing it from calling get_more callbacks), finishing any pending pcm transfers. pcm_play_unlock() shall get the host in again. Both should be fine if stubbed, as they're intended for hardware dma which you probably won't have.
Now, if the current pcm chunk has been played, you want to get a new one for the host so it can continue playing. This is where pcm_play_get_more_callback() comes into play. This is where you get a new chunk from. It shall be called after each chunk and thus establishes a continuous pcm stream. Make the host call it so that it can fetch new data automatically on its own.

Threads

Rockbox relies on cooperative multi-tasking. This is very tricky, because host's usually don't offer that. Preemptive multi-tasking seems to be generally accepted and chosen over cooperative multi-tasking.

One has 4(5) choices basically:
  • If the CPU the platform runs on is known, and if it's one of ARM, Coldfire, SH or MIPS, then you can use the Rockbox implementation. This is the easiest way but possibly not applicable. The Android port does this.
  • Gnu Pth threads (http://www.gnu.org/software/pth/) can be used, it has been implemented as threading engine for Rockbox as a proof of concept (http://repo.or.cz/w/kugel-rb.git/shortlog/refs/heads/gnu-pth-sim, this should probably be committed to Rockbox SVN). It requires a Unix environment though.
  • Emulating cooperative threads with preemptive ones. This can be done by locking out all but the current thread with a single mutex (a mutex provided by the host, not a Rockbox mutex). The context switch would also transfer ownership of the mutex to a different thread. The SDL port does this.
  • Implementing a custom context switch mechanism for your platform, so that firmware/thread.c can be used. The only platform specific bit is load_context() and store_context(). If that's a Unix environment, it looks like {get,set,swap,make}context can probably be used for this. This means implementing a user thread library without using the standard threads that the host offers
  • Nobody has tried it so far, because it's expected to need a hell lot of effort with all sorts of strange synchronization issues...but one could try to fix Rockbox to run under preemptive threads.
It's not very useful to list all the functions now. One basically needs to implement all of firmware/thread.c (unless it can be re-used).

One particular function worthwhile to mention, though, is core_sleep(). If you don't use the host's thread, then this function shall notify the host that "Rockbox doesn't do anything right now", i.e. that all threads are asleep. If this is not implemented, then the scheduler in thread.c will busy wait for the next thread to be runnable, which wastes a lot CPU time for nothing. If you use the host's thread then the host should be able to figure it out on its own.

Adding a platform port

I don't want to recite much information here, because it's largely covered by PortingHowTo.

However, at the time of writing, there are only two platform ports which have the same feature set. Because of that, they are not treated differently throughout the Rockbox code (except the target tree code). They share a single firmware/export/config/application.h and a single entry in tools/configure. There's effectively only a single application port; but the application is ported to different platforms.

This application port should be re-used if possible. This reduces the amount of work needed to add a new platform port to the target tree platform drivers (including conditional compilation of it, in firmware/SOURCES) and to logic to pick the right build tools in tools/configure.

However, if it needs a new application port, then refer to PortingHowTo.

-- ThomasMartitz - 2010-08-15

r4 - 02 Apr 2021 - 20:46:07 - UnknownUser

Copyright © by the contributing authors.