build: the libavr pin advances past phase 6, at byte parity everywhere

The pin crosses libavr's phase 6 - the renamed system surface, the named
serial configs, the receiver-tolerance table, the paged SPM receipts -
and every loader image comes out size-identical: the full matrix on six
representative chips (the exhaustive cross product on three of them),
the stock and autobaud columns untouched, the four tsb tiers back on
their recorded floors at 510/526/638/836.

Byte parity was not free, and the two libavr defects it surfaced were
fixed there rather than absorbed here. The EEPROM write procedure's
step 2 - the SPMEN spin - had landed unconditionally and cost every
build six bytes for a wait a polled loader can never take; it is scoped
now, and the loaders state the datasheet's own omission clause
(spm_interlock::omitted, DS40002061B 8.6.3). The blocking page
erase/write grew an internal wait the tiers' settle() already provides,
so the tiers issue the command form and pureboot keeps its host-driven
sp_spm path.

What the port states rather than inherits: the stock 115200 at 16 MHz
sits +2.1 % past the receiver-tolerance table libavr now holds rates
to, so the hardware links say .allow_baud_error = true - the same
2.5 % envelope pureboot_baud_feasible() has always enforced, proven on
silicon across the fleet. rx_ready() reads readable() now.

Alongside the pin: rule 33's ASCII sweep over every source (docs keep
their typography), rule 34's InsertBraces in .clang-format with the
tree reformatted, std::array over the simavr runners' raw buffers, and
the stale Studio size in ide/README.md replaced by the claim its
check-flags gate actually holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 11:43:44 +02:00
parent 0cb83ff36f
commit e4390d2ba8
38 changed files with 791 additions and 625 deletions

View File

@@ -11,18 +11,19 @@
// The link follows the chip's natural default (USART0 on the megas, the
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins,
// and `-l sw:D0,D1@0` where those pins are a USART's own see the pin
// and `-l sw:D0,D1@0` where those pins are a USART's own - see the pin
// ownership the bridge models below.
//
// simavr's tiny cores decode the SPM opcode but attach no NVM module SPM
// simavr's tiny cores decode the SPM opcode but attach no NVM module - SPM
// is a silent no-op (the mega's boot section has one, avr_flash). The
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
// implements buffer fill, page erase, page write, and CTPB, completing
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
// 'F' command answers with flash bytes the tests assert transport only.
// 'F' command answers with flash bytes - the tests assert transport only.
//
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
// ground-truth cross-check against what the host read back.
#include <array>
#include <csignal>
#include <cstdint>
#include <cstdio>
@@ -37,7 +38,7 @@
#include <unistd.h>
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
// unlike simavr's core headers the block covers both harmlessly.
// unlike simavr's core headers - the block covers both harmlessly.
extern "C" {
#include "avr_eeprom.h"
#include "avr_flash.h"
@@ -64,7 +65,7 @@ std::uint32_t reset_pc;
volatile std::sig_atomic_t reset_requested;
// -w: report the cycle of the first transmit activity, once. What the
// activation-window gate reads with an idle line and an application
// activation-window gate reads - with an idle line and an application
// installed, the first thing that ever talks is the application's banner,
// so this cycle *is* the loader's window plus a banner lead measured in
// microseconds. Idle pacing is skipped in this mode: there is no real-time
@@ -74,8 +75,9 @@ bool window_tx_seen;
void window_first_tx()
{
if (!window_report || window_tx_seen)
if (!window_report || window_tx_seen) {
return;
}
window_tx_seen = true;
std::println("PB_WINDOW_TX {}", avr->cycle);
std::fflush(stdout);
@@ -91,7 +93,7 @@ void window_uart_hook(avr_irq_t *, std::uint32_t, void *)
// direction the way the real wiring does: it drives only while the
// firmware's DDR bit reads input, decodes transitions as the firmware's
// transmit only while the firmware owns the line, ignores its own raises
// coming back through the shared irq and echoes every byte it drives back
// coming back through the shared irq - and echoes every byte it drives back
// to the pty, which is what the host-side FTDI tie does and what the host
// tool's --one-wire mode reads back and discards.
bool link_one_wire;
@@ -107,8 +109,9 @@ int parse_link(std::string_view spec)
}
if (spec.starts_with("sw")) {
link_software = true;
if (spec.size() == 2)
if (spec.size() == 2) {
return 0;
}
char owner = 0;
int fields =
std::sscanf(spec.data() + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner);
@@ -122,16 +125,16 @@ int parse_link(std::string_view spec)
}
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly)
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) -
// hardware ignores the in-page bits (section 26.8.1), so an erase issued with Z
// anywhere inside the page wipes half the neighbouring page in simulation
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
// with Z forced to the page boundary; everything else passes through.
//
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW absent on the m48
// core so the discard store falls through into the buffer-fill branch and
// temporary-buffer discard (Atmel-8271 section 26.2/section 26.3.1), but the stock model
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW - absent on the m48
// core - so the discard store falls through into the buffer-fill branch and
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
// here instead.
avr_flash_t *mega_flash;
@@ -171,7 +174,7 @@ void fix_mega_flash_erase()
return;
}
}
std::println(stderr, "device: no flash module to fix SPM page erases may misalign");
std::println(stderr, "device: no flash module to fix - SPM page erases may misalign");
}
void request_reset(int)
@@ -183,8 +186,8 @@ void request_reset(int)
struct tiny_nvm_t {
avr_io_t io;
std::uint8_t buffer[128];
std::uint8_t used[128]; // a buffer word loads once until erased like silicon
std::array<std::uint8_t, 128> buffer;
std::array<std::uint8_t, 128> used; // a buffer word loads once until erased - like silicon
unsigned page;
};
@@ -192,8 +195,9 @@ tiny_nvm_t nvm;
int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
{
if (ctl != AVR_IOCTL_FLASH_SPM)
if (ctl != AVR_IOCTL_FLASH_SPM) {
return -1;
}
auto *n = reinterpret_cast<tiny_nvm_t *>(io);
avr_t *mcu = io->avr;
std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
@@ -209,13 +213,14 @@ int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
} else if (command == 0x03) { // PGERS
std::memset(mcu->flash + page_base, 0xff, n->page);
} else if (command == 0x05) { // PGWRT: programming only clears bits
for (unsigned i = 0; i < n->page; i++)
for (unsigned i = 0; i < n->page; i++) {
mcu->flash[page_base + i] &= n->buffer[i];
std::memset(n->buffer, 0xff, n->page);
std::memset(n->used, 0, n->page);
}
std::memset(n->buffer.data(), 0xff, n->page);
std::memset(n->used.data(), 0, n->page);
} else if (command == 0x11) { // CTPB
std::memset(n->buffer, 0xff, n->page);
std::memset(n->used, 0, n->page);
std::memset(n->buffer.data(), 0xff, n->page);
std::memset(n->used.data(), 0, n->page);
}
mcu->data[0x57] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
return 0;
@@ -234,9 +239,9 @@ avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
{
if (tx_bit < 0) {
// Half a bit into the start bit: a real receiver re-samples here and
// abandons a false start. The device's own init produces one DDR
// abandons a false start. The device's own init produces one - DDR
// drives the pin low for the instructions until the idle level is
// written and without this check that glitch decodes as a stray
// written - and without this check that glitch decodes as a stray
// byte (and would read as first transmit activity under -w).
if (tx_level) {
tx_active = 0;
@@ -248,23 +253,25 @@ avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
}
if (tx_bit < 8) {
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
if (++tx_bit < 8)
if (++tx_bit < 8) {
return when + bit_cycles;
}
// The byte is delivered at the stop bit's sampling point (9.5 bit
// times), where a hardware receiver raises its RXC not sooner: a
// times), where a hardware receiver raises its RXC - not sooner: a
// host answering before the stop bit would put its start bit on the
// wire while the device is still driving, which the device,
// transmitting, is not watching for.
return when + bit_cycles;
}
if (write(pty_master, &tx_shift, 1) != 1)
if (write(pty_master, &tx_shift, 1) != 1) {
std::println(stderr, "device: pty write lost a byte");
}
tx_active = 0;
return 0;
}
// A USART owns its TxD pin whenever its transmitter is enabled, and the port
// register cannot drive it (§20.2 / Atmel-8271 §19.2) which is why a
// register cannot drive it (section 20.2 / Atmel-8271 section 19.2) - which is why a
// bit-banged link deployed on those pins is mute until it clears UCSRnB.
// simavr wires a USART entirely through IRQs and never touches the port pin
// model, so the ownership does not exist there and the mute cannot happen:
@@ -274,36 +281,40 @@ avr_uart_t *tx_owner;
bool tx_pin_taken()
{
if (!tx_owner)
if (!tx_owner) {
return false;
if (avr_regbit_get(avr, tx_owner->txen))
}
if (avr_regbit_get(avr, tx_owner->txen)) {
return true;
}
// One-wire on the USART's RXD: RXEN forces the shared pin's direction to
// input (§20.7.3), so the firmware's drive goes nowhere until the
// release the receive-side twin of the TXD hold.
// input (section 20.7.3), so the firmware's drive goes nowhere until the
// release - the receive-side twin of the TXD hold.
return link_one_wire && avr_regbit_get(avr, tx_owner->rxen);
}
// simavr leaves TXEN set in UCSRnB out of reset, where silicon clears the
// whole register (§20.11.3) which would hand the pin to a USART no code has
// whole register (section 20.11.3) - which would hand the pin to a USART no code has
// enabled, making a freshly reset chip mute for reasons hardware does not
// have. Reset it the way the datasheet does, so the ownership starts from
// nobody's and only an application that really enables the USART takes it.
void reset_tx_owner()
{
if (tx_owner)
if (tx_owner) {
avr_regbit_clear(avr, tx_owner->txen);
}
}
void find_tx_owner()
{
for (avr_io_t *io = avr->io_port; io; io = io->next)
for (avr_io_t *io = avr->io_port; io; io = io->next) {
if (io->kind && std::string_view{io->kind} == "uart" &&
reinterpret_cast<avr_uart_t *>(io)->name == sw_tx_owner) {
tx_owner = reinterpret_cast<avr_uart_t *>(io);
reset_tx_owner();
return;
}
}
std::println(stderr, "device: no USART{} to own the software link's TX pin", sw_tx_owner);
}
@@ -311,7 +322,7 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
{
if (link_one_wire && (self_drive || !mcu_owns_line)) {
// The bridge's own drive coming back through the shared irq, or a
// transition while the line is the bridge's either way not the
// transition while the line is the bridge's - either way not the
// firmware talking: the decoder sees an idle line.
tx_level = 1;
return;
@@ -329,7 +340,7 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
tx_level = level;
}
std::uint8_t rx_queue[8192];
std::array<std::uint8_t, 8192> rx_queue;
unsigned rx_head, rx_tail; // ring: head = next to send
int rx_active, rx_bit;
std::uint8_t rx_byte;
@@ -355,10 +366,11 @@ avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
bridge_drive(1);
// The host-side tie: an FTDI adapter on a one-wire line reads every
// byte it transmits supply that echo, which the host tool's
// byte it transmits - supply that echo, which the host tool's
// --one-wire mode consumes as its wiring check.
if (link_one_wire && write(pty_master, &rx_byte, 1) != 1)
if (link_one_wire && write(pty_master, &rx_byte, 1) != 1) {
std::println(stderr, "device: pty echo lost a byte");
}
rx_bit++;
return when + 2 * bit_cycles;
}
@@ -369,15 +381,17 @@ avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
void rx_start_next()
{
if (rx_active || rx_head == rx_tail)
if (rx_active || rx_head == rx_tail) {
return;
// The firmware is answering on the shared line: hold the byte — a real
}
// The firmware is answering on the shared line: hold the byte - a real
// host's transmission waits out the reply on the wire too. The next
// poll_pty tick retries once the line is handed back.
if (link_one_wire && mcu_owns_line)
if (link_one_wire && mcu_owns_line) {
return;
}
rx_byte = rx_queue[rx_head];
rx_head = (rx_head + 1) % sizeof(rx_queue);
rx_head = (rx_head + 1) % rx_queue.size();
rx_active = 1;
rx_bit = 0;
bridge_drive(0); // start bit
@@ -389,11 +403,12 @@ void rx_start_next()
void on_ddr(avr_irq_t *, std::uint32_t value, void *)
{
const bool owns = (value >> sw_rx_bit) & 1;
if (mcu_owns_line && !owns)
if (mcu_owns_line && !owns) {
bridge_drive(1); // hand-back: a turn-based host idles here, and the cache stays truthful
}
mcu_owns_line = owns;
// A byte held back while the firmware answered starts from the next
// poll_pty tick, never from inside the DDR write itself the port
// poll_pty tick, never from inside the DDR write itself - the port
// model's own pull-up re-derivation runs right after this notify and
// would erase a start edge raised here.
}
@@ -416,7 +431,7 @@ void bridge_reset()
mcu_owns_line = false; // avr_reset zeroed DDR: every pin reads input again
// Re-drive the idle line through a forced transition: ioport pin irqs are
// IRQ_FLAG_FILTERED, and avr_reset zeroes the port latch while the irq
// keeps its pre-reset cached value so a plain raise(1) against a cached
// keeps its pre-reset cached value - so a plain raise(1) against a cached
// 1 is dropped and the device reads the line stuck low. A loader entering
// calibration on that line measures reset-to-first-edge as one giant
// pulse and mis-locks or boots the application on the first real knock.
@@ -428,12 +443,13 @@ void bridge_reset()
void poll_pty()
{
std::uint8_t chunk[256];
ssize_t got = read(pty_master, chunk, sizeof(chunk));
std::array<std::uint8_t, 256> chunk;
ssize_t got = read(pty_master, chunk.data(), chunk.size());
for (ssize_t i = 0; i < got; i++) {
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
if (next == rx_head)
unsigned next = (rx_tail + 1) % rx_queue.size();
if (next == rx_head) {
break; // full: the host will retry on timeout
}
rx_queue[rx_tail] = chunk[i];
rx_tail = next;
}
@@ -452,19 +468,24 @@ void poll_pty()
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
std::fclose(f);
}
avr_eeprom_desc_t ee = {.ee = nullptr, .offset = 0, .size = 0};
avr_eeprom_desc_t ee = {
.ee = nullptr,
.offset = 0,
.size = 0,
};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
char path[512];
std::snprintf(path, sizeof(path), "%s.eeprom", dump_path);
f = std::fopen(path, "wb");
std::array<char, 512> path;
std::snprintf(path.data(), path.size(), "%s.eeprom", dump_path);
f = std::fopen(path.data(), "wb");
if (f) {
std::fwrite(ee.ee, 1, ee.size, f);
std::fclose(f);
}
}
}
if (!link_software)
if (!link_software) {
uart_pty_stop(&uart_pty);
}
_exit(0);
}
@@ -494,7 +515,7 @@ int main(int argc, char *argv[])
" -w: print PB_WINDOW_TX <cycle> at the first transmit activity and\n"
" free-run idle time (window measurement mode)\n"
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
" resume_flash: raw full-flash image loaded instead of the ELF a prior\n"
" resume_flash: raw full-flash image loaded instead of the ELF - a prior\n"
" run's dump, for power-fail resume tests\n",
argv[0]);
return 2;
@@ -506,8 +527,9 @@ int main(int argc, char *argv[])
auto baud = static_cast<unsigned>(std::atoi(argv[6]));
dump_path = argv[7];
const bool is_mega = mcu_name.starts_with("atmega");
if (!link_given)
if (!link_given) {
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
}
avr = avr_make_mcu_by_name(mcu_name.data());
if (!avr) {
@@ -534,17 +556,17 @@ int main(int argc, char *argv[])
}
// An image past flash end would smash the simulator's heap and turn
// into phantom peripheral behavior (lessons: believe the size gate
// first) refuse it loudly instead.
// first) - refuse it loudly instead.
if (base + fw.flashsize > avr->flashend + 1) {
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} image does not fit its slot",
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} - image does not fit its slot",
fw.flashsize, base, avr->flashend);
return 1;
}
std::memcpy(avr->flash + base, fw.flash, fw.flashsize);
}
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
// modeled the argument picks the modeled fuse's target); the tinies
// and the boot-section-less m48s reset to word 0 like silicon erased
// modeled - the argument picks the modeled fuse's target); the tinies
// and the boot-section-less m48s reset to word 0 like silicon - erased
// flash walks up into the loader, and after the host's surgery the
// patched vector routes there.
const bool boot_section = is_mega && !mcu_name.starts_with("atmega48");
@@ -553,11 +575,15 @@ int main(int argc, char *argv[])
avr->codeend = avr->flashend;
// Erased EEPROM, as hardware powers up (simavr zeroes it).
std::uint8_t blank[1024];
std::memset(blank, 0xff, sizeof(blank));
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
seed.ee = blank;
std::array<std::uint8_t, 1024> blank;
std::memset(blank.data(), 0xff, blank.size());
avr_eeprom_desc_t seed = {
.ee = blank.data(),
.offset = 0,
.size = 0,
};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= blank.size()) {
seed.ee = blank.data();
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
}
@@ -568,7 +594,7 @@ int main(int argc, char *argv[])
fix_mega_flash_erase();
} else {
nvm.page = page;
std::memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
std::memset(nvm.buffer.data(), 0xff, nvm.buffer.size());
nvm.io.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io);
@@ -582,35 +608,41 @@ int main(int argc, char *argv[])
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
// simavr leaves TXEN set out of reset where silicon clears the whole
// UCSR#B (§20.11.3). Harmless to a loader that enables TXEN itself
// UCSR#B (section 20.11.3). Harmless to a loader that enables TXEN itself -
// but a half-duplex build's receiver-only init then *drops* TXEN,
// and this uart model clears UDRE on that edge and never re-raises
// it on a later enable: the first transmitter after the hand-over
// waits UDRE forever, a wedge silicon does not have. Start from the
// datasheet's zero, as the software bridge's tx-owner model does.
for (avr_io_t *io = avr->io_port; io; io = io->next)
for (avr_io_t *io = avr->io_port; io; io = io->next) {
if (io->kind && std::string_view{io->kind} == "uart" &&
reinterpret_cast<avr_uart_t *>(io)->name == uart_digit)
reinterpret_cast<avr_uart_t *>(io)->name == uart_digit) {
hw_uart = reinterpret_cast<avr_uart_t *>(io);
if (hw_uart)
}
}
if (hw_uart) {
avr_regbit_clear(avr, hw_uart->txen);
}
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, uart_digit);
if (window_report)
if (window_report) {
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_UART_GETIRQ(uart_digit), UART_IRQ_OUTPUT),
window_uart_hook, nullptr);
}
std::println("PB_PTY {}", uart_pty.pty.slavename);
} else {
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
if (sw_tx_owner)
if (sw_tx_owner) {
find_tx_owner();
}
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), static_cast<unsigned>(sw_rx_bit));
avr_irq_register_notify(
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
nullptr);
if (link_one_wire)
if (link_one_wire) {
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), IOPORT_IRQ_DIRECTION_ALL),
on_ddr, nullptr);
}
bridge_drive(1); // idle line
int slave;
@@ -632,8 +664,9 @@ int main(int argc, char *argv[])
long since_poll = 0;
for (;;) {
int state = avr_run(avr);
if (state == cpu_Done || state == cpu_Crashed)
if (state == cpu_Done || state == cpu_Crashed) {
break;
}
if (reset_requested) {
reset_requested = 0;
avr_reset(avr);
@@ -643,8 +676,9 @@ int main(int argc, char *argv[])
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
if (hw_uart) // and simavr's bogus reset TXEN (§20.11.3: zero)
if (hw_uart) { // and simavr's bogus reset TXEN (section 20.11.3: zero)
avr_regbit_clear(avr, hw_uart->txen);
}
} else {
bridge_reset();
reset_tx_owner();
@@ -655,13 +689,14 @@ int main(int argc, char *argv[])
poll_pty();
// An unthrottled idle simulation runs the activation window out
// from under the host's real-time knock cadence: a 1 MHz build's
// 8 s window is 8 M cycles tens of wall milliseconds so a
// 8 s window is 8 M cycles - tens of wall milliseconds - so a
// first knock lost to an in-flight reset misses the window
// entirely. Pace the simulation only while the bridge is fully
// quiet (nothing decoding, nothing queued); transfers keep full
// speed, and a quiet window stretches toward real time.
if (!window_report && !rx_active && !tx_active && rx_head == rx_tail)
if (!window_report && !rx_active && !tx_active && rx_head == rx_tail) {
usleep(200);
}
}
}
finish(0);