Thursday, September 10, 2009

Some things have happened.

I have to apologize for the very bad update ratio on the blog. Hopefully someone will return to it anyway!

Since my last post, some 3 months ago, the kernel now implements 5 different system calls. These are:
  • yield
  • allocate a buffer
  • transmit a buffer to another process
  • receive a buffer from some other process
  • dispose of a buffer
The kernel is now a very simple message passing based one. It also handles different priorities, so that at every moment, the highest-prioritized ready process will run. Processes can now be written in C, as system call library functions are provided.

A couple of months ago, someone asked about a description of the entire process from booting up to scheduling. I will attempt to describe a few important steps:

First of all, the kernel and applications only run from RAM. Some basic support for running from ROM is implemented in the kernel and in the BSP (startup routines). There should not really need to be a lot of differences between these two cases and support for running from ROM will be there in time.

The way I run the system right now is using OpenOCD and GDB to download an elf file to the board. The elf file contains the monolith consisting of the kernel, the BSP and the application, everything linked to run from RAM. After downloading, I reset the board and let it run.

The first thing that happens is that the CPU is reset, which means that it will use the default exception vector location (which is at address 0x00000000, i.e. in flash) to find the reset routine. I have (once and for all) put a small assembly routine in flash, which is pointed out by the reset vector. This routine reads the desired reset vector from RAM (at 0x20000004), the initial main stack pointer (at 0x20000000) and simply jumps to the boot routine pointed out by the address in 0x20000004, which is the BSP startup. This bootstrapping assembly routine is not part of the OS, nor the BSP.

The BSP startup routine first of all sets up the STM32 clocks, so that the CPU core runs at 72 MHz (SYSCLK) which is generated from an 8 MHz external crystal which is multiplied by 9 using a PLL. AHB and APB2 clocks are also set up properly.

After setting up all clocks, the BSS segment is cleared. The area to be cleared is obtained by getting the addresses of the _bss_start and _bss_end symbols. These symbols are defined in the linker command file which is supplied by the BSP.

Next, by writing to the NVIC_VTOR register, the exception table location is moved to RAM (where it already is loaded). The GPIO module is set up to allow flashing the status LED.

After that, the BSP is almost finished setting up the board. There is one more thing to consider before handing over to the kernel.

All kernel setup processes (kernel initialization, process creation) must run from handler mode. I'll get back to the reason for that later. The default CPU mode after reset is thread mode, and the only way to switch to handler mode is through an exception. Therefore, the BSP writes the address of the kernel startup routine,
to the SVC (system call) vector in RAM and triggers it by issuing an svc instruction. This results in a switch to handler mode and a jump to the kernel.

The kernel startup routine clears the kernel pool area, then calls a hook which the application is supposed to implement. This hook function should do a number of rtos_create_process calls. This call allows the application to specify a process entry point, a priority and a stack size. For each call, a PCB is created in the kernel pool area, as well as a stack. The PCB is sorted into a readylist. After all these calls, the kernel is ready to start the highest prioritized process.

I mentioned earlier that the kernel startup must run from handler mode. The reason for this is that when the first process is scheduled, it will be started in the same way as if it was resumed after preemption. Therefore, for each created process, the architecture specific code is called to prepare the process stack in a way such that returning to the process and resuming it actually starts the process from the first instruction and with an empty stack.

When an application issues a system call, this is done by putting the syscall id (an integer, ranging from 0 to 4) in register r12 (done by an inline assembly routine). The scratch registers r0-r3 are transparently sent from the function call to the svc exception handler. The exception handler uses r12 as an offset into a jump table. Then a jump is taken to the system call implementation.

No interrupt handling is implemented yet, i.e. I haven't had to deal with critical regions in the kernel yet. It is still to be decided for instance what exception priority levels will be used for the SVC call in relation to other exceptions (interrupts from peripherals).

There is yet some more to be written about for instance context switching using the PendSV exception. I'll return to that later!