Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion Documentation/hal-interrupt.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,144 @@ void bad_critical_section(void) {
/* CORRECT */
void good_pattern(void) {
mo_sem_wait(semaphore); /* Block outside critical section */

CRITICAL_ENTER();
/* Quick critical work */
CRITICAL_LEAVE();
}
```

## ISR Context Safety Guide

### Overview
Interrupt Service Routines (ISRs), including timer callbacks, execute in interrupt context with special restrictions.
Violating these restrictions causes heap corruption, deadlocks, or undefined behavior.

### ISR-Safe vs Task-Only Functions

#### ISR-Safe Functions
These functions can be called from ISR context (timer callbacks, trap handlers):

| Function | Protection | Notes |
|----------|------------|-------|
| `mo_task_id()` | None needed | Read-only |
| `mo_task_count()` | None needed | Read-only |
| `mo_ticks()` | None needed | Volatile read |
| `mo_uptime()` | None needed | Calculated |
| `mo_timer_start/cancel()` | NOSCHED | Timer control only |
| `mo_sem_trywait()` | None | Non-blocking |
| `mo_sem_signal()` | NOSCHED | Wakes waiting tasks |
| `mo_mutex_trylock()` | None | Non-blocking |
| `mo_mutex_unlock()` | NOSCHED | Releases lock |
| `mo_cond_signal/broadcast()` | NOSCHED | Wakes waiters |
| `mo_pipe_nbread/nbwrite()` | None | Non-blocking I/O |
| `mo_logger_enqueue()` | CRITICAL | Deferred logging |
| `isr_puts()` | None | Direct UART output |
| `isr_putx()` | None | Direct UART hex output |

#### Task-Only Functions
These functions must NOT be called from ISR context:

| Function | Reason | Alternative |
|----------|--------|-------------|
| `mo_task_spawn()` | Uses malloc | Pre-create tasks |
| `mo_task_cancel()` | Uses free | Defer to task |
| `mo_timer_create()` | Uses malloc | Pre-create timers |
| `mo_timer_destroy()` | Uses free | Defer to task |
| `mo_task_delay/yield()` | Invokes scheduler | Use timer callback |
| `mo_task_suspend/resume()` | Modifies scheduler | Use semaphore |
| `mo_sem_wait()` | Blocks caller | Use `mo_sem_trywait()` |
| `mo_mutex_lock()` | Blocks caller | Use `mo_mutex_trylock()` |
| `mo_cond_wait()` | Blocks caller | Signal from ISR, wait in task |
| `mo_pipe_read/write()` | Blocks caller | Use `mo_pipe_nbread/nbwrite()` |
| `mo_mq_enqueue/dequeue()` | Uses malloc | Use pipe instead |
| `printf()` | May deadlock | Use `mo_logger_enqueue()` |

### Timer Callback Patterns

Timer callbacks execute in ISR context. Example safe callback:
```c
void *my_callback(void *arg) {
/* Safe: ISR-protected logging */
mo_logger_enqueue("Timer fired\n", 12);

/* Safe: Non-blocking semaphore signal to wake task */
mo_sem_signal(signal_sem);

/* Safe: Non-blocking pipe write */
char msg[] = "event";
mo_pipe_nbwrite(event_pipe, msg, sizeof(msg));

/* UNSAFE - do not use in callbacks:
* mo_task_spawn(...); // Uses malloc
* mo_task_delay(10); // Invokes scheduler
* mo_sem_wait(sem); // Blocks caller
* printf(...); // May deadlock
*/

return NULL;
}
```

### ISR-to-Task Communication Patterns

#### Pattern 1: Semaphore Signaling
```c
/* ISR/callback: Signal event */
void *timer_callback(void *arg) {
mo_sem_signal(event_sem);
return NULL;
}

/* Task: Wait for event */
void event_handler_task(void) {
while (1) {
mo_sem_wait(event_sem); /* Blocks until ISR signals */
process_event();
}
}
```

#### Pattern 2: Non-Blocking Pipe
```c
/* ISR/callback: Send data */
void *sensor_callback(void *arg) {
sensor_data_t data = read_sensor();
mo_pipe_nbwrite(sensor_pipe, &data, sizeof(data));
return NULL;
}

/* Task: Process data (non-blocking read, polls or uses semaphore wakeup) */
void sensor_task(void) {
sensor_data_t data;
while (1) {
if (mo_pipe_nbread(sensor_pipe, &data, sizeof(data)) > 0)
process_sensor_data(&data);
mo_task_delay(1); /* Yield or use semaphore for event-driven */
}
}
```

#### Pattern 3: Deferred Logging
```c
/* ISR/callback: Queue log message */
void *debug_callback(void *arg) {
mo_logger_enqueue("Tick!\n", 6);
return NULL;
}
/* Logger task automatically outputs queued messages */
```

### Emergency Debug Output

For debugging trap handlers or when the logger is unavailable, use direct UART:
```c
void *debug_isr(void *arg) {
isr_puts("[ISR] Value=0x");
isr_putx(some_value);
isr_puts("\r\n");
return NULL;
}
```

Warning: Direct UART output is blocking and slow. Use only for emergency debugging.
17 changes: 13 additions & 4 deletions app/timer.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
*
* It creates several timers with different periods and modes to verify
* their correct operation.
*
* Note: Timer callbacks execute in ISR context. This test uses printf()
* which may fall back to blocking direct output when the queue is full
* or the message exceeds LOG_ENTRY_SZ. For production ISR callbacks,
* prefer mo_logger_enqueue() or isr_puts() which are always ISR-safe.
*/
#include <linmo.h>

/* Helper function to print the current system uptime. */
void print_time()
/* Helper function to print the current system uptime.
* Called from timer callback (ISR context) - uses ISR-safe logging.
*/
static void print_time(void)
{
/* mo_uptime() returns a 64-bit value to prevent rollover. */
uint64_t time_ms = mo_uptime();
Expand All @@ -17,8 +24,10 @@ void print_time()
}

/* Generic timer callback.
* We can use a single callback for multiple timers by passing a unique
* identifier as the argument.
* Executes in ISR context - only ISR-safe functions are permitted here.
* This example uses printf() for convenience in a test app; it may block
* on direct output fallback. For guaranteed ISR-safe logging, use
* mo_logger_enqueue(), isr_puts(), or isr_putx().
*/
void *timer_callback(void *arg)
{
Expand Down
42 changes: 42 additions & 0 deletions include/lib/libc.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,48 @@ int random_r(struct random_data *buf, int32_t *result);
int32_t puts(const char *str);
int _putchar(int c); /* Low-level character output (used by logger) */

/* ISR-Safe I/O Functions
* [ISR-SAFE] These functions are safe to call from interrupt context.
* They perform direct UART output without locks, buffering, or malloc.
* Use for emergency debug output in timer callbacks and trap handlers.
*
* Warning: Direct UART output is slow and blocks until complete.
* For normal logging, prefer mo_logger_enqueue() which is non-blocking.
*/

/* Maximum characters isr_puts() will emit per call.
* Prevents unbounded blocking in ISR context on long strings.
*/
#define ISR_OUTPUT_MAX 128

/* ISR-safe string output - direct UART, no buffering.
* [ISR-SAFE] Safe to call from any context including ISR and timer callbacks.
* Output is capped at ISR_OUTPUT_MAX characters to bound ISR latency.
*
* @s : Null-terminated string to output
*/
static inline void isr_puts(const char *s)
{
for (uint32_t i = 0; s && *s && i < ISR_OUTPUT_MAX; i++)
_putchar(*s++);
}

/* ISR-safe hexadecimal output - direct UART, no buffering.
* [ISR-SAFE] Safe to call from any context including ISR and timer callbacks.
*
* Outputs a 32-bit value as 8 hex digits (e.g., "DEADBEEF").
* Useful for printing addresses and error codes in trap handlers.
*
* @val : 32-bit value to output as hexadecimal
*/
static inline void isr_putx(uint32_t val)
{
for (int i = 28; i >= 0; i -= 4) {
uint32_t nibble = (val >> i) & 0xF;
_putchar(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
}
}

/* Character and string input */
int getchar(void);
char *gets(char *s);
Expand Down
47 changes: 47 additions & 0 deletions include/linmo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
#pragma once

/* Linmo Operating System - Main API Header
*
* This header includes all kernel APIs for task management, synchronization,
* IPC, and system services.
*
* Interrupt Service Routines (ISRs), including timer callbacks, execute in
* interrupt context with special restrictions. Violating these restrictions
* causes heap corruption, deadlocks, or undefined behavior.
*
* [ISR-SAFE] Functions (callable from interrupt context):
* -------------------------------------------------------
* System Info: mo_task_id(), mo_task_count(), mo_ticks(), mo_uptime()
* Timer API: mo_timer_create/destroy/start/cancel() [NOSCHED protected]
* Semaphore: mo_sem_trywait(), mo_sem_signal() [non-blocking]
* Mutex: mo_mutex_trylock(), mo_mutex_unlock() [non-blocking]
* Condition Var: mo_cond_signal(), mo_cond_broadcast() [non-blocking]
* Pipe I/O: mo_pipe_nbread(), mo_pipe_nbwrite() [non-blocking]
* Logging: mo_logger_enqueue() [CRITICAL protected]
* Direct I/O: _putchar() for emergency/debug output
*
* [TASK-ONLY] Functions (must NOT call from ISR context):
* -------------------------------------------------------
* Memory: mo_task_spawn(), mo_task_cancel() - use malloc/free
* Blocking: mo_task_delay(), mo_task_yield(), mo_task_suspend()
* Blocking Sync: mo_sem_wait(), mo_mutex_lock(), mo_cond_wait()
* Blocking I/O: mo_pipe_read(), mo_pipe_write()
* Message Queue: mo_mq_enqueue(), mo_mq_dequeue() - unprotected malloc
* Stdio: printf(), puts() - may deadlock in preemptive mode
*
* Timer Callback Rules:
* ---------------------
* Timer callbacks execute in ISR context. Example safe callback:
*
* void *my_callback(void *arg) {
* mo_logger_enqueue("Timer fired\n", 12); // Safe: ISR-protected
* mo_sem_signal(signal_sem); // Safe: non-blocking
* // UNSAFE: mo_task_spawn(...); // Uses malloc
* // UNSAFE: printf(...); // May deadlock
* return NULL;
* }
*
* For emergency debug output in ISR, use the ISR-safe I/O functions:
* isr_puts("message"); // Direct UART string output
* isr_putx(0xDEADBEEF); // Direct UART hex output
* These are defined in lib/libc.h and available via linmo.h.
*/

#include <types.h>

#include <lib/libc.h>
Expand Down
7 changes: 7 additions & 0 deletions include/sys/logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@
int32_t mo_logger_init(void);

/* Enqueue a log message for deferred output.
* [ISR-SAFE] Protected by CRITICAL_ENTER - safe to call from ISR context
*
* Non-blocking: if queue is full, message is dropped.
* Thread-safe: protected by short critical section.
*
* This is the RECOMMENDED way to log from timer callbacks and ISRs.
* The message is queued and output later by the logger task, avoiding
* the risk of deadlock from direct UART output in ISR context.
*
* @msg : Null-terminated message string
* @length : Length of message (excluding null terminator)
*
Expand Down
11 changes: 11 additions & 0 deletions include/sys/mqueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
* Provides FIFO message passing between tasks with type-safe message
* containers. Messages consist of a user-allocated payload, a type
* discriminator, and size information.
*
* ISR-SAFETY WARNING:
* [TASK-ONLY] All message queue operations are task-context only.
* Message queue operations use underlying queue functions that may perform
* memory allocation. These operations are NOT protected by CRITICAL_ENTER
* and are NOT safe to call from ISR context.
*
* For ISR-to-task communication, use:
* - mo_pipe_nbwrite() for simple byte streams
* - mo_sem_signal() to wake a task
* - mo_logger_enqueue() for logging
*/

/* Message container structure */
Expand Down
16 changes: 16 additions & 0 deletions include/sys/mutex.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ int32_t mo_mutex_destroy(mutex_t *m);
/* Mutex Locking Operations */

/* Acquire mutex lock, blocking if necessary.
* [TASK-ONLY] May block caller - NOT safe from ISR context
*
* If the mutex is free, acquires it immediately. If owned by another task,
* the calling task is placed in the wait queue and blocked until the mutex
* becomes available. Non-recursive - returns error if caller already owns
Expand All @@ -65,6 +67,8 @@ int32_t mo_mutex_destroy(mutex_t *m);
int32_t mo_mutex_lock(mutex_t *m);

/* Attempt to acquire mutex lock without blocking.
* [ISR-SAFE] Non-blocking, returns immediately with success/failure
*
* Returns immediately whether the lock was acquired or not.
* @m : Pointer to mutex structure (must be valid)
*
Expand All @@ -73,6 +77,8 @@ int32_t mo_mutex_lock(mutex_t *m);
int32_t mo_mutex_trylock(mutex_t *m);

/* Attempt to acquire mutex lock with timeout.
* [TASK-ONLY] May block caller - NOT safe from ISR context
*
* Blocks for up to 'ticks' scheduler ticks waiting for the mutex.
* @m : Pointer to mutex structure (must be valid)
* @ticks : Maximum time to wait in scheduler ticks (0 = trylock behavior)
Expand All @@ -83,6 +89,8 @@ int32_t mo_mutex_trylock(mutex_t *m);
int32_t mo_mutex_timedlock(mutex_t *m, uint32_t ticks);

/* Release mutex lock.
* [ISR-SAFE] Protected by NOSCHED_ENTER - safe from any context
*
* If tasks are waiting, ownership is transferred to the next task in FIFO
* order. The released task is marked ready but may not run immediately
* depending on scheduler priority.
Expand Down Expand Up @@ -142,6 +150,8 @@ int32_t mo_cond_destroy(cond_t *c);
/* Condition Variable Wait Operations */

/* Wait on condition variable (atomically releases mutex).
* [TASK-ONLY] Blocks caller - NOT safe from ISR context
*
* The calling task must own the specified mutex. The mutex is atomically
* released and the task blocks until another task signals the condition.
* Upon waking, the mutex is re-acquired before returning.
Expand All @@ -153,6 +163,8 @@ int32_t mo_cond_destroy(cond_t *c);
int32_t mo_cond_wait(cond_t *c, mutex_t *m);

/* Wait on condition variable with timeout.
* [TASK-ONLY] Blocks caller - NOT safe from ISR context
*
* Like mo_cond_wait(), but limits wait time to specified number of ticks.
* @c : Pointer to condition variable structure (must be valid)
* @m : Pointer to mutex structure that caller must own
Expand All @@ -166,6 +178,8 @@ int32_t mo_cond_timedwait(cond_t *c, mutex_t *m, uint32_t ticks);
/* Condition Variable Signal Operations */

/* Signal one waiting task.
* [ISR-SAFE] Protected by NOSCHED_ENTER - safe for ISR-to-task signaling
*
* Wakes up the oldest task waiting on the condition variable (FIFO order).
* The signaled task will attempt to re-acquire the associated mutex.
* @c : Pointer to condition variable structure (must be valid)
Expand All @@ -175,6 +189,8 @@ int32_t mo_cond_timedwait(cond_t *c, mutex_t *m, uint32_t ticks);
int32_t mo_cond_signal(cond_t *c);

/* Signal all waiting tasks.
* [ISR-SAFE] Protected by NOSCHED_ENTER - safe from any context
*
* Wakes up all tasks waiting on the condition variable. All tasks will
* attempt to re-acquire their associated mutexes, but only one will succeed
* immediately (others will block on the mutex).
Expand Down
Loading