Finalize C <=> C++ interface

This commit is contained in:
Allen Hill
2026-07-06 19:39:49 -07:00
parent 7ceed80128
commit 9fd8c5636e
20 changed files with 396 additions and 346 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ add_compile_options(
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
$<$<CONFIG:Debug>:-fanalyzer>)
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13")
+19 -2
View File
@@ -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 {
+134 -22
View File
@@ -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 <auto N, std::unsigned_integral T,
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Error::Send send(T bits, Trailer /*tag*/) {
const auto parity = sendbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>)
sendbits<1>(static_cast<uint8_t>(parity));
return Send{0};
};
template <auto N, std::unsigned_integral T>
requires(sizeof(T) < 3 && N < 16)
Error::Send send(T bits, with_ack_t /*tag*/, bool expect_ack) {
send<N>(bits, with_parity);
if (expect_ack && !read_ACK())
return Send::NAK;
return Send{0};
};
template <auto N, std::unsigned_integral T,
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Error::Read read(T *bits, Trailer /*tag*/) {
const auto calc_parity = readbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>) {
uint8_t read_parity;
readbits<1>(&read_parity);
if (static_cast<uint8_t>(calc_parity) != read_parity)
return Read::BAD_PARITY;
}
return Read{0};
};
template <auto N, std::unsigned_integral T, class F>
requires(sizeof(T) < 3 && N < 16)
Error::Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
if (read<N>(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 <auto N, std::unsigned_integral T>
requires(sizeof(T) < 3 && N < 16)
Error::Read read(T *bits, with_ack_t /*tag*/, bool ack) {
return read<N>(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 <auto N, class T> Bit sendbits(T bits);
template <auto N, class T> Bit readbits(T *bits);
// Temporary specializations bridging to legacy C API
// Replace with proper (single?) template when phy has been ported
template <auto N>
requires(N > 1 && N < 8)
Bit sendbits(uint8_t bits) {
return phy_send_bits_u8(&bits, N);
};
template <auto N>
requires(N <= 16)
Bit sendbits(uint16_t bits) {
return phy_send_bits_u16(&bits, N);
};
template <auto N>
requires(N < 8)
Bit readbits(uint8_t *bits) {
return static_cast<Bit>(phy_read_bits_u8(bits, N));
};
template <auto N>
requires(N <= 16)
Bit readbits(uint16_t *bits) {
return static_cast<Bit>(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<Bit>(bits & 1U)};
phy_send_bit(bit);
return bit;
};
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
return static_cast<Bit>(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<Bit>(bits & 1U)};
AVCLAN_sendbit(bit);
return bit;
};
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
return static_cast<Bit>(AVCLAN_readbyte(bits));
};
#ifndef NDEBUG
void Bus::measure() { phy_measure(); }
#endif
} // namespace avclan
+8 -117
View File
@@ -52,139 +52,30 @@
#include <type_traits>
#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 <auto N, std::unsigned_integral T,
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Error::Send send(T bits, Trailer /*tag*/) {
const auto parity = sendbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>)
sendbits<1>(static_cast<uint8_t>(parity));
return Send{0};
};
template <auto N, std::unsigned_integral T>
requires(sizeof(T) < 3 && N < 16)
Error::Send send(T bits, with_ack_t /*tag*/, bool expect_ack) {
send<N>(bits, with_parity);
if (expect_ack && !read_ACK())
return Send::NAK;
return Send{0};
};
template <auto N, std::unsigned_integral T,
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Error::Read read(T *bits, Trailer /*tag*/) {
const auto calc_parity = readbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>) {
uint8_t read_parity;
readbits<1>(&read_parity);
if (static_cast<uint8_t>(calc_parity) != read_parity)
return Read::BAD_PARITY;
}
return Read{0};
};
template <auto N, std::unsigned_integral T, class F>
requires(sizeof(T) < 3 && N < 16)
Error::Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
if (read<N>(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 <auto N, std::unsigned_integral T>
requires(sizeof(T) < 3 && N < 16)
Error::Read read(T *bits, with_ack_t /*tag*/, bool ack) {
return read<N>(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 <auto N, class T> Bit sendbits(T bits);
template <auto N, class T> Bit readbits(T *bits);
// Temporary specializations bridging to legacy C API
// Replace with proper (single?) template when phy has been ported
template <auto N>
requires(N > 1 && N < 8)
Bit sendbits(uint8_t bits) {
return AVCLAN_sendbitsi(&bits, N);
};
template <auto N>
requires(N <= 16)
Bit sendbits(uint16_t bits) {
return AVCLAN_sendbitsl(&bits, N);
};
template <auto N>
requires(N < 8)
Bit readbits(uint8_t *bits) {
return static_cast<Bit>(AVCLAN_readbitsi(bits, N));
};
template <auto N>
requires(N <= 16)
Bit readbits(uint16_t *bits) {
return static_cast<Bit>(AVCLAN_readbitsl(bits, N));
};
};
} // namespace avclan
+46 -41
View File
@@ -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
+5
View File
@@ -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();
+1 -1
View File
@@ -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
}
+32
View File
@@ -0,0 +1,32 @@
// 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
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// 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
@@ -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
-34
View File
@@ -1,34 +0,0 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
// Media-control: route/handle head-unit button presses to the audio source.
#pragma once
#include <stdint.h>
// 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
+12 -3
View File
@@ -25,9 +25,18 @@ public:
uint16_t address() const { return address_; };
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(); };
#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 <class F> void poll_devices(F &&fun) {
(poller(std::get<Devs>(devices_), fun), ...);
}
-31
View File
@@ -1,31 +0,0 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// 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
@@ -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})
+2 -2
View File
@@ -10,7 +10,7 @@
#include <avr/io.h>
#include <avr/xmega.h> // _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(); }
@@ -8,7 +8,7 @@
#include <stdint.h>
#include <util/atomic.h>
#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;
}
+22 -12
View File
@@ -6,8 +6,8 @@
#include <stdint.h>
#include <util/atomic.h>
#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;
+3 -3
View File
@@ -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
}
+34 -34
View File
@@ -9,10 +9,10 @@
#include <stdint.h>
#include <util/atomic.h>
#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
+13 -13
View File
@@ -7,10 +7,10 @@
#include <cstdint>
#include <cstring>
#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<CDChanger>().mic_toggle(); break;
case 'p':
RS232_Print("First play/pause begin ... ");
AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE);
while (AVCLAN_isMediaFunctioning()) {}
peripheral.device<CDChanger>().media_action(MediaAction::Play_Pause);
while (peripheral.device<CDChanger>().media_busy()) {}
RS232_Print("end\nSecond play/pause begin ... ");
AVCLAN_mediaFunction(MEDIA_PLAY_PAUSE);
while (AVCLAN_isMediaFunctioning()) {}
peripheral.device<CDChanger>().media_action(MediaAction::Play_Pause);
while (peripheral.device<CDChanger>().media_busy()) {}
RS232_Print("end\n");
break;
case 's':
RS232_Print("Skip begin ... ");
AVCLAN_mediaFunction(MEDIA_SKIP_FORWARD);
while (AVCLAN_isMediaFunctioning()) {}
peripheral.device<CDChanger>().media_action(MediaAction::Track_Next);
while (peripheral.device<CDChanger>().media_busy()) {}
RS232_Print("end\n");
break;
case 'b':
RS232_Print("Skip back begin ... ");
AVCLAN_mediaFunction(MEDIA_SKIP_BACKWARD);
while (AVCLAN_isMediaFunctioning()) {}
peripheral.device<CDChanger>().media_action(MediaAction::Track_Prev);
while (peripheral.device<CDChanger>().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() {