Magic‐1 Emulator Project - retrotruestory/M1DEV GitHub Wiki

Step 16: Creating a Simple Operating System for Magic-1

Now that we have a complete development environment, let's create a simple operating system for the Magic-1 architecture. We'll call it "Magic-OS" and implement basic features like process management, memory allocation, and a simple shell.

First, let's create the OS kernel:

/**
 * Magic-OS Kernel
 * A simple operating system for the Magic-1 architecture
 */

// System constants
#define MAX_PROCESSES 8
#define PROCESS_STACK_SIZE 256
#define SHELL_BUFFER_SIZE 64

// I/O port definitions
#define UART_DATA   0x00
#define UART_STATUS 0x01
#define TIMER_CTRL  0x10
#define TIMER_DATA  0x11
#define POST_CODE   0xC0

// Process states
#define PROCESS_UNUSED 0
#define PROCESS_READY  1
#define PROCESS_RUNNING 2
#define PROCESS_BLOCKED 3

// System call numbers
#define SYS_EXIT    0
#define SYS_PRINT   1
#define SYS_READ    2
#define SYS_MALLOC  3
#define SYS_FREE    4
#define SYS_EXEC    5

// Process control block
typedef struct {
    int pid;
    int state;
    int priority;
    unsigned int sp;  // Stack pointer
    unsigned int pc;  // Program counter
    unsigned int regs[8]; // CPU registers
} process_t;

// Memory region
typedef struct mem_block {
    unsigned int address;
    unsigned int size;
    int allocated;
    struct mem_block *next;
} mem_block_t;

// Global variables
process_t processes[MAX_PROCESSES];
int current_process = -1;
mem_block_t *memory_blocks = 0;

// Function declarations
void kernel_init(void);
void schedule(void);
int create_process(unsigned int entry_point);
void exit_process(int pid);
void handle_interrupt(void);
void handle_syscall(void);
void shell_task(void);
void* malloc(unsigned int size);
void free(void* ptr);
void putchar(int c);
int getchar(void);
void print(const char* str);
void readline(char* buffer, int max_size);
void execute_command(char* command);

// Kernel entry point
void main() {
    // Initialize the system
    kernel_init();
    
    // Create the shell process
    create_process((unsigned int)shell_task);
    
    // Start the scheduler
    schedule();
    
    // Should never reach here
    while(1) {
        asm("NOP");
    }
}

// Initialize the kernel
void kernel_init(void) {
    // Output startup message
    putchar(0x42); // POST code
    
    // Initialize process table
    for (int i = 0; i < MAX_PROCESSES; i++) {
        processes[i].state = PROCESS_UNUSED;
    }
    
    // Setup initial memory block (16K of memory)
    memory_blocks = (mem_block_t*)0x1000;
    memory_blocks->address = 0x2000;
    memory_blocks->size = 0x4000 - 0x2000;  // 8KB of memory
    memory_blocks->allocated = 0;
    memory_blocks->next = 0;
    
    // Initialize hardware
    // Set up timer interrupt
    asm("LDI R0, 100");     // 100ms timer
    asm("OUT 0x10, R0");    // Write to timer control
    
    // Enable interrupts
    asm("STI");
    
    print("Magic-OS initialized\n");
}

// Schedule a process to run
void schedule(void) {
    // Simple round-robin scheduler
    int next_process = current_process;
    
    // Find the next ready process
    do {
        next_process = (next_process + 1) % MAX_PROCESSES;
    } while (processes[next_process].state != PROCESS_READY && 
             next_process != current_process);
    
    // If no process is ready, just return
    if (processes[next_process].state != PROCESS_READY) {
        return;
    }
    
    // Save current process context if there is one
    if (current_process != -1 && processes[current_process].state == PROCESS_RUNNING) {
        // Save CPU context (in a real system, this would be done in assembly)
        processes[current_process].state = PROCESS_READY;
    }
    
    // Switch to the new process
    current_process = next_process;
    processes[current_process].state = PROCESS_RUNNING;
    
    // Restore process context (in a real system, this would be done in assembly)
    // Jump to process code
}

// Create a new process
int create_process(unsigned int entry_point) {
    // Find an unused process slot
    int pid = -1;
    for (int i = 0; i < MAX_PROCESSES; i++) {
        if (processes[i].state == PROCESS_UNUSED) {
            pid = i;
            break;
        }
    }
    
    if (pid == -1) {
        // No free process slots
        return -1;
    }
    
    // Allocate stack for the process
    unsigned int *stack = (unsigned int*)malloc(PROCESS_STACK_SIZE);
    if (!stack) {
        return -1;  // Out of memory
    }
    
    // Initialize process control block
    processes[pid].pid = pid;
    processes[pid].state = PROCESS_READY;
    processes[pid].priority = 1;
    processes[pid].sp = (unsigned int)stack + PROCESS_STACK_SIZE - 4;  // Stack grows down
    processes[pid].pc = entry_point;
    
    // Initialize registers
    for (int i = 0; i < 8; i++) {
        processes[pid].regs[i] = 0;
    }
    
    return pid;
}

// Terminate a process
void exit_process(int pid) {
    if (pid < 0 || pid >= MAX_PROCESSES) {
        return;
    }
    
    if (processes[pid].state != PROCESS_UNUSED) {
        // Free the process stack
        free((void*)processes[pid].sp);
        
        // Mark the process as unused
        processes[pid].state = PROCESS_UNUSED;
    }
    
    // If the current process is exiting, schedule a new one
    if (pid == current_process) {
        current_process = -1;
        schedule();
    }
}

// Handle timer or hardware interrupt
void handle_interrupt(void) {
    // For now, just schedule the next process
    schedule();
}

// Handle system calls
void handle_syscall(void) {
    // Get system call number from R0
    int syscall = processes[current_process].regs[0];
    
    switch (syscall) {
        case SYS_EXIT:
            // Process wants to exit
            exit_process(current_process);
            break;
            
        case SYS_PRINT:
            // Print a string
            print((const char*)processes[current_process].regs[1]);
            break;
            
        case SYS_READ:
            // Read a string
            readline((char*)processes[current_process].regs[1], 
                     processes[current_process].regs[2]);
            break;
            
        case SYS_MALLOC:
            // Allocate memory
            processes[current_process].regs[0] = 
                (unsigned int)malloc(processes[current_process].regs[1]);
            break;
            
        case SYS_FREE:
            // Free memory
            free((void*)processes[current_process].regs[1]);
            break;
            
        case SYS_EXEC:
            // Execute a command
            execute_command((char*)processes[current_process].regs[1]);
            break;
    }
}

// Shell task
void shell_task(void) {
    char buffer[SHELL_BUFFER_SIZE];
    
    while (1) {
        print("Magic-OS> ");
        readline(buffer, SHELL_BUFFER_SIZE);
        
        if (buffer[0]) {
            execute_command(buffer);
        }
    }
}

// Simple memory allocator
void* malloc(unsigned int size) {
    mem_block_t *block = memory_blocks;
    mem_block_t *best_fit = 0;
    unsigned int best_size = 0xFFFFFFFF;
    
    // Find the best fit block
    while (block) {
        if (!block->allocated && block->size >= size) {
            if (block->size < best_size) {
                best_fit = block;
                best_size = block->size;
            }
        }
        block = block->next;
    }
    
    if (!best_fit) {
        return 0;  // No suitable block found
    }
    
    // If the block is much larger than needed, split it
    if (best_fit->size > size + sizeof(mem_block_t) + 16) {
        mem_block_t *new_block = (mem_block_t*)(best_fit->address + size);
        new_block->address = best_fit->address + size;
        new_block->size = best_fit->size - size - sizeof(mem_block_t);
        new_block->allocated = 0;
        new_block->next = best_fit->next;
        
        best_fit->size = size;
        best_fit->next = new_block;
    }
    
    best_fit->allocated = 1;
    return (void*)best_fit->address;
}

// Free memory
void free(void* ptr) {
    if (!ptr) return;
    
    mem_block_t *block = memory_blocks;
    mem_block_t *prev = 0;
    
    // Find the block
    while (block) {
        if (block->address == (unsigned int)ptr) {
            block->allocated = 0;
            
            // Try to merge with next block if it's free
            if (block->next && !block->next->allocated) {
                block->size += block->next->size + sizeof(mem_block_t);
                block->next = block->next->next;
            }
            
            // Try to merge with previous block if it's free
            if (prev && !prev->allocated) {
                prev->size += block->size + sizeof(mem_block_t);
                prev->next = block->next;
            }
            
            return;
        }
        
        prev = block;
        block = block->next;
    }
}

// Output a character to the UART
void putchar(int c) {
    // Wait for UART to be ready
    int status;
    do {
        asm("IN R0, 0x01");
        asm("MOV status, R0");
    } while ((status & 0x02) == 0);
    
    // Send character
    asm("LDI R0, c");
    asm("OUT 0x00, R0");
}

// Read a character from the UART
int getchar(void) {
    int c;
    int status;
    
    // Wait for character available
    do {
        asm("IN R0, 0x01");
        asm("MOV status, R0");
    } while ((status & 0x01) == 0);
    
    // Read character
    asm("IN R0, 0x00");
    asm("MOV c, R0");
    
    return c;
}

// Print a string
void print(const char* str) {
    while (*str) {
        putchar(*str++);
    }
}

// Read a line of text
void readline(char* buffer, int max_size) {
    int i = 0;
    int c;
    
    while (i < max_size - 1) {
        c = getchar();
        
        if (c == '\r' || c == '\n') {
            putchar('\r');
            putchar('\n');
            buffer[i] = 0;
            return;
        } else if (c == '\b' || c == 127) {
            if (i > 0) {
                i--;
                putchar('\b');
                putchar(' ');
                putchar('\b');
            }
        } else if (c >= ' ' && c <= '~') {
            buffer[i++] = c;
            putchar(c);
        }
    }
    
    buffer[i] = 0;
}

// Execute a shell command
void execute_command(char* command) {
    if (strcmp(command, "help") == 0) {
        print("Magic-OS Commands:\n");
        print("  help - Display this help message\n");
        print("  info - System information\n");
        print("  ps   - List processes\n");
        print("  mem  - Show memory usage\n");
        print("  exit - Exit the shell\n");
    } else if (strcmp(command, "info") == 0) {
        print("Magic-OS v1.0\n");
        print("CPU: Magic-1 (emulated)\n");
        print("Memory: 16KB\n");
    } else if (strcmp(command, "ps") == 0) {
        print("PID STATE  PRIORITY\n");
        print("--- ------ --------\n");
        
        for (int i = 0; i < MAX_PROCESSES; i++) {
            if (processes[i].state != PROCESS_UNUSED) {
                // Convert numbers to strings and print
                char pid[4], state[8], priority[4];
                
                // Simple number to string conversion
                pid[0] = i + '0';
                pid[1] = 0;
                
                switch (processes[i].state) {
                    case PROCESS_READY: 
                        strcpy(state, "READY"); 
                        break;
                    case PROCESS_RUNNING: 
                        strcpy(state, "RUNNING"); 
                        break;
                    case PROCESS_BLOCKED: 
                        strcpy(state, "BLOCKED"); 
                        break;
                    default: 
                        strcpy(state, "UNKNOWN"); 
                        break;
                }
                
                priority[0] = processes[i].priority + '0';
                priority[1] = 0;
                
                print(pid);
                print("   ");
                print(state);
                print(" ");
                print(priority);
                print("\n");
            }
        }
    } else if (strcmp(command, "mem") == 0) {
        print("Memory blocks:\n");
        print("ADDRESS  SIZE     STATUS\n");
        print("-------- -------- ------\n");
        
        mem_block_t *block = memory_blocks;
        while (block) {
            // Convert hex address to string
            char addr[5], size[5], status[10];
            
            // Simple hex conversion for demo
            addr[0] = '0';
            addr[1] = 'x';
            addr[2] = ((block->address >> 12) & 0xF) > 9 ? 
                      ((block->address >> 12) & 0xF) - 10 + 'A' : 
                      ((block->address >> 12) & 0xF) + '0';
            addr[3] = ((block->address >> 8) & 0xF) > 9 ? 
                      ((block->address >> 8) & 0xF) - 10 + 'A' : 
                      ((block->address >> 8) & 0xF) + '0';
            addr[4] = 0;
            
            // Simple decimal conversion for size
            size[0] = (block->size / 1000) + '0';
            size[1] = ((block->size / 100) % 10) + '0';
            size[2] = ((block->size / 10) % 10) + '0';
            size[3] = (block->size % 10) + '0';
            size[4] = 0;
            
            strcpy(status, block->allocated ? "USED" : "FREE");
            
            print(addr);
            print("     ");
            print(size);
            print("     ");
            print(status);
            print("\n");
            
            block = block->next;
        }
    } else if (strcmp(command, "exit") == 0) {
        print("Exiting shell...\n");
        exit_process(current_process);
    } else {
        print("Unknown command: ");
        print(command);
        print("\nType 'help' for available commands.\n");
    }
}

// String comparison
int strcmp(const char* s1, const char* s2) {
    while (*s1 && (*s1 == *s2)) {
        s1++;
        s2++;
    }
    return *(unsigned char*)s1 - *(unsigned char*)s2;
}

// String copy
char* strcpy(char* dest, const char* src) {
    char* d = dest;
    while ((*dest++ = *src++));
    return d;
}

Now, let's create a build script for our OS:

#!/bin/bash
set -e

# Directory paths
TOOLS_DIR="../tools"
C_COMPILER="${TOOLS_DIR}/compiler/c2asm"
ASSEMBLER="${TOOLS_DIR}/assembler/m1asm"
EMULATOR="../bin/magic1-emu"

# Check if C compiler exists, build if not
if [ ! -x "$C_COMPILER" ]; then
    echo "Building C compiler..."
    (cd "$(dirname "$C_COMPILER")" && gcc -o $(basename "$C_COMPILER") c2asm.c)
fi

# Check if assembler exists, build if not
if [ ! -x "$ASSEMBLER" ]; then
    echo "Building assembler..."
    (cd "$(dirname "$ASSEMBLER")" && ./build.sh)
fi

# Check if emulator exists, build if not
if [ ! -x "$EMULATOR" ]; then
    echo "Building emulator..."
    (cd ".." && make)
fi

# Compile kernel.c to assembly
echo "Compiling kernel.c to kernel.asm..."
"$C_COMPILER" kernel.c kernel.asm

# Assemble to binary
echo "Assembling kernel.asm to magicos.bin..."
"$ASSEMBLER" kernel.asm magicos.bin

# Run the OS in the emulator
echo "Running Magic-OS in the emulator..."
"$EMULATOR" -r magicos.bin

And finally, let's create a script to run the OS with the debugger:

#!/bin/bash
set -e

# Build the OS first
./build_os.sh

# Directory paths
DEBUGGER="../tools/debugger/m1debug"

# Check if debugger exists, build if not
if [ ! -x "$DEBUGGER" ]; then
    echo "Building debugger..."
    (cd "$(dirname "$DEBUGGER")" && ./build.sh)
fi

# Run the OS with the debugger
echo "Running Magic-OS in the debugger..."
"$DEBUGGER" magicos.bin

This simple operating system provides:

  1. Process management with a basic scheduler
  2. Memory management with malloc/free
  3. Console I/O
  4. A simple shell with basic commands
  5. Basic system calls

The OS demonstrates how to use our Magic-1 emulator for more complex software development beyond simple programs. It shows how an operating system can be structured on a minimal hardware platform and provides a foundation for further development.

In the next step, we can enhance the OS with more features like a file system, device drivers, or expand the standard library with additional functions.

⚠️ **GitHub.com Fallback** ⚠️