From fb130559e29a27ed745b7df08ae9a255a1ba4d70 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 22 Jun 2026 18:13:15 -0700 Subject: [PATCH] Reorganize code in prep for hardware decoupling and port to C++ Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 31 +- src/{avclandrv.h => avclan/avclan_defs.h} | 107 +- src/avclan/avclan_frame.c | 539 +++++++ src/avclan/avclan_frame.h | 72 + src/avclan/avclan_phy.c | 369 +++++ src/avclan/avclan_phy.h | 92 ++ src/avclan/avclan_protocol.c | 387 +++++ src/avclan/avclan_protocol.h | 55 + src/avclan/avclandrv.h | 33 + src/avclan/cdchanger.c | 157 +++ src/avclan/cdchanger.h | 93 ++ src/avclan/mediacontrol.c | 154 ++ src/avclan/mediacontrol.h | 46 + src/avclan/statustimer.c | 64 + src/avclan/statustimer.h | 38 + src/avclandrv.c | 1570 --------------------- 16 files changed, 2134 insertions(+), 1673 deletions(-) rename src/{avclandrv.h => avclan/avclan_defs.h} (56%) create mode 100644 src/avclan/avclan_frame.c create mode 100644 src/avclan/avclan_frame.h create mode 100644 src/avclan/avclan_phy.c create mode 100644 src/avclan/avclan_phy.h create mode 100644 src/avclan/avclan_protocol.c create mode 100644 src/avclan/avclan_protocol.h create mode 100644 src/avclan/avclandrv.h create mode 100644 src/avclan/cdchanger.c create mode 100644 src/avclan/cdchanger.h create mode 100644 src/avclan/mediacontrol.c create mode 100644 src/avclan/mediacontrol.h create mode 100644 src/avclan/statustimer.c create mode 100644 src/avclan/statustimer.h delete mode 100644 src/avclandrv.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 55f64fa..aa6dabe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,11 +75,28 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") endif() endif() +# The AVC-LAN stack as a (currently AVR-only) static library. Establishing this +# boundary now sets up the planned HAL extraction + off-target protocol testing. +add_avr_library(avclan + src/avclan/avclan_phy.c + src/avclan/avclan_frame.c + src/avclan/avclan_protocol.c + src/avclan/cdchanger.c + src/avclan/mediacontrol.c + src/avclan/statustimer.c) + add_avr_executable(mockingboard src/sniffer.c src/com232.c - src/queue.c - src/avclandrv.c) + src/queue.c) + +# avclan exports its public headers (src/avclan) to consumers, and reaches into +# src/ for sibling driver headers (com232.h, timing.h) during its own build. +target_include_directories(avclan + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/avclan + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +target_link_libraries(mockingboard avclan) # Handle libc versioning try_compile(LIBC_VERSION_TEST @@ -106,7 +123,9 @@ if(NOT LIBC_VERSION_TEST) if(NOT LIBC_VERSION_TEST) message(FATAL_ERROR "Insufficient AVR-LIBC/Microchip pack for chosen MCU '${AVR_MCU}'") else() - target_include_directories(mockingboard SYSTEM + # PUBLIC on the library so its compile inherits the device headers and + # the requirement propagates to mockingboard via linking. + target_include_directories(avclan SYSTEM PUBLIC "${attiny_atpack_SOURCE_DIR}/include") target_link_options(mockingboard PUBLIC -B "${attiny_atpack_SOURCE_DIR}/gcc/dev/${AVR_MCU}" @@ -114,7 +133,9 @@ if(NOT LIBC_VERSION_TEST) endif() endif() -target_compile_definitions(mockingboard PRIVATE +# PUBLIC on avclan so these reach both the library TUs and the executable TUs +# (mockingboard inherits them by linking avclan). +target_compile_definitions(avclan PUBLIC FREQSEL=${FREQSEL} CLK_PRESCALE=$,0x01,0x00> CLK_PRESCALE_DIV=${CLK_PRESCALE_DIV} @@ -123,7 +144,7 @@ target_compile_definitions(mockingboard PRIVATE USART_RXMODE=${USART_RXMODE} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} ) -target_compile_options(mockingboard PRIVATE +target_compile_options(avclan PUBLIC --param=min-pagesize=0 -ffunction-sections -fdata-sections diff --git a/src/avclandrv.h b/src/avclan/avclan_defs.h similarity index 56% rename from src/avclandrv.h rename to src/avclan/avclan_defs.h index 2fef1a6..c1aab06 100644 --- a/src/avclandrv.h +++ b/src/avclan/avclan_defs.h @@ -20,13 +20,15 @@ along with this program. If not, see . */ -#ifndef __AVCLANDRV_H -#define __AVCLANDRV_H +// Shared cross-cutting AVC-LAN definitions used by two or more layers +// (phy / frame / protocol / cdchanger). This is a leaf header: it must not +// include any other project header. -// AVC LAN bus on AC2 (PA6/7) -// PA6 AINP0 + -// PA7 AINN1 - -#define BUS_IS_IDLE (bit_is_clear(AC2_STATUS, AC_STATE_bp)) +#ifndef AVCLAN_DEFS_H +#define AVCLAN_DEFS_H + +#include +#include #define MAXMSGLEN 32 @@ -126,63 +128,6 @@ typedef enum : uint8_t { Report_TOC = 0xf9, } actions; -typedef enum : uint8_t { - cd_OPEN = 0x01, - cd_ERR1 = 0x02, - cd_SEEKING = 0x08, - cd_PLAYBACK = 0x10, - cd_SEEKING_TRACK = 0x20, - cd_LOADING = 0x80, -} cd_state; - -typedef enum : uint8_t { - cd_CD1 = 1 << 0, - cd_CD2 = 1 << 1, - cd_CD3 = 1 << 2, - cd_CD4 = 1 << 3, - cd_CD5 = 1 << 4, - cd_CD6 = 1 << 5, -} cd_present_t; - -typedef enum : uint8_t { - cd_DISK_RANDOM = 1 << 1, - cd_RANDOM = 1 << 2, - cd_DISK_REPEAT = 1 << 3, - cd_REPEAT = 1 << 4, - cd_DISK_SCAN = 1 << 5, - cd_SCAN = 1 << 6, -} cd_flag_t; - -typedef struct AVCLAN_CD_Status { - uint8_t cds; - uint8_t state; - uint8_t disc; - uint8_t track; // Decimal storage; serialize to BCD - uint8_t mins; // Decimal storage; serialize to BCD - uint8_t secs; // Decimal storage; serialize to BCD - uint8_t flags; - uint8_t flags2; -} AVCLAN_CD_Status_t; - -typedef enum : uint8_t { stStop = 0, stPlay = 1 } cd_modes; - -/// Message state machine -// - r_Nothing (0x00) means don't send current message -// - r_Handled means send current message and stop/finished state machine -// - All other instances mean send current message and imply the presence of -// follow-up messages within state machine -typedef enum : uint8_t { - r_Nothing = 0x00, - r_Handled, // No follow-up needed - r_StatusReport = 0x02, // Needs follow-up status report - r_NormalizeState, // cd_status needs normalized and resent - r_StartPlaying, // ~equivalent to normalizeState, but cycles to BeganPlaying - r_BeganPlaying, - r_TrackChange, // Time needs reset - r_Ejection, - r_Report_Load, -} response_t; - typedef struct print_struct { bool print : 1; // print at all bool binary : 1; // when also printing, format as binary instead of text @@ -198,38 +143,4 @@ typedef struct AVCLAN_frame_struct { uint8_t *data; } AVCLAN_frame_t; -typedef struct RFrame_struct { - response_t r; - AVCLAN_frame_t *frame; -} RFrame_t; - -void AVCLAN_init(); -void AVCLAN_muteDevice(bool mute); - -uint8_t AVCLAN_readframe(AVCLAN_frame_t *frame, log_t print); -response_t AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out); -uint8_t AVCLAN_sendframe(const AVCLAN_frame_t *frame, log_t print); -RFrame_t *AVCLAN_statemachine(RFrame_t *resp); -// uint8_t AVCLAN_tryrespond(const AVCLAN_frame_t *frame); -void AVCLAN_printframe(const AVCLAN_frame_t *frame, bool binary); -uint8_t AVCLAN_parseframe(const uint8_t *bytes, uint8_t len, - AVCLAN_frame_t *frame); -AVCLAN_frame_t *AVCLAN_getStatusFrame(); -void AVCLAN_generateStatus(AVCLAN_frame_t *status, bool is_unicast, devices to); - -bool AVCLAN_isPlaying(); -void AVCLAN_stopPlaying(); -void AVCLAN_incrementTime(); -void AVCLAN_setTime(uint8_t mins, uint8_t secs); -void AVCLAN_normalizeState(); - -#ifndef NDEBUG -bool AVCLAN_micToggle(); -void AVCLAN_micPlayPause(); -void AVCLAN_micSkipForward(); -void AVCLAN_micSkipBackward(); -bool AVCLAN_isMediaFunctioning(); -void AVCLan_Measure(); -#endif - -#endif // __AVCLANDRV_H +#endif // AVCLAN_DEFS_H diff --git a/src/avclan/avclan_frame.c b/src/avclan/avclan_frame.c new file mode 100644 index 0000000..07d59be --- /dev/null +++ b/src/avclan/avclan_frame.c @@ -0,0 +1,539 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + Portions of the following source code are based on code that is + copyright (C) 2006 Marcin Slonicki + copyright (C) 2007 Louis Frigon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include + +#include "avclan_frame.h" +#include "avclan_phy.h" +#include "cdchanger.h" +#include "com232.h" +#include "mediacontrol.h" +#include "statustimer.h" + +// F_CPU defined in timing.h and potentially needed by avr-libc (e.g. delay.h) +#include "timing.h" + +/* Disable non-read related interrupts (USART RX, PIT, TCA) during AVCLAN reads. + */ +void AVCLAN_stopEvent() { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + statustimer_disable(); + USART0.CTRLA &= ~USART_RXCIE_bm; + mediacontrol_syncDuringMask(); + } +} + +// Re-enable serial and periodic interrupts. +void AVCLAN_startEvent() { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + if (AVCLAN_isPlaying()) // Reenable status interrupt if currently playing + statustimer_enable(); + USART0.CTRLA |= USART_RXCIE_bm; + } +} + +uint8_t AVCLAN_readframe(AVCLAN_frame_t *frame, log_t print) { + struct errtype { + // Error enum is ordered such that a lower numeric value corresponds to more + // successful read + enum : uint8_t { + NO_ERROR = 0x00, + BAD_DATA_PARITY = 0x01, + BAD_LENGTH_RANGE, + BAD_LENGTH_PARITY, + BAD_PERIPHERAL_PARITY, + BAD_CONTROLLER_PARITY, + BAD_CONTROL_PARITY, + STARTBIT_TOO_SHORT, + STARTBIT_TOO_LONG, + LATCHED_COMPARATOR, + } errno; + union { + uint8_t val; // BAD_LENGTH_RANGE: the out-of-range length value + struct { + uint16_t read_val; + uint8_t parity; // received (bad) parity bit + }; + }; + } err = {0}; + + AVCLAN_stopEvent(); // disable timer1 interrupt + + uint8_t tmp = 0; + + uint16_t startbitlen = TCB1.CNT = 0; + while (!BUS_IS_IDLE) { + startbitlen = TCB1.CNT; + if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) { + err.errno = STARTBIT_TOO_LONG; + while (!BUS_IS_IDLE) { + // If bus is "driven" too long, assume the AC2 is latched (e.g. + // because the bus is actually floating). Kick it if so. + // This should prevent/resolve a flood of "STARTBIT_TOO_LONG" errors + if (TCB1.CNT > (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 3)) { + err.errno = LATCHED_COMPARATOR; + PORTA.OUTSET = PIN7_bm; // preset high before enabling the driver + PORTA.DIRSET = PIN7_bm; // drive (-) hard high + TCB1.CNT = 0; + while (!BUS_IS_IDLE && TCB1.CNT < (uint16_t)AVCLAN_BIT0_LOGIC_1) { + // Wait a max of ~6μs until bus is idle + } + PORTA.DIRCLR = PIN7_bm; // back to high-Z comparator input + PORTA.OUTCLR = PIN7_bm; + } + } + goto handle_err; + } + } + if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) { + err.errno = STARTBIT_TOO_SHORT; + // We missed the beginning of this message; wait for it to finish (bus + // continuously idle for >1 bit length) before returning, so we don't have + // multiple false-starts while the in-progress message keeps sending more + // bits. + TCB1.CNT = 0; + while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) { + if (!BUS_IS_IDLE) + TCB1.CNT = 0; + } + goto handle_err; + } + // Otherwise that was a start bit + + AVCLAN_readbits(&tmp, 1); + frame->is_unicast = tmp; + + uint8_t parity = AVCLAN_readbits(&frame->controller_addr, 12); + AVCLAN_readbits(&tmp, 1); + if (parity != (tmp &= 1)) { + err.errno = BAD_CONTROLLER_PARITY; + if (print.verbose) { + err.read_val = frame->controller_addr; + err.parity = tmp; + } + goto handle_err; + } + + parity = AVCLAN_readbits(&frame->peripheral_addr, 12); + AVCLAN_readbits(&tmp, 1); + if (parity != (tmp &= 1)) { + err.errno = BAD_PERIPHERAL_PARITY; + if (print.verbose) { + err.read_val = frame->peripheral_addr; + err.parity = tmp; + } + goto handle_err; + } + + bool shouldACK = !AVCLAN_ismuted() && (frame->peripheral_addr == DEVICE_ADDR); + + if (shouldACK) + AVCLAN_sendbit_ACK(); + else + AVCLAN_readbits(&tmp, 1); + + parity = AVCLAN_readbits(&frame->control, 4); + AVCLAN_readbits(&tmp, 1); + if (parity != (tmp &= 1)) { + err.errno = BAD_CONTROL_PARITY; + if (print.verbose) { + err.read_val = frame->control; + err.parity = tmp; + } + goto handle_err; + } else if (shouldACK) { + AVCLAN_sendbit_ACK(); + } else { + AVCLAN_readbits(&tmp, 1); + } + + parity = AVCLAN_readbyte(&frame->length); + AVCLAN_readbits(&tmp, 1); + if (parity != (tmp &= 1)) { + err.errno = BAD_LENGTH_PARITY; + if (print.verbose) { + err.read_val = frame->length; + err.parity = tmp; + } + goto handle_err; + } else if (shouldACK) { + AVCLAN_sendbit_ACK(); + } else { + AVCLAN_readbits(&tmp, 1); + } + + if (frame->length == 0 || frame->length > MAXMSGLEN) { + err.errno = BAD_LENGTH_RANGE; + err.val = frame->length; + goto handle_err; + } + + for (uint8_t i = 0; i < frame->length; i++) { + parity = AVCLAN_readbyte(&frame->data[i]); + AVCLAN_readbits(&tmp, 1); + if (parity != (tmp &= 1)) { + err.errno = BAD_DATA_PARITY; + if (print.verbose) { + err.read_val = frame->data[i]; + err.parity = tmp; + } + goto handle_err; + } else if (shouldACK) { + AVCLAN_sendbit_ACK(); + } else { + AVCLAN_readbits(&tmp, 1); + } + } + + if (false) { + handle_err:; + AVCLAN_startEvent(); + RS232_Print("ERR(read): "); + switch (err.errno) { + case LATCHED_COMPARATOR: RS232_Print("latched comparator"); break; + case STARTBIT_TOO_SHORT: RS232_Print("start bit too short"); break; + case STARTBIT_TOO_LONG: RS232_Print("start bit too long"); break; + case BAD_CONTROLLER_PARITY: + RS232_Print("reading controller addr."); + goto VERBOSE; + case BAD_PERIPHERAL_PARITY: + RS232_Print("reading peripheral addr."); + goto VERBOSE; + case BAD_CONTROL_PARITY: RS232_Print("reading control"); goto VERBOSE; + case BAD_LENGTH_PARITY: RS232_Print("reading length"); goto VERBOSE; + case BAD_LENGTH_RANGE: + RS232_Print("bad length 0x"); + RS232_PrintHex4(err.val); + break; + case BAD_DATA_PARITY: RS232_Print("reading data"); goto VERBOSE; + case NO_ERROR: + __builtin_unreachable(); + VERBOSE: + if (print.verbose) { + RS232_Print("; read 0x"); + RS232_PrintHex(err.read_val); + RS232_Print(" and got bad parity "); + RS232_PrintHex4(err.parity); + } + } + RS232_Print("\n"); + } else { + AVCLAN_startEvent(); + } + + // Only print if some data has been correctly received + if (print.print && (err.errno < STARTBIT_TOO_SHORT)) { + if (err.errno > BAD_DATA_PARITY) + frame->length = 0; + AVCLAN_printframe(frame, print.binary); + } + + return err.errno; +} + +uint8_t AVCLAN_sendframe(const AVCLAN_frame_t *frame, log_t print) { + struct errtype { + // Error enum is ordered such that a lower numeric value corresponds to more + // success + enum : uint8_t { + NO_ERROR = 0x00, + NAK_DATA = 0x01, + NAK_MESSAGE_LENGTH, + NAK_CONTROL, + NAK_ADDRESS, + BUSY, + MUTED, + } errno; + uint8_t val; + } err = {0}; + + if (AVCLAN_ismuted()) { + err.errno = MUTED; + goto handle_err; + } + + AVCLAN_stopEvent(); + + // wait for free line + TCB1.CNT = 0; + while (BUS_IS_IDLE) { + // Wait for 120% of a bit length + if (TCB1.CNT >= (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 2)) + break; + } + + // End of first loop could be due to bus being driven + TCB1.CNT = 0; + if (!BUS_IS_IDLE) { + // Some other device started sending + // Can't yet simultaneously send and recieve to do proper CSMA/CD + err.errno = BUSY; + goto handle_err; + + // Beginnings of CSMA/CD + // do { + // if (TCB1.CNT >= (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 1.2)) + // return 1; // Something's hinky; nothing is longer than the start bit + // } while (!BUS_IS_IDLE); + // if (TCB1.CNT <= (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) + // return 1; // Shouldn't be possible (waiting 2 bit lengths with idle + // bus, + // // then next bit should be a long one ie start) + // set_AVC_logic_for(1, AVCLAN_STARTBIT_LOGIC_1); // wait for end of start + // bit + } else { + AVCLAN_sendbit(bit_start); + } + AVCLAN_sendbits(&(uint8_t){frame->is_unicast}, 1); + + avclan_bit_t parity = AVCLAN_sendbits(&frame->controller_addr, 12); + AVCLAN_sendbit(parity); + + parity = AVCLAN_sendbits(&frame->peripheral_addr, 12); + AVCLAN_sendbit(parity); + + if (frame->is_unicast && !AVCLAN_readbit_ACK()) { + err.errno = NAK_ADDRESS; + goto handle_err; + } + + parity = AVCLAN_sendbits(&frame->control, 4); + AVCLAN_sendbit(parity); + + if (frame->is_unicast && !AVCLAN_readbit_ACK()) { + err.errno = NAK_CONTROL; + goto handle_err; + } + + parity = AVCLAN_sendbyte(&frame->length); // data length + AVCLAN_sendbit(parity); + + if (frame->is_unicast && !AVCLAN_readbit_ACK()) { + err.errno = NAK_MESSAGE_LENGTH; + goto handle_err; + } + + for (uint8_t i = 0; i < frame->length; i++) { + parity = AVCLAN_sendbyte(&frame->data[i]); + AVCLAN_sendbit(parity); + // Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem + // necessary (i.e. This deviates from the previous broadcast specific + // function that sent an extra `1` bit after each byte/parity) + if (frame->is_unicast && !AVCLAN_readbit_ACK()) { + err.errno = NAK_DATA; + err.val = i; + goto handle_err; + } + // else + // AVCLAN_sendbit_1(); + } + + // back to read mode + if (false) { + handle_err:; + AVCLAN_startEvent(); + RS232_Print("Error"); + switch (err.errno) { + case MUTED: RS232_Print(": Device muted"); break; + case BUSY: RS232_Print(": Busy bus"); break; + case NAK_ADDRESS: + case NAK_CONTROL: + case NAK_MESSAGE_LENGTH: + case NAK_DATA: + RS232_Print(" NAK: "); + switch (err.errno) { + case NAK_ADDRESS: RS232_Print("address"); break; + case NAK_CONTROL: RS232_Print("Control"); break; + case NAK_MESSAGE_LENGTH: RS232_Print("Message length"); break; + case NAK_DATA: + RS232_Print(" data["); + RS232_PrintDec(err.val); + RS232_Print("]"); + break; + case NO_ERROR: + case MUTED: + case BUSY: __builtin_unreachable(); + } + break; + case NO_ERROR: __builtin_unreachable(); + } + RS232_Print("\n"); + } else { + AVCLAN_startEvent(); + } + + if (print.print) + AVCLAN_printframe(frame, print.binary); + + return err.errno; +} + +void AVCLAN_printframe(const AVCLAN_frame_t *frame, bool binary) { + if (binary) { + uint8_t buffer[8]; + buffer[0] = 0x10; // Data Link Escape, signaling binary data forthcoming + buffer[1] = frame->is_unicast; + + // Send addresses in big-endian order + buffer[2] = (uint8_t)(frame->controller_addr >> 8); + buffer[3] = (uint8_t)frame->controller_addr; + buffer[4] = (uint8_t)(frame->peripheral_addr >> 8); + buffer[5] = (uint8_t)frame->peripheral_addr; + + buffer[6] = frame->control; + buffer[7] = frame->length; + RS232_sendbytes((uint8_t *)&buffer, 8); + RS232_sendbytes(frame->data, frame->length); + + buffer[0] = 0x17; // End of transmission block + buffer[1] = 0x0D; // \r + buffer[2] = 0x0A; // \n + RS232_sendbytes((uint8_t *)&buffer, 3); + } else { + RS232_PrintHex4(frame->is_unicast); + + RS232_Print(" 0x"); + RS232_PrintHex12(frame->controller_addr); + RS232_Print(" 0x"); + RS232_PrintHex12(frame->peripheral_addr); + + RS232_Print(" 0x"); + RS232_PrintHex4(frame->control); + + RS232_Print(" 0x"); + RS232_PrintHex4(frame->length); + + for (uint8_t i = 0; i < frame->length; i++) { + RS232_Print(" 0x"); + RS232_PrintHex8(frame->data[i]); + } + RS232_Print("\n"); + } +} + +uint8_t AVCLAN_parseframe(const uint8_t *bytes, uint8_t len, + AVCLAN_frame_t *frame) { + struct errtype { + enum : uint8_t { + TOO_SHORT = 0x01, + MISMATCH_LENGTH, + LENGTH_TOO_BIG, + } errno; + uint8_t val; + } err = {0}; + + if (len < sizeof(AVCLAN_frame_t)) { + err.errno = TOO_SHORT; + goto handle_err; + } + const uint8_t *last = bytes + len; + + frame->is_unicast = *bytes++; + frame->controller_addr = bytes[0] | ((uint16_t)bytes[1] << 8); + bytes += 2; + frame->peripheral_addr = bytes[0] | ((uint16_t)bytes[1] << 8); + bytes += 2; + frame->control = *bytes++; + frame->length = *bytes++; + + if (frame->length > MAXMSGLEN) { + err.errno = LENGTH_TOO_BIG; + err.val = frame->length; + goto handle_err; + } + + if ((bytes + frame->length) <= last) { + memcpy(frame->data, bytes, frame->length); + } else { + err.errno = MISMATCH_LENGTH; + goto handle_err; + } + + if (false) { + handle_err:; + RS232_Print("ERR(parse): "); + switch (err.errno) { + case TOO_SHORT: + RS232_Print("not enough bytes too fill AVCLAN frame"); + break; + case MISMATCH_LENGTH: + RS232_Print("frame->length is longer than remaining data"); + break; + case LENGTH_TOO_BIG: + RS232_Print("frame->length exceeds MAXMSGLEN: 0x"); + RS232_PrintHex8(err.val); + break; + default: break; + } + RS232_Print("\n"); + } + + return err.errno; +} + +#ifndef NDEBUG + // Only used immediately below + #define XSTR(x) #x + #define STR(x) XSTR(x) + +uint16_t pulses[100]; +uint16_t periods[100]; + +void AVCLan_Measure() { + AVCLAN_stopEvent(); + + uint8_t tmp = 0; + + RS232_Print( + "Timing config: F_CPU=" STR(F_CPU) ", TCB_CLKSEL=" STR(TCB_CLKSEL) "\n"); + RS232_Print("Sampling bit (pulse-width and period) timing...\n"); + + for (uint8_t n = 0; n < 100; n++) { + while (pulse_count == tmp) {} + pulses[n] = pulsewidth; + periods[n] = period; + tmp = pulse_count; + } + + RS232_Print("Pulses:\n"); + for (uint8_t i = 0; i < 100; i++) { + RS232_PrintHex8((uint8_t)(pulses[i] >> 8)); + RS232_PrintHex8((uint8_t)pulses[i]); + RS232_Print("\n"); + } + + RS232_Print("Periods:\n"); + for (uint8_t i = 0; i < 100; i++) { + RS232_PrintHex8((uint8_t)(periods[i] >> 8)); + RS232_PrintHex8((uint8_t)periods[i]); + RS232_Print("\n"); + } + RS232_Print("\nDone.\n"); + + AVCLAN_startEvent(); +} +#endif diff --git a/src/avclan/avclan_frame.h b/src/avclan/avclan_frame.h new file mode 100644 index 0000000..ab4c869 --- /dev/null +++ b/src/avclan/avclan_frame.h @@ -0,0 +1,72 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +-------------------------------------------------------------------------------------- + + AVC LAN Frame Format + │ Bits │ Description + ──────────────────────────────────────── + | 1 │ Start bit + | 1 │ Direct/broadcast + | 12 │ Controller address + | 1 │ Parity + | 12 │ Peripheral address + | 1 │ Parity + | 1 │ *Acknowledge* (read below) + | 4 │ Control + | 1 │ Parity + | 1 │ *Acknowledge* + | 8 │ Message length (n) + | 1 │ Parity + | 1 │ *Acknowledge* + ──────── + | 8 │ Data + | 1 │ Parity + | 1 │ *Acknowledge* + *repeat `n` times* + + No acknowledge bits are sent for broadcast frames. + +-------------------------------------------------------------------------------------- +*/ + +#ifndef AVCLAN_FRAME_H +#define AVCLAN_FRAME_H + +#include + +#include "avclan_defs.h" + +uint8_t AVCLAN_readframe(AVCLAN_frame_t *frame, log_t print); +uint8_t AVCLAN_sendframe(const AVCLAN_frame_t *frame, log_t print); +void AVCLAN_printframe(const AVCLAN_frame_t *frame, bool binary); +uint8_t AVCLAN_parseframe(const uint8_t *bytes, uint8_t len, + AVCLAN_frame_t *frame); + +// Bus-transaction guard: quiesce the other async sources (USART RX, the RTC +// status tick, the mic timer) around a bus read/send so framing isn't disturbed. +// NOTE (temporary): these couple the frame layer to the statustimer / +// mediacontrol / cdchanger modules; this intermingling is accepted pending the +// RP2350 port rework. TCB0 must remain enabled. +void AVCLAN_stopEvent(); +void AVCLAN_startEvent(); + +#ifndef NDEBUG +void AVCLan_Measure(); +#endif + +#endif // AVCLAN_FRAME_H diff --git a/src/avclan/avclan_phy.c b/src/avclan/avclan_phy.c new file mode 100644 index 0000000..8ad66fc --- /dev/null +++ b/src/avclan/avclan_phy.c @@ -0,0 +1,369 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + Portions of the following source code are based on code that is + copyright (C) 2006 Marcin Slonicki + copyright (C) 2007 Louis Frigon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +-------------------------------------------------------------------------------------- + + AVC LAN Theory + + The AVC LAN bus is an implementation of the IEBus (mode 1) which is a + differential signal; IEBus is electrically (but not logically) compatible with + CAN bus. + + - Logical `1`: Potential difference between bus lines (BUS+ pin and BUS– pin) + is 20 mV or lower (floating). + - Logical `0`: Potential difference between bus lines (BUS+ pin and BUS– pin) + is 120 mV or higher (driving). + + A nominal bit length is 39 us, composed of 3 periods: preparation, + synchronization, data. + + Figure 1. AVCLAN Bus bit format + + │ Prep │<─ Sync ─>│<─ Data ─>│ ... + Driving (logical `0`) ╭──────────╮──────────╮ + │ │ │ + Floating (logical `1`) ─────────╯ ╰──────────╰───────── + │ 6 μs │── 19 μs ─│─ 13 μs ──│ + + The logical value during the data period signifies the bit value, e.g. a bit + `0` continues the logical `0` (high potential difference between bus lines) of + the sync period thru the data period, and a bit `1` has a logical `1` + (low/floating potential between bus lines) during the data period. Using the + TCB pulse-width and frequency measure mode, the total bit length differs for + bit `1` and `0`; detailed bit timing can be found in "timing.h". The bus + idles at low potential (floating). + + A start bit is nominally 169 us high followed by 20 us low. + + A bit `0` is dominant on the bus, which is a design choice that affects + bit/interpretation: + - Low addresses have priority upon transmission conflicts + - The broadcast bit is `1` (floating, no effort) for normal communication + - For acknowledge bits, the receiver extends the logical '0' of the sync + period to the length of a normal bit `0`. Hence, a NAK (bit `1`) is + literally the absence of an ACK. + +-------------------------------------------------------------------------------------- +*/ + +#include +#include +#include +#include + +#include "avclan_phy.h" + +// F_CPU defined in timing.h and potentially needed by avr-libc (e.g. delay.h) +#include "timing.h" + +// Name difference between avr-libc and Microchip pack +#if defined(EVSYS_ASYNCCH00_bm) + #define EVSYS_ASYNCCH0_0_bm EVSYS_ASYNCCH00_bm +#endif + +#define READING_BYTE GPIOR1 +#define READING_NBITS GPIOR2 +#define READING_PARITY GPIOR3 + +#define TCB_CNTMODE TCB_CNTMODE_PW_gc + +volatile uint16_t pulsewidth; + +#ifndef NDEBUG +volatile uint8_t pulse_count = 0; +volatile uint16_t period = 0; +#endif + +// clang-format off +static inline void AVCLAN_setBusIdle() { + __asm__ __volatile__( + "cbi %[vporta_out], 4; \n\t" + "sbi %[vportc_out], 0; \n\t" + ::[vporta_out] "I"(_SFR_IO_ADDR(VPORTA_OUT)), + [vportc_out] "I"(_SFR_IO_ADDR(VPORTC_OUT))); +} +static inline void AVCLAN_setBusDriven() { + __asm__ __volatile__( + "sbi %[vporta_out], 4; \n\t" + "cbi %[vportc_out], 0; \n\t" + ::[vporta_out] "I"(_SFR_IO_ADDR(VPORTA_OUT)), + [vportc_out] "I"(_SFR_IO_ADDR(VPORTC_OUT))); +} +// clang-format on + +// Mute device TX on AVCLAN bus +void AVCLAN_muteDevice(bool mute) { + if (mute) { + // clang-format off + __asm__ __volatile__("cbi %[vporta_dir], 4; \n\t" // set as INPUT (output values ignored) + "cbi %[vportc_dir], 0; \n\t" // set as INPUT (output values ignored) + :: + [vporta_dir] "I"(_SFR_IO_ADDR(VPORTA_DIR)), + [vportc_dir] "I"(_SFR_IO_ADDR(VPORTC_DIR))); + // clang-format on + } else { + // clang-format off + __asm__ __volatile__("sbi %[vporta_dir], 4; \n\t" + "sbi %[vportc_dir], 0; \n\t" + :: + [vporta_dir] "I"(_SFR_IO_ADDR(VPORTA_DIR)), + [vportc_dir] "I"(_SFR_IO_ADDR(VPORTC_DIR))); + // clang-format on + } +} + +// Set AVC bus to `val` (logical 1 or 0) for `period` ticks of TCB1 +static void set_AVC_logic_for(uint8_t val, uint16_t period) { + TCB1.CNT = 0; + if (val) { + AVCLAN_setBusIdle(); // idle bus is logical 1 + } else { + AVCLAN_setBusDriven(); + } + while (TCB1.CNT <= period) {}; + + return; +} + +void AVCLAN_sendbit(avclan_bit_t bit) { + uint16_t zero_length, one_length; + switch (bit) { + case bit_zero: + zero_length = AVCLAN_BIT0_LOGIC_0; + one_length = AVCLAN_BIT0_LOGIC_1; + break; + case bit_one: + zero_length = AVCLAN_BIT1_LOGIC_0; + one_length = AVCLAN_BIT1_LOGIC_1; + break; + case bit_start: + zero_length = AVCLAN_STARTBIT_LOGIC_0; + one_length = AVCLAN_STARTBIT_LOGIC_1; + break; + default: __builtin_unreachable(); + } + set_AVC_logic_for(0, zero_length); + set_AVC_logic_for(1, one_length); +} + +void AVCLAN_sendbit_ACK() { + TCB1.CNT = 0; + + // Wait for controller to begin ACK bit + while (BUS_IS_IDLE) { + // Wait for approx the length of a bit; any longer and something has clearly + // gone wrong + if (TCB1.CNT >= AVCLAN_BIT_LENGTH_MAX) + return; + } + + AVCLAN_sendbit(bit_zero); +} + +/* Returns true if the peripheral sent an ACK bit. + An ACK bit is a cooperative bit, where the sender starts (drives the bus) a + sync period, and allows the receiver to drive the bus (or not) to finish a "1" + bit. +*/ +uint8_t AVCLAN_readbit_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 + + while (true) { + if (!BUS_IS_IDLE && (TCB1.CNT > AVCLAN_READBIT_THRESHOLD)) + break; // ACK + if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) + return 0; // NAK + } + + // Check/wait in case we get here before peripheral finishes ACK bit + while (!BUS_IS_IDLE) { + if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) + return 0; // NAK + } + return 1; +} + +// Send `len` bits on the AVCLAN bus; returns the even parity +avclan_bit_t AVCLAN_sendbitsi(const uint8_t *bits, int8_t len) { + uint8_t b = *bits; + uint8_t parity = 0; + int8_t len_mod8 = 8; + + if (len & 0x7) { + len_mod8 = (int8_t)(len & 0x7); + b <<= (uint8_t)(8 - len_mod8); + } + + while (len > 0) { + len -= len_mod8; + for (; len_mod8 > 0; len_mod8--) { + avclan_bit_t bit = (b & 0x80) != 0; + parity += (uint8_t)bit; + AVCLAN_sendbit(bit); + b <<= 1; + } + len_mod8 = 8; + b = *--bits; + } + return (parity & 1); +} + +// Send `len` bits on the AVCLAN bus; returns the even parity +avclan_bit_t AVCLAN_sendbitsl(const uint16_t *bits, int8_t len) { + return AVCLAN_sendbitsi((const uint8_t *)bits + 1, len); +} + +avclan_bit_t AVCLAN_sendbyte(const uint8_t *byte) { + uint8_t b = *byte; + uint8_t parity = 0; + + for (uint8_t nbits = 8; nbits > 0; nbits--) { + avclan_bit_t bit = (b & 0x80) != 0; + parity += (uint8_t)bit; + AVCLAN_sendbit(bit); + b <<= 1; + } + return (parity & 1); +} + +ISR(TCB0_INT_vect) { + // #ifndef NDEBUG + // pulse_count++; + // // PW mode fires on falling edge; measure period as TCB1 delta between + // // consecutive falling edges (equivalent to FRQPW's rising-to-rising). + // static uint16_t last_tcb1 = 0; + // uint16_t cur_tcb1 = TCB1.CNT; + // period = cur_tcb1 - last_tcb1; + // last_tcb1 = cur_tcb1; + // #endif + + READING_BYTE <<= 1; + // If the logical `0` pulse was less than the sync + data period threshold, + // bit was a 1 + pulsewidth = TCB0.CCMP; + if (pulsewidth < (uint16_t)AVCLAN_READBIT_THRESHOLD) { + READING_BYTE++; + READING_PARITY++; + } + READING_NBITS--; +} + +// Read `len` bits on the AVCLAN bus; returns the even parity +uint8_t AVCLAN_readbitsi(uint8_t *bits, uint8_t len) { + cli(); + READING_BYTE = 0; + READING_PARITY = 0; + READING_NBITS = len; + sei(); + + TCB1.CNT = 0; + while (READING_NBITS) { + // 200% the duration of `len` bits + if (TCB1.CNT > ((uint16_t)AVCLAN_BIT_LENGTH_MAX * 2 * len)) { + READING_BYTE = 0; + READING_PARITY = 0; + break; // Should have finished by now; something's wrong + } + }; + + cli(); + *bits = READING_BYTE; + uint8_t parity = READING_PARITY; + sei(); + + return (parity & 1); +} + +// Read `len` bits on the AVCLAN bus; returns the even parity +uint8_t AVCLAN_readbitsl(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); + len -= over; + } + parity += AVCLAN_readbitsi((uint8_t *)bits + 0, len); + + return (parity & 1); +} + +// Read a byte on the AVCLAN bus +uint8_t AVCLAN_readbyte(uint8_t *byte) { + cli(); + READING_BYTE = 0; + READING_PARITY = 0; + READING_NBITS = 8; + sei(); + + TCB1.CNT = 0; + while (READING_NBITS) { + // 200% the length of a byte + if (TCB1.CNT > ((uint16_t)AVCLAN_BIT_LENGTH_MAX * 2 * 8)) { + READING_BYTE = 0; + READING_PARITY = 0; + break; // Should have finished by now; something's wrong + } + }; + + cli(); + *byte = READING_BYTE; + uint8_t parity = READING_PARITY; + sei(); + + return (parity & 1); +} + +void AVCLAN_phyInit() { + // Set pin 6 and 7 as input + PORTA.DIRCLR = (PIN6_bm | PIN7_bm); + // Disable input buffer; recommended when using AC + PORTA.PIN6CTRL = PORT_ISC_INPUT_DISABLE_gc; + // PA7/AINN1(-) additionally gets a pull-up to help prevent the comparator + // latching high (ie false "driven" bus) + PORTA.PIN7CTRL = PORT_PULLUPEN_bm | PORT_ISC_INPUT_DISABLE_gc; + + // Analog comparator config + AC2.CTRLA = AC_OUTEN_bm | AC_HYSMODE_25mV_gc | AC_ENABLE_bm; + + PORTB.DIRSET = PIN2_bm; // Enable AC2 OUT for LED + PORTB.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc; // Output only + + // Set AC2 to generate events on async channel 0 + EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_AC2_OUT_gc; + EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // USER0 is TCB0 + + // TCB0 for read bit timing + TCB0.CTRLB = TCB_CNTMODE; + TCB0.INTCTRL = TCB_CAPT_bm; + TCB0.EVCTRL = TCB_CAPTEI_bm; + TCB0.CTRLA = TCB_CLKSEL | TCB_ENABLE_bm; + + // TCB1 for send bit timing + TCB1.CTRLB = TCB_CNTMODE_INT_gc; + TCB1.CCMP = 0xFFFF; + TCB1.CTRLA = TCB_CLKSEL | TCB_ENABLE_bm; + + AVCLAN_setBusIdle(); + + AVCLAN_muteDevice(false); // unmute AVCLAN bus TX +} diff --git a/src/avclan/avclan_phy.h b/src/avclan/avclan_phy.h new file mode 100644 index 0000000..b685d76 --- /dev/null +++ b/src/avclan/avclan_phy.h @@ -0,0 +1,92 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + Portions of the following source code are based on code that is + copyright (C) 2006 Marcin Slonicki + copyright (C) 2007 Louis Frigon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// AVC-LAN PHY: bus bit-banging via TCB timers + analog comparator AC2. +// This is the lowest layer and has no dependencies on the higher layers +// (frame / protocol / cdchanger) — keep it that way. + +#ifndef AVCLAN_PHY_H +#define AVCLAN_PHY_H + +#include +#include +#include + +#include "avclan_defs.h" + +// AVC LAN bus on AC2 (PA6/7) +// PA6 AINP0 + +// PA7 AINN1 - +#define BUS_IS_IDLE (bit_is_clear(AC2_STATUS, AC_STATE_bp)) + +typedef enum avclan_bit : uint8_t { + bit_zero = 0x00, + bit_one = 0x01, + bit_start = 0x10 +} avclan_bit_t; + +// One-time hardware bring-up for the PHY: AC2, EVSYS, TCB0/TCB1, the AC inputs +// (PA6/7) and AC2-OUT LED (PB2). Leaves the bus idle and TX unmuted. +void AVCLAN_phyInit(); + +// Returns true if device TX is muted on AVCLAN bus +static inline bool AVCLAN_ismuted() { + return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0); +} + +// Mute device TX on AVCLAN bus +void AVCLAN_muteDevice(bool mute); + +void AVCLAN_sendbit(avclan_bit_t bit); +void AVCLAN_sendbit_ACK(); +uint8_t AVCLAN_readbit_ACK(); + +avclan_bit_t AVCLAN_sendbitsi(const uint8_t *bits, int8_t len); +avclan_bit_t AVCLAN_sendbitsl(const uint16_t *bits, int8_t len); +avclan_bit_t AVCLAN_sendbyte(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); + +#define AVCLAN_sendbits(bits, len) \ + _Generic((bits), \ + const uint16_t *: AVCLAN_sendbitsl, \ + uint16_t *: AVCLAN_sendbitsl, \ + const uint8_t *: AVCLAN_sendbitsi, \ + uint8_t *: AVCLAN_sendbitsi)(bits, len) + +#define AVCLAN_readbits(bits, len) \ + _Generic((bits), \ + const uint16_t *: AVCLAN_readbitsl, \ + uint16_t *: AVCLAN_readbitsl, \ + const uint8_t *: AVCLAN_readbitsi, \ + uint8_t *: AVCLAN_readbitsi)(bits, len) + +#ifndef NDEBUG +// Bit-timing capture, populated by the TCB0 capture ISR; read by AVCLan_Measure. +extern volatile uint16_t pulsewidth; +extern volatile uint8_t pulse_count; +extern volatile uint16_t period; +#endif + +#endif // AVCLAN_PHY_H diff --git a/src/avclan/avclan_protocol.c b/src/avclan/avclan_protocol.c new file mode 100644 index 0000000..e2a987e --- /dev/null +++ b/src/avclan/avclan_protocol.c @@ -0,0 +1,387 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + Portions of the following source code are based on code that is + copyright (C) 2006 Marcin Slonicki + copyright (C) 2007 Louis Frigon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include "avclan_phy.h" // AVCLAN_ismuted +#include "avclan_protocol.h" +#include "cdchanger.h" +#include "mediacontrol.h" +#include "statustimer.h" + +#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) + +static const uint8_t cdloading_resp[] = {dev_CD_CHANGER, + dev_STATUS, + Loading_Status_Report, + 0x00, + 0x01, + 0x00, + 0x01, + 0x00, + 0x01, + 0x02}; + +response_t AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { + response_t respond = r_Nothing; + + if (AVCLAN_ismuted() || in->length < 3) + return respond; + + // 0xFF placeholders are variant bytes filled by writing directly to + // out->data[N] after memcpy. + static const uint8_t lancheck_resp[] = {0x00, dev_COMM_CTRL, dev_LAN, 0xFF, + 0xFF}; + static const uint8_t function_change_resp[] = {0x00, dev_CD_CHANGER, + dev_COMM_v1, 0xFF, 0x01}; + + out->controller_addr = DEVICE_ADDR; + out->control = 0xF; + + const uint8_t *data = in->data; + const uint8_t b0 = *data++; + const uint8_t b1 = *data++; + const uint8_t b2 = *data++; + uint8_t b3 = 0; + if (in->length > 3) // the shortest known/valid messages are 3 bytes long + b3 = *data++; + + if (!in->is_unicast) { + // Broadcast: bytes are (from, to, action, [extra...]). + // peripheral_addr unchecked — always 0xFFF or 0x1FF in known traffic. + switch (PACK3(b0, b1, b2)) { + case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Scan_Req): + out->length = sizeof(lancheck_resp); + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); + out->data[3] = Lancheck_Scan_Resp; + out->data[4] = 0x01; + respond = r_Handled; + break; + case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Req): + out->length = sizeof(lancheck_resp); + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); + out->data[3] = Lancheck_Resp; + out->data[4] = 0x00; + respond = r_Handled; + break; + case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_End_Req): + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + out->length = sizeof(lancheck_resp) - 1; + memcpy(out->data, lancheck_resp, out->length); + out->data[3] = Lancheck_End_Resp; + respond = r_Handled; + break; + case PACK3(dev_COMM_v1, dev_COMM_CTRL, Current_Function): + case PACK3(dev_COMM_v2, dev_COMM_CTRL, Current_Function): + if ((b3 == dev_CD_CHANGER) && !AVCLAN_isPlaying()) { + if (cd_status.mins > 99) + cd_status.mins = 0; + if (cd_status.secs > 99) + cd_status.secs = 0; + cd_status.state = cd_SEEKING | cd_SEEKING_TRACK; + cd_status.flags2 = 0xc0; + AVCLAN_generateStatus(out, true, dev_STATUS); + respond = r_StartPlaying; + } + break; + case PACK3(dev_COMM_v1, dev_COMM_CTRL, Ping_Req): + case PACK3(dev_COMM_v2, dev_COMM_CTRL, Ping_Req): { + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + const uint8_t ping_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, + Ping_Resp, 0xFF, b3}; + out->length = sizeof(ping_resp); + memcpy(out->data, ping_resp, sizeof(ping_resp)); + respond = r_Handled; + break; + } + case PACK3(dev_COMM_v1, dev_COMM_CTRL, List_Functions_Req): + case PACK3(dev_COMM_v2, dev_COMM_CTRL, List_Functions_Req): { + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + const uint8_t list_functions_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, + List_Functions_Resp, + dev_CD_CHANGER}; + out->length = sizeof(list_functions_resp); + memcpy(out->data, list_functions_resp, sizeof(list_functions_resp)); + respond = r_Handled; + break; + } + // case Restart_Lan: not handled + } + } else if (in->peripheral_addr == DEVICE_ADDR && b0 == 0x00) { + // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). + switch (PACK3(b1, b2, b3)) { + case PACK3(dev_COMM_v1, dev_CD_CHANGER, Enable_Function_Req): + [[fallthrough]]; + case PACK3(dev_COMM_v2, dev_CD_CHANGER, Enable_Function_Req): + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + out->length = sizeof(function_change_resp); + memcpy(out->data, function_change_resp, sizeof(function_change_resp)); + out->data[3] = Enable_Function_Resp; + cd_status.state = 0; + cd_status.flags2 = 0x80; + respond = r_StatusReport; + break; + case PACK3(dev_COMM_v1, dev_CD_CHANGER, Disable_Function_Req): + [[fallthrough]]; + case PACK3(dev_COMM_v2, dev_CD_CHANGER, Disable_Function_Req): + // No change/response needed if we're already not playing + if (AVCLAN_isPlaying()) { + AVCLAN_stopPlaying(); + out->length = sizeof(function_change_resp); + memcpy(out->data, function_change_resp, sizeof(function_change_resp)); + out->data[3] = Disable_Function_Resp; + cd_status.state = 0; + cd_status.flags2 = 0x80; + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + respond = r_StatusReport; + } + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Eject): { + // "Eject" label is multiply wrong; proper meaning unclear: + // - First observed on initial multiple presses of "CD" button, + // triggering (after {0x00, dev_CD_CHANGER, dev_COMM_v1, Insertion, + // 0x01} response) proper activation of mockingboard/cd-changer. + // - Subsequently observed when pressing (technically + // releasing?) the fast-forward button and rewind + if (cd_status.state | cd_SEEKING) { // FF/RW button released + cd_status.state &= ~cd_SEEKING; + } else { + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + { + const uint8_t msg[] = {0x00, dev_CD_CHANGER, dev_CMD_SW, Insertion, + 0x01}; + out->length = sizeof(msg); + memcpy(out->data, msg, sizeof(msg)); + } + respond = r_Handled; + } + break; + } + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Initial_Report_Request): + [[fallthrough]]; + case PACK3(dev_STATUS, dev_CD_CHANGER, Initial_Report_Request): { + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + + // No knowledge/understanding of field meaning/interpretation + const uint8_t cdinitreport_resp[] = { + 0x00, dev_CD_CHANGER, b1, Initial_Report_Response, 0x01, 0x31, + 0x10, 0x01, 0x01}; + out->length = sizeof(cdinitreport_resp); + memcpy(&out->data[1], cdinitreport_resp, sizeof(cdinitreport_resp)); + respond = r_Handled; + break; + } + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Playback_Request): [[fallthrough]]; + case PACK3(dev_STATUS, dev_CD_CHANGER, Playback_Request): + out->data[0] = 0x00; + out->data[1] = dev_CD_CHANGER; + out->data[2] = b1; + out->data[3] = Playback_Report; + out->length = sizeof(AVCLAN_CD_Status_t) + 4; + serializeCDStatus(&out->data[4]); + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + respond = r_Handled; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Loading_Request2): [[fallthrough]]; + case PACK3(dev_STATUS, dev_CD_CHANGER, Loading_Request2): + out->data[0] = 0x00; + out->length = sizeof(cdloading_resp) + 1; + memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp)); + out->data[2] = b1; + out->data[3] = Loading_Response2; + out->is_unicast = true; + out->peripheral_addr = HU_ADDR; + respond = r_Handled; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Up): + cd_status.state = cd_SEEKING_TRACK; + if (cd_status.track < 98) + ++cd_status.track; + else + cd_status.track = 1; + cd_status.mins = 0xff; + cd_status.secs = 0x7f; + cd_status.flags2 = 0xc0; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + AVCLAN_micSkipForward(); + respond = r_TrackChange; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Down): + cd_status.state = cd_SEEKING_TRACK; + // Track down returns to track beginning if in ~middle of song + if (cd_status.mins == 0 && cd_status.secs < 0x05) { + if (cd_status.track > 1) + --cd_status.track; + else + cd_status.track = 99; + } + cd_status.mins = 0xff; + cd_status.secs = 0x7f; + cd_status.flags2 = 0xc0; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + AVCLAN_micSkipBackward(); + respond = r_TrackChange; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Fast_Forward): { + cd_status.state |= cd_SEEKING; + cd_status.secs += 15; + if (cd_status.secs > 60) { + cd_status.secs -= 60; + ++cd_status.mins; + } + AVCLAN_generateStatus(out, true, dev_CMD_SW); + AVCLAN_micSkipForward(); + statustimer_reset(); // Skipped to a whole/round sec; ensure next tick is + // ~1 sec from now + respond = r_Handled; + break; + } + case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Rewind): { + cd_status.state |= cd_SEEKING; + if (cd_status.secs < 15) { + if (cd_status.mins > 0) { + uint8_t d = 15 - cd_status.secs; + cd_status.secs = 60 - d; + --cd_status.mins; + } else { + cd_status.mins = 0; + cd_status.secs = 0; + } + } else + cd_status.secs -= 15; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + AVCLAN_micSkipBackward(); + statustimer_reset(); // Skipped to a whole/round sec; ensure next tick is + // ~1 sec from now + respond = r_Handled; + break; + } + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Random): + cd_status.flags |= cd_RANDOM; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Random): + cd_status.flags &= ~cd_RANDOM; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Repeat): + cd_status.flags |= cd_REPEAT; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Repeat): + cd_status.flags &= ~cd_REPEAT; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Disk_Random): + cd_status.flags |= cd_DISK_RANDOM; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Disk_Random): + cd_status.flags &= ~cd_DISK_RANDOM; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Disk_Repeat): + cd_status.flags |= cd_DISK_REPEAT; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Disk_Repeat): + cd_status.flags &= ~cd_DISK_REPEAT; + AVCLAN_generateStatus(out, true, dev_CMD_SW); + respond = r_StatusReport; + break; + } + } + + return respond; +} + +#undef PACK3 + +RFrame_t *AVCLAN_statemachine(RFrame_t *resp) { + AVCLAN_frame_t *out = resp->frame; + switch (resp->r) { + case r_Ejection: { + const uint8_t play[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, + Insertion, dev_CD_CHANGER, 0x01}; + out->length = sizeof(play); + memcpy(out->data, play, sizeof(play)); + } + resp->r = r_Report_Load; + break; + case r_Report_Load: + out->is_unicast = false; + out->peripheral_addr = 0x1FF; + out->length = sizeof(cdloading_resp) + 1; + memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); + out->data[1] = dev_STATUS; + out->data[2] = Loading_Status_Report; + resp->r = r_Handled; + break; + case r_TrackChange: + AVCLAN_setTime(0x00, 0x00); + statustimer_reset(); // Skipped to a whole/round sec; ensure next tick is + // ~1 sec from now + [[fallthrough]]; + case r_NormalizeState: + AVCLAN_normalizeState(); + AVCLAN_generateStatus(out, true, dev_STATUS); + resp->r = r_Handled; + break; + case r_StartPlaying: + AVCLAN_normalizeState(); + AVCLAN_generateStatus(out, true, dev_STATUS); + resp->r = r_BeganPlaying; + break; + case r_BeganPlaying: + AVCLAN_startPlaying(); // only start PIT after normalizing state + resp->r = r_Nothing; + break; + case r_StatusReport: + AVCLAN_generateStatus(out, true, dev_STATUS); + resp->r = r_Handled; + break; + case r_Handled: [[fallthrough]]; + case r_Nothing: [[fallthrough]]; + default: resp->r = r_Nothing; + } + return resp; +} diff --git a/src/avclan/avclan_protocol.h b/src/avclan/avclan_protocol.h new file mode 100644 index 0000000..e0cf1d1 --- /dev/null +++ b/src/avclan/avclan_protocol.h @@ -0,0 +1,55 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// AVC-LAN message dispatcher and response state machine. Currently mixes +// generic peripheral-level handling with CD-changer device-specific handling; +// the device-specific cases will later migrate into cdchanger. + +#ifndef AVCLAN_PROTOCOL_H +#define AVCLAN_PROTOCOL_H + +#include + +#include "avclan_defs.h" + +/// Message state machine +// - r_Nothing (0x00) means don't send current message +// - r_Handled means send current message and stop/finished state machine +// - All other instances mean send current message and imply the presence of +// follow-up messages within state machine +typedef enum : uint8_t { + r_Nothing = 0x00, + r_Handled, // No follow-up needed + r_StatusReport = 0x02, // Needs follow-up status report + r_NormalizeState, // cd_status needs normalized and resent + r_StartPlaying, // ~equivalent to normalizeState, but cycles to BeganPlaying + r_BeganPlaying, + r_TrackChange, // Time needs reset + r_Ejection, + r_Report_Load, +} response_t; + +typedef struct RFrame_struct { + response_t r; + AVCLAN_frame_t *frame; +} RFrame_t; + +response_t AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out); +RFrame_t *AVCLAN_statemachine(RFrame_t *resp); + +#endif // AVCLAN_PROTOCOL_H diff --git a/src/avclan/avclandrv.h b/src/avclan/avclandrv.h new file mode 100644 index 0000000..d7da565 --- /dev/null +++ b/src/avclan/avclandrv.h @@ -0,0 +1,33 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Umbrella header for the avclan library. Consumers (e.g. the app in +// src/sniffer.c) include this single header to pull in the whole AVC-LAN stack. + +#ifndef __AVCLANDRV_H +#define __AVCLANDRV_H + +#include "avclan_defs.h" +#include "avclan_frame.h" +#include "avclan_phy.h" +#include "avclan_protocol.h" +#include "cdchanger.h" +#include "mediacontrol.h" +#include "statustimer.h" + +#endif // __AVCLANDRV_H diff --git a/src/avclan/cdchanger.c b/src/avclan/cdchanger.c new file mode 100644 index 0000000..16ab2c2 --- /dev/null +++ b/src/avclan/cdchanger.c @@ -0,0 +1,157 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + Portions of the following source code are based on code that is + copyright (C) 2006 Marcin Slonicki + copyright (C) 2007 Louis Frigon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include "avclan_phy.h" +#include "cdchanger.h" +#include "mediacontrol.h" +#include "statustimer.h" + +AVCLAN_CD_Status_t cd_status; + +static cd_modes CD_Mode; + +// Sets CD_mode to play and resets timer count (so that the next interrupt is in +// 1 sec) +void AVCLAN_startPlaying() { + static bool havePlayed = false; + if (havePlayed) + AVCLAN_micPlayPause(); + havePlayed |= true; + CD_Mode = stPlay; + statustimer_reset(); +} + +// Sets CD_mode to play and resets timer count (so that the next interrupt is in +// 1 sec) +void AVCLAN_stopPlaying() { + statustimer_disable(); + CD_Mode = stStop; + AVCLAN_micPlayPause(); +} + +/* Pack a 0–99 count into 2-digit BCD. Values >99 (sentinels such as 0xFF / + 0x7F meaning "no time") pass through unchanged so they survive the wire + round-trip. */ +static uint8_t toBCD(uint8_t x) { + if (x > 99) + return x; + return (uint8_t)(((x / 10) << 4) | (x % 10)); +} + +// Serialize cd_status into the wire format. The struct layout mirrors the wire +// format byte-for-byte, except for track/mins/secs, which need converted from +// decimal to BCD +void serializeCDStatus(uint8_t *dst) { + memcpy(dst, &cd_status, sizeof(cd_status)); + dst[3] = toBCD(cd_status.track); + dst[4] = toBCD(cd_status.mins); + dst[5] = toBCD(cd_status.secs); +} + +bool AVCLAN_isPlaying() { return (CD_Mode == stPlay); } + +void AVCLAN_incrementTime() { + // Sentinel values (>99) mean "no time"; leave them alone until setTime() + // replaces them with a real count. + if (cd_status.secs > 99) + return; + if (cd_status.secs == 59) { + cd_status.secs = 0; + if (cd_status.mins == 99) + cd_status.mins = 0; + else + cd_status.mins++; + } else + cd_status.secs++; +} + +void AVCLAN_setTime(uint8_t mins, uint8_t secs) { + cd_status.mins = mins; + cd_status.secs = secs; +} + +// Only used for regularly scheduled periodic updates +AVCLAN_frame_t *AVCLAN_getStatusFrame() { + static uint8_t status_data[sizeof(AVCLAN_CD_Status_t) + 3] = {0}; + static AVCLAN_frame_t status = {.is_unicast = false, + .controller_addr = DEVICE_ADDR, + .peripheral_addr = 0x1FF, + .control = 0xF, + .length = sizeof(status_data), + .data = status_data}; + + return &status; +} + +// Used for changed status messages +void AVCLAN_generateStatus(AVCLAN_frame_t *status, bool is_unicast, + devices to) { + *status = (AVCLAN_frame_t){ + .is_unicast = is_unicast, + .controller_addr = DEVICE_ADDR, + .peripheral_addr = (is_unicast) ? HU_ADDR : 0x1FF, + .control = 0xF, + .length = sizeof(AVCLAN_CD_Status_t) + ((is_unicast) ? 4 : 3), + .data = status->data, // don't overwrite data pointer + }; + + uint8_t *data = status->data; + if (is_unicast) + *data++ = 0x00; + *data++ = dev_CD_CHANGER; + *data++ = to; + *data++ = Status_Report; + serializeCDStatus(data); +} + +void AVCLAN_normalizeState() { + // if (cd_status.state != cd_PLAYBACK) { + if (cd_status.mins > 99) + cd_status.mins = 0; + if (cd_status.secs > 99) + cd_status.secs = 0; + cd_status.state = cd_PLAYBACK; + cd_status.flags &= (uint8_t)~(cd_DISK_SCAN | cd_SCAN); + cd_status.flags2 = 0x80; + // } +} + +void AVCLAN_init() { + AVCLAN_phyInit(); + mediacontrol_init(); + statustimer_init(); + + cd_status.cds = cd_CD1; + cd_status.disc = 1; + cd_status.state = cd_SEEKING | cd_SEEKING_TRACK; + cd_status.flags = 0; + cd_status.flags2 = 0xC0; + + cd_status.track = 1; + cd_status.mins = 0xFF; + cd_status.secs = 0x7F; + + CD_Mode = stStop; +} diff --git a/src/avclan/cdchanger.h b/src/avclan/cdchanger.h new file mode 100644 index 0000000..bb76ec1 --- /dev/null +++ b/src/avclan/cdchanger.h @@ -0,0 +1,93 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Emulated CD-changer device (AVC-LAN device 0x63): playback state, BCD time, +// status-frame generation, and the play/stop mode FSM. This is pure-ish device +// logic; it commands the mediacontrol and statustimer drivers rather than +// touching their hardware directly. + +#ifndef CDCHANGER_H +#define CDCHANGER_H + +#include + +#include "avclan_defs.h" + +typedef enum : uint8_t { + cd_OPEN = 0x01, + cd_ERR1 = 0x02, + cd_SEEKING = 0x08, + cd_PLAYBACK = 0x10, + cd_SEEKING_TRACK = 0x20, + cd_LOADING = 0x80, +} cd_state; + +typedef enum : uint8_t { + cd_CD1 = 1 << 0, + cd_CD2 = 1 << 1, + cd_CD3 = 1 << 2, + cd_CD4 = 1 << 3, + cd_CD5 = 1 << 4, + cd_CD6 = 1 << 5, +} cd_present_t; + +typedef enum : uint8_t { + cd_DISK_RANDOM = 1 << 1, + cd_RANDOM = 1 << 2, + cd_DISK_REPEAT = 1 << 3, + cd_REPEAT = 1 << 4, + cd_DISK_SCAN = 1 << 5, + cd_SCAN = 1 << 6, +} cd_flag_t; + +typedef struct AVCLAN_CD_Status { + uint8_t cds; + uint8_t state; + uint8_t disc; + uint8_t track; // Decimal storage; serialize to BCD + uint8_t mins; // Decimal storage; serialize to BCD + uint8_t secs; // Decimal storage; serialize to BCD + uint8_t flags; + uint8_t flags2; +} AVCLAN_CD_Status_t; + +typedef enum : uint8_t { stStop = 0, stPlay = 1 } cd_modes; + +// Mutable device state. Exposed for the protocol dispatcher, which currently +// mutates these fields directly. (Will be re-encapsulated when the device- +// specific message handling migrates here from avclan_protocol.) +extern AVCLAN_CD_Status_t cd_status; + +// Full device-stack bring-up (calls AVCLAN_phyInit / mediacontrol_init / +// statustimer_init, then initialises cd_status). +void AVCLAN_init(); + +bool AVCLAN_isPlaying(); +void AVCLAN_startPlaying(); +void AVCLAN_stopPlaying(); +void AVCLAN_incrementTime(); +void AVCLAN_setTime(uint8_t mins, uint8_t secs); +void AVCLAN_normalizeState(); + +// Serialize cd_status into the wire format (track/mins/secs as BCD). +void serializeCDStatus(uint8_t *dst); + +AVCLAN_frame_t *AVCLAN_getStatusFrame(); +void AVCLAN_generateStatus(AVCLAN_frame_t *status, bool is_unicast, devices to); + +#endif // CDCHANGER_H diff --git a/src/avclan/mediacontrol.c b/src/avclan/mediacontrol.c new file mode 100644 index 0000000..50b6183 --- /dev/null +++ b/src/avclan/mediacontrol.c @@ -0,0 +1,154 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include + +#include "mediacontrol.h" + +// F_CPU defined in timing.h; the mic tick constants below are derived from it. +// TODO(HAL): re-derive these from a target-agnostic TICK_NS instead of F_CPU. +#include "timing.h" + +// pending WO1 toggles (even); signed to avoid underflows from a stray OVF +static volatile int8_t mic_ntoggles = 0; + +// Target pulse length is ~40-150ms, with interval between pulses of +// ~100-200ms +// TCA0 period (CMP0/TOP) in ticks at F_CPU with the CLKSEL=DIV1024 prescaler. +// A press phase is ~100 ms; the final LOW phase is stretched to +// mic_refractory_period (~333 ms) so consecutive presses stay distinct +static constexpr uint16_t mic_press_ticks = (uint16_t)((F_CPU / 1024UL) / 10UL); +static constexpr uint16_t mic_refractory_period = + (uint16_t)((F_CPU / 1024UL) / 3UL); + +// the longest AVCLAN frame duration is ~15ms +static constexpr uint16_t close_thresh = + (uint16_t)(mic_press_ticks * 15UL / 100UL); + +#ifndef NDEBUG +// Toggle PB1 and return its new level. +bool AVCLAN_micToggle() { + // 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; } +#endif + +// Begin a press waveform of `nphases` × 100 ms level segments. +// - ~Immediately toggles high, alternates each phase (1 = single HIGH press, 3 +// = skip H/L/H, etc). +// - Halting the timer freezes WO1 at its last level; must run even number of +// phases to ensure we return to low +static void mic_pulse(uint8_t nphases) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + if (mic_ntoggles) // Skip if already pulsing + return; + + // must be even to return to idle-low + mic_ntoggles = (nphases & 0x01) ? nphases + 1 : nphases; + TCA0.SINGLE.CTRLB |= + TCA_SINGLE_CMP1EN_bm; // Reassert TCA control of WO1/PB1 + TCA0.SINGLE.CTRLC = 0; // Reset WO1 level just in case + TCA0.SINGLE.CNT = 0; + TCA0.SINGLE.CMP0 = // TOP + mic_press_ticks; // always restore default ~100 ms period + TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // clear any stale flag + TCA0.SINGLE.INTCTRL |= TCA_SINGLE_OVF_bm; + TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm; + } +} + +// OVF ISR body function: +// - counts phases +// - stretches the final LOW phase as a refractory period, to keep separate +// pulse trains distinct +// - stops the timer after the last phase +static inline void mic_timer_isr_body(bool is_early) { + if (--mic_ntoggles == 1) { + // Stretch final phase to a ~333 ms idle-low so back-to-back presses stay + // distinct + // Invariant: mic_refractory_period > mic_press_ticks, so counter can never + // silently wrap + TCA0.SINGLE.CMP0 = mic_refractory_period; + } else if (mic_ntoggles <= 0) { + TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm; + TCA0.SINGLE.CTRLC = 0; // Timer must be disabled before (re)setting + // CTRLC/WO1 level (§20.5.3) + mic_ntoggles = 0; // clamp to avoid perma-lockout in mic_pulse + } + if (is_early) + TCA0.SINGLE.CNT = 0; + + // OVF FLAG must be cleared via write. MUST BE PERFORMED LAST. + // This function is called in two cases: + // - ISR, triggered by actual OVF: OVF FLAG is set and needs clearing + // - stopEvent, running ISR body ~early: OVF flag may be set while in + // ATOMIC_BLOCK (either in stopEvent, or earlier in this function body). + // Clear OVF FLAG as a precaution to prevent erroneous ISR runs + TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; +} + +ISR(TCA0_OVF_vect) { mic_timer_isr_body(false); } + +// Emulate a single play/pause button press on the source device. +void AVCLAN_micPlayPause() { mic_pulse(1); } + +// Emulate skip-forward/backward button presses +void AVCLAN_micSkipForward() { mic_pulse(3); } // double-press +void AVCLAN_micSkipBackward() { mic_pulse(5); } // triple-press + +// Pre-emptively "overflow" and run the OVF ISR body early if a press is in +// progress and likely to overflow within the masked window. This maintains: +// - a rough absolute time for the pulse train +// - 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() { + if (mic_ntoggles && TCA0.SINGLE.CNT >= (TCA0.SINGLE.CMP0 - close_thresh)) + mic_timer_isr_body(true); +} + +void mediacontrol_init() { + // PB1 needs to be set as an output for TCA0 to set the level + PORTB.DIRSET = PIN1_bm; + + // Experimentally, a press should be ~100ms; multiple presses can be separated + // by the same ~100ms (but separate pulse trains need more separation to + // remain distinct) + TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc; + + // In frequency (FRQ) mode, channel N compare match triggers "UPDATE" + // When CMPnEN is set, TCA0 has control of the output level for the channel's + // pin, and UPDATE toggles the level + // Channel 1 controls WO1, which is mapped to PB1 + TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_FRQ_gc | TCA_SINGLE_CMP1EN_bm; + TCA0.SINGLE.CTRLC = 0; // Preset WO1 level low just to be sure + mic_ntoggles = 0; + + // toggle WO1 ~immediately after each period start; should go low => high + TCA0.SINGLE.CMP1 = 2; + TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear OVF flag just in case + TCA0.SINGLE.INTCTRL = 0; +} diff --git a/src/avclan/mediacontrol.h b/src/avclan/mediacontrol.h new file mode 100644 index 0000000..a2d8999 --- /dev/null +++ b/src/avclan/mediacontrol.h @@ -0,0 +1,46 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Media-control actuator: emulates play/pause and skip button presses on the +// audio source by toggling MIC_CONTROL (PB1/WO1) with TCA0 in FRQ mode. + +#ifndef MEDIACONTROL_H +#define MEDIACONTROL_H + +#include + +// One-time hardware bring-up for the mic/button-press driver (TCA0 + PB1). +void mediacontrol_init(); + +// Emulate a single play/pause button press on the source device. +void AVCLAN_micPlayPause(); +// Emulate skip-forward/backward button presses +void AVCLAN_micSkipForward(); +void AVCLAN_micSkipBackward(); + +// Keep the press waveform roughly in sync while a bus transaction has masked +// interrupts. MUST be called with interrupts disabled (e.g. from within the +// frame layer's AVCLAN_stopEvent ATOMIC_BLOCK). +void mediacontrol_syncDuringMask(); + +#ifndef NDEBUG +bool AVCLAN_micToggle(); +bool AVCLAN_isMediaFunctioning(); +#endif + +#endif // MEDIACONTROL_H diff --git a/src/avclan/statustimer.c b/src/avclan/statustimer.c new file mode 100644 index 0000000..cc1e75c --- /dev/null +++ b/src/avclan/statustimer.c @@ -0,0 +1,64 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "statustimer.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 +// OSCULP32K, which is only spec'd to +/-3% and has no user calibration +// register, so a nominal 32768-count period does not land on exactly 1 s. +// Override per-board via the CMake cache (see CMakeUserPresets.json). +#ifndef RTC_STATUS_PERIOD_MS + #define RTC_STATUS_PERIOD_MS 1000 +#endif + +// RTC overflow period (in 32.768 kHz ticks) for the ~1 Hz status-update tick. +// ticks = round(32768 * 1000 / RTC_STATUS_PERIOD_MS); the RTC overflows after +// PER+1 ticks, so PER = ticks - 1. +static constexpr uint16_t rtc_status_per = + (uint16_t)(32768UL * 1000UL / RTC_STATUS_PERIOD_MS) - 1U; + +void statustimer_init() { + // 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) + loop_until_bit_is_clear(RTC_STATUS, RTC_CTRLABUSY_bp); + RTC.CLKSEL = RTC_CLKSEL_INT32K_gc; + loop_until_bit_is_clear(RTC_STATUS, RTC_PERBUSY_bp); + RTC.PER = rtc_status_per; + RTC.INTCTRL = 0; + loop_until_bit_is_clear(RTC_STATUS, RTC_CTRLABUSY_bp); + RTC.CTRLA = RTC_PRESCALER_DIV1_gc | RTC_RTCEN_bm; +} + +void statustimer_reset() { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + loop_until_bit_is_clear(RTC_STATUS, RTC_CNTBUSY_bp); + RTC.CNT = 0; + RTC.INTFLAGS = RTC_OVF_bm; // Clear interrupt flag just in case + RTC.INTCTRL |= RTC_OVF_bm; + } +} + +void statustimer_enable() { RTC.INTCTRL |= RTC_OVF_bm; } + +void statustimer_disable() { RTC.INTCTRL &= ~RTC_OVF_bm; } diff --git a/src/avclan/statustimer.h b/src/avclan/statustimer.h new file mode 100644 index 0000000..4188d58 --- /dev/null +++ b/src/avclan/statustimer.h @@ -0,0 +1,38 @@ +/* + AVCLAN-Mockingboard + Copyright (C) 2015 Allen Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// ~1 Hz status-update tick, driven by the RTC overflow. The overflow handler +// (ISR(RTC_CNT_vect)) lives in the app (sniffer.c); this module owns only the +// RTC hardware configuration and enable/disable/reset of the tick. + +#ifndef STATUSTIMER_H +#define STATUSTIMER_H + +// One-time RTC hardware bring-up (clock source + period). Leaves the overflow +// interrupt disabled. +void statustimer_init(); + +// Reset the count so the next tick is ~1 s out, and enable the overflow tick. +void statustimer_reset(); + +// Enable / disable the ~1 Hz overflow interrupt (bare register RMW; wrap in a +// critical section if called with interrupts enabled and concurrency matters). +void statustimer_enable(); +void statustimer_disable(); + +#endif // STATUSTIMER_H diff --git a/src/avclandrv.c b/src/avclandrv.c deleted file mode 100644 index 02f9c2f..0000000 --- a/src/avclandrv.c +++ /dev/null @@ -1,1570 +0,0 @@ -/* - AVCLAN-Mockingboard - Copyright (C) 2015 Allen Hill - - Portions of the following source code are based on code that is - copyright (C) 2006 Marcin Slonicki - copyright (C) 2007 Louis Frigon - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - --------------------------------------------------------------------------------------- - - AVC LAN Theory - - The AVC LAN bus is an implementation of the IEBus (mode 1) which is a - differential signal; IEBus is electrically (but not logically) compatible with - CAN bus. - - - Logical `1`: Potential difference between bus lines (BUS+ pin and BUS– pin) - is 20 mV or lower (floating). - - Logical `0`: Potential difference between bus lines (BUS+ pin and BUS– pin) - is 120 mV or higher (driving). - - A nominal bit length is 39 us, composed of 3 periods: preparation, - synchronization, data. - - Figure 1. AVCLAN Bus bit format - - │ Prep │<─ Sync ─>│<─ Data ─>│ ... - Driving (logical `0`) ╭──────────╮──────────╮ - │ │ │ - Floating (logical `1`) ─────────╯ ╰──────────╰───────── - │ 6 μs │── 19 μs ─│─ 13 μs ──│ - - The logical value during the data period signifies the bit value, e.g. a bit - `0` continues the logical `0` (high potential difference between bus lines) of - the sync period thru the data period, and a bit `1` has a logical `1` - (low/floating potential between bus lines) during the data period. Using the - TCB pulse-width and frequency measure mode, the total bit length differs for - bit `1` and `0`; detailed bit timing can be found in "timing.h". The bus - idles at low potential (floating). - - AVC LAN Frame Format - │ Bits │ Description - ──────────────────────────────────────── - | 1 │ Start bit - | 1 │ Direct/broadcast - | 12 │ Controller address - | 1 │ Parity - | 12 │ Peripheral address - | 1 │ Parity - | 1 │ *Acknowledge* (read below) - | 4 │ Control - | 1 │ Parity - | 1 │ *Acknowledge* - | 8 │ Message length (n) - | 1 │ Parity - | 1 │ *Acknowledge* - ──────── - | 8 │ Data - | 1 │ Parity - | 1 │ *Acknowledge* - *repeat `n` times* - - - A start bit is nominally 169 us high followed by 20 us low. - - A bit `0` is dominant on the bus, which is a design choice that affects - bit/interpretation: - - Low addresses have priority upon transmission conflicts - - The broadcast bit is `1` (floating, no effort) for normal communication - - For acknowledge bits, the receiver extends the logical '0' of the sync - period to the length of a normal bit `0`. Hence, a NAK (bit `1`) is - literally the absence of an ACK. - - No acknowledge bits are sent for broadcast frames. - --------------------------------------------------------------------------------------- -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include "avclandrv.h" -#include "com232.h" - -// F_CPU defined in timing.h and potentially needed by avr-libc (e.g. delay.h) -#include "timing.h" - -// Name difference between avr-libc and Microchip pack -#if defined(EVSYS_ASYNCCH00_bm) - #define EVSYS_ASYNCCH0_0_bm EVSYS_ASYNCCH00_bm -#endif - -#define READING_BYTE GPIOR1 -#define READING_NBITS GPIOR2 -#define READING_PARITY GPIOR3 - -#ifndef NDEBUG - #define TCB_CNTMODE TCB_CNTMODE_FRQPW_gc -#else - #define TCB_CNTMODE TCB_CNTMODE_PW_gc -#endif - -#define MAX_SEND_ATTEMPTS 3 - -static AVCLAN_CD_Status_t cd_status; - -static cd_modes CD_Mode; - -#ifndef NDEBUG -static volatile uint8_t pulse_count = 0; -static volatile uint16_t period = 0; -#endif - -static volatile uint16_t pulsewidth; - -// pending WO1 toggles (even); signed to avoid underflows from a stray OVF -static volatile int8_t mic_ntoggles = 0; - -// Target pulse length is ~40-150ms, with interval between pulses of -// ~100-200ms -// TCA0 period (CMP0/TOP) in ticks at F_CPU with the CLKSEL=DIV1024 prescaler. -// A press phase is ~100 ms; the final LOW phase is stretched to -// mic_refractory_period (~333 ms) so consecutive presses stay distinct -static constexpr uint16_t mic_press_ticks = (uint16_t)((F_CPU / 1024UL) / 10UL); -static constexpr uint16_t mic_refractory_period = - (uint16_t)((F_CPU / 1024UL) / 3UL); - -// the longest AVCLAN frame duration is ~15ms -static constexpr uint16_t close_thresh = - (uint16_t)(mic_press_ticks * 15UL / 100UL); - -#ifndef NDEBUG -// Toggle PB1 and return its new level. -bool AVCLAN_micToggle() { - // 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; } -#endif - -// Begin a press waveform of `nphases` × 100 ms level segments. -// - ~Immediately toggles high, alternates each phase (1 = single HIGH press, 3 -// = skip H/L/H, etc). -// - Halting the timer freezes WO1 at its last level; must run even number of -// phases to ensure we return to low -static void mic_pulse(uint8_t nphases) { - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - if (mic_ntoggles) // Skip if already pulsing - return; - - // must be even to return to idle-low - mic_ntoggles = (nphases & 0x01) ? nphases + 1 : nphases; - TCA0.SINGLE.CTRLB |= - TCA_SINGLE_CMP1EN_bm; // Reassert TCA control of WO1/PB1 - TCA0.SINGLE.CTRLC = 0; // Reset WO1 level just in case - TCA0.SINGLE.CNT = 0; - TCA0.SINGLE.CMP0 = // TOP - mic_press_ticks; // always restore default ~100 ms period - TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // clear any stale flag - TCA0.SINGLE.INTCTRL |= TCA_SINGLE_OVF_bm; - TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm; - } -} - -// OVF ISR body function: -// - counts phases -// - stretches the final LOW phase as a refractory period, to keep separate -// pulse trains distinct -// - stops the timer after the last phase -static inline void mic_timer_isr_body(bool is_early) { - if (--mic_ntoggles == 1) { - // Stretch final phase to a ~333 ms idle-low so back-to-back presses stay - // distinct - // Invariant: mic_refractory_period > mic_press_ticks, so counter can never - // silently wrap - TCA0.SINGLE.CMP0 = mic_refractory_period; - } else if (mic_ntoggles <= 0) { - TCA0.SINGLE.CTRLA &= ~TCA_SINGLE_ENABLE_bm; - TCA0.SINGLE.CTRLC = 0; // Timer must be disabled before (re)setting - // CTRLC/WO1 level (§20.5.3) - mic_ntoggles = 0; // clamp to avoid perma-lockout in mic_pulse - } - if (is_early) - TCA0.SINGLE.CNT = 0; - - // OVF FLAG must be cleared via write. MUST BE PERFORMED LAST. - // This function is called in two cases: - // - ISR, triggered by actual OVF: OVF FLAG is set and needs clearing - // - stopEvent, running ISR body ~early: OVF flag may be set while in - // ATOMIC_BLOCK (either in stopEvent, or earlier in this function body). - // Clear OVF FLAG as a precaution to prevent erroneous ISR runs - TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; -} - -ISR(TCA0_OVF_vect) { mic_timer_isr_body(false); } - -// Emulate a single play/pause button press on the source device. -void AVCLAN_micPlayPause() { mic_pulse(1); } - -// Emulate skip-forward/backward button presses -void AVCLAN_micSkipForward() { mic_pulse(3); } // double-press -void AVCLAN_micSkipBackward() { mic_pulse(5); } // triple-press - -/* Disable non-read related interrupts (USART RX, PIT, TCA) during AVCLAN reads. - */ -static inline void stopEvent() { - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - RTC.INTCTRL &= ~RTC_OVF_bm; - USART0.CTRLA &= ~USART_RXCIE_bm; - - // Pre-emptively "overflow" and run ISR early if expected/possible to - // overflow within this frame. This maintains: - // - a rough absolute time for the pulse train - // - 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 - if (mic_ntoggles && TCA0.SINGLE.CNT >= (TCA0.SINGLE.CMP0 - close_thresh)) - mic_timer_isr_body(true); - } -} - -// Re-enable serial and periodic interrupts. -static inline void startEvent() { - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - if (AVCLAN_isPlaying()) // Reenable status interrupt if currently playing - RTC.INTCTRL |= RTC_OVF_bm; - USART0.CTRLA |= USART_RXCIE_bm; - } -} - -static inline void resetStatusTimer() { - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - loop_until_bit_is_clear(RTC_STATUS, RTC_CNTBUSY_bp); - RTC.CNT = 0; - RTC.INTFLAGS = RTC_OVF_bm; // Clear interrupt flag just in case - RTC.INTCTRL |= RTC_OVF_bm; - } -} - -// Sets CD_mode to play and resets timer count (so that the next interrupt is in -// 1 sec) -static void AVCLAN_startPlaying() { - static bool havePlayed = false; - if (havePlayed) - AVCLAN_micPlayPause(); - havePlayed |= true; - CD_Mode = stPlay; - resetStatusTimer(); -} - -// Sets CD_mode to play and resets timer count (so that the next interrupt is in -// 1 sec) -void AVCLAN_stopPlaying() { - RTC.INTCTRL &= ~RTC_OVF_bm; - CD_Mode = stStop; - AVCLAN_micPlayPause(); -} - -// clang-format off -static inline void AVCLAN_setBusIdle() { - __asm__ __volatile__( - "cbi %[vporta_out], 4; \n\t" - "sbi %[vportc_out], 0; \n\t" - ::[vporta_out] "I"(_SFR_IO_ADDR(VPORTA_OUT)), - [vportc_out] "I"(_SFR_IO_ADDR(VPORTC_OUT))); -} -static inline void AVCLAN_setBusDriven() { - __asm__ __volatile__( - "sbi %[vporta_out], 4; \n\t" - "cbi %[vportc_out], 0; \n\t" - ::[vporta_out] "I"(_SFR_IO_ADDR(VPORTA_OUT)), - [vportc_out] "I"(_SFR_IO_ADDR(VPORTC_OUT))); -} -// clang-format on - -// Returns true if device TX is muted on AVCLAN bus -static inline bool AVCLAN_ismuted() { - return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0); -} - -// Mute device TX on AVCLAN bus -void AVCLAN_muteDevice(bool mute) { - if (mute) { - // clang-format off - __asm__ __volatile__("cbi %[vporta_dir], 4; \n\t" // set as INPUT (output values ignored) - "cbi %[vportc_dir], 0; \n\t" // set as INPUT (output values ignored) - :: - [vporta_dir] "I"(_SFR_IO_ADDR(VPORTA_DIR)), - [vportc_dir] "I"(_SFR_IO_ADDR(VPORTC_DIR))); - // clang-format on - } else { - // clang-format off - __asm__ __volatile__("sbi %[vporta_dir], 4; \n\t" - "sbi %[vportc_dir], 0; \n\t" - :: - [vporta_dir] "I"(_SFR_IO_ADDR(VPORTA_DIR)), - [vportc_dir] "I"(_SFR_IO_ADDR(VPORTC_DIR))); - // clang-format on - } -} - -// 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 -// OSCULP32K, which is only spec'd to +/-3% and has no user calibration -// register, so a nominal 32768-count period does not land on exactly 1 s. -// Override per-board via the CMake cache (see CMakeUserPresets.json). -#ifndef RTC_STATUS_PERIOD_MS - #define RTC_STATUS_PERIOD_MS 1000 -#endif - -// RTC overflow period (in 32.768 kHz ticks) for the ~1 Hz status-update tick. -// ticks = round(32768 * 1000 / RTC_STATUS_PERIOD_MS); the RTC overflows after -// PER+1 ticks, so PER = ticks - 1. -static constexpr uint16_t rtc_status_per = - (uint16_t)(32768UL * 1000UL / RTC_STATUS_PERIOD_MS) - 1U; - -void AVCLAN_init() { - // Set pin 6 and 7 as input - PORTA.DIRCLR = (PIN6_bm | PIN7_bm); - // Disable input buffer; recommended when using AC - PORTA.PIN6CTRL = PORT_ISC_INPUT_DISABLE_gc; - // PA7/AINN1(-) additionally gets a pull-up to help prevent the comparator - // latching high (ie false "driven" bus) - PORTA.PIN7CTRL = PORT_PULLUPEN_bm | PORT_ISC_INPUT_DISABLE_gc; - - // Analog comparator config - AC2.CTRLA = AC_OUTEN_bm | AC_HYSMODE_25mV_gc | AC_ENABLE_bm; - - PORTB.DIRSET = PIN2_bm; // Enable AC2 OUT for LED - PORTB.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc; // Output only - - // Set AC2 to generate events on async channel 0 - EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_AC2_OUT_gc; - EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // USER0 is TCB0 - - // PB1 needs to be set as an output for TCA0 to set the level - PORTB.DIRSET = PIN1_bm; - - // Experimentally, a press should be ~100ms; multiple presses can be separated - // by the same ~100ms (but separate pulse trains need more separation to - // remain distinct) - TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc; - - // In frequency (FRQ) mode, channel N compare match triggers "UPDATE" - // When CMPnEN is set, TCA0 has control of the output level for the channel's - // pin, and UPDATE toggles the level - // Channel 1 controls WO1, which is mapped to PB1 - TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_FRQ_gc | TCA_SINGLE_CMP1EN_bm; - TCA0.SINGLE.CTRLC = 0; // Preset WO1 level low just to be sure - mic_ntoggles = 0; - - // toggle WO1 ~immediately after each period start; should go low => high - TCA0.SINGLE.CMP1 = 2; - TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear OVF flag just in case - TCA0.SINGLE.INTCTRL = 0; - - // TCB0 for read bit timing - TCB0.CTRLB = TCB_CNTMODE; - TCB0.INTCTRL = TCB_CAPT_bm; - TCB0.EVCTRL = TCB_CAPTEI_bm; - TCB0.CTRLA = TCB_CLKSEL | TCB_ENABLE_bm; - - // TCB1 for send bit timing - TCB1.CTRLB = TCB_CNTMODE_INT_gc; - TCB1.CCMP = 0xFFFF; - TCB1.CTRLA = TCB_CLKSEL | TCB_ENABLE_bm; - - // 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) - loop_until_bit_is_clear(RTC_STATUS, RTC_CTRLABUSY_bp); - RTC.CLKSEL = RTC_CLKSEL_INT32K_gc; - loop_until_bit_is_clear(RTC_STATUS, RTC_PERBUSY_bp); - RTC.PER = rtc_status_per; - RTC.INTCTRL = 0; - loop_until_bit_is_clear(RTC_STATUS, RTC_CTRLABUSY_bp); - RTC.CTRLA = RTC_PRESCALER_DIV1_gc | RTC_RTCEN_bm; - - AVCLAN_setBusIdle(); - - AVCLAN_muteDevice(false); // unmute AVCLAN bus TX - - cd_status.cds = cd_CD1; - cd_status.disc = 1; - cd_status.state = cd_SEEKING | cd_SEEKING_TRACK; - cd_status.flags = 0; - cd_status.flags2 = 0xC0; - - cd_status.track = 1; - cd_status.mins = 0xFF; - cd_status.secs = 0x7F; - - CD_Mode = stStop; -} - -/* Pack a 0–99 count into 2-digit BCD. Values >99 (sentinels such as 0xFF / - 0x7F meaning "no time") pass through unchanged so they survive the wire - round-trip. */ -static uint8_t toBCD(uint8_t x) { - if (x > 99) - return x; - return (uint8_t)(((x / 10) << 4) | (x % 10)); -} - -// Serialize cd_status into the wire format. The struct layout mirrors the wire -// format byte-for-byte, except for track/mins/secs, which need converted from -// decimal to BCD -static void serializeCDStatus(uint8_t *dst) { - memcpy(dst, &cd_status, sizeof(cd_status)); - dst[3] = toBCD(cd_status.track); - dst[4] = toBCD(cd_status.mins); - dst[5] = toBCD(cd_status.secs); -} - -bool AVCLAN_isPlaying() { return (CD_Mode == stPlay); } - -void AVCLAN_incrementTime() { - // Sentinel values (>99) mean "no time"; leave them alone until setTime() - // replaces them with a real count. - if (cd_status.secs > 99) - return; - if (cd_status.secs == 59) { - cd_status.secs = 0; - if (cd_status.mins == 99) - cd_status.mins = 0; - else - cd_status.mins++; - } else - cd_status.secs++; -} - -static void AVCLAN_setTime(uint8_t mins, uint8_t secs) { - cd_status.mins = mins; - cd_status.secs = secs; -} - -// Set AVC bus to `val` (logical 1 or 0) for `period` ticks of TCB1 -void set_AVC_logic_for(uint8_t val, uint16_t period) { - TCB1.CNT = 0; - if (val) { - AVCLAN_setBusIdle(); // idle bus is logical 1 - } else { - AVCLAN_setBusDriven(); - } - while (TCB1.CNT <= period) {}; - - return; -} - -typedef enum avclan_bit : uint8_t { - bit_zero = 0x00, - bit_one = 0x01, - bit_start = 0x10 -} avclan_bit_t; - -void AVCLAN_sendbit(avclan_bit_t bit) { - uint16_t zero_length, one_length; - switch (bit) { - case bit_zero: - zero_length = AVCLAN_BIT0_LOGIC_0; - one_length = AVCLAN_BIT0_LOGIC_1; - break; - case bit_one: - zero_length = AVCLAN_BIT1_LOGIC_0; - one_length = AVCLAN_BIT1_LOGIC_1; - break; - case bit_start: - zero_length = AVCLAN_STARTBIT_LOGIC_0; - one_length = AVCLAN_STARTBIT_LOGIC_1; - break; - default: __builtin_unreachable(); - } - set_AVC_logic_for(0, zero_length); - set_AVC_logic_for(1, one_length); -} - -void AVCLAN_sendbit_ACK() { - TCB1.CNT = 0; - - // Wait for controller to begin ACK bit - while (BUS_IS_IDLE) { - // Wait for approx the length of a bit; any longer and something has clearly - // gone wrong - if (TCB1.CNT >= AVCLAN_BIT_LENGTH_MAX) - return; - } - - AVCLAN_sendbit(bit_zero); -} - -/* Returns true if the peripheral sent an ACK bit. - An ACK bit is a cooperative bit, where the sender starts (drives the bus) a - sync period, and allows the receiver to drive the bus (or not) to finish a "1" - bit. -*/ -uint8_t AVCLAN_readbit_ACK() { - TCB1.CNT = 0; - set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); - AVCLAN_setBusIdle(); // Stop driving bus - - while (true) { - if (!BUS_IS_IDLE && (TCB1.CNT > AVCLAN_READBIT_THRESHOLD)) - break; // ACK - if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) - return 0; // NAK - } - - // Check/wait in case we get here before peripheral finishes ACK bit - while (!BUS_IS_IDLE) { - if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) - return 0; // NAK - } - return 1; -} - -#define AVCLAN_sendbits(bits, len) \ - _Generic((bits), \ - const uint16_t *: AVCLAN_sendbitsl, \ - uint16_t *: AVCLAN_sendbitsl, \ - const uint8_t *: AVCLAN_sendbitsi, \ - uint8_t *: AVCLAN_sendbitsi)(bits, len) - -// Send `len` bits on the AVCLAN bus; returns the even parity -avclan_bit_t AVCLAN_sendbitsi(const uint8_t *bits, int8_t len) { - uint8_t b = *bits; - uint8_t parity = 0; - int8_t len_mod8 = 8; - - if (len & 0x7) { - len_mod8 = (int8_t)(len & 0x7); - b <<= (uint8_t)(8 - len_mod8); - } - - while (len > 0) { - len -= len_mod8; - for (; len_mod8 > 0; len_mod8--) { - avclan_bit_t bit = (b & 0x80) != 0; - parity += (uint8_t)bit; - AVCLAN_sendbit(bit); - b <<= 1; - } - len_mod8 = 8; - b = *--bits; - } - return (parity & 1); -} - -// Send `len` bits on the AVCLAN bus; returns the even parity -avclan_bit_t AVCLAN_sendbitsl(const uint16_t *bits, int8_t len) { - return AVCLAN_sendbitsi((const uint8_t *)bits + 1, len); -} - -avclan_bit_t AVCLAN_sendbyte(const uint8_t *byte) { - uint8_t b = *byte; - uint8_t parity = 0; - - for (uint8_t nbits = 8; nbits > 0; nbits--) { - avclan_bit_t bit = (b & 0x80) != 0; - parity += (uint8_t)bit; - AVCLAN_sendbit(bit); - b <<= 1; - } - return (parity & 1); -} - -ISR(TCB0_INT_vect) { -#ifndef NDEBUG - pulse_count++; - period = TCB0.CNT; -#endif - - READING_BYTE <<= 1; - // If the logical `0` pulse was less than the sync + data period threshold, - // bit was a 1 - pulsewidth = TCB0.CCMP; - if (pulsewidth < (uint16_t)AVCLAN_READBIT_THRESHOLD) { - READING_BYTE++; - READING_PARITY++; - } - READING_NBITS--; -} - -#define AVCLAN_readbits(bits, len) \ - _Generic((bits), \ - const uint16_t *: AVCLAN_readbitsl, \ - uint16_t *: AVCLAN_readbitsl, \ - const uint8_t *: AVCLAN_readbitsi, \ - uint8_t *: AVCLAN_readbitsi)(bits, len) - -// Read `len` bits on the AVCLAN bus; returns the even parity -uint8_t AVCLAN_readbitsi(uint8_t *bits, uint8_t len) { - cli(); - READING_BYTE = 0; - READING_PARITY = 0; - READING_NBITS = len; - sei(); - - TCB1.CNT = 0; - while (READING_NBITS) { - // 200% the duration of `len` bits - if (TCB1.CNT > ((uint16_t)AVCLAN_BIT_LENGTH_MAX * 2 * len)) { - READING_BYTE = 0; - READING_PARITY = 0; - break; // Should have finished by now; something's wrong - } - }; - - cli(); - *bits = READING_BYTE; - uint8_t parity = READING_PARITY; - sei(); - - return (parity & 1); -} - -// Read `len` bits on the AVCLAN bus; returns the even parity -uint8_t AVCLAN_readbitsl(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); - len -= over; - } - parity += AVCLAN_readbitsi((uint8_t *)bits + 0, len); - - return (parity & 1); -} - -// Read a byte on the AVCLAN bus -uint8_t AVCLAN_readbyte(uint8_t *byte) { - cli(); - READING_BYTE = 0; - READING_PARITY = 0; - READING_NBITS = 8; - sei(); - - TCB1.CNT = 0; - while (READING_NBITS) { - // 200% the length of a byte - if (TCB1.CNT > ((uint16_t)AVCLAN_BIT_LENGTH_MAX * 2 * 8)) { - READING_BYTE = 0; - READING_PARITY = 0; - break; // Should have finished by now; something's wrong - } - }; - - cli(); - *byte = READING_BYTE; - uint8_t parity = READING_PARITY; - sei(); - - return (parity & 1); -} - -uint8_t AVCLAN_readframe(AVCLAN_frame_t *frame, log_t print) { - struct errtype { - // Error enum is ordered such that a lower numeric value corresponds to more - // successful read - enum : uint8_t { - NO_ERROR = 0x00, - BAD_DATA_PARITY = 0x01, - BAD_LENGTH_RANGE, - BAD_LENGTH_PARITY, - BAD_PERIPHERAL_PARITY, - BAD_CONTROLLER_PARITY, - BAD_CONTROL_PARITY, - STARTBIT_TOO_SHORT, - STARTBIT_TOO_LONG, - LATCHED_COMPARATOR, - } errno; - union { - uint8_t val; // BAD_LENGTH_RANGE: the out-of-range length value - struct { - uint16_t read_val; - uint8_t parity; // received (bad) parity bit - }; - }; - } err = {0}; - - stopEvent(); // disable timer1 interrupt - - uint8_t tmp = 0; - - uint16_t startbitlen = TCB1.CNT = 0; - while (!BUS_IS_IDLE) { - startbitlen = TCB1.CNT; - if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) { - err.errno = STARTBIT_TOO_LONG; - while (!BUS_IS_IDLE) { - // If bus is "driven" too long, assume the AC2 is latched (e.g. - // because the bus is actually floating). Kick it if so. - // This should prevent/resolve a flood of "STARTBIT_TOO_LONG" errors - if (TCB1.CNT > (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 3)) { - err.errno = LATCHED_COMPARATOR; - PORTA.OUTSET = PIN7_bm; // preset high before enabling the driver - PORTA.DIRSET = PIN7_bm; // drive (-) hard high - TCB1.CNT = 0; - while (!BUS_IS_IDLE && TCB1.CNT < (uint16_t)AVCLAN_BIT0_LOGIC_1) { - // Wait a max of ~6μs until bus is idle - } - PORTA.DIRCLR = PIN7_bm; // back to high-Z comparator input - PORTA.OUTCLR = PIN7_bm; - } - } - goto handle_err; - } - } - if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) { - err.errno = STARTBIT_TOO_SHORT; - // We missed the beginning of this message; wait for it to finish (bus - // continuously idle for >1 bit length) before returning, so we don't have - // multiple false-starts while the in-progress message keeps sending more - // bits. - TCB1.CNT = 0; - while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) { - if (!BUS_IS_IDLE) - TCB1.CNT = 0; - } - goto handle_err; - } - // Otherwise that was a start bit - - AVCLAN_readbits(&tmp, 1); - frame->is_unicast = tmp; - - uint8_t parity = AVCLAN_readbits(&frame->controller_addr, 12); - AVCLAN_readbits(&tmp, 1); - if (parity != (tmp &= 1)) { - err.errno = BAD_CONTROLLER_PARITY; - if (print.verbose) { - err.read_val = frame->controller_addr; - err.parity = tmp; - } - goto handle_err; - } - - parity = AVCLAN_readbits(&frame->peripheral_addr, 12); - AVCLAN_readbits(&tmp, 1); - if (parity != (tmp &= 1)) { - err.errno = BAD_PERIPHERAL_PARITY; - if (print.verbose) { - err.read_val = frame->peripheral_addr; - err.parity = tmp; - } - goto handle_err; - } - - bool shouldACK = !AVCLAN_ismuted() && (frame->peripheral_addr == DEVICE_ADDR); - - if (shouldACK) - AVCLAN_sendbit_ACK(); - else - AVCLAN_readbits(&tmp, 1); - - parity = AVCLAN_readbits(&frame->control, 4); - AVCLAN_readbits(&tmp, 1); - if (parity != (tmp &= 1)) { - err.errno = BAD_CONTROL_PARITY; - if (print.verbose) { - err.read_val = frame->control; - err.parity = tmp; - } - goto handle_err; - } else if (shouldACK) { - AVCLAN_sendbit_ACK(); - } else { - AVCLAN_readbits(&tmp, 1); - } - - parity = AVCLAN_readbyte(&frame->length); - AVCLAN_readbits(&tmp, 1); - if (parity != (tmp &= 1)) { - err.errno = BAD_LENGTH_PARITY; - if (print.verbose) { - err.read_val = frame->length; - err.parity = tmp; - } - goto handle_err; - } else if (shouldACK) { - AVCLAN_sendbit_ACK(); - } else { - AVCLAN_readbits(&tmp, 1); - } - - if (frame->length == 0 || frame->length > MAXMSGLEN) { - err.errno = BAD_LENGTH_RANGE; - err.val = frame->length; - goto handle_err; - } - - for (uint8_t i = 0; i < frame->length; i++) { - parity = AVCLAN_readbyte(&frame->data[i]); - AVCLAN_readbits(&tmp, 1); - if (parity != (tmp &= 1)) { - err.errno = BAD_DATA_PARITY; - if (print.verbose) { - err.read_val = frame->data[i]; - err.parity = tmp; - } - goto handle_err; - } else if (shouldACK) { - AVCLAN_sendbit_ACK(); - } else { - AVCLAN_readbits(&tmp, 1); - } - } - - if (false) { - handle_err:; - startEvent(); - RS232_Print("ERR(read): "); - switch (err.errno) { - case LATCHED_COMPARATOR: RS232_Print("latched comparator"); break; - case STARTBIT_TOO_SHORT: RS232_Print("start bit too short"); break; - case STARTBIT_TOO_LONG: RS232_Print("start bit too long"); break; - case BAD_CONTROLLER_PARITY: - RS232_Print("reading controller addr."); - goto VERBOSE; - case BAD_PERIPHERAL_PARITY: - RS232_Print("reading peripheral addr."); - goto VERBOSE; - case BAD_CONTROL_PARITY: RS232_Print("reading control"); goto VERBOSE; - case BAD_LENGTH_PARITY: RS232_Print("reading length"); goto VERBOSE; - case BAD_LENGTH_RANGE: - RS232_Print("bad length 0x"); - RS232_PrintHex4(err.val); - break; - case BAD_DATA_PARITY: RS232_Print("reading data"); goto VERBOSE; - case NO_ERROR: - __builtin_unreachable(); - VERBOSE: - if (print.verbose) { - RS232_Print("; read 0x"); - RS232_PrintHex(err.read_val); - RS232_Print(" and got bad parity "); - RS232_PrintHex4(err.parity); - } - } - RS232_Print("\n"); - } else { - startEvent(); - } - - // Only print if some data has been correctly received - if (print.print && (err.errno < STARTBIT_TOO_SHORT)) { - if (err.errno > BAD_DATA_PARITY) - frame->length = 0; - AVCLAN_printframe(frame, print.binary); - } - - return err.errno; -} - -uint8_t AVCLAN_sendframe(const AVCLAN_frame_t *frame, log_t print) { - struct errtype { - // Error enum is ordered such that a lower numeric value corresponds to more - // success - enum : uint8_t { - NO_ERROR = 0x00, - NAK_DATA = 0x01, - NAK_MESSAGE_LENGTH, - NAK_CONTROL, - NAK_ADDRESS, - BUSY, - MUTED, - } errno; - uint8_t val; - } err = {0}; - - if (AVCLAN_ismuted()) { - err.errno = MUTED; - goto handle_err; - } - - stopEvent(); - - // wait for free line - TCB1.CNT = 0; - while (BUS_IS_IDLE) { - // Wait for 120% of a bit length - if (TCB1.CNT >= (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 2)) - break; - } - - // End of first loop could be due to bus being driven - TCB1.CNT = 0; - if (!BUS_IS_IDLE) { - // Some other device started sending - // Can't yet simultaneously send and recieve to do proper CSMA/CD - err.errno = BUSY; - goto handle_err; - - // Beginnings of CSMA/CD - // do { - // if (TCB1.CNT >= (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 1.2)) - // return 1; // Something's hinky; nothing is longer than the start bit - // } while (!BUS_IS_IDLE); - // if (TCB1.CNT <= (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) - // return 1; // Shouldn't be possible (waiting 2 bit lengths with idle - // bus, - // // then next bit should be a long one ie start) - // set_AVC_logic_for(1, AVCLAN_STARTBIT_LOGIC_1); // wait for end of start - // bit - } else { - AVCLAN_sendbit(bit_start); - } - AVCLAN_sendbits(&(uint8_t){frame->is_unicast}, 1); - - avclan_bit_t parity = AVCLAN_sendbits(&frame->controller_addr, 12); - AVCLAN_sendbit(parity); - - parity = AVCLAN_sendbits(&frame->peripheral_addr, 12); - AVCLAN_sendbit(parity); - - if (frame->is_unicast && !AVCLAN_readbit_ACK()) { - err.errno = NAK_ADDRESS; - goto handle_err; - } - - parity = AVCLAN_sendbits(&frame->control, 4); - AVCLAN_sendbit(parity); - - if (frame->is_unicast && !AVCLAN_readbit_ACK()) { - err.errno = NAK_CONTROL; - goto handle_err; - } - - parity = AVCLAN_sendbyte(&frame->length); // data length - AVCLAN_sendbit(parity); - - if (frame->is_unicast && !AVCLAN_readbit_ACK()) { - err.errno = NAK_MESSAGE_LENGTH; - goto handle_err; - } - - for (uint8_t i = 0; i < frame->length; i++) { - parity = AVCLAN_sendbyte(&frame->data[i]); - AVCLAN_sendbit(parity); - // Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem - // necessary (i.e. This deviates from the previous broadcast specific - // function that sent an extra `1` bit after each byte/parity) - if (frame->is_unicast && !AVCLAN_readbit_ACK()) { - err.errno = NAK_DATA; - err.val = i; - goto handle_err; - } - // else - // AVCLAN_sendbit_1(); - } - - // back to read mode - if (false) { - handle_err:; - startEvent(); - RS232_Print("Error"); - switch (err.errno) { - case MUTED: RS232_Print(": Device muted"); break; - case BUSY: RS232_Print(": Busy bus"); break; - case NAK_ADDRESS: - case NAK_CONTROL: - case NAK_MESSAGE_LENGTH: - case NAK_DATA: - RS232_Print(" NAK: "); - switch (err.errno) { - case NAK_ADDRESS: RS232_Print("address"); break; - case NAK_CONTROL: RS232_Print("Control"); break; - case NAK_MESSAGE_LENGTH: RS232_Print("Message length"); break; - case NAK_DATA: - RS232_Print(" data["); - RS232_PrintDec(err.val); - RS232_Print("]"); - break; - case NO_ERROR: - case MUTED: - case BUSY: __builtin_unreachable(); - } - break; - case NO_ERROR: __builtin_unreachable(); - } - RS232_Print("\n"); - } else { - startEvent(); - } - - if (print.print) - AVCLAN_printframe(frame, print.binary); - - return err.errno; -} - -#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) - -static const uint8_t cdloading_resp[] = {dev_CD_CHANGER, - dev_STATUS, - Loading_Status_Report, - 0x00, - 0x01, - 0x00, - 0x01, - 0x00, - 0x01, - 0x02}; - -response_t AVCLAN_handleframe(const AVCLAN_frame_t *in, AVCLAN_frame_t *out) { - response_t respond = r_Nothing; - - if (AVCLAN_ismuted() || in->length < 3) - return respond; - - // 0xFF placeholders are variant bytes filled by writing directly to - // out->data[N] after memcpy. - static const uint8_t lancheck_resp[] = {0x00, dev_COMM_CTRL, dev_LAN, 0xFF, - 0xFF}; - static const uint8_t function_change_resp[] = {0x00, dev_CD_CHANGER, - dev_COMM_v1, 0xFF, 0x01}; - - out->controller_addr = DEVICE_ADDR; - out->control = 0xF; - - const uint8_t *data = in->data; - const uint8_t b0 = *data++; - const uint8_t b1 = *data++; - const uint8_t b2 = *data++; - uint8_t b3 = 0; - if (in->length > 3) // the shortest known/valid messages are 3 bytes long - b3 = *data++; - - if (!in->is_unicast) { - // Broadcast: bytes are (from, to, action, [extra...]). - // peripheral_addr unchecked — always 0xFFF or 0x1FF in known traffic. - switch (PACK3(b0, b1, b2)) { - case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Scan_Req): - out->length = sizeof(lancheck_resp); - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); - out->data[3] = Lancheck_Scan_Resp; - out->data[4] = 0x01; - respond = r_Handled; - break; - case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_Req): - out->length = sizeof(lancheck_resp); - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - memcpy(out->data, lancheck_resp, sizeof(lancheck_resp)); - out->data[3] = Lancheck_Resp; - out->data[4] = 0x00; - respond = r_Handled; - break; - case PACK3(dev_LAN, dev_COMM_CTRL, Lancheck_End_Req): - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->length = sizeof(lancheck_resp) - 1; - memcpy(out->data, lancheck_resp, out->length); - out->data[3] = Lancheck_End_Resp; - respond = r_Handled; - break; - case PACK3(dev_COMM_v1, dev_COMM_CTRL, Current_Function): - case PACK3(dev_COMM_v2, dev_COMM_CTRL, Current_Function): - if ((b3 == dev_CD_CHANGER) && !AVCLAN_isPlaying()) { - if (cd_status.mins > 99) - cd_status.mins = 0; - if (cd_status.secs > 99) - cd_status.secs = 0; - cd_status.state = cd_SEEKING | cd_SEEKING_TRACK; - cd_status.flags2 = 0xc0; - AVCLAN_generateStatus(out, true, dev_STATUS); - respond = r_StartPlaying; - } - break; - case PACK3(dev_COMM_v1, dev_COMM_CTRL, Ping_Req): - case PACK3(dev_COMM_v2, dev_COMM_CTRL, Ping_Req): { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - const uint8_t ping_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, - Ping_Resp, 0xFF, b3}; - out->length = sizeof(ping_resp); - memcpy(out->data, ping_resp, sizeof(ping_resp)); - respond = r_Handled; - break; - } - case PACK3(dev_COMM_v1, dev_COMM_CTRL, List_Functions_Req): - case PACK3(dev_COMM_v2, dev_COMM_CTRL, List_Functions_Req): { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - const uint8_t list_functions_resp[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, - List_Functions_Resp, - dev_CD_CHANGER}; - out->length = sizeof(list_functions_resp); - memcpy(out->data, list_functions_resp, sizeof(list_functions_resp)); - respond = r_Handled; - break; - } - // case Restart_Lan: not handled - } - } else if (in->peripheral_addr == DEVICE_ADDR && b0 == 0x00) { - // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). - switch (PACK3(b1, b2, b3)) { - case PACK3(dev_COMM_v1, dev_CD_CHANGER, Enable_Function_Req): - [[fallthrough]]; - case PACK3(dev_COMM_v2, dev_CD_CHANGER, Enable_Function_Req): - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - out->length = sizeof(function_change_resp); - memcpy(out->data, function_change_resp, sizeof(function_change_resp)); - out->data[3] = Enable_Function_Resp; - cd_status.state = 0; - cd_status.flags2 = 0x80; - respond = r_StatusReport; - break; - case PACK3(dev_COMM_v1, dev_CD_CHANGER, Disable_Function_Req): - [[fallthrough]]; - case PACK3(dev_COMM_v2, dev_CD_CHANGER, Disable_Function_Req): - // No change/response needed if we're already not playing - if (AVCLAN_isPlaying()) { - AVCLAN_stopPlaying(); - out->length = sizeof(function_change_resp); - memcpy(out->data, function_change_resp, sizeof(function_change_resp)); - out->data[3] = Disable_Function_Resp; - cd_status.state = 0; - cd_status.flags2 = 0x80; - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - respond = r_StatusReport; - } - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Eject): { - // "Eject" label is multiply wrong; proper meaning unclear: - // - First observed on initial multiple presses of "CD" button, - // triggering (after {0x00, dev_CD_CHANGER, dev_COMM_v1, Insertion, - // 0x01} response) proper activation of mockingboard/cd-changer. - // - Subsequently observed when pressing (technically - // releasing?) the fast-forward button and rewind - if (cd_status.state | cd_SEEKING) { // FF/RW button released - cd_status.state &= ~cd_SEEKING; - } else { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - { - const uint8_t msg[] = {0x00, dev_CD_CHANGER, dev_CMD_SW, Insertion, - 0x01}; - out->length = sizeof(msg); - memcpy(out->data, msg, sizeof(msg)); - } - respond = r_Handled; - } - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Initial_Report_Request): - [[fallthrough]]; - case PACK3(dev_STATUS, dev_CD_CHANGER, Initial_Report_Request): { - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - - // No knowledge/understanding of field meaning/interpretation - const uint8_t cdinitreport_resp[] = { - 0x00, dev_CD_CHANGER, b1, Initial_Report_Response, 0x01, 0x31, - 0x10, 0x01, 0x01}; - out->length = sizeof(cdinitreport_resp); - memcpy(&out->data[1], cdinitreport_resp, sizeof(cdinitreport_resp)); - respond = r_Handled; - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Playback_Request): [[fallthrough]]; - case PACK3(dev_STATUS, dev_CD_CHANGER, Playback_Request): - out->data[0] = 0x00; - out->data[1] = dev_CD_CHANGER; - out->data[2] = b1; - out->data[3] = Playback_Report; - out->length = sizeof(AVCLAN_CD_Status_t) + 4; - serializeCDStatus(&out->data[4]); - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - respond = r_Handled; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Loading_Request2): [[fallthrough]]; - case PACK3(dev_STATUS, dev_CD_CHANGER, Loading_Request2): - out->data[0] = 0x00; - out->length = sizeof(cdloading_resp) + 1; - memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp)); - out->data[2] = b1; - out->data[3] = Loading_Response2; - out->is_unicast = true; - out->peripheral_addr = HU_ADDR; - respond = r_Handled; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Up): - cd_status.state = cd_SEEKING_TRACK; - if (cd_status.track < 98) - ++cd_status.track; - else - cd_status.track = 1; - cd_status.mins = 0xff; - cd_status.secs = 0x7f; - cd_status.flags2 = 0xc0; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_micSkipForward(); - respond = r_TrackChange; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Seek_Down): - cd_status.state = cd_SEEKING_TRACK; - // Track down returns to track beginning if in ~middle of song - if (cd_status.mins == 0 && cd_status.secs < 0x05) { - if (cd_status.track > 1) - --cd_status.track; - else - cd_status.track = 99; - } - cd_status.mins = 0xff; - cd_status.secs = 0x7f; - cd_status.flags2 = 0xc0; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_micSkipBackward(); - respond = r_TrackChange; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Fast_Forward): { - cd_status.state |= cd_SEEKING; - cd_status.secs += 15; - if (cd_status.secs > 60) { - cd_status.secs -= 60; - ++cd_status.mins; - } - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_micSkipForward(); - resetStatusTimer(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now - respond = r_Handled; - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, Track_Rewind): { - cd_status.state |= cd_SEEKING; - if (cd_status.secs < 15) { - if (cd_status.mins > 0) { - uint8_t d = 15 - cd_status.secs; - cd_status.secs = 60 - d; - --cd_status.mins; - } else { - cd_status.mins = 0; - cd_status.secs = 0; - } - } else - cd_status.secs -= 15; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - AVCLAN_micSkipBackward(); - resetStatusTimer(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now - respond = r_Handled; - break; - } - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Random): - cd_status.flags |= cd_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Random): - cd_status.flags &= ~cd_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Repeat): - cd_status.flags |= cd_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Repeat): - cd_status.flags &= ~cd_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Disk_Random): - cd_status.flags |= cd_DISK_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Disk_Random): - cd_status.flags &= ~cd_DISK_RANDOM; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Enable_Disk_Repeat): - cd_status.flags |= cd_DISK_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - case PACK3(dev_CMD_SW, dev_CD_CHANGER, CD_Disable_Disk_Repeat): - cd_status.flags &= ~cd_DISK_REPEAT; - AVCLAN_generateStatus(out, true, dev_CMD_SW); - respond = r_StatusReport; - break; - } - } - - return respond; -} - -#undef PACK3 - -RFrame_t *AVCLAN_statemachine(RFrame_t *resp) { - AVCLAN_frame_t *out = resp->frame; - switch (resp->r) { - case r_Ejection: { - const uint8_t play[] = {0x00, dev_COMM_CTRL, dev_COMM_v1, - Insertion, dev_CD_CHANGER, 0x01}; - out->length = sizeof(play); - memcpy(out->data, play, sizeof(play)); - } - resp->r = r_Report_Load; - break; - case r_Report_Load: - out->is_unicast = false; - out->peripheral_addr = 0x1FF; - out->length = sizeof(cdloading_resp) + 1; - memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); - out->data[1] = dev_STATUS; - out->data[2] = Loading_Status_Report; - resp->r = r_Handled; - break; - case r_TrackChange: - AVCLAN_setTime(0x00, 0x00); - resetStatusTimer(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now - [[fallthrough]]; - case r_NormalizeState: - AVCLAN_normalizeState(); - AVCLAN_generateStatus(out, true, dev_STATUS); - resp->r = r_Handled; - break; - case r_StartPlaying: - AVCLAN_normalizeState(); - AVCLAN_generateStatus(out, true, dev_STATUS); - resp->r = r_BeganPlaying; - break; - case r_BeganPlaying: - AVCLAN_startPlaying(); // only start PIT after normalizing state - resp->r = r_Nothing; - break; - case r_StatusReport: - AVCLAN_generateStatus(out, true, dev_STATUS); - resp->r = r_Handled; - break; - case r_Handled: [[fallthrough]]; - case r_Nothing: [[fallthrough]]; - default: resp->r = r_Nothing; - } - return resp; -} - -uint8_t AVCLAN_tryrespond(const AVCLAN_frame_t *resp) { - uint8_t r = 0; - for (uint8_t i = 0; i < MAX_SEND_ATTEMPTS; i++) { - r = AVCLAN_sendframe(resp, (log_t){0}); - if (!r) // Send succeeded - break; - } - - return r; -} - -void AVCLAN_printframe(const AVCLAN_frame_t *frame, bool binary) { - if (binary) { - uint8_t buffer[8]; - buffer[0] = 0x10; // Data Link Escape, signaling binary data forthcoming - buffer[1] = frame->is_unicast; - - // Send addresses in big-endian order - buffer[2] = (uint8_t)(frame->controller_addr >> 8); - buffer[3] = (uint8_t)frame->controller_addr; - buffer[4] = (uint8_t)(frame->peripheral_addr >> 8); - buffer[5] = (uint8_t)frame->peripheral_addr; - - buffer[6] = frame->control; - buffer[7] = frame->length; - RS232_sendbytes((uint8_t *)&buffer, 8); - RS232_sendbytes(frame->data, frame->length); - - buffer[0] = 0x17; // End of transmission block - buffer[1] = 0x0D; // \r - buffer[2] = 0x0A; // \n - RS232_sendbytes((uint8_t *)&buffer, 3); - } else { - RS232_PrintHex4(frame->is_unicast); - - RS232_Print(" 0x"); - RS232_PrintHex12(frame->controller_addr); - RS232_Print(" 0x"); - RS232_PrintHex12(frame->peripheral_addr); - - RS232_Print(" 0x"); - RS232_PrintHex4(frame->control); - - RS232_Print(" 0x"); - RS232_PrintHex4(frame->length); - - for (uint8_t i = 0; i < frame->length; i++) { - RS232_Print(" 0x"); - RS232_PrintHex8(frame->data[i]); - } - RS232_Print("\n"); - } -} - -uint8_t AVCLAN_parseframe(const uint8_t *bytes, uint8_t len, - AVCLAN_frame_t *frame) { - struct errtype { - enum : uint8_t { - TOO_SHORT = 0x01, - MISMATCH_LENGTH, - LENGTH_TOO_BIG, - } errno; - uint8_t val; - } err = {0}; - - if (len < sizeof(AVCLAN_frame_t)) { - err.errno = TOO_SHORT; - goto handle_err; - } - const uint8_t *last = bytes + len; - - frame->is_unicast = *bytes++; - frame->controller_addr = bytes[0] | ((uint16_t)bytes[1] << 8); - bytes += 2; - frame->peripheral_addr = bytes[0] | ((uint16_t)bytes[1] << 8); - bytes += 2; - frame->control = *bytes++; - frame->length = *bytes++; - - if (frame->length > MAXMSGLEN) { - err.errno = LENGTH_TOO_BIG; - err.val = frame->length; - goto handle_err; - } - - if ((bytes + frame->length) <= last) { - memcpy(frame->data, bytes, frame->length); - } else { - err.errno = MISMATCH_LENGTH; - goto handle_err; - } - - if (false) { - handle_err:; - RS232_Print("ERR(parse): "); - switch (err.errno) { - case TOO_SHORT: - RS232_Print("not enough bytes too fill AVCLAN frame"); - break; - case MISMATCH_LENGTH: - RS232_Print("frame->length is longer than remaining data"); - break; - case LENGTH_TOO_BIG: - RS232_Print("frame->length exceeds MAXMSGLEN: 0x"); - RS232_PrintHex8(err.val); - break; - default: break; - } - RS232_Print("\n"); - } - - return err.errno; -} - -// Only used for regularly scheduled periodic updates -AVCLAN_frame_t *AVCLAN_getStatusFrame() { - static uint8_t status_data[sizeof(AVCLAN_CD_Status_t) + 3] = {0}; - static AVCLAN_frame_t status = {.is_unicast = false, - .controller_addr = DEVICE_ADDR, - .peripheral_addr = 0x1FF, - .control = 0xF, - .length = sizeof(status_data), - .data = status_data}; - - return &status; -} - -// Used for changed status messages -void AVCLAN_generateStatus(AVCLAN_frame_t *status, bool is_unicast, - devices to) { - *status = (AVCLAN_frame_t){ - .is_unicast = is_unicast, - .controller_addr = DEVICE_ADDR, - .peripheral_addr = (is_unicast) ? HU_ADDR : 0x1FF, - .control = 0xF, - .length = sizeof(AVCLAN_CD_Status_t) + ((is_unicast) ? 4 : 3), - .data = status->data, // don't overwrite data pointer - }; - - uint8_t *data = status->data; - if (is_unicast) - *data++ = 0x00; - *data++ = dev_CD_CHANGER; - *data++ = to; - *data++ = Status_Report; - serializeCDStatus(data); -} - -void AVCLAN_normalizeState() { - // if (cd_status.state != cd_PLAYBACK) { - if (cd_status.mins > 99) - cd_status.mins = 0; - if (cd_status.secs > 99) - cd_status.secs = 0; - cd_status.state = cd_PLAYBACK; - cd_status.flags &= (uint8_t)~(cd_DISK_SCAN | cd_SCAN); - cd_status.flags2 = 0x80; - // } -} - -#ifndef NDEBUG - // Only used immediately below - #define XSTR(x) #x - #define STR(x) XSTR(x) - -uint16_t pulses[100]; -uint16_t periods[100]; - -void AVCLan_Measure() { - stopEvent(); - - uint8_t tmp = 0; - - RS232_Print( - "Timing config: F_CPU=" STR(F_CPU) ", TCB_CLKSEL=" STR(TCB_CLKSEL) "\n"); - RS232_Print("Sampling bit (pulse-width and period) timing...\n"); - - for (uint8_t n = 0; n < 100; n++) { - while (pulse_count == tmp) {} - pulses[n] = pulsewidth; - periods[n] = period; - tmp = pulse_count; - } - - RS232_Print("Pulses:\n"); - for (uint8_t i = 0; i < 100; i++) { - RS232_PrintHex8((uint8_t)(pulses[i] >> 8)); - RS232_PrintHex8((uint8_t)pulses[i]); - RS232_Print("\n"); - } - - RS232_Print("Periods:\n"); - for (uint8_t i = 0; i < 100; i++) { - RS232_PrintHex8((uint8_t)(periods[i] >> 8)); - RS232_PrintHex8((uint8_t)periods[i]); - RS232_Print("\n"); - } - RS232_Print("\nDone.\n"); - - startEvent(); -} -#endif