13 Commits

Author SHA1 Message Date
Allen Hill d50964a26d Add debug method to manually set controller address (e.g. for bench testing) 2026-09-28 11:30:34 -07:00
Allen Hill 74d6bf36f1 Offload TX data section to DMA (releases up to 12.5ms back to RTOS) 2026-09-27 21:01:32 -07:00
Allen Hill 90e54b0896 Migrate to FreeRTOS 2026-09-22 20:34:38 -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, -readability-function-cognitive-complexity,
-misc-non-private-member-variables-in-classes -misc-non-private-member-variables-in-classes
WarningsAsErrors: "" WarningsAsErrors: ""
ExcludeHeaderFilterRegex: 'out/build' ExcludeHeaderFilterRegex: "out/build"
HeaderFilterRegex: '^src/.*' HeaderFilterRegex: "^src/.*"
SystemHeaders: false SystemHeaders: false
# AnalyzeTemporaryDtors: false # AnalyzeTemporaryDtors: false
FormatStyle: none FormatStyle: none
CheckOptions: CheckOptions:
- key: modernize-deprecated-headers.CheckHeaderFile
value: "false"
- key: readability-magic-numbers.IgnorePowersOf2IntegerValues - key: readability-magic-numbers.IgnorePowersOf2IntegerValues
value: "true" value: "true"
- key: readability-magic-numbers.IgnoredIntegerValues - key: readability-magic-numbers.IgnoredIntegerValues
+4
View File
@@ -8,3 +8,7 @@ CompileFlags:
- -ffixed-r2 - -ffixed-r2
- -ffixed-r3 - -ffixed-r3
- -ffixed-r4 - -ffixed-r4
- -fzero-init-padding-bits
- -fno-malloc-dce
Add:
- -Wno-missing-field-initializers
+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_C_STANDARD 23)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
add_compile_options(
-Wall -Wswitch-enum -Werror
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13")
@@ -52,15 +61,6 @@ if(INSTALL_GIT_HOOKS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
endif() endif()
endif() endif()
# Hardware target (port) selection. The chosen target/<name>/ directory supplies
# the port implementation sources, the per-target include path, and all
# hardware-specific build configuration (compile defs/options, device-pack
# handling, flashing) via its own CMakeLists, pulled in with add_subdirectory
# below. The matching cross-compiler is selected separately by a toolchain file
# (e.g. cmake/avr-gcc-toolchain.cmake) named in a CMake preset. Adding a new
# board = a sibling target/ directory + a toolchain file + a preset wiring them.
set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build")
# The AVC-LAN stack as a static library: the target-agnostic generic core (no # The AVC-LAN stack as a static library: the target-agnostic generic core (no
# <avr/...>, no register access). The selected target's port sources and flags # <avr/...>, no register access). The selected target's port sources and flags
# are contributed by its subdirectory. # are contributed by its subdirectory.
@@ -70,23 +70,45 @@ add_library(avclan STATIC
src/avclan/bus.cc src/avclan/bus.cc
) )
target_compile_definitions(avclan PUBLIC
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
AVCLAN_MSG_QUEUE_SIZE=${AVCLAN_MSG_QUEUE_SIZE}
)
target_compile_options(avclan PUBLIC
-Wall -Wswitch-enum -Werror
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
# avclan exports its public generic headers (src/avclan) to consumers and # avclan exports its public generic headers (src/avclan) to consumers and
# reaches into src/ for sibling headers (stdio.h, board.h) during its own # reaches into src/ for sibling headers (stdio.h, board.h) during its own
# build. The selected target adds its own per-target include path. # build. The selected target adds its own per-target include path.
target_include_directories(avclan PUBLIC target_include_directories(avclan PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/avclan) ${CMAKE_CURRENT_SOURCE_DIR}/src/avclan)
add_executable(mockingboard # Pull in the selected hardware target: its port sources, per-target headers,
src/sniffer.cc # hardware-specific compile options/definitions, device-pack handling, and the
) # flashing target. Added after the targets above so it can extend them.
target_link_libraries(mockingboard avclan) 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 # 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 # 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: # 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. # custom targets with no output are always considered stale and re-run.
# add_dependencies orders it after the link. # add_dependencies orders it after the link.
if(CMAKE_SIZE) if(TARGET mockingboard AND CMAKE_SIZE)
add_custom_target(mockingboard-size ALL add_custom_target(mockingboard-size ALL
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard> COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard>
COMMENT "Section sizes: mockingboard firmware" COMMENT "Section sizes: mockingboard firmware"
@@ -95,18 +117,12 @@ if(CMAKE_SIZE)
add_dependencies(mockingboard-size mockingboard) add_dependencies(mockingboard-size mockingboard)
endif() 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 ------------- # --- Polyfill headers for std facilities the toolchain may lack -------------
# tl::expected / tl::optional back-fill std::expected / std::optional (with # 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 # 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. # CMAKE_REQUIRED_INCLUDES, if necessary.
include(CheckCXXSourceCompiles) include(CheckCXXSourceCompiles)
include(FetchContent)
find_program(GIT_EXECUTABLE git REQUIRED) find_program(GIT_EXECUTABLE git REQUIRED)
if(TARGET libstdcpp) if(TARGET libstdcpp)
+46 -10
View File
@@ -1,6 +1,24 @@
{ {
"version": 3, "version": 9,
"configurePresets": [ "configurePresets": [
{
"name": "debug-base",
"hidden": true,
"description": "Debug build settings",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "relwithdebinfo-base",
"hidden": true,
"description": "RelWithDebInfo build settings",
"inherits": "debug-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{ {
"name": "usb0", "name": "usb0",
"hidden": true, "hidden": true,
@@ -31,20 +49,38 @@
} }
}, },
{ {
"name": "debug-base", "name": "pico2",
"hidden": true, "hidden": true,
"description": "Debug build settings", "binaryDir": "${sourceDir}/out/build/pico2",
"inherits": "debug-base",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug" "AVCLAN_TARGET": "pico2",
"AVCLAN_FRAME_POOL_N": "14",
"AVCLAN_TARGET_SDK_FILE": "${sourceDir}/cmake/pico_sdk_import.cmake",
"PICO_SDK_FETCH_FROM_GIT": true
},
"environment": {
"PICO_PLATFORM": "rp2350-arm-s",
"PICO_BOARD": "pimoroni_pico_plus2_w_rp2350"
} }
}, },
{ {
"name": "relwithdebinfo-base", "name": "pico2-gcc",
"hidden": true, "displayName": "Pico 2W (GCC)",
"description": "RelWithDebInfo build settings", "inherits": "pico2",
"inherits": "debug-base", "description": "",
"cacheVariables": { "generator": "Ninja",
"CMAKE_BUILD_TYPE": "RelWithDebInfo" "environment": {
"PICO_COMPILER": "pico_arm_gcc"
}
},
{
"name": "pico2-clang",
"displayName": "Pico 2W (Clang)",
"inherits": "pico2",
"description": "",
"environment": {
"PICO_COMPILER": "pico_arm_clang"
} }
}, },
{ {
+121
View File
@@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})
+4 -6
View File
@@ -147,6 +147,7 @@ struct Error {
STARTBIT_MALFORMED, STARTBIT_MALFORMED,
STARTBIT_TOO_LONG, STARTBIT_TOO_LONG,
BAD_STARTBIT, BAD_STARTBIT,
NO_FRAME,
POOL_EMPTY, // non-bus error POOL_EMPTY, // non-bus error
}; };
@@ -155,7 +156,10 @@ struct Error {
NAK_MESSAGE_LENGTH, NAK_MESSAGE_LENGTH,
NAK_CONTROL, NAK_CONTROL,
NAK_ADDRESS, NAK_ADDRESS,
NAK_TOO_LONG,
NAK, // generic NAK has max severity NAK, // generic NAK has max severity
CONTENDED_BUS,
LOST_ARBITRATION,
BUSY, BUSY,
MUTED, MUTED,
}; };
@@ -176,12 +180,6 @@ struct SendError {
}; };
#endif #endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t {
bit_zero = 0x00,
bit_one = 0x01,
bit_start = 0x10
};
#ifdef __cplusplus #ifdef __cplusplus
} // namespace detail } // namespace detail
} // namespace avclan } // namespace avclan
+143 -204
View File
@@ -26,13 +26,15 @@
| 1 │ *Acknowledge* | 1 │ *Acknowledge*
*repeat `n` times* *repeat `n` times*
No acknowledge bits are sent for broadcast frames. For broadcast frames the acknowledge (one) bit is sent, but an ACK response
(zero) is not expected.
*/ */
#include <concepts> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <new>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
@@ -43,16 +45,12 @@
namespace { namespace {
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send; using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
struct trailer_bits_t {}; // The bus spec has a unit that loses arbitration retry rather than fail: "if
struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit) // the unit loses in arbitration, the frame is automatically reset up twice
struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address) // (three times in total)". Only an attempt that is outbid every time is an
struct with_ack_t : trailer_bits_t { // error worth reporting.
}; // bits + parity + ACK slot (all other fields) constexpr uint8_t SEND_ATTEMPTS = 3;
inline constexpr no_parity_t no_parity{};
inline constexpr with_parity_t with_parity{};
inline constexpr with_ack_t with_ack{};
} // namespace } // namespace
namespace avclan { namespace avclan {
@@ -71,222 +69,136 @@ public:
Handle(const Handle &) = delete; Handle(const Handle &) = delete;
Handle(Handle &&) = delete; Handle(Handle &&) = delete;
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // Forward to phy API (organized so that hal/phy.h isn't public/visible at the
Send sendstartbit() { return phy_send_startbit(); }; // C++/library level)
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTBEGIN(readability-convert-member-functions-to-static)
Read readstartbit() { return phy_read_startbit(); }; Read read_header(bool *is_unicast) { return phy_read_header(is_unicast); };
Read read_controller_addr(uint16_t *addr) {
template <auto N, std::unsigned_integral T, return phy_read_controller_addr(addr);
std::derived_from<trailer_bits_t> Trailer> };
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>) Read read_peripheral_addr(uint16_t *addr) {
Send send(T bits, Trailer /*tag*/) { return phy_read_peripheral_addr(addr);
const Bit parity = sendbits<N>(bits); };
Read read_control(uint8_t *control) { return phy_read_control(control); };
if constexpr (std::is_same_v<Trailer, with_parity_t>) Read read_length(uint8_t *length) { return phy_read_length(length); };
sendbits<1>(to_underlying(parity)); Read read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
return phy_read_data(data, length, data_index);
return Send{0};
}; };
template <auto N, std::unsigned_integral T> Send send_header(bool is_unicast) { return phy_send_header(is_unicast); };
requires(sizeof(T) < 3 && N < 16) Send send_controller_addr(uint16_t addr) {
Send send(T bits, with_ack_t /*tag*/, bool expect_ack) { return phy_send_controller_addr(addr);
send<N>(bits, with_parity);
if (expect_ack)
return read_ACK();
sendbits<1>(1U); // still need to fill the ack bit slot
return Send{0};
}; };
Send send_peripheral_addr(uint16_t addr, bool expect_ack) {
template <auto N, std::unsigned_integral T, return phy_send_peripheral_addr(addr, expect_ack);
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_control(uint8_t control, bool expect_ack) {
template <auto N, std::unsigned_integral T, class F> return phy_send_control(control, expect_ack);
requires(sizeof(T) < 3 && N < 16)
Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
if (read<N>(bits, with_parity) == Read::BAD_PARITY)
return Read::BAD_PARITY;
if (ack()) {
send_ACK();
} else {
uint8_t slot;
readbits<1>(&slot);
}
return Read{0};
}; };
template <auto N, std::unsigned_integral T> Send send_length(uint8_t length, bool expect_ack) {
requires(sizeof(T) < 3 && N < 16) return phy_send_length(length, expect_ack);
Read read(T *bits, with_ack_t /*tag*/, bool ack) {
return read<N>(bits, with_ack, [=]() { return ack; });
}
private:
static void send_ACK() { phy_send_ack(); };
static Send read_ACK() { return phy_read_ack(); };
template <auto N, class T> Bit sendbits(T bits);
template <auto N, class T> Bit readbits(T *bits);
template <auto N>
requires(N > 1 && N < 8)
Bit sendbits(uint8_t bits) {
return phy_send_bits_u8(&bits, N);
}; };
template <auto N> Send send_data(const uint8_t *data, uint8_t length, bool expect_ack,
requires(N <= 16) uint8_t *data_index) {
Bit sendbits(uint16_t bits) { return phy_send_data(data, length, expect_ack, data_index);
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));
}; };
// NOLINTEND(readability-convert-member-functions-to-static)
}; };
template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) { void Bus::init(uint16_t address) {
return phy_send_byte(&bits);
};
template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) {
const Bit bit{static_cast<Bit>(bits & 1U)};
phy_send_bit(bit);
return bit;
};
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
return phy_read_byte(bits);
};
void Bus::init() {
// Idempotent: the single Bus is shared by reference, so every Peripheral's // Idempotent: the single Bus is shared by reference, so every Peripheral's
// ctor calls init() on it — but the hardware must be brought up exactly once // ctor calls init() on it — but the hardware must be brought up exactly once
// (phy_init is not assumed re-entrant/idempotent). // (phy_init is not assumed re-entrant/idempotent).
if (inited_) if (inited_)
return; return;
phy_init(); phy_init(address);
muted_ = false; // phy_init leaves the bus TX unmuted muted_ = false; // phy_init leaves the bus TX unmuted
inited_ = true; inited_ = true;
}; };
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
bool Bus::is_active() const { return phy_active(); };
void Bus::mute(bool mute) { void Bus::mute(bool mute) {
phy_mute(mute); phy_mute(mute);
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
}; };
// 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> { -> expected<std::unique_ptr<Frame>, Error::Read> {
struct errtype { struct errtype {
Read errno; Read type;
uint16_t val; uint16_t val;
} err = {}; } err = {};
using enum Read; using enum Read;
std::unique_ptr<Frame> in(new (std::nothrow) Frame); std::unique_ptr<Frame> in = Frame::acquire();
if (!in) { if (!in) {
err.errno = POOL_EMPTY; err.type = POOL_EMPTY;
goto handle_err; goto handle_err;
} }
phy_wait_frame(portMAX_DELAY);
{ // bound handle lifetime { // bound handle lifetime
auto handle = get(); auto handle = get();
bool shouldACK = false; err.type = handle.read_header(&in->is_unicast);
uint8_t tmp = 0; if (err.type != Read{0})
err.errno = handle.readstartbit();
if (err.errno != Read{0})
goto handle_err; goto handle_err;
handle.read<1>(&tmp, no_parity); err.type = handle.read_controller_addr(&in->controller_addr);
in->is_unicast = (tmp != 0U); if (err.type != Read{0}) {
if (auto rerr = handle.read<12>(&in->controller_addr, with_parity);
rerr == BAD_PARITY) {
err.errno = BAD_CONTROLLER_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->controller_addr; err.val = in->controller_addr;
goto handle_err; goto handle_err;
} }
// Using lambda for delayed evaluation of peripheral_addr field err.type = handle.read_peripheral_addr(&in->peripheral_addr);
// deref, which will be written by the time the lambda is evaluated if (err.type != Read{0}) {
auto should_ack_lambda = [&]() {
shouldACK = !is_muted() && (in->peripheral_addr == address);
return shouldACK;
};
if (auto rerr =
handle.read<12>(&in->peripheral_addr, with_ack, should_ack_lambda);
rerr == BAD_PARITY) {
err.errno = BAD_PERIPHERAL_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->peripheral_addr; err.val = in->peripheral_addr;
goto handle_err; goto handle_err;
} }
if (auto rerr = handle.read<4>(&in->control, with_ack, shouldACK); err.type = handle.read_control(&in->control);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_CONTROL_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->control; err.val = in->control;
goto handle_err; goto handle_err;
} }
if (auto rerr = handle.read<8>(&in->length, with_ack, shouldACK); err.type = handle.read_length(&in->length);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_LENGTH_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->length; err.val = in->length;
goto handle_err; goto handle_err;
} } else if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.type = BAD_LENGTH_RANGE;
if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.errno = BAD_LENGTH_RANGE;
err.val = in->length; err.val = in->length;
goto handle_err; goto handle_err;
} }
for (uint8_t i = 0; i < in->length; i++) { uint8_t data_i = 0;
if (auto rerr = handle.read<8>(&in->data[i], with_ack, shouldACK); err.type = handle.read_data(in->data, in->length, &data_i);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_DATA_PARITY; if (print.verbose)
if (print.verbose) err.val = in->data[data_i];
err.val = in->data[i];
goto handle_err; goto handle_err;
}
} }
} // destroy handle } // destroy handle
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
if (err.type == NO_FRAME)
return unexpected{NO_FRAME};
fputs("ERR(read): ", stdout); fputs("ERR(read): ", stdout);
switch (err.errno) { switch (err.type) {
case POOL_EMPTY: puts("failed Frame alloc"); break; case POOL_EMPTY: puts("failed Frame alloc"); break;
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break; case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_MISSED: fputs("missed start bit", stdout); break; case STARTBIT_MISSED: fputs("missed start bit", stdout); break;
@@ -304,6 +216,7 @@ auto Bus::read(uint16_t address, Frame::Print print)
case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE; case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE;
case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break; case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break;
case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE; case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE;
case NO_FRAME:
case BAD_PARITY: case BAD_PARITY:
__builtin_unreachable(); __builtin_unreachable();
VERBOSE: VERBOSE:
@@ -315,14 +228,14 @@ auto Bus::read(uint16_t address, Frame::Print print)
} }
// Only print if some data has been correctly received // Only print if some data has been correctly received
if (print.print && (err.errno < STARTBIT_MISSED)) { if (print.print && (err.type < STARTBIT_MISSED)) {
if (err.errno > BAD_DATA_PARITY) if (err.type > BAD_DATA_PARITY)
in->length = 0; in->length = 0;
in->print(print); in->print(print);
} }
if (err.errno != Read{0}) if (err.type != Read{0})
return unexpected(err.errno); return unexpected(err.type);
return in; return in;
} }
@@ -331,79 +244,90 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
struct errtype { struct errtype {
// Error enum is ordered such that a lower numeric value corresponds to // Error enum is ordered such that a lower numeric value corresponds to
// more success // more success
Send errno; Send type;
uint8_t val; uint8_t val;
} err = {}; } err = {};
using enum Send; using enum Send;
if (is_muted()) { if (is_muted()) {
err.errno = MUTED; err.type = MUTED;
goto handle_err; goto handle_err;
} }
{ // bound handle lifetime for (uint8_t attempt = 0; attempt < SEND_ATTEMPTS; attempt++) {
auto handle = get(); auto handle = get(); // bound handle lifetime
if (handle.sendstartbit() == BUSY) { err.type = Send{0};
// Some other device is already driving the bus
err.errno = BUSY; err.type = handle.send_header(out.is_unicast);
if (err.type != Send{0}) // BUSY or LOST_ARBITRATION (broadcast)
continue;
err.type = handle.send_controller_addr(out.controller_addr);
if (err.type != Send{0}) // LOST_ARBITRATION: other device has lower address
continue;
err.type = handle.send_peripheral_addr(out.peripheral_addr, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_control(out.control, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_length(out.length, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_data(out.data, out.length, out.is_unicast, &err.val);
if (err.type != Send{0})
goto handle_err;
// A phy that only queued the fields above settles them here
uint8_t data_i = 0;
err.type = phy_send_done(&data_i);
if (err.type == BUSY || err.type == LOST_ARBITRATION) // Queued header
continue;
if (err.type != Send{0}) {
err.val = data_i;
goto handle_err; goto handle_err;
} }
handle.send<1>(static_cast<uint8_t>(out.is_unicast), no_parity); break; // Sent
}
handle.send<12>(out.controller_addr, with_parity); if (err.type != Send{0})
goto handle_err; // Outbid (or busy) on every attempt
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
// back to read mode // back to read mode
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("Error", stdout); fputs("Error", stdout);
switch (err.errno) { switch (err.type) {
case MUTED: fputs(": Device muted", stdout); break; case MUTED: fputs(": Device muted", stdout); break;
case BUSY: fputs(": Busy bus", stdout); break; case BUSY: fputs(": Busy bus", stdout); break;
case CONTENDED_BUS:
fputs(": bus contended after arbitration", stdout);
break;
case LOST_ARBITRATION: fputs(": lost arbitration", stdout); break;
case NAK_ADDRESS: case NAK_ADDRESS:
case NAK_CONTROL: case NAK_CONTROL:
case NAK_MESSAGE_LENGTH: case NAK_MESSAGE_LENGTH:
case NAK_DATA: case NAK_DATA:
case NAK_TOO_LONG:
case NAK: case NAK:
fputs(" NAK: ", stdout); fputs(" NAK: ", stdout);
switch (err.errno) { switch (err.type) {
case NAK_ADDRESS: fputs("address", stdout); break; case NAK_ADDRESS: fputs("address", stdout); break;
case NAK_CONTROL: fputs("Control", stdout); break; case NAK_CONTROL: fputs("Control", stdout); break;
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break; case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
case NAK_DATA: printf(" data[%u]", err.val); break; case NAK_DATA: printf(" data[%u]", err.val); break;
case NAK_TOO_LONG: fputs("too long", stdout); break;
case NAK: case NAK:
case MUTED: case MUTED:
case CONTENDED_BUS:
case LOST_ARBITRATION:
case BUSY: __builtin_unreachable(); case BUSY: __builtin_unreachable();
} }
break; break;
@@ -414,16 +338,31 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
if (print.print) if (print.print)
out.print(print); out.print(print);
return err.errno; return err.type;
} }
Bus::Handle Bus::get() { return Handle{*this}; }; Bus::Handle Bus::get() { return Handle{*this}; };
#if !defined(NDEBUG) && defined(MEASURE_BUS) #if !defined(NDEBUG)
// Debug bit-timing measurement on the one physical bus; instance-scoped for the
// same reason as is_active(). void Bus::set_dominant() { phy_set_dominant(); }
void Bus::set_recessive() { phy_set_recessive(); }
Send Bus::sendbyte(uint8_t byte, bool ack) {
auto handle = get();
uint8_t data_i = 0;
if (const Send serr = handle.send_data(&byte, 1, ack, &data_i);
serr != Send{0})
return serr;
return phy_send_done(&data_i);
}
#ifdef MEASURE_BUS
// Debug bit-timing measurement on the one physical bus; instance-scoped like
// deafen().
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void Bus::measure() { phy_measure(); } void Bus::measure() { phy_measure(); }
#endif
#endif #endif
} // namespace avclan } // namespace avclan
+12 -4
View File
@@ -69,18 +69,26 @@ public:
Bus(const Bus &) = delete; Bus(const Bus &) = delete;
Bus &operator=(const Bus &) = delete; Bus &operator=(const Bus &) = delete;
void init(); // `address` is our own peripheral address; the phy keeps it and acknowledges
// frames addressed to it without further instruction. One address per bus.
void init(uint16_t address);
bool is_active() const; // Prevent the device from being active on the bus
void mute(bool mute); void mute(bool mute);
bool is_muted() const { return muted_; }; bool is_muted() const { return muted_; };
// Set the device to be deaf to (ie don't react to) bus activity. It is
// orthogonal to mute — a deaf device can still send frames.
void deafen(bool deaf);
#ifndef NDEBUG #ifndef NDEBUG
void measure(); void measure();
detail::Error::Send sendbyte(uint8_t byte, bool ack = false);
void set_dominant();
void set_recessive();
#endif #endif
expected<std::unique_ptr<Frame>, Error::Read> read(uint16_t address, expected<std::unique_ptr<Frame>, Error::Read> read(Frame::Print print);
Frame::Print print);
Error::Send send(const Frame &out, Frame::Print print); Error::Send send(const Frame &out, Frame::Print print);
private: private:
+110 -108
View File
@@ -3,19 +3,22 @@
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com> // Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <mutex>
#include "avclan.h" #include "avclan.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "device.hpp" #include "device.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/cd_timer.h"
#include "hal/media.h" #include "hal/media.h"
namespace { namespace {
using namespace avclan; using namespace avclan;
using namespace std::chrono_literals;
constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER), constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
to_underlying(Device::STATUS), to_underlying(Device::STATUS),
@@ -28,39 +31,45 @@ constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
0x01, 0x01,
0x02}; 0x02};
constexpr int WIRE_SIZE = 8; // cd state report size in bytes constexpr int WIRE_SIZE = 8; // cd state report size in bytes
constexpr int TIME_SKIP = 15; // seconds constexpr auto TIME_SKIP = 15s;
constexpr int TWODIGIT_MAX = 99; 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 // Pack a 0–TWODIGIT_MAX count into 2-digit BCD
such as 0xFF / 0x7F meaning "no time") pass through unchanged so they survive
the wire round-trip. */
constexpr uint8_t toBCD(uint8_t val) { constexpr uint8_t toBCD(uint8_t val) {
if (val > TWODIGIT_MAX)
return val;
return (uint8_t)(((val / 10) << 4) | (val % 10)); return (uint8_t)(((val / 10) << 4) | (val % 10));
} }
extern "C" void incrementTime_callback(void *self) { // Whole seconds, saturated to the displayable range
static_cast<avclan::CDChanger *>(self)->incrementTime(); constexpr std::chrono::seconds displaySeconds(std::chrono::milliseconds t) {
} return std::clamp(std::chrono::floor<std::chrono::seconds>(t), -MAX_TIME,
MAX_TIME);
extern "C" bool isPlaying_callback(void *self) {
return static_cast<avclan::CDChanger *>(self)->isPlaying();
} }
} // namespace } // namespace
namespace avclan { namespace avclan {
void CDChanger::init() { void CDChanger::init(Notifier notif) {
media_init(); 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) { void CDChanger::handle(const Frame &in, Frame &out) {
if (in.length < 4) if (in.length < 4)
return; // [Currently known] valid CDChanger frames have at least 4 bytes return; // [Currently known] valid CDChanger frames have at least 4 bytes
std::optional<MediaAction> media;
const uint8_t *data = &in.data[1]; const uint8_t *data = &in.data[1];
const auto from = static_cast<Device>(*data++); const auto from = static_cast<Device>(*data++);
/* const auto to = */ data++; /* const auto to = */ data++;
@@ -73,7 +82,7 @@ void CDChanger::handle(const Frame &in, Frame &out) {
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum" #pragma GCC diagnostic ignored "-Wswitch-enum"
// Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). // 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: case Enable_Function_Req:
out.is_unicast = true; out.is_unicast = true;
out.length = sizeof(function_change_resp); 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[2] = to_underlying(from);
out.data[3] = to_underlying(Disable_Function_Resp); out.data[3] = to_underlying(Disable_Function_Resp);
if (isPlaying()) { if (isPlaying()) {
stopPlaying(); media = stopPlaying();
state = 0; state = 0;
flags2 = 0x80; flags2 = 0x80;
out.reaction = r_StatusReport; out.reaction = r_StatusReport;
@@ -165,47 +174,39 @@ void CDChanger::handle(const Frame &in, Frame &out) {
++track; ++track;
else else
track = 1; track = 1;
mins = 0xff; time.reset();
secs = 0x7f;
flags2 &= ~NEGATIVE;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Track_Next); media = MediaAction::Track_Next;
out.reaction = r_TrackChange; out.reaction = r_TrackChange;
break; break;
case Track_Seek_Down: case Track_Seek_Down:
state = SEEKING_TRACK; state = SEEKING_TRACK;
// Track down returns to track beginning if in ~middle of song // 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) if (track > 1)
--track; --track;
else else
track = TWODIGIT_MAX; track = TWODIGIT_MAX;
media_action(MediaAction::Track_Prev); media = MediaAction::Track_Prev;
} }
mins = 0xff; time.reset();
secs = 0x7f;
flags2 &= ~NEGATIVE;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out.reaction = r_TrackChange; out.reaction = r_TrackChange;
break; break;
case Track_Fast_Forward: { case Track_Fast_Forward: {
state |= SEEKING; state |= SEEKING;
incrementTime(TIME_SKIP); seek(TIME_SKIP);
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Forward); media = MediaAction::Skip_Forward;
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now
out.reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
case Track_Rewind: { case Track_Rewind: {
state |= SEEKING; state |= SEEKING;
incrementTime(-TIME_SKIP); seek(-TIME_SKIP);
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Backward); media = MediaAction::Skip_Backward;
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now
out.reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
@@ -272,18 +273,24 @@ void CDChanger::handle(const Frame &in, Frame &out) {
default: break; default: break;
} }
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
// Media action handled outside of switch to minimize lock duration
if (media)
media_action(*media);
} }
std::unique_ptr<Frame> std::unique_ptr<Frame>
CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) { 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) && if (exp.error().reaction == to_underlying(r_StateReport) &&
exp.error().err == detail::Error::Send::NAK_ADDRESS && exp.error().err == detail::Error::Send::NAK_ADDRESS &&
++failedStatusReports > 1) { ++failedStatusReports > 1) {
failedStatusReports = 0; failedStatusReports = 0;
stopPlaying(); // Disable periodic updates if e.g. no-one's media = stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?) // listening (car was turned off?)
} }
} else { } else {
auto out = std::move(exp.value()); auto out = std::move(exp.value());
@@ -312,9 +319,9 @@ CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_SendOnly; out->reaction = r_SendOnly;
break; break;
case r_TrackChange: case r_TrackChange:
setTime(0, 0); setTime(0ms);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is // Track starts at a whole sec; next status is ~1 sec out
// ~1 sec from now xTimerReset(statusTimer, 0);
[[fallthrough]]; [[fallthrough]];
case r_NormalizeState: case r_NormalizeState:
normalizeState(); normalizeState();
@@ -327,7 +334,7 @@ CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_BeganPlaying; out->reaction = r_BeganPlaying;
break; break;
case r_BeganPlaying: case r_BeganPlaying:
startPlaying(); // only start PIT after normalizing state media = startPlaying(); // only start status timer after normalizing
out->reaction = r_Nothing; out->reaction = r_Nothing;
break; break;
case r_StatusReport: case r_StatusReport:
@@ -341,18 +348,19 @@ CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
} }
if (out->reaction > r_Nothing) if (out->reaction > r_Nothing)
return out; next = std::move(out);
} }
return {}; if (media)
media_action(*media);
return next;
} }
void CDChanger::enable(Frame &out) { void CDChanger::enable(Frame &out) {
if (!isPlaying()) { if (!isPlaying()) {
if (mins > TWODIGIT_MAX) const std::lock_guard lock(mutex);
mins = 0; if (!time)
if (secs > TWODIGIT_MAX) setTime(0ms);
secs = 0;
state = SEEKING | SEEKING_TRACK; state = SEEKING | SEEKING_TRACK;
flags2 = 0x80; flags2 = 0x80;
generateStatus(out, false, Device::STATUS); 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, uint32_t /*payload*/) {
statusQueued = false; // Clear first so a tick during emit() queues anew
void CDChanger::emit(Frame &out) { const std::lock_guard lock(mutex);
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out.reaction = r_StateReport; out.reaction = r_StateReport;
cdtimer_clear();
} }
bool CDChanger::isPlaying() const { return playing; } bool CDChanger::isPlaying() const { return playing; }
// Sets CD_mode to play and resets timer count (so that the next interrupt is in // Starts track time and the ~1 Hz status report (the first ~1 sec from now)
// 1 sec) std::optional<MediaAction> CDChanger::startPlaying() {
void CDChanger::startPlaying() {
static bool havePlayed = false; static bool havePlayed = false;
std::optional<MediaAction> media;
if (havePlayed) if (havePlayed)
media_action(MediaAction::Play); media = MediaAction::Play;
havePlayed |= true; havePlayed |= true;
if (!playing)
refTick = xTaskGetTickCount();
playing = true; playing = true;
cdtimer_reset(); xTimerReset(statusTimer, 0);
return media;
} }
void CDChanger::stopPlaying() { MediaAction CDChanger::stopPlaying() {
cdtimer_disable(); xTimerStop(statusTimer, 0);
time = trackTime();
playing = false; 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. // Serialize cd_status into the wire format.
// track/mins/secs, need converted from decimal to BCD // track/mins/secs, need converted from decimal to BCD
void CDChanger::serialize(uint8_t *dst) const { 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++ = cds;
*dst++ = state; *dst++ = state;
*dst++ = disc; *dst++ = disc;
*dst++ = toBCD(track); *dst++ = toBCD(track);
*dst++ = toBCD(mins); *dst++ = mins;
*dst++ = toBCD(secs); *dst++ = secs;
*dst++ = flags; *dst++ = flags;
*dst++ = flags2; *dst++ = negative ? (flags2 | NEGATIVE) : (flags2 & ~NEGATIVE);
}
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;
} }
// Used for changed status messages // Used for changed status messages
@@ -460,10 +464,8 @@ void CDChanger::generateStatus(Frame &status, bool is_unicast,
} }
void CDChanger::normalizeState() { void CDChanger::normalizeState() {
if (mins > TWODIGIT_MAX) if (!time)
mins = 0; setTime(0ms);
if (secs > TWODIGIT_MAX)
secs = 0;
state = PLAYBACK; state = PLAYBACK;
flags &= (uint8_t)~(DISK_SCAN | SCAN); flags &= (uint8_t)~(DISK_SCAN | SCAN);
} }
+40 -10
View File
@@ -5,8 +5,15 @@
#pragma once #pragma once
#include <atomic>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <optional>
#include "FreeRTOS.h" // IWYU pragma: export
#include "semphr.h"
#include "timers.h"
#include "avclan.h" #include "avclan.h"
#include "device.hpp" #include "device.hpp"
@@ -60,16 +67,17 @@ public:
}; };
static constexpr Device id = Device::CD_CHANGER; 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); void handle(const Frame &in, Frame &out);
std::unique_ptr<Frame> std::unique_ptr<Frame>
react(expected<std::unique_ptr<Frame>, detail::SendError> exp); react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
void enable(Frame &out); void enable(Frame &out);
void disable(Frame &out); void disable(Frame &out);
static bool pending(); void emit(Frame &out, uint32_t payload);
void emit(Frame &out);
void incrementTime(int8_t inc_sec = 1);
bool isPlaying() const; bool isPlaying() const;
#ifndef NDEBUG #ifndef NDEBUG
void media_action(MediaAction action); void media_action(MediaAction action);
@@ -78,21 +86,43 @@ public:
#endif #endif
private: private:
void startPlaying(); // Implements the BasicLockable named requirements for an xSemaphore
void stopPlaying(); 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 serialize(uint8_t *dst) const;
void setTime(uint8_t mins, uint8_t secs);
void generateStatus(Frame &status, bool is_unicast, Device to) const; void generateStatus(Frame &status, bool is_unicast, Device to) const;
void normalizeState(); 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; bool playing = false;
int failedStatusReports = 0; int failedStatusReports = 0;
uint8_t cds = CD1; uint8_t cds = CD1;
uint8_t state = SEEKING | SEEKING_TRACK; uint8_t state = SEEKING | SEEKING_TRACK;
uint8_t disc = 1; uint8_t disc = 1;
uint8_t track = 1; // Decimal storage; serialize to BCD 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 flags = 0; uint8_t flags = 0;
uint8_t flags2 = 0x80; uint8_t flags2 = 0x80;
}; };
+27 -10
View File
@@ -7,6 +7,9 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp" #include "stdshim.hpp"
@@ -47,20 +50,34 @@ enum class Device : uint8_t {
TRIP_INFO = 0xE5, 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> template <class T>
concept DeviceInterface = concept DeviceInterface =
requires { std::integral_constant<Device, T::id>{}; } && requires { std::integral_constant<Device, T::id>{}; } &&
requires(T dev, const Frame &in, Frame &out, requires(T dev, Notifier notifier, const Frame &in, Frame &out,
expected<std::unique_ptr<Frame>, detail::SendError> exp) { expected<std::unique_ptr<Frame>, detail::SendError> exp,
dev.init(); uint32_t payload) {
dev.init(notifier);
dev.handle(in, out); dev.handle(in, out);
dev.enable(out); dev.enable(out);
{ { dev.react(std::move(exp)) } -> std::same_as<std::unique_ptr<Frame>>;
dev.react(std::move(exp)) dev.emit(out, payload);
} -> std::same_as<std::unique_ptr<Frame>>;
{ dev.pending() } -> std::convertible_to<bool>;
// Devices must clear `pending()` after `emit()` is called
dev.emit(out);
}; };
} // namespace avclan } // namespace avclan
+26 -8
View File
@@ -6,8 +6,13 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory>
#include <new>
#include <type_traits> #include <type_traits>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "frame.hpp" #include "frame.hpp"
#include "hal/stdio.h" #include "hal/stdio.h"
#include "stdshim.hpp" #include "stdshim.hpp"
@@ -16,7 +21,6 @@
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <limits> #include <limits>
#include <new>
namespace { namespace {
template <class T, std::uint8_t N> template <class T, std::uint8_t N>
@@ -60,14 +64,28 @@ namespace avclan {
#if defined(AVCLAN_FRAME_POOL_N) #if defined(AVCLAN_FRAME_POOL_N)
void *Frame::operator new(std::size_t /*count*/, void *Frame::operator new(std::size_t /*count*/,
const std::nothrow_t & /*tag*/) noexcept { 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 // NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
void Frame::operator delete(void *ptr) noexcept { void Frame::operator delete(void *ptr) noexcept {
taskENTER_CRITICAL();
pool.release(static_cast<Frame *>(ptr)); pool.release(static_cast<Frame *>(ptr));
taskEXIT_CRITICAL();
} }
#endif #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 { namespace {
// Emit `value` as at least `width` (lowercase, as to_chars emits) hex digits, // 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 // 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) { Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
struct errtype { struct errtype {
Error::Parse errno; Error::Parse type;
uint8_t val; uint8_t val;
} err = {}; } err = {};
const uint8_t *last = bytes + len; const uint8_t *last = bytes + len;
if (len < Frame::MIN_SIZE) { if (len < Frame::MIN_SIZE) {
err.errno = TOO_SHORT; err.type = TOO_SHORT;
goto handle_err; goto handle_err;
} }
@@ -183,7 +201,7 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
length = *bytes++; length = *bytes++;
if (length > MAXLENGTH) { if (length > MAXLENGTH) {
err.errno = LENGTH_TOO_BIG; err.type = LENGTH_TOO_BIG;
err.val = length; err.val = length;
goto handle_err; goto handle_err;
} }
@@ -191,14 +209,14 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
if ((bytes + length) <= last) { if ((bytes + length) <= last) {
memcpy(data, bytes, length); memcpy(data, bytes, length);
} else { } else {
err.errno = MISMATCH_LENGTH; err.type = MISMATCH_LENGTH;
goto handle_err; goto handle_err;
} }
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("ERR(parse): ", stdout); fputs("ERR(parse): ", stdout);
switch (err.errno) { switch (err.type) {
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break; case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
case MISMATCH_LENGTH: case MISMATCH_LENGTH:
puts("frame->length is longer than remaining data"); puts("frame->length is longer than remaining data");
@@ -211,6 +229,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
} }
} }
return err.errno; return err.type;
} }
} // namespace avclan } // namespace avclan
+4
View File
@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
@@ -30,6 +31,9 @@ struct Frame {
Error::Parse parse(const uint8_t *bytes, uint8_t len); Error::Parse parse(const uint8_t *bytes, uint8_t len);
void print(Print print) const; 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) #if defined(AVCLAN_FRAME_POOL_N)
// O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is // O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported. // 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 <stdint.h>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h" #include "avclan.h"
#ifdef __cplusplus #ifdef __cplusplus
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send; using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
extern "C" { extern "C" {
#else #else
typedef enum Read Read; typedef enum Read Read;
typedef enum Send Send; typedef enum Send Send;
typedef enum Bit Bit;
#endif #endif
// One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted. // One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted.
void phy_init(void); //
// `address` is our device address.
void phy_init(uint16_t address);
// Mute/unmute device TX. "Muted" means transmission is disabled (RX is // Mute/unmute device TX. "Muted" means transmission is disabled (RX is
// unchanged/still allowed) // unchanged/still allowed)
@@ -30,56 +32,83 @@ void phy_mute(bool mute);
// Non-mutating (e.g. theoretically const qualified/-able) // Non-mutating (e.g. theoretically const qualified/-able)
bool phy_is_muted(void); bool phy_is_muted(void);
// True when bus is driven/"dominant" (logical 0) // Do not respond to or log incoming frames. Can still send frames.
bool phy_active(void); 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) // Bus-transaction guard: quiesce the other async sources (e.g. interrupts)
// so that bus read/send timing isn't disturbed. Re-enable relevant async // so that bus read/send timing isn't disturbed. Re-enable relevant async
// sources with `phy_guard_leave`. May be a no-op on a target where contention // sources with `phy_guard_leave`.
// isn't a concern. // - May be a no-op on a target where concurrency/preemption isn't a concern.
void phy_guard_enter(void); void phy_guard_enter(void);
void phy_guard_leave(void); void phy_guard_leave(void);
// Validates an incoming start bit; see avclan::detail::Error::Read. /* Per-field frame I/O.
// Invariants: *
// - Must only be called after positive phy_active() call. * All send functions may have asynchronous implementations (e.g. return before
Read phy_read_startbit(void); * the send has completed on the bus). Success is indicated by a zero value
* `Read` or `Send` enum. Non-zero error codes indicate a synchronously
* 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 // Returns NO_FRAME when there is no frame to read.
Send phy_send_startbit(void); 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 // Send start and broadcast bits. Returns success or one of these error values:
NAK. An ACK bit is a cooperative bit, where the sender starts (drives the bus) // MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
for the sync period, and allows the receiver to drive the bus (or not) to // a broadcast).
finish a "1" bit. Send phy_send_header(bool is_unicast);
*/
Send phy_read_ack(void);
void phy_send_ack(void);
// Per-symbol I/O. The send* helpers return the even parity of the bits sent; // Send the controller address. Returns success or LOST_ARBITRATION (a device
// the read* helpers return the even parity of the bits read. The _u8/_u16 // with a lower device is sending a frame).
// suffixes name the source-operand width. The function implementations need not Send phy_send_controller_addr(uint16_t addr);
// all be separate/independent (e.g. all send functions could be redirect to a Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack);
// single phy_send_bits_u16, etc). Send phy_send_control(uint8_t control, bool expect_ack);
// N.B: `len` is the number of bits to send. The Send phy_send_length(uint8_t length, bool expect_ack);
// C++ send/readbits templates are the only consumers and use constraints to // Send `length` data bytes. `data_index` is only written to for NAK_DATA.
// enforce valid len values, so runtime checks are unnecessary. Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index);
// Intended for sending parity bits // Allows asynchronous ports to block until the phy has finished sending the
void phy_send_bit(Bit bit); // 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 #ifndef NDEBUG
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);
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len); // Hold the bus at a level until the matching call.
Bit phy_read_bits_u16(uint16_t *bits, int8_t len); void phy_set_dominant(void);
Bit phy_read_byte(uint8_t *byte); 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`). // Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void); void phy_measure(void);
#endif
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus
+3 -2
View File
@@ -4,6 +4,7 @@
#pragma once #pragma once
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -11,8 +12,8 @@ extern "C" {
// Generic stdio interface initialization. All user I/O goes through <stdio.h> // Generic stdio interface initialization. All user I/O goes through <stdio.h>
// functions. Assumptions/invariants: // functions. Assumptions/invariants:
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when // - A libc read of stdin blocks the calling task (not the CPU) until at least
// empty). Necessary to avoid stalling the REPL poll loop. // one byte is available.
// - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not // - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
// change a bare LF or a binary frame payload. Writes through // change a bare LF or a binary frame payload. Writes through
// <stdio.h> and writes through stdio_write_nonblock() must reach the same // <stdio.h> and writes through stdio_write_nonblock() must reach the same
+47 -50
View File
@@ -4,15 +4,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
#include <algorithm>
#include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <new>
#include <tuple> #include <tuple>
#include <type_traits>
#include <utility> #include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "device.hpp" #include "device.hpp"
@@ -38,28 +43,38 @@ public:
static_assert(((Devs::id != NoDevice) && ...), static_assert(((Devs::id != NoDevice) && ...),
"a registered Device id collides with the NoDevice sentinel; " "a registered Device id collides with the NoDevice sentinel; "
"update the sentinel value in avclan.h"); "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} { static constexpr UBaseType_t EMIT_QUEUE_LEN = 2 * sizeof...(Devs);
bus.init();
(std::get<Devs>(devices_).init(), ...); 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_; }; uint16_t controller() const { return controller_; };
template <DeviceInterface Dev> Dev &device() { template <DeviceInterface Dev> Dev &device() {
return std::get<Dev>(devices_); return std::get<Dev>(devices_);
} }
bool bus_is_active() const { return bus.is_active(); };
void mute(bool mute) { bus.mute(mute); }; void mute(bool mute) { bus.mute(mute); };
bool is_muted() const { return bus.is_muted(); }; bool is_muted() const { return bus.is_muted(); };
#ifndef NDEBUG #ifndef NDEBUG
Bus &get_bus() { return bus; } Bus &get_bus() { return bus; }
void set_controller(uint16_t addr) { controller_ = addr; }
#endif #endif
expected<std::unique_ptr<Frame>, Error::Read> expected<std::unique_ptr<Frame>, Error::Read>
read(Frame::Print print = Frame::Print{}) { read(Frame::Print print = Frame::Print{}) {
return bus.read(address_, print); return bus.read(print);
}; };
expected<std::unique_ptr<Frame>, detail::SendError> expected<std::unique_ptr<Frame>, detail::SendError>
@@ -69,8 +84,9 @@ public:
out->control = 0xF; out->control = 0xF;
auto err = bus.send(*out, print); auto err = bus.send(*out, print);
if (err != Error::Send{0}) if (err != Error::Send{0})
return unexpected{ return unexpected{detail::SendError{.owning_device = out->owning_device,
detail::SendError{out->owning_device, out->reaction, err}}; .reaction = out->reaction,
.err = err}};
return out; return out;
}; };
@@ -85,7 +101,7 @@ public:
if (is_muted() || in.length < 3) if (is_muted() || in.length < 3)
return {}; return {};
std::unique_ptr<Frame> out(new (std::nothrow) Frame); std::unique_ptr<Frame> out = Frame::acquire();
if (!out) { if (!out) {
puts("!! failed Frame alloc in route !!"); puts("!! failed Frame alloc in route !!");
return unexpected{Error::Read::POOL_EMPTY}; return unexpected{Error::Read::POOL_EMPTY};
@@ -204,53 +220,33 @@ public:
return next; 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() { std::unique_ptr<Frame> poll() {
using U = std::unique_ptr<Frame>; std::unique_ptr<Frame> out = Frame::acquire();
auto does_emit = [&](DeviceInterface auto &dev) -> U { if (!out) {
if (!dev.pending()) puts("!! failed Frame alloc in poll !!");
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;
}
return {}; 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: 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 { template <Party P> void stamp(Frame &out) const {
if constexpr (P == Sender) if constexpr (P == Sender)
out.controller_addr = address_; out.controller_addr = address_;
@@ -268,5 +264,6 @@ private:
uint16_t controller_ = 0; uint16_t controller_ = 0;
const uint16_t address_; const uint16_t address_;
std::tuple<Devs...> devices_; std::tuple<Devs...> devices_;
QueueHandle_t emit_requests_;
}; };
} // namespace avclan } // namespace avclan
@@ -12,6 +12,11 @@ target_sources(avclan PRIVATE
) )
target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(avclan PUBLIC
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
)
find_program(AVRDUDE avrdude) find_program(AVRDUDE avrdude)
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware") set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
@@ -76,9 +81,6 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json. # tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)") set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_FRAME_POOL_N 14 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
try_compile(LIBC_VERSION_TEST try_compile(LIBC_VERSION_TEST
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp" SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
COMPILE_DEFINITIONS -mmcu=${AVR_MCU} COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
@@ -137,6 +139,7 @@ target_compile_definitions(jnk0le_uart
PRIVATE F_CPU=${FREQSEL} PRIVATE F_CPU=${FREQSEL}
) )
target_compile_options(jnk0le_uart PUBLIC target_compile_options(jnk0le_uart PUBLIC
$<$<CONFIG:Debug>:-Og>
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3) -ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
target_link_libraries(avclan PUBLIC jnk0le_uart) target_link_libraries(avclan PUBLIC jnk0le_uart)
@@ -148,7 +151,6 @@ target_compile_definitions(avclan PUBLIC
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV} __CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
TCB_CLKSEL=${TCB_CLKSEL} TCB_CLKSEL=${TCB_CLKSEL}
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
) )
target_compile_options(avclan PUBLIC target_compile_options(avclan PUBLIC
--param=min-pagesize=0 --param=min-pagesize=0
+198 -20
View File
@@ -9,6 +9,7 @@
#include <stdint.h> #include <stdint.h>
#include <util/atomic.h> #include <util/atomic.h>
#include "avclan.h"
#include "hal/cd_timer.h" // statustimer_enable/disable (guard) #include "hal/cd_timer.h" // statustimer_enable/disable (guard)
#include "hal/phy.h" #include "hal/phy.h"
#include "media_avr.h" // media_sync_during_mask (guard) #include "media_avr.h" // media_sync_during_mask (guard)
@@ -17,6 +18,9 @@
// avr-libc. // avr-libc.
#include "timing_avr.h" // IWYU pragma: keep #include "timing_avr.h" // IWYU pragma: keep
enum bit_t : uint8_t { bit_zero = 0x00, bit_one = 0x01, bit_start = 0x10 };
typedef enum bit_t Bit;
// USART0 TX ring indices owned by the jnk0le lib; the guard consults them to // USART0 TX ring indices owned by the jnk0le lib; the guard consults them to
// decide whether to resume the TX drain (DRE interrupt) on leave. // decide whether to resume the TX drain (DRE interrupt) on leave.
extern volatile uint8_t tx0_Head, tx0_Tail; extern volatile uint8_t tx0_Head, tx0_Tail;
@@ -79,8 +83,12 @@ bool phy_is_muted() {
return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0); return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0);
} }
// No-op: acks are driven from inside the synchronous read path, which a deaf
// caller never enters.
void phy_deafen([[maybe_unused]] bool deaf) {}
// True when the bus is being driven (i.e. not idle/floating). // True when the bus is being driven (i.e. not idle/floating).
bool phy_active() { return (!BUS_IS_IDLE) != 0; } bool phy_frame_pending() { return (!BUS_IS_IDLE) != 0; }
// Mute device TX on AVCLAN bus // Mute device TX on AVCLAN bus
void phy_mute(bool mute) { void phy_mute(bool mute) {
@@ -114,7 +122,7 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) {
while (TCB1.CNT <= period) {}; while (TCB1.CNT <= period) {};
} }
void phy_send_bit(Bit bit) { static void phy_send_bit(Bit bit) {
uint16_t zero_length, one_length; uint16_t zero_length, one_length;
switch (bit) { switch (bit) {
case bit_zero: case bit_zero:
@@ -135,7 +143,7 @@ void phy_send_bit(Bit bit) {
set_AVC_logic_for(1, one_length); set_AVC_logic_for(1, one_length);
} }
void phy_send_ack() { static void phy_send_ack() {
TCB1.CNT = 0; TCB1.CNT = 0;
// Wait for controller to begin ACK bit // Wait for controller to begin ACK bit
@@ -149,7 +157,7 @@ void phy_send_ack() {
phy_send_bit(bit_zero); phy_send_bit(bit_zero);
} }
Send phy_read_ack() { static Send phy_read_ack() {
TCB1.CNT = 0; // Double reset of TCB1.CNT: here TCB1.CNT = 0; // Double reset of TCB1.CNT: here
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within) set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
AVCLAN_setBusIdle(); // Stop driving bus AVCLAN_setBusIdle(); // Stop driving bus
@@ -164,13 +172,13 @@ Send phy_read_ack() {
// Check/wait in case we get here before peripheral finishes ACK bit // Check/wait in case we get here before peripheral finishes ACK bit
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
return NAK; return NAK_TOO_LONG;
} }
return (Send)0; return (Send)0;
} }
// Send `len` bits on the AVCLAN bus; returns the even parity // Send `len` bits on the AVCLAN bus; returns the even parity
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) { static Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
uint8_t b = *bits; uint8_t b = *bits;
uint8_t parity = 0; uint8_t parity = 0;
int8_t len_mod8 = 8; int8_t len_mod8 = 8;
@@ -189,17 +197,19 @@ Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
b <<= 1; b <<= 1;
} }
len_mod8 = 8; len_mod8 = 8;
b = *--bits; // Avoid under-read for last byte
if (len > 0)
b = *--bits;
} }
return (parity & 1); return (parity & 1);
} }
// Send `len` bits on the AVCLAN bus; returns the even parity // Send `len` bits on the AVCLAN bus; returns the even parity
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) { static Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
return phy_send_bits_u8((const uint8_t *)bits + 1, len); return phy_send_bits_u8((const uint8_t *)bits + 1, len);
} }
Bit phy_send_byte(const uint8_t *byte) { static Bit phy_send_byte(const uint8_t *byte) {
uint8_t b = *byte; uint8_t b = *byte;
uint8_t parity = 0; uint8_t parity = 0;
@@ -238,7 +248,7 @@ ISR(TCB0_INT_vect) {
} }
// Read `len` bits on the AVCLAN bus; returns the even parity // Read `len` bits on the AVCLAN bus; returns the even parity
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) { static Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
uint8_t parity; uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0; READING_BYTE = 0;
@@ -265,7 +275,7 @@ Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
} }
// Read `len` bits on the AVCLAN bus; returns the even parity // Read `len` bits on the AVCLAN bus; returns the even parity
Bit phy_read_bits_u16(uint16_t *bits, int8_t len) { static Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
uint8_t parity = 0; uint8_t parity = 0;
if (len > 8) { if (len > 8) {
uint8_t over = len - 8; uint8_t over = len - 8;
@@ -278,7 +288,7 @@ Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
} }
// Read a byte on the AVCLAN bus // Read a byte on the AVCLAN bus
Bit phy_read_byte(uint8_t *byte) { static Bit phy_read_byte(uint8_t *byte) {
uint8_t parity; uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0; READING_BYTE = 0;
@@ -304,7 +314,16 @@ Bit phy_read_byte(uint8_t *byte) {
return (Bit)(parity & 1); return (Bit)(parity & 1);
} }
void phy_init() { // Our own peripheral address, and whether the frame being read is addressed to
// it. hal/phy.h puts the ACK decision in the phy, so the latch that used to
// live in Bus::read lives here now: set when the peripheral address comes in,
// and reused by every field after it.
static uint16_t own_address_;
static bool acking_;
void phy_init(uint16_t address) {
own_address_ = address;
// Set pin 6 and 7 as input // Set pin 6 and 7 as input
PORTA.DIRCLR = (PIN6_bm | PIN7_bm); PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
// Disable input buffer; recommended when using AC // Disable input buffer; recommended when using AC
@@ -339,7 +358,7 @@ void phy_init() {
phy_mute(false); // unmute AVCLAN bus TX phy_mute(false); // unmute AVCLAN bus TX
} }
Read phy_read_startbit() { static Read phy_read_startbit() {
// Following HAL header docs, this function is only called after a driven bus // Following HAL header docs, this function is only called after a driven bus
// was ~recently detected. // was ~recently detected.
// Two main (designed) entry flows, depending on timing of pulse end w.r.t. // Two main (designed) entry flows, depending on timing of pulse end w.r.t.
@@ -430,7 +449,7 @@ Read phy_read_startbit() {
// Acquire the bus and emit a start bit. Returns false if another device is // Acquire the bus and emit a start bit. Returns false if another device is
// already driving the bus (we can't yet do proper CSMA/CD). // already driving the bus (we can't yet do proper CSMA/CD).
Send phy_send_startbit() { static Send phy_send_startbit() {
// wait for free line // wait for free line
TCB1.CNT = 0; TCB1.CNT = 0;
while (BUS_IS_IDLE) { while (BUS_IS_IDLE) {
@@ -459,6 +478,144 @@ Send phy_send_startbit() {
return (Send)0; return (Send)0;
} }
// Field bits, then the wire's parity bit, checked against what we counted.
static Read read_parity(Bit calc, Read bad) {
uint8_t wire = 0;
phy_read_bits_u8(&wire, 1);
return ((uint8_t)calc != wire) ? bad : (Read)0;
}
// An acknowledge slot follows the field whether or not we drive it, so a frame
// that isn't ours still has to have the slot read away.
static void ack_slot(void) {
if (acking_) {
phy_send_ack();
} else {
uint8_t slot = 0;
phy_read_bits_u8(&slot, 1);
}
}
Read phy_read_header(bool *is_unicast) {
acking_ = false; // New frame; nothing is addressed to us yet
const Read err = phy_read_startbit();
if (err != (Read)0)
return err;
uint8_t bit = 0;
phy_read_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
*is_unicast = (bit != 0);
return (Read)0;
}
Read phy_read_controller_addr(uint16_t *addr) {
// No acknowledge slot: this field is the arbitration window.
return read_parity(phy_read_bits_u16(addr, 12), BAD_CONTROLLER_PARITY);
}
Read phy_read_peripheral_addr(uint16_t *addr) {
const Read err =
read_parity(phy_read_bits_u16(addr, 12), BAD_PERIPHERAL_PARITY);
if (err != (Read)0)
return err;
// The one field that decides the ack; every field after it reuses the answer.
acking_ = !phy_is_muted() && (*addr == own_address_);
ack_slot();
return (Read)0;
}
Read phy_read_control(uint8_t *control) {
const Read err =
read_parity(phy_read_bits_u8(control, 4), BAD_CONTROL_PARITY);
if (err != (Read)0)
return err;
ack_slot();
return (Read)0;
}
Read phy_read_length(uint8_t *length) {
const Read err = read_parity(phy_read_byte(length), BAD_LENGTH_PARITY);
if (err != (Read)0)
return err;
ack_slot();
return (Read)0;
}
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
for (uint8_t i = 0; i < length; i++) {
const Read err = read_parity(phy_read_byte(&data[i]), BAD_DATA_PARITY);
if (err != (Read)0) {
*data_index = i;
return err;
}
ack_slot();
}
return (Read)0;
}
// `expect_ack` is "a NAK here is worth reporting", not "emit the slot": the
// slot goes out either way, and a broadcast frame simply has nobody to fill it.
static Send send_ack_slot(bool expect_ack, Send nak) {
if (expect_ack) {
const Send err = phy_read_ack();
return err == NAK ? nak : err;
}
const uint8_t fill = 1U;
phy_send_bits_u8(&fill, 1);
return (Send)0;
}
Send phy_send_header(bool is_unicast) {
const Send err = phy_send_startbit();
if (err != (Send)0)
return err;
const uint8_t bit = (is_unicast) ? 1U : 0U;
phy_send_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
return (Send)0;
}
Send phy_send_controller_addr(uint16_t addr) {
// No acknowledge slot: this field is the arbitration window.
phy_send_bit(phy_send_bits_u16(&addr, 12));
return (Send)0;
}
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
phy_send_bit(phy_send_bits_u16(&addr, 12));
return send_ack_slot(expect_ack, NAK_ADDRESS);
}
Send phy_send_control(uint8_t control, bool expect_ack) {
phy_send_bit(phy_send_bits_u8(&control, 4));
return send_ack_slot(expect_ack, NAK_CONTROL);
}
Send phy_send_length(uint8_t length, bool expect_ack) {
phy_send_bit(phy_send_byte(&length));
return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH);
}
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index) {
for (uint8_t i = 0; i < length; i++) {
phy_send_bit(phy_send_byte(&data[i]));
const Send err = send_ack_slot(expect_ack, NAK_DATA);
if (err != (Send)0) {
*data_index = i;
return err;
}
}
return (Send)0;
}
// Every field above is on the wire, and has reported its own outcome, by the
// time it returns: nothing is left to wait out or attribute.
Send phy_send_done([[maybe_unused]] uint8_t *data_index) { return (Send)0; }
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer) /* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
enabled. */ enabled. */
@@ -478,12 +635,31 @@ void phy_guard_leave() {
} }
} }
#if !defined(NDEBUG) && defined(MEASURE_BUS) #ifndef NDEBUG
#include <stdio.h> // phy_measure() reporting (debug builds only)
// Only used immediately below // Hold the bus at a given level.
#define XSTR(x) #x // Overrides and restores mute state upon release (i.e. set recessive).
#define STR(x) XSTR(x) 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 pulses[100];
static uint16_t periods[100]; static uint16_t periods[100];
@@ -516,4 +692,6 @@ void phy_measure() {
phy_guard_leave(); phy_guard_leave();
} }
#endif
#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 #pragma once
#include <array>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory> #include <memory>
#include <type_traits>
namespace detail { #include "FreeRTOS.h" // IWYU pragma: export
template <class T, auto N> struct Deleter; #include "queue.h"
}
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>;
// Owning FIFO of `T`s, safe to share between tasks
template <class T> class Queue {
public: public:
// Only empty construction is allowed for non-Owning, non-pool deleters explicit Queue(UBaseType_t length)
constexpr Queue() : handle{xQueueCreate(length, sizeof(T *))} {
requires(!Owning && !std::is_same_v<Deleter, detail::Deleter<T, N>>) configASSERT(handle);
: owner{nullptr} {} }
Queue(const Queue &) = delete; Queue(const Queue &) = delete;
// Moving is unsupported due to being self-referential // Blocks until accepted (INCLUDE_vTaskSuspend: portMAX_DELAY never times out)
Queue(Queue &&) = delete; void push(std::unique_ptr<T> x) {
Queue &operator=(Queue &&) = delete; T *ptr = x.release();
xQueueSend(handle, &ptr, portMAX_DELAY);
// 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;
} }
const T *peek() const { std::unique_ptr<T> pop(TickType_t wait = portMAX_DELAY) {
if (isEmpty()) T *ptr = nullptr;
return nullptr; xQueueReceive(handle, &ptr, wait);
return std::unique_ptr<T>(ptr);
return buf[mask(read)];
} }
std::unique_ptr<T, Deleter> pop() { // The front item stays owned by the queue
if (isEmpty()) const T *peek(TickType_t wait = portMAX_DELAY) const {
return nullptr; T *ptr = nullptr;
xQueuePeek(handle, &ptr, wait);
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) return ptr;
return {buf[mask(read++)], Deleter{}};
else if constexpr (Owning)
return {buf[mask(read++)], Deleter{this}};
else
return {buf[mask(read++)], Deleter{owner}};
} }
private: private:
uint8_t mask(uint8_t pos) const { return pos & (N - 1); } QueueHandle_t handle;
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;
}; };
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 <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <new>
#include <utility> #include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/board.h" #include "hal/board.h"
@@ -18,17 +20,30 @@
#include "queue.hpp" #include "queue.hpp"
#include "stdshim.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; using namespace avclan;
namespace { namespace {
constinit Queue<Frame, CACHE_SIZE> incoming; using Periph = Peripheral<CDChanger>;
constinit Queue<Frame, CACHE_SIZE> outgoing;
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 hexChars[2];
uint8_t hexDigit = 0; // current digit being written to hexChars 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 Setup();
void print_help(); void print_help();
} // namespace
int main() { [[noreturn]] void vReceiverTask(void *pvParameters) {
Bus phy;
using enum Action;
using enum Device;
Peripheral<CDChanger> peripheral(phy, 0x360);
using Print = Frame::Print; using Print = Frame::Print;
auto &[peripheral, incoming, outgoing] =
Setup(); *static_cast<Context *>(pvParameters);
print_help();
while (true) { while (true) {
if (peripheral.bus_is_active()) { if (auto msg = peripheral.read(Print{.print = printAllFrames,
if (auto msg = peripheral.read(Print{.print = printAllFrames, .binary = printBinary,
.binary = printBinary, .verbose = verbose}))
.verbose = verbose})) incoming.push(std::move(*msg));
incoming.push(std::move(*msg)); }
} }
if (const auto *in = incoming.peek()) { [[noreturn]] void vRoutingTask(void *pvParameters) {
if (auto resp = peripheral.route(*in)) { auto &[peripheral, incoming, outgoing] =
incoming.pop(); *static_cast<Context *>(pvParameters);
if (*resp) { while (true) {
outgoing.push(std::move(*resp)); const Frame *in = incoming.peek();
continue; // route can be long; re-check the bus before poll/send 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()) if (auto msg = peripheral.poll())
outgoing.push(std::move(msg)); outgoing.push(std::move(msg));
}
}
if (auto out = outgoing.pop()) { [[noreturn]] void vSenderTask(void *pvParameters) {
auto result = using Print = Frame::Print;
peripheral.send(std::move(out), Print{.print = printAllFrames, auto &[peripheral, incoming, outgoing] =
.binary = printBinary}); *static_cast<Context *>(pvParameters);
if (auto next = peripheral.react(std::move(result))) while (true) {
outgoing.push(std::move(next)); 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; [[noreturn]] void vREPLTask(void *pvParameters) {
} using enum Action;
using enum Device;
// stdin must be non-blocking: yielding EOF when idle/empty auto &[peripheral, incoming, outgoing] =
if (int readkey = getchar(); readkey != EOF) { *static_cast<Context *>(pvParameters);
while (true) {
if (int readkey = fgetc(stdin); readkey != EOF) {
switch (readkey) { switch (readkey) {
case '?': print_help(); break; case '?': print_help(); break;
case 'v': toggle_flag(&verbose, "Verbose errors:"); break; case 'v': toggle_flag(&verbose, "Verbose errors:"); break;
@@ -123,7 +147,7 @@ int main() {
case 'x': set_flag(&printBinary, false, "Binary:"); break; case 'x': set_flag(&printBinary, false, "Binary:"); break;
case 'E': // Beep case 'E': // Beep
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) { if (auto out = Frame::acquire()) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -138,7 +162,7 @@ int main() {
puts("!! failed Frame alloc for Beep !! "); puts("!! failed Frame alloc for Beep !! ");
break; break;
case 'P': case 'P':
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) { if (auto out = Frame::acquire()) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -180,6 +204,16 @@ int main() {
while (peripheral.device<CDChanger>().media_busy()) {} while (peripheral.device<CDChanger>().media_busy()) {}
puts("end"); puts("end");
break; break;
case '+':
peripheral.get_bus().deafen(true);
peripheral.get_bus().set_dominant();
puts("Set bus dominant...");
break;
case '-':
peripheral.get_bus().set_recessive();
peripheral.get_bus().deafen(false);
puts("Set bus recessive...");
break;
#ifdef MEASURE_BUS #ifdef MEASURE_BUS
case 'M': peripheral.get_bus().measure(); break; case 'M': peripheral.get_bus().measure(); break;
#endif #endif
@@ -211,12 +245,23 @@ int main() {
hexChars[0] = hexChars[1] = 0; hexChars[0] = hexChars[1] = 0;
seqIsUnicast = false; seqIsUnicast = false;
break; break;
case '\r': // Eat carriage return too
if (readBinary)
goto DEFAULT;
[[fallthrough]];
case '\n': case '\n':
if (readSeq && !readBinary && seqIdx == 0) {
// Nothing to send, so leave the mode instead: an escape for an
// entry started by accident.
readSeq = false;
hexDigit = hexChars[0] = hexChars[1] = 0;
printAllFrames = lastPrintAllFrames;
break;
}
if (readSeq && seqIdx > 0) { if (readSeq && seqIdx > 0) {
if (readBinary) { if (readBinary) {
if (data_tmp[seqIdx - 1] == 0x17) { if (data_tmp[seqIdx - 1] == 0x17) {
if (auto out = if (auto out = Frame::acquire()) {
std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (out->parse(data_tmp, --seqIdx) == if (out->parse(data_tmp, --seqIdx) ==
Frame::Error::Parse{0}) { Frame::Error::Parse{0}) {
out->reaction = 1; out->reaction = 1;
@@ -229,7 +274,7 @@ int main() {
goto DEFAULT; // reading binary and this is a real data byte; goto DEFAULT; // reading binary and this is a real data byte;
// fall through to default // fall through to default
} else { // ASCII message } else { // ASCII message
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) { if (auto out = Frame::acquire()) {
const uint8_t sendLen = const uint8_t sendLen =
seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH; seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH;
out->is_unicast = seqIsUnicast; out->is_unicast = seqIsUnicast;
@@ -270,6 +315,10 @@ int main() {
if (readBinary) { if (readBinary) {
data_tmp[seqIdx++] = readkey; data_tmp[seqIdx++] = readkey;
} else { } else {
// Only take valid hex digits
if (isxdigit(readkey) == 0)
break;
hexChars[hexDigit++] = readkey; hexChars[hexDigit++] = readkey;
if (hexDigit == 2) { if (hexDigit == 2) {
@@ -294,9 +343,38 @@ int main() {
} }
} }
} // switch (readkey) } // switch (readkey)
} else {
clearerr(stdin);
} // if (readkey != EOF) } // 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 { namespace {
@@ -324,6 +402,8 @@ void print_help() {
"s - MIC skip forward\n" "s - MIC skip forward\n"
"b - MIC skip backward\n" "b - MIC skip backward\n"
"M - Measure bit-timing (pulse-widths and periods)\n" "M - Measure bit-timing (pulse-widths and periods)\n"
"+ - Set bus driven/dominant\n"
"- - Set bus idle/recessive\n"
#endif #endif
"? - Print this message"); "? - Print this message");
} }