YANGTOS MicroKernel OS : Documents
Home News Docs Links
Kernel OS OS-Dev
This Document is a Work In Progress.
Introduction to the YangTOS Kernel.

Big change to the design (See News For Reason):
Parts of the documentation are likely to be out of sync, please check back as things are updated.

The YANGTOS kernel is a MicroKernel written in C and ARM Assembly with a little extended support builtin. Take a minimal MicroKenel, add support for a very crude RAMFS and you have the YANGTOS kernel. The features of the kernel are:
  • System Initialization and Startup.
  • Multithreading and multitasking.
  • SMP, multiprocessor thread allocation
  • Memory Management, and MMU based memory protection.
  • Starting up user mode Modules, that are protected.
  • Passing IRQ's and SWI's to user mode modules (by callback functions).
  • Message passing.
  • Mutex and Semaphore management.
  • Support for a RAM Disk, loaded by the bootloader, to simplify loading modules at boot.
  • Support for a simple boot file system used to orginize files in the RAM Disk.
Those parts of the kernel that are dependant on particular Hardware are orginized in a way to make replacing them when porting the kernel fairly easy.

The philosophy of a microkernel is that user mode modules will provide all other functionality. While our microkernel has a few extras to simplify booting, and debugging, it is still a fairly simple microkernel.

In this document I intend to provide a quick overview of the YANGTOS kernel. There will be other documents that cover the workings of the kernel in more detail.

The bootloader loads the kernel and boot RAM disk into ram, then hands control over to the kernel. Most bootloaders will load the kernel at 0x8000, and this is what the kernel expects.

When the kernel gets control of the system it begins the process of getting the OS in a usable state. This process takes a few steps, after each of these steps (except the first one) it draws another small blue square on the screen starting at the top left of the display, it will use a red square instead if something fails:
  1. If target has multiple CPU options, determine CPU and number of CPU cores.
  2. Bring up display, just enough for kernel status output.
  3. Setup MMU and caches, making sure to keep display working.
  4. Setup the ARM exception/vector table.
  5. If the SoC has more than one CPU core start the other cores.
  6. Search the root directory of the boot RAM Disk for a file named Startup having a filetype of 0xFFA (Module) and load that into its own ram area.
  7. Setup the Startup module, and call the run entry point of the startup module.
  8. The Startup module will then load the BOOTFS driver module so it can access subdirectories in the boot RAM Disk (which the in kernel implementation knows nothing about).
  9. The Startup module then follows a startup script, named BOOTSCR to load other needed modules, begining with Hardware support. A display driver should be first, followed by a keyboard driver.
  10. Once there is a proper display driver module loaded, the squares are no longer used, using instead text output to the display.
As can be seen the use of a RAM disk is kind of important, in order to load all the modules. The very simple script used by the Startup module makes it easier to make updates and changes, though this script is kept extremely simple.

Needless to say this is a bit simplified of an overview, more details will be available later in other documents as they are created.

Task Scheduling

I have decided to change the schedular. Now we are using a binary schedular.
It is easiest to let the code tell you how it works.

Remember the following code is only minimally tested at this time, and may contain errors and/or typos.

//We are going to select the next task in queue, based on priority.
//The Round Robin algorithm is long gone, replaced by a binary place
//  change algorithm that assures static priorities and 100% of tasks
//  get there time.
//When counting in binary it is for certain that only one bit will
//  transition from a 0 to a 1 value, and each higher bit takes
//  twice as long between switching.  This is a good priority
//  scheduling method (val ^ (val+1) & (val+1)) gives the one bit that
//  switched.
long NextTask(void){
  //We count until we swith a bit to one that corisponds with a queue
  //  having at least one runable task.
  int SwTo, LpEnd, bit;

  LpEnd = 0;

  while (!LpEnd){
    //Get just the bit that changes for next count.
    SwTo = CurPri ^ ((CurPri + 1) & 0xFF) & (CurPri + 1);
    //Determine the bit position (would be easier in asm).
    for (bit = 7; !((1 << bit) & SwTo); bit--);

    //If there is a runnable thread in that priority switch to it,
    //  this does not start the task running, it just sets the task
    //  to run next time we return to user mode.
    if (TskRunCnt[bit]){
      LpEnd = 1; break;

    //Increase the priority count, for the next pass, and wrap around
    //  at 255 to keep it in 8 bits.
    //A priority counter changing to zero does not have any bits that
    //  changed to 1.
    CurPri = (CurPri < 0xFE) ? (CurPri+1) & 0xFF : 0;

  //Yes we actually return, so that the OS can do other usefull work,
  //  or just to give the now active task time to run.
  return 0;
Memory Management:

The kernel only provides services for mapping memory, getting the physical address of memory, allocating/deallocating a message block, and allocating/dealocating a block to a process. That is the extent of actual memory management.

The kernel does track which process/module memory is allocated to, and automatically deallocates all memory belonging to a process when the process terminates.

A proces is a program that is run and is not a module, it can consist of one or more threads. A process always starts as a single thread, and is the parrent of all threads that it creates. This paragraph could easily have been placed either here or in the section on task scheduling.

One thing that can be lumped in is mapping the current running process level thread to have a base of 0x8000, as that is the static base for any process level thread (this does not apply to module threads).

User Mode Modules:

The only thing that runs in a privledged CPU mode is the kernel, nothing else. All system modules are run in user mode, even those that handle hardware registers, inturupts, or SWI's.

Any requested SWI's are allocated to a module at startup (so long as there is not a conflict). IRQ handlers must be requested by the module. When the SWI or IRQ comes into the kernel, the kernel setups a call to user mode and calls the specified function in the module in user mode, with full protected memory to keep the module from accessing anything it is not supposed to.

This does mean that a module must request that any physical address of a device it directly interacts with be mapped in and the address range be allocated to the module. Thus for modules that provide the service of a driver to interface with a hardware device are still possible, and indeed the only way to do so.

This also makes it possible to implement an API/ABI in user mode modules, allowing the Kernel not to control what the OS looks like to applications, modules control what the system looks like to applications.


This information is no longer correct.
I am going to have to rewrite this section. SWI's are now entirely decoded by the kernel.

When the kernel recieves an interrupt, it will just pass the inturrupt up to USER mode to a module that has claimed that interrupt. Generally this works, though there are a couple of places where this could be a problem in the future. As such there is a plan for an interrupt handling module, an exception to the rule that runs in SVC mode and should only do what is minimally needed before passing back to the user mode interrupt handler.

Then there is the case of SWI handling, a bit different though not much. The simple solution used in this kernel is to simply put a coppy of the SWI instruction that called us into R0, and if it is not a call to the kernel, call a user mode module that has claimed SWI decoding. This user mode module then calls the kernel to transfer control to the User mode module that handles the specific SWI.

There is one exception to the handling of SWI's, and that is to call the kernel when needed. For this SWI's in the range of 0xCFFF000 to 0xCFFF000are used. This range was chosen so as not to conflict with those used by any other current Operating System that I know of.

It is about as easy as you can get. I will likely have to rewrite this section, it is a bit of a runon ramble.

Message Passing:

There are two way in which messages get passed between processes, polling or callbacks. A task may use either or both together. Messages can be sent from any task, and recieved by any task. The format of the message is dependant on the message, though can not exceed 256 bytes in size.

Polling is prefered:
The first is by polling (a little cooperative nature to our preemptive kernel), and having the message returned to the target task on the poll return. To poll (also giving time back to the other tasks) the task can call either the KernPoll or KernPollIdle SWI, with a pointer to its recieving buffer in R1.

Callbacks Work:
The second way is to register a callback for recieving messages. In this case the task must register a buffer for its messages, and must handle any messages that it needs to before returning, as the message buffer will be overwritten by the next message sent to that task.

Send it:
To send a message a task setups the message data, in the same form as it will be recieved, and calls the KernSendMessage SWI, with a pointer to the message block in R1.

RAM Disk and BootFS:

The RAM disk is a very simple storage volume implementation, just a continous area of RAM containing a crude filesystem and the files.

The Boot file system is very simple by design. The Root Directory table begins 16 bytes from the start of the volume. The first 16 bytes are used to describe the volume.

At the start of the ram disk is a 16 byte header to describe the volume. The first 4-bytes is a string that contains "BOOT", the next 4-byte word contains the size of the volume in bytes, next is a word that points to the Root Directory table (for future expansion purposes), the last word is NULL and reserved for future use. After this 16 byte header is the Root Directory Table.

Each Directory Table has 64 entries, no more, no less (to keep it very simple for the kernel, that really should not need to know FS at all). The following table describes the layout of each entry in a directory table in the RAM disk file table, with offsets being in bytes.

0 FileStart: Gives the offset from the start of the RAM Disk where the file begins, in bytes.
4 FileLen: Gives the length of the file in bytes.
8 FileType: Contains the file type, we use the values defined by RISC OS, as we do not want to reinvent the wheel. The high byte is unused, and to be treated as reserved.
12 FileName: 16 bytes. This is a zero padded ASCII string containing the file name.
28 TimeStamp: A very simple timestamp, with the number of days since 1980 in the upper 16 bits, and half the number of seconds since midnight in the lower 16 bits.

If the file type is a subdirectory then the file pointed to is another directory table of the same format. For subdirectories the first entry is named "^" and references the parrent directory.

As can be seen all files must be continous. There is also one file named "--FREE--" that is actually the remaining free space, not a real file, this file is in the root directory.


Content to be added.

This page created and maintained on RISC OS using !Zap.
Copyright 2018 David Cagle (AKA DavidS).