The only regular thing on this machine was the screen finishing a frame, sixty times a second and not negotiable - a clock a program BORROWS rather than one it sets. Every duration became a multiple of 16,667 cycles, so a sixteenth note at 120 beats a minute, which is 125,000, is seven and a half frames and cannot be asked for at all. The way round it was to choose a tempo whose subdivisions happen to land on whole frames, which is making the music fit the machine. Examples/tune.asm says so in its own header. 0x50 Status: a period went by, it is running, it will interrupt 0x51 Control: run, repeat, interrupt 0x52-0x54 The period, in cycles, most significant first THE PERIOD IS IN CYCLES because that is what everything else here is counted in - the cost model counts them and a frame is measured in them - so a timer counting anything else would be a second unit to remember. Twenty four bits reaches from one cycle to sixteen and a half seconds, with 120 beats a minute at 500,000 in the middle, and there is no range left for a prescaler to buy. Starting loads the period; asking it to run while it already is does not, so turning interrupts on half way through a period does not silently move the beat being kept. What is left over carries into the next period, so a period of 1,000 ticks every 1,000 and not every 1,000 plus however late anybody looked. Reading the status takes the tick down and the line with it, which is the rule this machine settled two days ago about every status port. The timing check is in terminal.sh and not the manifest, and the reason is worth keeping: settle() strips cycle counts from recordings, which is right for every other program and useless for a clock. "It printed eight dots" would pass on a timer that fired them all at once. terminal.sh measures that eight periods of 125,000 come to a million within a couple of hundred cycles, and that 99.97% of them were spent asleep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
453 lines
22 KiB
C
453 lines
22 KiB
C
// io.h
|
|
// I/O for the SplitBit CPU Emulator
|
|
// Written by Anachronaut
|
|
// 10/16/2024
|
|
|
|
#ifndef IO_H
|
|
#define IO_H
|
|
|
|
#include <stdint.h>
|
|
#include "cpu.h"
|
|
#include "controller.h"
|
|
|
|
// ---- Ports ----
|
|
//
|
|
// Which port a device answers on is a property of the machine rather than of any
|
|
// program, so the numbers live here and everything else refers to them by name.
|
|
|
|
// The console answers on three ports. The data port is the machine's oldest promise and
|
|
// does not change: writing sends a byte, reading takes one and waits for it. The other two
|
|
// are additions, so a program written before they existed cannot notice them.
|
|
#define PORT_CONSOLE 0x00
|
|
#define PORT_CONSOLE_TOP 0x06
|
|
#define CONSOLE_DATA 0x00
|
|
#define CONSOLE_STATUS 0x01
|
|
#define CONSOLE_CONTROL 0x02
|
|
|
|
// ---- Where the cursor is, as registers ----
|
|
//
|
|
// Read as well as written, which is the thing an escape sequence cannot do without a query
|
|
// and a parse. A program that wants to put something back where it found it asks.
|
|
#define CONSOLE_CURSOR_ROW 0x03
|
|
#define CONSOLE_CURSOR_COLUMN 0x04
|
|
|
|
// Written, and it happens at once - the same shape as the memory controller's Command port
|
|
// rather than a bit in a register that otherwise holds state.
|
|
#define CONSOLE_COMMAND 0x05
|
|
#define CONSOLE_COMMAND_CLEAR 0x01
|
|
|
|
// ---- What colour to write in ----
|
|
//
|
|
// The attribute given to every cell the console draws from now on. Its low nibble picks one
|
|
// of sixteen ink and paper pairs, and the default palette is arranged so that XOR 8 turns
|
|
// any of them inside out - which is highlighting, and is also how the cursor is drawn.
|
|
#define CONSOLE_ATTRIBUTE 0x06
|
|
|
|
#define PORT_TEST 0x10
|
|
#define PORT_REFUSE 0x11
|
|
#define PORT_MEMORY 0x12
|
|
|
|
// ---- Starting again ----
|
|
//
|
|
// Writing 1 here asks the machine to start over: whatever put the first instruction in
|
|
// memory does it again, and the CPU begins where the boot vector points.
|
|
//
|
|
// A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not.
|
|
// Something that could only be asked for through SWI would be unavailable in exactly the
|
|
// case that most wants it, and a program that owns the whole machine has no system to ask.
|
|
//
|
|
// What it does NOT do is unplug anything. The disk stays attached and its image keeps
|
|
// whatever was written to it, which is what a warm restart means: the machine starts
|
|
// again, the world it starts into does not.
|
|
#define PORT_MACHINE 0x13
|
|
#define MACHINE_RESET 0x01
|
|
|
|
// The disk answers on a block of four ports and interrupts on the first of them. A device
|
|
// that spans more than one port raises its line on its base, which is the rule the
|
|
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
|
#define PORT_DISK 0x20
|
|
#define PORT_DISK_TOP 0x25
|
|
#define DISK_BLOCK_HIGH 0x20
|
|
#define DISK_BLOCK_LOW 0x21
|
|
#define DISK_COMMAND 0x22
|
|
#define DISK_STATUS 0x23
|
|
|
|
// ---- Several disks, one controller ----
|
|
//
|
|
// NOT SEVERAL DEVICES, and the instruction set is why. A port is an immediate byte inside the
|
|
// OUT that names it, so a program cannot compute one - "the disk on port 0x20 + drive * 4" is
|
|
// not something this machine can say. Two disks as two devices would mean a branch per access
|
|
// in every one of the eleven places the filesystem names a disk port.
|
|
//
|
|
// So it is one controller with a drive register, which is what the machines this one is
|
|
// pretending to be actually had: one floppy controller and four drives behind it. The block,
|
|
// command and status registers all refer to whichever drive was last selected, and so does
|
|
// the single buffer - which is honest, and which means a program that changes drives knows
|
|
// the buffer no longer holds what it thought.
|
|
#define DISK_DRIVE 0x24
|
|
#define DISK_DRIVES 0x25
|
|
|
|
// Four is a floppy controller's worth. The cost of another is a file handle.
|
|
#define DISK_DRIVE_COUNT 4
|
|
// ---- The screen ----
|
|
//
|
|
// Sixteen ports, like the controller, and it interrupts on its base the way the disk
|
|
// established for a device that spans more than one. The registers themselves are in
|
|
// video.h, with the memory layout they describe.
|
|
#define PORT_VIDEO 0x30
|
|
#define PORT_VIDEO_TOP 0x3F
|
|
|
|
// ---- The timer ----
|
|
//
|
|
// The only regular beat this machine had was the screen finishing a frame, which is fixed at
|
|
// sixty a second. That is a clock a program borrows rather than one it sets: every duration
|
|
// becomes a multiple of 16.67 ms, so a note worth a third of a beat cannot be asked for and
|
|
// the way round it is to choose a tempo whose subdivisions happen to land on whole frames -
|
|
// which is making the music fit the hardware.
|
|
#define PORT_TIMER 0x50
|
|
#define PORT_TIMER_TOP 0x54
|
|
|
|
#define TIMER_STATUS 0x50
|
|
#define TIMER_CONTROL 0x51
|
|
#define TIMER_PERIOD_HIGH 0x52
|
|
#define TIMER_PERIOD_MID 0x53
|
|
#define TIMER_PERIOD_LOW 0x54
|
|
|
|
#define TIMER_STATUS_TICKED 0x01
|
|
#define TIMER_STATUS_RUNNING 0x02
|
|
#define TIMER_STATUS_INTERRUPT 0x04
|
|
|
|
#define TIMER_CONTROL_RUN 0x01
|
|
#define TIMER_CONTROL_REPEAT 0x02
|
|
#define TIMER_CONTROL_INTERRUPT 0x04
|
|
|
|
#define PORT_REGISTRY 0xFF
|
|
|
|
// ---- The console ----
|
|
//
|
|
// Two modes, chosen by the program through the control port. The console starts in LINE
|
|
// mode, which is what the machine has always done: the terminal holds what is typed until
|
|
// Return, and does the echoing and the backspacing on the way. Reading the data port waits
|
|
// for a whole line to be finished somewhere else and then hands it over a byte at a time.
|
|
//
|
|
// KEY mode turns that off. Keys arrive as they are pressed, and nothing echoes them, so a
|
|
// program that wants them seen has to send them back out itself. That is not a choice this
|
|
// machine is making; it is what asking the terminal to stop holding a line means, and the
|
|
// editing goes away with it. A program that wants keys is expected to want that.
|
|
//
|
|
// READING THE DATA PORT WAITS IN BOTH MODES. The status port is how a program declines to
|
|
// wait, and keeping that in one place means the data port means one thing everywhere. A
|
|
// read that sometimes blocked and sometimes did not, depending on state set somewhere
|
|
// else, is the kind of thing that works until it does not.
|
|
//
|
|
// KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe
|
|
// there is nothing to put into another mode, and the status port answers by asking the
|
|
// operating system whether anything is waiting, which is true of a pipe with bytes in it.
|
|
//
|
|
// A PROGRAM THAT INTERRUPTS ON INPUT MUST NOT BLOCK ON THE DATA PORT. Reading it waits,
|
|
// and the machine executes no instructions while it is waiting, so nothing is serviced
|
|
// and the line the console is about to raise goes nowhere until the read it was meant to
|
|
// replace has already finished. Interrupting and blocking are two answers to the same
|
|
// question and a program wants one of them.
|
|
|
|
// The control port's bits, which are independent of one another. Writing zero asks for
|
|
// line mode with no interrupts, which is how the console starts and what a program that
|
|
// knows nothing of any of this leaves behind it.
|
|
#define CONSOLE_MODE_LINE 0x00
|
|
#define CONSOLE_MODE_KEY 0x01
|
|
|
|
// Asks the console to put its line up when a byte arrives, instead of the program having
|
|
// to come and look. It composes with the mode rather than depending on it: in line mode
|
|
// the terminal still holds what is typed until Return, and then a whole line's worth of
|
|
// bytes arrive at once, each raising the line in turn as the one before it is taken.
|
|
// That is not especially useful, but a control bit that quietly did nothing depending on
|
|
// another control bit would be worse than a burst of interrupts somebody asked for.
|
|
#define CONSOLE_CONTROL_INTERRUPT 0x02
|
|
// Show a cursor where the next character will go. Off when the machine starts, because a
|
|
// machine draws what it is told to and a program painting its own screen does not want one
|
|
// blinking in the middle of it. A system that reads lines from a person turns it on.
|
|
#define CONSOLE_CONTROL_CURSOR 0x04
|
|
|
|
// Set when there is a byte to be had. NOT set at the end of input, although a read would
|
|
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
|
|
// ready would make a loop that reads while READY spin on imaginary bytes forever. A loop
|
|
// like that now stops when the input does, which is what anybody writing one intends.
|
|
#define CONSOLE_STATUS_READY 0x01
|
|
// Set once input has run out for good. The data port still answers 0xFF, which is what it
|
|
// always did and what every program written before this expects, but 0xFF is also an
|
|
// ordinary byte and this bit is the only thing that can tell the difference.
|
|
#define CONSOLE_STATUS_ENDED 0x02
|
|
// Which mode the console is in, so that a program can put it back the way it found it
|
|
// rather than assuming it knows.
|
|
#define CONSOLE_STATUS_KEYMODE 0x04
|
|
// Whether the console is set to interrupt, for the same reason: everything a program can
|
|
// ask the console to be, it can also ask the console what it currently is.
|
|
#define CONSOLE_STATUS_INTERRUPT 0x08
|
|
// And whether a cursor is being shown, for the same reason as the rest: everything a program
|
|
// can ask the console to be, it can also ask the console what it currently is.
|
|
#define CONSOLE_STATUS_CURSOR 0x10
|
|
|
|
// Puts the terminal back the way it was found. Registered with atexit and called from a
|
|
// handler for every signal that can end this process and be caught, because a machine that
|
|
// stops in key mode and does not undo it leaves the shell that started it unusable, which
|
|
// is a far worse failure than anything the program was doing. atexit alone is not enough:
|
|
// it does not run when a process is killed, and the ending that matters most is SIGHUP,
|
|
// which is what arrives when whatever launched this machine dies and takes the terminal
|
|
// with it.
|
|
void consoleRestore(void);
|
|
|
|
// One byte from the console, waiting if it has to. Everything that reads standard input
|
|
// goes through here: the emulator owns one byte of pushback, and stdio holding a buffer
|
|
// of its own behind that would make the status port lie about what is waiting.
|
|
uint8_t consoleReadByte(void);
|
|
|
|
// Puts the cursor back in the corner. Called when the machine starts, since the screen is
|
|
// cleared then too and a cursor left where the last program stopped would be a cursor
|
|
// pointing into something that is gone.
|
|
void consoleHome(void);
|
|
|
|
// ---- How a front end with a window feeds the console ----
|
|
//
|
|
// Called while the console has nothing to give. It returns a byte, or one of the two
|
|
// answers below. They have to be told apart: a window with nobody typing yet is the normal
|
|
// case and happens sixty times a second, while a window that has gone is the end of input.
|
|
// One value for both would have made the first keystroke look like a closed machine.
|
|
//
|
|
// Without a hook the console reads standard input, which is what it has always done.
|
|
#define CONSOLE_NOTHING_YET (-1)
|
|
#define CONSOLE_GONE (-2)
|
|
//
|
|
// mayWait says which question is being asked. Zero is the status port looking, and must not
|
|
// present or sleep: a program polling in a loop would otherwise run at the frame rate. One
|
|
// is the data port blocking, where presenting is exactly right, because a machine waiting
|
|
// for a key is still a machine somebody is looking at.
|
|
void consoleSetInputHook(int (*hook)(int mayWait));
|
|
|
|
// ---- Device classes ----
|
|
//
|
|
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
|
// unimplemented port already gives zero, so "nothing there" needs no special case and a
|
|
// machine with no registry at all answers correctly by doing nothing.
|
|
//
|
|
// Classes 0x01 to 0x0F belong to the machine itself. Peripherals start at 0x10.
|
|
|
|
#define DEVICE_NONE 0x00
|
|
#define DEVICE_REGISTRY 0x01
|
|
#define DEVICE_CONSOLE 0x02
|
|
#define DEVICE_CONTROLLER 0x03
|
|
// The machine itself, which is what a reset is asking. In the range kept for the machine
|
|
// rather than among the peripherals, because it is not one: it is not attached to
|
|
// anything and cannot be unplugged.
|
|
#define DEVICE_MACHINE 0x04
|
|
#define DEVICE_TEST 0x10
|
|
#define DEVICE_REFUSE 0x11
|
|
#define DEVICE_MEMORY 0x12
|
|
#define DEVICE_DISK 0x13
|
|
#define DEVICE_VIDEO 0x14
|
|
#define DEVICE_SOUND 0x15
|
|
#define DEVICE_TIMER 0x16
|
|
|
|
// What a device brings besides itself. This means memory that somebody has to register
|
|
// with the controller, so the controller's own bank 2 does not count: it is already there.
|
|
#define DEVICE_FLAG_HAS_MEMORY 0x01
|
|
|
|
// ---- The disk ----
|
|
//
|
|
// Blocks are a page each, so a block number is the whole of a 16 bit address and the
|
|
// arithmetic never needs a multiply. Sixteen megabytes is absurd for this machine, which
|
|
// is the point: there is room for anything a filesystem might want to grow into later.
|
|
|
|
#define DISK_BLOCK_BYTES 256
|
|
|
|
// A fresh image is made the size of Program and Data together, which is a round number
|
|
// for this machine and small enough to read in a hex editor while it is being built.
|
|
#define DISK_DEFAULT_BLOCKS 512
|
|
|
|
#define DISK_COMMAND_READ 0x01
|
|
#define DISK_COMMAND_WRITE 0x02
|
|
|
|
// Set while an operation is still going, and it now really is set: a disk given a latency
|
|
// says busy, takes that many cycles, and finishes then. A program that does not wait gets
|
|
// whatever was in the buffer before, which is what the hardware would give it.
|
|
//
|
|
// It reads clear the whole time when the latency is zero, which is the default and how
|
|
// every test here has always run.
|
|
#define DISK_STATUS_BUSY 0x01
|
|
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not
|
|
// about the last operation: it is a standing property of the medium, readable before
|
|
// anything is attempted. A write protected disk is barred here, in the device, rather
|
|
// than by anything in the filesystem, so writing blocks directly cannot get around it.
|
|
#define DISK_STATUS_PROTECTED 0x04
|
|
|
|
// Set when the last operation did not work: no image, or a block that is not on it.
|
|
// A disk that cannot read a block is an ordinary thing that happens to working programs,
|
|
// so it says so rather than stopping the machine.
|
|
#define DISK_STATUS_ERROR 0x02
|
|
|
|
// ---- Devices that take time ----
|
|
//
|
|
// A real device does not finish inside the instruction that asked it to. It says it is
|
|
// busy, takes as long as it takes, and is done when the machine has run that far - so the
|
|
// emulator needs somewhere to notice that time has passed. That is this: called once per
|
|
// instruction with the machine's clock, it lets any device whose moment has come finish.
|
|
//
|
|
// It is written for the disk and is not about the disk. Anything that will take time - a
|
|
// display that refreshes, a port that waits on the host - wants exactly this shape.
|
|
void deviceTick(unsigned long now);
|
|
|
|
// ---- Time that passed while the machine was stopped ----
|
|
//
|
|
// A console waiting on a key it has not been given has stopped the machine, and time is
|
|
// still going by: the cursor still blinks, a disk still turns. That is exactly what idle
|
|
// cycles are for, and without them the machine's clock froze the moment somebody was asked
|
|
// a question - so the cursor stopped blinking precisely when there was a person looking at
|
|
// it and waiting to type.
|
|
//
|
|
// Returned and cleared, the way the controller's cycles are, and picked up in the same
|
|
// place: after a port access, by the CPU that was stopped.
|
|
unsigned long takeIdleCycles(void);
|
|
|
|
// How many cycles a block read or write takes. Zero means the answer is there before the
|
|
// next instruction is, which is what this machine has always done and what every recorded
|
|
// test assumes.
|
|
void setDiskLatency(unsigned long cycles);
|
|
|
|
// Attaches an image, making one if it is not there. A disk is read only if the host will
|
|
// not let the file be written, or if writeProtect asks for it, which is the emulated
|
|
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
|
uint8_t attachDisk(const char *path, uint8_t writeProtect);
|
|
|
|
void detachDisk(void);
|
|
|
|
// How many bytes a device's entry in the registry runs to. Reading past the end gives
|
|
// zero, so the record can grow later without anything already written having to change.
|
|
#define DEVICE_RECORD_BYTES 2
|
|
|
|
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address);
|
|
|
|
uint8_t InputHandler(uint8_t Address);
|
|
|
|
// ---- Interrupt lines ----
|
|
//
|
|
// One line per port. A device puts its line up to ask for attention, and the CPU takes
|
|
// it down when it answers. Which line a device uses is not a choice: a device on port N
|
|
// interrupts on N, which is what saves the machine from needing any arbitration.
|
|
//
|
|
// These belong to the bus rather than to the CPU. Nothing here is saved in a frame, and
|
|
// a program cannot read them except by being interrupted.
|
|
|
|
// Gives every device a moment to notice something the machine did not ask it about.
|
|
// Nothing here runs alongside the CPU: a device that waits on the outside world - the
|
|
// console is the only one so far - is never going to see a keystroke unless something
|
|
// asks it to look, and the CPU calling this between instructions is that something.
|
|
//
|
|
// It is called on every step and gets out of the way immediately when there is nothing to
|
|
// do, because there usually is not. A device that wants attention rarely is a device that
|
|
// must cost nothing when it does not.
|
|
void serviceDevices(void);
|
|
|
|
// Set when something has written MACHINE_RESET, and taken by the loop that acts on it. A
|
|
// request rather than an action, because a device cannot restart the machine from inside
|
|
// the instruction that asked - the CPU is mid-step and its state is not yet consistent.
|
|
int takeResetRequest(void);
|
|
|
|
// Whether one is waiting, without taking it. For anything that has to behave differently
|
|
// while a restart is on its way but is not the thing that performs it.
|
|
int resetIsPending(void);
|
|
|
|
// ---- The button on the front of the case ----
|
|
//
|
|
// A machine has one, and a window is the case. Writing MACHINE_RESET is how a PROGRAM asks;
|
|
// this is how a person does, without needing a program that is willing to listen - which is
|
|
// the whole point of a reset button and the reason the port exists at all.
|
|
void requestReset(void);
|
|
|
|
// Puts the console's input back to how a machine starts: nothing pushed back, no line half
|
|
// gathered, and NOT at the end of input. Called when the machine starts over, because a
|
|
// console that had run out of input would still have run out afterwards - and a reset that
|
|
// left the keyboard dead would be a reset nobody could use twice.
|
|
void consoleResetInput(void);
|
|
|
|
// ---- The lines on one bus ----
|
|
//
|
|
// One bit a port, so a device can ask for attention without anything having to poll it.
|
|
// Eight ports to the byte, low bit first.
|
|
//
|
|
// A MACHINE WITH TWO PROCESSORS HAS TWO SETS OF THESE. A peripheral core's devices raise
|
|
// lines on the core's bus and the host's devices on the host's, and they are not the same
|
|
// lines: one array indexed by port for the whole machine would have let each see the other's,
|
|
// so a disk finishing would interrupt a sound core and a sound core's tick would interrupt
|
|
// the shell. Both would arrive at a handler for something else entirely.
|
|
#define INTERRUPT_LINE_BYTES 32
|
|
|
|
typedef struct {
|
|
uint8_t bits[INTERRUPT_LINE_BYTES];
|
|
} InterruptLines;
|
|
|
|
void linesRaise(InterruptLines *lines, uint8_t port);
|
|
void linesClear(InterruptLines *lines, uint8_t port);
|
|
|
|
// The lowest numbered port with its line up, or below zero if none has. A scan rather than
|
|
// a priority encoder, so there is no arbitration to explain and a programmer can work out
|
|
// what happens next by reading the port numbers.
|
|
int linesNext(const InterruptLines *lines);
|
|
|
|
// The machine's own controller, which is what every device on this bus means when it moves
|
|
// memory. A peripheral core's device holds its own.
|
|
Controller *machineController(void);
|
|
|
|
// The machine's own lines, which is what every device on this bus means.
|
|
void raiseInterrupt(uint8_t port);
|
|
|
|
void clearInterrupt(uint8_t port);
|
|
|
|
// Every line down at once, for a reset. The vector table is cleared when the machine starts
|
|
// over because a handler left behind would aim an interrupt into a program that is no longer
|
|
// running; a LINE left behind does exactly the same thing, and arrives at a program that
|
|
// never asked the device for anything.
|
|
void clearAllInterrupts(void);
|
|
|
|
// The timer, which counts the machine's own cycles.
|
|
void timerReset(void);
|
|
void timerTick(unsigned long now);
|
|
|
|
// The lowest numbered port with its line up, or -1 if none of them are.
|
|
int nextPendingInterrupt(void);
|
|
|
|
// The bus this machine's own processor is on: its devices, its lines, its controller. A
|
|
// peripheral core is given a different one by whatever device contains it.
|
|
const Bus *machineBus(void);
|
|
|
|
// ---- Refusing ----
|
|
//
|
|
// A device can refuse what it was asked to do. Interrupting is a device asking for
|
|
// attention later; refusing is a device saying no to the instruction happening now, so
|
|
// it has to stop the machine where it stands rather than raise a line and let execution
|
|
// carry on past the mistake.
|
|
//
|
|
// The refusal names a software vector, so the cause is known from the entry it arrives
|
|
// through, the same way every other fault on this machine works.
|
|
|
|
void refuseAccess(uint8_t faultVector);
|
|
|
|
// ---- Memory a device brings ----
|
|
//
|
|
// Returns the memory owned by the device on this port, or NULL if it owns none, and
|
|
// fills in how much of it there is. This is how the controller finds out what a device
|
|
// brings when it is told to register a bank.
|
|
//
|
|
// It is a direct look at the machine's device table rather than a conversation through
|
|
// the registry's port. The port protocol remembers which port it was asked about, so a
|
|
// controller that used it would silently lose the place of any enumeration a program had
|
|
// in progress. Same table, two consumers, and only one of them needs the protocol.
|
|
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity);
|
|
|
|
// The vector a device refused with, or 0 if none did. Reading it clears it, because a
|
|
// refusal is answered once.
|
|
uint8_t takeRefusal(void);
|
|
|
|
// Which port did the refusing. Only meaningful alongside a refusal.
|
|
uint8_t refusingPort(void);
|
|
|
|
#endif // IO_H
|