1660 lines
43 KiB
C++
1660 lines
43 KiB
C++
/*
|
|
OqtaDrive - Sinclair Microdrive emulator
|
|
Copyright (c) 2021, Alexander Vollschwitz
|
|
|
|
developed on: Arduino Nano
|
|
|
|
This file is part of OqtaDrive.
|
|
|
|
OqtaDrive 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.
|
|
|
|
OqtaDrive 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 OqtaDrive. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
// ----------------------------------------------------------------- CONFIG ---
|
|
// This section contains all configuration items. Do not change anything below
|
|
// this section unless you know what you're doing!
|
|
//
|
|
// If you need to maintain configs for various adapters, you can alternatively
|
|
// place your settings in different header files in the `config` folder next
|
|
// to this file, one per adapter, and `#include` the desired one before
|
|
// uploading. Each header file only needs to contain the settings you want to
|
|
// change. All other settings will remain at their defaults. Note that for
|
|
// convenience, files in the `config` folder are git-ignored.
|
|
//
|
|
// NOTE:
|
|
//
|
|
// - After any firmware config changes, you need to build & flash the firmware
|
|
// to the adapter.
|
|
//
|
|
// - When flashing the firmware or performing an upgrade using the Makefile
|
|
// or the web UI, this very file will be reset to the version being
|
|
// flashed/upgraded! To make your config changes persist, do not make any
|
|
// changes here and instead use a header config file in location
|
|
// `{install folder}/oqtadrive/config.h`, e.g. `/home/pi/oqtadrive/config.h`.
|
|
//
|
|
//
|
|
//#include "config/dongle.h"
|
|
//#include "config/spectrum.h"
|
|
//#include "config/if1.h"
|
|
//#include "config/ql.h"
|
|
//#include "config/pi.h"
|
|
//#include "config/linino.h"
|
|
|
|
// Set whether read & write LEDs should be on when idling.
|
|
#ifndef LED_RW_IDLE_ON
|
|
#define LED_RW_IDLE_ON true
|
|
#endif
|
|
|
|
// Set whether the read & write LEDs should indicate that the adapter is
|
|
// waiting to sync with the daemon (LEDs alternate)
|
|
#ifndef LED_SYNC_WAIT
|
|
#define LED_SYNC_WAIT true
|
|
#endif
|
|
|
|
// rumble strength; this is a PWM setting (0-255), set to 0 for off
|
|
#ifndef RUMBLE_LEVEL
|
|
#define RUMBLE_LEVEL 35
|
|
#endif
|
|
|
|
/*
|
|
Automatic offset check only works for QL. If you're using OqtaDrive with an
|
|
actual Microdrive between IF1 and the adapter, you can set a fixed offset
|
|
here. Likewise for the QL, if the automatic check doesn't work reliably.
|
|
The offset denotes how many actual Microdrives are present between the
|
|
Microdrive interface and the adapter. So an offset of 0 means the adapter is
|
|
directly connected to the IF1 or internal Microdrive interface on the QL,
|
|
bypassing the two built-in drives. Max accepted value is 7. Keep at -1 to
|
|
use automatic offset check.
|
|
*/
|
|
#ifndef DRIVE_OFFSET_IF1
|
|
#define DRIVE_OFFSET_IF1 0
|
|
#endif
|
|
|
|
#ifndef DRIVE_OFFSET_QL
|
|
#define DRIVE_OFFSET_QL -1
|
|
#endif
|
|
|
|
/*
|
|
If you want to map hardware drives, i.e. move them to different slots within
|
|
the daisy chain, you need to chain them behind the OqtaDrive adapter. This
|
|
requires routing the COMMS_OUT signal from the adapter to the COMMS_IN of
|
|
the first hardware drive. See the documentation for more details.
|
|
|
|
Once you have set up OqtaDrive in this way, you can define here to which
|
|
slots the drives are mapped after the adapter starts up. During operation
|
|
you can then control the mapping via oqtactl. The hardware drives are always
|
|
mapped as a group. Setting start and end to 0 will deactivate the hardware
|
|
drives. Setting HW_GROUP_LOCK to true will lock the group settings so that
|
|
they cannot be changed with oqtactl.
|
|
|
|
Note: Set offsets above to 0, since hardware drive mapping requires the
|
|
OqtaDrive adapter to be first in the chain.
|
|
*/
|
|
#ifndef HW_GROUP_START
|
|
#define HW_GROUP_START 0
|
|
#endif
|
|
|
|
#ifndef HW_GROUP_END
|
|
#define HW_GROUP_END 0
|
|
#endif
|
|
|
|
#ifndef HW_GROUP_LOCK
|
|
#define HW_GROUP_LOCK true
|
|
#endif
|
|
|
|
// Use these settings to force either Interface 1 or QL, but not both! When
|
|
// left at false, automatic detection is used.
|
|
#ifndef FORCE_IF1
|
|
#define FORCE_IF1 false
|
|
#endif
|
|
|
|
#ifndef FORCE_QL
|
|
#define FORCE_QL false
|
|
#endif
|
|
|
|
/*
|
|
This is the baud rate of the serial link between adapter and daemon. It is
|
|
highly recommended to use the value defined here, which is the minimum speed
|
|
required for error free communication, and at the same time the maximum speed
|
|
at which an Arduino Nano can reliably operate the serial port. On some boards
|
|
used for running the daemon, such as the BananaPi M2 Zero, the 1 Mbps speed
|
|
is not available due to the combination of frequency and divider used to
|
|
clock its UART. For this platform, 500 kbps is currently being tested and
|
|
may work.
|
|
|
|
*/
|
|
#ifndef BAUD_RATE
|
|
#define BAUD_RATE 1000000
|
|
#endif
|
|
|
|
/*
|
|
If write protect is not working reliably with your Spectrum, the voltage
|
|
asserted to the /WR.PR line may not be high enough. An original Microdrive
|
|
outputs 9V to signal a writable cartridge, while OqtaDrive only outputs 5V.
|
|
In most cases, this is enough, but there have been reports about problems
|
|
with this. In this case, insert a transistor into the WR.PR output as
|
|
described in the project README, and change this setting to true.
|
|
*/
|
|
#ifndef WR_PROTECT_BOOST
|
|
#define WR_PROTECT_BOOST false
|
|
#endif
|
|
|
|
// ----------------------------------- END OF CONFIG - START OF DANGER ZONE ---
|
|
|
|
#include <EEPROM.h>
|
|
|
|
/*
|
|
Implementation notes:
|
|
- _delay_us only takes compile time constants as argument
|
|
*/
|
|
|
|
#define FIRMWARE_VERSION 27
|
|
|
|
// Change this to true for a calibration run. When not connecting the adapter to
|
|
// an Interface 1/QL during calibration, choose the desired interface via the
|
|
// force settings below.
|
|
#define CALIBRATION false
|
|
|
|
// --- port & pin assignments -------------------------------------------------
|
|
//
|
|
// Note: When adapting to new micro-controller, serial register, control port,
|
|
// track port, and LED port have to be figured out
|
|
//
|
|
#if defined(__AVR_ATmega328P__) // --------------------------------------------
|
|
|
|
#define SERIAL_PORT Serial
|
|
#define SERIAL_REGISTER UDR0
|
|
|
|
// port containing COMMS_IN, COMMS_OUT, COMMS_CLK, READ_WRITE, ERASE
|
|
#define CONTROL_PORT_IN PIND
|
|
#define CONTROL_PORT_OUT PORTD
|
|
|
|
// port containing the tracks
|
|
#define TRACK_PORT_IN PINC
|
|
#define TRACK_PORT_OUT PORTC
|
|
#define TRACK_PORT_DDR DDRC
|
|
|
|
// port for LEDs
|
|
#define LED_PORT PORTB
|
|
|
|
const int PIN_COMMS_CLK = 2; // HIGH idle on IF1, LOW on QL; interrupt
|
|
const int PIN_COMMS_IN = 4;
|
|
const int PIN_COMMS_OUT = 7;
|
|
const int PIN_ERASE = 5; // LOW active
|
|
const int PIN_READ_WRITE = 3; // READ is HIGH; interrupt
|
|
const int PIN_WR_PROTECT = 6; // LOW active
|
|
|
|
const int PIN_RUMBLE = 10;
|
|
const int PIN_LED_WRITE = 11;
|
|
const int PIN_LED_READ = 12;
|
|
|
|
const int PIN_TRACK_1 = A4;
|
|
const int PIN_TRACK_2 = A0;
|
|
|
|
// --- pin masks --------------------------------------------------------------
|
|
const uint8_t MASK_COMMS_CLK = B00000100;
|
|
const uint8_t MASK_COMMS_IN = B00010000;
|
|
const uint8_t MASK_COMMS_OUT = B10000000;
|
|
const uint8_t MASK_ERASE = B00100000;
|
|
const uint8_t MASK_READ_WRITE = B00001000;
|
|
const uint8_t MASK_RECORDING = MASK_ERASE | MASK_READ_WRITE;
|
|
|
|
const uint8_t MASK_LED_WRITE = B00001000;
|
|
const uint8_t MASK_LED_READ = B00010000;
|
|
|
|
|
|
#elif defined(__AVR_ATmega32U4__) // ------------------------------------------
|
|
|
|
#define SERIAL_PORT Serial1
|
|
#define SERIAL_REGISTER UDR1
|
|
|
|
// port containing COMMS_IN, COMMS_OUT, COMMS_CLK, READ_WRITE, ERASE
|
|
#define CONTROL_PORT_IN PIND
|
|
#define CONTROL_PORT_OUT PORTD
|
|
|
|
// port containing the tracks
|
|
#define TRACK_PORT_IN PINF
|
|
#define TRACK_PORT_OUT PORTF
|
|
#define TRACK_PORT_DDR DDRF
|
|
|
|
// port for LEDs
|
|
#define LED_PORT PORTB
|
|
|
|
const int PIN_COMMS_CLK = 2; // HIGH idle on IF1, LOW on QL; interrupt
|
|
const int PIN_COMMS_IN = 4;
|
|
const int PIN_COMMS_OUT = 6;
|
|
const int PIN_ERASE = 12; // LOW active
|
|
const int PIN_READ_WRITE = 3; // READ is HIGH; interrupt
|
|
const int PIN_WR_PROTECT = 8; // LOW active
|
|
|
|
const int PIN_RUMBLE = 9;
|
|
const int PIN_LED_WRITE = 11;
|
|
const int PIN_LED_READ = 10;
|
|
|
|
const int PIN_TRACK_1 = A3;
|
|
const int PIN_TRACK_2 = A5;
|
|
|
|
// --- pin masks --------------------------------------------------------------
|
|
const uint8_t MASK_COMMS_CLK = B00000010;
|
|
const uint8_t MASK_COMMS_IN = B00010000;
|
|
const uint8_t MASK_COMMS_OUT = B10000000;
|
|
const uint8_t MASK_ERASE = B01000000;
|
|
const uint8_t MASK_READ_WRITE = B00000001;
|
|
const uint8_t MASK_RECORDING = MASK_ERASE | MASK_READ_WRITE;
|
|
|
|
const uint8_t MASK_LED_WRITE = B10000000;
|
|
const uint8_t MASK_LED_READ = B01000000;
|
|
|
|
#endif
|
|
|
|
// --- common pin masks -------------------------------------------------------
|
|
const uint8_t MASK_TRACK_1 = B00010000;
|
|
const uint8_t MASK_TRACK_2 = B00000001;
|
|
const uint8_t MASK_BOTH_TRACKS = MASK_TRACK_1 | MASK_TRACK_2;
|
|
|
|
// --- LED behavior & rumble --------------------------------------------------
|
|
const bool ACTIVE = true;
|
|
const bool IDLE = false;
|
|
|
|
uint8_t blinkCount = 0;
|
|
uint8_t rumbleLevel = RUMBLE_LEVEL;
|
|
|
|
// --- tape format ------------------------------------------------------------
|
|
const int PREAMBLE_LENGTH = 12;
|
|
const int HEADER_LENGTH_IF1 = 27;
|
|
const int RECORD_LENGTH_IF1 = 540;
|
|
const int HEADER_LENGTH_QL = 28;
|
|
const int RECORD_LENGTH_QL = 538;
|
|
const int RECORD_EXTRA_IF1 = 99; // during format, Interface 1 with V2 ROM
|
|
const int RECORD_EXTRA_QL = 86; // and QLs send longer records
|
|
|
|
uint16_t headerLengthMux;
|
|
uint16_t recordLengthMux;
|
|
uint16_t sectorLengthMux;
|
|
|
|
// --- timer pre-loads --------------------------------------------------------
|
|
// values lower than 256 use a 256 pre-scaler (1 tick is 16us), values 256 and
|
|
// above use a 1024 pre-scaler (1 tick is 64us)
|
|
const int TIMER_COMMS = 512 - 157; // 10 msec
|
|
const int TIMER_HEADER_GAP_IF1 = 256 - 234; // 3.75 msec
|
|
const int TIMER_HEADER_GAP_QL = 256 - 225; // 3.60 msec
|
|
|
|
// --- drive select -----------------------------------------------------------
|
|
volatile uint8_t commsRegister = 0;
|
|
volatile uint8_t commsClkCount = 0;
|
|
volatile uint8_t activeDrive = 0;
|
|
volatile uint8_t lastActiveDrive = 0;
|
|
volatile uint8_t driveOffset = 0xff;
|
|
volatile uint8_t hwGroupStart = 0;
|
|
volatile uint8_t hwGroupEnd = 0;
|
|
volatile uint8_t maskHwOffset = 0;
|
|
|
|
bool commsClkState;
|
|
|
|
const uint8_t DRIVE_STATE_UNKNOWN = 0x80;
|
|
const uint8_t DRIVE_FLAG_LOADED = 1;
|
|
const uint8_t DRIVE_FLAG_FORMATTED = 2;
|
|
const uint8_t DRIVE_FLAG_READONLY = 4;
|
|
const uint8_t DRIVE_READABLE = DRIVE_FLAG_LOADED | DRIVE_FLAG_FORMATTED;
|
|
|
|
volatile uint8_t driveState = DRIVE_STATE_UNKNOWN;
|
|
|
|
// --- interrupt handler ------------------------------------------------------
|
|
typedef void (* TimerHandler)();
|
|
void setTimer(int preload, TimerHandler h);
|
|
void enableTimer(bool on, int preload, TimerHandler h);
|
|
TimerHandler timerHandler = NULL;
|
|
|
|
// --- sector buffer ----------------------------------------------------------
|
|
const uint16_t BUF_LENGTH = 10 +
|
|
max(HEADER_LENGTH_IF1, HEADER_LENGTH_QL) +
|
|
max(RECORD_LENGTH_IF1 + RECORD_EXTRA_IF1, RECORD_LENGTH_QL + RECORD_EXTRA_QL);
|
|
uint8_t buffer[BUF_LENGTH];
|
|
|
|
// --- message buffer ---------------------------------------------------------
|
|
uint8_t msgBuffer[4];
|
|
|
|
// --- Microdrive interface type - Interface 1 or QL --------------------------
|
|
bool IF1 = true;
|
|
#define QL !IF1
|
|
bool confirmInterface = true;
|
|
|
|
// --- state flags ------------------------------------------------------------
|
|
volatile bool spinning = false;
|
|
volatile bool recording = false;
|
|
volatile bool message = false;
|
|
volatile bool headerGap = false;
|
|
volatile bool calibration = false; // use the define setting at top to turn on!
|
|
volatile bool synced = false;
|
|
|
|
// --- daemon commands --------------------------------------------------------
|
|
const uint8_t PROTOCOL_VERSION = 4;
|
|
|
|
const char CMD_HELLO = 'h';
|
|
const char CMD_VERSION = 'v';
|
|
const char CMD_PING = 'P';
|
|
const char CMD_STATUS = 's';
|
|
const char CMD_GET = 'g';
|
|
const char CMD_PUT = 'p';
|
|
const char CMD_VERIFY = 'y';
|
|
const char CMD_MAP = 'm';
|
|
const char CMD_DEBUG = 'd';
|
|
const char CMD_RESYNC = 'r';
|
|
const char CMD_CONFIG = 'c';
|
|
|
|
const char CMD_CONFIG_RUMBLE = 'r';
|
|
|
|
const uint8_t CMD_LENGTH = 4;
|
|
const uint16_t PAYLOAD_LENGTH = BUF_LENGTH - CMD_LENGTH;
|
|
|
|
const char DAEMON_PING[] = {CMD_PING, 'i', 'n', 'g'};
|
|
const char DAEMON_PONG[] = {CMD_PING, 'o', 'n', 'g'};
|
|
const char DAEMON_HELLO[] = {CMD_HELLO, 'l' , 'o', 'd'};
|
|
const char IF1_HELLO[] = {CMD_HELLO, 'l' , 'o', 'i'};
|
|
const char QL_HELLO[] = {CMD_HELLO, 'l' , 'o', 'q'};
|
|
|
|
const uint8_t MASK_IF1 = 1;
|
|
const uint8_t MASK_QL = 2;
|
|
|
|
const unsigned long DAEMON_TIMEOUT = 5000;
|
|
const unsigned long RESYNC_THRESHOLD = 4500;
|
|
const unsigned long PING_INTERVAL = 10000;
|
|
|
|
unsigned long lastPing = 0;
|
|
|
|
// ------------------------------------------------------------------ SETUP ---
|
|
|
|
//
|
|
void setup() {
|
|
|
|
deactivateSignals();
|
|
|
|
loadState();
|
|
|
|
// FIXME: does this need deactivation?
|
|
pinMode(PIN_COMMS_OUT, OUTPUT);
|
|
digitalWrite(PIN_COMMS_OUT, LOW);
|
|
|
|
// rumble & LEDs
|
|
pinMode(PIN_RUMBLE, OUTPUT);
|
|
pinMode(PIN_LED_WRITE, OUTPUT);
|
|
pinMode(PIN_LED_READ, OUTPUT);
|
|
ledRead(IDLE);
|
|
ledWrite(IDLE);
|
|
|
|
detectInterface(false, false, false);
|
|
|
|
// open channel to daemon
|
|
SERIAL_PORT.begin(BAUD_RATE, SERIAL_8N1);
|
|
SERIAL_PORT.setTimeout(DAEMON_TIMEOUT);
|
|
|
|
// set up interrupts
|
|
attachInterrupt(digitalPinToInterrupt(PIN_COMMS_CLK), commsClk, CHANGE);
|
|
attachInterrupt(digitalPinToInterrupt(PIN_READ_WRITE), writeReq, FALLING);
|
|
|
|
rumbleGreet();
|
|
startupTests();
|
|
}
|
|
|
|
//
|
|
void startupTests() {
|
|
if (!testBuffer()) {
|
|
errorBlink(1, 2, 3); // bad RAM
|
|
}
|
|
}
|
|
|
|
//
|
|
void remoteConfig(uint8_t item, uint8_t arg1, uint8_t arg2) {
|
|
switch (item) {
|
|
case CMD_CONFIG_RUMBLE:
|
|
rumbleLevel = arg1;
|
|
rumbleGreet();
|
|
saveState();
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
void setHWGroup(uint8_t start, uint8_t end) {
|
|
if (start <= 8 && end <= 8 && start <= end) {
|
|
hwGroupStart = start;
|
|
hwGroupEnd = end;
|
|
maskHwOffset = 1 << (start - 2);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------- LOOP ---
|
|
|
|
void loop() {
|
|
|
|
// FIXME: verify that serial is only accessed from main loop
|
|
|
|
if (CALIBRATION) {
|
|
calibrate(0x0f);
|
|
}
|
|
|
|
if (!synced) {
|
|
driveOff();
|
|
daemonSync();
|
|
}
|
|
|
|
debugFlush();
|
|
ensureDriveState();
|
|
|
|
if (spinning) {
|
|
|
|
if (recording) {
|
|
record();
|
|
} else {
|
|
replay();
|
|
}
|
|
|
|
if (blinkCount++ % 2 == 0) {
|
|
if (recording) {
|
|
ledRead(IDLE);
|
|
ledWriteFlip();
|
|
} else {
|
|
ledWrite(IDLE);
|
|
ledReadFlip();
|
|
}
|
|
}
|
|
|
|
lastPing = millis() + 1500 - PING_INTERVAL;
|
|
|
|
} else if (daemonPing()) {
|
|
// After a successful ping exchange, open a brief window during which
|
|
// the daemon may send control commands. The daemon must not send
|
|
// commands at any other time.
|
|
daemonCheckControl();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------- Interface 1 / QL HANDLING ---
|
|
|
|
bool detectInterface(bool if1, bool ql, bool quickCheck) {
|
|
|
|
bool prev = IF1;
|
|
|
|
if1 = FORCE_IF1 ? true : if1;
|
|
ql = FORCE_QL ? true : ql;
|
|
|
|
if (if1) {
|
|
IF1 = true;
|
|
|
|
} else if (ql) {
|
|
IF1 = false;
|
|
|
|
} else {
|
|
// Idle level of COMMS_CLK is HIGH for Interface 1, LOW for QL.
|
|
// Sample COMMS_CLK line for two seconds to find out.
|
|
|
|
// avoid floating input during detect
|
|
pinMode(PIN_COMMS_CLK, INPUT_PULLUP);
|
|
|
|
if (quickCheck) {
|
|
IF1 = (CONTROL_PORT_IN & MASK_COMMS_CLK) != 0;
|
|
|
|
} else {
|
|
uint8_t high = 0, low = 0;
|
|
for (uint8_t i = 0; i < 21; i++) {
|
|
(CONTROL_PORT_IN & MASK_COMMS_CLK) == 0 ? low++ : high++;
|
|
delay(100);
|
|
}
|
|
IF1 = high > low;
|
|
}
|
|
|
|
// restore to input without pull-up; whenever detecInterface is called,
|
|
// COMMS_CLK input is configured that way; should that ever change, we
|
|
// need to get current state above and then restore here
|
|
pinMode(PIN_COMMS_CLK, INPUT);
|
|
}
|
|
|
|
if (IF1) {
|
|
if ((-1 < DRIVE_OFFSET_IF1) && (DRIVE_OFFSET_IF1 < 8)) {
|
|
driveOffset = DRIVE_OFFSET_IF1;
|
|
}
|
|
headerLengthMux = HEADER_LENGTH_IF1 + 1;
|
|
recordLengthMux = RECORD_LENGTH_IF1 + 1;
|
|
commsClkState = true;
|
|
|
|
} else {
|
|
if ((-1 < DRIVE_OFFSET_QL) && (DRIVE_OFFSET_QL < 8)) {
|
|
driveOffset = DRIVE_OFFSET_QL;
|
|
}
|
|
headerLengthMux = HEADER_LENGTH_QL + 1;
|
|
recordLengthMux = RECORD_LENGTH_QL + 1;
|
|
commsClkState = false;
|
|
}
|
|
|
|
sectorLengthMux = headerLengthMux + recordLengthMux;
|
|
|
|
return IF1 != prev;
|
|
}
|
|
|
|
// ----------------------------------------------------- READ/WRITE CONTROL ---
|
|
|
|
/*
|
|
Check whether the WRITE or ERASE line is active (indicates recording),
|
|
or an impending drive state change is indicated. Switches recording and
|
|
spinning states accordingly, and returns true if any of the above is the
|
|
case. This is used repeatedly while in replay mode to find out whether we
|
|
need to bail out.
|
|
|
|
Note: Any change in this function requires re-calibration of replay!
|
|
|
|
TODO: Consider whether to consolidate this with checkReplayOrStop, since
|
|
most of the code is the same. We may not want to do this though for
|
|
timing reasons.
|
|
*/
|
|
bool checkRecordingOrStop() {
|
|
|
|
uint8_t state = CONTROL_PORT_IN;
|
|
|
|
// turn recording on, but never off here
|
|
recording = recording || ((state & MASK_RECORDING) != MASK_RECORDING);
|
|
|
|
// when recording, flip track mode here already
|
|
// to give it more time to switch over
|
|
if (recording) {
|
|
setTracksToRecord();
|
|
}
|
|
|
|
// turn spinning off, but never on here; change in drive state is indicated
|
|
// for IF1 by COMMS_CLK going active (i.e. LOW), and for QL by going inactive
|
|
// (also LOW)
|
|
bool prev = commsClkState;
|
|
commsClkState = (state & MASK_COMMS_CLK) != 0;
|
|
spinning = spinning && (!prev || commsClkState);
|
|
|
|
return !spinning || recording;
|
|
}
|
|
|
|
/*
|
|
Check whether both WRITE and ERASE lines are inactive (indicates replay),
|
|
or an impending drive state change is indicated. Switches recording and
|
|
spinning states accordingly, and returns true if any of the above is the
|
|
case. This is used repeatedly while in record mode to find out whether we
|
|
need to bail out.
|
|
*/
|
|
bool checkReplayOrStop() {
|
|
|
|
uint8_t state = CONTROL_PORT_IN;
|
|
|
|
// turn recording off, but never on here
|
|
recording = recording && ((state & MASK_RECORDING) != MASK_RECORDING);
|
|
|
|
if (!recording) {
|
|
setTracksToReplay();
|
|
}
|
|
|
|
// turn spinning off, but never on here; change in drive state is indicated
|
|
// for IF1 by COMMS_CLK going active (i.e. LOW), and for QL by going inactive
|
|
// (also LOW)
|
|
bool prev = commsClkState;
|
|
commsClkState = (state & MASK_COMMS_CLK) != 0;
|
|
spinning = spinning && (!prev || commsClkState);
|
|
|
|
return !(spinning && recording);
|
|
}
|
|
|
|
// interrupt handler; switches tracks to record mode
|
|
void writeReq() {
|
|
if (activeDrive > 0) {
|
|
setTracksToRecord();
|
|
}
|
|
}
|
|
|
|
// interrupt handler; signals the end of the header gap
|
|
void endHeaderGap() {
|
|
headerGap = false;
|
|
}
|
|
|
|
/*
|
|
Tracks in RECORD mode means the Arduino reads incoming data. I.e. when the
|
|
Interface 1/QL wants to write to the Microdrive, the two track data pins
|
|
need to be put into input mode.
|
|
*/
|
|
void setTracksToRecord() {
|
|
TRACK_PORT_DDR = 0;
|
|
TRACK_PORT_OUT = 0x3f;
|
|
}
|
|
|
|
/*
|
|
Tracks in REPLAY mode means the Arduino sends data. I.e. when the
|
|
Interface 1/QL wants to read from the Microdrive, the two track data pins
|
|
need to be put into output mode.
|
|
*/
|
|
void setTracksToReplay() {
|
|
TRACK_PORT_DDR = MASK_BOTH_TRACKS;
|
|
TRACK_PORT_OUT = 0x3f; // idle level is HIGH
|
|
}
|
|
|
|
//
|
|
void setWriteProtect(bool protect) {
|
|
|
|
if (WR_PROTECT_BOOST) {
|
|
// This is used when an additional PNP transistor is added to pin D6,
|
|
// to overcome problems with write protect always being on (see README).
|
|
if (protect) { // think inverted here
|
|
// To signal that the cartridge is write protected, we must not
|
|
// assert any voltage on the WR.PR line, so we need to keep the
|
|
// transistor off. This is done by switching the pin to input
|
|
// mode, i.e. no voltage, high impedance.
|
|
pinMode(PIN_WR_PROTECT, INPUT);
|
|
} else {
|
|
// To signal that the cartridge is writable, we need to assert 9V
|
|
// on WR.PR, so we output LOW on pin D6, to drive the transistor
|
|
// open.
|
|
pinMode(PIN_WR_PROTECT, OUTPUT);
|
|
digitalWrite(PIN_WR_PROTECT, LOW);
|
|
}
|
|
|
|
} else {
|
|
// no transistor, directly driving WR.PR with pin D6
|
|
pinMode(PIN_WR_PROTECT, OUTPUT);
|
|
digitalWrite(PIN_WR_PROTECT, protect ? LOW : HIGH);
|
|
}
|
|
}
|
|
|
|
//
|
|
void activateSignals() {
|
|
// control signals
|
|
pinMode(PIN_READ_WRITE, INPUT_PULLUP);
|
|
pinMode(PIN_ERASE, INPUT_PULLUP);
|
|
pinMode(PIN_COMMS_CLK, INPUT_PULLUP);
|
|
|
|
// This must not be set to INPUT_PULLUP. If there is a Microdrive upstream
|
|
// of the adapter in the daisy chain, the pull-up resistor would feed into
|
|
// that drive's COMMS_CLK output and confuse it.
|
|
pinMode(PIN_COMMS_IN, INPUT);
|
|
}
|
|
|
|
// When drive is off, signal lines should be set to the least intrusive mode
|
|
// possible, to avoid interfering with any actual Microdrives that may be
|
|
// present. This is INPUT without pull-up, which for the ATmega328P has a
|
|
// typical input leakage current of 1uA.
|
|
void deactivateSignals() {
|
|
// control signals
|
|
pinMode(PIN_READ_WRITE, INPUT);
|
|
pinMode(PIN_ERASE, INPUT);
|
|
pinMode(PIN_COMMS_CLK, INPUT);
|
|
pinMode(PIN_COMMS_IN, INPUT);
|
|
pinMode(PIN_WR_PROTECT, INPUT);
|
|
|
|
// tracks off
|
|
TRACK_PORT_DDR = 0;
|
|
TRACK_PORT_OUT = 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------- DRIVE CONTROL ---
|
|
|
|
//
|
|
void driveOff() {
|
|
stopTimer();
|
|
deactivateSignals();
|
|
ledWrite(IDLE);
|
|
ledRead(IDLE);
|
|
spinning = false;
|
|
recording = false;
|
|
headerGap = false;
|
|
rumble(false);
|
|
}
|
|
|
|
//
|
|
void driveOn() {
|
|
activateSignals();
|
|
setTracksToReplay();
|
|
headerGap = false;
|
|
driveState = DRIVE_STATE_UNKNOWN;
|
|
recording = false;
|
|
spinning = true;
|
|
rumble(true);
|
|
// once we managed to turn on a drive, there's no more
|
|
// need to confirm the interface
|
|
confirmInterface = false;
|
|
}
|
|
|
|
// Active level of COMMS_CLK is LOW for Interface 1, HIGH for QL.
|
|
bool isCommsClk(uint8_t state) {
|
|
return (state & MASK_COMMS_CLK) == (IF1 ? 0 : MASK_COMMS_CLK);
|
|
}
|
|
|
|
/*
|
|
Interrupt handler for handling the 1 bit being pushed through the shift
|
|
register formed by the up to eight actual Microdrives (daisy chain), to
|
|
select the active drive. If the OqtaDrive adapter is connected directly to
|
|
the Microdrive interface, commsRegister represents the complete shift
|
|
register. If there are actual Microdrives present before the adapter, then
|
|
commsRegister only represents the remainder of that shift register.
|
|
*/
|
|
void commsClk() {
|
|
|
|
uint8_t d = CONTROL_PORT_IN;
|
|
bool comms = (d & MASK_COMMS_IN) != 0;
|
|
|
|
if (hwGroupStart == 1) {
|
|
// immediately pass through COMMS when h/w drives are first in chain
|
|
CONTROL_PORT_OUT = comms ? d | MASK_COMMS_OUT : d & ~MASK_COMMS_OUT;
|
|
}
|
|
|
|
if (isCommsClk(d)) {
|
|
// When COMMS_CLK goes active, we increase the clock count, shift the
|
|
// register by one, and add current COMMS state at the start.
|
|
|
|
stopTimer();
|
|
|
|
if ((driveOffset == 0xff) && QL && comms && (commsClkCount < 8)) {
|
|
// When we see the 1 bit at COMMS_CLK going active, clock count is
|
|
// the drive offset, with an offset of 0 meaning first drive in chain.
|
|
driveOffset = commsClkCount;
|
|
}
|
|
|
|
commsClkCount++;
|
|
commsRegister = commsRegister << 1;
|
|
if (comms) {
|
|
commsRegister |= 1;
|
|
}
|
|
|
|
} else {
|
|
// COMMS_CLK going inactive triggers the shift register. If there are
|
|
// h/w drives present further down the chain, we therefore sent them
|
|
// the COMMS signal here, with the correct delay using commsRegister.
|
|
if (hwGroupStart > 1) {
|
|
CONTROL_PORT_OUT = (commsRegister & maskHwOffset) != 0 ?
|
|
d | MASK_COMMS_OUT : d & ~MASK_COMMS_OUT;
|
|
}
|
|
setTimer(TIMER_COMMS, selectDrive);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Called by timer when TIMER_COMMS expires. That is, if for a duration of
|
|
TIMER_COMMS, there has been no change on the COMMS_CLK line, then the active
|
|
drive has been selected, or all drives deselected, by Interface 1/QL.
|
|
*/
|
|
void selectDrive() {
|
|
|
|
if (QL && isCommsClk(CONTROL_PORT_IN)) {
|
|
// QL switches COMMS_CLK to HIGH and keeps it HIGH as long as it's
|
|
// interested in reading more data. Should the timer fire when we're
|
|
// still reading, we just discard it.
|
|
return;
|
|
}
|
|
|
|
if (!synced) {
|
|
return;
|
|
}
|
|
|
|
uint8_t last = activeDrive;
|
|
activeDrive = 0;
|
|
|
|
// A drive offset of 0xff means we don't know yet, and that also means
|
|
// none of the virtual drives can possibly have been selected.
|
|
if (driveOffset != 0xff) {
|
|
for (uint8_t reg = IF1 ? commsRegister : commsRegister << driveOffset;
|
|
reg > 0; reg >>= 1) {
|
|
activeDrive++;
|
|
}
|
|
}
|
|
|
|
debugMsg('C', 'K', commsClkCount);
|
|
debugFlush();
|
|
debugMsg('C', 'R', commsRegister);
|
|
debugFlush();
|
|
debugMsg('O', 'F', driveOffset);
|
|
debugFlush();
|
|
debugMsg('D', 'R', activeDrive);
|
|
debugFlush();
|
|
|
|
if (driveOffset != 0xff || commsClkCount > 7) {
|
|
// As soon as the drive offset has been determined, we can reset comms
|
|
// clock count each time a drive is selected. We can do this also if the
|
|
// count goes beyond the maximum offset (7), which happens when
|
|
// resetting the QL.
|
|
commsClkCount = 0;
|
|
}
|
|
|
|
// avoid turning on a virtual drive when a h/w drive has been selected
|
|
if (activeDrive <= driveOffset
|
|
|| (hwGroupStart <= activeDrive && activeDrive <= hwGroupEnd)) {
|
|
activeDrive = 0;
|
|
}
|
|
|
|
if (activeDrive == 0) {
|
|
lastActiveDrive = last;
|
|
driveOff();
|
|
} else {
|
|
lastActiveDrive = activeDrive;
|
|
driveOn();
|
|
}
|
|
}
|
|
|
|
/*
|
|
Retrieve the drive state from the daemon, if it is still unknown.
|
|
Otherwise, cached value is used.
|
|
*/
|
|
void ensureDriveState() {
|
|
|
|
if (spinning && (driveState != DRIVE_STATE_UNKNOWN)) {
|
|
return;
|
|
}
|
|
|
|
if (!spinning && (driveState == DRIVE_STATE_UNKNOWN)) {
|
|
return;
|
|
}
|
|
|
|
// When a drive is turned off, selectDrive() may have been hit earlier then
|
|
// this function (seen with QL + Minerva ROM), so activeDrive will already
|
|
// be 0. Previous active drive is therefore saved in lastActiveDrive.
|
|
uint8_t drive = spinning ? activeDrive :
|
|
(activeDrive == 0 ? lastActiveDrive : activeDrive);
|
|
|
|
daemonCmdArgs(CMD_STATUS, drive, spinning ? 1 : 0, 0, 0);
|
|
|
|
if (spinning) {
|
|
for (int r = 0; r < 400; r++) {
|
|
if (SERIAL_PORT.available() > 0) {
|
|
driveState = SERIAL_PORT.read();
|
|
setWriteProtect(!isDriveWritable());
|
|
return;
|
|
}
|
|
delay(5);
|
|
}
|
|
synced = false;
|
|
|
|
} else {
|
|
driveState = DRIVE_STATE_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
//
|
|
bool isDriveReadable() {
|
|
return (driveState & DRIVE_READABLE) == DRIVE_READABLE;
|
|
}
|
|
|
|
//
|
|
bool isDriveWritable() {
|
|
return (driveState & DRIVE_FLAG_READONLY) == 0;
|
|
}
|
|
|
|
// -------------------------------------------------------------- RECORDING ---
|
|
|
|
void record() {
|
|
|
|
bool formatting = false;
|
|
uint8_t blocks = 0;
|
|
ledWrite(ACTIVE);
|
|
|
|
do {
|
|
daemonPendingCmd(CMD_PUT, activeDrive, 0);
|
|
uint16_t read = receiveBlock();
|
|
blocks++;
|
|
|
|
if (blocks % 4 == 0) {
|
|
ledWriteFlip();
|
|
}
|
|
|
|
// block stop marker; used by the daemon to get rid of spurious
|
|
// extra bytes at the end of a block, often seen on the QL
|
|
if (read > 0) {
|
|
daemonCmdArgs(3, 2, 1, 0, 0);
|
|
}
|
|
|
|
read += PREAMBLE_LENGTH; // preamble is not sent to daemon
|
|
|
|
if (read < headerLengthMux) {
|
|
break; // nothing useful received
|
|
} else if (read < recordLengthMux) {
|
|
// headers are only written during format
|
|
// when that happens, we stay here
|
|
formatting = true;
|
|
ledRead(IDLE);
|
|
}
|
|
|
|
} while (formatting);
|
|
|
|
if (formatting) {
|
|
driveState = DRIVE_FLAG_LOADED | DRIVE_FLAG_FORMATTED;
|
|
}
|
|
|
|
ledWrite(IDLE);
|
|
checkReplayOrStop();
|
|
}
|
|
|
|
/*
|
|
Receive a block (header or record). Change in drive state is checked only
|
|
while waiting for the start of the block. Once the block starts, no further
|
|
checks are done. End of data from Interface 1/QL however is detected.
|
|
|
|
We're reading both tracks simultaneously. Pin assignments are chosen such
|
|
that one track is at bit position 0, the other at 4. 4 bits from each track
|
|
are ORed into `d`, with a left shift before each OR.
|
|
|
|
--------------- track 1
|
|
| --- track 2
|
|
bit | |
|
|
position: 7 6 5 4 3 2 1 0
|
|
TRACK_PORT: X X X | X X X | X = don't care
|
|
A N D | |
|
|
MASK: 0 0 0 1 0 0 0 1
|
|
O R | |
|
|
d: [ track 1 *][ track 2 *] << before each OR
|
|
|
|
`d` is then forwarded to the daemon over the serial line. This is repeated
|
|
until the block (header or record) is done. The number of bytes read is
|
|
returned. The receiving side takes care of demuxing the data, additionally
|
|
considering that the tracks are shifted by 4 bits relative to one another.
|
|
*/
|
|
uint16_t receiveBlock() {
|
|
|
|
noInterrupts();
|
|
|
|
register uint8_t start = TRACK_PORT_IN & MASK_BOTH_TRACKS, end, bitCount, d, w;
|
|
register uint16_t read = 0, ww;
|
|
|
|
for (ww = 0xffff; ww > 0; ww--) {
|
|
// sync on first bit change of block, on either track
|
|
if (((TRACK_PORT_IN & MASK_BOTH_TRACKS) ^ start) != 0) {
|
|
break;
|
|
}
|
|
// but don't wait forever, and bail out when activity on COMMS_CLK
|
|
// indicates impending change of drive state
|
|
if (checkReplayOrStop() || ww == 1) {
|
|
SERIAL_REGISTER = ww == 1 ? 2 : 1; // cancel pending PUT command
|
|
interrupts();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// search for sync pattern, which is 10*'0' followed by 2*'ff'; we therefore
|
|
// look for at least 24 consecutive zeros on both tracks, followed by eight
|
|
// ones on at least one track.
|
|
register uint8_t zeros = 0, ones = 0;
|
|
while (zeros < 24 || ones < 8) {
|
|
|
|
for (w = 0xff; (((TRACK_PORT_IN & MASK_BOTH_TRACKS) ^ start) == 0) && w > 0; w--);
|
|
|
|
if (w == 0) { // could not sync
|
|
SERIAL_REGISTER = 3; // cancel pending PUT command
|
|
interrupts();
|
|
return 0;
|
|
}
|
|
|
|
_delay_us(2.0); // short delay to make sure track state has settled
|
|
start = TRACK_PORT_IN & MASK_BOTH_TRACKS;
|
|
|
|
if (IF1) _delay_us(6.50); else _delay_us(4.50);
|
|
|
|
if (((end = TRACK_PORT_IN & MASK_BOTH_TRACKS) ^ start) == 0) {
|
|
if (ones > 0) {
|
|
ones = 0;
|
|
zeros = 1;
|
|
} else {
|
|
zeros++;
|
|
}
|
|
} else {
|
|
ones++;
|
|
}
|
|
start = end;
|
|
}
|
|
|
|
SERIAL_REGISTER = 0; // complete pending PUT command to go ahead
|
|
|
|
while (true) {
|
|
|
|
d = 0;
|
|
|
|
for (bitCount = 4; bitCount > 0; bitCount--) {
|
|
|
|
// wait for start of cycle, or end of block
|
|
// skipped when coming here for the first time
|
|
for (w = 0x20; (((TRACK_PORT_IN & MASK_BOTH_TRACKS) ^ start) == 0) && w > 0; w--);
|
|
|
|
if (w == 0) { // end of block
|
|
interrupts();
|
|
return read;
|
|
}
|
|
|
|
_delay_us(2.0); // short delay to make sure track state has settled
|
|
start = TRACK_PORT_IN & MASK_BOTH_TRACKS; //then take start reading
|
|
// and wait for end of cycle
|
|
if (IF1) _delay_us(6.50); else _delay_us(4.50);
|
|
// When a track has changed state compared to start of cycle at this
|
|
// point, then it carries a 1 in this cycle, otherwise a 0.
|
|
d = (d << 1) | ((end = TRACK_PORT_IN & MASK_BOTH_TRACKS) ^ start); // store
|
|
start = end; // prepare for next cycle
|
|
}
|
|
|
|
SERIAL_REGISTER = d; // send over serial
|
|
read++;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------- REPLAY ---
|
|
|
|
/*
|
|
Replay one sector. Switching over into record mode or stopping of drive is
|
|
continuously monitored, and if detected, returns immediately.
|
|
*/
|
|
void replay() {
|
|
|
|
if (checkRecordingOrStop() || !isDriveReadable()) {
|
|
return;
|
|
}
|
|
|
|
daemonCmdArgs(CMD_GET, activeDrive, 0, 0, 0);
|
|
|
|
unsigned long start = millis();
|
|
uint16_t rcv = daemonRcv(0);
|
|
if (rcv == 0) {
|
|
synced = millis() - start < RESYNC_THRESHOLD;
|
|
return;
|
|
}
|
|
|
|
// header
|
|
if (replayBlock(buffer + CMD_LENGTH, headerLengthMux)) {
|
|
return;
|
|
}
|
|
|
|
headerGap = true;
|
|
if (timerEnabled()) { // COMMS_CLK timer may be active at this point
|
|
stopTimer();
|
|
}
|
|
setTimer(IF1 ? TIMER_HEADER_GAP_IF1 : TIMER_HEADER_GAP_QL, endHeaderGap);
|
|
|
|
while (headerGap) {
|
|
if (checkRecordingOrStop()) {
|
|
return;
|
|
}
|
|
_delay_us(20.0);
|
|
}
|
|
|
|
// record - for QL, this can be two different lengths due to extra bytes
|
|
// being sent during format
|
|
replayBlock(buffer + CMD_LENGTH + headerLengthMux, rcv - headerLengthMux);
|
|
}
|
|
|
|
/*
|
|
Get a sector from daemon and immediately reflect it back. For reliability
|
|
testing. FIXME: validate
|
|
*/
|
|
void verify() {
|
|
daemonCmdArgs(CMD_GET,
|
|
lowByte(sectorLengthMux), highByte(sectorLengthMux), ' ', 0);
|
|
daemonRcv(sectorLengthMux);
|
|
// send back for verification
|
|
daemonCmdArgs(CMD_VERIFY,
|
|
lowByte(sectorLengthMux), highByte(sectorLengthMux), ' ',
|
|
sectorLengthMux);
|
|
}
|
|
|
|
/*
|
|
Indefinitely replays the given pattern for checking wave form with
|
|
oscilloscope.
|
|
*/
|
|
void calibrate(uint8_t pattern) {
|
|
calibration = true;
|
|
for (int ix = 0; ix < BUF_LENGTH; ix++) {
|
|
buffer[ix] = pattern;
|
|
}
|
|
setTracksToReplay();
|
|
while (true) {
|
|
replayBlock(buffer, BUF_LENGTH);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Replay a block of bytes to the Interface 1/QL. Switching over to recording
|
|
and stopping of drive is periodically checked. If either of those was
|
|
detected, replay stops and true is returned. If replay was completed,
|
|
false is returned.
|
|
*/
|
|
bool replayBlock(uint8_t* buf, uint16_t len) {
|
|
|
|
noInterrupts();
|
|
|
|
register uint8_t bitCount, d, tracks = MASK_BOTH_TRACKS;
|
|
|
|
for (; len > 0; len--) {
|
|
d = *buf;
|
|
for (bitCount = 4; bitCount > 0; bitCount--) {
|
|
tracks = ~tracks; // tracks always flip at start of cycle
|
|
TRACK_PORT_OUT = tracks | ~MASK_BOTH_TRACKS; // cycle end
|
|
// wait for middle of cycle
|
|
if (IF1) _delay_us(5.40); else _delay_us(4.20);
|
|
tracks = tracks ^ d; // flip track where data is 1
|
|
TRACK_PORT_OUT = tracks | ~MASK_BOTH_TRACKS;// write out track flips
|
|
// wait for end of cycle
|
|
if (IF1) _delay_us(2.35); else _delay_us(1.15);
|
|
// note that calibration must not be a compile time constant,
|
|
// otherwise we'd get different timing due to optimizations
|
|
if (checkRecordingOrStop() && !calibration) {
|
|
interrupts();
|
|
return true;
|
|
}
|
|
d = d >> 1;
|
|
}
|
|
buf++;
|
|
}
|
|
|
|
TRACK_PORT_OUT = 0x3f; // return tracks to idle level (HIGH) at cycle end
|
|
|
|
interrupts();
|
|
return false;
|
|
}
|
|
|
|
// --------------------------------------------------------- DEBUG MESSAGES ---
|
|
|
|
void debugMsg(uint8_t a, uint8_t b, uint8_t c) {
|
|
msgBuffer[1] = a;
|
|
msgBuffer[2] = b;
|
|
msgBuffer[3] = c;
|
|
message = true;
|
|
}
|
|
|
|
void debugFlush() {
|
|
if (message) {
|
|
msgBuffer[0] = CMD_DEBUG;
|
|
SERIAL_PORT.write(msgBuffer, 4);
|
|
SERIAL_PORT.flush();
|
|
message = false;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------- DAEMON COMMUNICATION ---
|
|
|
|
//
|
|
void daemonSync() {
|
|
|
|
driveState = DRIVE_STATE_UNKNOWN;
|
|
|
|
if (LED_SYNC_WAIT) {
|
|
ledRead(true);
|
|
ledWrite(false);
|
|
}
|
|
|
|
while (true) {
|
|
|
|
// before sending hello, make sure receive buffer is empty; any garbage
|
|
// remaining in the buffer could potentially prevent alignment with the
|
|
// hello response sent by the daemon
|
|
while (SERIAL_PORT.available() > 0) {
|
|
SERIAL_PORT.read();
|
|
}
|
|
|
|
daemonCmd((uint8_t*)(IF1 ? IF1_HELLO : QL_HELLO));
|
|
|
|
if (daemonRcvAck(20, 100, (uint8_t*)DAEMON_HELLO)) {
|
|
// send protocol & firmware version
|
|
daemonCmdArgs(CMD_VERSION, PROTOCOL_VERSION, FIRMWARE_VERSION, 1, 0);
|
|
// send config
|
|
daemonCmdArgs(CMD_CONFIG, CMD_CONFIG_RUMBLE, rumbleLevel, 0, 0);
|
|
// send h/w drive setup
|
|
daemonHWGroup();
|
|
lastPing = millis();
|
|
synced = true;
|
|
|
|
if (LED_SYNC_WAIT) {
|
|
ledRead(IDLE);
|
|
ledWrite(IDLE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (LED_SYNC_WAIT) {
|
|
ledReadFlip();
|
|
ledWriteFlip();
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
void daemonCheckControl() {
|
|
|
|
uint8_t arg1, arg2, arg3;
|
|
|
|
while (daemonRcvCmd(5, 2)) {
|
|
|
|
arg1 = buffer[CMD_LENGTH + 1];
|
|
arg2 = buffer[CMD_LENGTH + 2];
|
|
arg3 = buffer[CMD_LENGTH + 3];
|
|
|
|
switch (buffer[CMD_LENGTH]) {
|
|
|
|
case CMD_MAP:
|
|
if (!HW_GROUP_LOCK) {
|
|
setHWGroup(arg1, arg2);
|
|
saveState();
|
|
}
|
|
daemonHWGroup();
|
|
break;
|
|
|
|
case CMD_CONFIG:
|
|
remoteConfig(arg1, arg2, arg3);
|
|
break;
|
|
|
|
case CMD_RESYNC:
|
|
detectInterface(
|
|
(arg1 & MASK_IF1) != 0, (arg1 & MASK_QL) != 0, false);
|
|
synced = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
void daemonHWGroup() {
|
|
daemonCmdArgs(CMD_MAP, hwGroupStart, hwGroupEnd, HW_GROUP_LOCK ? 1 : 0, 0);
|
|
}
|
|
|
|
//
|
|
bool daemonPing() {
|
|
|
|
if (millis() - lastPing < PING_INTERVAL) {
|
|
return false;
|
|
}
|
|
|
|
// If the interface hasn't been confirmed yet, quickly check whether we're
|
|
// still seeing the same interface. This is mostly to work around an
|
|
// Interface 1 quirk: after power on, the COMSS_CLK signal of the IF1
|
|
// occasionally is 0V, while it should be 5V. The first Microdrive action
|
|
// then corrects this. So, if we detect an interface different from what we
|
|
// saw at startup, we cause a re-sync. At first successful drive activation,
|
|
// we consider the interface confirmed and no longer perform this check.
|
|
if (confirmInterface && detectInterface(false, false, true)) {
|
|
synced = false;
|
|
|
|
} else {
|
|
// send PING command and check for daemon reply
|
|
daemonCmd((uint8_t*)DAEMON_PING);
|
|
synced = daemonRcvAck(10, 5, (uint8_t*)DAEMON_PONG);
|
|
lastPing = millis();
|
|
}
|
|
|
|
return synced;
|
|
}
|
|
|
|
//
|
|
void daemonCmd(uint8_t cmd[]) {
|
|
daemonCmdArgs(cmd[0], cmd[1], cmd[2], cmd[3], 0);
|
|
}
|
|
|
|
//
|
|
void daemonPendingCmd(uint8_t a, uint8_t b, uint8_t c) {
|
|
buffer[0] = a;
|
|
buffer[1] = b;
|
|
buffer[2] = c;
|
|
SERIAL_PORT.write(buffer, 3);
|
|
SERIAL_PORT.flush();
|
|
}
|
|
|
|
//
|
|
void daemonCmdArgs(uint8_t cmd, uint8_t arg1, uint8_t arg2, uint8_t arg3,
|
|
uint16_t bufferLen) {
|
|
buffer[0] = cmd;
|
|
buffer[1] = arg1;
|
|
buffer[2] = arg2;
|
|
buffer[3] = arg3;
|
|
SERIAL_PORT.write(buffer, CMD_LENGTH + bufferLen);
|
|
SERIAL_PORT.flush();
|
|
}
|
|
|
|
//
|
|
bool daemonRcvAck(uint8_t rounds, uint8_t wait, uint8_t exp[]) {
|
|
if (daemonRcvCmd(rounds, wait)) {
|
|
for (int ix = 0; ix < CMD_LENGTH; ix++) {
|
|
if (buffer[CMD_LENGTH + ix] != exp[ix]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//
|
|
bool daemonRcvCmd(uint8_t rounds, uint8_t wait) {
|
|
for (int r = 0; r < rounds; r++) {
|
|
if (SERIAL_PORT.available() < CMD_LENGTH) {
|
|
delay(wait);
|
|
} else {
|
|
return daemonRcv(CMD_LENGTH) == CMD_LENGTH;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//
|
|
uint16_t daemonRcv(uint16_t bufferLen) {
|
|
|
|
if (bufferLen == 0) { // unknown expected length, get from daemon
|
|
if (daemonRcv(2) == 0) {
|
|
return 0;
|
|
};
|
|
bufferLen = buffer[CMD_LENGTH]
|
|
| (((uint16_t)buffer[CMD_LENGTH + 1]) << 8);
|
|
}
|
|
|
|
if (bufferLen > 0) {
|
|
// don't overrun the buffer...
|
|
uint16_t excess = bufferLen > PAYLOAD_LENGTH ? bufferLen - PAYLOAD_LENGTH : 0;
|
|
uint16_t toRead = bufferLen - excess;
|
|
if (SERIAL_PORT.readBytes(buffer + CMD_LENGTH, toRead) != toRead) {
|
|
return 0;
|
|
}
|
|
// ...but still eat up all expected bytes coming in over serial
|
|
uint8_t dummy[1];
|
|
for (; excess > 0; excess--) {
|
|
SERIAL_PORT.readBytes(dummy, 1);
|
|
}
|
|
}
|
|
|
|
return bufferLen;
|
|
}
|
|
|
|
|
|
bool testBuffer() {
|
|
return testBufferPattern(0xff)
|
|
&& testBufferPattern(0x00)
|
|
&& testBufferPattern(0xaa)
|
|
&& testBufferPattern(0x55);
|
|
}
|
|
|
|
bool testBufferPattern(uint8_t p) {
|
|
for (uint16_t ix = 0; ix < BUF_LENGTH; ix++) {
|
|
buffer[ix] = p;
|
|
}
|
|
for (uint16_t ix = 0; ix < BUF_LENGTH; ix++) {
|
|
if (buffer[ix] != p) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ TIMER ---
|
|
|
|
void setTimer(int preload, TimerHandler h) {
|
|
enableTimer(true, preload, h);
|
|
}
|
|
|
|
void stopTimer() {
|
|
enableTimer(false, 0, NULL);
|
|
}
|
|
|
|
#if defined(__AVR_ATmega328P__)
|
|
|
|
bool timerEnabled() {
|
|
return TIMSK2 & (1<<TOIE2);
|
|
}
|
|
|
|
void enableTimer(bool on, int preload, TimerHandler h) {
|
|
|
|
if (timerEnabled() == on) {
|
|
return;
|
|
}
|
|
|
|
noInterrupts();
|
|
timerHandler = h;
|
|
|
|
if (on) {
|
|
TCCR2A = 0;
|
|
TCCR2B = 0;
|
|
TIFR2 |= _BV(TOV2); // clear the overflow interrupt flag
|
|
TCCR2B |= (1<<CS22) + (1<<CS21); // 256 pre-scaler
|
|
if (preload < 256) { // fast
|
|
TCNT2 = preload;
|
|
} else { // slow
|
|
TCNT2 = preload - 256;
|
|
TCCR2B |= (1<<CS20); // extend pre-scaler to 1024
|
|
}
|
|
TIMSK2 |= (1<<TOIE2); // enable timer overflow interrupt
|
|
} else {
|
|
TIMSK2 &= (0<<TOIE2);
|
|
}
|
|
interrupts();
|
|
}
|
|
|
|
ISR(TIMER2_OVF_vect) {
|
|
TimerHandler h = timerHandler;
|
|
stopTimer();
|
|
if (h != NULL) {
|
|
h();
|
|
}
|
|
}
|
|
|
|
#elif defined(__AVR_ATmega32U4__)
|
|
|
|
bool timerEnabled() {
|
|
return TIMSK3 & (1<<TOIE3);
|
|
}
|
|
|
|
void enableTimer(bool on, int preload, TimerHandler h) {
|
|
|
|
if (timerEnabled() == on) {
|
|
return;
|
|
}
|
|
|
|
noInterrupts();
|
|
timerHandler = h;
|
|
|
|
if (on) {
|
|
TCCR3A = 0;
|
|
TCCR3B = 0;
|
|
TIFR3 |= _BV(TOV3); // clear the overflow interrupt flag
|
|
TCCR3B |= (1<<CS32); // 256 pre-scaler
|
|
TCNT3H = 255;
|
|
if (preload < 256) { // fast
|
|
TCNT3L = preload;
|
|
} else { // slow
|
|
TCNT3L = preload - 256;
|
|
TCCR3B |= (1<<CS30); // extend pre-scaler to 1024
|
|
}
|
|
TIMSK3 |= (1<<TOIE3); // enable timer overflow interrupt
|
|
} else {
|
|
TIMSK3 &= (0<<TOIE3);
|
|
}
|
|
interrupts();
|
|
}
|
|
|
|
ISR(TIMER3_OVF_vect) {
|
|
TimerHandler h = timerHandler;
|
|
stopTimer();
|
|
if (h != NULL) {
|
|
h();
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
// -------------------------------------------------------------------- LED ---
|
|
|
|
void errorBlink(uint8_t a, uint8_t b, uint8_t c) {
|
|
ledWrite(false);
|
|
while (true) {
|
|
ledBlink(a);
|
|
ledBlink(b);
|
|
ledBlink(c);
|
|
delay(1500);
|
|
}
|
|
}
|
|
|
|
void ledBlink(uint8_t count) {
|
|
for (uint8_t c = count; c > 0; c--) {
|
|
ledWrite(true);
|
|
delay(250);
|
|
ledWrite(false);
|
|
delay(250);
|
|
}
|
|
delay(1000);
|
|
}
|
|
|
|
void ledReadFlip() {
|
|
ledFlip(MASK_LED_READ);
|
|
}
|
|
|
|
void ledRead(bool active) {
|
|
ledActivity(MASK_LED_READ, active, LED_RW_IDLE_ON);
|
|
}
|
|
|
|
void ledWriteFlip() {
|
|
ledFlip(MASK_LED_WRITE);
|
|
}
|
|
|
|
void ledWrite(bool active) {
|
|
ledActivity(MASK_LED_WRITE, active, LED_RW_IDLE_ON);
|
|
}
|
|
|
|
void ledActivity(uint8_t mask, bool active, bool idleOn) {
|
|
(idleOn ? !active : active) ? ledOn(mask) : ledOff(mask);
|
|
}
|
|
|
|
void ledOn(uint8_t led_mask) {
|
|
LED_PORT |= led_mask;
|
|
}
|
|
|
|
void ledOff(uint8_t led_mask) {
|
|
LED_PORT &= (~led_mask);
|
|
}
|
|
|
|
void ledFlip(uint8_t led_mask) {
|
|
LED_PORT ^= led_mask;
|
|
}
|
|
|
|
// ----------------------------------------------------------------- RUMBLE ---
|
|
|
|
void rumbleGreet() {
|
|
if (rumbleLevel > 0) {
|
|
rumble(true);
|
|
delay(1500);
|
|
rumble(false);
|
|
}
|
|
}
|
|
|
|
void rumble(bool on) {
|
|
if (on && (rumbleLevel == 0)) {
|
|
return;
|
|
}
|
|
analogWrite(PIN_RUMBLE, on ? rumbleLevel : 0);
|
|
}
|
|
|
|
// ------------------------------------------------------------- STATE SAVE ---
|
|
|
|
struct state {
|
|
uint8_t writes; // write count at EEPROM location, for leveled writes
|
|
uint8_t hwGroupStart; // h/w drive mapping
|
|
uint8_t hwGroupEnd;
|
|
uint8_t rumbleLevel; // rumble motor level
|
|
};
|
|
|
|
void loadState() {
|
|
state* s = &state{};
|
|
if (loadFromEEPROM(s)) {
|
|
debugMsg('S', 'L', 1);
|
|
} else {
|
|
debugMsg('S', 'L', 0);
|
|
resetState(s);
|
|
}
|
|
if (!HW_GROUP_LOCK) {
|
|
setHWGroup(s->hwGroupStart, s->hwGroupEnd);
|
|
}
|
|
rumbleLevel = s->rumbleLevel;
|
|
}
|
|
|
|
void saveState() {
|
|
state* s = &state{};
|
|
s->hwGroupStart = hwGroupStart;
|
|
s->hwGroupEnd = hwGroupEnd;
|
|
s->rumbleLevel = rumbleLevel;
|
|
storeToEEPROM(s);
|
|
}
|
|
|
|
void resetState(struct state* s) {
|
|
s->hwGroupStart = HW_GROUP_START;
|
|
s->hwGroupEnd = HW_GROUP_END;
|
|
s->rumbleLevel = RUMBLE_LEVEL;
|
|
}
|
|
|
|
// let's use good old QL checksum ;-)
|
|
uint16_t checksumState(struct state* s) {
|
|
uint16_t a = 0x0f0f;
|
|
uint8_t* p = (uint8_t*)s;
|
|
for (uint8_t ix = 0; ix < sizeof(struct state); ix++) {
|
|
a += *(p + ix);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// ----------------------------------------------------------------- EEPROM ---
|
|
|
|
const int EEPROM_START = sizeof(int); // start of EEPROM is used for index
|
|
|
|
void storeToEEPROM(struct state* s) {
|
|
|
|
s->writes++;
|
|
|
|
int ix;
|
|
if (s->writes > 100) {
|
|
ix = seekEEPROM(true);
|
|
s->writes = 0;
|
|
} else {
|
|
ix = seekEEPROM(false);
|
|
}
|
|
|
|
uint16_t sum = checksumState(s);
|
|
EEPROM.put(ix, sum);
|
|
EEPROM.put(ix + sizeof(sum), *s);
|
|
}
|
|
|
|
bool loadFromEEPROM(struct state* s) {
|
|
|
|
int ix = seekEEPROM(false);
|
|
|
|
uint16_t sum;
|
|
EEPROM.get(ix, sum);
|
|
EEPROM.get(ix + sizeof(sum), *s);
|
|
|
|
return (sum == checksumState(s));
|
|
}
|
|
|
|
int seekEEPROM(bool advance) {
|
|
|
|
int ixR;
|
|
EEPROM.get(0, ixR);
|
|
|
|
int ixV = validateIndex(ixR);
|
|
|
|
if (ixR != ixV) {
|
|
EEPROM.put(0, ixV);
|
|
}
|
|
if (advance) {
|
|
ixV = validateIndex(++ixV);
|
|
EEPROM.put(0, ixV);
|
|
}
|
|
|
|
return ixV;
|
|
}
|
|
|
|
int validateIndex(int ix) {
|
|
if (ix < EEPROM_START) {
|
|
return EEPROM_START;
|
|
} else {
|
|
int last = ix + sizeof(uint16_t) + sizeof(state);
|
|
if (last > E2END) {
|
|
return EEPROM_START;
|
|
}
|
|
}
|
|
return ix;
|
|
}
|