Magic‐1 Emulator Project - retrotruestory/M1DEV GitHub Wiki
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:
- Process management with a basic scheduler
- Memory management with malloc/free
- Console I/O
- A simple shell with basic commands
- 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.