static global variable declared in os.c. The OS itself runs as process 0; user processes run as processes 1 and 2.
The Process struct
Each process is described by aProcess struct defined in process.h:
process.h
Process identifier. Assigned sequentially: 0 for the OS, 1 for P1, 2 for P2.
Saved general-purpose registers r0 through r12. Captured on every context switch so the process resumes with the correct register values.
Saved stack pointer (banked SVC-mode
sp_svc). Each process has its own stack region.Saved link register (banked SVC-mode
lr_svc). Preserves the return address from within a process’s call frame.Saved program counter. Points to the instruction the process will execute when it is next scheduled.
Saved Program Status Register. Stores the CPSR of the interrupted process so that processor mode, condition flags, and interrupt mask bits are restored atomically on re-entry via
movs pc, lr.Current lifecycle state of the process (see below).
Intrusive linked-list pointer used by the scheduler to form a circular list. The scheduler does not allocate separate list nodes.
ProcessState enum
process.h
| State | Value | Meaning |
|---|---|---|
PROCESS_RUNNING | 0 | The process is currently executing on the CPU. Only one process holds this state at any time. |
PROCESS_READY | 1 | The process is enqueued and waiting for the scheduler to dispatch it. |
PROCESS_WAITING | 2 | The process is blocked waiting for an event (reserved for future use). |
PROCESS_SLEEPING | 3 | The process is suspended for a timed delay (reserved for future use). |
PROCESS_TERMINATED | 4 | The process has exited and its PCB is no longer in the ready queue. |
Only
PROCESS_RUNNING and PROCESS_READY are actively used by the current scheduler. The remaining states are defined for future expansion.process_init()
process_init() zeros and configures a PCB before a process is enqueued for the first time.
process.c
Parameters
Pointer to the statically allocated PCB to initialize.
Process identifier to assign.
Initial program counter — the entry-point address the process will begin executing from on its first dispatch.
Initial stack pointer — the top of the stack region reserved for this process. The value is typically
BASE + PROCESS_STACK_SIZE where PROCESS_STACK_SIZE is 0x1000 (4 KiB).Field initialization
| Field | Value set | Reason |
|---|---|---|
pid | Caller-supplied | Identifies the process. |
pc | Caller-supplied | Entry point for first dispatch. |
sp | Caller-supplied | Top of this process’s stack. |
lr | 0 | No caller to return to at process start. |
spsr | 0x00000013 | SVC mode (0x13) with IRQ disabled (bit 7 = 1). Atomically loaded into CPSR on first dispatch via movs pc, lr. |
state | PROCESS_READY | Process is ready to run immediately after init. |
r[0..12] | 0 | All general-purpose registers cleared. |
0x00000013 decodes as: mode bits 10011 = Supervisor (SVC), FIQ bit [6] = 0 (enabled), IRQ bit [7] = 0 (enabled). IRQs are enabled from the moment the process first runs. The OS main loop calls disable_irq() only around individual PRINT calls to prevent interleaved UART output — it does not keep IRQs masked continuously.Process initialization in os.c
Three PCBs are declared asstatic globals and initialized in main():
os.c
| PCB | PID | Entry point | Initial stack top |
|---|---|---|---|
p0 | 0 | PLATFORM_OS_BASE | PLATFORM_OS_STACK + 0x1000 |
p1 | 1 | P1_BASE | P1_STACK + 0x1000 |
p2 | 2 | P2_BASE | P2_STACK + 0x1000 |
p1 and p2 are enqueued in the ready queue before the timer starts. p0 (the OS process) is assigned directly to CurrProcess — it does not go through the queue on startup and begins running immediately after enable_irq().CurrProcess
CurrProcess is a single global pointer (Process *) exported from os.c:
os.c
root.s to find the PCB of the currently executing process, and written by schedule() to point at the next process after a context switch. Because it is accessed from both C and assembly, it is declared with external linkage (no static).
Circular linked list
Thenext field in each PCB forms an intrusive circular singly-linked list managed by the scheduler. After both user processes are enqueued, the list looks like:
tail->next. Enqueuing appends to the tail; dequeuing removes from the head. All linking is done via the next pointer already embedded in the Process struct — no heap allocation is ever performed.