Porting Guide

Porting RK to a new project is relatively straightforward. Users only need to provide a few hardware-dependent functions and hook RK into the existing interrupt system.

The porting process consists of the following steps:

1. Adding RK Source Code to Your Project

Download the RK source code and add it to your current project.

The directory structure can be organized as follows:

Project
├── application/
├── drivers/
├── rk/
├── ...

Ensure that the include paths are properly configured in your IDE or build system so the project can access the RK header files.

2. Porting Hardware-Dependent Functions

Create a rk_port.h file with the following template content:

#ifndef __RK_PORT_H__
#define __RK_PORT_H__

#ifdef __cplusplus
extern "C" {
#endif

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "sys.h"
#include "xprintf.h"

/* port critical section */
#define port_entry_critical()                   ENTRY_CRITICAL()
#define port_exit_critical()                    EXIT_CRITICAL()

/* port console print */
#define port_os_printf(fmt, ...)                xprintf(fmt, ##__VA_ARGS__)

/* port pending service trigger interrupt */
#define port_os_pendsv_trigger()                pendsv_trigger()

/* port fatal error log */
#define port_fatal_error(c, m)                  SYS_FATAL(c, m)

/* port kernel task pool */
#define PORT_TASK_POOL_MAX_SIZE                 (8)

/* port message pool memory */
#define PORT_PURE_MSG_POOL_SIZE                 (4)
#define PORT_COMMON_MSG_POOL_SIZE               (4)
#define PORT_DYNAMIC_MSG_POOL_SIZE              (4)

/* port timer service */
#define PORT_TIMER_TASK_STACK_SIZE              (256) /* word */
#define PORT_TIMER_TASK_QUEUE_MAX_SIZE          (4)
#define PORT_TIMER_EVENT_POOL_MAX_SIZE          (8)

/* port heap memory */
#define PORT_HEAP_BUFFER_MAX_SIZE               (8 * 1024) /* 8KB */

#ifdef __cplusplus
}
#endif

#endif /* __RK_PORT_H__ */

Users need to map the macros above to their existing system APIs.

Note: port_os_pendsv_trigger() must be implemented to trigger the microcontroller’s PendSV interrupt. This is the exact mechanism RK relies on to perform asynchronous context switching.

3. Connecting to the Interrupt System

RK needs to be integrated into two core system interrupts:

System Tick

Inside the SysTick interrupt service routine (ISR), call the RK tick handler function.

Example code:

void system_tick_handler() {
    /* system milisenconds increase */
    ...

    /* task os scheduler */
    task_os_tick(1);
}

This function is responsible for:

  • Updating the global system tick count.

  • Managing task delay periods.

  • Advancing software software timers.

  • Waking up tasks whose waiting time has expired (timeouts).

  • Triggering the scheduler when a context switch is required.

The argument passed into the task_os_tick(1) function depends entirely on the configured system tick frequency (heartbeat period), which typically ranges from 1ms to 10ms in standard RTOS applications.

PendSV

Place the task_os_pendsv_handler() function (located in task.h) into the nested vectored interrupt controller (NVIC) vector table.

Example code:

/* interrupt vector table */
__attribute__((section(".isr_vector"))) void (*const g_pfnVectors[])(void) = {
    /* system interrupt */
    (void (*)(void))&_estack,                        /* stack pointer */
    reset_handler,                                   /* reset handler */
    nmi_handler,                                     /* nmi handler */
    hardfault_handler,                               /* hard fault handler */
    mem_manage_handler,                              /* mem manage handler */
    bus_fault_handler,                               /* bus fault handler */
    usage_fault_handler,                             /* usage fault handler */
    0,                                               /* reserved */
    0,                                               /* reserved */
    0,                                               /* reserved */
    0,                                               /* reserved */
    svc_handler,                                     /* svc handler */
    debug_mon_handler,                               /* debug monitor handler */
    0,                                               /* reserved */
    task_os_pendsv_handler,                          /* pend sv handler */
    system_tick_handler,

    /* peripheral interrupts */
    ...

4. Initialization and Starting RK

Once the porting layer is complete, proceed to initialize and launch the system components within your main execution path.

Example code:

/* startup code */
...

void main() {
    /* system init */
    system_init();

    /* periperal init */
    uart_init();
    i2c_init();
    gpio_init();

    /* task os init */
    task_os_init();
    task_os_create(TASK_SAMPLE_ID, task_sample_handler, TASK_PRIORITY_2, 1024, MAILBOX_NULL, 0, (const uint8_t*)"task_sample");
    task_os_run();

    while (1) {
    }
}

Where:

Function

Description

task_os_init()

Prepares system memory, ready queues, and internal kernel configurations before any tasks can be created.

task_os_create()

Allocates task stack space, assigns the priority level, and registers the task into the scheduler’s management list.

task_os_run()

Hands over CPU control to the highest-priority ready task, officially starting multitasking execution.

After task_os_run() is successfully called, the CPU execution will be fully managed by the scheduler, and tasks will execute according to RK’s scheduling rules. Following these four steps, RK is successfully ported into your project and ready for use. You can now begin creating Tasks, Timers, Mailboxes, Mutexes, and other RK objects to power your application.

5. Example

Example for porting:

Demonstration: