Exploring Operating Systems

Day 54: Embedded OS Kernel Design - Minimal Kernel Implementation

Table of Contents

  1. Introduction
  2. Minimal Kernel Architecture
  3. Boot Process
  4. Memory Management
  5. Task Scheduling
  6. Interrupt Handling
  7. Device Drivers Framework
  8. Power Management
  9. Real-Time Capabilities
  10. Debug Infrastructure
  11. Implementation Examples
  12. Conclusion

1. Introduction

Embedded Operating System (OS) Kernel design focuses on creating a minimal, efficient operating system for resource-constrained devices. Unlike general-purpose operating systems, embedded OS kernels must be lightweight, fast, and optimized for specific hardware. This article explores the implementation details of a minimal kernel suitable for embedded systems, covering key components such as memory management, task scheduling, interrupt handling, and power management.

2. Minimal Kernel Architecture

The architecture of a minimal embedded OS kernel is designed to be simple yet functional. The following code demonstrates the core structure of the kernel:

// Core kernel structure
struct minimal_kernel {
    // Memory management
    struct mm_struct* mm;
    
    // Task management
    struct task_list tasks;
    struct task_struct* current_task;
    
    // Interrupt management
    struct interrupt_controller intc;
    
    // Device management
    struct device_manager dev_mgr;
    
    // Power management
    struct power_manager power;
    
    // System state
    atomic_t system_state;
    spinlock_t kernel_lock;
};

// Kernel initialization
int init_minimal_kernel(void) {
    struct minimal_kernel* kernel = &g_kernel;
    int ret;
    
    // Initialize spinlock
    spin_lock_init(&kernel->kernel_lock);
    
    // Initialize memory management
    ret = init_memory_management();
    if (ret)
        return ret;
        
    // Initialize task management
    ret = init_task_management();
    if (ret)
        goto err_task;
        
    // Initialize interrupt controller
    ret = init_interrupt_controller();
    if (ret)
        goto err_interrupt;
        
    // Initialize device manager
    ret = init_device_manager();
    if (ret)
        goto err_device;
        
    // Initialize power management
    ret = init_power_management();
    if (ret)
        goto err_power;
        
    return 0;
    
err_power:
    cleanup_device_manager();
err_device:
    cleanup_interrupt_controller();
err_interrupt:
    cleanup_task_management();
err_task:
    cleanup_memory_management();
    return ret;
}

The minimal_kernel structure represents the core of the embedded OS, including memory management, task management, interrupt handling, device management, and power management. The init_minimal_kernel function initializes these components in a specific order, ensuring that dependencies are properly handled.

3. Boot Process

The boot process is the first step in starting the embedded OS. It involves setting up the hardware, initializing the stack, and jumping to the kernel’s main function. The following code demonstrates the boot process:

// Boot header structure
struct boot_header {
    uint32_t magic;
    uint32_t kernel_size;
    uint32_t entry_point;
    uint32_t stack_pointer;
} __attribute__((packed));

// Assembly boot code
__attribute__((section(".boot")))
void _start(void) {
    // Disable interrupts
    __asm__ volatile("cli");
    
    // Set up stack
    __asm__ volatile("movl %0, %%esp" : : "r"(INITIAL_STACK_POINTER));
    
    // Clear BSS
    extern char __bss_start[], __bss_end[];
    for (char* p = __bss_start; p < __bss_end; p++)
        *p = 0;
        
    // Jump to C code
    kernel_main();
}

// Early initialization
void kernel_main(void) {
    // Initialize console for early printing
    early_console_init();
    
    // Initialize memory management
    early_mm_init();
    
    // Initialize interrupt vectors
    early_interrupt_init();
    
    // Start kernel proper
    init_minimal_kernel();
}

The _start function is the entry point of the kernel, written in assembly. It disables interrupts, sets up the stack, clears the BSS section, and jumps to the kernel_main function. The kernel_main function performs early initialization, including setting up the console, memory management, and interrupt vectors.

4. Memory Management

Memory management in an embedded OS is critical for allocating and deallocating memory efficiently. The following code demonstrates the implementation of a simple memory allocator:

// Memory management structure
struct mm_struct {
    // Physical memory management
    struct page* page_array;
    unsigned long nr_pages;
    
    // Virtual memory management
    struct vm_area_struct* vm_areas;
    spinlock_t vm_lock;
    
    // Memory allocator
    struct heap_allocator heap;
};

// Page structure
struct page {
    unsigned long flags;
    atomic_t ref_count;
    struct list_head list;
    void* virtual;
};

// Memory allocator implementation
void* kmalloc(size_t size) {
    struct mm_struct* mm = &g_kernel.mm;
    void* ptr;
    
    if (size == 0)
        return NULL;
        
    // Round up to alignment
    size = ALIGN(size, sizeof(void*));
    
    // Try to allocate from heap
    ptr = heap_alloc(&mm->heap, size);
    if (ptr)
        return ptr;
        
    // Fall back to page allocation
    size_t pages = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;
    struct page* page = alloc_pages(pages);
    if (!page)
        return NULL;
        
    return page_address(page);
}

// Page allocator
struct page* alloc_pages(unsigned int order) {
    struct mm_struct* mm = &g_kernel.mm;
    struct page* page = NULL;
    unsigned long flags;
    
    spin_lock_irqsave(&mm->vm_lock, flags);
    
    // Find contiguous free pages
    page = find_free_pages(order);
    if (page) {
        // Mark pages as allocated
        for (unsigned int i = 0; i < (1U << order); i++) {
            set_page_allocated(&page[i]);
            atomic_set(&page[i].ref_count, 1);
        }
    }
    
    spin_unlock_irqrestore(&mm->vm_lock, flags);
    return page;
}

The mm_struct structure represents the memory management subsystem, including physical and virtual memory management. The kmalloc function allocates memory from the heap or falls back to page allocation if necessary.

5. Task Scheduling

Task scheduling is essential for managing multiple tasks in an embedded OS. The following code demonstrates the implementation of a simple scheduler:

// Task structure
struct task_struct {
    pid_t pid;
    unsigned long stack;
    struct context ctx;
    enum task_state state;
    int priority;
    struct list_head list;
    struct mm_struct* mm;
};

// Scheduler implementation
struct scheduler {
    struct task_struct* current;
    struct list_head run_queue;
    spinlock_t lock;
    unsigned long ticks;
};

// Context switch
void __attribute__((naked)) context_switch(struct task_struct* prev,
                                         struct task_struct* next) {
    // Save current context
    __asm__ volatile(
        "push {r0-r12, lr}\n"
        "str sp, [r0, #0]\n"
        
        // Load new context
        "ldr sp, [r1, #0]\n"
        "pop {r0-r12, lr}\n"
        "bx lr"
    );
}

// Schedule next task
void schedule(void) {
    struct scheduler* sched = &g_kernel.scheduler;
    struct task_struct *prev, *next;
    unsigned long flags;
    
    spin_lock_irqsave(&sched->lock, flags);
    
    prev = sched->current;
    next = pick_next_task();
    
    if (prev != next) {
        sched->current = next;
        context_switch(prev, next);
    }
    
    spin_unlock_irqrestore(&sched->lock, flags);
}

The task_struct structure represents a task, while the scheduler structure manages the run queue and current task. The context_switch function switches between tasks, and the schedule function selects the next task to run.

6. Interrupt Handling

Interrupt handling is critical for responding to hardware events in an embedded OS. The following code demonstrates the implementation of an interrupt controller:

// Interrupt controller structure
struct interrupt_controller {
    void (*enable)(unsigned int irq);
    void (*disable)(unsigned int irq);
    void (*ack)(unsigned int irq);
    void (*mask)(unsigned int irq);
    void (*unmask)(unsigned int irq);
    spinlock_t lock;
};

// Interrupt handler registration
struct irq_handler {
    void (*handler)(void* data);
    void* data;
    const char* name;
};

// Interrupt vector table
static struct irq_handler irq_handlers[NR_IRQS];

// Register interrupt handler
int request_irq(unsigned int irq, void (*handler)(void*),
                void* data, const char* name) {
    unsigned long flags;
    
    if (irq >= NR_IRQS)
        return -EINVAL;
        
    spin_lock_irqsave(&g_kernel.intc.lock, flags);
    
    if (irq_handlers[irq].handler) {
        spin_unlock_irqrestore(&g_kernel.intc.lock, flags);
        return -EBUSY;
    }
    
    irq_handlers[irq].handler = handler;
    irq_handlers[irq].data = data;
    irq_handlers[irq].name = name;
    
    // Enable the interrupt
    g_kernel.intc.unmask(irq);
    
    spin_unlock_irqrestore(&g_kernel.intc.lock, flags);
    return 0;
}

// Interrupt dispatcher
void __attribute__((interrupt)) irq_dispatcher(void) {
    unsigned int irq = get_current_irq();
    
    if (irq < NR_IRQS && irq_handlers[irq].handler) {
        // Acknowledge the interrupt
        g_kernel.intc.ack(irq);
        
        // Call the handler
        irq_handlers[irq].handler(irq_handlers[irq].data);
    }
}

The interrupt_controller structure represents the interrupt controller, while the irq_handler structure represents an interrupt handler. The request_irq function registers an interrupt handler, and the irq_dispatcher function handles incoming interrupts.

7. Device Drivers Framework

Device drivers are essential for interacting with hardware peripherals. The following code demonstrates the implementation of a device driver framework:

// Device structure
struct device {
    const char* name;
    struct device_ops* ops;
    void* private_data;
    struct list_head list;
    atomic_t ref_count;
};

// Device operations
struct device_ops {
    int (*init)(struct device*);
    void (*shutdown)(struct device*);
    int (*suspend)(struct device*);
    int (*resume)(struct device*);
};

// Device manager
struct device_manager {
    struct list_head devices;
    spinlock_t lock;
};

// Register device
int register_device(struct device* dev) {
    unsigned long flags;
    
    if (!dev || !dev->ops)
        return -EINVAL;
        
    spin_lock_irqsave(&g_kernel.dev_mgr.lock, flags);
    
    // Initialize device
    if (dev->ops->init) {
        int ret = dev->ops->init(dev);
        if (ret) {
            spin_unlock_irqrestore(&g_kernel.dev_mgr.lock, flags);
            return ret;
        }
    }
    
    atomic_set(&dev->ref_count, 1);
    list_add(&dev->list, &g_kernel.dev_mgr.devices);
    
    spin_unlock_irqrestore(&g_kernel.dev_mgr.lock, flags);
    return 0;
}

The device structure represents a device, while the device_ops structure defines the operations that can be performed on the device. The register_device function registers a device with the device manager.

8. Power Management

Power management is critical for extending the battery life of embedded devices. The following code demonstrates the implementation of a power manager:

// Power management states
enum power_state {
    POWER_ON,
    POWER_SLEEP,
    POWER_DEEP_SLEEP,
    POWER_OFF
};

// Power manager structure
struct power_manager {
    enum power_state current_state;
    unsigned long sleep_timeout;
    struct list_head power_handlers;
    spinlock_t lock;
};

// Power state transition
int transition_power_state(enum power_state new_state) {
    struct power_manager* pm = &g_kernel.power;
    unsigned long flags;
    int ret = 0;
    
    spin_lock_irqsave(&pm->lock, flags);
    
    if (new_state == pm->current_state) {
        spin_unlock_irqrestore(&pm->lock, flags);
        return 0;
    }
    
    // Notify all devices
    ret = notify_power_handlers(new_state);
    if (ret) {
        spin_unlock_irqrestore(&pm->lock, flags);
        return ret;
    }
    
    // Perform state transition
    switch (new_state) {
        case POWER_SLEEP:
            prepare_for_sleep();
            break;
        case POWER_DEEP_SLEEP:
            prepare_for_deep_sleep();
            break;
        case POWER_OFF:
            prepare_for_shutdown();
            break;
        default:
            break;
    }
    
    pm->current_state = new_state;
    
    spin_unlock_irqrestore(&pm->lock, flags);
    return 0;
}

The power_manager structure represents the power management subsystem, while the transition_power_state function transitions the system between different power states.

9. Real-Time Capabilities

Real-time capabilities are essential for time-sensitive applications. The following code demonstrates the implementation of a real-time scheduler:

// Real-time task structure
struct rt_task {
    struct task_struct task;
    unsigned long deadline;
    unsigned long period;
    unsigned long execution_time;
};

// Real-time scheduler
struct rt_scheduler {
    struct list_head rt_tasks;
    spinlock_t lock;
    unsigned long current_time;
};

// Schedule real-time task
void schedule_rt_task(struct rt_task* rt_task) {
    struct rt_scheduler* rt_sched = &g_kernel.rt_scheduler;
    unsigned long flags;
    
    spin_lock_irqsave(&rt_sched->lock, flags);
    
    // Check if deadline can be met
    if (rt_sched->current_time + rt_task->execution_time <= rt_task->deadline) {
        // Add to real-time queue
        list_add_sorted(&rt_task->task.list, &rt_sched->rt_tasks);
    }
    
    spin_unlock_irqrestore(&rt_sched->lock, flags);
}

The rt_task structure represents a real-time task, while the rt_scheduler structure manages the real-time task queue. The schedule_rt_task function schedules a real-time task based on its deadline.

10. Debug Infrastructure

Debugging is essential for diagnosing issues in an embedded OS. The following code demonstrates the implementation of a debug infrastructure:

// Debug message levels
enum debug_level {
    DEBUG_EMERGENCY,
    DEBUG_ALERT,
    DEBUG_CRITICAL,
    DEBUG_ERROR,
    DEBUG_WARNING,
    DEBUG_NOTICE,
    DEBUG_INFO,
    DEBUG_DEBUG
};

// Debug structure
struct debug_info {
    enum debug_level level;
    const char* module;
    const char* function;
    int line;
    char message[256];
};

// Debug output
void debug_print(enum debug_level level, const char* module,
                 const char* function, int line, const char* fmt, ...) {
    struct debug_info info;
    va_list args;
    
    if (level > current_debug_level)
        return;
        
    info.level = level;
    info.module = module;
    info.function = function;
    info.line = line;
    
    va_start(args, fmt);
    vsnprintf(info.message, sizeof(info.message), fmt, args);
    va_end(args);
    
    output_debug_message(&info);
}

The debug_info structure represents a debug message, while the debug_print function outputs debug messages based on their level.

11. Implementation Examples

The following code demonstrates the initialization of a minimal embedded OS kernel:

// Main kernel initialization
int init_kernel(void) {
    int ret;
    
    // Initialize memory management
    ret = init_memory_management();
    if (ret)
        return ret;
        
    // Initialize task management
    ret = init_task_management();
    if (ret)
        goto err_task;
        
    // Initialize interrupt controller
    ret = init_interrupt_controller();
    if (ret)
        goto err_interrupt;
        
    // Initialize device manager
    ret = init_device_manager();
    if (ret)
        goto err_device;
        
    // Initialize power management
    ret = init_power_management();
    if (ret)
        goto err_power;
        
    return 0;
    
err_power:
    cleanup_device_manager();
err_device:
    cleanup_interrupt_controller();
err_interrupt:
    cleanup_task_management();
err_task:
    cleanup_memory_management();
    return ret;
}

The init_kernel function initializes the kernel’s core components, including memory management, task management, interrupt handling, device management, and power management.

12. Conclusion

Embedded OS Kernel Design requires careful consideration of resource constraints, real-time requirements, and power management. This article has covered the essential components needed to build a minimal yet functional embedded operating system kernel, including memory management, task scheduling, interrupt handling, device drivers, and power management. By following the techniques and patterns discussed in this article, developers can create efficient and reliable embedded systems.