diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce1b49a..52013a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,10 +59,10 @@ jobs: count="$(grep -c 'throw std::bad_alloc' src/strata/Allocator.h)" test "$count" -eq 1 - - name: Audit FreeRTOS mutex allocation boundary + - name: Audit FreeRTOS semaphore allocation boundary run: | - if grep -RInE 'xSemaphoreCreateMutex[[:space:]]*\(|xSemaphoreCreateRecursiveMutex[[:space:]]*\(' src; then - echo "Strata mutex wrappers must use static FreeRTOS creation APIs" + if grep -RInE 'xSemaphoreCreateMutex[[:space:]]*\(|xSemaphoreCreateRecursiveMutex[[:space:]]*\(|xSemaphoreCreateBinary[[:space:]]*\(' src; then + echo "Strata semaphore wrappers must use static FreeRTOS creation APIs" exit 1 fi @@ -123,6 +123,9 @@ jobs: g++ $common_flags -Itests/host/freertos_fakes tests/host/freertos_mutex_contract_test.cpp $common_sources -o build/freertos-mutex-contract-test ./build/freertos-mutex-contract-test + g++ $common_flags -Itests/host/freertos_fakes tests/host/freertos_binary_semaphore_contract_test.cpp $common_sources -o build/freertos-binary-semaphore-contract-test + ./build/freertos-binary-semaphore-contract-test + g++ $common_flags -Itests/host/arduinojson_fakes tests/host/arduinojson_allocator_contract_test.cpp $common_sources -o build/arduinojson-allocator-contract-test ./build/arduinojson-allocator-contract-test @@ -160,6 +163,9 @@ jobs: g++ $flags -Itests/host/freertos_fakes tests/host/esp32_freertos_mutex_backend_test.cpp $sources -o build/esp32-freertos-mutex-backend-test ./build/esp32-freertos-mutex-backend-test + g++ $flags -Itests/host/freertos_fakes tests/host/esp32_freertos_binary_semaphore_backend_test.cpp $sources -o build/esp32-freertos-binary-semaphore-backend-test + ./build/esp32-freertos-binary-semaphore-backend-test + g++ $flags -Itests/host/arduinojson_fakes tests/host/esp32_arduinojson_allocator_backend_test.cpp $sources -o build/esp32-arduinojson-allocator-backend-test ./build/esp32-arduinojson-allocator-backend-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 5920cb4..8c1c292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to Strata are documented in this file. +## 0.1.2 + +- Add move-only `Strata::FreeRTOS::BinarySemaphore` ownership using static FreeRTOS creation and internal Strata-backed control storage. +- Add task and ISR-safe take/give operations with explicit failure reporting and initially-empty semaphore semantics. +- Add host and ESP32 backend contracts covering lifecycle, ISR behavior, creation failure, allocation failure, and internal-only control storage. +- Extend the FreeRTOS source audit to prevent dynamic binary-semaphore creation and add ESP32/ESP32-S3/ESP32-C3/ESP32-P4 example coverage. +- Document the binary semaphore API, configuration requirements, example, and `v0.1.2` release contract. + ## 0.1.1 - Add reusable `Strata::MemoryPolicy` with consistent general-allocation and task-stack placement fields for consuming ZekStack library configuration. diff --git a/README.md b/README.md index 6f7fef8..d55bba2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ It gives applications and libraries one vocabulary for allocation intent while p - **PMR integration** — use the same placement policies through standard polymorphic allocators and nested PMR containers. - **Runtime diagnostics** — inspect actual memory regions, heap statistics, and current/peak region usage. - **Optional advanced diagnostics** — opt into allocation/failure/fallback counters without a global allocation registry. -- **Optional FreeRTOS memory primitives** — explicitly place task stacks and queue item storage, and own static mutex control blocks, while keeping FreeRTOS out of the core headers. +- **Optional FreeRTOS memory primitives** — explicitly place task stacks and queue item storage, and own static mutex/binary-semaphore control blocks, while keeping FreeRTOS out of the core headers. - **Optional ArduinoJson allocation** — route ArduinoJson 7 document memory through the same Strata placement policies. - **Standalone core** — Strata does not depend on other ZekStack libraries. @@ -118,11 +118,12 @@ config.memory.taskStack = Strata::Placement::Internal; - `Strata::Allocator` follows standard allocator expectations and throws `std::bad_alloc` when exceptions are enabled. - The optional PMR adapter requires standard-library `` support and exceptions to preserve `std::pmr::memory_resource` failure semantics. - External RAM is not automatically safe for DMA, ISR use, or cache-disabled flash windows. -- FreeRTOS task, queue, and mutex integrations are opt-in and require static allocation support. The task wrapper also requires `INCLUDE_vTaskDelete == 1` and `INCLUDE_uxTaskGetStackHighWaterMark == 1`; the mutex wrapper requires mutex and recursive-mutex support. +- FreeRTOS task, queue, mutex, and binary-semaphore integrations are opt-in and require static allocation support. The task wrapper also requires `INCLUDE_vTaskDelete == 1` and `INCLUDE_uxTaskGetStackHighWaterMark == 1`; the mutex wrapper requires mutex and recursive-mutex support. - A `Strata::FreeRTOS::Task` owner must be destroyed/reset from a different task context than the task it owns; managed tasks must not self-delete. - Tasks that can execute while flash/cache is disabled should keep their stacks in internal memory. - ISR-accessible Strata queues require internal item storage; external queue storage is task-only. -- Strata mutex control blocks are always internal and use FreeRTOS static creation APIs. +- Strata mutex and binary-semaphore control blocks are always internal and use FreeRTOS static creation APIs. +- `BinarySemaphore::create()` produces an initially-empty semaphore; task and ISR give/take operations report success explicitly. - ArduinoJson integration is opt-in, targets ArduinoJson 7, and requires the Strata allocator object to outlive the `JsonDocument` using it. - Advanced allocation counters are disabled by default; enable them build-wide with `STRATA_ENABLE_ADVANCED_DIAGNOSTICS=1`. @@ -141,6 +142,7 @@ config.memory.taskStack = Strata::Placement::Internal; | `FreeRTOSTask` | Optional placed FreeRTOS task stacks and diagnostics. | | `FreeRTOSQueue` | Optional typed FreeRTOS queues with placed item storage. | | `FreeRTOSMutex` | Optional static mutex and recursive-mutex ownership. | +| `FreeRTOSBinarySemaphore` | Optional static binary semaphore signaling with task and ISR APIs. | | `ArduinoJson` | Optional ArduinoJson 7 document allocation through Strata placement. | Start with: @@ -171,6 +173,7 @@ examples/Basic | [`docs/freertos-tasks.md`](docs/freertos-tasks.md) | Optional task-stack placement and static task creation. | | [`docs/freertos-queues.md`](docs/freertos-queues.md) | Optional typed queue storage placement and ISR safety. | | [`docs/freertos-mutexes.md`](docs/freertos-mutexes.md) | Optional internal static mutex and recursive-mutex ownership. | +| [`docs/freertos-binary-semaphores.md`](docs/freertos-binary-semaphores.md) | Optional internal static binary semaphore ownership and ISR signaling. | | [`docs/arduinojson.md`](docs/arduinojson.md) | Optional ArduinoJson 7 custom allocator integration. | | [`docs/roadmap.md`](docs/roadmap.md) | Completed `v0.1.0` roadmap and post-release planning boundary. | | [`docs/ecosystem-adoption.md`](docs/ecosystem-adoption.md) | Planned adoption across ZekStack and Core. | @@ -239,6 +242,7 @@ Optional FreeRTOS integrations: #include #include #include +#include Strata::FreeRTOS::Task task = Strata::FreeRTOS::Task::create(worker, nullptr, { .name = "worker", @@ -256,6 +260,8 @@ auto queue = Strata::FreeRTOS::Queue::create({ auto mutex = Strata::FreeRTOS::Mutex::create(); auto recursiveMutex = Strata::FreeRTOS::RecursiveMutex::create(); +auto ready = Strata::FreeRTOS::BinarySemaphore::create(); +ready.give(); ``` ## Compatibility @@ -269,11 +275,11 @@ auto recursiveMutex = Strata::FreeRTOS::RecursiveMutex::create(); | External memory | ESP32 PSRAM through ESP-IDF heap capabilities | | Core dependencies | none | | Optional PMR integration | Standard-library `` with exceptions enabled | -| Optional FreeRTOS integration | FreeRTOS with static allocation; task deletion/high-water-mark APIs for tasks; mutex and recursive-mutex support for mutex wrappers | +| Optional FreeRTOS integration | FreeRTOS with static allocation; task deletion/high-water-mark APIs for tasks; mutex and recursive-mutex support for mutex wrappers; binary semaphore task/ISR support | | Optional ArduinoJson integration | ArduinoJson 7; CI compatibility target 7.4.3 | | Advanced diagnostics | Optional compile-time counters; disabled by default | | Exceptions | Not required by core APIs; STL/PMR standard allocator surfaces follow standard semantics | -| Status | `v0.1.1` compatibility release for ZekStack memory-policy adoption | +| Status | `v0.1.2` binary semaphore compatibility release | ## License @@ -281,4 +287,4 @@ MIT — see [`LICENSE.md`](LICENSE.md). ## ZekStack -Part of the ZekStack library stack. `v0.1.1` adds the common consuming-library memory-policy contract and static FreeRTOS mutex ownership so Worker and subsequent libraries can migrate without inventing library-specific placement vocabulary. +Part of the ZekStack library stack. `v0.1.2` adds static FreeRTOS binary semaphore ownership while preserving the shared memory-policy and placement contracts established for Worker and subsequent ZekStack libraries. diff --git a/docs/api.md b/docs/api.md index 9b68411..046bea7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -12,6 +12,7 @@ Optional integrations are intentionally separate: #include #include #include +#include #include #include ``` @@ -126,6 +127,14 @@ See `freertos-queues.md` for lifecycle and ISR safety details. Both wrappers expose `lock()`, `tryLock()`, `unlock()`, `reset()`, `handle()`, `controlPlacement()`, and `controlRegion()`. See `freertos-mutexes.md` for compile-time requirements and ownership semantics. +## FreeRTOS binary semaphores + +`Strata::FreeRTOS::BinarySemaphore` is a move-only owner created through `xSemaphoreCreateBinaryStatic()`. Its `StaticSemaphore_t` control storage is always allocated through Strata with `Placement::Internal` and a newly created semaphore starts empty. + +The task-context API exposes `take()`, `tryTake()`, and `give()`. `giveFromISR()` and `takeFromISR()` forward the optional `higherPriorityTaskWoken` pointer to FreeRTOS. Give/take operations return `bool` so an already-given semaphore or unavailable take is not silently hidden. + +See `freertos-binary-semaphores.md` for signaling semantics, ISR usage, and ownership details. + ## ArduinoJson `Strata::ArduinoJson::Allocator` implements the ArduinoJson 7 custom allocator interface and forwards allocation, reallocation, and deallocation through a single Strata `Placement` policy. @@ -141,7 +150,7 @@ See `arduinojson.md` for placement, failure, lifetime, and PSRAM details. ## Stable API boundary -The core API remains the stable base for ecosystem migrations. `v0.1.1` adds the shared `MemoryPolicy` vocabulary and FreeRTOS mutex ownership without weakening the `v0.1.0` placement/failure contracts. +The core API remains the stable base for ecosystem migrations. `v0.1.2` adds FreeRTOS binary semaphore ownership without weakening the placement, failure, or memory-policy contracts established by earlier releases. The following semantic contracts are intentionally protected by tests and CI: @@ -153,6 +162,6 @@ The following semantic contracts are intentionally protected by tests and CI: - optional integrations are not pulled into `Strata.h`; - platform-specific allocator flags do not become part of the core public vocabulary; - advanced diagnostics remain optional and do not require an allocation registry; -- FreeRTOS mutex wrappers use static creation and internal Strata-backed control storage. +- FreeRTOS mutex and binary-semaphore wrappers use static creation and internal Strata-backed control storage. See `architecture.md` for the layering contract and `migration.md` for ecosystem adoption recipes. diff --git a/docs/configuration.md b/docs/configuration.md index 135d382..857d0ed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -84,12 +84,14 @@ Keep the `MemoryResource` object alive for at least as long as every PMR contain ## FreeRTOS integration -FreeRTOS support is opt-in through ``, ``, and `` and requires static allocation support. +FreeRTOS support is opt-in through ``, ``, ``, and `` and requires static allocation support. The task integration additionally requires `INCLUDE_vTaskDelete == 1` and `INCLUDE_uxTaskGetStackHighWaterMark == 1`. `Task.h` checks these settings at compile time so a FreeRTOS configuration that cannot satisfy the public task API fails with an actionable error instead of failing later on missing symbols. The mutex integration additionally requires `configUSE_MUTEXES == 1` and `configUSE_RECURSIVE_MUTEXES == 1`. Mutex control storage is always internal and is allocated through Strata before using FreeRTOS static creation APIs. +The binary-semaphore integration only adds the static-allocation requirement. Its `StaticSemaphore_t` control storage is always internal, newly created semaphores start empty, and the wrapper provides both task-context and ISR take/give operations. There is intentionally no placement configuration for semaphore control storage. + `TaskConfig` configures task name, stack bytes, stack placement, priority, and affinity. Use internal stack placement for tasks that may execute while flash/cache is disabled. A `Strata::FreeRTOS::Task` must be reset or destroyed from a different task context than the task it owns. Self-deletion cannot return through the owner cleanup path to release the caller-owned static stack and control block safely. Tasks owned by this wrapper must also not independently call `vTaskDelete(nullptr)`. diff --git a/docs/examples.md b/docs/examples.md index 8941a9a..9ec6380 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -15,6 +15,7 @@ Strata ships focused Arduino sketches under `examples/`. | `FreeRTOSTask` | Optional FreeRTOS stack placement and task diagnostics. | | `FreeRTOSQueue` | Optional typed FreeRTOS queue storage placement and task-only queue use. | | `FreeRTOSMutex` | Optional statically created mutex and recursive-mutex ownership with internal control storage. | +| `FreeRTOSBinarySemaphore` | Optional statically created binary semaphore signaling with internal control storage. | | `ArduinoJson` | Optional ArduinoJson 7 document allocation through Strata placement. | Start with `examples/Basic`. Move to the specialized example that matches the API you need rather than combining all Strata features into one sketch. diff --git a/docs/freertos-binary-semaphores.md b/docs/freertos-binary-semaphores.md new file mode 100644 index 0000000..d858cf2 --- /dev/null +++ b/docs/freertos-binary-semaphores.md @@ -0,0 +1,103 @@ +# FreeRTOS binary semaphores + +`Strata::FreeRTOS::BinarySemaphore` is an optional move-only owner for a FreeRTOS binary semaphore created with static FreeRTOS storage. + +Include it explicitly: + +```cpp +#include +``` + +It is intentionally not included by `Strata.h`, so the core Strata API remains FreeRTOS-independent. + +## Creation and initial state + +```cpp +auto ready = Strata::FreeRTOS::BinarySemaphore::create(); +if (!ready) { + // Strata could not allocate the control block or FreeRTOS creation failed. +} +``` + +A newly created binary semaphore is **empty**. `tryTake()` therefore fails until the semaphore is given. + +```cpp +assert(!ready.tryTake()); +assert(ready.give()); +assert(ready.tryTake()); +``` + +Giving an already available binary semaphore returns `false` instead of hiding the FreeRTOS failure. + +## Task-context API + +```cpp +bool take(TickType_t ticksToWait = portMAX_DELAY) noexcept; +bool tryTake() noexcept; +bool give() noexcept; +``` + +`take()` forwards the requested FreeRTOS timeout. `tryTake()` is equivalent to `take(0)`. + +The API uses `take`/`give` terminology rather than mutex-style `lock`/`unlock`: a binary semaphore is a signaling primitive and does not provide mutex ownership or priority-inheritance semantics. + +## ISR API + +```cpp +BaseType_t taskWoken = pdFALSE; +if (ready.giveFromISR(&taskWoken)) { + // Signal published from the ISR. +} +``` + +The wrapper exposes: + +```cpp +bool giveFromISR(BaseType_t *higherPriorityTaskWoken = nullptr) noexcept; +bool takeFromISR(BaseType_t *higherPriorityTaskWoken = nullptr) noexcept; +``` + +The optional `higherPriorityTaskWoken` pointer is forwarded directly to FreeRTOS. The caller remains responsible for performing the platform-appropriate ISR yield when FreeRTOS indicates that a higher-priority task was woken. + +## Memory placement + +The FreeRTOS `StaticSemaphore_t` control block is always allocated through Strata with: + +```cpp +Strata::Placement::Internal +``` + +There is deliberately no placement configuration. Synchronization primitives may be used from ISR or cache-sensitive contexts, so Strata keeps the control storage internal rather than allowing PSRAM placement. + +The requested and observed locations can be inspected with: + +```cpp +ready.controlPlacement(); // Placement::Internal +ready.controlRegion(); +``` + +## Ownership + +`BinarySemaphore` is move-only. Its destructor and `reset()` delete the FreeRTOS semaphore before releasing the Strata-owned control block. + +```cpp +auto first = Strata::FreeRTOS::BinarySemaphore::create(); +auto second = std::move(first); + +assert(!first); +assert(second); + +second.reset(); +``` + +Calling `reset()` repeatedly is safe. + +## Requirements + +The integration requires FreeRTOS static allocation support: + +```text +configSUPPORT_STATIC_ALLOCATION == 1 +``` + +The wrapper uses `xSemaphoreCreateBinaryStatic()` and never falls back to dynamic FreeRTOS semaphore allocation. diff --git a/docs/releasing.md b/docs/releasing.md index 48498d1..8151056 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -15,15 +15,16 @@ Before tagging a release: `scripts/validate_release_version.py` enforces the metadata/version/changelog requirements in CI. Repository visibility is an organization/repository setting and must be checked separately. -## Create v0.1.1 +## Create a release -Tag the validated `main` commit and push the tag: +Derive the intended release version from the validated PlatformIO metadata, tag the validated `main` commit, and push the tag: ```bash git checkout main git pull --ff-only -git tag v0.1.1 -git push origin v0.1.1 +VERSION="$(python3 -c 'import json; print(json.load(open("library.json"))["version"])')" +git tag "v${VERSION}" +git push origin "v${VERSION}" ``` The `CI` workflow runs again for the tag. The release job waits for metadata validation, source audits, host contracts, ESP32 backend contracts, and the full ESP32/ESP32-S3/ESP32-C3/ESP32-P4 example matrix. diff --git a/examples/FreeRTOSBinarySemaphore/FreeRTOSBinarySemaphore.ino b/examples/FreeRTOSBinarySemaphore/FreeRTOSBinarySemaphore.ino new file mode 100644 index 0000000..cebf1eb --- /dev/null +++ b/examples/FreeRTOSBinarySemaphore/FreeRTOSBinarySemaphore.ino @@ -0,0 +1,33 @@ +#include +#include + +Strata::FreeRTOS::BinarySemaphore ready; + +void setup() { + Serial.begin(115200); + + ready = Strata::FreeRTOS::BinarySemaphore::create(); + if (!ready) { + Serial.println("binary semaphore creation failed"); + return; + } + + Serial.println("binary semaphore starts empty"); +} + +void loop() { + if (!ready) { + delay(1000); + return; + } + + if (ready.give()) { + Serial.println("signaled"); + } + + if (ready.take(pdMS_TO_TICKS(100))) { + Serial.println("signal consumed"); + } + + delay(1000); +} diff --git a/library.json b/library.json index d34e05b..d0f9234 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Strata", - "version": "0.1.1", + "version": "0.1.2", "description": "Portable memory placement and allocation utilities for embedded and standard C++ applications.", "keywords": [ "memory", diff --git a/library.properties b/library.properties index 25436ac..37b3fa2 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=Strata -version=0.1.1 +version=0.1.2 author=zekageri maintainer=zekageri sentence=Portable memory placement and allocation utilities for embedded and standard C++ applications. -paragraph=Provides portable memory policy and internal/external placement, strict capability requirements, typed ownership, STL/PMR adapters, static FreeRTOS task/queue/mutex integrations, ArduinoJson integration, and optional allocation diagnostics. +paragraph=Provides portable memory policy and internal/external placement, strict capability requirements, typed ownership, STL/PMR adapters, static FreeRTOS task/queue/mutex/binary-semaphore integrations, ArduinoJson integration, and optional allocation diagnostics. category=Other url=https://github.com/ZekStack/strata repository=https://github.com/ZekStack/strata.git diff --git a/src/strata/freertos/BinarySemaphore.h b/src/strata/freertos/BinarySemaphore.h new file mode 100644 index 0000000..b09d663 --- /dev/null +++ b/src/strata/freertos/BinarySemaphore.h @@ -0,0 +1,111 @@ +#pragma once + +#include "../Allocation.h" +#include "../Diagnostics.h" + +#include + +extern "C" { +#include +#include +} + +#if configSUPPORT_STATIC_ALLOCATION != 1 +#error "Strata FreeRTOS binary semaphore integration requires configSUPPORT_STATIC_ALLOCATION == 1" +#endif + +namespace Strata::FreeRTOS { + +class BinarySemaphore { +public: + using Handle = SemaphoreHandle_t; + + BinarySemaphore() noexcept = default; + + ~BinarySemaphore() noexcept { + reset(); + } + + BinarySemaphore(const BinarySemaphore &) = delete; + BinarySemaphore &operator=(const BinarySemaphore &) = delete; + + BinarySemaphore(BinarySemaphore &&other) noexcept { + moveFrom(other); + } + + BinarySemaphore &operator=(BinarySemaphore &&other) noexcept { + if (this != &other) { + reset(); + moveFrom(other); + } + return *this; + } + + [[nodiscard]] static BinarySemaphore create() noexcept { + BinarySemaphore semaphore; + if (!semaphore.start()) { + semaphore.reset(); + } + return semaphore; + } + + void reset() noexcept { + if (handle_ != nullptr) { + vSemaphoreDelete(handle_); + handle_ = nullptr; + } + Strata::free(controlBlock_); + controlBlock_ = nullptr; + } + + [[nodiscard]] bool take(TickType_t ticksToWait = portMAX_DELAY) noexcept { + return handle_ != nullptr && xSemaphoreTake(handle_, ticksToWait) == pdTRUE; + } + + [[nodiscard]] bool tryTake() noexcept { + return take(0); + } + + [[nodiscard]] bool give() noexcept { + return handle_ != nullptr && xSemaphoreGive(handle_) == pdTRUE; + } + + [[nodiscard]] bool giveFromISR(BaseType_t *higherPriorityTaskWoken = nullptr) noexcept { + return handle_ != nullptr && xSemaphoreGiveFromISR(handle_, higherPriorityTaskWoken) == pdTRUE; + } + + [[nodiscard]] bool takeFromISR(BaseType_t *higherPriorityTaskWoken = nullptr) noexcept { + return handle_ != nullptr && xSemaphoreTakeFromISR(handle_, higherPriorityTaskWoken) == pdTRUE; + } + + [[nodiscard]] Handle handle() const noexcept { return handle_; } + [[nodiscard]] bool valid() const noexcept { return handle_ != nullptr; } + [[nodiscard]] explicit operator bool() const noexcept { return valid(); } + [[nodiscard]] Placement controlPlacement() const noexcept { return Placement::Internal; } + [[nodiscard]] Region controlRegion() const noexcept { return Strata::regionOf(controlBlock_); } + +private: + [[nodiscard]] bool start() noexcept { + controlBlock_ = static_cast(Strata::allocate(AllocationRequest{ + .sizeBytes = sizeof(StaticSemaphore_t), + .placement = Placement::Internal, + .alignment = alignof(StaticSemaphore_t), + })); + if (controlBlock_ == nullptr) { + return false; + } + + handle_ = xSemaphoreCreateBinaryStatic(controlBlock_); + return handle_ != nullptr; + } + + void moveFrom(BinarySemaphore &other) noexcept { + handle_ = std::exchange(other.handle_, nullptr); + controlBlock_ = std::exchange(other.controlBlock_, nullptr); + } + + Handle handle_{nullptr}; + StaticSemaphore_t *controlBlock_{nullptr}; +}; + +} // namespace Strata::FreeRTOS diff --git a/tests/host/esp32_freertos_binary_semaphore_backend_test.cpp b/tests/host/esp32_freertos_binary_semaphore_backend_test.cpp new file mode 100644 index 0000000..13d1564 --- /dev/null +++ b/tests/host/esp32_freertos_binary_semaphore_backend_test.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include + +int main() { + using Strata::FreeRTOS::BinarySemaphore; + using Strata::Placement; + using Strata::Region; + + fake_heap_caps_reset(); + fake_semaphore_reset(); + auto semaphore = BinarySemaphore::create(); + assert(semaphore); + assert(semaphore.controlPlacement() == Placement::Internal); + assert(semaphore.controlRegion() == Region::Internal); + assert(fake_heap_caps_internal_attempts == 1); + assert(fake_heap_caps_external_attempts == 0); + assert(fake_semaphore_binary_create_calls == 1); + assert(!semaphore.tryTake()); + assert(semaphore.give()); + assert(semaphore.tryTake()); + semaphore.reset(); + assert(fake_heap_caps_allocations.empty()); + assert(fake_semaphore_delete_calls == 1); + + fake_heap_caps_reset(); + fake_semaphore_reset(); + fake_semaphore_fail_create = true; + auto failedCreate = BinarySemaphore::create(); + assert(!failedCreate); + assert(fake_semaphore_binary_create_calls == 1); + assert(fake_heap_caps_allocations.empty()); + assert(fake_semaphore_delete_calls == 0); + + fake_heap_caps_reset(); + fake_semaphore_reset(); + fake_heap_caps_fail_internal = true; + auto failedAllocation = BinarySemaphore::create(); + assert(!failedAllocation); + assert(fake_heap_caps_internal_attempts == 1); + assert(fake_heap_caps_external_attempts == 0); + assert(fake_semaphore_binary_create_calls == 0); + assert(fake_heap_caps_allocations.empty()); + assert(fake_semaphore_delete_calls == 0); +} diff --git a/tests/host/fakes/esp_heap_caps.h b/tests/host/fakes/esp_heap_caps.h index e180460..f6fce57 100644 --- a/tests/host/fakes/esp_heap_caps.h +++ b/tests/host/fakes/esp_heap_caps.h @@ -13,6 +13,7 @@ inline constexpr std::uint32_t MALLOC_CAP_DMA = 1U << 3; inline constexpr std::uint32_t MALLOC_CAP_EXEC = 1U << 4; inline bool fake_heap_caps_fail_external = false; +inline bool fake_heap_caps_fail_internal = false; inline std::uint32_t fake_heap_caps_last_caps = 0; inline std::size_t fake_heap_caps_external_attempts = 0; inline std::size_t fake_heap_caps_internal_attempts = 0; @@ -29,6 +30,7 @@ inline std::unordered_map fake_heap_caps_allocation inline void fake_heap_caps_reset() { fake_heap_caps_fail_external = false; + fake_heap_caps_fail_internal = false; fake_heap_caps_last_caps = 0; fake_heap_caps_external_attempts = 0; fake_heap_caps_internal_attempts = 0; @@ -69,6 +71,9 @@ inline bool fake_heap_caps_should_fail(std::uint32_t caps) { } if ((caps & MALLOC_CAP_INTERNAL) != 0) { ++fake_heap_caps_internal_attempts; + if (fake_heap_caps_fail_internal) { + return true; + } } return fake_heap_caps_incompatible(caps); } diff --git a/tests/host/freertos_binary_semaphore_contract_test.cpp b/tests/host/freertos_binary_semaphore_contract_test.cpp new file mode 100644 index 0000000..8ce3c2b --- /dev/null +++ b/tests/host/freertos_binary_semaphore_contract_test.cpp @@ -0,0 +1,73 @@ +#include + +#include +#include +#include + +int main() { + using Strata::FreeRTOS::BinarySemaphore; + + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_move_assignable_v); + + fake_semaphore_reset(); + BinarySemaphore empty; + assert(!empty); + assert(empty.handle() == nullptr); + assert(!empty.tryTake()); + assert(!empty.give()); + assert(!empty.giveFromISR()); + assert(!empty.takeFromISR()); + empty.reset(); + + auto semaphore = BinarySemaphore::create(); + assert(semaphore); + assert(semaphore.handle() != nullptr); + assert(semaphore.controlPlacement() == Strata::Placement::Internal); + assert(fake_semaphore_binary_create_calls == 1); + assert(!semaphore.tryTake()); + assert(semaphore.give()); + assert(!semaphore.give()); + assert(semaphore.take(12)); + assert(fake_semaphore_last_ticks_to_wait == 12); + assert(!semaphore.tryTake()); + + BaseType_t taskWoken = pdFALSE; + assert(semaphore.giveFromISR(&taskWoken)); + assert(taskWoken == pdTRUE); + assert(fake_semaphore_give_from_isr_calls == 1); + taskWoken = pdFALSE; + assert(!semaphore.giveFromISR(&taskWoken)); + assert(taskWoken == pdFALSE); + assert(fake_semaphore_give_from_isr_calls == 2); + assert(semaphore.takeFromISR(&taskWoken)); + assert(taskWoken == pdTRUE); + assert(fake_semaphore_take_from_isr_calls == 1); + assert(!semaphore.takeFromISR()); + assert(fake_semaphore_take_from_isr_calls == 2); + + BinarySemaphore moved = std::move(semaphore); + assert(!semaphore); + assert(moved); + + auto replacement = BinarySemaphore::create(); + assert(replacement); + replacement = std::move(moved); + assert(!moved); + assert(replacement); + assert(fake_semaphore_delete_calls == 1); + replacement.reset(); + assert(!replacement); + assert(fake_semaphore_delete_calls == 2); + replacement.reset(); + assert(fake_semaphore_delete_calls == 2); + + fake_semaphore_reset(); + fake_semaphore_fail_create = true; + auto failed = BinarySemaphore::create(); + assert(!failed); + assert(fake_semaphore_binary_create_calls == 1); + assert(fake_semaphore_delete_calls == 0); +} diff --git a/tests/host/freertos_fakes/freertos/semphr.h b/tests/host/freertos_fakes/freertos/semphr.h index 5a98eca..9616bfd 100644 --- a/tests/host/freertos_fakes/freertos/semphr.h +++ b/tests/host/freertos_fakes/freertos/semphr.h @@ -4,8 +4,14 @@ #include +enum class FakeSemaphoreKind { + Mutex, + RecursiveMutex, + Binary, +}; + struct StaticSemaphore_t { - bool recursive{false}; + FakeSemaphoreKind kind{FakeSemaphoreKind::Mutex}; std::size_t depth{0}; bool deleted{false}; }; @@ -14,21 +20,29 @@ using SemaphoreHandle_t = StaticSemaphore_t *; inline std::size_t fake_semaphore_mutex_create_calls = 0; inline std::size_t fake_semaphore_recursive_create_calls = 0; +inline std::size_t fake_semaphore_binary_create_calls = 0; inline std::size_t fake_semaphore_delete_calls = 0; inline std::size_t fake_semaphore_take_calls = 0; inline std::size_t fake_semaphore_give_calls = 0; +inline std::size_t fake_semaphore_take_from_isr_calls = 0; +inline std::size_t fake_semaphore_give_from_isr_calls = 0; inline std::size_t fake_semaphore_recursive_take_calls = 0; inline std::size_t fake_semaphore_recursive_give_calls = 0; +inline TickType_t fake_semaphore_last_ticks_to_wait = 0; inline bool fake_semaphore_fail_create = false; inline void fake_semaphore_reset() { fake_semaphore_mutex_create_calls = 0; fake_semaphore_recursive_create_calls = 0; + fake_semaphore_binary_create_calls = 0; fake_semaphore_delete_calls = 0; fake_semaphore_take_calls = 0; fake_semaphore_give_calls = 0; + fake_semaphore_take_from_isr_calls = 0; + fake_semaphore_give_from_isr_calls = 0; fake_semaphore_recursive_take_calls = 0; fake_semaphore_recursive_give_calls = 0; + fake_semaphore_last_ticks_to_wait = 0; fake_semaphore_fail_create = false; } @@ -37,7 +51,7 @@ inline SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *controlB if (controlBlock == nullptr || fake_semaphore_fail_create) { return nullptr; } - controlBlock->recursive = false; + controlBlock->kind = FakeSemaphoreKind::Mutex; controlBlock->depth = 0; controlBlock->deleted = false; return controlBlock; @@ -48,7 +62,18 @@ inline SemaphoreHandle_t xSemaphoreCreateRecursiveMutexStatic(StaticSemaphore_t if (controlBlock == nullptr || fake_semaphore_fail_create) { return nullptr; } - controlBlock->recursive = true; + controlBlock->kind = FakeSemaphoreKind::RecursiveMutex; + controlBlock->depth = 0; + controlBlock->deleted = false; + return controlBlock; +} + +inline SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *controlBlock) { + ++fake_semaphore_binary_create_calls; + if (controlBlock == nullptr || fake_semaphore_fail_create) { + return nullptr; + } + controlBlock->kind = FakeSemaphoreKind::Binary; controlBlock->depth = 0; controlBlock->deleted = false; return controlBlock; @@ -61,27 +86,78 @@ inline void vSemaphoreDelete(SemaphoreHandle_t handle) { } } -inline BaseType_t xSemaphoreTake(SemaphoreHandle_t handle, TickType_t) { +inline BaseType_t xSemaphoreTake(SemaphoreHandle_t handle, TickType_t ticksToWait) { ++fake_semaphore_take_calls; - if (handle == nullptr || handle->deleted || handle->recursive || handle->depth != 0) { + fake_semaphore_last_ticks_to_wait = ticksToWait; + if (handle == nullptr || handle->deleted || handle->kind == FakeSemaphoreKind::RecursiveMutex) { return pdFALSE; } - handle->depth = 1; + if (handle->kind == FakeSemaphoreKind::Mutex) { + if (handle->depth != 0) { + return pdFALSE; + } + handle->depth = 1; + return pdTRUE; + } + if (handle->depth == 0) { + return pdFALSE; + } + handle->depth = 0; return pdTRUE; } inline BaseType_t xSemaphoreGive(SemaphoreHandle_t handle) { ++fake_semaphore_give_calls; - if (handle == nullptr || handle->deleted || handle->recursive || handle->depth == 0) { + if (handle == nullptr || handle->deleted || handle->kind == FakeSemaphoreKind::RecursiveMutex) { + return pdFALSE; + } + if (handle->kind == FakeSemaphoreKind::Mutex) { + if (handle->depth == 0) { + return pdFALSE; + } + handle->depth = 0; + return pdTRUE; + } + if (handle->depth != 0) { + return pdFALSE; + } + handle->depth = 1; + return pdTRUE; +} + +inline BaseType_t xSemaphoreTakeFromISR( + SemaphoreHandle_t handle, + BaseType_t *higherPriorityTaskWoken) { + ++fake_semaphore_take_from_isr_calls; + if (handle == nullptr || handle->deleted || handle->kind != FakeSemaphoreKind::Binary || + handle->depth == 0) { return pdFALSE; } handle->depth = 0; + if (higherPriorityTaskWoken != nullptr) { + *higherPriorityTaskWoken = pdTRUE; + } + return pdTRUE; +} + +inline BaseType_t xSemaphoreGiveFromISR( + SemaphoreHandle_t handle, + BaseType_t *higherPriorityTaskWoken) { + ++fake_semaphore_give_from_isr_calls; + if (handle == nullptr || handle->deleted || handle->kind != FakeSemaphoreKind::Binary || + handle->depth != 0) { + return pdFALSE; + } + handle->depth = 1; + if (higherPriorityTaskWoken != nullptr) { + *higherPriorityTaskWoken = pdTRUE; + } return pdTRUE; } inline BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t handle, TickType_t) { ++fake_semaphore_recursive_take_calls; - if (handle == nullptr || handle->deleted || !handle->recursive) { + if (handle == nullptr || handle->deleted || handle->kind != FakeSemaphoreKind::RecursiveMutex) { return pdFALSE; } ++handle->depth; @@ -90,7 +166,8 @@ inline BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t handle, TickType_t) inline BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t handle) { ++fake_semaphore_recursive_give_calls; - if (handle == nullptr || handle->deleted || !handle->recursive || handle->depth == 0) { + if (handle == nullptr || handle->deleted || handle->kind != FakeSemaphoreKind::RecursiveMutex || + handle->depth == 0) { return pdFALSE; } --handle->depth;