13 Commits

Author SHA1 Message Date
Allen Hill e64fe519d6 Add debug method to manually set controller address (e.g. for bench testing) 2026-09-28 11:36:01 -07:00
Allen Hill 598f6ede0e Offload TX data section to DMA (releases up to 12.5ms back to RTOS) 2026-09-28 11:36:01 -07:00
Allen Hill c24bab7a80 Migrate to FreeRTOS 2026-09-28 11:36:01 -07:00
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
28 changed files with 2694 additions and 702 deletions
+4 -2
View File
@@ -19,12 +19,14 @@ Checks: |
-readability-function-cognitive-complexity,
-misc-non-private-member-variables-in-classes
WarningsAsErrors: ""
ExcludeHeaderFilterRegex: 'out/build'
HeaderFilterRegex: '^src/.*'
ExcludeHeaderFilterRegex: "out/build"
HeaderFilterRegex: "^src/.*"
SystemHeaders: false
# AnalyzeTemporaryDtors: false
FormatStyle: none
CheckOptions:
- key: modernize-deprecated-headers.CheckHeaderFile
value: "false"
- key: readability-magic-numbers.IgnorePowersOf2IntegerValues
value: "true"
- key: readability-magic-numbers.IgnoredIntegerValues
+4
View File
@@ -8,3 +8,7 @@ CompileFlags:
- -ffixed-r2
- -ffixed-r3
- -ffixed-r4
- -fzero-init-padding-bits
- -fno-malloc-dce
Add:
- -Wno-missing-field-initializers
+53 -37
View File
@@ -1,25 +1,34 @@
cmake_minimum_required(VERSION 3.24)
cmake_minimum_required(VERSION 3.30)
project(avclan-mockingboard VERSION 1.1 LANGUAGES C CXX)
include(FetchContent)
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_CXX_STANDARD 23)
set(CMAKE_C_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_VERSION VERSION_LESS "13")
@@ -52,15 +61,6 @@ if(INSTALL_GIT_HOOKS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
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
# <avr/...>, no register access). The selected target's port sources and flags
# are contributed by its subdirectory.
@@ -70,23 +70,45 @@ add_library(avclan STATIC
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
# reaches into src/ for sibling headers (stdio.h, board.h) during its own
# build. The selected target adds its own per-target include path.
target_include_directories(avclan PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/avclan)
add_executable(mockingboard
src/sniffer.cc
)
target_link_libraries(mockingboard avclan)
# Pull in the selected hardware target: its port sources, per-target headers,
# hardware-specific compile options/definitions, device-pack handling, and the
# flashing target. Added after the targets above so it can extend them.
add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
if(NOT TARGET mockingboard)
add_executable(mockingboard
src/sniffer.cc
)
target_link_libraries(mockingboard avclan)
endif()
# Report firmware section sizes on every build, when the toolchain provides a
# size tool (CMAKE_SIZE). A standalone custom target (rather than a POST_BUILD
# command on mockingboard) so it prints even when the link is up to date:
# custom targets with no output are always considered stale and re-run.
# add_dependencies orders it after the link.
if(CMAKE_SIZE)
if(TARGET mockingboard AND CMAKE_SIZE)
add_custom_target(mockingboard-size ALL
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard>
COMMENT "Section sizes: mockingboard firmware"
@@ -95,18 +117,12 @@ if(CMAKE_SIZE)
add_dependencies(mockingboard-size mockingboard)
endif()
# Pull in the selected hardware target: its port sources, per-target headers,
# hardware-specific compile options/definitions, device-pack handling, and the
# flashing target. Added after the targets above so it can extend them.
add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
# --- Polyfill headers for std facilities the toolchain may lack -------------
# tl::expected / tl::optional back-fill std::expected / std::optional (with
# C++23 monadic ops) when the build's stdlib predates them. Runs after the port
# subdirectory to provide a port-supplied freestanding stdlib via
# subdirectory to provide a port-supplied freestanding stdlib via
# CMAKE_REQUIRED_INCLUDES, if necessary.
include(CheckCXXSourceCompiles)
include(FetchContent)
find_program(GIT_EXECUTABLE git REQUIRED)
if(TARGET libstdcpp)
+46 -10
View File
@@ -1,6 +1,24 @@
{
"version": 3,
"version": 9,
"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",
"hidden": true,
@@ -31,20 +49,38 @@
}
},
{
"name": "debug-base",
"name": "pico2",
"hidden": true,
"description": "Debug build settings",
"binaryDir": "${sourceDir}/out/build/pico2",
"inherits": "debug-base",
"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",
"hidden": true,
"description": "RelWithDebInfo build settings",
"inherits": "debug-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
"name": "pico2-gcc",
"displayName": "Pico 2W (GCC)",
"inherits": "pico2",
"description": "",
"generator": "Ninja",
"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_TOO_LONG,
BAD_STARTBIT,
NO_FRAME,
POOL_EMPTY, // non-bus error
};
@@ -155,7 +156,10 @@ struct Error {
NAK_MESSAGE_LENGTH,
NAK_CONTROL,
NAK_ADDRESS,
NAK_TOO_LONG,
NAK, // generic NAK has max severity
CONTENDED_BUS,
LOST_ARBITRATION,
BUSY,
MUTED,
};
@@ -176,12 +180,6 @@ struct SendError {
};
#endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t {
bit_zero = 0x00,
bit_one = 0x01,
bit_start = 0x10
};
#ifdef __cplusplus
} // namespace detail
} // namespace avclan
+143 -204
View File
@@ -26,13 +26,15 @@
| 1 │ *Acknowledge*
*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 <cstdint>
#include <cstdio>
#include <memory>
#include <new>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h"
#include "bus.hpp"
@@ -43,16 +45,12 @@
namespace {
using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
struct trailer_bits_t {};
struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit)
struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address)
struct with_ack_t : trailer_bits_t {
}; // bits + parity + ACK slot (all other fields)
inline constexpr no_parity_t no_parity{};
inline constexpr with_parity_t with_parity{};
inline constexpr with_ack_t with_ack{};
// The bus spec has a unit that loses arbitration retry rather than fail: "if
// the unit loses in arbitration, the frame is automatically reset up twice
// (three times in total)". Only an attempt that is outbid every time is an
// error worth reporting.
constexpr uint8_t SEND_ATTEMPTS = 3;
} // namespace
namespace avclan {
@@ -71,222 +69,136 @@ public:
Handle(const Handle &) = delete;
Handle(Handle &&) = delete;
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
Send sendstartbit() { return phy_send_startbit(); };
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
Read readstartbit() { return phy_read_startbit(); };
template <auto N, std::unsigned_integral T,
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Send send(T bits, Trailer /*tag*/) {
const Bit parity = sendbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>)
sendbits<1>(to_underlying(parity));
return Send{0};
// Forward to phy API (organized so that hal/phy.h isn't public/visible at the
// C++/library level)
// NOLINTBEGIN(readability-convert-member-functions-to-static)
Read read_header(bool *is_unicast) { return phy_read_header(is_unicast); };
Read read_controller_addr(uint16_t *addr) {
return phy_read_controller_addr(addr);
};
Read read_peripheral_addr(uint16_t *addr) {
return phy_read_peripheral_addr(addr);
};
Read read_control(uint8_t *control) { return phy_read_control(control); };
Read read_length(uint8_t *length) { return phy_read_length(length); };
Read read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
return phy_read_data(data, length, data_index);
};
template <auto N, std::unsigned_integral T>
requires(sizeof(T) < 3 && N < 16)
Send send(T bits, with_ack_t /*tag*/, bool expect_ack) {
send<N>(bits, with_parity);
if (expect_ack)
return read_ACK();
sendbits<1>(1U); // still need to fill the ack bit slot
return Send{0};
Send send_header(bool is_unicast) { return phy_send_header(is_unicast); };
Send send_controller_addr(uint16_t addr) {
return phy_send_controller_addr(addr);
};
template <auto N, std::unsigned_integral T,
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};
Send send_peripheral_addr(uint16_t addr, bool expect_ack) {
return phy_send_peripheral_addr(addr, expect_ack);
};
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};
Send send_control(uint8_t control, bool expect_ack) {
return phy_send_control(control, expect_ack);
};
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);
Send send_length(uint8_t length, bool expect_ack) {
return phy_send_length(length, expect_ack);
};
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));
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 <> 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() {
void Bus::init(uint16_t address) {
// 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
// (phy_init is not assumed re-entrant/idempotent).
if (inited_)
return;
phy_init();
phy_init(address);
muted_ = false; // phy_init leaves the bus TX unmuted
inited_ = true;
};
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
bool Bus::is_active() const { return phy_active(); };
void Bus::mute(bool mute) {
phy_mute(mute);
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
};
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void Bus::deafen(bool deaf) { phy_deafen(deaf); }
auto Bus::read(uint16_t address, Frame::Print print)
auto Bus::read(Frame::Print print)
-> expected<std::unique_ptr<Frame>, Error::Read> {
struct errtype {
Read errno;
Read type;
uint16_t val;
} err = {};
using enum Read;
std::unique_ptr<Frame> in(new (std::nothrow) Frame);
std::unique_ptr<Frame> in = Frame::acquire();
if (!in) {
err.errno = POOL_EMPTY;
err.type = POOL_EMPTY;
goto handle_err;
}
phy_wait_frame(portMAX_DELAY);
{ // bound handle lifetime
auto handle = get();
bool shouldACK = false;
uint8_t tmp = 0;
err.errno = handle.readstartbit();
if (err.errno != Read{0})
err.type = handle.read_header(&in->is_unicast);
if (err.type != Read{0})
goto handle_err;
handle.read<1>(&tmp, no_parity);
in->is_unicast = (tmp != 0U);
if (auto rerr = handle.read<12>(&in->controller_addr, with_parity);
rerr == BAD_PARITY) {
err.errno = BAD_CONTROLLER_PARITY;
err.type = handle.read_controller_addr(&in->controller_addr);
if (err.type != Read{0}) {
if (print.verbose)
err.val = in->controller_addr;
goto handle_err;
}
// Using lambda for delayed evaluation of peripheral_addr field
// deref, which will be written by the time the lambda is evaluated
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;
err.type = handle.read_peripheral_addr(&in->peripheral_addr);
if (err.type != Read{0}) {
if (print.verbose)
err.val = in->peripheral_addr;
goto handle_err;
}
if (auto rerr = handle.read<4>(&in->control, with_ack, shouldACK);
rerr == BAD_PARITY) {
err.errno = BAD_CONTROL_PARITY;
err.type = handle.read_control(&in->control);
if (err.type != Read{0}) {
if (print.verbose)
err.val = in->control;
goto handle_err;
}
if (auto rerr = handle.read<8>(&in->length, with_ack, shouldACK);
rerr == BAD_PARITY) {
err.errno = BAD_LENGTH_PARITY;
err.type = handle.read_length(&in->length);
if (err.type != Read{0}) {
if (print.verbose)
err.val = in->length;
goto handle_err;
}
if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.errno = BAD_LENGTH_RANGE;
} else if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.type = BAD_LENGTH_RANGE;
err.val = in->length;
goto handle_err;
}
for (uint8_t i = 0; i < in->length; i++) {
if (auto rerr = handle.read<8>(&in->data[i], with_ack, shouldACK);
rerr == BAD_PARITY) {
err.errno = BAD_DATA_PARITY;
if (print.verbose)
err.val = in->data[i];
uint8_t data_i = 0;
err.type = handle.read_data(in->data, in->length, &data_i);
if (err.type != Read{0}) {
if (print.verbose)
err.val = in->data[data_i];
goto handle_err;
}
goto handle_err;
}
} // destroy handle
if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:;
if (err.type == NO_FRAME)
return unexpected{NO_FRAME};
fputs("ERR(read): ", stdout);
switch (err.errno) {
switch (err.type) {
case POOL_EMPTY: puts("failed Frame alloc"); break;
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_MISSED: fputs("missed start bit", stdout); break;
@@ -304,6 +216,7 @@ auto Bus::read(uint16_t address, Frame::Print print)
case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE;
case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break;
case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE;
case NO_FRAME:
case BAD_PARITY:
__builtin_unreachable();
VERBOSE:
@@ -315,14 +228,14 @@ auto Bus::read(uint16_t address, Frame::Print print)
}
// Only print if some data has been correctly received
if (print.print && (err.errno < STARTBIT_MISSED)) {
if (err.errno > BAD_DATA_PARITY)
if (print.print && (err.type < STARTBIT_MISSED)) {
if (err.type > BAD_DATA_PARITY)
in->length = 0;
in->print(print);
}
if (err.errno != Read{0})
return unexpected(err.errno);
if (err.type != Read{0})
return unexpected(err.type);
return in;
}
@@ -331,79 +244,90 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
struct errtype {
// Error enum is ordered such that a lower numeric value corresponds to
// more success
Send errno;
Send type;
uint8_t val;
} err = {};
using enum Send;
if (is_muted()) {
err.errno = MUTED;
err.type = MUTED;
goto handle_err;
}
{ // bound handle lifetime
auto handle = get();
for (uint8_t attempt = 0; attempt < SEND_ATTEMPTS; attempt++) {
auto handle = get(); // bound handle lifetime
if (handle.sendstartbit() == BUSY) {
// Some other device is already driving the bus
err.errno = BUSY;
err.type = Send{0};
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;
}
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);
serr == NAK) {
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
if (err.type != Send{0})
goto handle_err; // Outbid (or busy) on every attempt
// back to read mode
if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:;
fputs("Error", stdout);
switch (err.errno) {
switch (err.type) {
case MUTED: fputs(": Device muted", 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_CONTROL:
case NAK_MESSAGE_LENGTH:
case NAK_DATA:
case NAK_TOO_LONG:
case NAK:
fputs(" NAK: ", stdout);
switch (err.errno) {
switch (err.type) {
case NAK_ADDRESS: fputs("address", stdout); break;
case NAK_CONTROL: fputs("Control", stdout); break;
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
case NAK_DATA: printf(" data[%u]", err.val); break;
case NAK_TOO_LONG: fputs("too long", stdout); break;
case NAK:
case MUTED:
case CONTENDED_BUS:
case LOST_ARBITRATION:
case BUSY: __builtin_unreachable();
}
break;
@@ -414,16 +338,31 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
if (print.print)
out.print(print);
return err.errno;
return err.type;
}
Bus::Handle Bus::get() { return Handle{*this}; };
#if !defined(NDEBUG) && defined(MEASURE_BUS)
// Debug bit-timing measurement on the one physical bus; instance-scoped for the
// same reason as is_active().
#if !defined(NDEBUG)
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 like
// deafen().
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void Bus::measure() { phy_measure(); }
#endif
#endif
} // namespace avclan
+12 -4
View File
@@ -69,18 +69,26 @@ public:
Bus(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);
bool is_active() const;
// Prevent the device from being active on the bus
void mute(bool mute);
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
void measure();
detail::Error::Send sendbyte(uint8_t byte, bool ack = false);
void set_dominant();
void set_recessive();
#endif
expected<std::unique_ptr<Frame>, Error::Read> read(uint16_t address,
Frame::Print print);
expected<std::unique_ptr<Frame>, Error::Read> read(Frame::Print print);
Error::Send send(const Frame &out, Frame::Print print);
private:
+110 -108
View File
@@ -3,19 +3,22 @@
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include "avclan.h"
#include "cdchanger.hpp"
#include "device.hpp"
#include "frame.hpp"
#include "hal/cd_timer.h"
#include "hal/media.h"
namespace {
using namespace avclan;
using namespace std::chrono_literals;
constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
to_underlying(Device::STATUS),
@@ -28,39 +31,45 @@ constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
0x01,
0x02};
constexpr int WIRE_SIZE = 8; // cd state report size in bytes
constexpr int TIME_SKIP = 15; // seconds
constexpr int WIRE_SIZE = 8; // cd state report size in bytes
constexpr auto TIME_SKIP = 15s;
constexpr int TWODIGIT_MAX = 99;
constexpr auto MAX_TIME = std::chrono::minutes{TWODIGIT_MAX} + 59s;
/* Pack a 0–TWODIGIT_MAX count into 2-digit BCD. Values >TWODIGIT_MAX (sentinels
such as 0xFF / 0x7F meaning "no time") pass through unchanged so they survive
the wire round-trip. */
// Pack a 0–TWODIGIT_MAX count into 2-digit BCD
constexpr uint8_t toBCD(uint8_t val) {
if (val > TWODIGIT_MAX)
return val;
return (uint8_t)(((val / 10) << 4) | (val % 10));
}
extern "C" void incrementTime_callback(void *self) {
static_cast<avclan::CDChanger *>(self)->incrementTime();
}
extern "C" bool isPlaying_callback(void *self) {
return static_cast<avclan::CDChanger *>(self)->isPlaying();
// Whole seconds, saturated to the displayable range
constexpr std::chrono::seconds displaySeconds(std::chrono::milliseconds t) {
return std::clamp(std::chrono::floor<std::chrono::seconds>(t), -MAX_TIME,
MAX_TIME);
}
} // namespace
namespace avclan {
void CDChanger::init() {
void CDChanger::init(Notifier notif) {
media_init();
cdtimer_init(this, &incrementTime_callback, &isPlaying_callback);
notifier = notif;
statusTimer = xTimerCreate(
"cd status", pdMS_TO_TICKS(1000), true, this, [](TimerHandle_t timer) {
auto &self = *static_cast<CDChanger *>(pvTimerGetTimerID(timer));
// Timer callbacks must not block; a refused request is re-made next
// tick
if (!self.statusQueued.exchange(true) && !self.notifier.notify(0, 0))
self.statusQueued = false;
});
configASSERT(statusTimer);
}
void CDChanger::handle(const Frame &in, Frame &out) {
if (in.length < 4)
return; // [Currently known] valid CDChanger frames have at least 4 bytes
std::optional<MediaAction> media;
const uint8_t *data = &in.data[1];
const auto from = static_cast<Device>(*data++);
/* const auto to = */ data++;
@@ -73,7 +82,7 @@ void CDChanger::handle(const Frame &in, Frame &out) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
// Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]).
switch (action) {
switch (const std::lock_guard lock(mutex); action) {
case Enable_Function_Req:
out.is_unicast = true;
out.length = sizeof(function_change_resp);
@@ -93,7 +102,7 @@ void CDChanger::handle(const Frame &in, Frame &out) {
out.data[2] = to_underlying(from);
out.data[3] = to_underlying(Disable_Function_Resp);
if (isPlaying()) {
stopPlaying();
media = stopPlaying();
state = 0;
flags2 = 0x80;
out.reaction = r_StatusReport;
@@ -165,47 +174,39 @@ void CDChanger::handle(const Frame &in, Frame &out) {
++track;
else
track = 1;
mins = 0xff;
secs = 0x7f;
flags2 &= ~NEGATIVE;
time.reset();
generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Track_Next);
media = MediaAction::Track_Next;
out.reaction = r_TrackChange;
break;
case Track_Seek_Down:
state = SEEKING_TRACK;
// Track down returns to track beginning if in ~middle of song
if ((flags2 & NEGATIVE) != 0 || (mins == 0 && secs < 5)) {
if (const auto t = trackTime(); t && *t < 5s) {
if (track > 1)
--track;
else
track = TWODIGIT_MAX;
media_action(MediaAction::Track_Prev);
media = MediaAction::Track_Prev;
}
mins = 0xff;
secs = 0x7f;
flags2 &= ~NEGATIVE;
time.reset();
generateStatus(out, true, Device::CMD_SW);
out.reaction = r_TrackChange;
break;
case Track_Fast_Forward: {
state |= SEEKING;
incrementTime(TIME_SKIP);
seek(TIME_SKIP);
generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Forward);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now
media = MediaAction::Skip_Forward;
out.reaction = r_SendOnly;
break;
}
case Track_Rewind: {
state |= SEEKING;
incrementTime(-TIME_SKIP);
seek(-TIME_SKIP);
generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Backward);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now
media = MediaAction::Skip_Backward;
out.reaction = r_SendOnly;
break;
}
@@ -272,18 +273,24 @@ void CDChanger::handle(const Frame &in, Frame &out) {
default: break;
}
#pragma GCC diagnostic pop
// Media action handled outside of switch to minimize lock duration
if (media)
media_action(*media);
}
std::unique_ptr<Frame>
CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
std::optional<MediaAction> media;
std::unique_ptr<Frame> next;
if (!exp) {
if (const std::lock_guard lock(mutex); !exp) {
if (exp.error().reaction == to_underlying(r_StateReport) &&
exp.error().err == detail::Error::Send::NAK_ADDRESS &&
++failedStatusReports > 1) {
failedStatusReports = 0;
stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?)
media = stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?)
}
} else {
auto out = std::move(exp.value());
@@ -312,9 +319,9 @@ CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_SendOnly;
break;
case r_TrackChange:
setTime(0, 0);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is
// ~1 sec from now
setTime(0ms);
// Track starts at a whole sec; next status is ~1 sec out
xTimerReset(statusTimer, 0);
[[fallthrough]];
case r_NormalizeState:
normalizeState();
@@ -327,7 +334,7 @@ CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_BeganPlaying;
break;
case r_BeganPlaying:
startPlaying(); // only start PIT after normalizing state
media = startPlaying(); // only start status timer after normalizing
out->reaction = r_Nothing;
break;
case r_StatusReport:
@@ -341,18 +348,19 @@ CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
}
if (out->reaction > r_Nothing)
return out;
next = std::move(out);
}
return {};
if (media)
media_action(*media);
return next;
}
void CDChanger::enable(Frame &out) {
if (!isPlaying()) {
if (mins > TWODIGIT_MAX)
mins = 0;
if (secs > TWODIGIT_MAX)
secs = 0;
const std::lock_guard lock(mutex);
if (!time)
setTime(0ms);
state = SEEKING | SEEKING_TRACK;
flags2 = 0x80;
generateStatus(out, false, Device::STATUS);
@@ -360,85 +368,81 @@ void CDChanger::enable(Frame &out) {
}
}
bool CDChanger::pending() { return cdtimer_pending(); }
void CDChanger::emit(Frame &out) {
void CDChanger::emit(Frame &out, uint32_t /*payload*/) {
statusQueued = false; // Clear first so a tick during emit() queues anew
const std::lock_guard lock(mutex);
generateStatus(out, false, Device::STATUS);
out.reaction = r_StateReport;
cdtimer_clear();
}
bool CDChanger::isPlaying() const { return playing; }
// Sets CD_mode to play and resets timer count (so that the next interrupt is in
// 1 sec)
void CDChanger::startPlaying() {
// Starts track time and the ~1 Hz status report (the first ~1 sec from now)
std::optional<MediaAction> CDChanger::startPlaying() {
static bool havePlayed = false;
std::optional<MediaAction> media;
if (havePlayed)
media_action(MediaAction::Play);
media = MediaAction::Play;
havePlayed |= true;
if (!playing)
refTick = xTaskGetTickCount();
playing = true;
cdtimer_reset();
xTimerReset(statusTimer, 0);
return media;
}
void CDChanger::stopPlaying() {
cdtimer_disable();
MediaAction CDChanger::stopPlaying() {
xTimerStop(statusTimer, 0);
time = trackTime();
playing = false;
media_action(MediaAction::Pause);
return MediaAction::Pause;
}
std::optional<std::chrono::milliseconds> CDChanger::trackTime() const {
if (!time || !playing)
return time;
return *time + std::chrono::milliseconds{
pdTICKS_TO_MS(xTaskGetTickCount() - refTick)};
}
void CDChanger::setTime(std::chrono::milliseconds t) {
time = t;
refTick = xTaskGetTickCount();
}
// Seek `by` from the current whole second; no-op while no time is shown
void CDChanger::seek(std::chrono::seconds by) {
const auto t = trackTime();
if (!t)
return;
setTime(displaySeconds(*t + by));
if (playing) // Seeked to a whole sec; next status is ~1 sec out
xTimerReset(statusTimer, 0);
}
// Serialize cd_status into the wire format.
// track/mins/secs, need converted from decimal to BCD
void CDChanger::serialize(uint8_t *dst) const {
uint8_t mins = 0xFF; // "no time" sentinels
uint8_t secs = 0x7F;
bool negative = false;
if (const auto t = trackTime()) {
const auto sec = displaySeconds(*t);
const auto magnitude = std::chrono::abs(sec);
const auto wholeMins = std::chrono::floor<std::chrono::minutes>(magnitude);
mins = toBCD((uint8_t)wholeMins.count());
secs = toBCD((uint8_t)(magnitude - wholeMins).count());
negative = sec < 0s;
}
*dst++ = cds;
*dst++ = state;
*dst++ = disc;
*dst++ = toBCD(track);
*dst++ = toBCD(mins);
*dst++ = toBCD(secs);
*dst++ = mins;
*dst++ = secs;
*dst++ = flags;
*dst++ = flags2;
}
void CDChanger::setTime(uint8_t min, uint8_t sec) {
mins = min;
secs = sec;
}
// Increment the time by inc_sec (REQUIRES |inc_sec| <= 59).
void CDChanger::incrementTime(int8_t inc_sec) {
// Sentinel values (>TWODIGIT_MAX) mean "no time"; leave them alone until
// setTime() replaces them with a real count.
if (mins > TWODIGIT_MAX)
return;
if ((flags2 & NEGATIVE) != 0)
inc_sec = -inc_sec; // time forward shrinks a negative magnitude
int8_t sum = secs + inc_sec;
if (sum < 0 && mins == 0) {
// Stepped through zero: the display flips sign and counts away from it.
secs = (uint8_t)-sum;
flags2 ^= NEGATIVE;
return;
}
if (sum > 59) {
if (mins == TWODIGIT_MAX) { // saturate at 99:59 rather than wrap the hour
secs = 59;
return;
}
sum -= 60;
++mins;
} else if (sum < 0) {
sum += 60;
--mins; // mins > 0: the mins == 0 borrow was handled above
}
secs = (uint8_t)sum;
// Zero is neither sign, so it must never display as -00:00.
if ((mins | secs) == 0)
flags2 &= ~NEGATIVE;
*dst++ = negative ? (flags2 | NEGATIVE) : (flags2 & ~NEGATIVE);
}
// Used for changed status messages
@@ -460,10 +464,8 @@ void CDChanger::generateStatus(Frame &status, bool is_unicast,
}
void CDChanger::normalizeState() {
if (mins > TWODIGIT_MAX)
mins = 0;
if (secs > TWODIGIT_MAX)
secs = 0;
if (!time)
setTime(0ms);
state = PLAYBACK;
flags &= (uint8_t)~(DISK_SCAN | SCAN);
}
+40 -10
View File
@@ -5,8 +5,15 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <optional>
#include "FreeRTOS.h" // IWYU pragma: export
#include "semphr.h"
#include "timers.h"
#include "avclan.h"
#include "device.hpp"
@@ -60,16 +67,17 @@ public:
};
static constexpr Device id = Device::CD_CHANGER;
void init();
CDChanger() : mutex(xSemaphoreCreateMutex()) {}
CDChanger(const CDChanger &) = delete;
void init(Notifier notifier);
void handle(const Frame &in, Frame &out);
std::unique_ptr<Frame>
react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
void enable(Frame &out);
void disable(Frame &out);
static bool pending();
void emit(Frame &out);
void incrementTime(int8_t inc_sec = 1);
void emit(Frame &out, uint32_t payload);
bool isPlaying() const;
#ifndef NDEBUG
void media_action(MediaAction action);
@@ -78,21 +86,43 @@ public:
#endif
private:
void startPlaying();
void stopPlaying();
// Implements the BasicLockable named requirements for an xSemaphore
class xMutex {
public:
explicit xMutex(SemaphoreHandle_t mutex) : mutex_{mutex} {}
xMutex(const xMutex &) = delete;
void lock() noexcept { xSemaphoreTake(mutex_, portMAX_DELAY); }
void unlock() noexcept { xSemaphoreGive(mutex_); }
private:
SemaphoreHandle_t mutex_ = nullptr;
};
std::optional<MediaAction> startPlaying();
MediaAction stopPlaying();
std::optional<std::chrono::milliseconds> trackTime() const;
void setTime(std::chrono::milliseconds t);
void seek(std::chrono::seconds by);
void serialize(uint8_t *dst) const;
void setTime(uint8_t mins, uint8_t secs);
void generateStatus(Frame &status, bool is_unicast, Device to) const;
void normalizeState();
Notifier notifier{};
TimerHandle_t statusTimer = nullptr;
std::atomic<bool> statusQueued = false; // coalesces status emit requests
xMutex mutex;
// Track time is a stopwatch: `time` is relative to `refTick`, and
// advances while `playing`. nullopt means no time is shown.
std::optional<std::chrono::milliseconds> time;
TickType_t refTick = 0;
bool playing = false;
int failedStatusReports = 0;
uint8_t cds = CD1;
uint8_t state = SEEKING | SEEKING_TRACK;
uint8_t disc = 1;
uint8_t track = 1; // Decimal storage; serialize to BCD
uint8_t mins = 0xFF; // Decimal storage; serialize to BCD
uint8_t secs = 0x7F; // Decimal storage; serialize to BCD
uint8_t track = 1; // Decimal storage; serialize to BCD
uint8_t flags = 0;
uint8_t flags2 = 0x80;
};
+27 -10
View File
@@ -7,6 +7,9 @@
#include <cstdint>
#include <memory>
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
#include "avclan.h"
#include "frame.hpp"
#include "stdshim.hpp"
@@ -47,20 +50,34 @@ enum class Device : uint8_t {
TRIP_INFO = 0xE5,
};
// Notifies the Peripheral to emit for a device at `index`. Requests are served
// in order, one `emit()` each; coalescing is up to the device. `val` (24 bits)
// is device-defined. Returns whether the request was accepted within `wait`.
class Notifier {
public:
Notifier() = default;
Notifier(QueueHandle_t queue, uint8_t index) : queue{queue}, index{index} {}
bool notify(uint32_t val = 0, TickType_t wait = portMAX_DELAY) const {
const uint32_t item = (val << 8) | index;
return xQueueSend(queue, &item, wait) == pdPASS;
}
private:
QueueHandle_t queue = nullptr;
uint8_t index = 0;
};
template <class T>
concept DeviceInterface =
requires { std::integral_constant<Device, T::id>{}; } &&
requires(T dev, const Frame &in, Frame &out,
expected<std::unique_ptr<Frame>, detail::SendError> exp) {
dev.init();
requires(T dev, Notifier notifier, const Frame &in, Frame &out,
expected<std::unique_ptr<Frame>, detail::SendError> exp,
uint32_t payload) {
dev.init(notifier);
dev.handle(in, out);
dev.enable(out);
{
dev.react(std::move(exp))
} -> std::same_as<std::unique_ptr<Frame>>;
{ dev.pending() } -> std::convertible_to<bool>;
// Devices must clear `pending()` after `emit()` is called
dev.emit(out);
{ dev.react(std::move(exp)) } -> std::same_as<std::unique_ptr<Frame>>;
dev.emit(out, payload);
};
} // namespace avclan
+26 -8
View File
@@ -6,8 +6,13 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <type_traits>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "frame.hpp"
#include "hal/stdio.h"
#include "stdshim.hpp"
@@ -16,7 +21,6 @@
#include <array>
#include <cstddef>
#include <limits>
#include <new>
namespace {
template <class T, std::uint8_t N>
@@ -60,14 +64,28 @@ namespace avclan {
#if defined(AVCLAN_FRAME_POOL_N)
void *Frame::operator new(std::size_t /*count*/,
const std::nothrow_t & /*tag*/) noexcept {
return pool.acquire();
taskENTER_CRITICAL();
Frame *frame = pool.acquire();
taskEXIT_CRITICAL();
return frame;
}
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
void Frame::operator delete(void *ptr) noexcept {
taskENTER_CRITICAL();
pool.release(static_cast<Frame *>(ptr));
taskEXIT_CRITICAL();
}
#endif
std::unique_ptr<Frame> Frame::acquire() {
std::unique_ptr<Frame> frame(new (std::nothrow) Frame);
for (uint8_t retries = 0; !frame && retries < 3; retries++) {
vTaskDelay(pdMS_TO_TICKS(2));
frame.reset(new (std::nothrow) Frame);
}
return frame;
}
namespace {
// Emit `value` as at least `width` (lowercase, as to_chars emits) hex digits,
// zero-padded. `width` must be <= 3 (see the padding below); every call site
@@ -163,14 +181,14 @@ void Frame::print(Frame::Print print) const {
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
struct errtype {
Error::Parse errno;
Error::Parse type;
uint8_t val;
} err = {};
const uint8_t *last = bytes + len;
if (len < Frame::MIN_SIZE) {
err.errno = TOO_SHORT;
err.type = TOO_SHORT;
goto handle_err;
}
@@ -183,7 +201,7 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
length = *bytes++;
if (length > MAXLENGTH) {
err.errno = LENGTH_TOO_BIG;
err.type = LENGTH_TOO_BIG;
err.val = length;
goto handle_err;
}
@@ -191,14 +209,14 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
if ((bytes + length) <= last) {
memcpy(data, bytes, length);
} else {
err.errno = MISMATCH_LENGTH;
err.type = MISMATCH_LENGTH;
goto handle_err;
}
if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:;
fputs("ERR(parse): ", stdout);
switch (err.errno) {
switch (err.type) {
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
case MISMATCH_LENGTH:
puts("frame->length is longer than remaining data");
@@ -211,6 +229,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
}
}
return err.errno;
return err.type;
}
} // namespace avclan
+4
View File
@@ -6,6 +6,7 @@
#pragma once
#include <cstdint>
#include <memory>
#include "avclan.h"
@@ -30,6 +31,9 @@ struct Frame {
Error::Parse parse(const uint8_t *bytes, uint8_t len);
void print(Print print) const;
// Retries every 2ms up to 3 times (max 6ms wait); null if still exhausted
static std::unique_ptr<Frame> acquire();
#if defined(AVCLAN_FRAME_POOL_N)
// O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported.
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
// CDChanger periodic-timer HAL: the ~1 Hz tick that drives the CD changer's
// status updates. The timer hardware is target-specific; the CD changer owns
// what a tick means (see cdchanger.cc). The device polls cdtimer_pending() and
// clears the tick with cdtimer_clear().
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// One-time hardware bring-up. Leaves the tick disabled.
void cdtimer_init(void *ptr, void(clbk)(void *), bool(isplay)(void *));
// Reset the count so the next tick is ~1 s out, and enable the tick.
void cdtimer_reset(void);
// Restore / disable the ~1 Hz tick.
void cdtimer_restore(void);
void cdtimer_disable(void);
extern volatile bool cdtimer_pending_flag;
static inline bool cdtimer_pending() { return cdtimer_pending_flag; }
static inline void cdtimer_clear() { cdtimer_pending_flag = false; }
#ifdef __cplusplus
}
#endif
+67 -38
View File
@@ -7,21 +7,23 @@
#include <stdint.h>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h"
#ifdef __cplusplus
using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
extern "C" {
#else
typedef enum Read Read;
typedef enum Send Send;
typedef enum Bit Bit;
#endif
// 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
// unchanged/still allowed)
@@ -30,56 +32,83 @@ void phy_mute(bool mute);
// Non-mutating (e.g. theoretically const qualified/-able)
bool phy_is_muted(void);
// True when bus is driven/"dominant" (logical 0)
bool phy_active(void);
// Do not respond to or log incoming frames. Can still send frames.
void phy_deafen(bool deaf);
// Blocks until a frame is ready
void phy_wait_frame(TickType_t xTicksToWait);
// Bus-transaction guard: quiesce the other async sources (e.g. interrupts)
// 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
// isn't a concern.
// sources with `phy_guard_leave`.
// - May be a no-op on a target where concurrency/preemption isn't a concern.
void phy_guard_enter(void);
void phy_guard_leave(void);
// Validates an incoming start bit; see avclan::detail::Error::Read.
// Invariants:
// - Must only be called after positive phy_active() call.
Read phy_read_startbit(void);
/* Per-field frame I/O.
*
* All send functions may have asynchronous implementations (e.g. return before
* 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
* completed send failure. Otherwise, `phy_send_done` must be called to block
* 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
* queued send's have finished sending over the bus.
*
* The read functions are similarly optionally asynchronous, and may return the
* 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.
*/
// Acquire the bus and emit a start bit; may return BUSY
Send phy_send_startbit(void);
// Returns NO_FRAME when there is no frame to read.
Read phy_read_header(bool *is_unicast);
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);
/* Returns 0 (`(Send)0`) if the peripheral sent an ACK bit, otherwise returns
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
finish a "1" bit.
*/
Send phy_read_ack(void);
void phy_send_ack(void);
// Send start and broadcast bits. Returns success or one of these error values:
// MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
// a broadcast).
Send phy_send_header(bool is_unicast);
// Per-symbol I/O. The send* helpers return the even parity of the bits sent;
// the read* helpers return the even parity of the bits read. The _u8/_u16
// suffixes name the source-operand width. The function implementations need not
// all be separate/independent (e.g. all send functions could be redirect to a
// 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.
// Send the controller address. Returns success or LOST_ARBITRATION (a device
// 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);
// Intended for sending parity bits
void phy_send_bit(Bit bit);
// Allows asynchronous ports to block until the phy has finished sending the
// frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS,
// etc) or CONTENDED_BUS; a port that queues the header and controller address
// 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);
// Variants available to minimize unnecessary work for max runtime efficiency
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len);
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len);
Bit phy_send_byte(const uint8_t *byte);
#ifndef NDEBUG
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len);
Bit phy_read_bits_u16(uint16_t *bits, int8_t len);
Bit phy_read_byte(uint8_t *byte);
// Hold the bus at a level until the matching call.
void phy_set_dominant(void);
void phy_set_recessive(void);
#if !defined(NDEBUG) && defined(MEASURE_BUS)
#ifdef MEASURE_BUS
// Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void);
#endif
#endif
#ifdef __cplusplus
+3 -2
View File
@@ -4,6 +4,7 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
@@ -11,8 +12,8 @@ extern "C" {
// Generic stdio interface initialization. All user I/O goes through <stdio.h>
// functions. Assumptions/invariants:
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when
// empty). Necessary to avoid stalling the REPL poll loop.
// - A libc read of stdin blocks the calling task (not the CPU) until at least
// one byte is available.
// - 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
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
+47 -50
View File
@@ -4,15 +4,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <tuple>
#include <type_traits>
#include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
#include "avclan.h"
#include "bus.hpp"
#include "device.hpp"
@@ -38,28 +43,38 @@ public:
static_assert(((Devs::id != NoDevice) && ...),
"a registered Device id collides with the NoDevice sentinel; "
"update the sentinel value in avclan.h");
static_assert(
sizeof...(Devs) <= 0x100,
"too many devices: Notifier packs the device index into 8 bits");
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
bus.init();
(std::get<Devs>(devices_).init(), ...);
static constexpr UBaseType_t EMIT_QUEUE_LEN = 2 * sizeof...(Devs);
Peripheral(Bus &bus, uint16_t address)
: bus{bus}, address_{address},
emit_requests_{xQueueCreate(EMIT_QUEUE_LEN, sizeof(uint32_t))} {
configASSERT(emit_requests_);
bus.init(address);
(std::get<Devs>(devices_).init(Notifier{emit_requests_, index_of<Devs>()}),
...);
}
Peripheral(const Peripheral &) = delete;
uint16_t controller() const { return controller_; };
template <DeviceInterface Dev> Dev &device() {
return std::get<Dev>(devices_);
}
bool bus_is_active() const { return bus.is_active(); };
void mute(bool mute) { bus.mute(mute); };
bool is_muted() const { return bus.is_muted(); };
#ifndef NDEBUG
Bus &get_bus() { return bus; }
void set_controller(uint16_t addr) { controller_ = addr; }
#endif
expected<std::unique_ptr<Frame>, Error::Read>
read(Frame::Print print = Frame::Print{}) {
return bus.read(address_, print);
return bus.read(print);
};
expected<std::unique_ptr<Frame>, detail::SendError>
@@ -69,8 +84,9 @@ public:
out->control = 0xF;
auto err = bus.send(*out, print);
if (err != Error::Send{0})
return unexpected{
detail::SendError{out->owning_device, out->reaction, err}};
return unexpected{detail::SendError{.owning_device = out->owning_device,
.reaction = out->reaction,
.err = err}};
return out;
};
@@ -85,7 +101,7 @@ public:
if (is_muted() || in.length < 3)
return {};
std::unique_ptr<Frame> out(new (std::nothrow) Frame);
std::unique_ptr<Frame> out = Frame::acquire();
if (!out) {
puts("!! failed Frame alloc in route !!");
return unexpected{Error::Read::POOL_EMPTY};
@@ -204,53 +220,33 @@ public:
return next;
}
// Service ready devices in round-robin order
// Blocks until a device requests an emit; requests are served in order.
std::unique_ptr<Frame> poll() {
using U = std::unique_ptr<Frame>;
auto does_emit = [&](DeviceInterface auto &dev) -> U {
if (!dev.pending())
return {};
U out(new (std::nothrow) Frame);
if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
originate(dev, *out, [](auto &d, auto &out) { d.emit(out); });
return out;
};
// Runtime tuple index helper
auto does_index_emit = [&](std::size_t t) -> U {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> U {
U out;
((Is == t && (out = does_emit(std::get<Is>(devices_)))) || ...);
return out;
}(std::index_sequence_for<Devs...>{});
};
constexpr std::size_t N = sizeof...(Devs);
if constexpr (N == 1) { // round-robin not needed
return does_emit(std::get<0>(devices_));
} else {
static uint8_t rr_ = 0; // round-robin cursor
const std::size_t start = rr_;
for (std::size_t t = start; t < N; ++t) // [start, N)
if (auto out = does_index_emit(t)) {
rr_ = (t + 1 == N) ? 0 : t + 1;
return out;
}
for (std::size_t t = 0; t < start; ++t) // [0, start); t+1 <= start < N
if (auto out = does_index_emit(t)) {
rr_ = t + 1;
return out;
}
std::unique_ptr<Frame> out = Frame::acquire();
if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
uint32_t val;
xQueueReceive(emit_requests_, &val, portMAX_DELAY);
const uint8_t index = val & 0xFF;
auto emit_d = [val](auto &d, auto &out) { d.emit(out, val >> 8); };
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
((Is == index ? originate(std::get<Is>(devices_), *out, emit_d) : void()),
...);
}(std::index_sequence_for<Devs...>{});
return out;
}
private:
// Dev's position in Devs
template <class Dev> static constexpr uint8_t index_of() {
constexpr std::array is_dev{std::is_same_v<Dev, Devs>...};
return std::ranges::find(is_dev, true) - is_dev.begin();
}
template <Party P> void stamp(Frame &out) const {
if constexpr (P == Sender)
out.controller_addr = address_;
@@ -268,5 +264,6 @@ private:
uint16_t controller_ = 0;
const uint16_t address_;
std::tuple<Devs...> devices_;
QueueHandle_t emit_requests_;
};
} // namespace avclan
@@ -12,6 +12,11 @@ target_sources(avclan PRIVATE
)
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)
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.
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
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
@@ -137,6 +139,7 @@ target_compile_definitions(jnk0le_uart
PRIVATE F_CPU=${FREQSEL}
)
target_compile_options(jnk0le_uart PUBLIC
$<$<CONFIG:Debug>:-Og>
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
target_link_libraries(avclan PUBLIC jnk0le_uart)
@@ -148,7 +151,6 @@ target_compile_definitions(avclan PUBLIC
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
TCB_CLKSEL=${TCB_CLKSEL}
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
)
target_compile_options(avclan PUBLIC
--param=min-pagesize=0
+198 -20
View File
@@ -9,6 +9,7 @@
#include <stdint.h>
#include <util/atomic.h>
#include "avclan.h"
#include "hal/cd_timer.h" // statustimer_enable/disable (guard)
#include "hal/phy.h"
#include "media_avr.h" // media_sync_during_mask (guard)
@@ -17,6 +18,9 @@
// avr-libc.
#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
// decide whether to resume the TX drain (DRE interrupt) on leave.
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);
}
// 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).
bool phy_active() { return (!BUS_IS_IDLE) != 0; }
bool phy_frame_pending() { return (!BUS_IS_IDLE) != 0; }
// Mute device TX on AVCLAN bus
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) {};
}
void phy_send_bit(Bit bit) {
static void phy_send_bit(Bit bit) {
uint16_t zero_length, one_length;
switch (bit) {
case bit_zero:
@@ -135,7 +143,7 @@ void phy_send_bit(Bit bit) {
set_AVC_logic_for(1, one_length);
}
void phy_send_ack() {
static void phy_send_ack() {
TCB1.CNT = 0;
// Wait for controller to begin ACK bit
@@ -149,7 +157,7 @@ void phy_send_ack() {
phy_send_bit(bit_zero);
}
Send phy_read_ack() {
static Send phy_read_ack() {
TCB1.CNT = 0; // Double reset of TCB1.CNT: here
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
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
while (!BUS_IS_IDLE) {
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
return NAK;
return NAK_TOO_LONG;
}
return (Send)0;
}
// 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 parity = 0;
int8_t len_mod8 = 8;
@@ -189,17 +197,19 @@ Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
b <<= 1;
}
len_mod8 = 8;
b = *--bits;
// Avoid under-read for last byte
if (len > 0)
b = *--bits;
}
return (parity & 1);
}
// 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);
}
Bit phy_send_byte(const uint8_t *byte) {
static Bit phy_send_byte(const uint8_t *byte) {
uint8_t b = *byte;
uint8_t parity = 0;
@@ -238,7 +248,7 @@ ISR(TCB0_INT_vect) {
}
// 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;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
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
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;
if (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
Bit phy_read_byte(uint8_t *byte) {
static Bit phy_read_byte(uint8_t *byte) {
uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0;
@@ -304,7 +314,16 @@ Bit phy_read_byte(uint8_t *byte) {
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
PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
// Disable input buffer; recommended when using AC
@@ -339,7 +358,7 @@ void phy_init() {
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
// was ~recently detected.
// 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
// 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
TCB1.CNT = 0;
while (BUS_IS_IDLE) {
@@ -459,6 +478,144 @@ Send phy_send_startbit() {
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)
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
enabled. */
@@ -478,12 +635,31 @@ void phy_guard_leave() {
}
}
#if !defined(NDEBUG) && defined(MEASURE_BUS)
#include <stdio.h> // phy_measure() reporting (debug builds only)
#ifndef NDEBUG
// Only used immediately below
#define XSTR(x) #x
#define STR(x) XSTR(x)
// 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)
// Only used immediately below
#define XSTR(x) #x
#define STR(x) XSTR(x)
static uint16_t pulses[100];
static uint16_t periods[100];
@@ -516,4 +692,6 @@ void phy_measure() {
phy_guard_leave();
}
#endif
#endif
+79
View File
@@ -0,0 +1,79 @@
pico_sdk_init()
include_directories(
${CMAKE_CURRENT_LIST_DIR}
include
)
FetchContent_Declare( freertos_kernel
EXCLUDE_FROM_ALL
GIT_REPOSITORY https://github.com/FreeRTOS/FreeRTOS-Kernel.git
GIT_TAG 3a22924e0a9ddbbc8b0758881c33b3422a5cc20d # v11.3.1
)
add_library(freertos_config INTERFACE)
target_include_directories(freertos_config SYSTEM
INTERFACE
include
)
target_compile_definitions(freertos_config
INTERFACE
projCOVERAGE_TEST=0
)
target_compile_options(freertos_config
INTERFACE
$<$<COMPILE_LANGUAGE:CXX>:-Wno-volatile>
)
set( FREERTOS_PORT "GCC_RP2040" CACHE STRING "" FORCE)
FetchContent_MakeAvailable(freertos_kernel)
set(FREERTOS_KERNEL_PATH "${freertos_kernel_SOURCE_DIR}" CACHE STRING "" FORCE)
include(${freertos_kernel_SOURCE_DIR}/portable/ThirdParty/GCC/RP2040/FreeRTOS_Kernel_import.cmake)
target_sources(avclan PRIVATE
phy.cc
media.cc
board.cc
stdio.cc
)
pico_generate_pio_header(avclan ${CMAKE_CURRENT_LIST_DIR}/iebus.pio)
target_link_libraries(avclan PUBLIC
hardware_pio
hardware_dma
pico_stdlib
pico_stdio_usb
pico_multicore
pico_async_context_freertos
freertos_config
FreeRTOS-Kernel-Heap4
)
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` 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
${avclan-mockingboard_SOURCE_DIR}/src/sniffer.cc
)
target_link_libraries(mockingboard 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
PROPERTIES COMPILE_OPTIONS "-w")
unset(_tinyusb_sources)
pico_add_extra_outputs(mockingboard)
pico_set_program_description(mockingboard
"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
"${avclan-mockingboard_VERSION}")
pico_set_program_url(mockingboard "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() {}
+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 | 5 bits | 1 bit | 22 - N bits |
; | Length (N) | Data+P | Next | NAK ok | padding |
;
; Parity is generated by the driver and appended as the last (least significant)
; bit of the data field. "Next" should be the address of `reset` or `ack_drive`
.in 1 left
.out 1 left
.define public nak_irq 7
.define public lost_arb_irq 6
.define public eof_irq 3 ; An acknowledge slot is over
;;; 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 [11] ; 15 cycles from the release to `set pins, 0` below
; (~AVCLAN_BIT0_LOGIC_1)
irq set eof_irq
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 pc, 5 [1] ; "Next": reset, or ack_drive to read ACK
;;; 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:
jmp send_bit [3] ; 15 cycles from bit_end to `set pins, 0`, the same
; as the last bit's path through `out pc` and reset
;;; 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
@@ -0,0 +1,161 @@
/*
* FreeRTOS V202111.00
* Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* http://www.FreeRTOS.org
* http://aws.amazon.com/freertos
*
* 1 tab == 4 spaces!
*/
#ifndef FREERTOS_CONFIG_EXAMPLES_COMMON_H
#define FREERTOS_CONFIG_EXAMPLES_COMMON_H
/*-----------------------------------------------------------
* Application specific definitions.
*
* These definitions should be adjusted for your particular hardware and
* application requirements.
*
* THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
* FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
*
* See http://www.freertos.org/a00110.html
*----------------------------------------------------------*/
/* Scheduler Related */
#define configUSE_PREEMPTION 1
#define configUSE_TICKLESS_IDLE 0
#define configCPU_CLOCK_HZ (150000000UL)
#define configTICK_RATE_HZ ((TickType_t)1000)
#define configMAX_PRIORITIES 32
#define configMINIMAL_STACK_SIZE (configSTACK_DEPTH_TYPE)512
#define configIDLE_SHOULD_YIELD 1
#define configUSE_16_BIT_TICKS 0
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
/* Synchronization Related */
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_APPLICATION_TASK_TAG 0
#define configUSE_COUNTING_SEMAPHORES 1
#define configQUEUE_REGISTRY_SIZE 8
#define configUSE_QUEUE_SETS 1
#define configUSE_TIME_SLICING 1
#define configUSE_NEWLIB_REENTRANT 0
#define configENABLE_BACKWARD_COMPATIBILITY 0
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
/* System */
#define configSTACK_DEPTH_TYPE uint32_t
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
/* Memory allocation related definitions. */
#ifndef configSUPPORT_STATIC_ALLOCATION
#define configSUPPORT_STATIC_ALLOCATION 0
#endif
#ifndef configSUPPORT_DYNAMIC_ALLOCATION
#define configSUPPORT_DYNAMIC_ALLOCATION 1
#endif
#define configTOTAL_HEAP_SIZE (128 * 1024)
#define configAPPLICATION_ALLOCATED_HEAP 0
/* Hook function related definitions. */
#define configCHECK_FOR_STACK_OVERFLOW 0
#define configUSE_MALLOC_FAILED_HOOK 0
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
/* Run time and task stats gathering related definitions. */
#define configGENERATE_RUN_TIME_STATS 0
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 0
/* Co-routine related definitions. */
#define configUSE_CO_ROUTINES 0
#define configMAX_CO_ROUTINE_PRIORITIES 1
/* Software timer related definitions. */
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 1024
/* Interrupt nesting behaviour configuration. */
/*
#define configKERNEL_INTERRUPT_PRIORITY [dependent of processor]
#define configMAX_SYSCALL_INTERRUPT_PRIORITY [dependent on processor and
application] #define configMAX_API_CALL_INTERRUPT_PRIORITY [dependent on
processor and application]
*/
#if FREE_RTOS_KERNEL_SMP // set by the RP2xxx SMP port of FreeRTOS
/* SMP port only */
#ifndef configNUMBER_OF_CORES
#define configNUMBER_OF_CORES 2
#endif
#define configNUM_CORES configNUMBER_OF_CORES
#define configTICK_CORE 0
#define configRUN_MULTIPLE_PRIORITIES 1
#if configNUMBER_OF_CORES > 1
#define configUSE_CORE_AFFINITY 1
#endif
#define configUSE_PASSIVE_IDLE_HOOK 0
#endif
/* RP2040 specific */
#define configSUPPORT_PICO_SYNC_INTEROP 1
#define configSUPPORT_PICO_TIME_INTEROP 1
#include <assert.h>
/* Define to trap errors during development. */
#define configASSERT(x) assert(x)
/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_uxTaskPriorityGet 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_vTaskDelay 1
#define INCLUDE_xTaskDelayUntil 1
#define INCLUDE_xTaskGetSchedulerState 1
#define INCLUDE_xTaskGetCurrentTaskHandle 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#define INCLUDE_xTaskGetIdleTaskHandle 1
#define INCLUDE_eTaskGetState 1
#define INCLUDE_xTimerPendFunctionCall 1
#define INCLUDE_xTaskAbortDelay 1
#define INCLUDE_xTaskGetHandle 1
#define INCLUDE_xTaskResumeFromISR 1
#define INCLUDE_xQueueGetMutexHolder 1
#if PICO_RP2350
#define configENABLE_MPU 0
#define configENABLE_TRUSTZONE 0
#define configRUN_FREERTOS_SECURE_ONLY 1
#define configENABLE_FPU 1
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 16
#endif
/* A header file that defines trace macro can be included here. */
#endif /* FREERTOS_CONFIG_H */
+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 false; }
#endif
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#include "FreeRTOS.h" // IWYU pragma: export
#include "semphr.h"
#include "hal/stdio.h"
#include "pico/stdio.h"
#include "pico/stdio_usb.h"
#include "pico/time.h"
#include "tusb.h" // IWYU pragma: export
// 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);
}
SemaphoreHandle_t rx_ready = nullptr;
// Called from the stdio_usb background IRQ after each tud_task() with RX data
// pending.
void on_rx([[maybe_unused]] void *param) {
BaseType_t woken = false;
xSemaphoreGiveFromISR(rx_ready, &woken);
portYIELD_FROM_ISR(woken);
}
} // namespace
extern "C" void stdio_init() {
rx_ready = xSemaphoreCreateBinary();
stdio_usb_init();
stdio_set_chars_available_callback(on_rx, nullptr);
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 busy-waits.
extern "C" int _read(int handle, char *buffer, int length) {
if (handle != STDIN_FILENO) {
errno = EBADF;
return -1;
}
while (true) {
const int count = stdio_get_until(buffer, length, make_timeout_time_us(0));
if (count > 0)
return count;
xSemaphoreTake(rx_ready, portMAX_DELAY);
}
}
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;
}
+22 -107
View File
@@ -3,124 +3,39 @@
#pragma once
#include <array>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <type_traits>
namespace detail {
template <class T, auto N> struct Deleter;
}
template <class T, std::integral auto N, bool Owning = false,
class Deleter = std::default_delete<T>>
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max() &&
// push() drops the passed-in deleter and pop() fabricates a fresh
// one, so a deleter must either derive its state from the queue
// itself (the pool Deleter) or carry no state at all
(std::is_same_v<Deleter, detail::Deleter<T, N>> ||
(std::is_empty_v<Deleter> &&
std::is_nothrow_default_constructible_v<Deleter>)))
class Queue {
friend detail::Deleter<T, N>;
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
// Owning FIFO of `T`s, safe to share between tasks
template <class T> class Queue {
public:
// Only empty construction is allowed for non-Owning, non-pool deleters
constexpr Queue()
requires(!Owning && !std::is_same_v<Deleter, detail::Deleter<T, N>>)
: owner{nullptr} {}
explicit Queue(UBaseType_t length)
: handle{xQueueCreate(length, sizeof(T *))} {
configASSERT(handle);
}
Queue(const Queue &) = delete;
// Moving is unsupported due to being self-referential
Queue(Queue &&) = delete;
Queue &operator=(Queue &&) = delete;
// Construct from pre-defined storage
constexpr Queue(T (&items)[N])
requires(Owning) && std::same_as<Deleter, detail::Deleter<T, N>>
: owner{this}, write{N} {
for (uint8_t i = 0; i < N; ++i)
buf[i] = &items[i];
}
// Construct non-Owning empty from Owning queue
constexpr Queue(Queue<T, N, true, Deleter> &queue)
requires(!Owning)
: owner{&queue} {}
bool isEmpty() const { return write == read; }
uint8_t size() const { return write - read; }
uint8_t capacity() const { return N; }
bool isFull() const { return size() == capacity(); }
uint8_t push(std::unique_ptr<T, Deleter> x)
requires(!Owning)
{
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) {
// structurally unnecessary for detail::Deleter, where empty construction
// is only from same size parent
if (isFull())
return 1;
}
if (!x)
return 1;
if constexpr (std::is_same_v<Deleter, detail::Deleter<T, N>>) {
if (!x.get_deleter().is_owned_by(owner))
return 1;
}
claim(x.release());
return 0;
// Blocks until accepted (INCLUDE_vTaskSuspend: portMAX_DELAY never times out)
void push(std::unique_ptr<T> x) {
T *ptr = x.release();
xQueueSend(handle, &ptr, portMAX_DELAY);
}
const T *peek() const {
if (isEmpty())
return nullptr;
return buf[mask(read)];
std::unique_ptr<T> pop(TickType_t wait = portMAX_DELAY) {
T *ptr = nullptr;
xQueueReceive(handle, &ptr, wait);
return std::unique_ptr<T>(ptr);
}
std::unique_ptr<T, Deleter> pop() {
if (isEmpty())
return nullptr;
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>)
return {buf[mask(read++)], Deleter{}};
else if constexpr (Owning)
return {buf[mask(read++)], Deleter{this}};
else
return {buf[mask(read++)], Deleter{owner}};
// The front item stays owned by the queue
const T *peek(TickType_t wait = portMAX_DELAY) const {
T *ptr = nullptr;
xQueuePeek(handle, &ptr, wait);
return ptr;
}
private:
uint8_t mask(uint8_t pos) const { return pos & (N - 1); }
void claim(T *x) { buf[mask(write++)] = x; }
std::array<T *, N> buf = {};
Queue<T, N, true, Deleter> *const owner;
uint8_t read = 0;
uint8_t write = 0;
QueueHandle_t handle;
};
template <class T, auto N, class D>
Queue(Queue<T, N, true, D> &) -> Queue<T, N, false, D>;
template <class T, auto N>
Queue(T (&items)[N]) -> Queue<T, N, true, detail::Deleter<T, N>>;
namespace detail {
template <class T, auto N> struct Deleter {
Deleter() = default;
constexpr Deleter(Queue<T, N, true, Deleter> *owner) : owner{owner} {}
void operator()(T *x) const { owner->claim(x); }
constexpr bool is_owned_by(const Queue<T, N, true, Deleter> *parent) const {
return owner == parent;
}
private:
Queue<T, N, true, Deleter> *const owner = nullptr;
};
} // namespace detail
+130 -50
View File
@@ -7,9 +7,11 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "cdchanger.hpp"
#include "frame.hpp"
#include "hal/board.h"
@@ -18,17 +20,30 @@
#include "queue.hpp"
#include "stdshim.hpp"
const char *const offon[] = {"OFF", "ON"};
constexpr uint8_t CACHE_SIZE = 16;
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
using namespace avclan;
namespace {
constinit Queue<Frame, CACHE_SIZE> incoming;
constinit Queue<Frame, CACHE_SIZE> outgoing;
using Periph = Peripheral<CDChanger>;
const char *const offon[] = {"OFF", "ON"};
constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE;
// A queue that can hold the entire Frame pool is never full, so the sender's
// blocking push of a follow-up frame onto its own queue can't deadlock
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
// Must be ranked such that queues deterministically tend to empty
constexpr UBaseType_t SendPriority = tskIDLE_PRIORITY + 3;
constexpr UBaseType_t RoutePriority = tskIDLE_PRIORITY + 2;
constexpr UBaseType_t PollPriority = tskIDLE_PRIORITY + 2;
constexpr UBaseType_t ReceivePriority = tskIDLE_PRIORITY + 1;
struct Context {
Periph &peripheral;
Queue<Frame> &incoming;
Queue<Frame> &outgoing;
};
uint8_t hexChars[2];
uint8_t hexDigit = 0; // current digit being written to hexChars
@@ -61,51 +76,60 @@ void set_flag(bool *flag, bool val, const char *msg) {
void Setup();
void print_help();
} // namespace
int main() {
Bus phy;
using enum Action;
using enum Device;
Peripheral<CDChanger> peripheral(phy, 0x360);
[[noreturn]] void vReceiverTask(void *pvParameters) {
using Print = Frame::Print;
Setup();
print_help();
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (peripheral.bus_is_active()) {
if (auto msg = peripheral.read(Print{.print = printAllFrames,
.binary = printBinary,
.verbose = verbose}))
incoming.push(std::move(*msg));
}
if (auto msg = peripheral.read(Print{.print = printAllFrames,
.binary = printBinary,
.verbose = verbose}))
incoming.push(std::move(*msg));
}
}
if (const auto *in = incoming.peek()) {
if (auto resp = peripheral.route(*in)) {
incoming.pop();
if (*resp) {
outgoing.push(std::move(*resp));
continue; // route can be long; re-check the bus before poll/send
}
}
[[noreturn]] void vRoutingTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
const Frame *in = incoming.peek();
if (auto resp = peripheral.route(*in)) {
incoming.pop();
if (*resp)
outgoing.push(std::move(*resp));
}
}
}
[[noreturn]] void vPollTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (auto msg = peripheral.poll())
outgoing.push(std::move(msg));
}
}
if (auto out = outgoing.pop()) {
auto result =
peripheral.send(std::move(out), Print{.print = printAllFrames,
.binary = printBinary});
if (auto next = peripheral.react(std::move(result)))
outgoing.push(std::move(next));
[[noreturn]] void vSenderTask(void *pvParameters) {
using Print = Frame::Print;
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
auto result = peripheral.send(
outgoing.pop(), Print{.print = printAllFrames, .binary = printBinary});
if (auto next = peripheral.react(std::move(result)))
outgoing.push(std::move(next));
}
}
continue;
}
// stdin must be non-blocking: yielding EOF when idle/empty
if (int readkey = getchar(); readkey != EOF) {
[[noreturn]] void vREPLTask(void *pvParameters) {
using enum Action;
using enum Device;
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (int readkey = fgetc(stdin); readkey != EOF) {
switch (readkey) {
case '?': print_help(); break;
case 'v': toggle_flag(&verbose, "Verbose errors:"); break;
@@ -123,7 +147,7 @@ int main() {
case 'x': set_flag(&printBinary, false, "Binary:"); break;
case 'E': // Beep
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (auto out = Frame::acquire()) {
out->is_unicast = true;
out->peripheral_addr = peripheral.controller();
{
@@ -138,7 +162,7 @@ int main() {
puts("!! failed Frame alloc for Beep !! ");
break;
case 'P':
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (auto out = Frame::acquire()) {
out->is_unicast = true;
out->peripheral_addr = peripheral.controller();
{
@@ -180,6 +204,16 @@ int main() {
while (peripheral.device<CDChanger>().media_busy()) {}
puts("end");
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
case 'M': peripheral.get_bus().measure(); break;
#endif
@@ -211,12 +245,23 @@ int main() {
hexChars[0] = hexChars[1] = 0;
seqIsUnicast = false;
break;
case '\r': // Eat carriage return too
if (readBinary)
goto DEFAULT;
[[fallthrough]];
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 (readBinary) {
if (data_tmp[seqIdx - 1] == 0x17) {
if (auto out =
std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (auto out = Frame::acquire()) {
if (out->parse(data_tmp, --seqIdx) ==
Frame::Error::Parse{0}) {
out->reaction = 1;
@@ -229,7 +274,7 @@ int main() {
goto DEFAULT; // reading binary and this is a real data byte;
// fall through to default
} else { // ASCII message
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (auto out = Frame::acquire()) {
const uint8_t sendLen =
seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH;
out->is_unicast = seqIsUnicast;
@@ -270,6 +315,10 @@ int main() {
if (readBinary) {
data_tmp[seqIdx++] = readkey;
} else {
// Only take valid hex digits
if (isxdigit(readkey) == 0)
break;
hexChars[hexDigit++] = readkey;
if (hexDigit == 2) {
@@ -294,9 +343,38 @@ int main() {
}
}
} // switch (readkey)
} else {
clearerr(stdin);
} // if (readkey != EOF)
}
return 0;
}
} // namespace
int main() {
Setup();
print_help();
// Static: the scheduler reclaims main's stack for ISRs
static Bus phy;
static Periph peripheral(phy, 0x360);
static Queue<Frame> incoming(CACHE_SIZE);
static Queue<Frame> outgoing(CACHE_SIZE);
static Context ctx{
.peripheral = peripheral, .incoming = incoming, .outgoing = outgoing};
xTaskCreateAffinitySet(vSenderTask, "send/react", configMINIMAL_STACK_SIZE,
&ctx, SendPriority, 0b01, nullptr);
xTaskCreateAffinitySet(vRoutingTask, "router", configMINIMAL_STACK_SIZE, &ctx,
RoutePriority, 0b01, nullptr);
xTaskCreateAffinitySet(vPollTask, "poll", configMINIMAL_STACK_SIZE, &ctx,
PollPriority, 0b01, nullptr);
xTaskCreateAffinitySet(vReceiverTask, "receiver", configMINIMAL_STACK_SIZE,
&ctx, ReceivePriority, 0b01, nullptr);
xTaskCreateAffinitySet(vREPLTask, "repl", configMINIMAL_STACK_SIZE, &ctx,
tskIDLE_PRIORITY, 0b01, nullptr);
vTaskStartScheduler();
}
namespace {
@@ -324,6 +402,8 @@ void print_help() {
"s - MIC skip forward\n"
"b - MIC skip backward\n"
"M - Measure bit-timing (pulse-widths and periods)\n"
"+ - Set bus driven/dominant\n"
"- - Set bus idle/recessive\n"
#endif
"? - Print this message");
}