From 90e54b0896dbbee18057b2275d9945d69bc68ef3 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 22 Sep 2026 20:34:38 -0700 Subject: [PATCH] Migrate to FreeRTOS --- CMakeLists.txt | 3 +- src/avclan/bus.cc | 28 ++- src/avclan/bus.hpp | 5 - src/avclan/cdchanger.cc | 218 +++++++++--------- src/avclan/cdchanger.hpp | 50 +++- src/avclan/device.hpp | 37 ++- src/avclan/frame.cc | 22 +- src/avclan/frame.hpp | 4 + src/avclan/hal/cd_timer.h | 32 --- src/avclan/hal/phy.h | 18 +- src/avclan/hal/stdio.h | 4 +- src/avclan/peripheral.hpp | 94 ++++---- src/avclan/target/pico2/CMakeLists.txt | 40 +++- src/avclan/target/pico2/cd_timer.cc | 12 - .../target/pico2/include/FreeRTOSConfig.h | 161 +++++++++++++ src/avclan/target/pico2/media.cc | 2 +- src/avclan/target/pico2/phy.cc | 37 ++- src/avclan/target/pico2/stdio.cc | 31 ++- src/queue.hpp | 129 ++--------- src/sniffer.cc | 138 +++++++---- 20 files changed, 637 insertions(+), 428 deletions(-) delete mode 100644 src/avclan/hal/cd_timer.h delete mode 100644 src/avclan/target/pico2/cd_timer.cc create mode 100644 src/avclan/target/pico2/include/FreeRTOSConfig.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eea396d..ebcf8ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.30) +include(FetchContent) + if(AVCLAN_TARGET_SDK_FILE) include(${AVCLAN_TARGET_SDK_FILE}) endif() @@ -119,7 +121,6 @@ add_subdirectory(src/avclan/target/${AVCLAN_TARGET}) # subdirectory to provide a port-supplied freestanding stdlib via # CMAKE_REQUIRED_INCLUDES, if necessary. include(CheckCXXSourceCompiles) -include(FetchContent) find_program(GIT_EXECUTABLE git REQUIRED) if(TARGET libstdcpp) diff --git a/src/avclan/bus.cc b/src/avclan/bus.cc index 7232dd4..acd25fe 100644 --- a/src/avclan/bus.cc +++ b/src/avclan/bus.cc @@ -30,11 +30,11 @@ (zero) is not expected. */ -#include #include #include #include -#include + +#include "FreeRTOS.h" // IWYU pragma: export #include "avclan.h" #include "bus.hpp" @@ -112,21 +112,16 @@ void Bus::init(uint16_t address) { if (inited_) return; phy_init(address); - muted_ = false; // phy_init leaves the bus TX unmuted - deafened_ = false; // Default to listening + muted_ = false; // phy_init leaves the bus TX unmuted inited_ = true; }; -// NOLINTNEXTLINE(readability-convert-member-functions-to-static) -bool Bus::is_active() const { return !deafened_ && phy_frame_pending(); }; void Bus::mute(bool mute) { phy_mute(mute); muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting }; -void Bus::deafen(bool deaf) { - phy_deafen(deaf); - deafened_ = deaf; // Only update deafened_ *AFTER* the phy has stopped acking -} +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void Bus::deafen(bool deaf) { phy_deafen(deaf); } auto Bus::read(Frame::Print print) -> expected, Error::Read> { @@ -137,12 +132,14 @@ auto Bus::read(Frame::Print print) using enum Read; - std::unique_ptr in(new (std::nothrow) Frame); + std::unique_ptr in = Frame::acquire(); if (!in) { err.type = POOL_EMPTY; goto handle_err; } + phy_wait_frame(portMAX_DELAY); + { // bound handle lifetime auto handle = get(); @@ -198,6 +195,8 @@ auto Bus::read(Frame::Print print) if (false) { // NOLINT(readability-simplify-boolean-expr) handle_err:; + if (err.type == NO_FRAME) + return unexpected{NO_FRAME}; fputs("ERR(read): ", stdout); switch (err.type) { case POOL_EMPTY: puts("failed Frame alloc"); break; @@ -281,8 +280,7 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send { if (err.type != Send{0}) goto handle_err; - err.type = - handle.send_data(out.data, out.length, out.is_unicast, &err.val); + err.type = handle.send_data(out.data, out.length, out.is_unicast, &err.val); if (err.type != Send{0}) goto handle_err; @@ -360,8 +358,8 @@ Send Bus::sendbyte(uint8_t byte, bool ack) { } #ifdef MEASURE_BUS -// Debug bit-timing measurement on the one physical bus; instance-scoped for -// the same reason as is_active(). +// Debug bit-timing measurement on the one physical bus; instance-scoped like +// deafen(). // NOLINTNEXTLINE(readability-convert-member-functions-to-static) void Bus::measure() { phy_measure(); } #endif diff --git a/src/avclan/bus.hpp b/src/avclan/bus.hpp index 9e40592..70bac5e 100644 --- a/src/avclan/bus.hpp +++ b/src/avclan/bus.hpp @@ -73,10 +73,6 @@ public: // frames addressed to it without further instruction. One address per bus. void init(uint16_t address); - // True when there is a frame to read and we aren't deafened. Depending on the - // target that means the bus has gone dominant or a frame is already buffered. - bool is_active() const; - // Prevent the device from being active on the bus void mute(bool mute); bool is_muted() const { return muted_; }; @@ -101,7 +97,6 @@ private: // Assume mute after default ctor; only viable after init call bool muted_ = true; - bool deafened_ = false; bool inited_ = false; }; diff --git a/src/avclan/cdchanger.cc b/src/avclan/cdchanger.cc index c0c2bfe..b4b7d14 100644 --- a/src/avclan/cdchanger.cc +++ b/src/avclan/cdchanger.cc @@ -3,19 +3,22 @@ // Copyright (C) 2015 Allen Hill // SPDX-License-Identifier: GPL-3.0-or-later +#include +#include #include #include #include +#include #include "avclan.h" #include "cdchanger.hpp" #include "device.hpp" #include "frame.hpp" -#include "hal/cd_timer.h" #include "hal/media.h" namespace { using namespace avclan; +using namespace std::chrono_literals; constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER), to_underlying(Device::STATUS), @@ -28,39 +31,45 @@ constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER), 0x01, 0x02}; -constexpr int WIRE_SIZE = 8; // cd state report size in bytes -constexpr int TIME_SKIP = 15; // seconds +constexpr int WIRE_SIZE = 8; // cd state report size in bytes +constexpr auto TIME_SKIP = 15s; constexpr int TWODIGIT_MAX = 99; +constexpr auto MAX_TIME = std::chrono::minutes{TWODIGIT_MAX} + 59s; -/* Pack a 0–TWODIGIT_MAX count into 2-digit BCD. Values >TWODIGIT_MAX (sentinels - such as 0xFF / 0x7F meaning "no time") pass through unchanged so they survive - the wire round-trip. */ +// Pack a 0–TWODIGIT_MAX count into 2-digit BCD constexpr uint8_t toBCD(uint8_t val) { - if (val > TWODIGIT_MAX) - return val; return (uint8_t)(((val / 10) << 4) | (val % 10)); } -extern "C" void incrementTime_callback(void *self) { - static_cast(self)->incrementTime(); -} - -extern "C" bool isPlaying_callback(void *self) { - return static_cast(self)->isPlaying(); +// Whole seconds, saturated to the displayable range +constexpr std::chrono::seconds displaySeconds(std::chrono::milliseconds t) { + return std::clamp(std::chrono::floor(t), -MAX_TIME, + MAX_TIME); } } // namespace namespace avclan { -void CDChanger::init() { +void CDChanger::init(Notifier notif) { media_init(); - cdtimer_init(this, &incrementTime_callback, &isPlaying_callback); + notifier = notif; + statusTimer = xTimerCreate( + "cd status", pdMS_TO_TICKS(1000), true, this, [](TimerHandle_t timer) { + auto &self = *static_cast(pvTimerGetTimerID(timer)); + // Timer callbacks must not block; a refused request is re-made next + // tick + if (!self.statusQueued.exchange(true) && !self.notifier.notify(0, 0)) + self.statusQueued = false; + }); + configASSERT(statusTimer); } void CDChanger::handle(const Frame &in, Frame &out) { if (in.length < 4) return; // [Currently known] valid CDChanger frames have at least 4 bytes + std::optional media; + const uint8_t *data = &in.data[1]; const auto from = static_cast(*data++); /* const auto to = */ data++; @@ -73,7 +82,7 @@ void CDChanger::handle(const Frame &in, Frame &out) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). - switch (action) { + switch (const std::lock_guard lock(mutex); action) { case Enable_Function_Req: out.is_unicast = true; out.length = sizeof(function_change_resp); @@ -93,7 +102,7 @@ void CDChanger::handle(const Frame &in, Frame &out) { out.data[2] = to_underlying(from); out.data[3] = to_underlying(Disable_Function_Resp); if (isPlaying()) { - stopPlaying(); + media = stopPlaying(); state = 0; flags2 = 0x80; out.reaction = r_StatusReport; @@ -165,47 +174,39 @@ void CDChanger::handle(const Frame &in, Frame &out) { ++track; else track = 1; - mins = 0xff; - secs = 0x7f; - flags2 &= ~NEGATIVE; + time.reset(); generateStatus(out, true, Device::CMD_SW); - media_action(MediaAction::Track_Next); + media = MediaAction::Track_Next; out.reaction = r_TrackChange; break; case Track_Seek_Down: state = SEEKING_TRACK; // Track down returns to track beginning if in ~middle of song - if ((flags2 & NEGATIVE) != 0 || (mins == 0 && secs < 5)) { + if (const auto t = trackTime(); t && *t < 5s) { if (track > 1) --track; else track = TWODIGIT_MAX; - media_action(MediaAction::Track_Prev); + media = MediaAction::Track_Prev; } - mins = 0xff; - secs = 0x7f; - flags2 &= ~NEGATIVE; + time.reset(); generateStatus(out, true, Device::CMD_SW); out.reaction = r_TrackChange; break; case Track_Fast_Forward: { state |= SEEKING; - incrementTime(TIME_SKIP); + seek(TIME_SKIP); generateStatus(out, true, Device::CMD_SW); - media_action(MediaAction::Skip_Forward); - cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick - // is ~1 sec from now + media = MediaAction::Skip_Forward; out.reaction = r_SendOnly; break; } case Track_Rewind: { state |= SEEKING; - incrementTime(-TIME_SKIP); + seek(-TIME_SKIP); generateStatus(out, true, Device::CMD_SW); - media_action(MediaAction::Skip_Backward); - cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick - // is ~1 sec from now + media = MediaAction::Skip_Backward; out.reaction = r_SendOnly; break; } @@ -272,18 +273,24 @@ void CDChanger::handle(const Frame &in, Frame &out) { default: break; } #pragma GCC diagnostic pop + + // Media action handled outside of switch to minimize lock duration + if (media) + media_action(*media); } std::unique_ptr CDChanger::react(expected, detail::SendError> exp) { + std::optional media; + std::unique_ptr next; - if (!exp) { + if (const std::lock_guard lock(mutex); !exp) { if (exp.error().reaction == to_underlying(r_StateReport) && exp.error().err == detail::Error::Send::NAK_ADDRESS && ++failedStatusReports > 1) { failedStatusReports = 0; - stopPlaying(); // Disable periodic updates if e.g. no-one's - // listening (car was turned off?) + media = stopPlaying(); // Disable periodic updates if e.g. no-one's + // listening (car was turned off?) } } else { auto out = std::move(exp.value()); @@ -312,9 +319,9 @@ CDChanger::react(expected, detail::SendError> exp) { out->reaction = r_SendOnly; break; case r_TrackChange: - setTime(0, 0); - cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now + setTime(0ms); + // Track starts at a whole sec; next status is ~1 sec out + xTimerReset(statusTimer, 0); [[fallthrough]]; case r_NormalizeState: normalizeState(); @@ -327,7 +334,7 @@ CDChanger::react(expected, detail::SendError> exp) { out->reaction = r_BeganPlaying; break; case r_BeganPlaying: - startPlaying(); // only start PIT after normalizing state + media = startPlaying(); // only start status timer after normalizing out->reaction = r_Nothing; break; case r_StatusReport: @@ -341,18 +348,19 @@ CDChanger::react(expected, detail::SendError> exp) { } if (out->reaction > r_Nothing) - return out; + next = std::move(out); } - return {}; + if (media) + media_action(*media); + return next; } void CDChanger::enable(Frame &out) { if (!isPlaying()) { - if (mins > TWODIGIT_MAX) - mins = 0; - if (secs > TWODIGIT_MAX) - secs = 0; + const std::lock_guard lock(mutex); + if (!time) + setTime(0ms); state = SEEKING | SEEKING_TRACK; flags2 = 0x80; generateStatus(out, false, Device::STATUS); @@ -360,85 +368,81 @@ void CDChanger::enable(Frame &out) { } } -bool CDChanger::pending() { return cdtimer_pending(); } - -void CDChanger::emit(Frame &out) { +void CDChanger::emit(Frame &out, uint32_t /*payload*/) { + statusQueued = false; // Clear first so a tick during emit() queues anew + const std::lock_guard lock(mutex); generateStatus(out, false, Device::STATUS); out.reaction = r_StateReport; - cdtimer_clear(); } bool CDChanger::isPlaying() const { return playing; } -// Sets CD_mode to play and resets timer count (so that the next interrupt is in -// 1 sec) -void CDChanger::startPlaying() { +// Starts track time and the ~1 Hz status report (the first ~1 sec from now) +std::optional CDChanger::startPlaying() { static bool havePlayed = false; + std::optional media; if (havePlayed) - media_action(MediaAction::Play); + media = MediaAction::Play; havePlayed |= true; + if (!playing) + refTick = xTaskGetTickCount(); playing = true; - cdtimer_reset(); + xTimerReset(statusTimer, 0); + return media; } -void CDChanger::stopPlaying() { - cdtimer_disable(); +MediaAction CDChanger::stopPlaying() { + xTimerStop(statusTimer, 0); + time = trackTime(); playing = false; - media_action(MediaAction::Pause); + return MediaAction::Pause; +} + +std::optional CDChanger::trackTime() const { + if (!time || !playing) + return time; + return *time + std::chrono::milliseconds{ + pdTICKS_TO_MS(xTaskGetTickCount() - refTick)}; +} + +void CDChanger::setTime(std::chrono::milliseconds t) { + time = t; + refTick = xTaskGetTickCount(); +} + +// Seek `by` from the current whole second; no-op while no time is shown +void CDChanger::seek(std::chrono::seconds by) { + const auto t = trackTime(); + if (!t) + return; + setTime(displaySeconds(*t + by)); + if (playing) // Seeked to a whole sec; next status is ~1 sec out + xTimerReset(statusTimer, 0); } // Serialize cd_status into the wire format. // track/mins/secs, need converted from decimal to BCD void CDChanger::serialize(uint8_t *dst) const { + uint8_t mins = 0xFF; // "no time" sentinels + uint8_t secs = 0x7F; + bool negative = false; + if (const auto t = trackTime()) { + const auto sec = displaySeconds(*t); + const auto magnitude = std::chrono::abs(sec); + const auto wholeMins = std::chrono::floor(magnitude); + mins = toBCD((uint8_t)wholeMins.count()); + secs = toBCD((uint8_t)(magnitude - wholeMins).count()); + negative = sec < 0s; + } + *dst++ = cds; *dst++ = state; *dst++ = disc; *dst++ = toBCD(track); - *dst++ = toBCD(mins); - *dst++ = toBCD(secs); + *dst++ = mins; + *dst++ = secs; *dst++ = flags; - *dst++ = flags2; -} - -void CDChanger::setTime(uint8_t min, uint8_t sec) { - mins = min; - secs = sec; -} - -// Increment the time by inc_sec (REQUIRES |inc_sec| <= 59). -void CDChanger::incrementTime(int8_t inc_sec) { - // Sentinel values (>TWODIGIT_MAX) mean "no time"; leave them alone until - // setTime() replaces them with a real count. - if (mins > TWODIGIT_MAX) - return; - - if ((flags2 & NEGATIVE) != 0) - inc_sec = -inc_sec; // time forward shrinks a negative magnitude - int8_t sum = secs + inc_sec; - - if (sum < 0 && mins == 0) { - // Stepped through zero: the display flips sign and counts away from it. - secs = (uint8_t)-sum; - flags2 ^= NEGATIVE; - return; - } - - if (sum > 59) { - if (mins == TWODIGIT_MAX) { // saturate at 99:59 rather than wrap the hour - secs = 59; - return; - } - sum -= 60; - ++mins; - } else if (sum < 0) { - sum += 60; - --mins; // mins > 0: the mins == 0 borrow was handled above - } - secs = (uint8_t)sum; - - // Zero is neither sign, so it must never display as -00:00. - if ((mins | secs) == 0) - flags2 &= ~NEGATIVE; + *dst++ = negative ? (flags2 | NEGATIVE) : (flags2 & ~NEGATIVE); } // Used for changed status messages @@ -460,10 +464,8 @@ void CDChanger::generateStatus(Frame &status, bool is_unicast, } void CDChanger::normalizeState() { - if (mins > TWODIGIT_MAX) - mins = 0; - if (secs > TWODIGIT_MAX) - secs = 0; + if (!time) + setTime(0ms); state = PLAYBACK; flags &= (uint8_t)~(DISK_SCAN | SCAN); } diff --git a/src/avclan/cdchanger.hpp b/src/avclan/cdchanger.hpp index 72538e8..b7889f3 100644 --- a/src/avclan/cdchanger.hpp +++ b/src/avclan/cdchanger.hpp @@ -5,8 +5,15 @@ #pragma once +#include +#include #include #include +#include + +#include "FreeRTOS.h" // IWYU pragma: export +#include "semphr.h" +#include "timers.h" #include "avclan.h" #include "device.hpp" @@ -60,16 +67,17 @@ public: }; static constexpr Device id = Device::CD_CHANGER; - void init(); + CDChanger() : mutex(xSemaphoreCreateMutex()) {} + CDChanger(const CDChanger &) = delete; + + void init(Notifier notifier); void handle(const Frame &in, Frame &out); std::unique_ptr react(expected, detail::SendError> exp); void enable(Frame &out); void disable(Frame &out); - static bool pending(); - void emit(Frame &out); - void incrementTime(int8_t inc_sec = 1); + void emit(Frame &out, uint32_t payload); bool isPlaying() const; #ifndef NDEBUG void media_action(MediaAction action); @@ -78,21 +86,43 @@ public: #endif private: - void startPlaying(); - void stopPlaying(); + // Implements the BasicLockable named requirements for an xSemaphore + class xMutex { + public: + explicit xMutex(SemaphoreHandle_t mutex) : mutex_{mutex} {} + xMutex(const xMutex &) = delete; + + void lock() noexcept { xSemaphoreTake(mutex_, portMAX_DELAY); } + void unlock() noexcept { xSemaphoreGive(mutex_); } + + private: + SemaphoreHandle_t mutex_ = nullptr; + }; + + std::optional startPlaying(); + MediaAction stopPlaying(); + std::optional trackTime() const; + void setTime(std::chrono::milliseconds t); + void seek(std::chrono::seconds by); void serialize(uint8_t *dst) const; - void setTime(uint8_t mins, uint8_t secs); void generateStatus(Frame &status, bool is_unicast, Device to) const; void normalizeState(); + Notifier notifier{}; + TimerHandle_t statusTimer = nullptr; + std::atomic statusQueued = false; // coalesces status emit requests + xMutex mutex; + + // Track time is a stopwatch: `time` is relative to `refTick`, and + // advances while `playing`. nullopt means no time is shown. + std::optional time; + TickType_t refTick = 0; bool playing = false; int failedStatusReports = 0; uint8_t cds = CD1; uint8_t state = SEEKING | SEEKING_TRACK; uint8_t disc = 1; - uint8_t track = 1; // Decimal storage; serialize to BCD - uint8_t mins = 0xFF; // Decimal storage; serialize to BCD - uint8_t secs = 0x7F; // Decimal storage; serialize to BCD + uint8_t track = 1; // Decimal storage; serialize to BCD uint8_t flags = 0; uint8_t flags2 = 0x80; }; diff --git a/src/avclan/device.hpp b/src/avclan/device.hpp index deaa337..e63e56a 100644 --- a/src/avclan/device.hpp +++ b/src/avclan/device.hpp @@ -7,6 +7,9 @@ #include #include +#include "FreeRTOS.h" // IWYU pragma: export +#include "queue.h" + #include "avclan.h" #include "frame.hpp" #include "stdshim.hpp" @@ -47,20 +50,34 @@ enum class Device : uint8_t { TRIP_INFO = 0xE5, }; +// Notifies the Peripheral to emit for a device at `index`. Requests are served +// in order, one `emit()` each; coalescing is up to the device. `val` (24 bits) +// is device-defined. Returns whether the request was accepted within `wait`. +class Notifier { +public: + Notifier() = default; + Notifier(QueueHandle_t queue, uint8_t index) : queue{queue}, index{index} {} + + bool notify(uint32_t val = 0, TickType_t wait = portMAX_DELAY) const { + const uint32_t item = (val << 8) | index; + return xQueueSend(queue, &item, wait) == pdPASS; + } + +private: + QueueHandle_t queue = nullptr; + uint8_t index = 0; +}; + template concept DeviceInterface = requires { std::integral_constant{}; } && - requires(T dev, const Frame &in, Frame &out, - expected, detail::SendError> exp) { - dev.init(); + requires(T dev, Notifier notifier, const Frame &in, Frame &out, + expected, detail::SendError> exp, + uint32_t payload) { + dev.init(notifier); dev.handle(in, out); dev.enable(out); - { - dev.react(std::move(exp)) - } -> std::same_as>; - - { dev.pending() } -> std::convertible_to; - // Devices must clear `pending()` after `emit()` is called - dev.emit(out); + { dev.react(std::move(exp)) } -> std::same_as>; + dev.emit(out, payload); }; } // namespace avclan diff --git a/src/avclan/frame.cc b/src/avclan/frame.cc index d83d6fd..d3bcba0 100644 --- a/src/avclan/frame.cc +++ b/src/avclan/frame.cc @@ -6,8 +6,13 @@ #include #include #include +#include +#include #include +#include "FreeRTOS.h" // IWYU pragma: export +#include "task.h" + #include "frame.hpp" #include "hal/stdio.h" #include "stdshim.hpp" @@ -16,7 +21,6 @@ #include #include #include - #include namespace { template @@ -60,14 +64,28 @@ namespace avclan { #if defined(AVCLAN_FRAME_POOL_N) void *Frame::operator new(std::size_t /*count*/, const std::nothrow_t & /*tag*/) noexcept { - return pool.acquire(); + taskENTER_CRITICAL(); + Frame *frame = pool.acquire(); + taskEXIT_CRITICAL(); + return frame; } // NOLINTNEXTLINE(misc-new-delete-overloads) false-positive void Frame::operator delete(void *ptr) noexcept { + taskENTER_CRITICAL(); pool.release(static_cast(ptr)); + taskEXIT_CRITICAL(); } #endif +std::unique_ptr Frame::acquire() { + std::unique_ptr frame(new (std::nothrow) Frame); + for (uint8_t retries = 0; !frame && retries < 3; retries++) { + vTaskDelay(pdMS_TO_TICKS(2)); + frame.reset(new (std::nothrow) Frame); + } + return frame; +} + namespace { // Emit `value` as at least `width` (lowercase, as to_chars emits) hex digits, // zero-padded. `width` must be <= 3 (see the padding below); every call site diff --git a/src/avclan/frame.hpp b/src/avclan/frame.hpp index e327f14..9f9f0cb 100644 --- a/src/avclan/frame.hpp +++ b/src/avclan/frame.hpp @@ -6,6 +6,7 @@ #pragma once #include +#include #include "avclan.h" @@ -30,6 +31,9 @@ struct Frame { Error::Parse parse(const uint8_t *bytes, uint8_t len); void print(Print print) const; + // Retries every 2ms up to 3 times (max 6ms wait); null if still exhausted + static std::unique_ptr acquire(); + #if defined(AVCLAN_FRAME_POOL_N) // O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is // supported. diff --git a/src/avclan/hal/cd_timer.h b/src/avclan/hal/cd_timer.h deleted file mode 100644 index 24a7f31..0000000 --- a/src/avclan/hal/cd_timer.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2026 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -// CDChanger periodic-timer HAL: the ~1 Hz tick that drives the CD changer's -// status updates. The timer hardware is target-specific; the CD changer owns -// what a tick means (see cdchanger.cc). The device polls cdtimer_pending() and -// clears the tick with cdtimer_clear(). - -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -// One-time hardware bring-up. Leaves the tick disabled. -void cdtimer_init(void *ptr, void(clbk)(void *), bool(isplay)(void *)); - -// Reset the count so the next tick is ~1 s out, and enable the tick. -void cdtimer_reset(void); - -// Restore / disable the ~1 Hz tick. -void cdtimer_restore(void); -void cdtimer_disable(void); - -extern volatile bool cdtimer_pending_flag; - -static inline bool cdtimer_pending() { return cdtimer_pending_flag; } -static inline void cdtimer_clear() { cdtimer_pending_flag = false; } - -#ifdef __cplusplus -} -#endif diff --git a/src/avclan/hal/phy.h b/src/avclan/hal/phy.h index bdc7240..1ab591f 100644 --- a/src/avclan/hal/phy.h +++ b/src/avclan/hal/phy.h @@ -7,6 +7,8 @@ #include +#include "FreeRTOS.h" // IWYU pragma: export + #include "avclan.h" #ifdef __cplusplus @@ -30,21 +32,16 @@ void phy_mute(bool mute); // Non-mutating (e.g. theoretically const qualified/-able) bool phy_is_muted(void); -// Withhold acknowledgement. Unlike mute this leaves TX alone: a deaf device -// can still manipulate the bus/send frames, it just never responds (e.g. ACK, -// etc). An empty implementation is sufficient for synchronous ports. +// Do not respond to or log incoming frames. Can still send frames. void phy_deafen(bool deaf); -// True when there is a frame to read. This may reflect current bus state (e.g. -// a frame can be synchronously read from the bus) or indicate that a buffered -// frame is available to "read". -bool phy_frame_pending(void); +// Blocks until a frame is ready +void phy_wait_frame(TickType_t xTicksToWait); // Bus-transaction guard: quiesce the other async sources (e.g. interrupts) // so that bus read/send timing isn't disturbed. Re-enable relevant async // sources with `phy_guard_leave`. -// - May be a no-op on a target where contention isn't a concern. -// - May acquire a hardware lock to prevent concurrent use +// - May be a no-op on a target where concurrency/preemption isn't a concern. void phy_guard_enter(void); void phy_guard_leave(void); @@ -67,8 +64,9 @@ void phy_guard_leave(void); * * `expect_ack` indicates whether the recipient should be ACK'ing; false for * broadcast frames which don't have a single recipient. - * */ + +// Returns NO_FRAME when there is no frame to read. Read phy_read_header(bool *is_unicast); Read phy_read_controller_addr(uint16_t *addr); Read phy_read_peripheral_addr(uint16_t *addr); diff --git a/src/avclan/hal/stdio.h b/src/avclan/hal/stdio.h index 7545e1b..562fdfe 100644 --- a/src/avclan/hal/stdio.h +++ b/src/avclan/hal/stdio.h @@ -12,8 +12,8 @@ extern "C" { // Generic stdio interface initialization. All user I/O goes through // functions. Assumptions/invariants: -// - stdin MUST be non-blocking (ie. a libc read yields EOF immediately when no -// input is buffered). Necessary to avoid stalling the REPL poll loop. +// - A libc read of stdin blocks the calling task (not the CPU) until at least +// one byte is available. // - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not // change a bare LF or a binary frame payload. Writes through // and writes through stdio_write_nonblock() must reach the same diff --git a/src/avclan/peripheral.hpp b/src/avclan/peripheral.hpp index c6697e7..5f88d40 100644 --- a/src/avclan/peripheral.hpp +++ b/src/avclan/peripheral.hpp @@ -4,15 +4,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include +#include #include #include #include #include #include -#include #include +#include #include +#include "FreeRTOS.h" // IWYU pragma: export +#include "queue.h" + #include "avclan.h" #include "bus.hpp" #include "device.hpp" @@ -38,18 +43,26 @@ public: static_assert(((Devs::id != NoDevice) && ...), "a registered Device id collides with the NoDevice sentinel; " "update the sentinel value in avclan.h"); + static_assert(sizeof...(Devs) <= 0x100, + "too many devices: Notifier packs the device index into 8 bits"); - Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} { + static constexpr UBaseType_t EMIT_QUEUE_LEN = 2 * sizeof...(Devs); + + Peripheral(Bus &bus, uint16_t address) + : bus{bus}, address_{address}, + emit_requests_{xQueueCreate(EMIT_QUEUE_LEN, sizeof(uint32_t))} { + configASSERT(emit_requests_); bus.init(address); - (std::get(devices_).init(), ...); + (std::get(devices_).init(Notifier{emit_requests_, index_of()}), + ...); } + Peripheral(const Peripheral &) = delete; uint16_t controller() const { return controller_; }; template Dev &device() { return std::get(devices_); } - bool bus_is_active() const { return bus.is_active(); }; void mute(bool mute) { bus.mute(mute); }; bool is_muted() const { return bus.is_muted(); }; @@ -59,9 +72,6 @@ public: expected, Error::Read> read(Frame::Print print = Frame::Print{}) { - if (!bus.is_active()) - return unexpected{detail::Error::Read::NO_FRAME}; - return bus.read(print); }; @@ -72,8 +82,9 @@ public: out->control = 0xF; auto err = bus.send(*out, print); if (err != Error::Send{0}) - return unexpected{ - detail::SendError{out->owning_device, out->reaction, err}}; + return unexpected{detail::SendError{.owning_device = out->owning_device, + .reaction = out->reaction, + .err = err}}; return out; }; @@ -88,7 +99,7 @@ public: if (is_muted() || in.length < 3) return {}; - std::unique_ptr out(new (std::nothrow) Frame); + std::unique_ptr out = Frame::acquire(); if (!out) { puts("!! failed Frame alloc in route !!"); return unexpected{Error::Read::POOL_EMPTY}; @@ -207,53 +218,33 @@ public: return next; } - // Service ready devices in round-robin order + // Blocks until a device requests an emit; requests are served in order. std::unique_ptr poll() { - using U = std::unique_ptr; - auto does_emit = [&](DeviceInterface auto &dev) -> U { - if (!dev.pending()) - return {}; - - U out(new (std::nothrow) Frame); - if (!out) { - puts("!! failed Frame alloc in poll !!"); - return {}; - } - - originate(dev, *out, [](auto &d, auto &out) { d.emit(out); }); - return out; - }; - - // Runtime tuple index helper - auto does_index_emit = [&](std::size_t t) -> U { - return [&](std::index_sequence) -> U { - U out; - ((Is == t && (out = does_emit(std::get(devices_)))) || ...); - return out; - }(std::index_sequence_for{}); - }; - - constexpr std::size_t N = sizeof...(Devs); - if constexpr (N == 1) { // round-robin not needed - return does_emit(std::get<0>(devices_)); - } else { - static uint8_t rr_ = 0; // round-robin cursor - const std::size_t start = rr_; - for (std::size_t t = start; t < N; ++t) // [start, N) - if (auto out = does_index_emit(t)) { - rr_ = (t + 1 == N) ? 0 : t + 1; - return out; - } - for (std::size_t t = 0; t < start; ++t) // [0, start); t+1 <= start < N - if (auto out = does_index_emit(t)) { - rr_ = t + 1; - return out; - } + std::unique_ptr out = Frame::acquire(); + if (!out) { + puts("!! failed Frame alloc in poll !!"); return {}; } + + uint32_t val; + xQueueReceive(emit_requests_, &val, portMAX_DELAY); + const uint8_t index = val & 0xFF; + + auto emit_d = [val](auto &d, auto &out) { d.emit(out, val >> 8); }; + [&](std::index_sequence) { + ((Is == index ? originate(std::get(devices_), *out, emit_d) : void()), + ...); + }(std::index_sequence_for{}); + return out; } private: + // Dev's position in Devs + template static constexpr uint8_t index_of() { + constexpr std::array is_dev{std::is_same_v...}; + return std::ranges::find(is_dev, true) - is_dev.begin(); + } + template void stamp(Frame &out) const { if constexpr (P == Sender) out.controller_addr = address_; @@ -271,5 +262,6 @@ private: uint16_t controller_ = 0; const uint16_t address_; std::tuple devices_; + QueueHandle_t emit_requests_; }; } // namespace avclan diff --git a/src/avclan/target/pico2/CMakeLists.txt b/src/avclan/target/pico2/CMakeLists.txt index 204b201..4c6276e 100644 --- a/src/avclan/target/pico2/CMakeLists.txt +++ b/src/avclan/target/pico2/CMakeLists.txt @@ -1,16 +1,50 @@ pico_sdk_init() +include_directories( + ${CMAKE_CURRENT_LIST_DIR} + include +) +FetchContent_Declare( freertos_kernel + EXCLUDE_FROM_ALL + GIT_REPOSITORY https://github.com/FreeRTOS/FreeRTOS-Kernel.git + GIT_TAG 3a22924e0a9ddbbc8b0758881c33b3422a5cc20d # v11.3.1 +) +add_library(freertos_config INTERFACE) +target_include_directories(freertos_config SYSTEM + INTERFACE + include +) +target_compile_definitions(freertos_config + INTERFACE + projCOVERAGE_TEST=0 +) +target_compile_options(freertos_config + INTERFACE + $<$:-Wno-volatile> +) + +set( FREERTOS_PORT "GCC_RP2040" CACHE STRING "" FORCE) +FetchContent_MakeAvailable(freertos_kernel) +set(FREERTOS_KERNEL_PATH "${freertos_kernel_SOURCE_DIR}" CACHE STRING "" FORCE) +include(${freertos_kernel_SOURCE_DIR}/portable/ThirdParty/GCC/RP2040/FreeRTOS_Kernel_import.cmake) + target_sources(avclan PRIVATE phy.cc media.cc - cd_timer.cc board.cc stdio.cc ) pico_generate_pio_header(avclan ${CMAKE_CURRENT_LIST_DIR}/iebus.pio) target_link_libraries(avclan PUBLIC - pico_stdlib pico_stdio_usb hardware_pio hardware_dma pico_multicore + hardware_pio + hardware_dma + pico_stdlib + pico_stdio_usb + pico_multicore + pico_async_context_freertos + freertos_config + FreeRTOS-Kernel-Heap4 ) pico_enable_stdio_usb(avclan 1) @@ -36,8 +70,6 @@ set_source_files_properties(${_tinyusb_sources} unset(_tinyusb_sources) pico_add_extra_outputs(mockingboard_pico) -pico_set_float_implementation(mockingboard_pico none) -pico_set_double_implementation(mockingboard_pico none) pico_set_program_name(mockingboard_pico mockingboard) pico_set_program_description(mockingboard_pico diff --git a/src/avclan/target/pico2/cd_timer.cc b/src/avclan/target/pico2/cd_timer.cc deleted file mode 100644 index d00d29a..0000000 --- a/src/avclan/target/pico2/cd_timer.cc +++ /dev/null @@ -1,12 +0,0 @@ -#include "hal/cd_timer.h" - -extern "C" void cdtimer_init(void *ptr, void(clbk)(void *), - bool(isplay)(void *)) {} - -extern "C" void cdtimer_reset() {} - -extern "C" void cdtimer_restore() {} - -extern "C" void cdtimer_disable() {} - -volatile bool cdtimer_pending_flag; diff --git a/src/avclan/target/pico2/include/FreeRTOSConfig.h b/src/avclan/target/pico2/include/FreeRTOSConfig.h new file mode 100644 index 0000000..2d78fd9 --- /dev/null +++ b/src/avclan/target/pico2/include/FreeRTOSConfig.h @@ -0,0 +1,161 @@ +/* + * FreeRTOS V202111.00 + * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * http://www.FreeRTOS.org + * http://aws.amazon.com/freertos + * + * 1 tab == 4 spaces! + */ + +#ifndef FREERTOS_CONFIG_EXAMPLES_COMMON_H +#define FREERTOS_CONFIG_EXAMPLES_COMMON_H + +/*----------------------------------------------------------- + * Application specific definitions. + * + * These definitions should be adjusted for your particular hardware and + * application requirements. + * + * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE + * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. + * + * See http://www.freertos.org/a00110.html + *----------------------------------------------------------*/ + +/* Scheduler Related */ +#define configUSE_PREEMPTION 1 +#define configUSE_TICKLESS_IDLE 0 +#define configCPU_CLOCK_HZ (150000000UL) +#define configTICK_RATE_HZ ((TickType_t)1000) +#define configMAX_PRIORITIES 32 +#define configMINIMAL_STACK_SIZE (configSTACK_DEPTH_TYPE)512 +#define configIDLE_SHOULD_YIELD 1 +#define configUSE_16_BIT_TICKS 0 +#define configUSE_IDLE_HOOK 0 +#define configUSE_TICK_HOOK 0 + +/* Synchronization Related */ +#define configUSE_MUTEXES 1 +#define configUSE_RECURSIVE_MUTEXES 1 +#define configUSE_APPLICATION_TASK_TAG 0 +#define configUSE_COUNTING_SEMAPHORES 1 +#define configQUEUE_REGISTRY_SIZE 8 +#define configUSE_QUEUE_SETS 1 +#define configUSE_TIME_SLICING 1 +#define configUSE_NEWLIB_REENTRANT 0 +#define configENABLE_BACKWARD_COMPATIBILITY 0 +#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5 + +/* System */ +#define configSTACK_DEPTH_TYPE uint32_t +#define configMESSAGE_BUFFER_LENGTH_TYPE size_t + +/* Memory allocation related definitions. */ +#ifndef configSUPPORT_STATIC_ALLOCATION + #define configSUPPORT_STATIC_ALLOCATION 0 +#endif +#ifndef configSUPPORT_DYNAMIC_ALLOCATION + #define configSUPPORT_DYNAMIC_ALLOCATION 1 +#endif +#define configTOTAL_HEAP_SIZE (128 * 1024) +#define configAPPLICATION_ALLOCATED_HEAP 0 + +/* Hook function related definitions. */ +#define configCHECK_FOR_STACK_OVERFLOW 0 +#define configUSE_MALLOC_FAILED_HOOK 0 +#define configUSE_DAEMON_TASK_STARTUP_HOOK 0 + +/* Run time and task stats gathering related definitions. */ +#define configGENERATE_RUN_TIME_STATS 0 +#define configUSE_TRACE_FACILITY 1 +#define configUSE_STATS_FORMATTING_FUNCTIONS 0 + +/* Co-routine related definitions. */ +#define configUSE_CO_ROUTINES 0 +#define configMAX_CO_ROUTINE_PRIORITIES 1 + +/* Software timer related definitions. */ +#define configUSE_TIMERS 1 +#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) +#define configTIMER_QUEUE_LENGTH 10 +#define configTIMER_TASK_STACK_DEPTH 1024 + +/* Interrupt nesting behaviour configuration. */ +/* +#define configKERNEL_INTERRUPT_PRIORITY [dependent of processor] +#define configMAX_SYSCALL_INTERRUPT_PRIORITY [dependent on processor and +application] #define configMAX_API_CALL_INTERRUPT_PRIORITY [dependent on +processor and application] +*/ + +#if FREE_RTOS_KERNEL_SMP // set by the RP2xxx SMP port of FreeRTOS + /* SMP port only */ + #ifndef configNUMBER_OF_CORES + #define configNUMBER_OF_CORES 2 + #endif + #define configNUM_CORES configNUMBER_OF_CORES + #define configTICK_CORE 0 + #define configRUN_MULTIPLE_PRIORITIES 1 + #if configNUMBER_OF_CORES > 1 + #define configUSE_CORE_AFFINITY 1 + #endif + #define configUSE_PASSIVE_IDLE_HOOK 0 +#endif + +/* RP2040 specific */ +#define configSUPPORT_PICO_SYNC_INTEROP 1 +#define configSUPPORT_PICO_TIME_INTEROP 1 + +#include +/* Define to trap errors during development. */ +#define configASSERT(x) assert(x) + +/* Set the following definitions to 1 to include the API function, or zero +to exclude the API function. */ +#define INCLUDE_vTaskPrioritySet 1 +#define INCLUDE_uxTaskPriorityGet 1 +#define INCLUDE_vTaskDelete 1 +#define INCLUDE_vTaskSuspend 1 +#define INCLUDE_vTaskDelay 1 +#define INCLUDE_xTaskDelayUntil 1 +#define INCLUDE_xTaskGetSchedulerState 1 +#define INCLUDE_xTaskGetCurrentTaskHandle 1 +#define INCLUDE_uxTaskGetStackHighWaterMark 1 +#define INCLUDE_xTaskGetIdleTaskHandle 1 +#define INCLUDE_eTaskGetState 1 +#define INCLUDE_xTimerPendFunctionCall 1 +#define INCLUDE_xTaskAbortDelay 1 +#define INCLUDE_xTaskGetHandle 1 +#define INCLUDE_xTaskResumeFromISR 1 +#define INCLUDE_xQueueGetMutexHolder 1 + +#if PICO_RP2350 + #define configENABLE_MPU 0 + #define configENABLE_TRUSTZONE 0 + #define configRUN_FREERTOS_SECURE_ONLY 1 + #define configENABLE_FPU 1 + #define configMAX_SYSCALL_INTERRUPT_PRIORITY 16 +#endif + +/* A header file that defines trace macro can be included here. */ + +#endif /* FREERTOS_CONFIG_H */ diff --git a/src/avclan/target/pico2/media.cc b/src/avclan/target/pico2/media.cc index 281f107..034b592 100644 --- a/src/avclan/target/pico2/media.cc +++ b/src/avclan/target/pico2/media.cc @@ -6,5 +6,5 @@ extern "C" void media_action(MediaAction fn) {} #ifndef NDEBUG extern "C" bool media_mic_toggle(void) { return false; } -extern "C" bool media_busy(void) { return true; } +extern "C" bool media_busy(void) { return false; } #endif diff --git a/src/avclan/target/pico2/phy.cc b/src/avclan/target/pico2/phy.cc index 81b3e8f..337ffe7 100644 --- a/src/avclan/target/pico2/phy.cc +++ b/src/avclan/target/pico2/phy.cc @@ -5,6 +5,9 @@ #include #include +#include "FreeRTOS.h" // IWYU pragma: export +#include "semphr.h" + #include "avclan.h" #include "hal/phy.h" #include "hardware/gpio.h" @@ -136,11 +139,14 @@ public: prepare_ack(); begin_frame(); + xNotice = xSemaphoreCreateCounting(RXQ_N, 0); // one give per buffered frame + configASSERT(xNotice); + pio_set_irq0_source_enabled( pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), true); irq_ = (uint)pio_get_irq_num(pio_, 0); irq_set_exclusive_handler(irq_, irq_handler); - irq_set_priority(irq_, PICO_HIGHEST_IRQ_PRIORITY); + irq_set_priority(irq_, configMAX_SYSCALL_INTERRUPT_PRIORITY); irq_set_enabled(irq_, true); } @@ -178,6 +184,10 @@ public: // Only valid while frame_pending(). const RxFrame &frame() const { return rxq_[rxq_tail_]; } + void wait_frame(TickType_t xTicksToWait = portMAX_DELAY) { + xSemaphoreTake(xNotice, xTicksToWait); + } + void release() { rxq_tail_ = (rxq_tail_ + 1) & (RXQ_N - 1); } private: @@ -189,6 +199,7 @@ private: static void __time_critical_func(irq_handler)() { instance_->isr(); } void __time_critical_func(isr)() { + xHigherPriorityTaskWoken = false; // Every field after the controller address is followed by an ack slot, // which the SM consumes whether or not it drives it. const auto next_after_ack = [this](RxField next, uint8_t bits) { @@ -219,15 +230,20 @@ private: // its own acks, and a copy here would only cost a queue slot. A frame // we owe an ack for is refused when there is no room for it -- the // NAK asks the sender to send it again, rather than losing it behind - // an ack we can't honour. Both come before the field checks: a frame - // being given up needs no parity verdict, and reporting one would - // name the wrong cause for the same NAK. + // an ack we can't honour. One we will not ack at all goes the same + // way: deaf we keep nothing, and a lost arbitration leaves the + // winner's frame unacked until we rearm. Not mute -- a muted device + // still listens, and its log takes a duplicate over a gap. They come + // before the field checks: a frame being given up needs no parity + // verdict, and reporting one would name the wrong cause for the same + // NAK. const bool ours = building_.controller_addr == (uint16_t)(self_addrp_ >> 1); + const bool not_acking = deafened_ || transmitting_; const bool refuse = !ours && queue_full() && pio_interrupt_get(pio_, ack_latch); - if (ours || refuse) { + if (ours || not_acking || refuse) { if (refuse) rxq_refused_ = rxq_refused_ + 1; // Withdrawing the ack is part of giving the frame up; its slot is @@ -288,6 +304,7 @@ private: break; } } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } // Start (or restart) a frame. The start-bit block is dispatched like any @@ -360,6 +377,7 @@ private: } else { rxq_[rxq_head_] = building_; rxq_head_ = (rxq_head_ + 1) & (RXQ_N - 1); + xSemaphoreGiveFromISR(xNotice, &xHigherPriorityTaskWoken); } begin_frame(); } @@ -393,6 +411,9 @@ private: // NAK'd for want of room; the sender still owns the frame and sends it again. volatile uint32_t rxq_refused_ = 0; + SemaphoreHandle_t xNotice = nullptr; + BaseType_t xHigherPriorityTaskWoken = false; + RxFrame building_ = {}; RxField state_ = RxField::Broadcast; uint8_t data_i_ = 0; @@ -795,7 +816,9 @@ extern "C" bool phy_is_muted() { return phy.is_muted(); } extern "C" void phy_deafen(bool deaf) { phy.rx().deafen(deaf); } -extern "C" bool phy_frame_pending() { return phy.rx().frame_pending(); } +extern "C" void phy_wait_frame(TickType_t xTicksToWait) { + phy.rx().wait_frame(xTicksToWait); +} extern "C" void phy_guard_enter() {} extern "C" void phy_guard_leave() {} @@ -807,6 +830,8 @@ extern "C" void phy_guard_leave() {} // call of the frame; the caller abandons the frame on it, which releases it. extern "C" Read phy_read_header(bool *is_unicast) { + if (!phy.rx().frame_pending()) + return Read::NO_FRAME; const IEBusRx::RxFrame &frame = phy.rx().frame(); if (frame.err != Read{0}) { phy.rx().release(); diff --git a/src/avclan/target/pico2/stdio.cc b/src/avclan/target/pico2/stdio.cc index 347adf0..5d00df1 100644 --- a/src/avclan/target/pico2/stdio.cc +++ b/src/avclan/target/pico2/stdio.cc @@ -3,11 +3,14 @@ #include #include +#include "FreeRTOS.h" // IWYU pragma: export +#include "semphr.h" + #include "hal/stdio.h" #include "pico/stdio.h" #include "pico/stdio_usb.h" #include "pico/time.h" -#include "tusb.h" // IWYU pragma: keep +#include "tusb.h" // IWYU pragma: export // stdio_write_nonblock() is all-or-nothing. The CDC TX FIFO must hold the // largest buffer the app writes; that is a text frame log line (Frame::print), @@ -26,10 +29,22 @@ bool drop_indicator_pending = false; void queue(const char *str, int len) { stdio_put_string(str, len, false, false); } + +SemaphoreHandle_t rx_ready = nullptr; + +// Called from the stdio_usb background IRQ after each tud_task() with RX data +// pending. +void on_rx([[maybe_unused]] void *param) { + BaseType_t woken = false; + xSemaphoreGiveFromISR(rx_ready, &woken); + portYIELD_FROM_ISR(woken); +} } // namespace extern "C" void stdio_init() { + rx_ready = xSemaphoreCreateBinary(); stdio_usb_init(); + stdio_set_chars_available_callback(on_rx, nullptr); stdio_set_translate_crlf(&stdio_usb, false); // pico_stdio wraps printf/puts/putchar straight onto the CDC, but not // fputs/fwrite. Unbuffered stdout keeps the newlib path in step with them @@ -37,21 +52,19 @@ extern "C" void stdio_init() { setvbuf(stdout, nullptr, _IONBF, 0); } -// Overrides the SDK's weak newlib hook, which waits forever. EAGAIN rather than -// a 0-length read: newlib's refill skips a stream that has ever seen EOF. +// Overrides the SDK's weak newlib hook, which busy-waits. extern "C" int _read(int handle, char *buffer, int length) { if (handle != STDIN_FILENO) { errno = EBADF; return -1; } - const int count = stdio_get_until(buffer, length, make_timeout_time_us(0)); - if (count < 0) { - errno = EAGAIN; - return -1; + while (true) { + const int count = stdio_get_until(buffer, length, make_timeout_time_us(0)); + if (count > 0) + return count; + xSemaphoreTake(rx_ready, portMAX_DELAY); } - - return count; } extern "C" bool stdio_write_nonblock(const void *buf, uint8_t len) { diff --git a/src/queue.hpp b/src/queue.hpp index 20da5c4..32132fb 100644 --- a/src/queue.hpp +++ b/src/queue.hpp @@ -3,124 +3,39 @@ #pragma once -#include -#include -#include -#include #include -#include -namespace detail { -template struct Deleter; -} - -template > - requires((N & (N - 1)) == 0 && N <= std::numeric_limits::max() && - // push() drops the passed-in deleter and pop() fabricates a fresh - // one, so a deleter must either derive its state from the queue - // itself (the pool Deleter) or carry no state at all - (std::is_same_v> || - (std::is_empty_v && - std::is_nothrow_default_constructible_v))) -class Queue { - friend detail::Deleter; +#include "FreeRTOS.h" // IWYU pragma: export +#include "queue.h" +// Owning FIFO of `T`s, safe to share between tasks +template class Queue { public: - // Only empty construction is allowed for non-Owning, non-pool deleters - constexpr Queue() - requires(!Owning && !std::is_same_v>) - : owner{nullptr} {} + explicit Queue(UBaseType_t length) + : handle{xQueueCreate(length, sizeof(T *))} { + configASSERT(handle); + } Queue(const Queue &) = delete; - // Moving is unsupported due to being self-referential - Queue(Queue &&) = delete; - Queue &operator=(Queue &&) = delete; - - // Construct from pre-defined storage - constexpr Queue(T (&items)[N]) - requires(Owning) && std::same_as> - : owner{this}, write{N} { - for (uint8_t i = 0; i < N; ++i) - buf[i] = &items[i]; - } - // Construct non-Owning empty from Owning queue - constexpr Queue(Queue &queue) - requires(!Owning) - : owner{&queue} {} - - bool isEmpty() const { return write == read; } - uint8_t size() const { return write - read; } - uint8_t capacity() const { return N; } - bool isFull() const { return size() == capacity(); } - - uint8_t push(std::unique_ptr x) - requires(!Owning) - { - if constexpr (!std::is_same_v>) { - // structurally unnecessary for detail::Deleter, where empty construction - // is only from same size parent - if (isFull()) - return 1; - } - - if (!x) - return 1; - - if constexpr (std::is_same_v>) { - if (!x.get_deleter().is_owned_by(owner)) - return 1; - } - - claim(x.release()); - - return 0; + // Blocks until accepted (INCLUDE_vTaskSuspend: portMAX_DELAY never times out) + void push(std::unique_ptr x) { + T *ptr = x.release(); + xQueueSend(handle, &ptr, portMAX_DELAY); } - const T *peek() const { - if (isEmpty()) - return nullptr; - - return buf[mask(read)]; + std::unique_ptr pop(TickType_t wait = portMAX_DELAY) { + T *ptr = nullptr; + xQueueReceive(handle, &ptr, wait); + return std::unique_ptr(ptr); } - std::unique_ptr pop() { - if (isEmpty()) - return nullptr; - - if constexpr (!std::is_same_v>) - return {buf[mask(read++)], Deleter{}}; - else if constexpr (Owning) - return {buf[mask(read++)], Deleter{this}}; - else - return {buf[mask(read++)], Deleter{owner}}; + // The front item stays owned by the queue + const T *peek(TickType_t wait = portMAX_DELAY) const { + T *ptr = nullptr; + xQueuePeek(handle, &ptr, wait); + return ptr; } private: - uint8_t mask(uint8_t pos) const { return pos & (N - 1); } - void claim(T *x) { buf[mask(write++)] = x; } - - std::array buf = {}; - Queue *const owner; - uint8_t read = 0; - uint8_t write = 0; + QueueHandle_t handle; }; - -template -Queue(Queue &) -> Queue; -template -Queue(T (&items)[N]) -> Queue>; - -namespace detail { -template struct Deleter { - Deleter() = default; - constexpr Deleter(Queue *owner) : owner{owner} {} - void operator()(T *x) const { owner->claim(x); } - constexpr bool is_owned_by(const Queue *parent) const { - return owner == parent; - } - -private: - Queue *const owner = nullptr; -}; -} // namespace detail diff --git a/src/sniffer.cc b/src/sniffer.cc index fbb4ae9..24ce3b5 100644 --- a/src/sniffer.cc +++ b/src/sniffer.cc @@ -7,9 +7,11 @@ #include #include #include -#include #include +#include "FreeRTOS.h" // IWYU pragma: export +#include "task.h" + #include "cdchanger.hpp" #include "frame.hpp" #include "hal/board.h" @@ -18,17 +20,30 @@ #include "queue.hpp" #include "stdshim.hpp" -const char *const offon[] = {"OFF", "ON"}; - -constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE; -static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N, - "CACHE_SIZE must be >= avclan::Frame allocator pool capacity"); - using namespace avclan; namespace { -constinit Queue incoming; -constinit Queue outgoing; +using Periph = Peripheral; + +const char *const offon[] = {"OFF", "ON"}; + +constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE; +// A queue that can hold the entire Frame pool is never full, so the sender's +// blocking push of a follow-up frame onto its own queue can't deadlock +static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N, + "CACHE_SIZE must be >= avclan::Frame allocator pool capacity"); + +// Must be ranked such that queues deterministically tend to empty +constexpr UBaseType_t SendPriority = tskIDLE_PRIORITY + 3; +constexpr UBaseType_t RoutePriority = tskIDLE_PRIORITY + 2; +constexpr UBaseType_t PollPriority = tskIDLE_PRIORITY + 2; +constexpr UBaseType_t ReceivePriority = tskIDLE_PRIORITY + 1; + +struct Context { + Periph &peripheral; + Queue &incoming; + Queue &outgoing; +}; uint8_t hexChars[2]; uint8_t hexDigit = 0; // current digit being written to hexChars @@ -61,48 +76,59 @@ void set_flag(bool *flag, bool val, const char *msg) { void Setup(); void print_help(); -} // namespace -int main() { - Bus phy; - using enum Action; - using enum Device; - Peripheral peripheral(phy, 0x360); +[[noreturn]] void vReceiverTask(void *pvParameters) { using Print = Frame::Print; - - Setup(); - print_help(); - + auto &[peripheral, incoming, outgoing] = + *static_cast(pvParameters); while (true) { if (auto msg = peripheral.read(Print{.print = printAllFrames, .binary = printBinary, .verbose = verbose})) incoming.push(std::move(*msg)); + } +} - if (const auto *in = incoming.peek()) { - if (auto resp = peripheral.route(*in)) { - incoming.pop(); - if (*resp) { - outgoing.push(std::move(*resp)); - continue; // route can be long; re-check the bus before poll/send - } - } +[[noreturn]] void vRoutingTask(void *pvParameters) { + auto &[peripheral, incoming, outgoing] = + *static_cast(pvParameters); + while (true) { + const Frame *in = incoming.peek(); + if (auto resp = peripheral.route(*in)) { + incoming.pop(); + if (*resp) + outgoing.push(std::move(*resp)); } + } +} +[[noreturn]] void vPollTask(void *pvParameters) { + auto &[peripheral, incoming, outgoing] = + *static_cast(pvParameters); + while (true) { if (auto msg = peripheral.poll()) outgoing.push(std::move(msg)); + } +} - if (auto out = outgoing.pop()) { - auto result = - peripheral.send(std::move(out), Print{.print = printAllFrames, - .binary = printBinary}); - if (auto next = peripheral.react(std::move(result))) - outgoing.push(std::move(next)); +[[noreturn]] void vSenderTask(void *pvParameters) { + using Print = Frame::Print; + auto &[peripheral, incoming, outgoing] = + *static_cast(pvParameters); + while (true) { + auto result = peripheral.send( + outgoing.pop(), Print{.print = printAllFrames, .binary = printBinary}); + if (auto next = peripheral.react(std::move(result))) + outgoing.push(std::move(next)); + } +} - continue; - } - - // stdin must be non-blocking: yielding EOF when idle/empty +[[noreturn]] void vREPLTask(void *pvParameters) { + using enum Action; + using enum Device; + auto &[peripheral, incoming, outgoing] = + *static_cast(pvParameters); + while (true) { if (int readkey = fgetc(stdin); readkey != EOF) { switch (readkey) { case '?': print_help(); break; @@ -121,7 +147,7 @@ int main() { case 'x': set_flag(&printBinary, false, "Binary:"); break; case 'E': // Beep - if (auto out = std::unique_ptr(new (std::nothrow) Frame)) { + if (auto out = Frame::acquire()) { out->is_unicast = true; out->peripheral_addr = peripheral.controller(); { @@ -136,7 +162,7 @@ int main() { puts("!! failed Frame alloc for Beep !! "); break; case 'P': - if (auto out = std::unique_ptr(new (std::nothrow) Frame)) { + if (auto out = Frame::acquire()) { out->is_unicast = true; out->peripheral_addr = peripheral.controller(); { @@ -235,8 +261,7 @@ int main() { if (readSeq && seqIdx > 0) { if (readBinary) { if (data_tmp[seqIdx - 1] == 0x17) { - if (auto out = - std::unique_ptr(new (std::nothrow) Frame)) { + if (auto out = Frame::acquire()) { if (out->parse(data_tmp, --seqIdx) == Frame::Error::Parse{0}) { out->reaction = 1; @@ -249,7 +274,7 @@ int main() { goto DEFAULT; // reading binary and this is a real data byte; // fall through to default } else { // ASCII message - if (auto out = std::unique_ptr(new (std::nothrow) Frame)) { + if (auto out = Frame::acquire()) { const uint8_t sendLen = seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH; out->is_unicast = seqIsUnicast; @@ -322,7 +347,34 @@ int main() { clearerr(stdin); } // if (readkey != EOF) } - return 0; +} + +} // namespace + +int main() { + Setup(); + print_help(); + + // Static: the scheduler reclaims main's stack for ISRs + static Bus phy; + static Periph peripheral(phy, 0x360); + static Queue incoming(CACHE_SIZE); + static Queue outgoing(CACHE_SIZE); + static Context ctx{ + .peripheral = peripheral, .incoming = incoming, .outgoing = outgoing}; + + xTaskCreateAffinitySet(vSenderTask, "send/react", configMINIMAL_STACK_SIZE, + &ctx, SendPriority, 0b01, nullptr); + xTaskCreateAffinitySet(vRoutingTask, "router", configMINIMAL_STACK_SIZE, &ctx, + RoutePriority, 0b01, nullptr); + xTaskCreateAffinitySet(vPollTask, "poll", configMINIMAL_STACK_SIZE, &ctx, + PollPriority, 0b01, nullptr); + xTaskCreateAffinitySet(vReceiverTask, "receiver", configMINIMAL_STACK_SIZE, + &ctx, ReceivePriority, 0b01, nullptr); + xTaskCreateAffinitySet(vREPLTask, "repl", configMINIMAL_STACK_SIZE, &ctx, + tskIDLE_PRIORITY, 0b01, nullptr); + + vTaskStartScheduler(); } namespace {