Skip to content

Commit 424616f

Browse files
committed
Use preemptive EDF scheduler with ecall-based switch
This commit introduces a preemptive Earliest Deadline First (EDF) scheduler that uses RISC-V ecall instructions for voluntary context switches while preserving the existing cooperative scheduling mode. The preemptive scheduler required several architectural changes. Tasks now maintain separate stack pointer (sp) fields for ISR-based context switching, distinct from the jmp_buf context used in cooperative mode. The dispatcher accepts a from_timer parameter to distinguish timer-driven preemption from voluntary yields, ensuring tick counters only increment on actual timer interrupts. Context switching in preemptive mode builds ISR stack frames with mepc pointing to task entry points, allowing mret to resume execution. The ecall handler invokes the dispatcher directly, enabling tasks to yield without relying on setjmp/longjmp which are incompatible with interrupt contexts. The cooperative mode preserves its setjmp/longjmp semantics. The dispatcher always calls hal_context_restore() even when the same task continues, because the longjmp completes the save/restore cycle initiated by hal_context_save(). The hal_interrupt_tick() function enables interrupts on a task's first run by detecting when the entry point still resides in the context's return address slot. Real-time scheduling support includes EDF with deadline-based priority calculation, configurable through mo_task_rt_priority(). The RT scheduler hook in KCB allows custom scheduling policies. Delay handling was enhanced with batch updates to minimize critical section duration. The logger subsystem gained a direct_mode flag for ISR-safe output, and printf was made flush-aware to support synchronous output when needed. Exception handling uses trap_puts() to avoid printf deadlock in trap context. Close #26
1 parent d75c1da commit 424616f

File tree

10 files changed

+1013
-184
lines changed

10 files changed

+1013
-184
lines changed

app/rtsched.c

Lines changed: 446 additions & 99 deletions
Large diffs are not rendered by default.

arch/riscv/boot.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,19 @@ __attribute__((naked, aligned(4))) void _isr(void)
156156
/* Save trap-related CSRs and prepare arguments for do_trap */
157157
"csrr a0, mcause\n" /* Arg 1: cause */
158158
"csrr a1, mepc\n" /* Arg 2: epc */
159+
"mv a2, sp\n" /* Arg 3: isr_sp (current stack frame) */
159160
"sw a0, 30*4(sp)\n"
160161
"sw a1, 31*4(sp)\n"
161162

162-
/* Call the high-level C trap handler */
163+
/* Call the high-level C trap handler.
164+
* Returns: a0 = SP to use for restoring context (may be different
165+
* task's stack if context switch occurred).
166+
*/
163167
"call do_trap\n"
164168

169+
/* Use returned SP for context restore (enables context switching) */
170+
"mv sp, a0\n"
171+
165172
/* Restore context. mepc might have been modified by the handler */
166173
"lw a1, 31*4(sp)\n"
167174
"csrw mepc, a1\n"

arch/riscv/hal.c

Lines changed: 222 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@
4242
*/
4343
#define ISR_STACK_FRAME_SIZE 128
4444

45+
/* Global variable to hold the new stack pointer for pending context switch.
46+
* When a context switch is needed, hal_switch_stack() saves the current SP
47+
* and stores the new SP here. The ISR epilogue then uses this value.
48+
* NULL means no context switch is pending, use current SP.
49+
*/
50+
static void *pending_switch_sp = NULL;
51+
52+
/* Global variable to hold the ISR frame SP for the current trap.
53+
* Set at the start of do_trap() so hal_switch_stack() can save the correct
54+
* SP to the previous task (the ISR frame SP, not the current function's SP).
55+
*/
56+
static uint32_t current_isr_frame_sp = 0;
57+
4558
/* NS16550A UART0 - Memory-mapped registers for the QEMU 'virt' machine's serial
4659
* port.
4760
*/
@@ -248,31 +261,48 @@ void hal_cpu_idle(void)
248261

249262
/* Interrupt and Trap Handling */
250263

264+
/* Direct UART output for trap context (avoids printf deadlock) */
265+
extern int _putchar(int c);
266+
static void trap_puts(const char *s)
267+
{
268+
while (*s)
269+
_putchar(*s++);
270+
}
271+
272+
/* Exception message table per RISC-V Privileged Spec */
273+
static const char *exc_msg[] = {
274+
[0] = "Instruction address misaligned",
275+
[1] = "Instruction access fault",
276+
[2] = "Illegal instruction",
277+
[3] = "Breakpoint",
278+
[4] = "Load address misaligned",
279+
[5] = "Load access fault",
280+
[6] = "Store/AMO address misaligned",
281+
[7] = "Store/AMO access fault",
282+
[8] = "Environment call from U-mode",
283+
[9] = "Environment call from S-mode",
284+
[10] = "Reserved",
285+
[11] = "Environment call from M-mode",
286+
[12] = "Instruction page fault",
287+
[13] = "Load page fault",
288+
[14] = "Reserved",
289+
[15] = "Store/AMO page fault",
290+
};
291+
251292
/* C-level trap handler, called by the '_isr' assembly routine.
252293
* @cause : The value of the 'mcause' CSR, indicating the reason for the trap.
253294
* @epc : The value of the 'mepc' CSR, the PC at the time of the trap.
295+
* @isr_sp: The stack pointer pointing to the ISR frame.
296+
*
297+
* Returns The SP to use for restoring context (same or new task's frame).
254298
*/
255-
void do_trap(uint32_t cause, uint32_t epc)
299+
uint32_t do_trap(uint32_t cause, uint32_t epc, uint32_t isr_sp)
256300
{
257-
static const char *exc_msg[] = {
258-
/* For printing helpful debug messages */
259-
[0] = "Instruction address misaligned",
260-
[1] = "Instruction access fault",
261-
[2] = "Illegal instruction",
262-
[3] = "Breakpoint",
263-
[4] = "Load address misaligned",
264-
[5] = "Load access fault",
265-
[6] = "Store/AMO address misaligned",
266-
[7] = "Store/AMO access fault",
267-
[8] = "Environment call from U-mode",
268-
[9] = "Environment call from S-mode",
269-
[10] = "Reserved",
270-
[11] = "Environment call from M-mode",
271-
[12] = "Instruction page fault",
272-
[13] = "Load page fault",
273-
[14] = "Reserved",
274-
[15] = "Store/AMO page fault",
275-
};
301+
/* Reset pending switch at start of every trap */
302+
pending_switch_sp = NULL;
303+
304+
/* Store ISR frame SP so hal_switch_stack() can save it to prev task */
305+
current_isr_frame_sp = isr_sp;
276306

277307
if (MCAUSE_IS_INTERRUPT(cause)) { /* Asynchronous Interrupt */
278308
uint32_t int_code = MCAUSE_GET_CODE(cause);
@@ -282,28 +312,64 @@ void do_trap(uint32_t cause, uint32_t epc)
282312
* consistent tick frequency even with interrupt latency.
283313
*/
284314
mtimecmp_w(mtimecmp_r() + (F_CPU / F_TIMER));
285-
dispatcher(); /* Invoke the OS scheduler */
315+
/* Invoke scheduler - parameter 1 = from timer, increment ticks */
316+
dispatcher(1);
286317
} else {
287318
/* All other interrupt sources are unexpected and fatal */
288-
printf("[UNHANDLED INTERRUPT] code=%u, cause=%08x, epc=%08x\n",
289-
int_code, cause, epc);
290319
hal_panic();
291320
}
292321
} else { /* Synchronous Exception */
293322
uint32_t code = MCAUSE_GET_CODE(cause);
294-
const char *reason = "Unknown exception";
323+
324+
/* Handle ecall from M-mode - used for yielding in preemptive mode */
325+
if (code == MCAUSE_ECALL_MMODE) {
326+
/* Advance mepc past the ecall instruction (4 bytes) */
327+
uint32_t new_epc = epc + 4;
328+
write_csr(mepc, new_epc);
329+
330+
/* Also update mepc in the ISR frame on the stack!
331+
* The ISR epilogue will restore mepc from the frame (offset 31*4 =
332+
* 124 bytes). If we don't update the frame, mret will jump back to
333+
* the ecall instruction!
334+
*/
335+
uint32_t *isr_frame = (uint32_t *) isr_sp;
336+
isr_frame[31] = new_epc;
337+
338+
/* Invoke dispatcher for context switch - parameter 0 = from ecall,
339+
* don't increment ticks.
340+
*/
341+
dispatcher(0);
342+
343+
/* Return the SP to use - new task's frame or current frame */
344+
return pending_switch_sp ? (uint32_t) pending_switch_sp : isr_sp;
345+
}
346+
347+
/* Print exception info via direct UART (safe in trap context) */
348+
trap_puts("[EXCEPTION] ");
295349
if (code < ARRAY_SIZE(exc_msg) && exc_msg[code])
296-
reason = exc_msg[code];
297-
printf("[EXCEPTION] code=%u (%s), epc=%08x, cause=%08x\n", code, reason,
298-
epc, cause);
350+
trap_puts(exc_msg[code]);
351+
else
352+
trap_puts("Unknown");
353+
trap_puts(" epc=0x");
354+
for (int i = 28; i >= 0; i -= 4) {
355+
uint32_t nibble = (epc >> i) & 0xF;
356+
_putchar(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
357+
}
358+
trap_puts("\r\n");
359+
299360
hal_panic();
300361
}
362+
363+
/* Return the SP to use for context restore - new task's frame or current */
364+
return pending_switch_sp ? (uint32_t) pending_switch_sp : isr_sp;
301365
}
302366

303367
/* Enables the machine-level timer interrupt source */
304368
void hal_timer_enable(void)
305369
{
306-
mtimecmp_w(mtime_r() + (F_CPU / F_TIMER));
370+
uint64_t now = mtime_r();
371+
uint64_t target = now + (F_CPU / F_TIMER);
372+
mtimecmp_w(target);
307373
write_csr(mie, read_csr(mie) | MIE_MTIE);
308374
}
309375

@@ -313,20 +379,66 @@ void hal_timer_disable(void)
313379
write_csr(mie, read_csr(mie) & ~MIE_MTIE);
314380
}
315381

316-
/* Hook called by the scheduler after a context switch.
317-
* Its primary purpose is to enable global interrupts ('mstatus.MIE') only
318-
* AFTER the first task has been launched. This ensures interrupts are not
319-
* globally enabled until the OS is fully running in a valid task context.
382+
/* Enable timer interrupt bit only - does NOT reset mtimecmp.
383+
* Use this for NOSCHED_LEAVE to avoid pushing the interrupt deadline forward.
320384
*/
321-
void hal_interrupt_tick(void)
385+
void hal_timer_irq_enable(void)
322386
{
323-
tcb_t *task = kcb->task_current->data;
324-
if (unlikely(!task))
325-
hal_panic(); /* Fatal error - invalid task state */
387+
write_csr(mie, read_csr(mie) | MIE_MTIE);
388+
}
326389

327-
/* The task's entry point is still in RA, so this is its very first run */
328-
if ((uint32_t) task->entry == task->context[CONTEXT_RA])
329-
_ei(); /* Enable global interrupts now that execution is in a task */
390+
/* Disable timer interrupt bit only - does NOT touch mtimecmp.
391+
* Use this for NOSCHED_ENTER to temporarily disable preemption.
392+
*/
393+
void hal_timer_irq_disable(void)
394+
{
395+
write_csr(mie, read_csr(mie) & ~MIE_MTIE);
396+
}
397+
398+
/* Linker script symbols - needed for task initialization */
399+
extern uint32_t _gp, _end;
400+
401+
/* Build initial ISR frame on task stack for preemptive mode.
402+
* Returns the stack pointer that points to the frame.
403+
* When ISR restores from this frame, it will jump to task_entry.
404+
*
405+
* CRITICAL: ISR deallocates the frame before mret (sp += 128).
406+
* We place the frame such that after deallocation, SP is at a safe location.
407+
*
408+
* ISR Stack Frame Layout (must match boot.c _isr):
409+
* 0: ra, 4: gp, 8: tp, 12: t0, ... 116: t6
410+
* 120: mcause, 124: mepc
411+
*/
412+
void *hal_build_initial_frame(void *stack_top, void (*task_entry)(void))
413+
{
414+
#define INITIAL_STACK_RESERVE \
415+
256 /* Reserve space below stack_top for task startup */
416+
417+
/* Place frame deeper in stack so after ISR deallocates (sp += 128),
418+
* SP will be at (stack_top - INITIAL_STACK_RESERVE), not at stack_top.
419+
*/
420+
uint32_t *frame =
421+
(uint32_t *) ((uint8_t *) stack_top - INITIAL_STACK_RESERVE -
422+
ISR_STACK_FRAME_SIZE);
423+
424+
/* Zero out entire frame */
425+
for (int i = 0; i < 32; i++) {
426+
frame[i] = 0;
427+
}
428+
429+
/* Compute tp value same as boot.c: aligned to 64 bytes from _end */
430+
uint32_t tp_val = ((uint32_t) &_end + 63) & ~63U;
431+
432+
/* Initialize critical registers for proper task startup:
433+
* - frame[1] = gp: Global pointer, required for accessing global variables
434+
* - frame[2] = tp: Thread pointer, required for thread-local storage
435+
* - frame[31] = mepc: Task entry point, where mret will jump to
436+
*/
437+
frame[1] = (uint32_t) &_gp; /* gp - global pointer */
438+
frame[2] = tp_val; /* tp - thread pointer */
439+
frame[31] = (uint32_t) task_entry; /* mepc - entry point */
440+
441+
return (void *) frame;
330442
}
331443

332444
/* Context Switching */
@@ -468,6 +580,18 @@ __attribute__((noreturn)) void hal_context_restore(jmp_buf env, int32_t val)
468580
if (unlikely(!env))
469581
hal_panic(); /* Cannot proceed with invalid context */
470582

583+
/* Validate RA is in text section (simple sanity check) */
584+
uint32_t ra = env[15]; /* CONTEXT_RA = 15 */
585+
if (ra < 0x80000000 || ra > 0x80010000) {
586+
trap_puts("[CTX_ERR] Bad RA=0x");
587+
for (int i = 28; i >= 0; i -= 4) {
588+
uint32_t nibble = (ra >> i) & 0xF;
589+
_putchar(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
590+
}
591+
trap_puts("\r\n");
592+
hal_panic();
593+
}
594+
471595
if (val == 0)
472596
val = 1; /* Must return a non-zero value after restore */
473597

@@ -503,12 +627,60 @@ __attribute__((noreturn)) void hal_context_restore(jmp_buf env, int32_t val)
503627
__builtin_unreachable(); /* Tell compiler this point is never reached */
504628
}
505629

630+
/* Stack pointer switching for preemptive context switch.
631+
* Saves current SP to *old_sp and loads new SP from new_sp.
632+
* Called by dispatcher when switching tasks in preemptive mode.
633+
* After this returns, ISR will restore registers from the new stack.
634+
*
635+
* @old_sp: Pointer to location where current SP should be saved
636+
* @new_sp: New stack pointer to switch to
637+
*/
638+
void hal_switch_stack(void **old_sp, void *new_sp)
639+
{
640+
/* Save the ISR frame SP (NOT current SP which is deep in call stack!)
641+
* to prev task. DO NOT change SP here - that would corrupt the C call
642+
* stack! Instead, store new_sp in pending_switch_sp for ISR epilogue.
643+
*/
644+
*old_sp = (void *) current_isr_frame_sp;
645+
646+
/* Set pending switch - ISR epilogue will use this SP for restore */
647+
pending_switch_sp = new_sp;
648+
}
649+
650+
/* Enable interrupts on first run of a task.
651+
* Checks if task's return address still points to entry (meaning it hasn't
652+
* run yet), and if so, enables global interrupts.
653+
*/
654+
void hal_interrupt_tick(void)
655+
{
656+
tcb_t *task = kcb->task_current->data;
657+
if (unlikely(!task))
658+
hal_panic();
659+
660+
/* The task's entry point is still in RA, so this is its very first run */
661+
if ((uint32_t) task->entry == task->context[CONTEXT_RA])
662+
_ei();
663+
}
664+
506665
/* Low-level context restore helper. Expects a pointer to a 'jmp_buf' in 'a0'.
507-
* Restores the GPRs and jumps to the restored return address.
666+
* Restores the GPRs, mstatus, and jumps to the restored return address.
667+
*
668+
* This function must restore mstatus from the context to be
669+
* consistent with hal_context_restore(). The first task context is initialized
670+
* with MSTATUS_MIE | MSTATUS_MPP_MACH by hal_context_init(), which enables
671+
* interrupts. Failing to restore this value would create an inconsistency
672+
* where the first task inherits the kernel's mstatus instead of its own.
508673
*/
509674
static void __attribute__((naked, used)) __dispatch_init(void)
510675
{
511676
asm volatile(
677+
/* Restore mstatus FIRST to ensure correct processor state.
678+
* This is critical for interrupt enable state (MSTATUS_MIE).
679+
* Context was initialized with MIE=1 by hal_context_init().
680+
*/
681+
"lw t0, 16*4(a0)\n"
682+
"csrw mstatus, t0\n"
683+
/* Now restore all general-purpose registers */
512684
"lw s0, 0*4(a0)\n"
513685
"lw s1, 1*4(a0)\n"
514686
"lw s2, 2*4(a0)\n"
@@ -536,6 +708,7 @@ __attribute__((noreturn)) void hal_dispatch_init(jmp_buf env)
536708

537709
if (kcb->preemptive)
538710
hal_timer_enable();
711+
539712
_ei(); /* Enable global interrupts just before launching the first task */
540713

541714
asm volatile(
@@ -574,6 +747,15 @@ void hal_context_init(jmp_buf *ctx, size_t sp, size_t ss, size_t ra)
574747
/* Zero the context for predictability */
575748
memset(ctx, 0, sizeof(*ctx));
576749

750+
/* Compute tp value same as boot.c: aligned to 64 bytes from _end */
751+
uint32_t tp_val = ((uint32_t) &_end + 63) & ~63U;
752+
753+
/* Set global pointer and thread pointer for proper task execution.
754+
* These are critical for accessing global variables and TLS.
755+
*/
756+
(*ctx)[CONTEXT_GP] = (uint32_t) &_gp;
757+
(*ctx)[CONTEXT_TP] = tp_val;
758+
577759
/* Set the essential registers for a new task:
578760
* - SP is set to the prepared top of the task's stack.
579761
* - RA is set to the task's entry point.

0 commit comments

Comments
 (0)