Skip to content

Commit e4cb3e3

Browse files
committed
Use EDF scheduler with ecall-based context switch
The preemptive EDF (Earliest Deadline First) scheduler requires a mechanism for tasks to voluntarily yield CPU time while blocked on delays. In RISC-V M-mode, the ecall instruction provides a clean way to trigger a synchronous trap that invokes the scheduler without relying on timer interrupts. The dispatcher function now accepts a from_timer parameter to distinguish between timer-driven preemption and ecall-driven yields. Timer interrupts increment the system tick counter and process time slices, while ecall-based yields skip tick advancement to prevent time drift. When handling ecall from M-mode, the trap handler advances mepc past the 4-byte ecall instruction to prevent re-execution upon return. Critically, the ISR stack frame must also be updated because the ISR epilogue restores mepc from the saved frame rather than the CSR directly. Without this fix, mret would jump back to the ecall instruction causing an infinite trap loop. The logger subsystem gains a flush mechanism with a direct_mode flag that bypasses the async queue. This ensures multi-line output like statistics reports prints in order, as printf normally enqueues to the ring buffer which the logger task drains asynchronously. The rtsched test application validates the EDF implementation by running periodic RT tasks with different periods and deadlines, measuring execution counts, deadline misses, response times, and jitter to verify correct scheduler behavior.
1 parent d75c1da commit e4cb3e3

File tree

8 files changed

+896
-183
lines changed

8 files changed

+896
-183
lines changed

app/rtsched.c

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

arch/riscv/hal.c

Lines changed: 141 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -248,32 +248,40 @@ void hal_cpu_idle(void)
248248

249249
/* Interrupt and Trap Handling */
250250

251+
/* Direct UART output for trap context (avoids printf deadlock) */
252+
extern int _putchar(int c);
253+
static void trap_puts(const char *s)
254+
{
255+
while (*s)
256+
_putchar(*s++);
257+
}
258+
259+
/* Exception message table per RISC-V Privileged Spec */
260+
static const char *exc_msg[] = {
261+
[0] = "Instruction address misaligned",
262+
[1] = "Instruction access fault",
263+
[2] = "Illegal instruction",
264+
[3] = "Breakpoint",
265+
[4] = "Load address misaligned",
266+
[5] = "Load access fault",
267+
[6] = "Store/AMO address misaligned",
268+
[7] = "Store/AMO access fault",
269+
[8] = "Environment call from U-mode",
270+
[9] = "Environment call from S-mode",
271+
[10] = "Reserved",
272+
[11] = "Environment call from M-mode",
273+
[12] = "Instruction page fault",
274+
[13] = "Load page fault",
275+
[14] = "Reserved",
276+
[15] = "Store/AMO page fault",
277+
};
278+
251279
/* C-level trap handler, called by the '_isr' assembly routine.
252280
* @cause : The value of the 'mcause' CSR, indicating the reason for the trap.
253281
* @epc : The value of the 'mepc' CSR, the PC at the time of the trap.
254282
*/
255283
void do_trap(uint32_t cause, uint32_t epc)
256284
{
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-
};
276-
277285
if (MCAUSE_IS_INTERRUPT(cause)) { /* Asynchronous Interrupt */
278286
uint32_t int_code = MCAUSE_GET_CODE(cause);
279287
if (int_code == MCAUSE_MTI) { /* Machine Timer Interrupt */
@@ -282,28 +290,59 @@ void do_trap(uint32_t cause, uint32_t epc)
282290
* consistent tick frequency even with interrupt latency.
283291
*/
284292
mtimecmp_w(mtimecmp_r() + (F_CPU / F_TIMER));
285-
dispatcher(); /* Invoke the OS scheduler */
293+
/* Invoke scheduler - parameter 1 = from timer, increment ticks */
294+
dispatcher(1);
286295
} else {
287296
/* All other interrupt sources are unexpected and fatal */
288-
printf("[UNHANDLED INTERRUPT] code=%u, cause=%08x, epc=%08x\n",
289-
int_code, cause, epc);
290297
hal_panic();
291298
}
292299
} else { /* Synchronous Exception */
293300
uint32_t code = MCAUSE_GET_CODE(cause);
294-
const char *reason = "Unknown exception";
301+
302+
/* Handle ecall from M-mode - used for yielding in preemptive mode */
303+
if (code == MCAUSE_ECALL_MMODE) {
304+
/* Advance mepc past the ecall instruction (4 bytes) */
305+
uint32_t new_epc = epc + 4;
306+
write_csr(mepc, new_epc);
307+
308+
/* Also update mepc in the ISR frame on the stack!
309+
* The ISR epilogue will restore mepc from the frame (offset 31*4 =
310+
* 124 bytes). If we don't update the frame, mret will jump back to
311+
* the ecall instruction!
312+
*/
313+
uint32_t *isr_frame = (uint32_t *) __builtin_frame_address(0);
314+
isr_frame[31] = new_epc;
315+
316+
/* Invoke dispatcher for context switch - parameter 0 = from ecall,
317+
* don't increment ticks.
318+
*/
319+
dispatcher(0);
320+
return;
321+
}
322+
323+
/* Print exception info via direct UART (safe in trap context) */
324+
trap_puts("[EXCEPTION] ");
295325
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);
326+
trap_puts(exc_msg[code]);
327+
else
328+
trap_puts("Unknown");
329+
trap_puts(" epc=0x");
330+
for (int i = 28; i >= 0; i -= 4) {
331+
uint32_t nibble = (epc >> i) & 0xF;
332+
_putchar(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
333+
}
334+
trap_puts("\r\n");
335+
299336
hal_panic();
300337
}
301338
}
302339

303340
/* Enables the machine-level timer interrupt source */
304341
void hal_timer_enable(void)
305342
{
306-
mtimecmp_w(mtime_r() + (F_CPU / F_TIMER));
343+
uint64_t now = mtime_r();
344+
uint64_t target = now + (F_CPU / F_TIMER);
345+
mtimecmp_w(target);
307346
write_csr(mie, read_csr(mie) | MIE_MTIE);
308347
}
309348

@@ -313,20 +352,50 @@ void hal_timer_disable(void)
313352
write_csr(mie, read_csr(mie) & ~MIE_MTIE);
314353
}
315354

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.
355+
/* Enable timer interrupt bit only - does NOT reset mtimecmp.
356+
* Use this for NOSCHED_LEAVE to avoid pushing the interrupt deadline forward.
357+
*/
358+
void hal_timer_irq_enable(void)
359+
{
360+
write_csr(mie, read_csr(mie) | MIE_MTIE);
361+
}
362+
363+
/* Disable timer interrupt bit only - does NOT touch mtimecmp.
364+
* Use this for NOSCHED_ENTER to temporarily disable preemption.
365+
*/
366+
void hal_timer_irq_disable(void)
367+
{
368+
write_csr(mie, read_csr(mie) & ~MIE_MTIE);
369+
}
370+
371+
/* Build initial ISR frame on task stack for preemptive mode.
372+
* Returns the stack pointer that points to the frame.
373+
* When ISR restores from this frame, it will jump to task_entry.
374+
*
375+
* CRITICAL: ISR deallocates the frame before mret (sp += 128).
376+
* We place the frame such that after deallocation, SP is at a safe location.
320377
*/
321-
void hal_interrupt_tick(void)
378+
void *hal_build_initial_frame(void *stack_top, void (*task_entry)(void))
322379
{
323-
tcb_t *task = kcb->task_current->data;
324-
if (unlikely(!task))
325-
hal_panic(); /* Fatal error - invalid task state */
380+
#define INITIAL_STACK_RESERVE \
381+
256 /* Reserve space below stack_top for task startup */
326382

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 */
383+
/* Place frame deeper in stack so after ISR deallocates (sp += 128),
384+
* SP will be at (stack_top - INITIAL_STACK_RESERVE), not at stack_top.
385+
*/
386+
uint32_t *frame =
387+
(uint32_t *) ((uint8_t *) stack_top - INITIAL_STACK_RESERVE -
388+
ISR_STACK_FRAME_SIZE);
389+
390+
/* Zero out entire frame */
391+
for (int i = 0; i < 32; i++) {
392+
frame[i] = 0;
393+
}
394+
395+
/* Set mepc (offset 31 words) to task entry point */
396+
frame[31] = (uint32_t) task_entry;
397+
398+
return (void *) frame;
330399
}
331400

332401
/* Context Switching */
@@ -503,12 +572,43 @@ __attribute__((noreturn)) void hal_context_restore(jmp_buf env, int32_t val)
503572
__builtin_unreachable(); /* Tell compiler this point is never reached */
504573
}
505574

575+
/* Stack pointer switching for preemptive context switch.
576+
* Saves current SP to *old_sp and loads new SP from new_sp.
577+
* Called by dispatcher when switching tasks in preemptive mode.
578+
* After this returns, ISR will restore registers from the new stack.
579+
*
580+
* @old_sp: Pointer to location where current SP should be saved
581+
* @new_sp: New stack pointer to switch to
582+
*/
583+
void hal_switch_stack(void **old_sp, void *new_sp)
584+
{
585+
asm volatile(
586+
"sw sp, 0(%0)\n" /* Save current SP to *old_sp */
587+
"mv sp, %1\n" /* Load new SP from new_sp */
588+
: /* no outputs */
589+
: "r"(old_sp), "r"(new_sp)
590+
: "memory");
591+
}
592+
506593
/* 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.
594+
* Restores the GPRs, mstatus, and jumps to the restored return address.
595+
*
596+
* This function must restore mstatus from the context to be
597+
* consistent with hal_context_restore(). The first task context is initialized
598+
* with MSTATUS_MIE | MSTATUS_MPP_MACH by hal_context_init(), which enables
599+
* interrupts. Failing to restore this value would create an inconsistency
600+
* where the first task inherits the kernel's mstatus instead of its own.
508601
*/
509602
static void __attribute__((naked, used)) __dispatch_init(void)
510603
{
511604
asm volatile(
605+
/* Restore mstatus FIRST to ensure correct processor state.
606+
* This is critical for interrupt enable state (MSTATUS_MIE).
607+
* Context was initialized with MIE=1 by hal_context_init().
608+
*/
609+
"lw t0, 16*4(a0)\n"
610+
"csrw mstatus, t0\n"
611+
/* Now restore all general-purpose registers */
512612
"lw s0, 0*4(a0)\n"
513613
"lw s1, 1*4(a0)\n"
514614
"lw s2, 2*4(a0)\n"
@@ -536,6 +636,7 @@ __attribute__((noreturn)) void hal_dispatch_init(jmp_buf env)
536636

537637
if (kcb->preemptive)
538638
hal_timer_enable();
639+
539640
_ei(); /* Enable global interrupts just before launching the first task */
540641

541642
asm volatile(

arch/riscv/hal.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ int32_t hal_context_save(jmp_buf env);
7676
void hal_context_restore(jmp_buf env, int32_t val);
7777
void hal_dispatch_init(jmp_buf env);
7878

79+
/* Stack switching for preemptive context switch.
80+
* Saves current SP to *old_sp and loads new SP from new_sp.
81+
* Used by dispatcher when switching tasks in preemptive mode.
82+
*/
83+
void hal_switch_stack(void **old_sp, void *new_sp);
84+
7985
/* Provides a blocking, busy-wait delay.
8086
* This function monopolizes the CPU and should only be used for very short
8187
* delays or in pre-scheduling initialization code.
@@ -92,7 +98,13 @@ uint64_t _read_us(void);
9298
void hal_hardware_init(void);
9399
void hal_timer_enable(void);
94100
void hal_timer_disable(void);
95-
void hal_interrupt_tick(void);
101+
void hal_timer_irq_enable(
102+
void); /* Enable timer interrupt bit only (for NOSCHED) */
103+
void hal_timer_irq_disable(
104+
void); /* Disable timer interrupt bit only (for NOSCHED) */
105+
void *hal_build_initial_frame(
106+
void *stack_top,
107+
void (*task_entry)(void)); /* Build ISR frame for preemptive mode */
96108

97109
/* Initializes the context structure for a new task.
98110
* @ctx : Pointer to jmp_buf to initialize (must be non-NULL).
@@ -109,4 +121,4 @@ void hal_panic(void);
109121
void hal_cpu_idle(void);
110122

111123
/* Default stack size for new tasks if not otherwise specified */
112-
#define DEFAULT_STACK_SIZE 4096
124+
#define DEFAULT_STACK_SIZE 8192

include/sys/logger.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,21 @@ uint32_t mo_logger_queue_depth(void);
5858
* Returns total dropped message count since logger init
5959
*/
6060
uint32_t mo_logger_dropped_count(void);
61+
62+
/* Check if logger is in direct output mode.
63+
* Lock-free read for performance - safe to call frequently.
64+
* Returns true if printf/puts should bypass the queue.
65+
*/
66+
bool mo_logger_direct_mode(void);
67+
68+
/* Flush all pending messages and enter direct output mode.
69+
* Drains the queue directly from caller's context.
70+
* After flush, printf/puts bypass the queue for ordered output.
71+
* Call mo_logger_async_resume() to re-enable async logging.
72+
*/
73+
void mo_logger_flush(void);
74+
75+
/* Re-enable async logging after a flush.
76+
* Call this after completing ordered output that required direct mode.
77+
*/
78+
void mo_logger_async_resume(void);

include/sys/task.h

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ enum task_states {
6767
typedef struct tcb {
6868
/* Context and Stack Management */
6969
jmp_buf context; /* Saved CPU context (GPRs, SP, PC) for task switching */
70+
void *sp; /* Saved stack pointer for preemptive context switch */
7071
void *stack; /* Pointer to base of task's allocated stack memory */
7172
size_t stack_sz; /* Total size of the stack in bytes */
7273
void (*entry)(void); /* Task's entry point function */
@@ -150,16 +151,16 @@ extern kcb_t *kcb;
150151
* other hardware interrupts (e.g., UART) to be serviced, minimizing latency.
151152
* Use when protecting data shared between tasks.
152153
*/
153-
#define NOSCHED_ENTER() \
154-
do { \
155-
if (kcb->preemptive) \
156-
hal_timer_disable(); \
154+
#define NOSCHED_ENTER() \
155+
do { \
156+
if (kcb->preemptive) \
157+
hal_timer_irq_disable(); \
157158
} while (0)
158159

159-
#define NOSCHED_LEAVE() \
160-
do { \
161-
if (kcb->preemptive) \
162-
hal_timer_enable(); \
160+
#define NOSCHED_LEAVE() \
161+
do { \
162+
if (kcb->preemptive) \
163+
hal_timer_irq_enable(); \
163164
} while (0)
164165

165166
/* Core Kernel and Task Management API */
@@ -169,8 +170,8 @@ extern kcb_t *kcb;
169170
/* Prints a fatal error message and halts the system */
170171
void panic(int32_t ecode);
171172

172-
/* Main scheduler dispatch function, called by the timer ISR */
173-
void dispatcher(void);
173+
/* Main scheduler dispatch function, called by timer ISR or ecall */
174+
void dispatcher(int from_timer);
174175

175176
/* Architecture-specific context switch implementations */
176177
void _dispatch(void);

0 commit comments

Comments
 (0)