mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-06 17:03:17 +00:00
Reorganize code in prep for hardware decoupling and port to C++
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+26
-5
@@ -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=$<IF:$<BOOL:${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
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
Portions of the following source code are based on code that is
|
||||
copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#include <avr/sfr_defs.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
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 <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
Portions of the following source code are based on code that is
|
||||
copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
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 <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#include <avr/sfr_defs.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
Portions of the following source code are based on code that is
|
||||
copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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 <avr/io.h>
|
||||
#include <avr/sfr_defs.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
Portions of the following source code are based on code that is
|
||||
copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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 <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
Portions of the following source code are based on code that is
|
||||
copyright (C) 2006 Marcin Slonicki <marcin@softservice.com.pl>
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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 <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#include <stdint.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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 <stdbool.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <avr/io.h>
|
||||
#include <stdint.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
#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; }
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
AVCLAN-Mockingboard
|
||||
Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ~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
|
||||
-1570
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user