mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-24 14:25:34 +00:00
Speed up Frame printing to (finally) solve missing AVCLAN bus frames
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+83
-23
@@ -6,8 +6,11 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
#include "frame.hpp"
|
#include "frame.hpp"
|
||||||
|
#include "hal/stdio.h"
|
||||||
|
#include "stdshim.hpp"
|
||||||
|
|
||||||
#if defined(AVCLAN_FRAME_POOL_N)
|
#if defined(AVCLAN_FRAME_POOL_N)
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -65,40 +68,97 @@ void Frame::operator delete(void *ptr) noexcept {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Emit `value` as at least `width` (lowercase, as to_chars emits) hex digits,
|
||||||
|
// zero-padded. `width` must be <= 3 (see the padding below); every call site
|
||||||
|
// reserves its field's worst case in the destination buffer, so this can't
|
||||||
|
// overflow.
|
||||||
|
template <class T>
|
||||||
|
requires std::is_unsigned_v<T>
|
||||||
|
char *put_hex(char *dest, T value, uint8_t width) {
|
||||||
|
// to_chars emits the minimum number of digits, so work out up front how many
|
||||||
|
// that will be to know how much zero padding goes in front of them. At most
|
||||||
|
// two iterations for the field widths used here.
|
||||||
|
uint8_t ndigits = 1;
|
||||||
|
for (T rest = value; rest >= 16; rest /= 16)
|
||||||
|
ndigits++;
|
||||||
|
|
||||||
|
// At most two pad digits (width <= 3). Written out rather than as a counted
|
||||||
|
// loop because GCC turns that into a memset() call — call overhead an order
|
||||||
|
// of magnitude above the one or two stores it replaces.
|
||||||
|
if (ndigits < width) {
|
||||||
|
*dest++ = '0';
|
||||||
|
if (ndigits + 1 < width)
|
||||||
|
*dest++ = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_chars(dest, dest + ndigits, value, 16).ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// " 0xNNN" for each field, plus the leading unicast digit and trailing newline
|
||||||
|
constexpr uint8_t LINE_MAX = 1 + (4 * 6) + (Frame::MAXLENGTH * 5) + 1;
|
||||||
|
// The same buffer serves the binary branch, which is the shorter of the two
|
||||||
|
static_assert(LINE_MAX >= 8 + Frame::MAXLENGTH + 2);
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void Frame::print(Frame::Print print) const {
|
void Frame::print(Frame::Print print) const {
|
||||||
|
// The AVC-LAN read loop polls for start bits between calls, so emitting per
|
||||||
|
// character (~3 µs each through stdio) would blow past a start bit's ~169 µs
|
||||||
|
// and lose the next frame.
|
||||||
|
char buffer[LINE_MAX];
|
||||||
|
char *bptr = buffer;
|
||||||
|
|
||||||
if (print.binary) {
|
if (print.binary) {
|
||||||
uint8_t buffer[8];
|
|
||||||
uint8_t *bptr = buffer;
|
|
||||||
*bptr++ = 0x10; // Data Link Escape, signaling binary data forthcoming
|
*bptr++ = 0x10; // Data Link Escape, signaling binary data forthcoming
|
||||||
*bptr++ = static_cast<uint8_t>(is_unicast);
|
*bptr++ = static_cast<char>(is_unicast);
|
||||||
|
|
||||||
// Send addresses in big-endian order
|
// Send addresses in big-endian order
|
||||||
*bptr++ = static_cast<uint8_t>(controller_addr >> 8);
|
*bptr++ = static_cast<char>(controller_addr >> 8);
|
||||||
*bptr++ = static_cast<uint8_t>(controller_addr);
|
*bptr++ = static_cast<char>(controller_addr);
|
||||||
*bptr++ = static_cast<uint8_t>(peripheral_addr >> 8);
|
*bptr++ = static_cast<char>(peripheral_addr >> 8);
|
||||||
*bptr++ = static_cast<uint8_t>(peripheral_addr);
|
*bptr++ = static_cast<char>(peripheral_addr);
|
||||||
|
|
||||||
*bptr++ = control;
|
*bptr++ = static_cast<char>(control);
|
||||||
*bptr++ = length;
|
*bptr++ = static_cast<char>(length);
|
||||||
fwrite(buffer, 1, 8, stdout);
|
|
||||||
fwrite(data, 1, length, stdout);
|
memcpy(bptr, data, length);
|
||||||
|
bptr += length;
|
||||||
|
|
||||||
bptr = buffer;
|
|
||||||
*bptr++ = 0x17; // End of transmission block
|
*bptr++ = 0x17; // End of transmission block
|
||||||
*bptr++ = 0x0A; // \n
|
*bptr++ = 0x0A; // \n
|
||||||
fwrite(buffer, 1, 2, stdout);
|
|
||||||
} else {
|
|
||||||
printf("%X", static_cast<unsigned>(is_unicast));
|
|
||||||
printf(" 0x%03X", static_cast<unsigned>(controller_addr & 0x0FFF));
|
|
||||||
printf(" 0x%03X", static_cast<unsigned>(peripheral_addr & 0x0FFF));
|
|
||||||
printf(" 0x%X", static_cast<unsigned>(control & 0x0F));
|
|
||||||
printf(" 0x%X", static_cast<unsigned>(length & 0x0F));
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < length; i++) {
|
stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
|
||||||
printf(" 0x%02X", static_cast<unsigned>(data[i]));
|
return;
|
||||||
}
|
|
||||||
putchar('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct field_t {
|
||||||
|
uint16_t value;
|
||||||
|
uint8_t width; // minimum digits, matching the old %03x / %x formats
|
||||||
|
};
|
||||||
|
|
||||||
|
*bptr++ = is_unicast ? '1' : '0';
|
||||||
|
for (const field_t field :
|
||||||
|
{field_t{.value = static_cast<uint16_t>(controller_addr & 0x0FFF),
|
||||||
|
.width = 3},
|
||||||
|
field_t{.value = static_cast<uint16_t>(peripheral_addr & 0x0FFF),
|
||||||
|
.width = 3},
|
||||||
|
field_t{.value = static_cast<uint16_t>(control & 0x0F), .width = 1},
|
||||||
|
field_t{.value = static_cast<uint16_t>(length & 0x0F), .width = 1}}) {
|
||||||
|
*bptr++ = ' ';
|
||||||
|
*bptr++ = '0';
|
||||||
|
*bptr++ = 'x';
|
||||||
|
bptr = put_hex(bptr, field.value, field.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < length; i++) {
|
||||||
|
*bptr++ = ' ';
|
||||||
|
*bptr++ = '0';
|
||||||
|
*bptr++ = 'x';
|
||||||
|
bptr = put_hex(bptr, data[i], 2);
|
||||||
|
}
|
||||||
|
*bptr++ = '\n';
|
||||||
|
|
||||||
|
stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
||||||
|
|||||||
+19
-2
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
@@ -11,10 +13,25 @@ extern "C" {
|
|||||||
// functions. Assumptions/invariants:
|
// functions. Assumptions/invariants:
|
||||||
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when
|
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when
|
||||||
// empty). Necessary to avoid stalling the REPL poll loop.
|
// empty). Necessary to avoid stalling the REPL poll loop.
|
||||||
// - stdout is *raw* — no '\n' -> "\r\n" translation: bare LF and binary frame
|
// - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
|
||||||
// payloads (fwrite) are emitted verbatim
|
// change a bare LF or a binary frame payload. Writes through
|
||||||
|
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
|
||||||
|
// stream, in order.
|
||||||
void stdio_init(void);
|
void stdio_init(void);
|
||||||
|
|
||||||
|
// Queue a buffer onto stdout--if it has space, or return false.
|
||||||
|
//
|
||||||
|
// If stdout cannot accept all `len` bytes now, the function queues
|
||||||
|
// a "\n!\n" indicator onto stdout, overwriting the last 3 chars if stdout is
|
||||||
|
// full. The caller must not assume that the buffer went out.
|
||||||
|
//
|
||||||
|
// A port MUST emit that indicator for every dropped buffer, and MUST be able to
|
||||||
|
// emit it even if stdout is full. A reader detects the loss as a line that
|
||||||
|
// holds only '!'. In the overwrite case the buffer in front of that line is
|
||||||
|
// truncated, so a reader MUST tolerate one malformed buffer there. The return
|
||||||
|
// value is useful only for diagnostics.
|
||||||
|
bool stdio_write_nonblock(const void *buf, size_t len);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -23,3 +23,50 @@ using std::unexpect;
|
|||||||
using std::unexpect_t;
|
using std::unexpect_t;
|
||||||
} // namespace avclan
|
} // namespace avclan
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(__cpp_lib_to_chars) || __cpp_lib_to_chars < 201611L
|
||||||
|
#include <cstdint>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace avclan {
|
||||||
|
// No <charconv> (nor the <system_error> its result type needs) in the
|
||||||
|
// freestanding stdlib, so mirror the std API surface for the unsigned-integral
|
||||||
|
// overload — the only one used here. Values are those of std::errc, so call
|
||||||
|
// sites written against either spelling behave the same.
|
||||||
|
enum class errc : uint8_t { value_too_large = 75 };
|
||||||
|
|
||||||
|
struct to_chars_result {
|
||||||
|
char *ptr;
|
||||||
|
errc ec;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
requires std::is_integral_v<T> && std::is_unsigned_v<T>
|
||||||
|
constexpr to_chars_result to_chars(char *first, char *last, T value,
|
||||||
|
int base = 10) {
|
||||||
|
char digits[sizeof(T) * 8]; // worst case: base 2
|
||||||
|
uint8_t ndigits = 0;
|
||||||
|
do {
|
||||||
|
const auto digit = static_cast<unsigned>(value % static_cast<T>(base));
|
||||||
|
digits[ndigits++] =
|
||||||
|
static_cast<char>(digit < 10 ? '0' + digit : 'a' + (digit - 10));
|
||||||
|
value = static_cast<T>(value / static_cast<T>(base));
|
||||||
|
} while (value != 0);
|
||||||
|
|
||||||
|
if ((last - first) < ndigits)
|
||||||
|
return {.ptr = last, .ec = errc::value_too_large};
|
||||||
|
|
||||||
|
while (ndigits > 0)
|
||||||
|
*first++ = digits[--ndigits];
|
||||||
|
return {.ptr = first, .ec = errc{}};
|
||||||
|
}
|
||||||
|
} // namespace avclan
|
||||||
|
#else
|
||||||
|
#include <charconv>
|
||||||
|
|
||||||
|
namespace avclan {
|
||||||
|
using errc = std::errc;
|
||||||
|
using std::to_chars;
|
||||||
|
using std::to_chars_result;
|
||||||
|
} // namespace avclan
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -4,11 +4,27 @@
|
|||||||
#include <avr/io.h>
|
#include <avr/io.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <util/atomic.h>
|
||||||
|
|
||||||
#include "hal/stdio.h"
|
#include "hal/stdio.h"
|
||||||
#include "timing_avr.h" // IWYU pragma: export
|
#include "timing_avr.h" // IWYU pragma: export
|
||||||
#include "usart.h" // jnk0le AVR-UART-lib
|
#include "usart.h" // jnk0le AVR-UART-lib
|
||||||
|
|
||||||
|
// TX ring owned by the jnk0le lib. `tx0_Head` is the index of the last byte
|
||||||
|
// written, so the next write goes to head+1. The DRE ISR reads tail+1 and then
|
||||||
|
// advances `tx0_Tail`. The ring is empty when the two indexes are equal. The
|
||||||
|
// ring holds at most TX0_BUFFER_SIZE-1 bytes.
|
||||||
|
extern char tx0_buffer[TX0_BUFFER_SIZE];
|
||||||
|
|
||||||
|
// stdio_write_nonblock() is all-or-nothing. If the ring cannot hold the largest
|
||||||
|
// buffer that the app writes, the function drops every buffer and queues none.
|
||||||
|
// The largest buffer is a text frame log line (Frame::print). That line is
|
||||||
|
// ~186 bytes at MAXLENGTH=32. The ring therefore needs headroom above this
|
||||||
|
// size.
|
||||||
|
static_assert(TX0_BUFFER_SIZE - 1 >= 192,
|
||||||
|
"TX ring too small to hold a whole frame log line");
|
||||||
|
|
||||||
// Raw stdout backend: emit the byte verbatim, no '\n' -> "\r\n" translation
|
// Raw stdout backend: emit the byte verbatim, no '\n' -> "\r\n" translation
|
||||||
// (unlike the lib's uart_putchar). Keeps binary frame payloads intact.
|
// (unlike the lib's uart_putchar). Keeps binary frame payloads intact.
|
||||||
static int stdio_putchar(char data, FILE *stream) {
|
static int stdio_putchar(char data, FILE *stream) {
|
||||||
@@ -17,6 +33,62 @@ static int stdio_putchar(char data, FILE *stream) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool stdio_write_nonblock(const void *buf, size_t len) {
|
||||||
|
if (len == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// A single unguarded read of each index is enough:
|
||||||
|
// - Only main writes tx0_Head.
|
||||||
|
// - Only the DRE ISR writes tx0_Tail.
|
||||||
|
// - 8-bit accesses are atomic on AVR.
|
||||||
|
// If the ISR advances the tail during this function, the ring has more free
|
||||||
|
// space than this function measured. That direction is safe.
|
||||||
|
const uint8_t head = tx0_Head;
|
||||||
|
const uint8_t space = (uint8_t)((tx0_Tail - head - 1) & TX0_BUFFER_MASK);
|
||||||
|
if (len > space) {
|
||||||
|
// The ring cannot take the whole buffer, so drop it and queue the indicator
|
||||||
|
// instead. A buffer may be refused because it is larger than
|
||||||
|
// available space or because the ring is full. For the former case, the
|
||||||
|
// full indicator can be appended without truncating previously queued
|
||||||
|
// bytes.
|
||||||
|
if (space >= 3) {
|
||||||
|
tx0_buffer[(head + 1) & TX0_BUFFER_MASK] = '\n';
|
||||||
|
tx0_buffer[(head + 2) & TX0_BUFFER_MASK] = '!';
|
||||||
|
tx0_buffer[(head + 3) & TX0_BUFFER_MASK] = '\n';
|
||||||
|
tx0_Head = (uint8_t)((head + 3) & TX0_BUFFER_MASK);
|
||||||
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { USART0.CTRLA |= USART_DREIE_bm; }
|
||||||
|
} else {
|
||||||
|
// No room for even the indicator, so it goes over the last three queued
|
||||||
|
// bytes. `space` and the queued byte count always sum to
|
||||||
|
// TX0_BUFFER_MASK, so space < 3 means at least 253 bytes are queued and
|
||||||
|
// all three writes land inside them. The ISR already drains the ring, so
|
||||||
|
// DREIE needs no change.
|
||||||
|
tx0_buffer[(head - 2) & TX0_BUFFER_MASK] = '\n';
|
||||||
|
tx0_buffer[(head - 1) & TX0_BUFFER_MASK] = '!';
|
||||||
|
tx0_buffer[head] = '\n';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t start = (uint8_t)((head + 1) & TX0_BUFFER_MASK);
|
||||||
|
const size_t contiguous = TX0_BUFFER_SIZE - start;
|
||||||
|
|
||||||
|
if (len <= contiguous) {
|
||||||
|
memcpy(&tx0_buffer[start], buf, len);
|
||||||
|
} else { // wraps the end of the ring
|
||||||
|
memcpy(&tx0_buffer[start], buf, contiguous);
|
||||||
|
memcpy(&tx0_buffer[0], (const char *)buf + contiguous, len - contiguous);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the bytes only after all of them are in place. This is a single
|
||||||
|
// store, so the ISR never sees a partly filled buffer.
|
||||||
|
tx0_Head = (uint8_t)((head + len) & TX0_BUFFER_MASK);
|
||||||
|
|
||||||
|
// The ISR also writes CTRLA (to clear DREIE when it drains the last byte).
|
||||||
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { USART0.CTRLA |= USART_DREIE_bm; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Non-blocking stdin backend: next received byte, or _FDEV_EOF when the RX ring
|
// Non-blocking stdin backend: next received byte, or _FDEV_EOF when the RX ring
|
||||||
// is empty (uart0_getData() returns a negative value when there is no data).
|
// is empty (uart0_getData() returns a negative value when there is no data).
|
||||||
static int stdio_getchar(FILE *stream) {
|
static int stdio_getchar(FILE *stream) {
|
||||||
@@ -34,14 +106,14 @@ void stdio_init(void) {
|
|||||||
// The lib's uart0_init configures the USART registers/baud but not the pins;
|
// The lib's uart0_init configures the USART registers/baud but not the pins;
|
||||||
// keep the ATtiny3216 pin-mux the old driver did.
|
// keep the ATtiny3216 pin-mux the old driver did.
|
||||||
PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2
|
PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2
|
||||||
PORTA.DIRSET = PIN1_bm; // TxD output
|
PORTA.DIRSET = PIN1_bm; // TxD output
|
||||||
PORTA.DIRCLR = PIN2_bm; // RxD input
|
PORTA.DIRCLR = PIN2_bm; // RxD input
|
||||||
|
|
||||||
#ifdef USE_DOUBLE_SPEED
|
#ifdef USE_DOUBLE_SPEED
|
||||||
uart0_init(DOUBLE_BAUD_CALC(1200000));
|
uart0_init(DOUBLE_BAUD_CALC(1200000));
|
||||||
#else
|
#else
|
||||||
uart0_init(BAUD_CALC(1200000));
|
uart0_init(BAUD_CALC(1200000));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
stdout = stdin = &stdio_stream; // printf/fputs/fwrite + non-blocking getchar
|
stdout = stdin = &stdio_stream; // printf/fputs/fwrite + non-blocking getchar
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user