Migrate to FreeRTOS

This commit is contained in:
Allen Hill
2026-09-22 20:34:38 -07:00
parent 5b9cb1da8b
commit 90e54b0896
20 changed files with 637 additions and 428 deletions
+2 -1
View File
@@ -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)
+12 -14
View File
@@ -30,11 +30,11 @@
(zero) is not expected.
*/
#include <concepts>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <new>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h"
#include "bus.hpp"
@@ -113,20 +113,15 @@ void Bus::init(uint16_t address) {
return;
phy_init(address);
muted_ = false; // phy_init leaves the bus TX unmuted
deafened_ = false; // Default to listening
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<std::unique_ptr<Frame>, Error::Read> {
@@ -137,12 +132,14 @@ auto Bus::read(Frame::Print print)
using enum Read;
std::unique_ptr<Frame> in(new (std::nothrow) Frame);
std::unique_ptr<Frame> 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
-5
View File
@@ -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;
};
+108 -106
View File
@@ -3,19 +3,22 @@
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#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),
@@ -29,38 +32,44 @@ constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
0x02};
constexpr int WIRE_SIZE = 8; // cd state report size in bytes
constexpr int TIME_SKIP = 15; // seconds
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<avclan::CDChanger *>(self)->incrementTime();
}
extern "C" bool isPlaying_callback(void *self) {
return static_cast<avclan::CDChanger *>(self)->isPlaying();
// Whole seconds, saturated to the displayable range
constexpr std::chrono::seconds displaySeconds(std::chrono::milliseconds t) {
return std::clamp(std::chrono::floor<std::chrono::seconds>(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<CDChanger *>(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<MediaAction> media;
const uint8_t *data = &in.data[1];
const auto from = static_cast<Device>(*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,17 +273,23 @@ 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<Frame>
CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
std::optional<MediaAction> media;
std::unique_ptr<Frame> 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
media = stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?)
}
} else {
@@ -312,9 +319,9 @@ CDChanger::react(expected<std::unique_ptr<Frame>, 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<std::unique_ptr<Frame>, 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<std::unique_ptr<Frame>, 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<MediaAction> CDChanger::startPlaying() {
static bool havePlayed = false;
std::optional<MediaAction> 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<std::chrono::milliseconds> 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<std::chrono::minutes>(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);
}
+39 -9
View File
@@ -5,8 +5,15 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <optional>
#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<Frame>
react(expected<std::unique_ptr<Frame>, 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<MediaAction> startPlaying();
MediaAction stopPlaying();
std::optional<std::chrono::milliseconds> 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<bool> 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<std::chrono::milliseconds> 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 flags = 0;
uint8_t flags2 = 0x80;
};
+27 -10
View File
@@ -7,6 +7,9 @@
#include <cstdint>
#include <memory>
#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 <class T>
concept DeviceInterface =
requires { std::integral_constant<Device, T::id>{}; } &&
requires(T dev, const Frame &in, Frame &out,
expected<std::unique_ptr<Frame>, detail::SendError> exp) {
dev.init();
requires(T dev, Notifier notifier, const Frame &in, Frame &out,
expected<std::unique_ptr<Frame>, detail::SendError> exp,
uint32_t payload) {
dev.init(notifier);
dev.handle(in, out);
dev.enable(out);
{
dev.react(std::move(exp))
} -> std::same_as<std::unique_ptr<Frame>>;
{ dev.pending() } -> std::convertible_to<bool>;
// Devices must clear `pending()` after `emit()` is called
dev.emit(out);
{ dev.react(std::move(exp)) } -> std::same_as<std::unique_ptr<Frame>>;
dev.emit(out, payload);
};
} // namespace avclan
+20 -2
View File
@@ -6,8 +6,13 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <type_traits>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "frame.hpp"
#include "hal/stdio.h"
#include "stdshim.hpp"
@@ -16,7 +21,6 @@
#include <array>
#include <cstddef>
#include <limits>
#include <new>
namespace {
template <class T, std::uint8_t N>
@@ -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<Frame *>(ptr));
taskEXIT_CRITICAL();
}
#endif
std::unique_ptr<Frame> Frame::acquire() {
std::unique_ptr<Frame> 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
+4
View File
@@ -6,6 +6,7 @@
#pragma once
#include <cstdint>
#include <memory>
#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<Frame> acquire();
#if defined(AVCLAN_FRAME_POOL_N)
// O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported.
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// 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
+8 -10
View File
@@ -7,6 +7,8 @@
#include <stdint.h>
#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);
+2 -2
View File
@@ -12,8 +12,8 @@ extern "C" {
// Generic stdio interface initialization. All user I/O goes through <stdio.h>
// 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
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
+37 -45
View File
@@ -4,15 +4,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <tuple>
#include <type_traits>
#include <utility>
#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<Devs>(devices_).init(), ...);
(std::get<Devs>(devices_).init(Notifier{emit_requests_, index_of<Devs>()}),
...);
}
Peripheral(const Peripheral &) = delete;
uint16_t controller() const { return controller_; };
template <DeviceInterface Dev> Dev &device() {
return std::get<Dev>(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<std::unique_ptr<Frame>, 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<Frame> out(new (std::nothrow) Frame);
std::unique_ptr<Frame> 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<Frame> poll() {
using U = std::unique_ptr<Frame>;
auto does_emit = [&](DeviceInterface auto &dev) -> U {
if (!dev.pending())
return {};
U out(new (std::nothrow) Frame);
std::unique_ptr<Frame> out = Frame::acquire();
if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
originate(dev, *out, [](auto &d, auto &out) { d.emit(out); });
return out;
};
uint32_t val;
xQueueReceive(emit_requests_, &val, portMAX_DELAY);
const uint8_t index = val & 0xFF;
// Runtime tuple index helper
auto does_index_emit = [&](std::size_t t) -> U {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> U {
U out;
((Is == t && (out = does_emit(std::get<Is>(devices_)))) || ...);
return out;
auto emit_d = [val](auto &d, auto &out) { d.emit(out, val >> 8); };
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
((Is == index ? originate(std::get<Is>(devices_), *out, emit_d) : void()),
...);
}(std::index_sequence_for<Devs...>{});
};
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;
}
return {};
}
}
private:
// Dev's position in Devs
template <class Dev> static constexpr uint8_t index_of() {
constexpr std::array is_dev{std::is_same_v<Dev, Devs>...};
return std::ranges::find(is_dev, true) - is_dev.begin();
}
template <Party P> 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<Devs...> devices_;
QueueHandle_t emit_requests_;
};
} // namespace avclan
+36 -4
View File
@@ -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
$<$<COMPILE_LANGUAGE:CXX>:-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
-12
View File
@@ -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;
@@ -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 <assert.h>
/* 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 */
+1 -1
View File
@@ -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
+31 -6
View File
@@ -5,6 +5,9 @@
#include <hardware/clocks.h>
#include <limits>
#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();
+21 -8
View File
@@ -3,11 +3,14 @@
#include <cstdio>
#include <unistd.h>
#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;
}
while (true) {
const int count = stdio_get_until(buffer, length, make_timeout_time_us(0));
if (count < 0) {
errno = EAGAIN;
return -1;
}
if (count > 0)
return count;
xSemaphoreTake(rx_ready, portMAX_DELAY);
}
}
extern "C" bool stdio_write_nonblock(const void *buf, uint8_t len) {
+22 -107
View File
@@ -3,124 +3,39 @@
#pragma once
#include <array>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <type_traits>
namespace detail {
template <class T, auto N> struct Deleter;
}
template <class T, std::integral auto N, bool Owning = false,
class Deleter = std::default_delete<T>>
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::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<Deleter, detail::Deleter<T, N>> ||
(std::is_empty_v<Deleter> &&
std::is_nothrow_default_constructible_v<Deleter>)))
class Queue {
friend detail::Deleter<T, N>;
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
// Owning FIFO of `T`s, safe to share between tasks
template <class T> class Queue {
public:
// Only empty construction is allowed for non-Owning, non-pool deleters
constexpr Queue()
requires(!Owning && !std::is_same_v<Deleter, detail::Deleter<T, N>>)
: 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<Deleter, detail::Deleter<T, N>>
: 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<T, N, true, Deleter> &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<T, Deleter> x)
requires(!Owning)
{
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) {
// structurally unnecessary for detail::Deleter, where empty construction
// is only from same size parent
if (isFull())
return 1;
// Blocks until accepted (INCLUDE_vTaskSuspend: portMAX_DELAY never times out)
void push(std::unique_ptr<T> x) {
T *ptr = x.release();
xQueueSend(handle, &ptr, portMAX_DELAY);
}
if (!x)
return 1;
if constexpr (std::is_same_v<Deleter, detail::Deleter<T, N>>) {
if (!x.get_deleter().is_owned_by(owner))
return 1;
std::unique_ptr<T> pop(TickType_t wait = portMAX_DELAY) {
T *ptr = nullptr;
xQueueReceive(handle, &ptr, wait);
return std::unique_ptr<T>(ptr);
}
claim(x.release());
return 0;
}
const T *peek() const {
if (isEmpty())
return nullptr;
return buf[mask(read)];
}
std::unique_ptr<T, Deleter> pop() {
if (isEmpty())
return nullptr;
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>)
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<T *, N> buf = {};
Queue<T, N, true, Deleter> *const owner;
uint8_t read = 0;
uint8_t write = 0;
QueueHandle_t handle;
};
template <class T, auto N, class D>
Queue(Queue<T, N, true, D> &) -> Queue<T, N, false, D>;
template <class T, auto N>
Queue(T (&items)[N]) -> Queue<T, N, true, detail::Deleter<T, N>>;
namespace detail {
template <class T, auto N> struct Deleter {
Deleter() = default;
constexpr Deleter(Queue<T, N, true, Deleter> *owner) : owner{owner} {}
void operator()(T *x) const { owner->claim(x); }
constexpr bool is_owned_by(const Queue<T, N, true, Deleter> *parent) const {
return owner == parent;
}
private:
Queue<T, N, true, Deleter> *const owner = nullptr;
};
} // namespace detail
+88 -36
View File
@@ -7,9 +7,11 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <utility>
#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<Frame, CACHE_SIZE> incoming;
constinit Queue<Frame, CACHE_SIZE> outgoing;
using Periph = Peripheral<CDChanger>;
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<Frame> &incoming;
Queue<Frame> &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<CDChanger> peripheral(phy, 0x360);
[[noreturn]] void vReceiverTask(void *pvParameters) {
using Print = Frame::Print;
Setup();
print_help();
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(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()) {
[[noreturn]] void vRoutingTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
const Frame *in = incoming.peek();
if (auto resp = peripheral.route(*in)) {
incoming.pop();
if (*resp) {
if (*resp)
outgoing.push(std::move(*resp));
continue; // route can be long; re-check the bus before poll/send
}
}
}
}
[[noreturn]] void vPollTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(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});
[[noreturn]] void vSenderTask(void *pvParameters) {
using Print = Frame::Print;
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(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<Context *>(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<Frame>(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<Frame>(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<Frame>(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<Frame>(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<Frame> incoming(CACHE_SIZE);
static Queue<Frame> 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 {