diff --git a/CMakeLists.txt b/CMakeLists.txt index d75ec18..dfe256f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,8 @@ add_compile_options( $<$:-fno-threadsafe-statics> $<$:-fno-exceptions> $<$:-fno-rtti> - $<$:-fanalyzer>) + $<$:-fanalyzer> + $<$:-Wno-analyzer-use-of-uninitialized-value>) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13") diff --git a/src/avclan/avclan.h b/src/avclan/avclan.h index eabd534..efcf941 100644 --- a/src/avclan/avclan.h +++ b/src/avclan/avclan.h @@ -99,11 +99,28 @@ enum AVCLAN_ENUM_CLASS Action : uint8_t { Track_Name_Resp = 0xfd, // Reports - Playback_Status = 0xf1, // Typically unprompted, sent to Device::STATUS - Loading_Status = 0xf3, // Typically unprompted, sent to Device::STATUS + Playback_Status = 0xf1, // Typically unprompted, sent to Device::STATUS + Loading_Status = 0xf3, // Typically unprompted, sent to Device::STATUS Report_TOC = 0xf9, }; +// Media functions a source device can perform (ultimately bounded by the Action +// set above). +enum AVCLAN_ENUM_CLASS MediaAction : uint8_t { + Play = 0x01, + Pause = 0x02, + Play_Pause = Play | Pause, + Skip_Forward, + Skip_Backward, + Track_Next, + Track_Prev, + Repeat, + Repeat_Single, + Shuffle, + Volume_Up, + Volume_Down, +}; + #ifdef __cplusplus namespace detail { struct Error { diff --git a/src/avclan/bus.cc b/src/avclan/bus.cc index 40e12d9..a54b9a9 100644 --- a/src/avclan/bus.cc +++ b/src/avclan/bus.cc @@ -31,24 +31,149 @@ #include "bus.hpp" #include "avclan.h" -#include "avclan_phy.h" // bridge until phy has been ported #include "com232.h" #include "frame.hpp" +#include "hal/phy.h" // bridge until phy has been ported namespace { constexpr int ADDR_WIDTH = 12; constexpr int CONTROL_WIDTH = 4; constexpr int BYTE_WIDTH = 8; + +struct trailer_bits_t {}; +struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit) +struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address) +struct with_ack_t : trailer_bits_t { +}; // bits + parity + ACK slot (all other fields) +inline constexpr no_parity_t no_parity{}; +inline constexpr with_parity_t with_parity{}; +inline constexpr with_ack_t with_ack{}; } // namespace namespace avclan { -Bus::Handle::Handle() { AVCLAN_stopEvent(); } -Bus::Handle::~Handle() { AVCLAN_startEvent(); } +class Bus::Handle { + Handle() { phy_guard_enter(); }; + friend Bus; -void Bus::init() { AVCLAN_busInit(); }; -void Bus::mute(bool mute) { AVCLAN_muteDevice(mute); }; -bool Bus::is_muted() const { return AVCLAN_ismuted(); }; +public: + ~Handle() { phy_guard_leave(); }; + Handle(const Handle &) = delete; + Handle(Handle &&) = delete; + using Error = detail::Error; + + bool sendstartbit() { return phy_send_startbit(); }; + auto readstartbit() -> Read { return phy_read_startbit(); }; + + template Trailer> + requires(sizeof(T) < 3 && N < 16 && !std::same_as) + Error::Send send(T bits, Trailer /*tag*/) { + const auto parity = sendbits(bits); + + if constexpr (std::is_same_v) + sendbits<1>(static_cast(parity)); + + return Send{0}; + }; + + template + requires(sizeof(T) < 3 && N < 16) + Error::Send send(T bits, with_ack_t /*tag*/, bool expect_ack) { + send(bits, with_parity); + + if (expect_ack && !read_ACK()) + return Send::NAK; + + return Send{0}; + }; + + template Trailer> + requires(sizeof(T) < 3 && N < 16 && !std::same_as) + Error::Read read(T *bits, Trailer /*tag*/) { + const auto calc_parity = readbits(bits); + if constexpr (std::is_same_v) { + uint8_t read_parity; + readbits<1>(&read_parity); + if (static_cast(calc_parity) != read_parity) + return Read::BAD_PARITY; + } + return Read{0}; + }; + + template + requires(sizeof(T) < 3 && N < 16) + Error::Read read(T *bits, with_ack_t /*tag*/, F &&ack) { + if (read(bits, with_parity) == Read::BAD_PARITY) + return Read::BAD_PARITY; + + if (ack()) { + send_ACK(); + } else { + uint8_t slot; + readbits<1>(&slot); + } + + return Read{0}; + }; + template + requires(sizeof(T) < 3 && N < 16) + Error::Read read(T *bits, with_ack_t /*tag*/, bool ack) { + return read(bits, with_ack, [=]() { return ack; }); + } + +private: + using Read = Error::Read; + using Send = Error::Send; + using Bit = detail::Bit; + + static void send_ACK() { phy_send_ack(); }; + static uint8_t read_ACK() { return phy_read_ack(); }; + + template Bit sendbits(T bits); + template Bit readbits(T *bits); + + // Temporary specializations bridging to legacy C API + // Replace with proper (single?) template when phy has been ported + template + requires(N > 1 && N < 8) + Bit sendbits(uint8_t bits) { + return phy_send_bits_u8(&bits, N); + }; + template + requires(N <= 16) + Bit sendbits(uint16_t bits) { + return phy_send_bits_u16(&bits, N); + }; + template + requires(N < 8) + Bit readbits(uint8_t *bits) { + return static_cast(phy_read_bits_u8(bits, N)); + }; + template + requires(N <= 16) + Bit readbits(uint16_t *bits) { + return static_cast(phy_read_bits_u16(bits, N)); + }; +}; + +template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) { + return phy_send_byte(&bits); +}; +template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) { + const Bit bit{static_cast(bits & 1U)}; + phy_send_bit(bit); + return bit; +}; +template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) { + return static_cast(phy_read_byte(bits)); +}; + +void Bus::init() { phy_init(); }; +bool Bus::is_active() const { return phy_active(); }; +void Bus::mute(bool mute) { phy_mute(mute); }; +bool Bus::is_muted() const { return phy_is_muted(); }; auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Error::Read { struct errtype { @@ -278,21 +403,8 @@ auto Bus::send(const Frame *out, Frame::Print print) -> Error::Send { Bus::Handle Bus::get() { return {}; }; -bool Bus::Handle::sendstartbit() { return AVCLAN_sendstartbit(); }; -auto Bus::Handle::readstartbit() -> Read { return AVCLAN_readstartbit(); }; -void Bus::Handle::send_ACK() { AVCLAN_sendbit_ACK(); }; -uint8_t Bus::Handle::read_ACK() { return AVCLAN_readbit_ACK(); }; - -template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) { - return AVCLAN_sendbyte(&bits); -}; -template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) { - const Bit bit{static_cast(bits & 1U)}; - AVCLAN_sendbit(bit); - return bit; -}; -template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) { - return static_cast(AVCLAN_readbyte(bits)); -}; +#ifndef NDEBUG +void Bus::measure() { phy_measure(); } +#endif } // namespace avclan diff --git a/src/avclan/bus.hpp b/src/avclan/bus.hpp index dc4fc96..07c93eb 100644 --- a/src/avclan/bus.hpp +++ b/src/avclan/bus.hpp @@ -52,139 +52,30 @@ #include #include "avclan.h" -#include "avclan_phy.h" // bridge until phy has been ported #include "frame.hpp" namespace avclan { -struct trailer_bits_t {}; -struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit) -struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address) -struct with_ack_t : trailer_bits_t { -}; // bits + parity + ACK slot (all other fields) -inline constexpr no_parity_t no_parity{}; -inline constexpr with_parity_t with_parity{}; -inline constexpr with_ack_t with_ack{}; - class Bus { public: - class Handle; using Error = detail::Error; void init(); + + bool is_active() const; void mute(bool mute); bool is_muted() const; +#ifndef NDEBUG + void measure(); +#endif + Error::Read read(uint16_t address, Frame *in, Frame::Print print); Error::Send send(const Frame *out, Frame::Print print); +private: + class Handle; static Handle get(); }; -class Bus::Handle { - Handle(); - friend Bus; - -public: - ~Handle(); - Handle(const Handle &) = delete; - Handle(Handle &&) = delete; - using Error = detail::Error; - - bool sendstartbit(); - Error::Read readstartbit(); - - template Trailer> - requires(sizeof(T) < 3 && N < 16 && !std::same_as) - Error::Send send(T bits, Trailer /*tag*/) { - const auto parity = sendbits(bits); - - if constexpr (std::is_same_v) - sendbits<1>(static_cast(parity)); - - return Send{0}; - }; - - template - requires(sizeof(T) < 3 && N < 16) - Error::Send send(T bits, with_ack_t /*tag*/, bool expect_ack) { - send(bits, with_parity); - - if (expect_ack && !read_ACK()) - return Send::NAK; - - return Send{0}; - }; - - template Trailer> - requires(sizeof(T) < 3 && N < 16 && !std::same_as) - Error::Read read(T *bits, Trailer /*tag*/) { - const auto calc_parity = readbits(bits); - if constexpr (std::is_same_v) { - uint8_t read_parity; - readbits<1>(&read_parity); - if (static_cast(calc_parity) != read_parity) - return Read::BAD_PARITY; - } - return Read{0}; - }; - - template - requires(sizeof(T) < 3 && N < 16) - Error::Read read(T *bits, with_ack_t /*tag*/, F &&ack) { - if (read(bits, with_parity) == Read::BAD_PARITY) - return Read::BAD_PARITY; - - if (ack()) { - send_ACK(); - } else { - uint8_t slot; - readbits<1>(&slot); - } - - return Read{0}; - }; - template - requires(sizeof(T) < 3 && N < 16) - Error::Read read(T *bits, with_ack_t /*tag*/, bool ack) { - return read(bits, with_ack, [=]() { return ack; }); - } - -private: - using Read = Error::Read; - using Send = Error::Send; - using Bit = detail::Bit; - - static void send_ACK(); - static uint8_t read_ACK(); - - template Bit sendbits(T bits); - template Bit readbits(T *bits); - - // Temporary specializations bridging to legacy C API - // Replace with proper (single?) template when phy has been ported - template - requires(N > 1 && N < 8) - Bit sendbits(uint8_t bits) { - return AVCLAN_sendbitsi(&bits, N); - }; - template - requires(N <= 16) - Bit sendbits(uint16_t bits) { - return AVCLAN_sendbitsl(&bits, N); - }; - template - requires(N < 8) - Bit readbits(uint8_t *bits) { - return static_cast(AVCLAN_readbitsi(bits, N)); - }; - template - requires(N <= 16) - Bit readbits(uint16_t *bits) { - return static_cast(AVCLAN_readbitsl(bits, N)); - }; -}; - } // namespace avclan diff --git a/src/avclan/cdchanger.cc b/src/avclan/cdchanger.cc index 842d6d4..3cfed57 100644 --- a/src/avclan/cdchanger.cc +++ b/src/avclan/cdchanger.cc @@ -10,23 +10,22 @@ #include "cdchanger.hpp" #include "device.hpp" #include "frame.hpp" -#include "mediacontrol.h" -#include "statustimer.h" +#include "hal/cd_timer.h" +#include "hal/media.h" namespace { using namespace avclan; -constexpr uint8_t cdloading_resp[] = { - to_underlying(Device::CD_CHANGER), - to_underlying(Device::STATUS), - to_underlying(Action::Loading_Status), - 0x00, - 0x01, - 0x00, - 0x01, - 0x00, - 0x01, - 0x02}; +constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER), + to_underlying(Device::STATUS), + to_underlying(Action::Loading_Status), + 0x00, + 0x01, + 0x00, + 0x01, + 0x00, + 0x01, + 0x02}; constexpr int WIRE_SIZE = 8; // cd state report size in bytes constexpr int TIME_SKIP = 15; // seconds @@ -53,8 +52,8 @@ extern "C" bool isPlaying_callback(void *self) { namespace avclan { void CDChanger::init() { - mediacontrol_init(); - statustimer_init(this, &incrementTime_callback, &isPlaying_callback); + media_init(); + cdtimer_init(this, &incrementTime_callback, &isPlaying_callback); } void CDChanger::handle(const Frame *in, Frame *out) { @@ -119,16 +118,15 @@ void CDChanger::handle(const Frame *in, Frame *out) { case Initial_Report_Req: { out->is_unicast = true; // No knowledge/understanding of field meaning/interpretation - const uint8_t cdinitreport_resp[] = { - 0x00, - to_underlying(Device::CD_CHANGER), - to_underlying(from), - to_underlying(Initial_Report_Resp), - 0x01, - 0x31, - 0x10, - 0x01, - 0x01}; + const uint8_t cdinitreport_resp[] = {0x00, + to_underlying(Device::CD_CHANGER), + to_underlying(from), + to_underlying(Initial_Report_Resp), + 0x01, + 0x31, + 0x10, + 0x01, + 0x01}; out->length = sizeof(cdinitreport_resp); memcpy(out->data, cdinitreport_resp, sizeof(cdinitreport_resp)); out->reaction = r_SendOnly; @@ -163,7 +161,7 @@ void CDChanger::handle(const Frame *in, Frame *out) { secs = 0x7f; flags2 = 0xc0; generateStatus(out, true, Device::CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); + media_action(MediaAction::Track_Next); out->reaction = r_TrackChange; break; case Track_Seek_Down: @@ -174,12 +172,13 @@ void CDChanger::handle(const Frame *in, Frame *out) { --track; else track = TWODIGIT_MAX; + + media_action(MediaAction::Track_Prev); } mins = 0xff; secs = 0x7f; flags2 = 0xc0; generateStatus(out, true, Device::CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); out->reaction = r_TrackChange; break; case Track_Fast_Forward: { @@ -190,9 +189,9 @@ void CDChanger::handle(const Frame *in, Frame *out) { ++mins; } generateStatus(out, true, Device::CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); - statustimer_reset(); // Skipped to a whole/round sec; ensure next tick - // is ~1 sec from now + media_action(MediaAction::Skip_Forward); + cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick + // is ~1 sec from now out->reaction = r_SendOnly; break; } @@ -210,9 +209,9 @@ void CDChanger::handle(const Frame *in, Frame *out) { } else secs -= TIME_SKIP; generateStatus(out, true, Device::CMD_SW); - AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); - statustimer_reset(); // Skipped to a whole/round sec; ensure next tick - // is ~1 sec from now + media_action(MediaAction::Skip_Backward); + cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick + // is ~1 sec from now out->reaction = r_SendOnly; break; } @@ -296,8 +295,8 @@ void CDChanger::react(Frame *out, detail::Error::Send err) { break; case r_TrackChange: setTime(0, 0); - statustimer_reset(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now + cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is + // ~1 sec from now [[fallthrough]]; case r_NormalizeState: normalizeState(); @@ -336,8 +335,8 @@ void CDChanger::enable(Frame *out) { } } -bool CDChanger::pending() { return statustimer_tickPending(); } -void CDChanger::resolvepending() { statustimer_clearTick(); } +bool CDChanger::pending() { return cdtimer_pending(); } +void CDChanger::resolvepending() { cdtimer_clear(); } void CDChanger::emit(Frame *out, uint16_t peripheral) { out->peripheral_addr = peripheral; @@ -352,16 +351,16 @@ bool CDChanger::isPlaying() const { return playing; } void CDChanger::startPlaying() { static bool havePlayed = false; if (havePlayed) - AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); + media_action(MediaAction::Play); havePlayed |= true; playing = true; - statustimer_reset(); + cdtimer_reset(); } void CDChanger::stopPlaying() { - statustimer_disable(); + cdtimer_disable(); playing = false; - AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); + media_action(MediaAction::Pause); } // Serialize cd_status into the wire format. @@ -425,4 +424,10 @@ void CDChanger::normalizeState() { flags2 = 0x80; } +#ifndef NDEBUG +void CDChanger::media_action(MediaAction action) { ::media_action(action); } +bool CDChanger::media_busy() const { return ::media_busy(); }; +void CDChanger::mic_toggle() { media_mic_toggle(); }; +#endif + } // namespace avclan diff --git a/src/avclan/cdchanger.hpp b/src/avclan/cdchanger.hpp index e8a24f5..7abd1d7 100644 --- a/src/avclan/cdchanger.hpp +++ b/src/avclan/cdchanger.hpp @@ -69,6 +69,11 @@ public: void emit(Frame *out, uint16_t peripheral); void incrementTime(); bool isPlaying() const; +#ifndef NDEBUG + void media_action(MediaAction action); + bool media_busy() const; + void mic_toggle(); +#endif private: void startPlaying(); diff --git a/src/board.h b/src/avclan/hal/board.h similarity index 91% rename from src/board.h rename to src/avclan/hal/board.h index 1ae956e..af3a366 100644 --- a/src/board.h +++ b/src/avclan/hal/board.h @@ -12,7 +12,7 @@ extern "C" { void board_init(void); // Globally enable interrupts. Call after all peripherals are initialized. -void board_interruptsEnable(void); +void board_enable_interrupts(void); #ifdef __cplusplus } diff --git a/src/avclan/hal/cd_timer.h b/src/avclan/hal/cd_timer.h new file mode 100644 index 0000000..24a7f31 --- /dev/null +++ b/src/avclan/hal/cd_timer.h @@ -0,0 +1,32 @@ +// 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/media.h b/src/avclan/hal/media.h new file mode 100644 index 0000000..f68d5fd --- /dev/null +++ b/src/avclan/hal/media.h @@ -0,0 +1,32 @@ +// Copyright (C) 2026 Allen Hill +// SPDX-License-Identifier: GPL-3.0-or-later + +// Media-control HAL: emulate head-unit button presses to the audio source. This +// is device functionality (see CDChanger), so this contract is consumed only by +// the device implementation, not the app. + +#pragma once + +#include "avclan.h" + +#ifdef __cplusplus +using MediaAction = avclan::MediaAction; +extern "C" { +#else +typedef enum MediaAction MediaAction; +#endif + +// One-time hardware bring-up for the media driver. +void media_init(void); + +// Emulate a media function (button press) on the source device. +void media_action(MediaAction fn); + +#ifndef NDEBUG +bool media_mic_toggle(void); +bool media_busy(void); +#endif + +#ifdef __cplusplus +} +#endif diff --git a/src/avclan/avclan_phy.h b/src/avclan/hal/phy.h similarity index 58% rename from src/avclan/avclan_phy.h rename to src/avclan/hal/phy.h index bf53ce6..6bdc8cf 100644 --- a/src/avclan/avclan_phy.h +++ b/src/avclan/hal/phy.h @@ -19,48 +19,49 @@ typedef enum Bit Bit; #endif // One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted. -void AVCLAN_busInit(void); +void phy_init(void); // Mute/unmute device TX. "Muted" means we still listen, we just don't ACK or // transmit. -void AVCLAN_muteDevice(bool mute); -bool AVCLAN_ismuted(void); +void phy_mute(bool mute); +bool phy_is_muted(void); // True when there is activity on the bus (something is driving it). -bool AVCLAN_busActive(void); +bool phy_active(void); // Bus-transaction guard: quiesce the target's other async sources around a bus // read/send so framing isn't disturbed, then restore them. May be a no-op on a // target without such contention. -void AVCLAN_stopEvent(void); -void AVCLAN_startEvent(void); +void phy_guard_enter(void); +void phy_guard_leave(void); // Start-bit handling, factored out of read/sendframe so the framing layer holds // no bus-timing or hardware-recovery logic. -// - AVCLAN_readstartbit waits for and validates an incoming start bit, doing +// - phy_read_startbit waits for and validates an incoming start bit, doing // any target-specific bus recovery; see avclan::detail::Error::Read. -// - AVCLAN_sendstartbit acquires the bus and emits a start bit; returns false +// - phy_send_startbit acquires the bus and emits a start bit; returns false // if the bus was busy. -Read AVCLAN_readstartbit(void); -bool AVCLAN_sendstartbit(void); +Read phy_read_startbit(void); +bool phy_send_startbit(void); // Per-symbol I/O. The send* helpers return the even parity of the bits sent; -// the read* helpers return the even parity of the bits read. -void AVCLAN_sendbit(Bit bit); -void AVCLAN_sendbit_ACK(void); -uint8_t AVCLAN_readbit_ACK(void); +// the read* helpers return the even parity of the bits read. The _u8/_u16 +// suffixes name the source-operand width; `len` is how many bits (<= width). +void phy_send_bit(Bit bit); +void phy_send_ack(void); +uint8_t phy_read_ack(void); -Bit AVCLAN_sendbitsi(const uint8_t *bits, int8_t len); -Bit AVCLAN_sendbitsl(const uint16_t *bits, int8_t len); -Bit AVCLAN_sendbyte(const uint8_t *byte); +Bit phy_send_bits_u8(const uint8_t *bits, int8_t len); +Bit phy_send_bits_u16(const uint16_t *bits, int8_t len); +Bit phy_send_byte(const uint8_t *byte); -uint8_t AVCLAN_readbitsi(uint8_t *bits, uint8_t len); -uint8_t AVCLAN_readbitsl(uint16_t *bits, int8_t len); -uint8_t AVCLAN_readbyte(uint8_t *byte); +uint8_t phy_read_bits_u8(uint8_t *bits, uint8_t len); +uint8_t phy_read_bits_u16(uint16_t *bits, int8_t len); +uint8_t phy_read_byte(uint8_t *byte); #ifndef NDEBUG // Sample and dump bus bit timing over the serial link (REPL `M`). -void AVCLan_Measure(void); +void phy_measure(void); #endif #ifdef __cplusplus diff --git a/src/avclan/mediacontrol.h b/src/avclan/mediacontrol.h deleted file mode 100644 index 9d2d44d..0000000 --- a/src/avclan/mediacontrol.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2026 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -// Media-control: route/handle head-unit button presses to the audio source. - -#pragma once - -#include - -// Actions list -typedef enum : uint8_t { - MEDIA_PLAY_PAUSE = 0, - MEDIA_SKIP_FORWARD, - MEDIA_SKIP_BACKWARD, -} AVCLAN_media_fn_t; - -#ifdef __cplusplus -extern "C" { -#endif - -// One-time hardware bring-up for the media driver. -void mediacontrol_init(); - -// Emulate a button press on the source device. -void AVCLAN_mediaFunction(AVCLAN_media_fn_t fn); - -#ifndef NDEBUG -bool AVCLAN_micToggle(); -bool AVCLAN_isMediaFunctioning(); -#endif - -#ifdef __cplusplus -} -#endif diff --git a/src/avclan/peripheral.hpp b/src/avclan/peripheral.hpp index 213147c..c52aaf7 100644 --- a/src/avclan/peripheral.hpp +++ b/src/avclan/peripheral.hpp @@ -25,9 +25,18 @@ public: uint16_t address() const { return address_; }; 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(); }; +#ifndef NDEBUG + Bus &get_bus() { return bus; } +#endif + Error::Read read(Frame *in, Frame::Print print) { return bus.read(address_, in, print); }; @@ -44,7 +53,7 @@ public: using enum Action; out->reaction = 0; - if (AVCLAN_ismuted() || in->length < 3) + if (is_muted() || in->length < 3) return; // 0xFF placeholders are variant bytes filled by writing directly to @@ -137,6 +146,8 @@ public: } } +#undef PACK3 + void react(Frame *out, Error::Send err) { if (((Devs::id == out->owning_device) || ...)) ((Devs::id == out->owning_device @@ -147,8 +158,6 @@ public: out->reaction = 0; } -#undef PACK3 - template void poll_devices(F &&fun) { (poller(std::get(devices_), fun), ...); } diff --git a/src/avclan/statustimer.h b/src/avclan/statustimer.h deleted file mode 100644 index 79729ea..0000000 --- a/src/avclan/statustimer.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2026 Allen Hill -// SPDX-License-Identifier: GPL-3.0-or-later - -// ~1 Hz status-update tick interface. The app -// polls statustimer_tickPending() and clears the tick with -// statustimer_clearTick() - -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -// One-time hardware bring-up. Leaves the tick disabled. -void statustimer_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 statustimer_reset(void); - -// Enable / disable the ~1 Hz tick. -void statustimer_restore(void); -void statustimer_disable(void); - -extern volatile bool tick_pending; - -static inline bool statustimer_tickPending() { return tick_pending; } -static inline void statustimer_clearTick() { tick_pending = false; } - -#ifdef __cplusplus -} -#endif diff --git a/src/avclan/target/avr-attiny3216/CMakeLists.txt b/src/avclan/target/avr-attiny3216/CMakeLists.txt index 89d60eb..d7262b5 100644 --- a/src/avclan/target/avr-attiny3216/CMakeLists.txt +++ b/src/avclan/target/avr-attiny3216/CMakeLists.txt @@ -6,7 +6,7 @@ include(FetchContent) target_sources(avclan PRIVATE phy_avr.c media_avr.c - statustick_avr.c + cd_timer_avr.c board_avr.c) target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/avclan/target/avr-attiny3216/board_avr.c b/src/avclan/target/avr-attiny3216/board_avr.c index 889b303..b10211e 100644 --- a/src/avclan/target/avr-attiny3216/board_avr.c +++ b/src/avclan/target/avr-attiny3216/board_avr.c @@ -10,7 +10,7 @@ #include #include // _PROTECTED_WRITE -#include "board.h" +#include "hal/board.h" void board_init(void) { // Main clock prescale (CLK_PRESCALE / CLK_PRESCALE_DIV come from the build). @@ -46,4 +46,4 @@ void board_init(void) { PORTC.PIN1CTRL = PORT_ISC_INPUT_DISABLE_gc; // WOD } -void board_interruptsEnable(void) { sei(); } +void board_enable_interrupts(void) { sei(); } diff --git a/src/avclan/target/avr-attiny3216/statustick_avr.c b/src/avclan/target/avr-attiny3216/cd_timer_avr.c similarity index 85% rename from src/avclan/target/avr-attiny3216/statustick_avr.c rename to src/avclan/target/avr-attiny3216/cd_timer_avr.c index 40f7e28..2e10a55 100644 --- a/src/avclan/target/avr-attiny3216/statustick_avr.c +++ b/src/avclan/target/avr-attiny3216/cd_timer_avr.c @@ -8,7 +8,7 @@ #include #include -#include "statustimer.h" +#include "hal/cd_timer.h" // Measured wall-clock duration (in ms) of one nominal 32768-tick RTC period, // used to calibrate out the internal OSCULP32K's error. The RTC runs from @@ -29,7 +29,7 @@ static void* changer = nullptr; static void (*increment)(void *) = nullptr; static bool (*isplaying)(void *) = nullptr; -void statustimer_init(void *ptr, void (inc)(void *), bool (isplay)(void *)) { +void cdtimer_init(void *ptr, void (inc)(void *), bool (isplay)(void *)) { // Setup RTC as a ~1 sec periodic timer via the normal counter's overflow. // Use the RTC directly (not PIT) to tune the status report interval closer to // 1 sec (internal osc may be slightly off) @@ -46,7 +46,7 @@ void statustimer_init(void *ptr, void (inc)(void *), bool (isplay)(void *)) { isplaying = isplay; } -void statustimer_reset() { +void cdtimer_reset() { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { loop_until_bit_is_clear(RTC_STATUS, RTC_CNTBUSY_bp); RTC.CNT = 0; @@ -55,18 +55,18 @@ void statustimer_reset() { } } -void statustimer_restore() { +void cdtimer_restore() { if (isplaying(changer)) RTC.INTCTRL |= RTC_OVF_bm; } -void statustimer_disable() { RTC.INTCTRL &= ~RTC_OVF_bm; } +void cdtimer_disable() { RTC.INTCTRL &= ~RTC_OVF_bm; } -// Set once per overflow; consumed by the app via statustimer_tickPending(). -volatile bool tick_pending = false; +// Set once per overflow; consumed by the app via cdtimer_pending(). +volatile bool cdtimer_pending_flag = false; // Periodic interrupt with a ~1 sec period; only enabled while playing. ISR(RTC_CNT_vect) { increment(changer); - tick_pending = true; + cdtimer_pending_flag = true; RTC.INTFLAGS = RTC_OVF_bm; } diff --git a/src/avclan/target/avr-attiny3216/media_avr.c b/src/avclan/target/avr-attiny3216/media_avr.c index 837e2e7..a217695 100644 --- a/src/avclan/target/avr-attiny3216/media_avr.c +++ b/src/avclan/target/avr-attiny3216/media_avr.c @@ -6,8 +6,8 @@ #include #include -#include "media_avr.h" // mediacontrol_syncDuringMask (used by the bus guard) -#include "mediacontrol.h" +#include "media_avr.h" // media_sync_during_mask (used by the bus guard) +#include "hal/media.h" // F_CPU defined in timing_avr.h; the mic tick constants below are derived from // it (this hardware generation's TCA0/PB1 button-press implementation). @@ -31,14 +31,14 @@ static constexpr uint16_t close_thresh = #ifndef NDEBUG // Toggle PB1 and return its new level. -bool AVCLAN_micToggle() { +bool media_mic_toggle() { // Take manual control of PB1 (CMP1EN gives TCA0 control of WO1/PB1 level) TCA0.SINGLE.CTRLB &= ~TCA_SINGLE_CMP1EN_bm; VPORTB.OUT ^= PIN1_bm; return (VPORTB.OUT & PIN1_bm) != 0; } -bool AVCLAN_isMediaFunctioning() { return mic_ntoggles != 0; } +bool media_busy() { return mic_ntoggles != 0; } #endif // Begin a press waveform of `nphases` × 100 ms level segments. @@ -99,11 +99,21 @@ ISR(TCA0_OVF_vect) { mic_timer_isr_body(false); } // Emulate a transport-control button press on the source device. Each action // maps to a press-train of a given length on MIC_CONTROL. -void AVCLAN_mediaFunction(AVCLAN_media_fn_t fn) { - switch (fn) { - case MEDIA_PLAY_PAUSE: mic_pulse(1); break; // single press - case MEDIA_SKIP_FORWARD: mic_pulse(3); break; // double-press - case MEDIA_SKIP_BACKWARD: mic_pulse(5); break; // triple-press +void media_action(enum MediaAction action) { + switch (action) { + case Play: + case Pause: + case Play_Pause: mic_pulse(1); break; // single press + case Skip_Forward: + case Track_Next: mic_pulse(3); break; // double-press + case Skip_Backward: + case Track_Prev: mic_pulse(5); break; // triple-press + case Repeat: + case Repeat_Single: + case Shuffle: + case Volume_Up: + case Volume_Down: + default: break; } } @@ -113,13 +123,13 @@ void AVCLAN_mediaFunction(AVCLAN_media_fn_t fn) { // - peripheral WO1 toggles and mic_ntoggles kept in sync // - maximum frame duration is ~15ms, "early" OVF remains within acceptable // ranges for either high/low pulses -// Caller (AVCLAN_stopEvent) guarantees interrupts are disabled. -void mediacontrol_syncDuringMask() { +// Caller (phy_guard_enter) guarantees interrupts are disabled. +void media_sync_during_guard() { if (mic_ntoggles && TCA0.SINGLE.CNT >= (TCA0.SINGLE.CMP0 - close_thresh)) mic_timer_isr_body(true); } -void mediacontrol_init() { +void media_init() { // PB1 needs to be set as an output for TCA0 to set the level PORTB.DIRSET = PIN1_bm; diff --git a/src/avclan/target/avr-attiny3216/media_avr.h b/src/avclan/target/avr-attiny3216/media_avr.h index 14cdebd..5c19bac 100644 --- a/src/avclan/target/avr-attiny3216/media_avr.h +++ b/src/avclan/target/avr-attiny3216/media_avr.h @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // AVR-internal media-driver hooks, shared between media_avr.c and the bus -// transaction guard (phy_avr.c's AVCLAN_stopEvent). Not part of the public +// transaction guard (phy_avr.c's phy_guard_enter). Not part of the public // mediacontrol.h interface. #pragma once @@ -13,9 +13,9 @@ extern "C" { // Keep the TCA0 press waveform roughly in sync while a bus transaction has // masked interrupts (runs the OVF ISR body early if an overflow is imminent). -// MUST be called with interrupts disabled (from within AVCLAN_stopEvent's +// MUST be called with interrupts disabled (from within phy_guard_enter's // ATOMIC_BLOCK). -void mediacontrol_syncDuringMask(); +void media_sync_during_guard(); #ifdef __cplusplus } diff --git a/src/avclan/target/avr-attiny3216/phy_avr.c b/src/avclan/target/avr-attiny3216/phy_avr.c index 0585c4c..4caf396 100644 --- a/src/avclan/target/avr-attiny3216/phy_avr.c +++ b/src/avclan/target/avr-attiny3216/phy_avr.c @@ -9,10 +9,10 @@ #include #include -#include "avclan_phy.h" +#include "hal/phy.h" #include "com232.h" // RS232_setRxInterrupt (guard); RS232_Print (Measure) -#include "media_avr.h" // mediacontrol_syncDuringMask (guard) -#include "statustimer.h" // statustimer_enable/disable (guard) +#include "media_avr.h" // media_sync_during_mask (guard) +#include "hal/cd_timer.h" // statustimer_enable/disable (guard) // F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by // avr-libc. @@ -58,15 +58,15 @@ static inline void AVCLAN_setBusDriven() { // Returns true if device TX is muted on the AVCLAN bus (both drive pins are // configured as inputs). -bool AVCLAN_ismuted() { +bool phy_is_muted() { return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0); } // True when the bus is being driven (i.e. not idle/floating). -bool AVCLAN_busActive() { return !BUS_IS_IDLE; } +bool phy_active() { return !BUS_IS_IDLE; } // Mute device TX on AVCLAN bus -void AVCLAN_muteDevice(bool mute) { +void phy_mute(bool mute) { if (mute) { // clang-format off __asm__ __volatile__("cbi %[vporta_dir], 4; \n\t" // set as INPUT (output values ignored) @@ -99,7 +99,7 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) { return; } -void AVCLAN_sendbit(Bit bit) { +void phy_send_bit(Bit bit) { uint16_t zero_length, one_length; switch (bit) { case bit_zero: @@ -120,7 +120,7 @@ void AVCLAN_sendbit(Bit bit) { set_AVC_logic_for(1, one_length); } -void AVCLAN_sendbit_ACK() { +void phy_send_ack() { TCB1.CNT = 0; // Wait for controller to begin ACK bit @@ -131,7 +131,7 @@ void AVCLAN_sendbit_ACK() { return; } - AVCLAN_sendbit(bit_zero); + phy_send_bit(bit_zero); } /* Returns true if the peripheral sent an ACK bit. @@ -139,7 +139,7 @@ void AVCLAN_sendbit_ACK() { sync period, and allows the receiver to drive the bus (or not) to finish a "1" bit. */ -uint8_t AVCLAN_readbit_ACK() { +uint8_t phy_read_ack() { TCB1.CNT = 0; // Double reset of TCB1.CNT: here set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within) AVCLAN_setBusIdle(); // Stop driving bus @@ -160,7 +160,7 @@ uint8_t AVCLAN_readbit_ACK() { } // Send `len` bits on the AVCLAN bus; returns the even parity -Bit AVCLAN_sendbitsi(const uint8_t *bits, int8_t len) { +Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) { uint8_t b = *bits; uint8_t parity = 0; int8_t len_mod8 = 8; @@ -175,7 +175,7 @@ Bit AVCLAN_sendbitsi(const uint8_t *bits, int8_t len) { for (; len_mod8 > 0; len_mod8--) { Bit bit = (b & 0x80) != 0; parity += (uint8_t)bit; - AVCLAN_sendbit(bit); + phy_send_bit(bit); b <<= 1; } len_mod8 = 8; @@ -185,18 +185,18 @@ Bit AVCLAN_sendbitsi(const uint8_t *bits, int8_t len) { } // Send `len` bits on the AVCLAN bus; returns the even parity -Bit AVCLAN_sendbitsl(const uint16_t *bits, int8_t len) { - return AVCLAN_sendbitsi((const uint8_t *)bits + 1, len); +Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) { + return phy_send_bits_u8((const uint8_t *)bits + 1, len); } -Bit AVCLAN_sendbyte(const uint8_t *byte) { +Bit phy_send_byte(const uint8_t *byte) { uint8_t b = *byte; uint8_t parity = 0; for (uint8_t nbits = 8; nbits > 0; nbits--) { Bit bit = (b & 0x80) != 0; parity += (uint8_t)bit; - AVCLAN_sendbit(bit); + phy_send_bit(bit); b <<= 1; } return (parity & 1); @@ -225,7 +225,7 @@ ISR(TCB0_INT_vect) { } // Read `len` bits on the AVCLAN bus; returns the even parity -uint8_t AVCLAN_readbitsi(uint8_t *bits, uint8_t len) { +uint8_t phy_read_bits_u8(uint8_t *bits, uint8_t len) { cli(); READING_BYTE = 0; READING_PARITY = 0; @@ -251,20 +251,20 @@ uint8_t AVCLAN_readbitsi(uint8_t *bits, uint8_t len) { } // Read `len` bits on the AVCLAN bus; returns the even parity -uint8_t AVCLAN_readbitsl(uint16_t *bits, int8_t len) { +uint8_t phy_read_bits_u16(uint16_t *bits, int8_t len) { uint8_t parity = 0; if (len > 8) { uint8_t over = len - 8; - parity = AVCLAN_readbitsi((uint8_t *)bits + 1, over); + parity = phy_read_bits_u8((uint8_t *)bits + 1, over); len -= over; } - parity += AVCLAN_readbitsi((uint8_t *)bits + 0, len); + parity += phy_read_bits_u8((uint8_t *)bits + 0, len); return (parity & 1); } // Read a byte on the AVCLAN bus -uint8_t AVCLAN_readbyte(uint8_t *byte) { +uint8_t phy_read_byte(uint8_t *byte) { cli(); READING_BYTE = 0; READING_PARITY = 0; @@ -289,7 +289,7 @@ uint8_t AVCLAN_readbyte(uint8_t *byte) { return (parity & 1); } -void AVCLAN_busInit() { +void phy_init() { // Set pin 6 and 7 as input PORTA.DIRCLR = (PIN6_bm | PIN7_bm); // Disable input buffer; recommended when using AC @@ -321,14 +321,14 @@ void AVCLAN_busInit() { AVCLAN_setBusIdle(); - AVCLAN_muteDevice(false); // unmute AVCLAN bus TX + phy_mute(false); // unmute AVCLAN bus TX } // Wait for and validate an incoming start bit. On an over-long "driven" bus // (AC2 latched high because the bus is actually floating) this kicks PA7 hard // high to unlatch the comparator. The framing layer maps the result to its own // error reporting; no printing happens here. -Read AVCLAN_readstartbit() { +Read phy_read_startbit() { uint16_t startbitlen = TCB1.CNT = 0; while (!BUS_IS_IDLE) { startbitlen = TCB1.CNT; @@ -370,7 +370,7 @@ Read AVCLAN_readstartbit() { // Acquire the bus and emit a start bit. Returns false if another device is // already driving the bus (we can't yet do proper CSMA/CD). -bool AVCLAN_sendstartbit() { +bool phy_send_startbit() { // wait for free line TCB1.CNT = 0; while (BUS_IS_IDLE) { @@ -395,25 +395,25 @@ bool AVCLAN_sendstartbit() { // set_AVC_logic_for(1, AVCLAN_STARTBIT_LOGIC_1); // wait for end of start return false; } - AVCLAN_sendbit(bit_start); + phy_send_bit(bit_start); return true; } /* Disable non-read related interrupts (USART RX, RTC status tick, mic timer) during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain enabled. */ -void AVCLAN_stopEvent() { +void phy_guard_enter() { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - statustimer_disable(); + cdtimer_disable(); RS232_setRxInterrupt(false); - mediacontrol_syncDuringMask(); + media_sync_during_guard(); } } // Re-enable serial and periodic interrupts after a bus transaction. -void AVCLAN_startEvent() { +void phy_guard_leave() { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - statustimer_restore(); // Reenable status interrupt if currently playing + cdtimer_restore(); // Reenable status interrupt if currently playing RS232_setRxInterrupt(true); } } @@ -426,8 +426,8 @@ void AVCLAN_startEvent() { static uint16_t pulses[100]; static uint16_t periods[100]; -void AVCLan_Measure() { - AVCLAN_stopEvent(); +void phy_measure() { + phy_guard_enter(); uint8_t tmp = 0; @@ -457,6 +457,6 @@ void AVCLan_Measure() { } RS232_Print("\nDone.\n"); - AVCLAN_startEvent(); + phy_guard_leave(); } #endif diff --git a/src/sniffer.cc b/src/sniffer.cc index a0fc3d8..1a143e2 100644 --- a/src/sniffer.cc +++ b/src/sniffer.cc @@ -7,10 +7,10 @@ #include #include -#include "board.h" #include "cdchanger.hpp" #include "com232.h" #include "frame.hpp" +#include "hal/board.h" #include "peripheral.hpp" #include "queue.hpp" @@ -76,7 +76,7 @@ int main() { print_help(); while (true) { - if (AVCLAN_busActive()) { + if (peripheral.bus_is_active()) { if (auto msg = cache.pop()) { auto err = peripheral.read(msg.get(), Print{.print = printAllFrames, .binary = printBinary, @@ -171,29 +171,29 @@ int main() { break; #ifndef NDEBUG - case 'g': AVCLAN_micToggle(); break; + case 'g': peripheral.device().mic_toggle(); break; case 'p': RS232_Print("First play/pause begin ... "); - AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); - while (AVCLAN_isMediaFunctioning()) {} + peripheral.device().media_action(MediaAction::Play_Pause); + while (peripheral.device().media_busy()) {} RS232_Print("end\nSecond play/pause begin ... "); - AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE); - while (AVCLAN_isMediaFunctioning()) {} + peripheral.device().media_action(MediaAction::Play_Pause); + while (peripheral.device().media_busy()) {} RS232_Print("end\n"); break; case 's': RS232_Print("Skip begin ... "); - AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD); - while (AVCLAN_isMediaFunctioning()) {} + peripheral.device().media_action(MediaAction::Track_Next); + while (peripheral.device().media_busy()) {} RS232_Print("end\n"); break; case 'b': RS232_Print("Skip back begin ... "); - AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD); - while (AVCLAN_isMediaFunctioning()) {} + peripheral.device().media_action(MediaAction::Track_Prev); + while (peripheral.device().media_busy()) {} RS232_Print("end\n"); break; - case 'M': AVCLan_Measure(); break; + case 'M': peripheral.get_bus().measure(); break; #endif case 0x10: // Signals binary sequence incoming @@ -290,7 +290,7 @@ namespace { void Setup() { board_init(); // clock + GPIO bring-up (target-specific) RS232_Init(); - board_interruptsEnable(); + board_enable_interrupts(); } void print_help() {