diff --git a/components/zkernel/CMakeLists.txt b/components/zkernel/CMakeLists.txt index 6a89b51..f6355a2 100644 --- a/components/zkernel/CMakeLists.txt +++ b/components/zkernel/CMakeLists.txt @@ -5,6 +5,7 @@ set(srcs "src/k_event.c" "src/k_thread.c" "src/k_work.c" + "src/ring_buf.c" "src/sys_init.c" "src/fatal.c" ) diff --git a/components/zkernel/Kconfig b/components/zkernel/Kconfig index 5bde062..ed1b15c 100644 --- a/components/zkernel/Kconfig +++ b/components/zkernel/Kconfig @@ -1,5 +1,16 @@ menu "Boreas Kernel (zkernel)" + config RING_BUFFER_LARGE + bool "Use 32-bit ring buffer indices" + default n + help + Widens the ring buffer index type from uint16_t to uint32_t, + raising the maximum ring_buf capacity from 32767 bytes to + ~2 GiB at the cost of 12 extra bytes per struct ring_buf + (six index fields widen by 2 bytes each; 20 -> 32 bytes on + the 32-bit target). Mirrors upstream Zephyr's + CONFIG_RING_BUFFER_LARGE. + config ZKERNEL_MUTEX_DEBUG bool "Enable mutex lock-ordering assertions" default n diff --git a/components/zkernel/include/boreas/zephyr/sys/__assert.h b/components/zkernel/include/boreas/zephyr/sys/__assert.h new file mode 100644 index 0000000..bade260 --- /dev/null +++ b/components/zkernel/include/boreas/zephyr/sys/__assert.h @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 Intercreate + * + * Zephyr-compatible runtime assertions (upstream + * spellings). + * + * Divergence: upstream compiles these out unless CONFIG_ASSERT=y + * (default n); Boreas keeps them always on -- they log and abort. + * NOT safe from IRAM ISR context (ESP_LOGE + abort are flash-resident). + * Use k_panic() (sys/util.h) for unrecoverable errors in ISR/IRAM + * context. + */ + +#pragma once + +#ifndef __ASSERT +#include /* abort() */ + +#include "esp_log.h" +#define __ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + ESP_LOGE("ASSERT", "%s at %s:%d", (msg), __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) +#endif + +/* Assertion with no message (upstream spelling). */ +#ifndef __ASSERT_NO_MSG +#define __ASSERT_NO_MSG(cond) __ASSERT((cond), "") +#endif diff --git a/components/zkernel/include/boreas/zephyr/sys/byteorder.h b/components/zkernel/include/boreas/zephyr/sys/byteorder.h index 87f589d..474dbae 100644 --- a/components/zkernel/include/boreas/zephyr/sys/byteorder.h +++ b/components/zkernel/include/boreas/zephyr/sys/byteorder.h @@ -1,9 +1,13 @@ /* * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 Intercreate + * Copyright (c) 2015-2016 Intel Corporation (upstream Zephyr) + * Copyright 2026 Intercreate (Boreas) * * Zephyr-compatible byte-order helpers for little- and big-endian - * 16- and 32-bit access over byte buffers. + * 16- and 32-bit access over byte buffers. The big/little-endian + * 32-bit composition mirrors upstream's canonical chaining closely + * enough to retain the upstream copyright; the 16-bit leaf functions + * are independently written. * * @note Upstream Zephyr also provides 24/48/64-bit variants * (sys_{get,put}_{le,be}{24,48,64}). These are intentionally diff --git a/components/zkernel/include/boreas/zephyr/sys/ring_buffer.h b/components/zkernel/include/boreas/zephyr/sys/ring_buffer.h new file mode 100644 index 0000000..3f87edc --- /dev/null +++ b/components/zkernel/include/boreas/zephyr/sys/ring_buffer.h @@ -0,0 +1,427 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2015 Intel Corporation (upstream Zephyr original) + * Copyright 2026 Intercreate (Boreas port) + * + * Zephyr-compatible ring buffer (sys/ring_buffer.h). Near-verbatim port + * of upstream Zephyr (include/zephyr/sys/ring_buffer.h, + * lib/utils/ring_buffer.c -- both Apache-2.0), adapted only for the + * Boreas include layout and toolchain shims (see sys/util.h). + * + * Divergences: + * - the upstream item-mode API (ring_buf_item_put/_get, + * ring_buf_item_init, RING_BUF_ITEM_DECLARE*) is intentionally NOT + * ported -- it is @deprecated upstream ("use ") + * and the byte-mode API below is what the UART IRQ pattern, the + * direct-UART shell transport, and the modbus serial backend use; + * - the implementation functions are K_ISR_SAFE (IRAM-resident) so + * the byte API is callable from ESP_INTR_FLAG_IRAM ISRs -- see + * src/ring_buf.c. + * + * Concurrency (code unchanged from upstream): the buffer is lock-free + * for a SINGLE producer and a SINGLE consumer in separate contexts + * (two threads, or thread + ISR) -- the producer writes only `put`, + * the consumer writes only `get`, and the query inlines (is_empty, + * size_get, space_get) read one index from each side as aligned + * single-word loads, so they are tear-free from either side and return + * conservatively stale answers. No field is volatile and no memory + * barriers are issued, so: + * - do NOT busy-wait on the query inlines: in a call-free loop the + * compiler may legally hoist the index load and never observe the + * other side's progress. Block on a k_sem/k_event signaled by the + * other side instead; + * - on SMP, the buffer alone does not order the data bytes against + * the index publication: pair each handoff with a k_sem (or other + * acquire/release primitive) and do not drain past the handoff + * you were signaled for. + * Multiple producers OR multiple consumers must serialize access + * externally (e.g. a k_mutex or by locking interrupts). + */ + +#pragma once + +#include +#include +#include + +/* struct ring_buf's index width is selected by CONFIG_RING_BUFFER_LARGE + * below, so the config must be visible wherever this struct is defined + * -- include it explicitly rather than relying on a transitive chain + * (the library and every consumer must agree on the layout). */ +#include "sdkconfig.h" + +#include "zephyr/sys/util.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @cond INTERNAL_HIDDEN */ + +#ifdef CONFIG_RING_BUFFER_LARGE +typedef uint32_t ring_buf_idx_t; +#define RING_BUFFER_MAX_SIZE (UINT32_MAX / 2) +#define RING_BUFFER_SIZE_ASSERT_MSG "Size too big" +#else +typedef uint16_t ring_buf_idx_t; +#define RING_BUFFER_MAX_SIZE (UINT16_MAX / 2) +#define RING_BUFFER_SIZE_ASSERT_MSG "Size too big, please enable CONFIG_RING_BUFFER_LARGE" +#endif + +struct ring_buf_index { + ring_buf_idx_t head; + ring_buf_idx_t tail; + ring_buf_idx_t base; +}; + +/** @endcond */ + +/** + * @brief A structure to represent a ring buffer. + */ +struct ring_buf { + /** @cond INTERNAL_HIDDEN */ + uint8_t *buffer; + struct ring_buf_index put; + struct ring_buf_index get; + uint32_t size; + /** @endcond */ +}; + +/** @cond INTERNAL_HIDDEN */ + +uint32_t ring_buf_area_claim(struct ring_buf *buf, struct ring_buf_index *ring, uint8_t **data, + uint32_t size); +int ring_buf_area_finish(struct ring_buf *buf, struct ring_buf_index *ring, uint32_t size); + +/** + * @brief Force ring_buf internal states to a given value. + * + * Any value other than 0 makes sense only in validation-testing context. + */ +static inline void ring_buf_internal_reset(struct ring_buf *buf, ring_buf_idx_t value) +{ + buf->put.head = buf->put.tail = buf->put.base = value; + buf->get.head = buf->get.tail = buf->get.base = value; +} + +/** @endcond */ + +/** + * @brief Statically initialize a ring buffer for byte data. + * + * For use in a struct ring_buf initializer with a caller-provided data + * area, e.g. `struct ring_buf rb = RING_BUF_INIT(buf, sizeof(buf));`. + * + * @warning @a size8 must be <= RING_BUFFER_MAX_SIZE. Unlike + * @ref RING_BUF_DECLARE (BUILD_ASSERT) and @ref ring_buf_init + * (runtime __ASSERT), an initializer cannot check this; an oversized + * buffer compiles silently and corrupts data without error once + * cumulative traffic exceeds the index range (upstream has the same + * unchecked macro). + * + * @param buf Pointer to the data area (uint8_t[]). + * @param size8 Size of the data area (in bytes). + */ +#define RING_BUF_INIT(buf, size8) \ + { \ + .buffer = (buf), \ + .size = (size8), \ + } + +/** + * @brief Define and initialize a ring buffer for byte data. + * + * This macro establishes a ring buffer of an arbitrary size. + * The basic storage unit is a byte. + * + * The ring buffer can be accessed outside the module where it is defined + * using: + * + * @code extern struct ring_buf ; @endcode + * + * @note File-scope only: the macro expands to a BUILD_ASSERT plus two + * declarations, so it cannot be prefixed with `static`, and at block + * scope it would silently create a fresh automatic struct ring_buf per + * call over one shared function-static data array. + * + * @param name Name of the ring buffer. + * @param size8 Size of ring buffer (in bytes). + */ +#define RING_BUF_DECLARE(name, size8) \ + BUILD_ASSERT((size8) <= RING_BUFFER_MAX_SIZE, RING_BUFFER_SIZE_ASSERT_MSG); \ + static uint8_t __noinit _ring_buffer_data_##name[size8]; \ + struct ring_buf name = RING_BUF_INIT(_ring_buffer_data_##name, size8) + +/** + * @brief Initialize a ring buffer for byte data. + * + * This routine initializes a ring buffer, prior to its first use. It is only + * used for ring buffers not defined using RING_BUF_DECLARE. + * + * @param buf Address of ring buffer. + * @param size Ring buffer size (in bytes). + * @param data Ring buffer data area (uint8_t data[size]). + */ +static inline void ring_buf_init(struct ring_buf *buf, uint32_t size, uint8_t *data) +{ + __ASSERT(size <= RING_BUFFER_MAX_SIZE, RING_BUFFER_SIZE_ASSERT_MSG); + + buf->size = size; + buf->buffer = data; + ring_buf_internal_reset(buf, 0); +} + +/** + * @brief Determine if a ring buffer is empty. + * + * @param buf Address of ring buffer. + * + * @return true if the ring buffer is empty, or false if not. + */ +static inline bool ring_buf_is_empty(const struct ring_buf *buf) +{ + return buf->get.head == buf->put.tail; +} + +/** + * @brief Reset ring buffer state. + * + * @param buf Address of ring buffer. + */ +static inline void ring_buf_reset(struct ring_buf *buf) +{ + ring_buf_internal_reset(buf, 0); +} + +/** + * @brief Determine free space in a ring buffer. + * + * @param buf Address of ring buffer. + * + * @return Ring buffer free space (in bytes). + */ +static inline uint32_t ring_buf_space_get(const struct ring_buf *buf) +{ + ring_buf_idx_t allocated = buf->put.head - buf->get.tail; + + return buf->size - allocated; +} + +/** + * @brief Return ring buffer capacity. + * + * @param buf Address of ring buffer. + * + * @return Ring buffer capacity (in bytes). + */ +static inline uint32_t ring_buf_capacity_get(const struct ring_buf *buf) +{ + return buf->size; +} + +/** + * @brief Determine size of available data in a ring buffer. + * + * Counts committed-and-unread bytes only: bytes inside an outstanding + * (unfinished) claim are counted neither here nor by + * @ref ring_buf_space_get. (Wording from current upstream main; the + * v3.7-era "used space" phrasing was misleading around claims.) + * + * @param buf Address of ring buffer. + * + * @return Ring buffer data size (in bytes). + */ +static inline uint32_t ring_buf_size_get(const struct ring_buf *buf) +{ + ring_buf_idx_t available = buf->put.tail - buf->get.head; + + return available; +} + +/** + * @brief Allocate buffer for writing data to a ring buffer. + * + * With this routine, memory copying can be reduced since internal ring buffer + * can be used directly by the user. Once data is written to allocated area + * number of bytes written must be confirmed (see @ref ring_buf_put_finish). + * + * @warning + * Use cases involving multiple writers to the ring buffer must prevent + * concurrent write operations, either by preventing all writers from + * being preempted or by using a mutex to govern writes to the ring buffer. + * + * @note An outstanding claim must be completed with + * @ref ring_buf_put_finish before any other put-side call (including + * copy-mode @ref ring_buf_put): finishing rewinds the allocation head to + * the committed tail, silently discarding whatever was still claimed. + * + * @param buf Address of ring buffer. + * @param data Pointer to the address. It is set to a location within + * ring buffer. + * @param size Requested allocation size (in bytes). + * + * @return Size of allocated buffer which can be smaller than requested if + * there is not enough free space or buffer wraps. + */ +static inline uint32_t ring_buf_put_claim(struct ring_buf *buf, uint8_t **data, uint32_t size) +{ + uint32_t space = ring_buf_space_get(buf); + + return ring_buf_area_claim(buf, &buf->put, data, MIN(size, space)); +} + +/** + * @brief Indicate number of bytes written to allocated buffers. + * + * The number of bytes must be equal to or lower than the sum corresponding + * to all preceding @ref ring_buf_put_claim invocations (or even 0). Surplus + * bytes will be returned to the available free buffer space. + * + * @warning + * Use cases involving multiple writers to the ring buffer must prevent + * concurrent write operations, either by preventing all writers from + * being preempted or by using a mutex to govern writes to the ring buffer. + * + * @param buf Address of ring buffer. + * @param size Number of valid bytes in the allocated buffers. + * + * @retval 0 Successful operation. + * @retval -EINVAL Provided @a size exceeds the bytes claimed by preceding + * @ref ring_buf_put_claim invocations and not yet finished. + * (Upstream documents "exceeds free space", but the code -- here + * and upstream -- checks the outstanding claimed amount.) + */ +static inline int ring_buf_put_finish(struct ring_buf *buf, uint32_t size) +{ + return ring_buf_area_finish(buf, &buf->put, size); +} + +/** + * @brief Write (copy) data to a ring buffer. + * + * This routine writes data to a ring buffer @a buf. + * + * @warning + * Use cases involving multiple writers to the ring buffer must prevent + * concurrent write operations, either by preventing all writers from + * being preempted or by using a mutex to govern writes to the ring buffer. + * + * @param buf Address of ring buffer. + * @param data Address of data. + * @param size Data size (in bytes). + * + * @return Number of bytes written. + */ +uint32_t ring_buf_put(struct ring_buf *buf, const uint8_t *data, uint32_t size); + +/** + * @brief Get address of a valid data in a ring buffer. + * + * With this routine, memory copying can be reduced since internal ring buffer + * can be used directly by the user. Once data is processed it must be freed + * using @ref ring_buf_get_finish. + * + * @warning + * Use cases involving multiple reads of the ring buffer must prevent + * concurrent read operations, either by preventing all readers from being + * preempted or by using a mutex to govern reads to the ring buffer. + * + * @note An outstanding claim must be completed with + * @ref ring_buf_get_finish before any other get-side call (including + * copy-mode @ref ring_buf_get and @ref ring_buf_peek): finishing rewinds + * the claim head to the freed tail, silently discarding whatever was + * still claimed. + * + * @param buf Address of ring buffer. + * @param data Pointer to the address. It is set to a location within + * ring buffer. + * @param size Requested size (in bytes). + * + * @return Number of valid bytes in the provided buffer which can be smaller + * than requested if there is not enough free space or buffer wraps. + */ +static inline uint32_t ring_buf_get_claim(struct ring_buf *buf, uint8_t **data, uint32_t size) +{ + uint32_t buf_size = ring_buf_size_get(buf); + + return ring_buf_area_claim(buf, &buf->get, data, MIN(size, buf_size)); +} + +/** + * @brief Indicate number of bytes read from claimed buffer. + * + * The number of bytes must be equal or lower than the sum corresponding to + * all preceding @ref ring_buf_get_claim invocations (or even 0). Surplus + * bytes will remain available in the buffer. + * + * @warning + * Use cases involving multiple reads of the ring buffer must prevent + * concurrent read operations, either by preventing all readers from being + * preempted or by using a mutex to govern reads to the ring buffer. + * + * @param buf Address of ring buffer. + * @param size Number of bytes that can be freed. + * + * @retval 0 Successful operation. + * @retval -EINVAL Provided @a size exceeds the bytes claimed by preceding + * @ref ring_buf_get_claim invocations and not yet finished. + * (Upstream documents "exceeds valid bytes", but the code -- here + * and upstream -- checks the outstanding claimed amount.) + */ +static inline int ring_buf_get_finish(struct ring_buf *buf, uint32_t size) +{ + return ring_buf_area_finish(buf, &buf->get, size); +} + +/** + * @brief Read data from a ring buffer. + * + * This routine reads data from a ring buffer @a buf. + * + * @warning + * Use cases involving multiple reads of the ring buffer must prevent + * concurrent read operations, either by preventing all readers from being + * preempted or by using a mutex to govern reads to the ring buffer. + * + * @param buf Address of ring buffer. + * @param data Address of the output buffer. Can be NULL to discard data. + * @param size Data size (in bytes). + * + * @return Number of bytes written to the output buffer. + */ +uint32_t ring_buf_get(struct ring_buf *buf, uint8_t *data, uint32_t size); + +/** + * @brief Peek at data from a ring buffer. + * + * This routine reads data from a ring buffer @a buf without removing it from + * the buffer. + * + * @warning + * Use cases involving multiple reads of the ring buffer must prevent + * concurrent read operations, either by preventing all readers from being + * preempted or by using a mutex to govern reads to the ring buffer. + * + * @warning + * Multiple calls to peek will result in the same data being 'peeked' multiple + * times. To remove data, use either @ref ring_buf_get or @ref + * ring_buf_get_claim followed by @ref ring_buf_get_finish with a non-zero + * `size`. + * + * @note Internally peek claims and then unclaims (finishes with size 0), + * which rewinds the get-side claim head -- so it must not be called while + * a @ref ring_buf_get_claim is outstanding (the claim would be silently + * voided and peek would return bytes past it). + * + * @param buf Address of ring buffer. + * @param data Address of the output buffer. Cannot be NULL. + * @param size Data size (in bytes). + * + * @return Number of bytes written to the output buffer. + */ +uint32_t ring_buf_peek(struct ring_buf *buf, uint8_t *data, uint32_t size); + +#ifdef __cplusplus +} +#endif diff --git a/components/zkernel/include/boreas/zephyr/sys/util.h b/components/zkernel/include/boreas/zephyr/sys/util.h index 281b9cc..9229298 100644 --- a/components/zkernel/include/boreas/zephyr/sys/util.h +++ b/components/zkernel/include/boreas/zephyr/sys/util.h @@ -1,14 +1,24 @@ /* * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 Intercreate + * Copyright (c) 2011-2014 Wind River Systems, Inc. (upstream Zephyr) + * Copyright 2026 Intercreate (Boreas) * - * Zephyr-compatible utility macros. + * Zephyr-compatible utility macros. Most macros here are universal C + * idioms (independently written), but the IS_ENABLED machinery (the + * _XXXX##/_YYYY token-paste trick) is taken verbatim from upstream + * Zephyr, hence the retained upstream copyright. */ #pragma once #include "esp_attr.h" +/* Compatibility re-exports: upstream code reaches these symbols through + * and , but historical + * Boreas ports include only this header -- keep both paths working. */ +#include "zephyr/sys/__assert.h" +#include "zephyr/toolchain.h" + #ifdef __cplusplus extern "C" { #endif @@ -93,30 +103,6 @@ extern "C" { #define ALIGNED(x) __attribute__((__aligned__(x))) #endif -/* Weak symbol */ -#ifndef __weak -#define __weak __attribute__((__weak__)) -#endif - -/* Always inline — undef any prior definition to guarantee `inline` is present - * (RISC-V GCC errors without it under -Werror=attributes). */ -#undef ALWAYS_INLINE -#define ALWAYS_INLINE __attribute__((always_inline)) inline - -/* Runtime assertion -- logs and aborts. - * NOT safe from IRAM ISR context (ESP_LOGE + abort are flash-resident). - * Use k_panic() for unrecoverable errors in ISR/IRAM context. */ -#ifndef __ASSERT -#include "esp_log.h" -#define __ASSERT(cond, msg) \ - do { \ - if (!(cond)) { \ - ESP_LOGE("ASSERT", "%s at %s:%d", (msg), __FILE__, __LINE__); \ - abort(); \ - } \ - } while (0) -#endif - /* IRAM-safe panic -- triggers an illegal-instruction exception caught by * the ESP-IDF panic handler (which is IRAM-resident). Produces a full * backtrace on UART and reboots. Safe to call from IRAM_ATTR ISR context. diff --git a/components/zkernel/include/boreas/zephyr/toolchain.h b/components/zkernel/include/boreas/zephyr/toolchain.h new file mode 100644 index 0000000..faa15a5 --- /dev/null +++ b/components/zkernel/include/boreas/zephyr/toolchain.h @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 Intercreate + * + * Zephyr-compatible toolchain shims (upstream + * spellings), kept thin so near-verbatim ports can keep their upstream + * #include shape. Definitions are #ifndef-guarded -- a TU that already + * got a definition elsewhere keeps the first one it saw -- EXCEPT + * ALWAYS_INLINE, which is intentionally #undef'd and redefined + * unconditionally (see the note on it below). + */ + +#pragma once + +#include "esp_attr.h" + +/* Branch-prediction hints. + * Divergence from ESP-IDF: esp_compiler.h defines likely/unlikely as + * plain (x) unless CONFIG_COMPILER_OPTIMIZATION_PERF is set; Boreas + * matches upstream Zephyr's unconditional __builtin_expect instead. + * Both definitions are #ifndef-guarded, so include order picks the + * winner per TU -- the difference is codegen-only (identical + * semantics either way). */ +#ifndef likely +#define likely(x) __builtin_expect(!!(x), 1) +#endif +#ifndef unlikely +#define unlikely(x) __builtin_expect(!!(x), 0) +#endif + +/* Weak symbol */ +#ifndef __weak +#define __weak __attribute__((__weak__)) +#endif + +/* Always inline -- undef any prior definition to guarantee `inline` is + * present (RISC-V GCC errors without it under -Werror=attributes). */ +#undef ALWAYS_INLINE +#define ALWAYS_INLINE __attribute__((always_inline)) inline + +/* Upstream places __noinit data in a section the loader does not zero + * (so it also survives a warm reset). Mapped to ESP-IDF's + * __NOINIT_ATTR: a genuine .noinit section on hardware targets, and a + * no-op on the linux/host test target (esp_attr.h's _SECTION_ATTR_IMPL + * expands to nothing under CONFIG_IDF_TARGET_LINUX), where __noinit + * data is ordinary zero-initialized BSS instead. Code that must retain + * data across a warm reset therefore cannot rely on this shim on the + * host target; on hardware, prefer RTC_NOINIT_ATTR when the data must + * also survive deep sleep. */ +#ifndef __noinit +#define __noinit __NOINIT_ATTR +#endif diff --git a/components/zkernel/src/ring_buf.c b/components/zkernel/src/ring_buf.c new file mode 100644 index 0000000..6bd5fea --- /dev/null +++ b/components/zkernel/src/ring_buf.c @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2015 Intel Corporation (upstream Zephyr original) + * Copyright 2026 Intercreate (Boreas port) + * + * Zephyr-compatible ring buffer. Near-verbatim port of upstream Zephyr + * lib/utils/ring_buffer.c (Apache-2.0), adapted only for the Boreas + * include layout and toolchain shims: + * - upstream's lowercase min() (from ) -> MIN + * (sys/util.h); + * - the deprecated item-mode API (ring_buf_item_put/_get) is not + * ported -- see sys/ring_buffer.h; + * - all functions are K_ISR_SAFE (IRAM-resident) and the internal + * __ASSERT_NO_MSGs are replaced with IRAM-safe k_panic() checks: + * the byte API is documented for thread + ISR use, and ESP-IDF + * ISRs registered with ESP_INTR_FLAG_IRAM fire during + * flash-cache-disabled windows where flash-resident code (and + * __ASSERT's ESP_LOGE + abort path) would fault. memcpy is + * ROM-resident on ESP32-S3, so the copy loops remain safe. + * + * The index scheme is upstream's base-offset design: head/tail/base are + * ring_buf_idx_t (uint16_t, or uint32_t with CONFIG_RING_BUFFER_LARGE), + * the byte offset is (head - base) with a single wrap correction, and + * RING_BUFFER_MAX_SIZE caps size to half the index range so unsigned + * subtractions disambiguate full vs empty. Keep ring_buf_idx_t exactly + * as the header declares it -- widening only one side breaks the + * empty/full discrimination. + */ + +#include +#include + +#include "sdkconfig.h" /* CONFIG_RING_BUFFER_LARGE selects the index width */ + +#include "zephyr/sys/ring_buffer.h" +#include "zephyr/sys/util.h" + +uint32_t K_ISR_SAFE ring_buf_area_claim(struct ring_buf *buf, struct ring_buf_index *ring, + uint8_t **data, uint32_t size) +{ + ring_buf_idx_t head_offset, wrap_size; + + head_offset = ring->head - ring->base; + if (unlikely(head_offset >= buf->size)) { + /* ring->base is not yet adjusted */ + head_offset -= buf->size; + } + wrap_size = buf->size - head_offset; + size = MIN(size, wrap_size); + + *data = &buf->buffer[head_offset]; + ring->head += size; + + return size; +} + +int K_ISR_SAFE ring_buf_area_finish(struct ring_buf *buf, struct ring_buf_index *ring, + uint32_t size) +{ + ring_buf_idx_t claimed_size, tail_offset; + + claimed_size = ring->head - ring->tail; + if (unlikely(size > claimed_size)) { + return -EINVAL; + } + + ring->tail += size; + ring->head = ring->tail; + + tail_offset = ring->tail - ring->base; + if (unlikely(tail_offset >= buf->size)) { + /* we wrapped: adjust ring->base */ + ring->base += buf->size; + } + + return 0; +} + +uint32_t K_ISR_SAFE ring_buf_put(struct ring_buf *buf, const uint8_t *data, uint32_t size) +{ + uint8_t *dst; + uint32_t partial_size; + uint32_t total_size = 0U; + int err; + + do { + partial_size = ring_buf_put_claim(buf, &dst, size); + if (partial_size == 0) { + break; + } + memcpy(dst, data, partial_size); + total_size += partial_size; + size -= partial_size; + data += partial_size; + } while (size != 0); + + err = ring_buf_put_finish(buf, total_size); + if (unlikely(err != 0)) { + /* finish of our own claim cannot fail unless the index + * state was corrupted by unserialized access */ + k_panic(); + } + + return total_size; +} + +uint32_t K_ISR_SAFE ring_buf_get(struct ring_buf *buf, uint8_t *data, uint32_t size) +{ + uint8_t *src; + uint32_t partial_size; + uint32_t total_size = 0U; + int err; + + do { + partial_size = ring_buf_get_claim(buf, &src, size); + if (partial_size == 0) { + break; + } + if (data) { + memcpy(data, src, partial_size); + data += partial_size; + } + total_size += partial_size; + size -= partial_size; + } while (size != 0); + + err = ring_buf_get_finish(buf, total_size); + if (unlikely(err != 0)) { + /* finish of our own claim cannot fail unless the index + * state was corrupted by unserialized access */ + k_panic(); + } + + return total_size; +} + +uint32_t K_ISR_SAFE ring_buf_peek(struct ring_buf *buf, uint8_t *data, uint32_t size) +{ + uint8_t *src; + uint32_t partial_size; + uint32_t total_size = 0U; + int err; + + /* checked before the loop so the doc contract ("Cannot be NULL") + * is enforced on an empty buffer too, not only once data flows */ + if (unlikely(data == NULL)) { + k_panic(); + } + + do { + partial_size = ring_buf_get_claim(buf, &src, size); + if (partial_size == 0) { + break; + } + memcpy(data, src, partial_size); + data += partial_size; + total_size += partial_size; + size -= partial_size; + } while (size != 0); + + /* effectively unclaim total_size bytes */ + err = ring_buf_get_finish(buf, 0); + if (unlikely(err != 0)) { + k_panic(); + } + + return total_size; +} diff --git a/test/main/CMakeLists.txt b/test/main/CMakeLists.txt index ec93172..b04800e 100644 --- a/test/main/CMakeLists.txt +++ b/test/main/CMakeLists.txt @@ -5,6 +5,7 @@ set(test_srcs "test_dlist.c" "test_byteorder.c" "test_atomic.c" + "test_ring_buf.c" "test_k_sem.c" "test_k_mutex.c" "test_k_msgq.c" diff --git a/test/main/test_main.c b/test/main/test_main.c index 549a67e..ab5e7ab 100644 --- a/test/main/test_main.c +++ b/test/main/test_main.c @@ -15,6 +15,7 @@ void test_slist_group(void); void test_dlist_group(void); void test_byteorder_group(void); void test_atomic_group(void); +void test_ring_buf_group(void); void test_k_sem_group(void); void test_k_mutex_group(void); void test_k_msgq_group(void); @@ -53,6 +54,7 @@ void app_main(void) test_dlist_group(); test_byteorder_group(); test_atomic_group(); + test_ring_buf_group(); /* Layer 1: Kernel primitives */ test_k_sem_group(); diff --git a/test/main/test_ring_buf.c b/test/main/test_ring_buf.c new file mode 100644 index 0000000..cca59a7 --- /dev/null +++ b/test/main/test_ring_buf.c @@ -0,0 +1,503 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 Intercreate + */ + +#include +#include + +#include "unity.h" +#include "zephyr/sys/ring_buffer.h" + +/* ---------------------------------------------------------------- + * Accounting: capacity / size / space / is_empty + * ---------------------------------------------------------------- */ + +static void test_ring_buf_init_accounting(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + + ring_buf_init(&rb, sizeof(storage), storage); + + TEST_ASSERT_EQUAL(16, ring_buf_capacity_get(&rb)); + TEST_ASSERT_EQUAL(0, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(16, ring_buf_space_get(&rb)); + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); +} + +static void test_ring_buf_put_get_roundtrip(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + const uint8_t in[] = {1, 2, 3, 4, 5}; + uint8_t out[8] = {0}; + + ring_buf_init(&rb, sizeof(storage), storage); + + TEST_ASSERT_EQUAL(5, ring_buf_put(&rb, in, sizeof(in))); + TEST_ASSERT_EQUAL(5, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(11, ring_buf_space_get(&rb)); + TEST_ASSERT_FALSE(ring_buf_is_empty(&rb)); + + TEST_ASSERT_EQUAL(5, ring_buf_get(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(in, out, sizeof(in)); + TEST_ASSERT_EQUAL(0, ring_buf_size_get(&rb)); + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); +} + +/* ---------------------------------------------------------------- + * Full / partial semantics + * ---------------------------------------------------------------- */ + +static void test_ring_buf_put_saturates_at_capacity(void) +{ + uint8_t storage[8]; + struct ring_buf rb; + /* contents never inspected -- this test asserts only counts */ + uint8_t in[12] = {0}; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* Only capacity bytes fit; the rest is refused. */ + TEST_ASSERT_EQUAL(8, ring_buf_put(&rb, in, sizeof(in))); + TEST_ASSERT_EQUAL(0, ring_buf_space_get(&rb)); + /* A further put on a full buffer writes nothing. */ + TEST_ASSERT_EQUAL(0, ring_buf_put(&rb, in, 1)); + + /* The zero-copy claim path (what the UART IRQ uses) must also + * report no room: put_claim returns 0 on a full buffer. */ + uint8_t *dst; + TEST_ASSERT_EQUAL(0, ring_buf_put_claim(&rb, &dst, 1)); +} + +static void test_ring_buf_get_more_than_available(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + const uint8_t in[] = {9, 8, 7}; + uint8_t out[8] = {0}; + + ring_buf_init(&rb, sizeof(storage), storage); + ring_buf_put(&rb, in, sizeof(in)); + + /* Asking for more than present returns only what is there. */ + TEST_ASSERT_EQUAL(3, ring_buf_get(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(in, out, 3); +} + +static void test_ring_buf_get_null_discards(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + const uint8_t in[] = {1, 2, 3, 4}; + + ring_buf_init(&rb, sizeof(storage), storage); + ring_buf_put(&rb, in, sizeof(in)); + + /* NULL output discards without copying. */ + TEST_ASSERT_EQUAL(4, ring_buf_get(&rb, NULL, sizeof(in))); + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); +} + +/* ---------------------------------------------------------------- + * peek does not consume + * ---------------------------------------------------------------- */ + +static void test_ring_buf_peek_does_not_consume(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + const uint8_t in[] = {5, 6, 7, 8}; + uint8_t out[4] = {0}; + + ring_buf_init(&rb, sizeof(storage), storage); + ring_buf_put(&rb, in, sizeof(in)); + + TEST_ASSERT_EQUAL(4, ring_buf_peek(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(in, out, sizeof(in)); + /* Still all there after peek. */ + TEST_ASSERT_EQUAL(4, ring_buf_size_get(&rb)); + + /* Second peek returns the same bytes. */ + memset(out, 0, sizeof(out)); + TEST_ASSERT_EQUAL(4, ring_buf_peek(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(in, out, sizeof(in)); +} + +/* ---------------------------------------------------------------- + * reset + * ---------------------------------------------------------------- */ + +static void test_ring_buf_reset_empties(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + const uint8_t in[] = {1, 2, 3}; + + ring_buf_init(&rb, sizeof(storage), storage); + ring_buf_put(&rb, in, sizeof(in)); + TEST_ASSERT_FALSE(ring_buf_is_empty(&rb)); + + ring_buf_reset(&rb); + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); + TEST_ASSERT_EQUAL(0, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(16, ring_buf_space_get(&rb)); +} + +/* ---------------------------------------------------------------- + * claim / finish (zero-copy paths) + * ---------------------------------------------------------------- */ + +static void test_ring_buf_put_claim_finish(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + uint8_t *dst; + uint8_t out[8] = {0}; + + ring_buf_init(&rb, sizeof(storage), storage); + + uint32_t claimed = ring_buf_put_claim(&rb, &dst, 6); + TEST_ASSERT_EQUAL(6, claimed); + for (uint32_t i = 0; i < claimed; i++) { + dst[i] = (uint8_t)(0x10 + i); + } + TEST_ASSERT_EQUAL(0, ring_buf_put_finish(&rb, 6)); + TEST_ASSERT_EQUAL(6, ring_buf_size_get(&rb)); + + TEST_ASSERT_EQUAL(6, ring_buf_get(&rb, out, 6)); + for (int i = 0; i < 6; i++) { + TEST_ASSERT_EQUAL_UINT8(0x10 + i, out[i]); + } +} + +static void test_ring_buf_put_finish_surplus_returns_space(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + uint8_t *dst; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* Claim 10 but only commit 4; the surplus 6 returns to free space. */ + TEST_ASSERT_EQUAL(10, ring_buf_put_claim(&rb, &dst, 10)); + TEST_ASSERT_EQUAL(0, ring_buf_put_finish(&rb, 4)); + TEST_ASSERT_EQUAL(4, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(12, ring_buf_space_get(&rb)); +} + +static void test_ring_buf_put_finish_over_claim_einval(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + uint8_t *dst; + + ring_buf_init(&rb, sizeof(storage), storage); + + TEST_ASSERT_EQUAL(4, ring_buf_put_claim(&rb, &dst, 4)); + /* Committing more than claimed is rejected. */ + TEST_ASSERT_EQUAL(-EINVAL, ring_buf_put_finish(&rb, 5)); +} + +static void test_ring_buf_get_claim_finish(void) +{ + uint8_t storage[16]; + struct ring_buf rb; + const uint8_t in[] = {1, 2, 3, 4, 5, 6}; + uint8_t *src; + + ring_buf_init(&rb, sizeof(storage), storage); + ring_buf_put(&rb, in, sizeof(in)); + + uint32_t got = ring_buf_get_claim(&rb, &src, 6); + TEST_ASSERT_EQUAL(6, got); + TEST_ASSERT_EQUAL_UINT8_ARRAY(in, src, 6); + + /* Free only 2; the other 4 remain available. */ + TEST_ASSERT_EQUAL(0, ring_buf_get_finish(&rb, 2)); + TEST_ASSERT_EQUAL(4, ring_buf_size_get(&rb)); +} + +/* ---------------------------------------------------------------- + * Wraparound: a claim never crosses the physical end of the buffer, + * so a put/get spanning the wrap is split across two claims. This is + * the load-bearing index arithmetic (base-offset scheme). + * ---------------------------------------------------------------- */ + +static void test_ring_buf_put_claim_wraps(void) +{ + uint8_t storage[8]; + struct ring_buf rb; + uint8_t *dst; + const uint8_t a[] = {1, 2, 3, 4, 5, 6}; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* Advance head/tail to offset 6, then drain so space==capacity + * but the write offset sits near the end. */ + ring_buf_put(&rb, a, sizeof(a)); + TEST_ASSERT_EQUAL(6, ring_buf_get(&rb, NULL, sizeof(a))); + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); + + /* Now a claim for 8 returns only the 2 contiguous bytes to the + * physical end; the rest needs a second claim after it. */ + uint32_t first = ring_buf_put_claim(&rb, &dst, 8); + TEST_ASSERT_EQUAL(2, first); + dst[0] = 0xAA; + dst[1] = 0xBB; + + uint32_t second = ring_buf_put_claim(&rb, &dst, 8); + TEST_ASSERT_EQUAL(6, second); /* wrapped to the start */ + for (uint32_t i = 0; i < second; i++) { + dst[i] = (uint8_t)(0xC0 + i); + } + TEST_ASSERT_EQUAL(0, ring_buf_put_finish(&rb, first + second)); + TEST_ASSERT_EQUAL(8, ring_buf_size_get(&rb)); + + /* Drain and verify the two claims landed at the correct physical + * offsets (end segment first, then the wrapped start). */ + const uint8_t expect[8] = {0xAA, 0xBB, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5}; + uint8_t out[8] = {0}; + TEST_ASSERT_EQUAL(8, ring_buf_get(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expect, out, sizeof(expect)); +} + +/* ring_buf_put/get themselves loop over the wrap, so a single call that + * spans the boundary must still round-trip the bytes in order. */ +static void test_ring_buf_put_get_across_wrap(void) +{ + uint8_t storage[8]; + struct ring_buf rb; + uint8_t out[8] = {0}; + const uint8_t a[] = {10, 11, 12, 13, 14}; + const uint8_t b[] = {20, 21, 22, 23, 24, 25}; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* Push the offset forward by 5, discard it. */ + ring_buf_put(&rb, a, sizeof(a)); + TEST_ASSERT_EQUAL(5, ring_buf_get(&rb, NULL, sizeof(a))); + + /* This 6-byte write starts at offset 5 and wraps the 8-byte buffer. */ + TEST_ASSERT_EQUAL(6, ring_buf_put(&rb, b, sizeof(b))); + /* Accounting must be correct across the wrap: space_get uses the + * other unsigned-subtraction path (put.head - get.tail). */ + TEST_ASSERT_EQUAL(6, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(2, ring_buf_space_get(&rb)); + + TEST_ASSERT_EQUAL(6, ring_buf_get(&rb, out, sizeof(b))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(b, out, sizeof(b)); + /* Fully drained after the wrap. */ + TEST_ASSERT_EQUAL(0, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(8, ring_buf_space_get(&rb)); +} + +/* Many wrap cycles exercise base advancement and the unsigned index + * subtractions across the full ring_buf_idx_t range without drift. */ +static void test_ring_buf_many_wrap_cycles(void) +{ + uint8_t storage[7]; /* non-power-of-two on purpose */ + struct ring_buf rb; + uint8_t next_write = 0; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* 100000 bytes streamed in 5-byte chunks through a 7-byte buffer so + * the physical offset wraps constantly and the default uint16_t + * indices cross their 65536 range ~1.5 times (the seeded test below + * covers the index wrap under CONFIG_RING_BUFFER_LARGE). */ + for (int cycle = 0; cycle < 20000; cycle++) { + uint8_t chunk[5]; + + for (int i = 0; i < 5; i++) { + chunk[i] = next_write++; + } + TEST_ASSERT_EQUAL(5, ring_buf_put(&rb, chunk, sizeof(chunk))); + + /* drained fully each cycle, so the chunk is the expectation */ + uint8_t out[5] = {0}; + TEST_ASSERT_EQUAL(5, ring_buf_get(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(chunk, out, sizeof(out)); + } + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); +} + +/* Fill-to-full / drain-to-empty repeatedly, enough cycles to carry the + * default uint16_t indices across their 65536 wrap, asserting the full + * and empty discriminations AT high `allocated` each cycle. The + * many-wrap-cycles test above drains within a 7-byte span so allocated + * never approaches capacity; this one holds the buffer completely full + * across the index wrap, exercising space_get == 0 / put returning 0 + * (full) vs is_empty (empty) right where the modular subtraction must + * stay unambiguous. (Traffic volume cannot reach the 2^32 wrap under + * CONFIG_RING_BUFFER_LARGE -- the seeded test below covers that.) */ +static void test_ring_buf_full_empty_across_wrap(void) +{ + static uint8_t storage[4096]; + static uint8_t expected[sizeof(storage)]; /* per-cycle ramp, off-stack */ + struct ring_buf rb; + uint8_t *dst; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* 4096 bytes/cycle; 40 cycles = 163840 bytes, crossing the 65536 + * index wrap ~2.5 times. */ + for (int cycle = 0; cycle < 40; cycle++) { + uint8_t seed = (uint8_t)cycle; + + for (uint32_t i = 0; i < sizeof(storage); i++) { + expected[i] = (uint8_t)(seed + i); + } + + /* Fill to exactly full via the claim path. */ + uint32_t total = 0; + while (total < sizeof(storage)) { + uint32_t n = ring_buf_put_claim(&rb, &dst, sizeof(storage) - total); + TEST_ASSERT_NOT_EQUAL(0, n); + memcpy(dst, &expected[total], n); + total += n; + TEST_ASSERT_EQUAL(0, ring_buf_put_finish(&rb, n)); + } + + /* Full: no space, both put paths refuse. */ + TEST_ASSERT_EQUAL(sizeof(storage), ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(0, ring_buf_space_get(&rb)); + TEST_ASSERT_FALSE(ring_buf_is_empty(&rb)); + TEST_ASSERT_EQUAL(0, ring_buf_put_claim(&rb, &dst, 1)); + + /* Drain fully, verifying the bytes survived the wrap. */ + uint32_t got = 0; + while (got < sizeof(storage)) { + uint8_t *src; + uint32_t n = ring_buf_get_claim(&rb, &src, sizeof(storage) - got); + TEST_ASSERT_NOT_EQUAL(0, n); + TEST_ASSERT_EQUAL_UINT8_ARRAY(&expected[got], src, n); + got += n; + TEST_ASSERT_EQUAL(0, ring_buf_get_finish(&rb, n)); + } + + /* Empty: discrimination from the full state above. */ + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); + TEST_ASSERT_EQUAL(0, ring_buf_size_get(&rb)); + TEST_ASSERT_EQUAL(sizeof(storage), ring_buf_space_get(&rb)); + } +} + +/* The full/empty discrimination must also hold while head/tail/base + * cross the numeric wrap of ring_buf_idx_t itself, whatever its width. + * Traffic volume can only reach the default uint16_t wrap, so this test + * seeds the indices just below the wrap via ring_buf_internal_reset + * (whose doc blesses non-zero values for validation testing -- the same + * technique upstream's ringbuffer test suite uses), making the coverage + * independent of CONFIG_RING_BUFFER_LARGE. */ +static void test_ring_buf_index_wrap_seeded(void) +{ + uint8_t storage[64]; + struct ring_buf rb; + uint8_t chunk[64]; + uint8_t out[64]; + + ring_buf_init(&rb, sizeof(storage), storage); + /* Two full buffer-loads below the wrap: the indices land exactly on + * 0 at the end of cycle 2, so cycles 3-4 exercise post-wrap state. */ + ring_buf_internal_reset(&rb, (ring_buf_idx_t)(0U - 2U * sizeof(storage))); + + for (int cycle = 0; cycle < 4; cycle++) { + for (uint32_t i = 0; i < sizeof(chunk); i++) { + chunk[i] = (uint8_t)(cycle + i); + } + TEST_ASSERT_EQUAL(sizeof(storage), ring_buf_put(&rb, chunk, sizeof(chunk))); + + /* Full at (and across) the index wrap. */ + TEST_ASSERT_EQUAL(0, ring_buf_space_get(&rb)); + TEST_ASSERT_FALSE(ring_buf_is_empty(&rb)); + TEST_ASSERT_EQUAL(0, ring_buf_put(&rb, chunk, 1)); + + memset(out, 0, sizeof(out)); + TEST_ASSERT_EQUAL(sizeof(storage), ring_buf_get(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(chunk, out, sizeof(chunk)); + + /* Empty at (and across) the index wrap. */ + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); + TEST_ASSERT_EQUAL(sizeof(storage), ring_buf_space_get(&rb)); + } +} + +/* peek must walk both physical segments of wrapped data (two internal + * claim iterations) and then rewind the claim head back across the + * physical end via its internal get_finish(0); the get side must + * likewise allow several claims before one finish covering all of + * them. Neither path is reachable through the contiguous tests above. */ +static void test_ring_buf_peek_across_wrap_multi_claim_get(void) +{ + uint8_t storage[8]; + struct ring_buf rb; + const uint8_t fill[] = {1, 2, 3, 4, 5, 6}; + const uint8_t wrapped[] = {0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5}; + uint8_t out[8] = {0}; + uint8_t *src; + + ring_buf_init(&rb, sizeof(storage), storage); + + /* Park the offset at 6 so the next 6 bytes span the physical end. */ + ring_buf_put(&rb, fill, sizeof(fill)); + TEST_ASSERT_EQUAL(6, ring_buf_get(&rb, NULL, sizeof(fill))); + TEST_ASSERT_EQUAL(6, ring_buf_put(&rb, wrapped, sizeof(wrapped))); + + /* peek the wrapped data: both segments, in order, not consumed. */ + TEST_ASSERT_EQUAL(6, ring_buf_peek(&rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(wrapped, out, sizeof(wrapped)); + TEST_ASSERT_EQUAL(6, ring_buf_size_get(&rb)); + + /* Drain with two claims (split by the physical end) and a single + * finish covering both. */ + TEST_ASSERT_EQUAL(2, ring_buf_get_claim(&rb, &src, 8)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(&wrapped[0], src, 2); + TEST_ASSERT_EQUAL(4, ring_buf_get_claim(&rb, &src, 8)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(&wrapped[2], src, 4); + TEST_ASSERT_EQUAL(0, ring_buf_get_finish(&rb, 6)); + TEST_ASSERT_TRUE(ring_buf_is_empty(&rb)); + TEST_ASSERT_EQUAL(8, ring_buf_space_get(&rb)); +} + +/* ---------------------------------------------------------------- + * RING_BUF_DECLARE static instance + * ---------------------------------------------------------------- */ + +RING_BUF_DECLARE(declared_rb, 12); + +static void test_ring_buf_declare_usable(void) +{ + const uint8_t in[] = {0x55, 0x66, 0x77}; + uint8_t out[3] = {0}; + + TEST_ASSERT_EQUAL(12, ring_buf_capacity_get(&declared_rb)); + TEST_ASSERT_TRUE(ring_buf_is_empty(&declared_rb)); + + TEST_ASSERT_EQUAL(3, ring_buf_put(&declared_rb, in, sizeof(in))); + TEST_ASSERT_EQUAL(3, ring_buf_get(&declared_rb, out, sizeof(out))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(in, out, sizeof(in)); +} + +void test_ring_buf_group(void) +{ + RUN_TEST(test_ring_buf_init_accounting); + RUN_TEST(test_ring_buf_put_get_roundtrip); + RUN_TEST(test_ring_buf_put_saturates_at_capacity); + RUN_TEST(test_ring_buf_get_more_than_available); + RUN_TEST(test_ring_buf_get_null_discards); + RUN_TEST(test_ring_buf_peek_does_not_consume); + RUN_TEST(test_ring_buf_reset_empties); + RUN_TEST(test_ring_buf_put_claim_finish); + RUN_TEST(test_ring_buf_put_finish_surplus_returns_space); + RUN_TEST(test_ring_buf_put_finish_over_claim_einval); + RUN_TEST(test_ring_buf_get_claim_finish); + RUN_TEST(test_ring_buf_put_claim_wraps); + RUN_TEST(test_ring_buf_put_get_across_wrap); + RUN_TEST(test_ring_buf_many_wrap_cycles); + RUN_TEST(test_ring_buf_full_empty_across_wrap); + RUN_TEST(test_ring_buf_index_wrap_seeded); + RUN_TEST(test_ring_buf_peek_across_wrap_multi_claim_get); + RUN_TEST(test_ring_buf_declare_usable); +}