mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-24 22:35:35 +00:00
Decouple hardware specific code from generic, agnostic code
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# AVR / ATtiny3216 hardware target (port).
|
||||
|
||||
include(CMakeDependentOption)
|
||||
|
||||
# --- Port implementation sources -------------------------------------------
|
||||
target_sources(avclan PRIVATE
|
||||
phy_avr.c
|
||||
media_avr.c
|
||||
statustick_avr.c
|
||||
board_avr.c)
|
||||
|
||||
target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
find_program(AVRDUDE avrdude)
|
||||
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
|
||||
set(AVRDUDE_PORT /dev/ttyUSB0 CACHE STRING "avrdude serial port")
|
||||
set(AVRDUDE_BAUDRATE 230400 CACHE STRING "avrdude baud rate")
|
||||
|
||||
set(AVRDUDE_BASE_OPTIONS
|
||||
-p ${AVR_MCU}
|
||||
-c ${AVR_PROGRAMMER}
|
||||
-b ${AVRDUDE_BAUDRATE})
|
||||
|
||||
# --- Hardware configuration options ----------------------------------------
|
||||
set(FREQSEL 16MHz CACHE STRING "Select the operating frequency")
|
||||
set_property(CACHE FREQSEL PROPERTY STRINGS "20MHz" "16MHz")
|
||||
|
||||
if(FREQSEL MATCHES "20MHz")
|
||||
set(FREQSEL 20000000L)
|
||||
set(AVRDUDE_BASE_OPTIONS ${AVRDUDE_BASE_OPTIONS} -U osccfg:w:0x2:m)
|
||||
else()
|
||||
set(FREQSEL 16000000L)
|
||||
set(AVRDUDE_BASE_OPTIONS ${AVRDUDE_BASE_OPTIONS} -U osccfg:w:0x1:m)
|
||||
endif()
|
||||
|
||||
# Set startup time to 8 ms (0x4)
|
||||
set(AVRDUDE_BASE_OPTIONS ${AVRDUDE_BASE_OPTIONS} -U syscfg1:w:0x4:m)
|
||||
|
||||
option(CLK_PRESCALE "Enable the main clock prescaler")
|
||||
cmake_dependent_option(CLK_PRESCALE_DIV "Prescaler divisor" CLKCTRL_PDIV_2X_gc STRING "CLK_PRESCALE")
|
||||
if(DEFINED CACHE{CLK_PRESCALE_DIV})
|
||||
set_property(CACHE CLK_PRESCALE_DIV PROPERTY STRINGS
|
||||
CLKCTRL_PDIV_2X_gc
|
||||
CLKCTRL_PDIV_4X_gc
|
||||
CLKCTRL_PDIV_8X_gc
|
||||
CLKCTRL_PDIV_16X_gc
|
||||
CLKCTRL_PDIV_32X_gc
|
||||
CLKCTRL_PDIV_64X_gc
|
||||
CLKCTRL_PDIV_6X_gc
|
||||
CLKCTRL_PDIV_10X_gc
|
||||
CLKCTRL_PDIV_12X_gc
|
||||
CLKCTRL_PDIV_24X_gc
|
||||
CLKCTRL_PDIV_48X_gc
|
||||
)
|
||||
else()
|
||||
set(CLK_PRESCALE_DIV CLKCTRL_PDIV_2X_gc)
|
||||
endif()
|
||||
|
||||
set(TCB_CLKSEL "TCB_CLKSEL_CLKDIV2_gc" CACHE STRING "Choose the clock for TCB")
|
||||
set_property(CACHE TCB_CLKSEL PROPERTY STRINGS
|
||||
TCB_CLKSEL_CLKDIV1_gc
|
||||
TCB_CLKSEL_CLKDIV2_gc
|
||||
TCB_CLKSEL_CLKTCA_gc
|
||||
)
|
||||
|
||||
set(USART_RXMODE "USART_RXMODE_CLK2X_gc" CACHE STRING "USART at normal or double speed operation")
|
||||
set_property(CACHE USART_RXMODE PROPERTY STRINGS
|
||||
USART_RXMODE_CLK2X_gc
|
||||
USART_RXMODE_NORMAL_gc
|
||||
)
|
||||
|
||||
# Measured wall-clock duration (ms) of one nominal 32768-tick RTC period, used
|
||||
# to calibrate out the internal OSCULP32K's tolerance for the status-update
|
||||
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
|
||||
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
|
||||
|
||||
try_compile(LIBC_VERSION_TEST
|
||||
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
|
||||
COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
|
||||
)
|
||||
|
||||
if(NOT LIBC_VERSION_TEST)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
attiny_atpack
|
||||
URL http://packs.download.atmel.com/Atmel.ATtiny_DFP.2.0.368.atpack
|
||||
URL_HASH SHA512=ee16a8ebecb57bd998a9cd4373368e3d45982cbbc3825e18d1dcac58215db6b9d907ad1ba2020cba9187fed7ba8c6f255a4fa1214e40c7a17ab2d18474f4d079
|
||||
DOWNLOAD_NAME Atmel.ATtiny_DFP.2.0.368.atpack.zip
|
||||
)
|
||||
FetchContent_MakeAvailable(attiny_atpack)
|
||||
try_compile(LIBC_VERSION_TEST
|
||||
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
|
||||
COMPILE_DEFINITIONS
|
||||
-B "${attiny_atpack_SOURCE_DIR}/gcc/dev/${AVR_MCU}"
|
||||
-isystem "${attiny_atpack_SOURCE_DIR}/include"
|
||||
-mmcu=${AVR_MCU}
|
||||
)
|
||||
if(NOT LIBC_VERSION_TEST)
|
||||
message(FATAL_ERROR "Insufficient AVR-LIBC/Microchip pack for chosen MCU '${AVR_MCU}'")
|
||||
else()
|
||||
# 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}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- Compile definitions / options -----------------------------------------
|
||||
target_compile_definitions(avclan PUBLIC
|
||||
FREQSEL=${FREQSEL}
|
||||
CLK_PRESCALE=$<IF:$<BOOL:${CLK_PRESCALE}>,0x01,0x00>
|
||||
CLK_PRESCALE_DIV=${CLK_PRESCALE_DIV}
|
||||
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
|
||||
TCB_CLKSEL=${TCB_CLKSEL}
|
||||
USART_RXMODE=${USART_RXMODE}
|
||||
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
|
||||
)
|
||||
target_compile_options(avclan PUBLIC
|
||||
--param=min-pagesize=0
|
||||
-ffunction-sections
|
||||
-fdata-sections
|
||||
)
|
||||
|
||||
# --- Flashing --------------------------------------------------------------
|
||||
add_custom_target(flash
|
||||
${AVRDUDE} ${AVRDUDE_BASE_OPTIONS} ${AVRDUDE_OPTIONS}
|
||||
-U flash:w:$<TARGET_FILE:mockingboard>:e
|
||||
-P ${AVRDUDE_PORT}
|
||||
DEPENDS mockingboard
|
||||
COMMENT "Flashing mockingboard to ${AVR_MCU} using ${AVR_PROGRAMMER}"
|
||||
VERBATIM USES_TERMINAL
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
// ATtiny3216 board bring-up: main-clock prescaler + GPIO config for pins not
|
||||
// owned by a peripheral's own init.
|
||||
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#include <avr/xmega.h> // _PROTECTED_WRITE
|
||||
|
||||
#include "board.h"
|
||||
|
||||
void board_init(void) {
|
||||
// Main clock prescale (CLK_PRESCALE / CLK_PRESCALE_DIV come from the build).
|
||||
_PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, (CLK_PRESCALE | CLK_PRESCALE_DIV));
|
||||
|
||||
// Set pins PC2-3, PB0,3-5 as inputs
|
||||
PORTC.DIRCLR = (PIN2_bm | // Unconnected
|
||||
PIN3_bm); // CTS
|
||||
PORTB.DIRCLR = (PIN0_bm | // Unconnected
|
||||
PIN3_bm | // IGN_SENSE
|
||||
PIN4_bm | // Unused, but connected to WOC (PC0)
|
||||
PIN5_bm); // Unused, but connected to WOD (PC1)
|
||||
|
||||
// Enable pull-up resistor and disable input buffer (reduces any EM caused
|
||||
// pin toggling and saves power) for unused and unconnected pins
|
||||
PORTC.PIN2CTRL = PORT_PULLUPEN_bm | PORT_ISC_INPUT_DISABLE_gc;
|
||||
PORTB.PIN0CTRL = PORT_PULLUPEN_bm | PORT_ISC_INPUT_DISABLE_gc;
|
||||
|
||||
// TODO: Remove once IGN_SENSE hardware is fixed
|
||||
PORTB.DIRSET = PIN3_bm;
|
||||
PORTB.OUTSET = PIN3_bm;
|
||||
|
||||
// Output only pins: PA3-5, PB1-2,4-5; PC0-1
|
||||
// TODO: TxD (PA1), RTS (PA3) is output only, test if RxD needs the input
|
||||
// buffer or if the UART peripheral bypasses it
|
||||
PORTA.PIN3CTRL = PORT_ISC_INPUT_DISABLE_gc; // RTS
|
||||
PORTA.PIN4CTRL = PORT_ISC_INPUT_DISABLE_gc; // WOA
|
||||
PORTA.PIN5CTRL = PORT_ISC_INPUT_DISABLE_gc; // WOB
|
||||
PORTB.PIN1CTRL = PORT_ISC_INPUT_DISABLE_gc; // MIC_CONTROL
|
||||
PORTB.PIN4CTRL = PORT_ISC_INPUT_DISABLE_gc; // non-driving WOC
|
||||
PORTB.PIN5CTRL = PORT_ISC_INPUT_DISABLE_gc; // non-driving WOD
|
||||
PORTC.PIN0CTRL = PORT_ISC_INPUT_DISABLE_gc; // WOC
|
||||
PORTC.PIN1CTRL = PORT_ISC_INPUT_DISABLE_gc; // WOD
|
||||
}
|
||||
|
||||
void board_interruptsEnable(void) { sei(); }
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
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 "media_avr.h" // mediacontrol_syncDuringMask (used by the bus guard)
|
||||
#include "mediacontrol.h"
|
||||
|
||||
// F_CPU defined in timing_avr.h; the mic tick constants below are derived from
|
||||
// it (this hardware generation's TCA0/PB1 button-press implementation).
|
||||
#include "timing_avr.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 transport-control button press on the source device. Each action
|
||||
// maps to a press-train of a given length on MIC_CONTROL.
|
||||
void AVCLAN_mediaFunction(AVCLAN_media_fn_t fn) {
|
||||
switch (fn) {
|
||||
case MEDIA_PLAY_PAUSE: mic_pulse(1); break; // single press
|
||||
case MEDIA_SKIP_FORWARD: mic_pulse(3); break; // double-press
|
||||
case MEDIA_SKIP_BACKWARD: mic_pulse(5); break; // triple-press
|
||||
}
|
||||
}
|
||||
|
||||
// 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,32 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
// AVR-internal media-driver hooks, shared between media_avr.c and the bus
|
||||
// transaction guard (phy_avr.c's AVCLAN_stopEvent). Not part of the public
|
||||
// mediacontrol.h interface.
|
||||
|
||||
#ifndef MEDIA_AVR_H
|
||||
#define MEDIA_AVR_H
|
||||
|
||||
// Keep the TCA0 press waveform roughly in sync while a bus transaction has
|
||||
// masked interrupts (runs the OVF ISR body early if an overflow is imminent).
|
||||
// MUST be called with interrupts disabled (from within AVCLAN_stopEvent's
|
||||
// ATOMIC_BLOCK).
|
||||
void mediacontrol_syncDuringMask();
|
||||
|
||||
#endif // MEDIA_AVR_H
|
||||
@@ -0,0 +1,525 @@
|
||||
/*
|
||||
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 <util/atomic.h>
|
||||
|
||||
#include "avclan_phy.h"
|
||||
#include "cdchanger.h" // AVCLAN_isPlaying (startEvent)
|
||||
#include "com232.h" // RS232_setRxInterrupt (guard); RS232_Print (Measure)
|
||||
#include "media_avr.h" // mediacontrol_syncDuringMask (guard)
|
||||
#include "statustimer.h" // statustimer_enable/disable (guard)
|
||||
|
||||
// F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by
|
||||
// avr-libc.
|
||||
#include "timing_avr.h"
|
||||
|
||||
// Name difference between avr-libc and Microchip pack
|
||||
#if defined(EVSYS_ASYNCCH00_bm)
|
||||
#define EVSYS_ASYNCCH0_0_bm EVSYS_ASYNCCH00_bm
|
||||
#endif
|
||||
|
||||
// AVC LAN bus on AC2 (PA6/7): PA6 AINP0 (+), PA7 AINN1 (-)
|
||||
#define BUS_IS_IDLE (bit_is_clear(AC2_STATUS, AC_STATE_bp))
|
||||
|
||||
#define READING_BYTE GPIOR1
|
||||
#define READING_NBITS GPIOR2
|
||||
#define READING_PARITY GPIOR3
|
||||
|
||||
#define TCB_CNTMODE TCB_CNTMODE_PW_gc
|
||||
|
||||
static volatile uint16_t pulsewidth;
|
||||
|
||||
#ifndef NDEBUG
|
||||
static volatile uint8_t pulse_count = 0;
|
||||
static 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
|
||||
|
||||
// Returns true if device TX is muted on the AVCLAN bus (both drive pins are
|
||||
// configured as inputs).
|
||||
bool AVCLAN_ismuted() {
|
||||
return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0);
|
||||
}
|
||||
|
||||
// True when the bus is being driven (i.e. not idle/floating).
|
||||
bool AVCLAN_busActive() { return !BUS_IS_IDLE; }
|
||||
|
||||
// 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_busInit() {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Wait for and validate an incoming start bit. On an over-long "driven" bus
|
||||
// (AC2 latched high because the bus is actually floating) this kicks PA7 hard
|
||||
// high to unlatch the comparator. The framing layer maps the result to its own
|
||||
// error reporting; no printing happens here.
|
||||
avclan_readerr_t AVCLAN_readstartbit() {
|
||||
uint16_t startbitlen = TCB1.CNT = 0;
|
||||
while (!BUS_IS_IDLE) {
|
||||
startbitlen = TCB1.CNT;
|
||||
if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
|
||||
avclan_readerr_t result = rSTARTBIT_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)) {
|
||||
result = rLATCHED_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;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) {
|
||||
// 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;
|
||||
}
|
||||
return rSTARTBIT_TOO_SHORT;
|
||||
}
|
||||
return rNO_ERROR; // that was a start bit
|
||||
}
|
||||
|
||||
// Acquire the bus and emit a start bit. Returns false if another device is
|
||||
// already driving the bus (we can't yet do proper CSMA/CD).
|
||||
bool AVCLAN_sendstartbit() {
|
||||
// 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 receive to do proper CSMA/CD
|
||||
|
||||
// Beginnings of CSMA/CD
|
||||
// do {
|
||||
// if (TCB1.CNT >= (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 1.2))
|
||||
// return false; // Something's hinky; nothing is longer than start bit
|
||||
// } while (!BUS_IS_IDLE);
|
||||
// if (TCB1.CNT <= (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8))
|
||||
// return false; // Shouldn't be possible
|
||||
// set_AVC_logic_for(1, AVCLAN_STARTBIT_LOGIC_1); // wait for end of start
|
||||
return false;
|
||||
}
|
||||
AVCLAN_sendbit(bit_start);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
|
||||
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
|
||||
enabled. */
|
||||
void AVCLAN_stopEvent() {
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
statustimer_disable();
|
||||
RS232_setRxInterrupt(false);
|
||||
mediacontrol_syncDuringMask();
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable serial and periodic interrupts after a bus transaction.
|
||||
void AVCLAN_startEvent() {
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
if (AVCLAN_isPlaying()) // Reenable status interrupt if currently playing
|
||||
statustimer_enable();
|
||||
RS232_setRxInterrupt(true);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Only used immediately below
|
||||
#define XSTR(x) #x
|
||||
#define STR(x) XSTR(x)
|
||||
|
||||
static uint16_t pulses[100];
|
||||
static 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,76 @@
|
||||
/*
|
||||
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 "cdchanger.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; }
|
||||
|
||||
// Set once per overflow; consumed by the app via statustimer_tickPending().
|
||||
volatile bool tick_pending = false;
|
||||
|
||||
// Periodic interrupt with a ~1 sec period; only enabled while playing.
|
||||
ISR(RTC_CNT_vect) {
|
||||
AVCLAN_incrementTime();
|
||||
tick_pending = true;
|
||||
RTC.INTFLAGS = RTC_OVF_bm;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef TIMING_AVR_H
|
||||
#define TIMING_AVR_H
|
||||
|
||||
// AVR ATtiny3216 timing parameters. Derives F_CPU (needed by avr-libc, e.g.
|
||||
// util/delay.h) and the bus-timer (TCB) tick period from the CMake-provided
|
||||
// FREQSEL / CLK_PRESCALE / TCB_CLKSEL, then hands the generic timing.h a TICK_US
|
||||
// (microseconds per TCB tick) so the physical bit-phase durations resolve to
|
||||
// TCB-tick counts. TICK_US == TCB_TICK / 1000, so every derived constant is
|
||||
// numerically identical to the previous F_CPU/TCB_CLKSEL formulation.
|
||||
|
||||
#define __CLKCTRL_PDIV_2X_gc 2
|
||||
#define __CLKCTRL_PDIV_4X_gc 4
|
||||
#define __CLKCTRL_PDIV_8X_gc 8
|
||||
#define __CLKCTRL_PDIV_16X_gc 16
|
||||
#define __CLKCTRL_PDIV_32X_gc 32
|
||||
#define __CLKCTRL_PDIV_64X_gc 64
|
||||
#define __CLKCTRL_PDIV_6X_gc 6
|
||||
#define __CLKCTRL_PDIV_10X_gc 10
|
||||
#define __CLKCTRL_PDIV_12X_gc 12
|
||||
#define __CLKCTRL_PDIV_24X_gc 24
|
||||
#define __CLKCTRL_PDIV_48X_gc 48
|
||||
|
||||
#if CLK_PRESCALE == 0x01
|
||||
#define F_CPU (FREQSEL / __CLK_PRESCALE_DIV)
|
||||
#define CYCLE_MUL __CLK_PRESCALE_DIV
|
||||
#else
|
||||
#define F_CPU (FREQSEL)
|
||||
#define CYCLE_MUL 1
|
||||
#endif
|
||||
|
||||
// CPU_CYCLE / TCB_TICK are in nanoseconds.
|
||||
#if FREQSEL == 20000000L
|
||||
#define CPU_CYCLE (50 * CYCLE_MUL)
|
||||
#elif FREQSEL == 16000000L
|
||||
#define CPU_CYCLE (62.5 * CYCLE_MUL)
|
||||
#else
|
||||
#error "Not implemented"
|
||||
#endif
|
||||
|
||||
#ifndef TCB_CLKSEL_CLKDIV1_gc
|
||||
#define TCB_CLKSEL_CLKDIV1_gc (0x00 << 1)
|
||||
#endif
|
||||
|
||||
#ifndef TCB_CLKSEL_CLKDIV2_gc
|
||||
#define TCB_CLKSEL_CLKDIV2_gc (0x01 << 1)
|
||||
#endif
|
||||
|
||||
#ifndef TCB_CLKSEL_CLKTCA_gc
|
||||
#define TCB_CLKSEL_CLKTCA_gc (0x02 << 1)
|
||||
#endif
|
||||
|
||||
#if TCB_CLKSEL == TCB_CLKSEL_CLKDIV1_gc
|
||||
#define TCB_TICK (CPU_CYCLE)
|
||||
#elif TCB_CLKSEL == TCB_CLKSEL_CLKDIV2_gc
|
||||
#define TCB_TICK (CPU_CYCLE * 2)
|
||||
#elif TCB_CLKSEL == TCB_CLKSEL_CLKTCA_gc
|
||||
#error "Not implemented"
|
||||
#endif
|
||||
|
||||
// TCB_TICK is nanoseconds/tick; the generic timing.h wants microseconds/tick.
|
||||
#define TICK_US (TCB_TICK / 1000.0)
|
||||
|
||||
#include "timing.h"
|
||||
|
||||
#endif // TIMING_AVR_H
|
||||
Reference in New Issue
Block a user