10 Commits

Author SHA1 Message Date
Allen Hill 5b9cb1da8b Add single byte send debug function 2026-09-20 15:08:19 -07:00
Allen Hill fbf5811496 Refine (further) the Phy API
- Refactor send/read data to read entire array instead of byte-oriented
  reads
- Simplify/improve consistency of asynchrony (all send functions now
  optionally async)
- Fix start-bit TX (now actually validate joinable start-bits)

Assisted-by: Claude Code (Opus 5)
2026-09-20 15:08:15 -07:00
Allen Hill 7252e6bfdd fixup PIO comment rewriting oops 2026-09-20 15:08:15 -07:00
Allen Hill 6b22f14e6d Fix deafen semantics 2026-09-20 15:08:15 -07:00
Allen Hill 2c85305c0d Finish Pico/PIO phy layer
Assisted-by: Claude Code (Opus 5)
2026-09-20 15:08:11 -07:00
Allen Hill 8a47b5b776 Refactor peripheral/bus/phy APIs (prep for pico/PIO)
- Refactor PHY to be higher-level with a field focus (controller/peripheral address,
  control byte, etc)
  - Phy layer is now device address aware (ACK's automatically) (Hardens
    previously ~implicit design to only support emulating one AVCLAN
    unit per mockingboard build)
- Redesign `phy_active` => `phy_frame_pending` (to allow
  asynchronous/buffered frame reads on pico) and embed within
  Peripheral::read
- Add `deafen` function: like mute, but also ignore incoming messages (i.e.
  don't receive/process at all). Makes `Bus::is_active` always return
  `false`.
- Add debug bus driving functions

Assisted-by: Claude Code (Opus 5)
2026-09-20 15:07:42 -07:00
Allen Hill 9efbb01ccf PIO proof-of-concept 2026-09-20 15:03:31 -07:00
Allen Hill 53c5abb3e9 Fix compile errors due to non-portable code 2026-09-03 16:07:27 -07:00
Allen Hill ac47cebe86 Add ~empty implementation of HAL for pico2 2026-09-03 16:06:57 -07:00
Allen Hill e30dbc501d initial build system tweaks for pico hardware 2026-09-03 12:50:00 -07:00
22 changed files with 2011 additions and 329 deletions
+4 -2
View File
@@ -19,12 +19,14 @@ Checks: |
-readability-function-cognitive-complexity, -readability-function-cognitive-complexity,
-misc-non-private-member-variables-in-classes -misc-non-private-member-variables-in-classes
WarningsAsErrors: "" WarningsAsErrors: ""
ExcludeHeaderFilterRegex: 'out/build' ExcludeHeaderFilterRegex: "out/build"
HeaderFilterRegex: '^src/.*' HeaderFilterRegex: "^src/.*"
SystemHeaders: false SystemHeaders: false
# AnalyzeTemporaryDtors: false # AnalyzeTemporaryDtors: false
FormatStyle: none FormatStyle: none
CheckOptions: CheckOptions:
- key: modernize-deprecated-headers.CheckHeaderFile
value: "false"
- key: readability-magic-numbers.IgnorePowersOf2IntegerValues - key: readability-magic-numbers.IgnorePowersOf2IntegerValues
value: "true" value: "true"
- key: readability-magic-numbers.IgnoredIntegerValues - key: readability-magic-numbers.IgnoredIntegerValues
+4
View File
@@ -8,3 +8,7 @@ CompileFlags:
- -ffixed-r2 - -ffixed-r2
- -ffixed-r3 - -ffixed-r3
- -ffixed-r4 - -ffixed-r4
- -fzero-init-padding-bits
- -fno-malloc-dce
Add:
- -Wno-missing-field-initializers
+38 -25
View File
@@ -1,25 +1,32 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.30)
project(avclan-mockingboard VERSION 1.1 LANGUAGES C CXX) if(AVCLAN_TARGET_SDK_FILE)
include(${AVCLAN_TARGET_SDK_FILE})
endif()
project(avclan-mockingboard VERSION 1.2 LANGUAGES C CXX ASM)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
# Hardware target (port) selection. The chosen target/<name>/ directory supplies
# the port implementation sources, the per-target include path, and all
# hardware-specific build configuration (compile defs/options, device-pack
# handling, flashing) via its own CMakeLists, pulled in with add_subdirectory
# below. The matching cross-compiler is selected separately by a toolchain file
# (e.g. cmake/avr-gcc-toolchain.cmake) named in a CMake preset. Adding a new
# board = a sibling target/ directory + a toolchain file + a preset wiring them.
set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build")
set_property(CACHE AVCLAN_TARGET PROPERTY STRINGS "avr-attiny3216" "pico2" "esp32")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_MSG_QUEUE_SIZE 16 CACHE STRING "Size of incoming/outgoing queues")
set(AVCLAN_FRAME_POOL_N 14 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
set(CMAKE_C_STANDARD 23) set(CMAKE_C_STANDARD 23)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
add_compile_options(
-Wall -Wswitch-enum -Werror
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13")
@@ -52,15 +59,6 @@ if(INSTALL_GIT_HOOKS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
endif() endif()
endif() endif()
# Hardware target (port) selection. The chosen target/<name>/ directory supplies
# the port implementation sources, the per-target include path, and all
# hardware-specific build configuration (compile defs/options, device-pack
# handling, flashing) via its own CMakeLists, pulled in with add_subdirectory
# below. The matching cross-compiler is selected separately by a toolchain file
# (e.g. cmake/avr-gcc-toolchain.cmake) named in a CMake preset. Adding a new
# board = a sibling target/ directory + a toolchain file + a preset wiring them.
set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build")
# The AVC-LAN stack as a static library: the target-agnostic generic core (no # The AVC-LAN stack as a static library: the target-agnostic generic core (no
# <avr/...>, no register access). The selected target's port sources and flags # <avr/...>, no register access). The selected target's port sources and flags
# are contributed by its subdirectory. # are contributed by its subdirectory.
@@ -70,6 +68,21 @@ add_library(avclan STATIC
src/avclan/bus.cc src/avclan/bus.cc
) )
target_compile_definitions(avclan PUBLIC
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
AVCLAN_MSG_QUEUE_SIZE=${AVCLAN_MSG_QUEUE_SIZE}
)
target_compile_options(avclan PUBLIC
-Wall -Wswitch-enum -Werror
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
# avclan exports its public generic headers (src/avclan) to consumers and # avclan exports its public generic headers (src/avclan) to consumers and
# reaches into src/ for sibling headers (stdio.h, board.h) during its own # reaches into src/ for sibling headers (stdio.h, board.h) during its own
# build. The selected target adds its own per-target include path. # build. The selected target adds its own per-target include path.
+46 -10
View File
@@ -1,6 +1,24 @@
{ {
"version": 3, "version": 9,
"configurePresets": [ "configurePresets": [
{
"name": "debug-base",
"hidden": true,
"description": "Debug build settings",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "relwithdebinfo-base",
"hidden": true,
"description": "RelWithDebInfo build settings",
"inherits": "debug-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{ {
"name": "usb0", "name": "usb0",
"hidden": true, "hidden": true,
@@ -31,20 +49,38 @@
} }
}, },
{ {
"name": "debug-base", "name": "pico2",
"hidden": true, "hidden": true,
"description": "Debug build settings", "binaryDir": "${sourceDir}/out/build/pico2",
"inherits": "debug-base",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug" "AVCLAN_TARGET": "pico2",
"AVCLAN_FRAME_POOL_N": "14",
"AVCLAN_TARGET_SDK_FILE": "${sourceDir}/cmake/pico_sdk_import.cmake",
"PICO_SDK_FETCH_FROM_GIT": true
},
"environment": {
"PICO_PLATFORM": "rp2350-arm-s",
"PICO_BOARD": "pimoroni_pico_plus2_w_rp2350"
} }
}, },
{ {
"name": "relwithdebinfo-base", "name": "pico2-gcc",
"hidden": true, "displayName": "Pico 2W (GCC)",
"description": "RelWithDebInfo build settings", "inherits": "pico2",
"inherits": "debug-base", "description": "",
"cacheVariables": { "generator": "Ninja",
"CMAKE_BUILD_TYPE": "RelWithDebInfo" "environment": {
"PICO_COMPILER": "pico_arm_gcc"
}
},
{
"name": "pico2-clang",
"displayName": "Pico 2W (Clang)",
"inherits": "pico2",
"description": "",
"environment": {
"PICO_COMPILER": "pico_arm_clang"
} }
}, },
{ {
+121
View File
@@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})
+4 -6
View File
@@ -147,6 +147,7 @@ struct Error {
STARTBIT_MALFORMED, STARTBIT_MALFORMED,
STARTBIT_TOO_LONG, STARTBIT_TOO_LONG,
BAD_STARTBIT, BAD_STARTBIT,
NO_FRAME,
POOL_EMPTY, // non-bus error POOL_EMPTY, // non-bus error
}; };
@@ -155,7 +156,10 @@ struct Error {
NAK_MESSAGE_LENGTH, NAK_MESSAGE_LENGTH,
NAK_CONTROL, NAK_CONTROL,
NAK_ADDRESS, NAK_ADDRESS,
NAK_TOO_LONG,
NAK, // generic NAK has max severity NAK, // generic NAK has max severity
CONTENDED_BUS,
LOST_ARBITRATION,
BUSY, BUSY,
MUTED, MUTED,
}; };
@@ -176,12 +180,6 @@ struct SendError {
}; };
#endif #endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t {
bit_zero = 0x00,
bit_one = 0x01,
bit_start = 0x10
};
#ifdef __cplusplus #ifdef __cplusplus
} // namespace detail } // namespace detail
} // namespace avclan } // namespace avclan
+143 -202
View File
@@ -26,10 +26,12 @@
| 1 │ *Acknowledge* | 1 │ *Acknowledge*
*repeat `n` times* *repeat `n` times*
No acknowledge bits are sent for broadcast frames. For broadcast frames the acknowledge (one) bit is sent, but an ACK response
(zero) is not expected.
*/ */
#include <concepts> #include <concepts>
#include <cstdint>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <new> #include <new>
@@ -43,16 +45,12 @@
namespace { namespace {
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send; using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
struct trailer_bits_t {}; // The bus spec has a unit that loses arbitration retry rather than fail: "if
struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit) // the unit loses in arbitration, the frame is automatically reset up twice
struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address) // (three times in total)". Only an attempt that is outbid every time is an
struct with_ack_t : trailer_bits_t { // error worth reporting.
}; // bits + parity + ACK slot (all other fields) constexpr uint8_t SEND_ATTEMPTS = 3;
inline constexpr no_parity_t no_parity{};
inline constexpr with_parity_t with_parity{};
inline constexpr with_ack_t with_ack{};
} // namespace } // namespace
namespace avclan { namespace avclan {
@@ -71,133 +69,69 @@ public:
Handle(const Handle &) = delete; Handle(const Handle &) = delete;
Handle(Handle &&) = delete; Handle(Handle &&) = delete;
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // Forward to phy API (organized so that hal/phy.h isn't public/visible at the
Send sendstartbit() { return phy_send_startbit(); }; // C++/library level)
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTBEGIN(readability-convert-member-functions-to-static)
Read readstartbit() { return phy_read_startbit(); }; Read read_header(bool *is_unicast) { return phy_read_header(is_unicast); };
Read read_controller_addr(uint16_t *addr) {
template <auto N, std::unsigned_integral T, return phy_read_controller_addr(addr);
std::derived_from<trailer_bits_t> Trailer> };
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>) Read read_peripheral_addr(uint16_t *addr) {
Send send(T bits, Trailer /*tag*/) { return phy_read_peripheral_addr(addr);
const Bit parity = sendbits<N>(bits); };
Read read_control(uint8_t *control) { return phy_read_control(control); };
if constexpr (std::is_same_v<Trailer, with_parity_t>) Read read_length(uint8_t *length) { return phy_read_length(length); };
sendbits<1>(to_underlying(parity)); Read read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
return phy_read_data(data, length, data_index);
return Send{0};
}; };
template <auto N, std::unsigned_integral T> Send send_header(bool is_unicast) { return phy_send_header(is_unicast); };
requires(sizeof(T) < 3 && N < 16) Send send_controller_addr(uint16_t addr) {
Send send(T bits, with_ack_t /*tag*/, bool expect_ack) { return phy_send_controller_addr(addr);
send<N>(bits, with_parity); };
Send send_peripheral_addr(uint16_t addr, bool expect_ack) {
if (expect_ack) return phy_send_peripheral_addr(addr, expect_ack);
return read_ACK(); };
Send send_control(uint8_t control, bool expect_ack) {
sendbits<1>(1U); // still need to fill the ack bit slot return phy_send_control(control, expect_ack);
return Send{0}; };
Send send_length(uint8_t length, bool expect_ack) {
return phy_send_length(length, expect_ack);
};
Send send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index) {
return phy_send_data(data, length, expect_ack, data_index);
};
// NOLINTEND(readability-convert-member-functions-to-static)
}; };
template <auto N, std::unsigned_integral T, void Bus::init(uint16_t address) {
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Read read(T *bits, Trailer /*tag*/) {
const Bit calc_parity = readbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>) {
uint8_t read_parity;
readbits<1>(&read_parity);
if (to_underlying(calc_parity) != read_parity)
return Read::BAD_PARITY;
}
return Read{0};
};
template <auto N, std::unsigned_integral T, class F>
requires(sizeof(T) < 3 && N < 16)
Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
if (read<N>(bits, with_parity) == Read::BAD_PARITY)
return Read::BAD_PARITY;
if (ack()) {
send_ACK();
} else {
uint8_t slot;
readbits<1>(&slot);
}
return Read{0};
};
template <auto N, std::unsigned_integral T>
requires(sizeof(T) < 3 && N < 16)
Read read(T *bits, with_ack_t /*tag*/, bool ack) {
return read<N>(bits, with_ack, [=]() { return ack; });
}
private:
static void send_ACK() { phy_send_ack(); };
static Send read_ACK() { return phy_read_ack(); };
template <auto N, class T> Bit sendbits(T bits);
template <auto N, class T> Bit readbits(T *bits);
template <auto N>
requires(N > 1 && N < 8)
Bit sendbits(uint8_t bits) {
return phy_send_bits_u8(&bits, N);
};
template <auto N>
requires(N <= 16)
Bit sendbits(uint16_t bits) {
return phy_send_bits_u16(&bits, N);
};
template <auto N>
requires(N < 8)
Bit readbits(uint8_t *bits) {
return static_cast<Bit>(phy_read_bits_u8(bits, N));
};
template <auto N>
requires(N <= 16)
Bit readbits(uint16_t *bits) {
return static_cast<Bit>(phy_read_bits_u16(bits, N));
};
};
template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) {
return phy_send_byte(&bits);
};
template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) {
const Bit bit{static_cast<Bit>(bits & 1U)};
phy_send_bit(bit);
return bit;
};
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
return phy_read_byte(bits);
};
void Bus::init() {
// Idempotent: the single Bus is shared by reference, so every Peripheral's // Idempotent: the single Bus is shared by reference, so every Peripheral's
// ctor calls init() on it — but the hardware must be brought up exactly once // ctor calls init() on it — but the hardware must be brought up exactly once
// (phy_init is not assumed re-entrant/idempotent). // (phy_init is not assumed re-entrant/idempotent).
if (inited_) if (inited_)
return; return;
phy_init(); phy_init(address);
muted_ = false; // phy_init leaves the bus TX unmuted muted_ = false; // phy_init leaves the bus TX unmuted
deafened_ = false; // Default to listening
inited_ = true; inited_ = true;
}; };
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
bool Bus::is_active() const { return phy_active(); }; bool Bus::is_active() const { return !deafened_ && phy_frame_pending(); };
void Bus::mute(bool mute) { void Bus::mute(bool mute) {
phy_mute(mute); phy_mute(mute);
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
}; };
void Bus::deafen(bool deaf) {
phy_deafen(deaf);
deafened_ = deaf; // Only update deafened_ *AFTER* the phy has stopped acking
}
auto Bus::read(uint16_t address, Frame::Print print) auto Bus::read(Frame::Print print)
-> expected<std::unique_ptr<Frame>, Error::Read> { -> expected<std::unique_ptr<Frame>, Error::Read> {
struct errtype { struct errtype {
Read errno; Read type;
uint16_t val; uint16_t val;
} err = {}; } err = {};
@@ -205,88 +139,67 @@ auto Bus::read(uint16_t address, Frame::Print print)
std::unique_ptr<Frame> in(new (std::nothrow) Frame); std::unique_ptr<Frame> in(new (std::nothrow) Frame);
if (!in) { if (!in) {
err.errno = POOL_EMPTY; err.type = POOL_EMPTY;
goto handle_err; goto handle_err;
} }
{ // bound handle lifetime { // bound handle lifetime
auto handle = get(); auto handle = get();
bool shouldACK = false; err.type = handle.read_header(&in->is_unicast);
uint8_t tmp = 0; if (err.type != Read{0})
err.errno = handle.readstartbit();
if (err.errno != Read{0})
goto handle_err; goto handle_err;
handle.read<1>(&tmp, no_parity); err.type = handle.read_controller_addr(&in->controller_addr);
in->is_unicast = (tmp != 0U); if (err.type != Read{0}) {
if (auto rerr = handle.read<12>(&in->controller_addr, with_parity);
rerr == BAD_PARITY) {
err.errno = BAD_CONTROLLER_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->controller_addr; err.val = in->controller_addr;
goto handle_err; goto handle_err;
} }
// Using lambda for delayed evaluation of peripheral_addr field err.type = handle.read_peripheral_addr(&in->peripheral_addr);
// deref, which will be written by the time the lambda is evaluated if (err.type != Read{0}) {
auto should_ack_lambda = [&]() {
shouldACK = !is_muted() && (in->peripheral_addr == address);
return shouldACK;
};
if (auto rerr =
handle.read<12>(&in->peripheral_addr, with_ack, should_ack_lambda);
rerr == BAD_PARITY) {
err.errno = BAD_PERIPHERAL_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->peripheral_addr; err.val = in->peripheral_addr;
goto handle_err; goto handle_err;
} }
if (auto rerr = handle.read<4>(&in->control, with_ack, shouldACK); err.type = handle.read_control(&in->control);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_CONTROL_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->control; err.val = in->control;
goto handle_err; goto handle_err;
} }
if (auto rerr = handle.read<8>(&in->length, with_ack, shouldACK); err.type = handle.read_length(&in->length);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_LENGTH_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->length; err.val = in->length;
goto handle_err; goto handle_err;
} } else if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.type = BAD_LENGTH_RANGE;
if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.errno = BAD_LENGTH_RANGE;
err.val = in->length; err.val = in->length;
goto handle_err; goto handle_err;
} }
for (uint8_t i = 0; i < in->length; i++) { uint8_t data_i = 0;
if (auto rerr = handle.read<8>(&in->data[i], with_ack, shouldACK); err.type = handle.read_data(in->data, in->length, &data_i);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_DATA_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->data[i]; err.val = in->data[data_i];
goto handle_err; goto handle_err;
} }
}
} // destroy handle } // destroy handle
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("ERR(read): ", stdout); fputs("ERR(read): ", stdout);
switch (err.errno) { switch (err.type) {
case POOL_EMPTY: puts("failed Frame alloc"); break; case POOL_EMPTY: puts("failed Frame alloc"); break;
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break; case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_MISSED: fputs("missed start bit", stdout); break; case STARTBIT_MISSED: fputs("missed start bit", stdout); break;
@@ -304,6 +217,7 @@ auto Bus::read(uint16_t address, Frame::Print print)
case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE; case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE;
case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break; case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break;
case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE; case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE;
case NO_FRAME:
case BAD_PARITY: case BAD_PARITY:
__builtin_unreachable(); __builtin_unreachable();
VERBOSE: VERBOSE:
@@ -315,14 +229,14 @@ auto Bus::read(uint16_t address, Frame::Print print)
} }
// Only print if some data has been correctly received // Only print if some data has been correctly received
if (print.print && (err.errno < STARTBIT_MISSED)) { if (print.print && (err.type < STARTBIT_MISSED)) {
if (err.errno > BAD_DATA_PARITY) if (err.type > BAD_DATA_PARITY)
in->length = 0; in->length = 0;
in->print(print); in->print(print);
} }
if (err.errno != Read{0}) if (err.type != Read{0})
return unexpected(err.errno); return unexpected(err.type);
return in; return in;
} }
@@ -331,79 +245,91 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
struct errtype { struct errtype {
// Error enum is ordered such that a lower numeric value corresponds to // Error enum is ordered such that a lower numeric value corresponds to
// more success // more success
Send errno; Send type;
uint8_t val; uint8_t val;
} err = {}; } err = {};
using enum Send; using enum Send;
if (is_muted()) { if (is_muted()) {
err.errno = MUTED; err.type = MUTED;
goto handle_err; goto handle_err;
} }
{ // bound handle lifetime for (uint8_t attempt = 0; attempt < SEND_ATTEMPTS; attempt++) {
auto handle = get(); auto handle = get(); // bound handle lifetime
if (handle.sendstartbit() == BUSY) { err.type = Send{0};
// Some other device is already driving the bus
err.errno = BUSY; err.type = handle.send_header(out.is_unicast);
if (err.type != Send{0}) // BUSY or LOST_ARBITRATION (broadcast)
continue;
err.type = handle.send_controller_addr(out.controller_addr);
if (err.type != Send{0}) // LOST_ARBITRATION: other device has lower address
continue;
err.type = handle.send_peripheral_addr(out.peripheral_addr, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_control(out.control, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_length(out.length, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type =
handle.send_data(out.data, out.length, out.is_unicast, &err.val);
if (err.type != Send{0})
goto handle_err;
// A phy that only queued the fields above settles them here
uint8_t data_i = 0;
err.type = phy_send_done(&data_i);
if (err.type == BUSY || err.type == LOST_ARBITRATION) // Queued header
continue;
if (err.type != Send{0}) {
err.val = data_i;
goto handle_err; goto handle_err;
} }
handle.send<1>(static_cast<uint8_t>(out.is_unicast), no_parity); break; // Sent
handle.send<12>(out.controller_addr, with_parity);
if (auto serr =
handle.send<12>(out.peripheral_addr, with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_ADDRESS;
goto handle_err;
} }
if (auto serr = handle.send<4>(out.control, with_ack, out.is_unicast); if (err.type != Send{0})
serr == NAK) { goto handle_err; // Outbid (or busy) on every attempt
err.errno = NAK_CONTROL;
goto handle_err;
}
if (auto serr = handle.send<8>(out.length, with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_MESSAGE_LENGTH;
goto handle_err;
}
for (uint8_t i = 0; i < out.length; i++) {
if (auto serr = handle.send<8>(out.data[i], with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_DATA;
err.val = i;
goto handle_err;
}
}
} // destroy handle
// back to read mode // back to read mode
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("Error", stdout); fputs("Error", stdout);
switch (err.errno) { switch (err.type) {
case MUTED: fputs(": Device muted", stdout); break; case MUTED: fputs(": Device muted", stdout); break;
case BUSY: fputs(": Busy bus", stdout); break; case BUSY: fputs(": Busy bus", stdout); break;
case CONTENDED_BUS:
fputs(": bus contended after arbitration", stdout);
break;
case LOST_ARBITRATION: fputs(": lost arbitration", stdout); break;
case NAK_ADDRESS: case NAK_ADDRESS:
case NAK_CONTROL: case NAK_CONTROL:
case NAK_MESSAGE_LENGTH: case NAK_MESSAGE_LENGTH:
case NAK_DATA: case NAK_DATA:
case NAK_TOO_LONG:
case NAK: case NAK:
fputs(" NAK: ", stdout); fputs(" NAK: ", stdout);
switch (err.errno) { switch (err.type) {
case NAK_ADDRESS: fputs("address", stdout); break; case NAK_ADDRESS: fputs("address", stdout); break;
case NAK_CONTROL: fputs("Control", stdout); break; case NAK_CONTROL: fputs("Control", stdout); break;
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break; case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
case NAK_DATA: printf(" data[%u]", err.val); break; case NAK_DATA: printf(" data[%u]", err.val); break;
case NAK_TOO_LONG: fputs("too long", stdout); break;
case NAK: case NAK:
case MUTED: case MUTED:
case CONTENDED_BUS:
case LOST_ARBITRATION:
case BUSY: __builtin_unreachable(); case BUSY: __builtin_unreachable();
} }
break; break;
@@ -414,16 +340,31 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
if (print.print) if (print.print)
out.print(print); out.print(print);
return err.errno; return err.type;
} }
Bus::Handle Bus::get() { return Handle{*this}; }; Bus::Handle Bus::get() { return Handle{*this}; };
#if !defined(NDEBUG) && defined(MEASURE_BUS) #if !defined(NDEBUG)
// Debug bit-timing measurement on the one physical bus; instance-scoped for the
// same reason as is_active(). void Bus::set_dominant() { phy_set_dominant(); }
void Bus::set_recessive() { phy_set_recessive(); }
Send Bus::sendbyte(uint8_t byte, bool ack) {
auto handle = get();
uint8_t data_i = 0;
if (const Send serr = handle.send_data(&byte, 1, ack, &data_i);
serr != Send{0})
return serr;
return phy_send_done(&data_i);
}
#ifdef MEASURE_BUS
// Debug bit-timing measurement on the one physical bus; instance-scoped for
// the same reason as is_active().
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void Bus::measure() { phy_measure(); } void Bus::measure() { phy_measure(); }
#endif #endif
#endif
} // namespace avclan } // namespace avclan
+16 -3
View File
@@ -69,18 +69,30 @@ public:
Bus(const Bus &) = delete; Bus(const Bus &) = delete;
Bus &operator=(const Bus &) = delete; Bus &operator=(const Bus &) = delete;
void init(); // `address` is our own peripheral address; the phy keeps it and acknowledges
// frames addressed to it without further instruction. One address per bus.
void init(uint16_t address);
// True when there is a frame to read and we aren't deafened. Depending on the
// target that means the bus has gone dominant or a frame is already buffered.
bool is_active() const; bool is_active() const;
// Prevent the device from being active on the bus
void mute(bool mute); void mute(bool mute);
bool is_muted() const { return muted_; }; bool is_muted() const { return muted_; };
// Set the device to be deaf to (ie don't react to) bus activity. It is
// orthogonal to mute — a deaf device can still send frames.
void deafen(bool deaf);
#ifndef NDEBUG #ifndef NDEBUG
void measure(); void measure();
detail::Error::Send sendbyte(uint8_t byte, bool ack = false);
void set_dominant();
void set_recessive();
#endif #endif
expected<std::unique_ptr<Frame>, Error::Read> read(uint16_t address, expected<std::unique_ptr<Frame>, Error::Read> read(Frame::Print print);
Frame::Print print);
Error::Send send(const Frame &out, Frame::Print print); Error::Send send(const Frame &out, Frame::Print print);
private: private:
@@ -89,6 +101,7 @@ private:
// Assume mute after default ctor; only viable after init call // Assume mute after default ctor; only viable after init call
bool muted_ = true; bool muted_ = true;
bool deafened_ = false;
bool inited_ = false; bool inited_ = false;
}; };
+6 -6
View File
@@ -163,14 +163,14 @@ void Frame::print(Frame::Print print) const {
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) { Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
struct errtype { struct errtype {
Error::Parse errno; Error::Parse type;
uint8_t val; uint8_t val;
} err = {}; } err = {};
const uint8_t *last = bytes + len; const uint8_t *last = bytes + len;
if (len < Frame::MIN_SIZE) { if (len < Frame::MIN_SIZE) {
err.errno = TOO_SHORT; err.type = TOO_SHORT;
goto handle_err; goto handle_err;
} }
@@ -183,7 +183,7 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
length = *bytes++; length = *bytes++;
if (length > MAXLENGTH) { if (length > MAXLENGTH) {
err.errno = LENGTH_TOO_BIG; err.type = LENGTH_TOO_BIG;
err.val = length; err.val = length;
goto handle_err; goto handle_err;
} }
@@ -191,14 +191,14 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
if ((bytes + length) <= last) { if ((bytes + length) <= last) {
memcpy(data, bytes, length); memcpy(data, bytes, length);
} else { } else {
err.errno = MISMATCH_LENGTH; err.type = MISMATCH_LENGTH;
goto handle_err; goto handle_err;
} }
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("ERR(parse): ", stdout); fputs("ERR(parse): ", stdout);
switch (err.errno) { switch (err.type) {
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break; case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
case MISMATCH_LENGTH: case MISMATCH_LENGTH:
puts("frame->length is longer than remaining data"); puts("frame->length is longer than remaining data");
@@ -211,6 +211,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
} }
} }
return err.errno; return err.type;
} }
} // namespace avclan } // namespace avclan
+70 -39
View File
@@ -12,16 +12,16 @@
#ifdef __cplusplus #ifdef __cplusplus
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send; using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
extern "C" { extern "C" {
#else #else
typedef enum Read Read; typedef enum Read Read;
typedef enum Send Send; typedef enum Send Send;
typedef enum Bit Bit;
#endif #endif
// One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted. // One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted.
void phy_init(void); //
// `address` is our device address.
void phy_init(uint16_t address);
// Mute/unmute device TX. "Muted" means transmission is disabled (RX is // Mute/unmute device TX. "Muted" means transmission is disabled (RX is
// unchanged/still allowed) // unchanged/still allowed)
@@ -30,57 +30,88 @@ void phy_mute(bool mute);
// Non-mutating (e.g. theoretically const qualified/-able) // Non-mutating (e.g. theoretically const qualified/-able)
bool phy_is_muted(void); bool phy_is_muted(void);
// True when bus is driven/"dominant" (logical 0) // Withhold acknowledgement. Unlike mute this leaves TX alone: a deaf device
bool phy_active(void); // can still manipulate the bus/send frames, it just never responds (e.g. ACK,
// etc). An empty implementation is sufficient for synchronous ports.
void phy_deafen(bool deaf);
// True when there is a frame to read. This may reflect current bus state (e.g.
// a frame can be synchronously read from the bus) or indicate that a buffered
// frame is available to "read".
bool phy_frame_pending(void);
// Bus-transaction guard: quiesce the other async sources (e.g. interrupts) // Bus-transaction guard: quiesce the other async sources (e.g. interrupts)
// so that bus read/send timing isn't disturbed. Re-enable relevant async // so that bus read/send timing isn't disturbed. Re-enable relevant async
// sources with `phy_guard_leave`. May be a no-op on a target where contention // sources with `phy_guard_leave`.
// isn't a concern. // - May be a no-op on a target where contention isn't a concern.
// - May acquire a hardware lock to prevent concurrent use
void phy_guard_enter(void); void phy_guard_enter(void);
void phy_guard_leave(void); void phy_guard_leave(void);
// Validates an incoming start bit; see avclan::detail::Error::Read. /* Per-field frame I/O.
// Invariants: *
// - Must only be called after positive phy_active() call. * All send functions may have asynchronous implementations (e.g. return before
Read phy_read_startbit(void); * the send has completed on the bus). Success is indicated by a zero value
* `Read` or `Send` enum. Non-zero error codes indicate a synchronously
// Acquire the bus and emit a start bit; may return BUSY * completed send failure. Otherwise, `phy_send_done` must be called to block
Send phy_send_startbit(void); * until all queued send's have completed, and may return the error code for a
* previous (queued) send failure; a success return value indicates that all
/* Returns 0 (`(Send)0`) if the peripheral sent an ACK bit, otherwise returns * queued send's have finished sending over the bus.
NAK. An ACK bit is a cooperative bit, where the sender starts (drives the bus) *
for the sync period, and allows the receiver to drive the bus (or not) to * The read functions are similarly optionally asynchronous, and may return the
finish a "1" bit. * results of buffered reads. When this is the case, phy_read_header returns the
* relevant error for the entire frame.
*
* It is invalid to call any later read or send function after getting an
* error (e.g. calling `phy_read_data` after `phy_read_length` errored).
*
* `expect_ack` indicates whether the recipient should be ACK'ing; false for
* broadcast frames which don't have a single recipient.
*
*/ */
Send phy_read_ack(void); Read phy_read_header(bool *is_unicast);
void phy_send_ack(void); Read phy_read_controller_addr(uint16_t *addr);
Read phy_read_peripheral_addr(uint16_t *addr);
Read phy_read_control(uint8_t *control);
Read phy_read_length(uint8_t *length);
// Read `length` data bytes. `data_index` is only written to on error, with the
// index of the failed byte.
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index);
// Per-symbol I/O. The send* helpers return the even parity of the bits sent; // Send start and broadcast bits. Returns success or one of these error values:
// the read* helpers return the even parity of the bits read. The _u8/_u16 // MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
// suffixes name the source-operand width. The function implementations need not // a broadcast).
// all be separate/independent (e.g. all send functions could be redirect to a Send phy_send_header(bool is_unicast);
// single phy_send_bits_u16, etc).
// N.B: `len` is the number of bits to send. The
// C++ send/readbits templates are the only consumers and use constraints to
// enforce valid len values, so runtime checks are unnecessary.
// Intended for sending parity bits // Send the controller address. Returns success or LOST_ARBITRATION (a device
void phy_send_bit(Bit bit); // with a lower device is sending a frame).
Send phy_send_controller_addr(uint16_t addr);
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack);
Send phy_send_control(uint8_t control, bool expect_ack);
Send phy_send_length(uint8_t length, bool expect_ack);
// Send `length` data bytes. `data_index` is only written to for NAK_DATA.
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index);
// Variants available to minimize unnecessary work for max runtime efficiency // Allows asynchronous ports to block until the phy has finished sending the
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len); // frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS,
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len); // etc) or CONTENDED_BUS; a port that queues the header and controller address
Bit phy_send_byte(const uint8_t *byte); // also reports their BUSY or LOST_ARBITRATION here. `data_index` is only
// written to for NAK_DATA. A fully synchronous port should always report
// success.
Send phy_send_done(uint8_t *data_index);
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len); #ifndef NDEBUG
Bit phy_read_bits_u16(uint16_t *bits, int8_t len);
Bit phy_read_byte(uint8_t *byte);
#if !defined(NDEBUG) && defined(MEASURE_BUS) // Hold the bus at a level until the matching call.
void phy_set_dominant(void);
void phy_set_recessive(void);
#ifdef MEASURE_BUS
// Sample and dump bus bit timing over the serial link (REPL `M`). // Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void); void phy_measure(void);
#endif #endif
#endif
#ifdef __cplusplus #ifdef __cplusplus
} }
+3 -2
View File
@@ -4,6 +4,7 @@
#pragma once #pragma once
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -11,8 +12,8 @@ extern "C" {
// Generic stdio interface initialization. All user I/O goes through <stdio.h> // Generic stdio interface initialization. All user I/O goes through <stdio.h>
// functions. Assumptions/invariants: // functions. Assumptions/invariants:
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when // - stdin MUST be non-blocking (ie. a libc read yields EOF immediately when no
// empty). Necessary to avoid stalling the REPL poll loop. // input is buffered). Necessary to avoid stalling the REPL poll loop.
// - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not // - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
// change a bare LF or a binary frame payload. Writes through // change a bare LF or a binary frame payload. Writes through
// <stdio.h> and writes through stdio_write_nonblock() must reach the same // <stdio.h> and writes through stdio_write_nonblock() must reach the same
+5 -2
View File
@@ -40,7 +40,7 @@ public:
"update the sentinel value in avclan.h"); "update the sentinel value in avclan.h");
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} { Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
bus.init(); bus.init(address);
(std::get<Devs>(devices_).init(), ...); (std::get<Devs>(devices_).init(), ...);
} }
@@ -59,7 +59,10 @@ public:
expected<std::unique_ptr<Frame>, Error::Read> expected<std::unique_ptr<Frame>, Error::Read>
read(Frame::Print print = Frame::Print{}) { read(Frame::Print print = Frame::Print{}) {
return bus.read(address_, print); if (!bus.is_active())
return unexpected{detail::Error::Read::NO_FRAME};
return bus.read(print);
}; };
expected<std::unique_ptr<Frame>, detail::SendError> expected<std::unique_ptr<Frame>, detail::SendError>
@@ -12,6 +12,11 @@ target_sources(avclan PRIVATE
) )
target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(avclan PUBLIC
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
)
find_program(AVRDUDE avrdude) find_program(AVRDUDE avrdude)
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware") set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
@@ -76,9 +81,6 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json. # tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)") set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_FRAME_POOL_N 14 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
try_compile(LIBC_VERSION_TEST try_compile(LIBC_VERSION_TEST
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp" SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
COMPILE_DEFINITIONS -mmcu=${AVR_MCU} COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
@@ -137,6 +139,7 @@ target_compile_definitions(jnk0le_uart
PRIVATE F_CPU=${FREQSEL} PRIVATE F_CPU=${FREQSEL}
) )
target_compile_options(jnk0le_uart PUBLIC target_compile_options(jnk0le_uart PUBLIC
$<$<CONFIG:Debug>:-Og>
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3) -ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
target_link_libraries(avclan PUBLIC jnk0le_uart) target_link_libraries(avclan PUBLIC jnk0le_uart)
@@ -148,7 +151,6 @@ target_compile_definitions(avclan PUBLIC
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV} __CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
TCB_CLKSEL=${TCB_CLKSEL} TCB_CLKSEL=${TCB_CLKSEL}
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
) )
target_compile_options(avclan PUBLIC target_compile_options(avclan PUBLIC
--param=min-pagesize=0 --param=min-pagesize=0
+193 -15
View File
@@ -9,6 +9,7 @@
#include <stdint.h> #include <stdint.h>
#include <util/atomic.h> #include <util/atomic.h>
#include "avclan.h"
#include "hal/cd_timer.h" // statustimer_enable/disable (guard) #include "hal/cd_timer.h" // statustimer_enable/disable (guard)
#include "hal/phy.h" #include "hal/phy.h"
#include "media_avr.h" // media_sync_during_mask (guard) #include "media_avr.h" // media_sync_during_mask (guard)
@@ -17,6 +18,9 @@
// avr-libc. // avr-libc.
#include "timing_avr.h" // IWYU pragma: keep #include "timing_avr.h" // IWYU pragma: keep
enum bit_t : uint8_t { bit_zero = 0x00, bit_one = 0x01, bit_start = 0x10 };
typedef enum bit_t Bit;
// USART0 TX ring indices owned by the jnk0le lib; the guard consults them to // USART0 TX ring indices owned by the jnk0le lib; the guard consults them to
// decide whether to resume the TX drain (DRE interrupt) on leave. // decide whether to resume the TX drain (DRE interrupt) on leave.
extern volatile uint8_t tx0_Head, tx0_Tail; extern volatile uint8_t tx0_Head, tx0_Tail;
@@ -79,8 +83,12 @@ bool phy_is_muted() {
return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0); return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0);
} }
// No-op: acks are driven from inside the synchronous read path, which a deaf
// caller never enters.
void phy_deafen([[maybe_unused]] bool deaf) {}
// True when the bus is being driven (i.e. not idle/floating). // True when the bus is being driven (i.e. not idle/floating).
bool phy_active() { return (!BUS_IS_IDLE) != 0; } bool phy_frame_pending() { return (!BUS_IS_IDLE) != 0; }
// Mute device TX on AVCLAN bus // Mute device TX on AVCLAN bus
void phy_mute(bool mute) { void phy_mute(bool mute) {
@@ -114,7 +122,7 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) {
while (TCB1.CNT <= period) {}; while (TCB1.CNT <= period) {};
} }
void phy_send_bit(Bit bit) { static void phy_send_bit(Bit bit) {
uint16_t zero_length, one_length; uint16_t zero_length, one_length;
switch (bit) { switch (bit) {
case bit_zero: case bit_zero:
@@ -135,7 +143,7 @@ void phy_send_bit(Bit bit) {
set_AVC_logic_for(1, one_length); set_AVC_logic_for(1, one_length);
} }
void phy_send_ack() { static void phy_send_ack() {
TCB1.CNT = 0; TCB1.CNT = 0;
// Wait for controller to begin ACK bit // Wait for controller to begin ACK bit
@@ -149,7 +157,7 @@ void phy_send_ack() {
phy_send_bit(bit_zero); phy_send_bit(bit_zero);
} }
Send phy_read_ack() { static Send phy_read_ack() {
TCB1.CNT = 0; // Double reset of TCB1.CNT: here TCB1.CNT = 0; // Double reset of TCB1.CNT: here
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within) set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
AVCLAN_setBusIdle(); // Stop driving bus AVCLAN_setBusIdle(); // Stop driving bus
@@ -164,13 +172,13 @@ Send phy_read_ack() {
// Check/wait in case we get here before peripheral finishes ACK bit // Check/wait in case we get here before peripheral finishes ACK bit
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
return NAK; return NAK_TOO_LONG;
} }
return (Send)0; return (Send)0;
} }
// Send `len` bits on the AVCLAN bus; returns the even parity // Send `len` bits on the AVCLAN bus; returns the even parity
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) { static Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
uint8_t b = *bits; uint8_t b = *bits;
uint8_t parity = 0; uint8_t parity = 0;
int8_t len_mod8 = 8; int8_t len_mod8 = 8;
@@ -189,17 +197,19 @@ Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
b <<= 1; b <<= 1;
} }
len_mod8 = 8; len_mod8 = 8;
// Avoid under-read for last byte
if (len > 0)
b = *--bits; b = *--bits;
} }
return (parity & 1); return (parity & 1);
} }
// Send `len` bits on the AVCLAN bus; returns the even parity // Send `len` bits on the AVCLAN bus; returns the even parity
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) { static Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
return phy_send_bits_u8((const uint8_t *)bits + 1, len); return phy_send_bits_u8((const uint8_t *)bits + 1, len);
} }
Bit phy_send_byte(const uint8_t *byte) { static Bit phy_send_byte(const uint8_t *byte) {
uint8_t b = *byte; uint8_t b = *byte;
uint8_t parity = 0; uint8_t parity = 0;
@@ -238,7 +248,7 @@ ISR(TCB0_INT_vect) {
} }
// Read `len` bits on the AVCLAN bus; returns the even parity // Read `len` bits on the AVCLAN bus; returns the even parity
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) { static Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
uint8_t parity; uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0; READING_BYTE = 0;
@@ -265,7 +275,7 @@ Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
} }
// Read `len` bits on the AVCLAN bus; returns the even parity // Read `len` bits on the AVCLAN bus; returns the even parity
Bit phy_read_bits_u16(uint16_t *bits, int8_t len) { static Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
uint8_t parity = 0; uint8_t parity = 0;
if (len > 8) { if (len > 8) {
uint8_t over = len - 8; uint8_t over = len - 8;
@@ -278,7 +288,7 @@ Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
} }
// Read a byte on the AVCLAN bus // Read a byte on the AVCLAN bus
Bit phy_read_byte(uint8_t *byte) { static Bit phy_read_byte(uint8_t *byte) {
uint8_t parity; uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0; READING_BYTE = 0;
@@ -304,7 +314,16 @@ Bit phy_read_byte(uint8_t *byte) {
return (Bit)(parity & 1); return (Bit)(parity & 1);
} }
void phy_init() { // Our own peripheral address, and whether the frame being read is addressed to
// it. hal/phy.h puts the ACK decision in the phy, so the latch that used to
// live in Bus::read lives here now: set when the peripheral address comes in,
// and reused by every field after it.
static uint16_t own_address_;
static bool acking_;
void phy_init(uint16_t address) {
own_address_ = address;
// Set pin 6 and 7 as input // Set pin 6 and 7 as input
PORTA.DIRCLR = (PIN6_bm | PIN7_bm); PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
// Disable input buffer; recommended when using AC // Disable input buffer; recommended when using AC
@@ -339,7 +358,7 @@ void phy_init() {
phy_mute(false); // unmute AVCLAN bus TX phy_mute(false); // unmute AVCLAN bus TX
} }
Read phy_read_startbit() { static Read phy_read_startbit() {
// Following HAL header docs, this function is only called after a driven bus // Following HAL header docs, this function is only called after a driven bus
// was ~recently detected. // was ~recently detected.
// Two main (designed) entry flows, depending on timing of pulse end w.r.t. // Two main (designed) entry flows, depending on timing of pulse end w.r.t.
@@ -430,7 +449,7 @@ Read phy_read_startbit() {
// Acquire the bus and emit a start bit. Returns false if another device is // Acquire the bus and emit a start bit. Returns false if another device is
// already driving the bus (we can't yet do proper CSMA/CD). // already driving the bus (we can't yet do proper CSMA/CD).
Send phy_send_startbit() { static Send phy_send_startbit() {
// wait for free line // wait for free line
TCB1.CNT = 0; TCB1.CNT = 0;
while (BUS_IS_IDLE) { while (BUS_IS_IDLE) {
@@ -459,6 +478,144 @@ Send phy_send_startbit() {
return (Send)0; return (Send)0;
} }
// Field bits, then the wire's parity bit, checked against what we counted.
static Read read_parity(Bit calc, Read bad) {
uint8_t wire = 0;
phy_read_bits_u8(&wire, 1);
return ((uint8_t)calc != wire) ? bad : (Read)0;
}
// An acknowledge slot follows the field whether or not we drive it, so a frame
// that isn't ours still has to have the slot read away.
static void ack_slot(void) {
if (acking_) {
phy_send_ack();
} else {
uint8_t slot = 0;
phy_read_bits_u8(&slot, 1);
}
}
Read phy_read_header(bool *is_unicast) {
acking_ = false; // New frame; nothing is addressed to us yet
const Read err = phy_read_startbit();
if (err != (Read)0)
return err;
uint8_t bit = 0;
phy_read_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
*is_unicast = (bit != 0);
return (Read)0;
}
Read phy_read_controller_addr(uint16_t *addr) {
// No acknowledge slot: this field is the arbitration window.
return read_parity(phy_read_bits_u16(addr, 12), BAD_CONTROLLER_PARITY);
}
Read phy_read_peripheral_addr(uint16_t *addr) {
const Read err =
read_parity(phy_read_bits_u16(addr, 12), BAD_PERIPHERAL_PARITY);
if (err != (Read)0)
return err;
// The one field that decides the ack; every field after it reuses the answer.
acking_ = !phy_is_muted() && (*addr == own_address_);
ack_slot();
return (Read)0;
}
Read phy_read_control(uint8_t *control) {
const Read err =
read_parity(phy_read_bits_u8(control, 4), BAD_CONTROL_PARITY);
if (err != (Read)0)
return err;
ack_slot();
return (Read)0;
}
Read phy_read_length(uint8_t *length) {
const Read err = read_parity(phy_read_byte(length), BAD_LENGTH_PARITY);
if (err != (Read)0)
return err;
ack_slot();
return (Read)0;
}
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
for (uint8_t i = 0; i < length; i++) {
const Read err = read_parity(phy_read_byte(&data[i]), BAD_DATA_PARITY);
if (err != (Read)0) {
*data_index = i;
return err;
}
ack_slot();
}
return (Read)0;
}
// `expect_ack` is "a NAK here is worth reporting", not "emit the slot": the
// slot goes out either way, and a broadcast frame simply has nobody to fill it.
static Send send_ack_slot(bool expect_ack, Send nak) {
if (expect_ack) {
const Send err = phy_read_ack();
return err == NAK ? nak : err;
}
const uint8_t fill = 1U;
phy_send_bits_u8(&fill, 1);
return (Send)0;
}
Send phy_send_header(bool is_unicast) {
const Send err = phy_send_startbit();
if (err != (Send)0)
return err;
const uint8_t bit = (is_unicast) ? 1U : 0U;
phy_send_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
return (Send)0;
}
Send phy_send_controller_addr(uint16_t addr) {
// No acknowledge slot: this field is the arbitration window.
phy_send_bit(phy_send_bits_u16(&addr, 12));
return (Send)0;
}
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
phy_send_bit(phy_send_bits_u16(&addr, 12));
return send_ack_slot(expect_ack, NAK_ADDRESS);
}
Send phy_send_control(uint8_t control, bool expect_ack) {
phy_send_bit(phy_send_bits_u8(&control, 4));
return send_ack_slot(expect_ack, NAK_CONTROL);
}
Send phy_send_length(uint8_t length, bool expect_ack) {
phy_send_bit(phy_send_byte(&length));
return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH);
}
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index) {
for (uint8_t i = 0; i < length; i++) {
phy_send_bit(phy_send_byte(&data[i]));
const Send err = send_ack_slot(expect_ack, NAK_DATA);
if (err != (Send)0) {
*data_index = i;
return err;
}
}
return (Send)0;
}
// Every field above is on the wire, and has reported its own outcome, by the
// time it returns: nothing is left to wait out or attribute.
Send phy_send_done([[maybe_unused]] uint8_t *data_index) { return (Send)0; }
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer) /* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
enabled. */ enabled. */
@@ -478,7 +635,26 @@ void phy_guard_leave() {
} }
} }
#if !defined(NDEBUG) && defined(MEASURE_BUS) #ifndef NDEBUG
// Hold the bus at a given level.
// Overrides and restores mute state upon release (i.e. set recessive).
static void phy_set_state(bool dominant) {
static bool saved_mute = false;
if (dominant) {
saved_mute = phy_is_muted();
AVCLAN_setBusDriven();
phy_mute(false);
} else {
AVCLAN_setBusIdle();
phy_mute(saved_mute);
}
}
void phy_set_dominant(void) { phy_set_state(true); }
void phy_set_recessive(void) { phy_set_state(false); }
#ifdef MEASURE_BUS
#include <stdio.h> // phy_measure() reporting (debug builds only) #include <stdio.h> // phy_measure() reporting (debug builds only)
// Only used immediately below // Only used immediately below
@@ -516,4 +692,6 @@ void phy_measure() {
phy_guard_leave(); phy_guard_leave();
} }
#endif
#endif #endif
+48
View File
@@ -0,0 +1,48 @@
pico_sdk_init()
target_sources(avclan PRIVATE
phy.cc
media.cc
cd_timer.cc
board.cc
stdio.cc
)
pico_generate_pio_header(avclan ${CMAKE_CURRENT_LIST_DIR}/iebus.pio)
target_link_libraries(avclan PUBLIC
pico_stdlib pico_stdio_usb hardware_pio hardware_dma pico_multicore
)
pico_enable_stdio_usb(avclan 1)
# stdio_write_nonblock() is all-or-nothing and the largest buffer is a ~186 byte
# frame log line; the SDK default of 64 would drop every one. PUBLIC so the
# TinyUSB sources compiled into `mockingboard_pico` agree on the FIFO size.
target_compile_definitions(avclan PUBLIC CFG_TUD_CDC_TX_BUFSIZE=256)
# Needs separate/new executable target because `pico_add_extra_outputs` only
# works on locally (to this file/directory) defined targets
add_executable(mockingboard_pico
${avclan-mockingboard_SOURCE_DIR}/src/sniffer.cc
)
target_link_libraries(mockingboard_pico PUBLIC
avclan
)
# Silence warnings in TinyUSB, and only there.
file(GLOB_RECURSE _tinyusb_sources ${PICO_TINYUSB_PATH}/src/*.c)
set_source_files_properties(${_tinyusb_sources}
TARGET_DIRECTORY avclan mockingboard_pico
PROPERTIES COMPILE_OPTIONS "-w")
unset(_tinyusb_sources)
pico_add_extra_outputs(mockingboard_pico)
pico_set_float_implementation(mockingboard_pico none)
pico_set_double_implementation(mockingboard_pico none)
pico_set_program_name(mockingboard_pico mockingboard)
pico_set_program_description(mockingboard_pico
"Emulate an external CD Changer on an AVCLAN bus to add Bluetooth and aux-input audio to older Toyota's"
)
pico_set_program_version(mockingboard_pico
"${avclan-mockingboard_VERSION}")
pico_set_program_url(mockingboard_pico "https://github.com/halleysfifthinc/AVCLAN-Mockingboard")
+5
View File
@@ -0,0 +1,5 @@
#include "hal/board.h"
extern "C" void board_init() {}
extern "C" void board_enable_interrupts() {}
+12
View File
@@ -0,0 +1,12 @@
#include "hal/cd_timer.h"
extern "C" void cdtimer_init(void *ptr, void(clbk)(void *),
bool(isplay)(void *)) {}
extern "C" void cdtimer_reset() {}
extern "C" void cdtimer_restore() {}
extern "C" void cdtimer_disable() {}
volatile bool cdtimer_pending_flag;
+268
View File
@@ -0,0 +1,268 @@
.define public ack_latch 4 ; (PIO0, internal) frame is addressed to us
.define public ack_irq 5 ; (PIO1) rx -> iebus_ack: hold this slot low
; This is used to set the clkdiv.
; The delay cycle counts in read_bit (AVCLAN_READBIT_THRESHOLD) and iebus_tx's
; ack_drive (AVCLAN_BIT0_LOGIC_0) must be updated in sync with this variable
.define public CYCLES_PER_READBIT_PERIOD 64
.pio_version 1
.program iebus_rx
; TX encoding: one read per word
; | 31 | 30:16 | 15:0 |
; | has ACK | zeros | Instr |
;
; Instr must be `out x, (14 - N)` for a read of N bits + parity, or `jmp rx_startbit`.
; The `out` zeroes x (the parity accumulator) and leaves exactly N + 1 bits before
; the OSR reaches its threshold, which ends the read loop. "has ACK" marks a field
; followed by an acknowledge slot.
;
; RX encoding: one read per word (RX FIFO can hold max 4 reads)
; | 31:16 | 15:0 |
; | zeros | slot |
;
; where each slot is encoded as
; | MSB .................... LSB |
; | 15-N zeros | N bits | parity |
;
; Parity is validated by this SM, but is still reported in the LSB. `y` is reserved
; for the address match below.
.in 1 left
; OSR threshold set to 31 so that the OSRE is reached when all the data+parity bits
; have been read
.out 0 right 31
.mov_status irq set ack_latch
;;; Start-bit read (auto-resets until a proper start bit is observed)
; A start bit is dominant for AVCLAN_STARTBIT_LOGIC_0 (~169us) and recessive for
; AVCLAN_STARTBIT_LOGIC_0 (~20us) more.
; The bus state is polled as fast as target bounds (80% and 120%) can be evenly
; divided (must be faster than shortest recessive period AVCLAN_BIT0_LOGIC_1 to
; detect/distinguish normal bits mid-frame).
public rx_startbit:
wait 1 pin 0
leading_edge:
wait 0 pin 0
set x 29
dominant:
jmp pin leading_edge [10] ; Pin HIGH == released before 0.8x: not a start bit
; Polled every 11 cycles, starting 2 cycles after leading edge
; Last poll at 332 cycles (~80% * AVCLAN_STARTBIT_LOGIC_0)
jmp x-- dominant ; Cycle 333 on fall-through
set x 18 ; 334
recessive:
jmp pin valid_startbit ; Pin HIGH == released within bounds
jmp x-- recessive [8] ; + 19 * 9 = 506 total (~120% * AVCLAN_STARTBIT_LOGIC_0)
; Delay on decrement jmp to keep the release jmp quick
jmp rx_startbit ; Still dominant: too long to be proper start bit
valid_startbit:
irq clear ack_latch ; Once per frame, before any address can set it
.wrap_target
wait_read:
pull
out exec, 16 ; Set up the read (zero x and set up output shift
; count as bit count), or jmp to rx_startbit
read_bit:
;;; Resynchronize at each leading (recessive => dominant) edge
wait 1 pin 0
; Worst-case (slowest) path to return here is for non-ACK flow.
wait 0 pin 0 [31]
out null, 1 [31] ; Count this bit
; Read bus state ~AVCLAN_READBIT_THRESHOLD after bus goes dominant
in pins, 1
jmp pin counted ; Fall through for recessive (i.e. a one bit)
mov x, ~x ; Accumulate parity over the zero bits
counted:
jmp !osre read_bit
;;; Check parity
; Every parity-checked field is an odd number of bits, so even parity (an even
; number of ones) is an odd number of zeros: x == ~0. The broadcast bit has no
; parity; its verdict can only clear a latch that was never set.
jmp x-- parity_ok
irq clear ack_latch ; NAK this field and the rest of the frame
;;; Check if we're the frame recipient
; Our peripheral address can only appear in individual 12-bit reads as the
; controller or peripheral address. We should only ack if we're the frame recipient.
; y is preset in phy_init to our peripheral address + parity, and temporarily
; inverted when we are sending (so we don't self-ACK)
parity_ok:
mov x, isr ; Save read value before clobbering the ISR with push
jmp x!=y push_slot ; Fall-through to set ACK latch only if the read matches
; our address + parity
irq set ack_latch ; Set before the push, so the driver's veto can't be overwritten
push_slot:
push
;;; Consume the ack slot if has ACK, driving it if ACK latch is set
out x, 1 ; Load "has ACK"
jmp !x wait_read
wait 1 pin 0
wait 0 pin 0 [30] ; Sender begins the ACK bit, and will hold it dominant
; for ~48 cycles (~AVCLAN_BIT1_LOGIC_0) before releasing
; for peripheral to take over
; Delay reloading ack_latch to give the driver as much
; time as possible to veto an ACK (i.e. clear it for
; e.g. a bad length)
mov x, status ; Load ack_latch state into x
jmp !x wait_read
irq next set ack_irq ; The ack SM (iebus_tx's ack_entry, on the other (TX)
; PIO) is released 32 cycles after sender began bit,
; and its jmp to ack_drive takes the slot over at 34
; (~70% of AVCLAN_BIT1_LOGIC_0), holding it dominant.
.wrap
; Can execute one streamed instruction every 3 cycles
; An even number of instructions must be executed to maintain the correct shift-count/autopull
; when returning to normal reads after exec'ing. Pad instructions with `nop` as needed.
; public do_exec:
; out exec, 16 ; Cycle 1: Load the streamed instruction
; ; Cycle 2: Execute the streamed instruction
; jmp x-- do_exec ; Cycle 3: Restart loop (if x != 0)
; jmp wait_read
.program iebus_tx
.fifo tx
; TX Encoding (8 deep TX FIFO):
; | MSB ............................................. LSB |
; | 4 bits | N bits | 1 bit | 1 bit | 26 - N bits |
; | Length (N) | Data+P | has ACK | NAK ok | padding |
;
; Parity is generated by the driver and appended as the last (least significant)
; bit of the data field
.in 1 left
.out 1 left
.define public nak_irq 7
.define public lost_arb_irq 6
;;; Emit or synchronize to a start bit
; Reached by a CPU initiated force exec jmp to tx_startbit, with x = JOIN_SAMPLES
; and y = IDLE_SAMPLES. We originate once the bus has stayed recessive for the
; idle window. A dominant pulse is joined only if it outlasts every bit's
; dominant period (i.e. it's a start bit); otherwise the bus is busy, reported as
; a lost bid before any word is pulled.
.define public IDLE_SAMPLES 11 ; 12 * 16 = 192 cycles (~2 * AVCLAN_BIT_LENGTH_MAX)
; TODO: Explore tightening the idle window to 3 * AVCLAN_STARTBIT_LOGIC_1 (the
; longest recessive period within a frame)
.define public JOIN_SAMPLES 8 ; 9 * 12 = 108 cycles (> 1.2 * AVCLAN_BIT0_LOGIC_0)
idle_ok:
jmp y-- tx_startbit [14] ; Sampled every 16 cycles
set x 31 ; Idle: originate
set pins, 0 [30]
public tx_startbit:
jmp pin idle_ok
hold:
jmp pin lost ; Released too soon for a start bit (never while we drive)
jmp x-- hold [10] ; Sampled every 12 cycles, within a zero bit's recessive
; period (~AVCLAN_BIT0_LOGIC_1). Ours is 31 + 1 + 32 * 12
; = 416 total (~AVCLAN_STARTBIT_LOGIC_0)
set pins, 1 ; Release
wait 1 pin 0 [31] ; The start bit's trailing edge, whoever drove it
jmp reset [16] ; 51 cycles from that edge to reset's first
; `set pins, 0` (~AVCLAN_STARTBIT_LOGIC_1)
handle_nak:
jmp x-- ack_done ; Slot is over either way if a NAK was expected
irq wait nak_irq
;;; Close the acknowledge slot
; The peripheral owns this slot's trailing edge, so the recessive period that
; ends the slot is timed from its release rather than from our leading edge --
; the one bit in a frame we don't drive to completion ourselves. A slot nobody
; answered is already recessive and falls straight through.
ack_done:
wait 1 pin 0 [12] ; 15 cycles from the release to `set pins, 0` below
; (~AVCLAN_BIT0_LOGIC_1)
reset:
.wrap_target
pull
out x, 4
send_bit:
set pins, 0 [31] ; OUT pin remains low/dominant for 32+16 cycles (~AVCLAN_BIT1_LOGIC_0)
out y, 1 [15] ; Save output bit to y
mov pins, y [15] ; Output the bit
; Read bus state ~AVCLAN_READBIT_THRESHOLD after bus goes dominant
jmp pin bit_end [13] ; JMP_PIN must be the IEBUS_RX pin (TX activity will mirror
; back to the RX pin). The delay burns the common duration
; between the one and zero bits.
bit_zero:
; This branch catches a difference between what we intended to send and what is actually
; present on the bus (a difference is only expected/allowed during send arbitration,
; where lower addresses have higher priority: since one bits are recessive, another
; device sending a zero will "override" the value of that bit)
jmp !y bit_end [2] ; Delay 3 more cycles before releasing bus at ~AVCLAN_BIT0_LOGIC_0
; Fall through means we lost arbitration (we read a zero, when we expected a one)
lost:
irq wait lost_arb_irq
.wrap
bit_end:
set pins, 1 [9] ; Delay must be common to all following control flow paths
jmp x-- slow_jmp ; Fall through once x (the bit count) is exhausted
out x, 1
jmp !x reset ; Fall through to read ACK if "has ACK" is set
;;; read ack
public ack_drive:
set pins, 0 [31] ; Start ACK bit with a AVCLAN_BIT1_LOGIC_0 length pulse
out x, 1 [15] ; Preload NAK ok
public ack_release:
set pins, 1 [15] ; Release bus
jmp pin handle_nak [13] ; Sampled at 64 cycles (~AVCLAN_READBIT_THRESHOLD);
; both flow paths reach ack_done one instr later
jmp ack_done
slow_jmp:
;;; The recessive ("prep") period in the send_bit loop is 2 instr's shorter than
; the non-"has ACK" branch and
jmp send_bit [3]
;;; Send an ack for iebus_rx
; Run by its own SM, wrapping from ack_release back to ack_entry: it borrows
; ack_drive's 48-cycle pulse to hold the slot dominant for *less than* a complete
; AVCLAN_BIT0_LOGIC_0, releasing at ~AVCLAN_BIT0_LOGIC_0 after the slot's
; leading edge.
public ack_entry:
wait 1 irq ack_irq ; Released by iebus_rx after bus goes dominant
jmp ack_drive
; `mov pins, pins` reads the IN mapping (bit 0 == in base) and writes the OUT
; mapping (out base, 1 pin), so one program instance serves any src->dst pair;
; give each pair its own SM with its own pin mapping.
.program pin_mirror
mov pins, ~pins
; === IDIOMS ===
;;; Isolate first bit (for left shifting)
; mov osr, ::isr ; Reverse ISR so its LSB becomes the MSB in osr
; out x, 1 ; Set LSB of x from MSB of osr
;;; Incrementing (counting up)
; set x 0
; set x ~x ; x == 0xffffffff
; jmp x-- label ; "Increment"
; set x ~x ; Re-inversing gives the actual count
;;; Multiplying delays/counts (e.g. 2*20)
; set y 1
; loop1:
; set x 19
; loop2:
; jmp x-- loop2
+10
View File
@@ -0,0 +1,10 @@
#include "hal/media.h"
extern "C" void media_init(void) {}
extern "C" void media_action(MediaAction fn) {}
#ifndef NDEBUG
extern "C" bool media_mic_toggle(void) { return false; }
extern "C" bool media_busy(void) { return true; }
#endif
+885
View File
@@ -0,0 +1,885 @@
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <hardware/clocks.h>
#include <limits>
#include "avclan.h"
#include "hal/phy.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "iebus.pio.h"
#define TICK_US 1000000
#include "timing.h"
using namespace avclan;
using enum detail::Error::Read;
using enum detail::Error::Send;
namespace {
constexpr int IEBUS_TX = 16;
constexpr int IEBUS_RX = 17;
// Activity indicators: each follows its bus pin exactly, so an LED wired
// supply -> resistor -> pin is lit while that line is dominant (low). The bus
// pins themselves stay unloaded.
constexpr int LED_RX = 8;
constexpr int LED_TX = 26;
constexpr uint32_t parity(uint32_t val) {
static_assert(sizeof(uint32_t) == sizeof(unsigned int));
if consteval {
int N = 0;
for (; val != 0UL; N++) {
val = val & (val - 1);
}
return (N & 1);
}
return __builtin_parity(val);
}
// CYCLES_PER_READBIT_PERIOD PIO cycles should take ~AVCLAN_READBIT_THRESHOLD
// μs. All three bus SMs share it: iebus_rx and iebus_ack are cross-PIO IRQ
// partners, which the datasheet (S11.4) requires to have equal dividers,
// synchronised by a single CTRL write -- see the enable in Phy::init.
inline float bus_clkdiv() {
return clock_get_hz(clk_sys) /
(CYCLES_PER_READBIT_PERIOD / (float)AVCLAN_READBIT_THRESHOLD);
}
// The reader runs from the PIO's RX-FIFO interrupt rather than from the calling
// thread, so slots are drained and the next script queued no matter what the
// main loop is doing. That is not polish: the RX FIFO is 4 deep and printf over
// USB CDC blocks for milliseconds, which is exactly the failure rx_log_push
// exists to dodge. It is also what lets a frame we lost arbitration to still
// arrive -- the engine never stopped receiving.
//
// The frames it completes are handed out whole; hal/phy.h's read calls pick
// their field out of one.
class IEBusRx {
// Field widths, in the SM's "data bits" terms. The broadcast bit carries no
// parity, so it is the one field read as a bare bit (count 0 => 1 bit).
static constexpr uint8_t W_BROADCAST = 0;
static constexpr uint8_t W_ADDR = 12;
static constexpr uint8_t W_CONTROL = 4;
static constexpr uint8_t W_BYTE = 8;
enum class RxField : uint8_t {
Broadcast,
Controller,
Peripheral,
Control,
Length,
Data,
};
// Must be a power of two (the index wrap is a mask).
static constexpr uint32_t RXQ_N = 4;
static_assert((RXQ_N & (RXQ_N - 1)) == 0,
"RX buffer size must be a power of 2");
public:
struct RxFrame {
uint16_t controller_addr;
uint16_t peripheral_addr;
uint8_t control;
uint8_t length;
uint8_t data[32];
bool is_unicast;
Read err;
};
IEBusRx() = default;
IEBusRx(const IEBusRx &) = delete;
IEBusRx &operator=(const IEBusRx &) = delete;
~IEBusRx() {
if (instance_ != this)
return;
irq_set_enabled(irq_, false);
irq_remove_handler(irq_, irq_handler);
pio_set_irq0_source_enabled(
pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), false);
pio_sm_set_enabled(pio_, sm_, false);
pio_remove_program_and_unclaim_sm(&iebus_rx_program, pio_, sm_, offset_);
instance_ = nullptr;
}
// Claims and configures the iebus_rx SM and its interrupt, but doesn't start
// the SM.
void init(PIO pio, uint pin_rx, uint16_t address) {
hard_assert(instance_ == nullptr);
instance_ = this;
pio_ = pio;
pin_ = pin_rx;
self_addrp_ = ((uint32_t)address << 1) | parity(address);
offset_ = (uint)pio_add_program(pio_, &iebus_rx_program);
sm_ = (uint)pio_claim_unused_sm(pio_, true);
gpio_init(pin_);
gpio_set_dir(pin_, false); // CAN/AVCLAN RX; PIO reads it without a claim
gpio_set_input_enabled(pin_, true);
pio_sm_set_consecutive_pindirs(pio_, sm_, pin_, 1, false);
pio_sm_config cfg = iebus_rx_program_get_default_config(offset_);
sm_config_set_in_pins(&cfg, pin_);
sm_config_set_jmp_pin(&cfg, pin_);
sm_config_set_clkdiv(&cfg, bus_clkdiv());
// Entry is `wait_read`, not the program start -- offset 0 is rx_startbit,
// which would fall through into the header reads before the driver's own
// jmp to it, leaving every later field one read out of step.
pio_sm_init(pio_, sm_, offset_ + iebus_rx_wrap_target, &cfg);
prepare_ack();
begin_frame();
pio_set_irq0_source_enabled(
pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), true);
irq_ = (uint)pio_get_irq_num(pio_, 0);
irq_set_exclusive_handler(irq_, irq_handler);
irq_set_priority(irq_, PICO_HIGHEST_IRQ_PRIORITY);
irq_set_enabled(irq_, true);
}
PIO pio() const { return pio_; }
uint32_t sm_mask() const { return 1U << sm_; }
// Muted means we still listen, we just don't answer: no ack, ever. Disarming
// the latch is what makes that true of the SM as well.
void mute(bool mute) {
muted_ = mute;
sync_ack_arming();
}
void deafen(bool deaf) {
deafened_ = deaf;
sync_ack_arming();
}
// Disarm RX ACK'ing behavior; called prior to frame TX to prevent
// self-ACK'ing.
void disarm_ack() {
transmitting_ = true;
sync_ack_arming();
}
// Rearm RX ACK'ing behavior; called once our frame is over or lost.
void rearm_ack() {
transmitting_ = false;
sync_ack_arming();
}
// Buffered frames exist to be read
bool frame_pending() const { return rxq_tail_ != rxq_head_; }
// Only valid while frame_pending().
const RxFrame &frame() const { return rxq_[rxq_tail_]; }
void release() { rxq_tail_ = (rxq_tail_ + 1) & (RXQ_N - 1); }
private:
// True when publish() would have nowhere to put the frame being received.
bool queue_full() const {
return ((rxq_head_ + 1) & (RXQ_N - 1)) == rxq_tail_;
}
static void __time_critical_func(irq_handler)() { instance_->isr(); }
void __time_critical_func(isr)() {
// Every field after the controller address is followed by an ack slot,
// which the SM consumes whether or not it drives it.
const auto next_after_ack = [this](RxField next, uint8_t bits) {
state_ = next;
enqueue_rx(bits, true);
};
while (!pio_sm_is_rx_fifo_empty(pio_, sm_)) {
const auto slot = (uint16_t)pio_sm_get(pio_, sm_);
uint16_t value = 0;
switch (state_) {
case RxField::Broadcast:
building_.is_unicast = (slot & 1U) != 0U;
state_ = RxField::Controller;
break;
case RxField::Controller:
// Judged on the peripheral slot: that read is already running, and a
// restart now would take its slot for the next frame's broadcast bit.
controller_ok_ = update_value<W_ADDR>(slot, &value);
building_.controller_addr = value;
state_ = RxField::Peripheral;
break;
case RxField::Peripheral: {
// Past arbitration our own frame is the tx SM's business: it reads
// its own acks, and a copy here would only cost a queue slot. A frame
// we owe an ack for is refused when there is no room for it -- the
// NAK asks the sender to send it again, rather than losing it behind
// an ack we can't honour. Both come before the field checks: a frame
// being given up needs no parity verdict, and reporting one would
// name the wrong cause for the same NAK.
const bool ours =
building_.controller_addr == (uint16_t)(self_addrp_ >> 1);
const bool refuse =
!ours && queue_full() && pio_interrupt_get(pio_, ack_latch);
if (ours || refuse) {
if (refuse)
rxq_refused_ = rxq_refused_ + 1;
// Withdrawing the ack is part of giving the frame up; its slot is
// still ahead of the SM. Re-dispatching the start-bit block hands
// the rest of the frame back to it: those bits are far too short to
// read as a start bit, so it re-syncs on the next real one by
// itself.
pio_interrupt_clear(pio_, ack_latch);
begin_frame();
break;
}
if (!controller_ok_) {
publish(BAD_CONTROLLER_PARITY);
break;
}
if (!update_value<W_ADDR>(slot, &value)) {
publish(BAD_PERIPHERAL_PARITY);
break;
}
building_.peripheral_addr = value;
next_after_ack(RxField::Control, W_CONTROL);
break;
}
case RxField::Control:
if (!update_value<W_CONTROL>(slot, &value)) {
publish(BAD_CONTROL_PARITY);
break;
}
building_.control = (uint8_t)value;
next_after_ack(RxField::Length, W_BYTE);
break;
case RxField::Length:
if (!update_value<W_BYTE>(slot, &value)) {
publish(BAD_LENGTH_PARITY);
break;
}
building_.length = (uint8_t)value;
if (value == 0 || value > sizeof(building_.data)) {
publish(BAD_LENGTH_RANGE);
break;
}
next_after_ack(RxField::Data, W_BYTE);
break;
case RxField::Data:
if (!update_value<W_BYTE>(slot, &value)) {
publish(BAD_DATA_PARITY);
break;
}
building_.data[data_i_++] = (uint8_t)value;
if (data_i_ >= building_.length)
publish(Read{0});
else
next_after_ack(RxField::Data, W_BYTE);
break;
}
}
}
// Start (or restart) a frame. The start-bit block is dispatched like any
// other field, except the streamed instruction is a jmp instead of a read's
// setup. It only falls through on a start bit, so the header's reads queue
// right behind it. Four words fill the FIFO, which is empty here: the SM
// takes each word before pushing the slot that ends a frame.
void __time_critical_func(begin_frame)() {
state_ = RxField::Broadcast;
building_ = {};
data_i_ = 0;
pio_sm_put(pio_, sm_,
pio_encode_jmp(offset_ + iebus_rx_offset_rx_startbit));
enqueue_rx(W_BROADCAST, false);
enqueue_rx(W_ADDR, false);
enqueue_rx(W_ADDR, true);
}
// Load our address (with parity) into the SM's Y register.
// The SM compares read values to Y to set the ack latch and trigger ACK'ing.
// SM must be stopped with an empty FIFO.
void prepare_ack() {
pio_sm_put(pio_, sm_, self_addrp_);
pio_sm_exec(pio_, sm_, pio_encode_pull(false, true));
pio_sm_exec(pio_, sm_, pio_encode_out(pio_y, 32));
ack_armed_ = true;
}
// Bring the latch's arming in line with the reasons to withhold an ack.
// Disarming inverts the Y register (self address + parity), which leaves the
// upper 19 bits set and so prevents any read from matching.
void sync_ack_arming() {
const bool arm = !muted_ && !deafened_ && !transmitting_;
if (arm == ack_armed_)
return;
pio_sm_exec_wait_blocking(pio_, sm_, pio_encode_mov_not(pio_y, pio_y));
ack_armed_ = arm;
}
// One read's script word: [has_ack 31][zeros 30:16][instr 15:0]. The SM reads
// bits + 1 (the field plus its parity); the instruction zeroes its parity
// accumulator and leaves that many bits before the OSR threshold. `bits` must
// be <= 13: at 14 the instruction would encode as `out x, 0`, which the ISA
// reads as 32. `has_ack` has the SM consume the ack slot after the field.
static constexpr uint32_t encode_rx(uint8_t bits, bool has_ack) {
return ((uint32_t)has_ack << 31) | pio_encode_out(pio_x, 14 - bits);
}
// Update `*value` with the `read` value. Returns true for a correct parity.
template <auto N> static bool update_value(uint16_t read, uint16_t *value) {
*value = (uint16_t)((read >> 1) & ((1U << N) - 1U));
return parity(*value) == (unsigned)(read & 1U);
}
void __time_critical_func(enqueue_rx)(uint8_t bits, bool has_ack) {
pio_sm_put_blocking(pio_, sm_, encode_rx(bits, has_ack));
}
// New frames are dropped if the rx queue is full. A frame we acked is not
// among them: there was room for it at the peripheral slot, and from here the
// queue only gains space. What is left to lose is what no ack was owed for.
void __time_critical_func(publish)(Read err) {
// The SM NAKs bad parity itself; this vetoes what only we can judge
// (controller parity, length range) before its late read of the latch.
if (err != Read{0})
pio_interrupt_clear(pio_, ack_latch);
building_.err = err;
if (queue_full()) {
rxq_drops_ = rxq_drops_ + 1; // Never stall the bus for a lagging reader
} else {
rxq_[rxq_head_] = building_;
rxq_head_ = (rxq_head_ + 1) & (RXQ_N - 1);
}
begin_frame();
}
// The initialized instance, for irq_handler: SDK IRQ handlers take no
// context.
static inline IEBusRx *instance_ = nullptr;
PIO pio_;
uint sm_;
uint offset_;
uint pin_;
uint irq_;
// Our address as the rx SM sees it: a slot is the field plus its parity bit,
// and parity is a function of the field, so there is exactly one legal slot
// value for us.
uint32_t self_addrp_;
bool ack_armed_ = false; // hardware: y holds self_addrp_, or its complement
bool muted_ = false; // we don't answer on the bus
bool deafened_ = false; // we don't answer, but may still transmit
bool transmitting_ = false; // our own frame is on the wire
std::array<RxFrame, RXQ_N> rxq_ = {};
volatile uint32_t rxq_head_ = 0; // written by the ISR only
volatile uint32_t rxq_tail_ = 0; // written by the reader only
// Lost: no ack was owed, so there was nothing to refuse -- a broadcast, or a
// unicast addressed elsewhere.
volatile uint32_t rxq_drops_ = 0;
// NAK'd for want of room; the sender still owns the frame and sends it again.
volatile uint32_t rxq_refused_ = 0;
RxFrame building_ = {};
RxField state_ = RxField::Broadcast;
uint8_t data_i_ = 0;
bool controller_ok_ = false; // parity verdict, held until the peripheral slot
};
// The transmit engine: the iebus_tx SM, plus the ack SM that drives the ack
// slot on the rx engine's behalf. Both run iebus_tx, sharing its ack-bit drive
// and the TX pin.
//
// Every field, arbitration included, is queued, not sent, so the fields report
// nothing and the frame's verdict comes from send_done.
class IEBusTx {
// Word positions in a frame
static constexpr uint8_t WORD_PERIPHERAL = 2; // After broadcast, controller
static constexpr uint8_t WORD_CONTROL = 3;
static constexpr uint8_t WORD_LENGTH = 4;
public:
// The ack match lives in the rx SM but exists for our sake: it has to be
// parked while our own address is on the wire, and put back the moment we
// stop transmitting -- including when we lose arbitration mid-field.
explicit IEBusTx(IEBusRx &rx) : rx_(rx) {}
IEBusTx(const IEBusTx &) = delete;
IEBusTx &operator=(const IEBusTx &) = delete;
// Hands both SMs and their program memory back. Stopping them leaves the pad
// at its last driven level, which is recessive -- the same property mute
// relies on. The pad keeps its PIO function select: handing it back to SIO
// would float the line, and a floating bus reads dominant.
~IEBusTx() {
if (!claimed_)
return;
pio_set_sm_mask_enabled(pio_, sm_mask(), false);
pio_remove_program_and_unclaim_sm(&iebus_tx_program, pio_, tx_sm_,
tx_offset_);
pio_sm_unclaim(pio_, ack_sm_);
}
// Claims and configures both SMs, but doesn't start them.
void init(PIO pio, uint pin_rx, uint pin_tx) {
pio_ = pio;
pin_tx_ = pin_tx;
tx_offset_ = (uint)pio_add_program(pio_, &iebus_tx_program);
tx_sm_ = (uint)pio_claim_unused_sm(pio_, true);
ack_sm_ = (uint)pio_claim_unused_sm(pio_, true);
// Drive the pad recessive *before* handing its function select to this PIO,
// so the handover cannot glitch the bus dominant. Level and direction are
// block-wide registers, so setting them through one SM covers both.
pio_sm_set_pins_with_mask(pio_, tx_sm_, 1U << pin_tx, 1U << pin_tx);
pio_sm_set_consecutive_pindirs(pio_, tx_sm_, pin_tx, 1, true);
pio_gpio_init(pio_, pin_tx);
pio_sm_config tx_cfg = iebus_tx_program_get_default_config(tx_offset_);
sm_config_set_out_pins(&tx_cfg, pin_tx, 1);
sm_config_set_set_pins(&tx_cfg, pin_tx, 1);
// Both the bit value and the arbitration check come from the readback.
sm_config_set_jmp_pin(&tx_cfg, pin_rx);
// The start bit's trailing-edge `wait` indexes the IN mapping, not JMP_PIN.
sm_config_set_in_pins(&tx_cfg, pin_rx);
sm_config_set_clkdiv(&tx_cfg, bus_clkdiv());
// Entry is `reset`; offset 0 is the start-bit block.
pio_sm_init(pio_, tx_sm_, tx_offset_ + iebus_tx_wrap_target, &tx_cfg);
pio_sm_config ack_cfg = iebus_tx_program_get_default_config(tx_offset_);
sm_config_set_wrap(&ack_cfg, tx_offset_ + iebus_tx_offset_ack_entry,
tx_offset_ + iebus_tx_offset_ack_release);
sm_config_set_set_pins(&ack_cfg, pin_tx, 1);
sm_config_set_clkdiv(&ack_cfg, bus_clkdiv());
pio_sm_init(pio_, ack_sm_, tx_offset_ + iebus_tx_offset_ack_entry,
&ack_cfg);
claimed_ = true;
}
PIO pio() const { return pio_; }
uint32_t sm_mask() const { return (1U << tx_sm_) | (1U << ack_sm_); }
// Must only be called between transactions to ensure the TX pin is left
// high/recessive.
void mute(bool mute) {
pio_set_sm_mask_enabled(pio_, sm_mask(), !mute);
muted_ = mute;
}
bool is_muted() const { return muted_; }
// Hold the bus at a given level.
// Overrides and restores mute state upon release (i.e. set recessive).
void set_state(bool dominant) {
static bool saved_mute = false;
if (dominant) {
saved_mute = muted_;
mute(true);
pio_sm_set_pins_with_mask(pio_, tx_sm_, 0U, 1U << pin_tx_);
} else {
pio_sm_set_pins_with_mask(pio_, tx_sm_, 1U << pin_tx_, 1U << pin_tx_);
// The ack SM parks on `wait 1 irq`, which consumes a flag raised while it
// was down and drives the slot immediately -- Phy::mute's ordering.
pio_interrupt_clear(pio_, ack_irq);
mute(saved_mute);
}
}
// Send start and broadcast bits.
Send send_header(bool is_unicast) {
if (muted_)
return MUTED;
rx_.disarm_ack();
words_ = 0;
data_start_ = std::numeric_limits<uint8_t>::max();
// Jump to the tx_startbit section from the default "reset" stall on pull,
// with the start-bit block's sample counts loaded
pio_sm_exec_wait_blocking(pio_, tx_sm_,
pio_encode_set(pio_x, iebus_tx_JOIN_SAMPLES));
pio_sm_exec_wait_blocking(pio_, tx_sm_,
pio_encode_set(pio_y, iebus_tx_IDLE_SAMPLES));
pio_sm_exec_wait_blocking(
pio_, tx_sm_, pio_encode_jmp(tx_offset_ + iebus_tx_offset_tx_startbit));
// The broadcast bit carries no parity, so it is sent as a bare bit: length
// 0 (one bit) with the parity slot standing in for the bit itself.
return send_field(0, is_unicast ? 1U : 0U, false, false);
}
// A field is only queued, and its outcome left to send_done. Once one has
// failed, the rest of the frame is dropped.
Send send_field(size_t len, uint32_t bits, bool has_ack, bool expect_ack) {
if (muted_)
return MUTED;
if (!check())
put(encode_tx(len, (uint16_t)bits, has_ack, expect_ack));
return Send{0};
}
Send send_data(const uint8_t *data, uint8_t length, bool expect_ack) {
if (muted_)
return MUTED;
data_start_ = words_;
for (uint8_t i = 0; i < length && !check(); i++)
put(encode_tx(8, data[i], true, expect_ack));
return Send{0};
}
Send send_done(uint8_t *data_index) {
const auto flagged = [this] {
return pio_interrupt_get(pio_, iebus_tx_lost_arb_irq) ||
pio_interrupt_get(pio_, iebus_tx_nak_irq);
};
// The second PC test confirms that SM has finished sending last value and
// not in-progress (i.e. pulled the last FIFO value and still mid-send)
const auto idle = [this] {
return pio_sm_is_tx_fifo_empty(pio_, tx_sm_) && // Read before the pc
pio_sm_get_pc(pio_, tx_sm_) == tx_offset_ + iebus_tx_wrap_target;
};
while (!(flagged() || idle()))
tight_loop_contents();
check();
// Our frame is over, won or not.
rx_.rearm_ack();
words_ = 0;
const Send err = fault_;
fault_ = Send{0};
if (err == NAK_DATA)
*data_index = (uint8_t)(failed_word_ - data_start_);
return err;
}
private:
// One field's word: [len:4][data+parity:len+1][has_ack:1][nak_ok:1][pad]. The
// SM sends count + 1 bits, so the count is `len` and the parity bit the
// driver appends rides along as the extra one. `has_ack` emits the
// acknowledge slot; a NAK in it raises nak_irq unless `nak_ok`, i.e. unless
// we don't `expect_ack`.
static constexpr uint32_t encode_tx(size_t len, uint16_t bits, bool has_ack,
bool expect_ack) {
const auto n = (uint8_t)(len + 1);
// Masked so a bit above the field (the broadcast bit's copy of itself, when
// len is 0) can't spill into the count.
const uint16_t with_parity =
((bits << 1) | parity(bits)) & ((1U << n) - 1U);
uint32_t word = (uint32_t)len << 28;
word |= with_parity << (28 - n);
word |= (uint32_t)has_ack << (27 - n);
word |= (uint32_t)!expect_ack << (26 - n);
return word;
}
// Collect a flag if the SM has raised one, without waiting. Returns whether
// the frame has failed, now or earlier.
bool check() {
const bool lost_arb = pio_interrupt_get(pio_, iebus_tx_lost_arb_irq);
if (!lost_arb && !pio_interrupt_get(pio_, iebus_tx_nak_irq))
return fault_ != Send{0};
// Read the level before the clear throws it away, and clear before the
// flag: releasing the SM with words still queued would send the rest of the
// frame.
const auto pulled =
(uint8_t)(words_ - pio_sm_get_tx_fifo_level(pio_, tx_sm_));
failed_word_ = (uint8_t)(pulled - 1U);
pio_sm_clear_fifos(pio_, tx_sm_);
if (lost_arb) {
pio_interrupt_clear(pio_, iebus_tx_lost_arb_irq);
// The other frame is still arriving; put the ack match back so we can
// answer it if it turns out to be addressed to us.
rx_.rearm_ack();
// A bid lost before any word was pulled: the start-bit block saw the bus
// dominant with something other than a start bit.
if (pulled == 0)
fault_ = BUSY;
else if (failed_word_ < WORD_PERIPHERAL && failed_word_ < data_start_)
fault_ = LOST_ARBITRATION;
else
fault_ = CONTENDED_BUS;
} else {
pio_interrupt_clear(pio_, iebus_tx_nak_irq);
if (failed_word_ >= data_start_)
fault_ = NAK_DATA;
else if (failed_word_ == WORD_PERIPHERAL)
fault_ = NAK_ADDRESS;
else if (failed_word_ == WORD_CONTROL)
fault_ = NAK_CONTROL;
else if (failed_word_ == WORD_LENGTH)
fault_ = NAK_MESSAGE_LENGTH;
else
fault_ = NAK;
}
return true;
}
// Only a full FIFO waits, and not on a parked SM: it never frees a slot,
// which is why this isn't pio_sm_put_blocking.
void put(uint32_t word) {
while (pio_sm_is_tx_fifo_full(pio_, tx_sm_)) {
if (check())
return;
tight_loop_contents();
}
pio_sm_put(pio_, tx_sm_, word);
words_++;
}
IEBusRx &rx_;
PIO pio_;
uint pin_tx_;
uint tx_sm_;
uint tx_offset_;
uint ack_sm_;
bool claimed_ = false; // init() ran, so the destructor has something to undo
bool muted_ = false;
// Words put since the header, taken by the SM or not. Its flags park it,
// which freezes the FIFO, so the word it failed on is the last one it pulled:
// words_ - level - 1. Wrapping is harmless; the FIFO holds at most 8.
uint8_t words_;
uint8_t data_start_; // Position of the first data word
uint8_t failed_word_;
// Sticky until send_done reports it; nothing more is queued meanwhile.
Send fault_;
};
// The bus as a whole. Only it can hold the invariants that span the two
// engines: they sit on different PIOs and so must be started by one
// synchronised CTRL write, and muting has to reach both.
class Phy {
public:
Phy() = default;
Phy(const Phy &) = delete;
Phy &operator=(const Phy &) = delete;
~Phy() { activity_leds_deinit(); }
void init(uint16_t address) {
rx_.init(pio0, IEBUS_RX, address);
tx_.init(pio1, IEBUS_RX, IEBUS_TX);
// Cross-PIO IRQ partners must share a clock divider *and* have it restarted
// in the same cycle (S11.4). One CTRL write starts all three in step.
// Reception runs from here on, independent of the main loop.
pio_enable_sm_multi_mask_in_sync(rx_.pio(), 0, rx_.sm_mask(),
tx_.sm_mask());
activity_leds_init();
}
// "Muted" means we still listen but neither transmit nor ACK. Ordered so no
// ack request can be stranded across the transition: the rx engine stops
// asking before its driver goes away, and any flag it did leave behind is
// dropped before that driver comes back -- the iebus_ack SM's `wait 1 irq`
// clears an already-set flag and drives the slot immediately.
void mute(bool mute) {
muted_ = mute;
if (mute) {
rx_.mute(true);
tx_.mute(true);
} else {
pio_interrupt_clear(tx_.pio(), ack_irq);
tx_.mute(false);
rx_.mute(false);
}
}
bool is_muted() const { return muted_; }
IEBusRx &rx() { return rx_; }
IEBusTx &tx() { return tx_; }
private:
// Point one mirror SM at one src -> dst pair. Only the destination gets
// `pio_gpio_init`: taking the function select of a source pin would hand it
// to this PIO block and cut whoever actually drives it (IEBUS_TX) loose.
// Reading needs nothing but the pad's input buffer, which is on for both bus
// pins already -- asserted here so the mirror cannot go dark if that changes.
void pin_mirror_sm_init(uint sm, uint offset, uint src, uint dst) {
PIO pio = pio2;
gpio_set_input_enabled(src, true);
pio_gpio_init(pio, dst);
pio_sm_set_consecutive_pindirs(pio, sm, dst, 1, true);
pio_sm_config cfg = pin_mirror_program_get_default_config(offset);
sm_config_set_in_pins(&cfg, src);
sm_config_set_out_pins(&cfg, dst, 1);
// Default clkdiv: one copy per system clock, so the LED tracks the line far
// faster than a bit period.
pio_sm_init(pio, sm, offset, &cfg);
pio_sm_set_enabled(pio, sm, true);
}
// Initialize hardware to display bus TX/RX activity on two LEDs
// The bus idles HIGH, so the PIO program mirrors inverted pin state from the
// IEBUS TX/RX pins to the LED pins so that dominant bus activity (i.e. LOW
// state for IEBUS TX/RX pins) lights the respective LED
void activity_leds_init() {
PIO pio = pio2; // iebus_rx and iebus_tx each fill a block
// Indicators are cosmetic; never fail the bus bring-up for them.
if (!pio_can_add_program(pio, &pin_mirror_program))
return;
const int msm_rx = pio_claim_unused_sm(pio, false);
if (msm_rx < 0)
return;
const int msm_tx = pio_claim_unused_sm(pio, false);
if (msm_tx < 0) {
pio_sm_unclaim(pio, (uint)msm_rx);
return;
}
led_offset_ = (uint)pio_add_program(pio, &pin_mirror_program);
led_sm_rx_ = (uint)msm_rx;
led_sm_tx_ = (uint)msm_tx;
leds_claimed_ = true;
pin_mirror_sm_init(led_sm_rx_, led_offset_, IEBUS_RX, LED_RX);
pin_mirror_sm_init(led_sm_tx_, led_offset_, IEBUS_TX, LED_TX);
}
// The two mirrors share one copy of the program, so the slots go back
// individually but the program memory only once -- which is why this isn't
// two pio_remove_program_and_unclaim_sm calls.
void activity_leds_deinit() {
if (!leds_claimed_)
return;
PIO pio = pio2;
pio_sm_set_enabled(pio, led_sm_rx_, false);
pio_sm_set_enabled(pio, led_sm_tx_, false);
pio_sm_unclaim(pio, led_sm_rx_);
pio_sm_unclaim(pio, led_sm_tx_);
pio_remove_program(pio, &pin_mirror_program, led_offset_);
leds_claimed_ = false;
}
IEBusRx rx_;
IEBusTx tx_{rx_};
bool muted_ = false;
uint led_sm_rx_;
uint led_sm_tx_;
uint led_offset_;
bool leds_claimed_ = false;
};
Phy phy;
} // namespace
extern "C" void phy_init(uint16_t address) { phy.init(address); }
extern "C" void phy_mute(bool mute) { phy.mute(mute); }
extern "C" bool phy_is_muted() { return phy.is_muted(); }
extern "C" void phy_deafen(bool deaf) { phy.rx().deafen(deaf); }
extern "C" bool phy_frame_pending() { return phy.rx().frame_pending(); }
extern "C" void phy_guard_enter() {}
extern "C" void phy_guard_leave() {}
// --- Reads: served from the buffered frame ----------------------------------
//
// The engine has already checked every parity bit and driven every ack slot, so
// these only hand back fields. The error it recorded is reported by the first
// call of the frame; the caller abandons the frame on it, which releases it.
extern "C" Read phy_read_header(bool *is_unicast) {
const IEBusRx::RxFrame &frame = phy.rx().frame();
if (frame.err != Read{0}) {
phy.rx().release();
return frame.err;
}
*is_unicast = frame.is_unicast;
return Read{0};
}
extern "C" Read phy_read_controller_addr(uint16_t *addr) {
*addr = phy.rx().frame().controller_addr;
return Read{0};
}
extern "C" Read phy_read_peripheral_addr(uint16_t *addr) {
*addr = phy.rx().frame().peripheral_addr;
return Read{0};
}
extern "C" Read phy_read_control(uint8_t *control) {
*control = phy.rx().frame().control;
return Read{0};
}
extern "C" Read phy_read_length(uint8_t *length) {
*length = phy.rx().frame().length;
return Read{0};
}
extern "C" Read phy_read_data(uint8_t *data, uint8_t length,
[[maybe_unused]] uint8_t *data_index) {
memcpy(data, phy.rx().frame().data, length);
phy.rx().release();
return Read{0};
}
extern "C" Send phy_send_header(bool is_unicast) {
return phy.tx().send_header(is_unicast);
}
extern "C" Send phy_send_controller_addr(uint16_t addr) {
return phy.tx().send_field(12, addr, false, false);
}
extern "C" Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
return phy.tx().send_field(12, addr, true, expect_ack);
}
extern "C" Send phy_send_control(uint8_t control, bool expect_ack) {
return phy.tx().send_field(4, control, true, expect_ack);
}
extern "C" Send phy_send_length(uint8_t length, bool expect_ack) {
return phy.tx().send_field(8, length, true, expect_ack);
}
extern "C" Send phy_send_data(const uint8_t *data, uint8_t length,
bool expect_ack,
[[maybe_unused]] uint8_t *data_index) {
return phy.tx().send_data(data, length, expect_ack);
}
extern "C" Send phy_send_done(uint8_t *data_index) {
return phy.tx().send_done(data_index);
}
#ifndef NDEBUG
extern "C" void phy_set_dominant() { phy.tx().set_state(true); }
extern "C" void phy_set_recessive() { phy.tx().set_state(false); }
#ifdef MEASURE_BUS
// Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void) {};
#endif
#endif
+83
View File
@@ -0,0 +1,83 @@
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#include "hal/stdio.h"
#include "pico/stdio.h"
#include "pico/stdio_usb.h"
#include "pico/time.h"
#include "tusb.h" // IWYU pragma: keep
// stdio_write_nonblock() is all-or-nothing. The CDC TX FIFO must hold the
// largest buffer the app writes; that is a text frame log line (Frame::print),
// ~186 bytes at MAXLENGTH=32.
static_assert(CFG_TUD_CDC_TX_BUFSIZE >= 256,
"CDC TX FIFO too small to hold a whole frame log line");
namespace {
// Set when a dropped buffer had no room for its indicator; the next call emits
// it. The AVR port instead overwrites the last three queued bytes, which
// collapses a burst of drops to a single '!' line in exactly the same way.
bool drop_indicator_pending = false;
// stdout's only backend here is the usb driver, which writes into the same CDC
// TX FIFO measured below, so printf and this share one stream and one order.
void queue(const char *str, int len) {
stdio_put_string(str, len, false, false);
}
} // namespace
extern "C" void stdio_init() {
stdio_usb_init();
stdio_set_translate_crlf(&stdio_usb, false);
// pico_stdio wraps printf/puts/putchar straight onto the CDC, but not
// fputs/fwrite. Unbuffered stdout keeps the newlib path in step with them
// instead of stranding whole strings in the FILE buffer.
setvbuf(stdout, nullptr, _IONBF, 0);
}
// Overrides the SDK's weak newlib hook, which waits forever. EAGAIN rather than
// a 0-length read: newlib's refill skips a stream that has ever seen EOF.
extern "C" int _read(int handle, char *buffer, int length) {
if (handle != STDIN_FILENO) {
errno = EBADF;
return -1;
}
const int count = stdio_get_until(buffer, length, make_timeout_time_us(0));
if (count < 0) {
errno = EAGAIN;
return -1;
}
return count;
}
extern "C" bool stdio_write_nonblock(const void *buf, uint8_t len) {
uint32_t avail = tud_cdc_write_available();
if (drop_indicator_pending) {
if (avail < 3)
return false;
queue("!\n", 2);
drop_indicator_pending = false;
avail -= 2;
}
if (len == 0)
return true;
if (len <= avail) {
queue(static_cast<const char *>(buf), len);
return true;
}
// Every buffer ends with '\n', so the FIFO already sits at a line boundary.
if (avail >= 3)
queue("!\n", 2);
else
drop_indicator_pending = true;
return false;
}
+32 -4
View File
@@ -20,7 +20,7 @@
const char *const offon[] = {"OFF", "ON"}; const char *const offon[] = {"OFF", "ON"};
constexpr uint8_t CACHE_SIZE = 16; constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE;
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N, static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity"); "CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
@@ -74,12 +74,10 @@ int main() {
print_help(); print_help();
while (true) { while (true) {
if (peripheral.bus_is_active()) {
if (auto msg = peripheral.read(Print{.print = printAllFrames, if (auto msg = peripheral.read(Print{.print = printAllFrames,
.binary = printBinary, .binary = printBinary,
.verbose = verbose})) .verbose = verbose}))
incoming.push(std::move(*msg)); incoming.push(std::move(*msg));
}
if (const auto *in = incoming.peek()) { if (const auto *in = incoming.peek()) {
if (auto resp = peripheral.route(*in)) { if (auto resp = peripheral.route(*in)) {
@@ -105,7 +103,7 @@ int main() {
} }
// stdin must be non-blocking: yielding EOF when idle/empty // stdin must be non-blocking: yielding EOF when idle/empty
if (int readkey = getchar(); readkey != EOF) { if (int readkey = fgetc(stdin); readkey != EOF) {
switch (readkey) { switch (readkey) {
case '?': print_help(); break; case '?': print_help(); break;
case 'v': toggle_flag(&verbose, "Verbose errors:"); break; case 'v': toggle_flag(&verbose, "Verbose errors:"); break;
@@ -180,6 +178,16 @@ int main() {
while (peripheral.device<CDChanger>().media_busy()) {} while (peripheral.device<CDChanger>().media_busy()) {}
puts("end"); puts("end");
break; break;
case '+':
peripheral.get_bus().deafen(true);
peripheral.get_bus().set_dominant();
puts("Set bus dominant...");
break;
case '-':
peripheral.get_bus().set_recessive();
peripheral.get_bus().deafen(false);
puts("Set bus recessive...");
break;
#ifdef MEASURE_BUS #ifdef MEASURE_BUS
case 'M': peripheral.get_bus().measure(); break; case 'M': peripheral.get_bus().measure(); break;
#endif #endif
@@ -211,7 +219,19 @@ int main() {
hexChars[0] = hexChars[1] = 0; hexChars[0] = hexChars[1] = 0;
seqIsUnicast = false; seqIsUnicast = false;
break; break;
case '\r': // Eat carriage return too
if (readBinary)
goto DEFAULT;
[[fallthrough]];
case '\n': case '\n':
if (readSeq && !readBinary && seqIdx == 0) {
// Nothing to send, so leave the mode instead: an escape for an
// entry started by accident.
readSeq = false;
hexDigit = hexChars[0] = hexChars[1] = 0;
printAllFrames = lastPrintAllFrames;
break;
}
if (readSeq && seqIdx > 0) { if (readSeq && seqIdx > 0) {
if (readBinary) { if (readBinary) {
if (data_tmp[seqIdx - 1] == 0x17) { if (data_tmp[seqIdx - 1] == 0x17) {
@@ -270,6 +290,10 @@ int main() {
if (readBinary) { if (readBinary) {
data_tmp[seqIdx++] = readkey; data_tmp[seqIdx++] = readkey;
} else { } else {
// Only take valid hex digits
if (isxdigit(readkey) == 0)
break;
hexChars[hexDigit++] = readkey; hexChars[hexDigit++] = readkey;
if (hexDigit == 2) { if (hexDigit == 2) {
@@ -294,6 +318,8 @@ int main() {
} }
} }
} // switch (readkey) } // switch (readkey)
} else {
clearerr(stdin);
} // if (readkey != EOF) } // if (readkey != EOF)
} }
return 0; return 0;
@@ -324,6 +350,8 @@ void print_help() {
"s - MIC skip forward\n" "s - MIC skip forward\n"
"b - MIC skip backward\n" "b - MIC skip backward\n"
"M - Measure bit-timing (pulse-widths and periods)\n" "M - Measure bit-timing (pulse-widths and periods)\n"
"+ - Set bus driven/dominant\n"
"- - Set bus idle/recessive\n"
#endif #endif
"? - Print this message"); "? - Print this message");
} }