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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ BAREMETAL_TESTS = test_aria test_async test_atomic test_buddy test_chacha20_poly
test_owner_complex test_pool test_priority test_security \
test_security_hardening test_security_util test_seed test_shared \
test_simple_ds test_smart_sched test_stats test_status test_sync \
test_table test_timing test_track_mem
test_table test_timing test_track_mem test_ringbuf_isr
BAREMETAL_THREAD_TESTS = test_epoch_grace test_ringbuf test_sharded_sched \
test_thread test_worker_abort test_worker_dirty_recovery
BAREMETAL_NET_TESTS = test_net_tcp
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,28 @@ What changes on bare metal:
build with FreeRTOS and lwIP (see [lwIP networking](#lwip-networking)),
`/proc` statistics, and the `__int128` accelerator backend.

## ISR safety and critical sections

`<ttak/arch/ttak_critical.h>` provides two critical-section macro pairs:

- `TTAK_ENTER_CRITICAL()` / `TTAK_EXIT_CRITICAL()` — task context only.
- `TTAK_ENTER_CRITICAL_ISR(state)` / `TTAK_EXIT_CRITICAL_ISR(state)` —
interrupt context only; `state` is a `ttak_critical_state_t` declared by
the caller and carries the interrupt mask from enter to exit.

On Cortex-M the task variants mask IRQs with `cpsid i`/`cpsie i` and the
ISR variants save/restore PRIMASK; on FreeRTOS they map to
`taskENTER_CRITICAL()` / `taskENTER_CRITICAL_FROM_ISR()`. On hosted POSIX
and MSVC/x86 builds both pairs are no-ops.

Containers that take a pthread lock cannot be touched from an ISR (the
lock is illegal there). The ring buffer therefore adds interrupt-safe
variants, `ttak_ringbuf_push_isr()` / `ttak_ringbuf_pop_isr()`: they never
take the rwlock and serialize with a critical section instead. When a
buffer is shared between a task and an ISR, **both** sides must use the
`_isr` variants; the regular task-context APIs must not be mixed with them
on the same buffer.

## Stack usage analysis

Bare-metal builds compile with `-fstack-usage` (disable with
Expand Down
110 changes: 110 additions & 0 deletions include/ttak/arch/ttak_critical.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* @file ttak_critical.h
* @brief Task-context and ISR-context critical sections.
*
* - @c TTAK_ENTER_CRITICAL() / @c TTAK_EXIT_CRITICAL(): task context only.
* - @c TTAK_ENTER_CRITICAL_ISR(state) / @c TTAK_EXIT_CRITICAL_ISR(state):
* interrupt context only. @p state is a caller-declared
* @c ttak_critical_state_t that carries the interrupt mask from enter
* to exit; it is mandatory because FreeRTOS' FromISR API returns the
* mask from enter and needs it back at exit.
*
* Mapping: Cortex-M uses @c cpsid i / @c cpsie i (task) and PRIMASK
* save/restore (ISR); FreeRTOS uses taskENTER_CRITICAL() and
* taskENTER_CRITICAL_FROM_ISR(). Hosted POSIX and MSVC/x86 get no-ops —
* preemption is the OS scheduler's job there.
*/

#ifndef TTAK_ARCH_CRITICAL_H
#define TTAK_ARCH_CRITICAL_H

#include <ttak/compat/baremetal.h>

#ifdef __cplusplus
extern "C" {
#endif

/* ============================================================================
* Cortex-M detection (ttak_arch.h intentionally covers hosted 64-bit ISAs)
* ============================================================================ */

#if !defined(TTAK_ARCH_CORTEX_M)
# if defined(__ARM_ARCH_PROFILE) && (__ARM_ARCH_PROFILE == 'M')
# define TTAK_ARCH_CORTEX_M 1
# elif defined(__ARM_ARCH_6M__) || defined(__ARM_ARCH_7M__) \
|| defined(__ARM_ARCH_7EM__) || defined(__ARM_ARCH_8M_MAIN__) \
|| defined(__ARM_ARCH_8M_BASE__)
# define TTAK_ARCH_CORTEX_M 1
# endif
#endif

/* Interrupt-mask state passed from TTAK_ENTER_CRITICAL_ISR() to
* TTAK_EXIT_CRITICAL_ISR(). */
#if TTAK_PLATFORM_FREERTOS
# include "FreeRTOS.h"
# include "task.h"
typedef UBaseType_t ttak_critical_state_t;
#elif defined(TTAK_ARCH_CORTEX_M)
# include <stdint.h>
typedef uint32_t ttak_critical_state_t;
#else
typedef unsigned int ttak_critical_state_t;
#endif

/* ============================================================================
* Task-context critical sections
* ============================================================================ */

#if TTAK_PLATFORM_FREERTOS

# define TTAK_ENTER_CRITICAL() taskENTER_CRITICAL()
# define TTAK_EXIT_CRITICAL() taskEXIT_CRITICAL()

#elif defined(TTAK_ARCH_CORTEX_M) && (defined(__GNUC__) || defined(__clang__))

# define TTAK_ENTER_CRITICAL() __asm__ volatile("cpsid i" ::: "memory")
# define TTAK_EXIT_CRITICAL() __asm__ volatile("cpsie i" ::: "memory")

#else

/* Interrupts are not maskable from hosted user code. */
# define TTAK_ENTER_CRITICAL() ((void)0)
# define TTAK_EXIT_CRITICAL() ((void)0)

#endif

/* ============================================================================
* ISR-context critical sections
* ============================================================================ */

#if TTAK_PLATFORM_FREERTOS

# define TTAK_ENTER_CRITICAL_ISR(state) \
do { (state) = taskENTER_CRITICAL_FROM_ISR(); } while (0)
# define TTAK_EXIT_CRITICAL_ISR(state) \
do { taskEXIT_CRITICAL_FROM_ISR(state); } while (0)

#elif defined(TTAK_ARCH_CORTEX_M) && (defined(__GNUC__) || defined(__clang__))

# define TTAK_ENTER_CRITICAL_ISR(state) \
do { \
__asm__ volatile("mrs %0, primask\n\t" \
"cpsid i" \
: "=r"(state) :: "memory"); \
} while (0)
# define TTAK_EXIT_CRITICAL_ISR(state) \
do { __asm__ volatile("msr primask, %0" :: "r"(state) : "memory"); } while (0)

#else

/* No-ops on hosted targets. */
# define TTAK_ENTER_CRITICAL_ISR(state) ((void)(state))
# define TTAK_EXIT_CRITICAL_ISR(state) ((void)(state))

#endif

#ifdef __cplusplus
}
#endif

#endif /* TTAK_ARCH_CRITICAL_H */
22 changes: 21 additions & 1 deletion include/ttak/container/ringbuf.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ void ttak_ringbuf_destroy(ttak_ringbuf_t *rb);

/**
* @brief Pushes an item to the buffer.
*
* Task context only. Do not mix with the _isr variants on the same
* buffer; see ttak_ringbuf_push_isr().
*
* @param rb Buffer.
* @param item Pointer to data to copy in.
Expand All @@ -79,13 +82,30 @@ bool ttak_ringbuf_push(ttak_ringbuf_t *rb, const void *item);

/**
* @brief Pops an item from the buffer.
*
*
* @param rb Buffer.
* @param out_item Pointer to copy data out to.
* @return true if popped, false if empty.
*/
bool ttak_ringbuf_pop(ttak_ringbuf_t *rb, void *out_item);

/**
* @brief ISR-context push: callable only from interrupt context.
*
* Never takes the rwlock (a pthread lock is illegal in ISR); the
* operation is serialized by the ISR critical section instead. If the
* buffer is shared between a task and an ISR, both sides must use the
* _isr variants — do not mix with the task-context APIs.
*/
bool ttak_ringbuf_push_isr(ttak_ringbuf_t *rb, const void *item);

/**
* @brief ISR-context pop: callable only from interrupt context.
*
* Same locking rules as ttak_ringbuf_push_isr().
*/
bool ttak_ringbuf_pop_isr(ttak_ringbuf_t *rb, void *out_item);

/**
* @brief Checks if empty.
*/
Expand Down
63 changes: 48 additions & 15 deletions src/container/ringbuf.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include <ttak/container/ringbuf.h>
#include <ttak/mem/mem.h>
#include <ttak/timing/timing.h>
#include <ttak/arch/ttak_critical.h>
#include <string.h>
#include <stdlib.h>

static bool ringbuf_is_empty_locked(const ttak_ringbuf_t *rb);
static bool ringbuf_push_unlocked(ttak_ringbuf_t *rb, const void *item);
static bool ringbuf_pop_unlocked(ttak_ringbuf_t *rb, void *out_item);

static inline size_t next_power_of_two(size_t x) {
if (x <= 1) return 1;
Expand Down Expand Up @@ -97,46 +100,76 @@ void ttak_ringbuf_destroy(ttak_ringbuf_t *rb) {
*/
bool ttak_ringbuf_push(ttak_ringbuf_t *rb, const void *item) {
ttak_rwlock_wrlock(&rb->lock);
bool ok = ringbuf_push_unlocked(rb, item);
ttak_rwlock_unlock(&rb->lock);
return ok;
}

/**
* @brief Pops item (copy).
*/
bool ttak_ringbuf_pop(ttak_ringbuf_t *rb, void *out_item) {
ttak_rwlock_wrlock(&rb->lock);
bool ok = ringbuf_pop_unlocked(rb, out_item);
ttak_rwlock_unlock(&rb->lock);
return ok;
}

/**
* @brief ISR-context push (see ringbuf.h): never takes the rwlock, so it
* cannot deadlock against a task holding it. Both sides of an
* ISR-shared buffer must use the _isr variants.
*/
bool ttak_ringbuf_push_isr(ttak_ringbuf_t *rb, const void *item) {
ttak_critical_state_t state;
TTAK_ENTER_CRITICAL_ISR(state);
bool ok = ringbuf_push_unlocked(rb, item);
TTAK_EXIT_CRITICAL_ISR(state);
return ok;
}

/**
* @brief ISR-context pop (see ttak_ringbuf_push_isr).
*/
bool ttak_ringbuf_pop_isr(ttak_ringbuf_t *rb, void *out_item) {
ttak_critical_state_t state;
TTAK_ENTER_CRITICAL_ISR(state);
bool ok = ringbuf_pop_unlocked(rb, out_item);
TTAK_EXIT_CRITICAL_ISR(state);
return ok;
}

static bool ringbuf_push_unlocked(ttak_ringbuf_t *rb, const void *item) {
if (rb->full) {
ttak_rwlock_unlock(&rb->lock);
return false;
}

char *dest = (char *)rb->buffer + (rb->head * rb->item_size);
memcpy(dest, item, rb->item_size);

size_t next_head = rb->mask ? ((rb->head + 1) & rb->mask)
: ((rb->head + 1 < rb->capacity) ? (rb->head + 1) : 0);
rb->head = next_head;
if (rb->head == rb->tail) {
rb->full = true;
}

ttak_rwlock_unlock(&rb->lock);
return true;
}

/**
* @brief Pops item (copy).
*/
bool ttak_ringbuf_pop(ttak_ringbuf_t *rb, void *out_item) {
ttak_rwlock_wrlock(&rb->lock);
static bool ringbuf_pop_unlocked(ttak_ringbuf_t *rb, void *out_item) {
if (ringbuf_is_empty_locked(rb)) {
ttak_rwlock_unlock(&rb->lock);
return false;
}

char *src = (char *)rb->buffer + (rb->tail * rb->item_size);
if (out_item) {
memcpy(out_item, src, rb->item_size);
}

size_t next_tail = rb->mask ? ((rb->tail + 1) & rb->mask)
: ((rb->tail + 1 < rb->capacity) ? (rb->tail + 1) : 0);
rb->tail = next_tail;
rb->full = false;

ttak_rwlock_unlock(&rb->lock);
return true;
}

Expand Down
13 changes: 12 additions & 1 deletion tests/baremetal/startup_cortex_m.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ void DebugMon_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));

/* External interrupt slots (NVIC IRQ0..IRQ3); tests override the weak
* aliases and trigger the IRQ through NVIC->ISPR. */
void TTAK_IRQ0_Handler(void) __attribute__((weak, alias("Default_Handler")));
void TTAK_IRQ1_Handler(void) __attribute__((weak, alias("Default_Handler")));
void TTAK_IRQ2_Handler(void) __attribute__((weak, alias("Default_Handler")));
void TTAK_IRQ3_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((section(".isr_vector"), used))
void (*const g_vectors[16])(void) = {
void (*const g_vectors[16 + 4])(void) = {
(void (*)(void))&_estack,
Reset_Handler,
NMI_Handler,
Expand All @@ -40,6 +47,10 @@ void (*const g_vectors[16])(void) = {
0,
PendSV_Handler,
SysTick_Handler,
TTAK_IRQ0_Handler,
TTAK_IRQ1_Handler,
TTAK_IRQ2_Handler,
TTAK_IRQ3_Handler,
};

#define SCB_CPACR (*(volatile uint32_t *)0xE000ED88u)
Expand Down
Loading
Loading