39 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
Allen Hill 6c460aa3a0 more infra/clangd updates 2026-08-19 11:45:06 -07:00
Allen Hill 6ce4c2cc21 Improve stdio_write_nonblock perf and indicator printing 2026-08-19 11:38:08 -07:00
Allen Hill b9f7978d3e react to [disk] scan button presses 2026-08-13 20:46:57 -07:00
Allen Hill cd68ee7267 misc infra/clangd updates 2026-08-13 17:55:46 -07:00
Allen Hill ee5b4a955c Update Lua dissector for newly understood bit flag (NEGATIVE time)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:50:21 -07:00
Allen Hill 5d6b5f4ce0 Handle flag for NEGATIVE time; unify time incrementing 2026-08-13 17:47:31 -07:00
Allen Hill 0bef565564 Speed up Frame printing to (finally) solve missing AVCLAN bus frames
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 17:34:15 -07:00
Allen Hill 66dd441610 Max the UART TX buffer size 2026-08-13 16:48:44 -07:00
Allen Hill 3ea7c2d44a Improve correctness of phy_read_startbit invariants/assumptions 2026-08-13 16:31:03 -07:00
Allen Hill 95dae572a0 Tighten up non-reading TCB0 ISR timing 2026-08-13 16:27:22 -07:00
Allen Hill f45a0f5e92 Shorten time to next phy_active check (early continue) 2026-08-13 16:25:50 -07:00
Allen Hill 994759dd39 Free up some ram 2026-08-13 16:24:55 -07:00
Allen Hill eab535db98 fixup 7871e92 2026-08-03 13:30:14 -07:00
Allen Hill f3d0d99ce4 Update Disable_Function_Req response 2026-08-01 13:15:06 -07:00
Allen Hill 76f5da0125 Restore dynamic from device in function change resp 2026-08-01 13:14:38 -07:00
Allen Hill 7871e92dbe Fix Debug builds 2026-07-31 17:53:31 -07:00
Allen Hill fb17b7e75b Update gitignore and silence some tidy warnings 2026-07-25 11:22:33 -07:00
Allen Hill ae539e27f6 Fix some overflow behavior when reading input msgs 2026-07-23 12:41:00 -07:00
Allen Hill a99fe56aae Fix intermittent, early boot short start bit errors 2026-07-21 13:22:19 -07:00
Allen Hill 909915730b Not all error conditions print a message 2026-07-21 12:52:25 -07:00
Allen Hill 5bbbb6c011 Party should be ~private 2026-07-21 12:51:49 -07:00
Allen Hill 8950bd6917 Make b3 const 2026-07-21 12:51:18 -07:00
Allen Hill 56936aed10 Switch to using references where possible 2026-07-17 14:31:54 -07:00
Allen Hill 5208e083f3 Redesign (simplify) top-level avclan interface to use expected/nullable unique_ptr
- Peripheral::read/send now return `expected<unique_ptr<Frame>, Error>`
for a unified success/error interface.
- Peripheral::route returns `expected<unique_ptr<Frame>, Error>` to
distinguish intent: (intentional) non-response vs unable to respond
- Other functions with optional message semantics (handle,poll,react)
return a unique_ptr whose ownership-state indicates response intent
(i.e. send new message)

Bundled necessary changes:
- Switch Frame allocation from global to local (enabled via member
  function new/delete)
- Add new header-library tl::expected to shim avr-libstdcpp

Unrelated: Defensive `continue` added after `route`, to reduce Bus
activity check latency

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:47:47 -07:00
Allen Hill 200a225add Unify frame metadata setting (addrs and owning_device) 2026-07-16 13:35:25 -07:00
Allen Hill 568c2af9e6 Only check tracked files and don't print on empty lint 2026-07-16 13:35:06 -07:00
41 changed files with 3703 additions and 909 deletions
+6 -2
View File
@@ -16,13 +16,17 @@ Checks: |
-modernize-avoid-c-arrays, -modernize-avoid-c-arrays,
-modernize-use-std-print, -modernize-use-std-print,
-readability-magic-numbers, -readability-magic-numbers,
-readability-function-cognitive-complexity,
-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
+14
View File
@@ -0,0 +1,14 @@
CompileFlags:
Remove:
- -mlongcalls
- -fno-shrink-wrap
- -fstrict-volatile-bitfields
- -fno-tree-switch-conversion
- -fanalyzer
- -ffixed-r2
- -ffixed-r3
- -ffixed-r4
- -fzero-init-padding-bits
- -fno-malloc-dce
Add:
- -Wno-missing-field-initializers
+23 -16
View File
@@ -5,7 +5,17 @@
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:alpine-3.22", "image": "mcr.microsoft.com/devcontainers/base:alpine-3.22",
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
"features": {}, "features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {}
},
"mounts": [
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached",
// ~/.claude/skills/* are symlinks into ~/.agents/skills — mount it too or they dangle
"source=${localEnv:HOME}/.agents,target=/home/vscode/.agents,type=bind,consistency=cached"
],
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
@@ -17,28 +27,25 @@
"extensions": [ "extensions": [
"ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-extension-pack",
"rockcat.avr-support", "rockcat.avr-support",
"harikrishnan94.cxx-compiler-explorer" "harikrishnan94.cxx-compiler-explorer",
"llvm-vs-code-extensions.vscode-clangd",
"Anthropic.claude-code"
], ],
"settings": { "settings": {
"editor.formatOnSave": true,
"C_Cpp.codeAnalysis.clangTidy.checks.disabled": [
"clang-analyzer-core.NullDereference"
],
"C_Cpp.files.exclude": {
"build/**": true
},
"files.associations": { "files.associations": {
"*.S": "avr" "*.S": "avr"
}, },
"cmake.configureOnOpen": true, "clangd.path": "/usr/bin/clangd",
"compilerexplorer.compilationDirectory": "${workspaceFolder}/build" "clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/out/build"
]
} }
} }
} },
// "runArgs": [ "runArgs": [
// "--device=/dev/ttyUSB0", "--device=/dev/ttyUSB0",
// "--device=/dev/ttyUSB1" "--network=host"
// ] ]
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root" // "remoteUser": "root"
} }
+1
View File
@@ -12,3 +12,4 @@ __pycache__/
~* ~*
\#* \#*
/CMakeUserPresets.json /CMakeUserPresets.json
.cache/
+127 -24
View File
@@ -1,20 +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)
add_compile_options(
-Wall -Wswitch-enum -Werror
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
$<$<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")
@@ -47,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.
@@ -65,6 +70,21 @@ add_library(avclan STATIC
src/avclan/bus.cc src/avclan/bus.cc
) )
target_compile_definitions(avclan PUBLIC
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
AVCLAN_MSG_QUEUE_SIZE=${AVCLAN_MSG_QUEUE_SIZE}
)
target_compile_options(avclan PUBLIC
-Wall -Wswitch-enum -Werror
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
# avclan exports its public generic headers (src/avclan) to consumers and # avclan exports its public generic headers (src/avclan) to consumers and
# reaches into src/ for sibling headers (stdio.h, board.h) during its own # reaches into src/ for sibling headers (stdio.h, board.h) during its own
# build. The selected target adds its own per-target include path. # build. The selected target adds its own per-target include path.
@@ -76,8 +96,91 @@ target_include_directories(avclan PUBLIC
# flashing target. Added after the targets above so it can extend them. # flashing target. Added after the targets above so it can extend them.
add_subdirectory(src/avclan/target/${AVCLAN_TARGET}) add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
add_executable(mockingboard if(NOT TARGET mockingboard)
src/sniffer.cc add_executable(mockingboard
) src/sniffer.cc
)
target_link_libraries(mockingboard avclan)
endif()
target_link_libraries(mockingboard avclan) # Report firmware section sizes on every build, when the toolchain provides a
# size tool (CMAKE_SIZE). A standalone custom target (rather than a POST_BUILD
# command on mockingboard) so it prints even when the link is up to date:
# custom targets with no output are always considered stale and re-run.
# add_dependencies orders it after the link.
if(TARGET mockingboard AND CMAKE_SIZE)
add_custom_target(mockingboard-size ALL
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard>
COMMENT "Section sizes: mockingboard firmware"
VERBATIM
)
add_dependencies(mockingboard-size mockingboard)
endif()
# --- Polyfill headers for std facilities the toolchain may lack -------------
# tl::expected / tl::optional back-fill std::expected / std::optional (with
# C++23 monadic ops) when the build's stdlib predates them. Runs after the port
# subdirectory to provide a port-supplied freestanding stdlib via
# CMAKE_REQUIRED_INCLUDES, if necessary.
include(CheckCXXSourceCompiles)
find_program(GIT_EXECUTABLE git REQUIRED)
if(TARGET libstdcpp)
get_target_property(_stdlib_incs libstdcpp INTERFACE_INCLUDE_DIRECTORIES)
set(CMAKE_REQUIRED_INCLUDES ${_stdlib_incs})
endif()
check_cxx_source_compiles("
#include <version>
#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L
#error no std::expected
#endif
int main() { return 0; }" HAVE_STD_EXPECTED)
# check_cxx_source_compiles("
# #include <version>
# #if !defined(__cpp_lib_optional) || __cpp_lib_optional < 202110L
# #error no C++23 std::optional
# #endif
# int main() { return 0; }" HAVE_STD_OPTIONAL)
unset(CMAKE_REQUIRED_INCLUDES)
if(NOT HAVE_STD_EXPECTED)
FetchContent_Declare(
tl_expected
GIT_REPOSITORY https://github.com/TartanLlama/expected.git
GIT_TAG 1770e3559f2f6ea4a5fb4f577ad22aeb30fbd8e4
PATCH_COMMAND ${CMAKE_COMMAND}
-DGIT_EXECUTABLE=${GIT_EXECUTABLE}
-DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/tl-expected-no-exception-header.patch
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/apply-patch.cmake
SYSTEM
)
set(EXPECTED_BUILD_TESTS OFF)
set(EXPECTED_BUILD_PACKAGE OFF)
FetchContent_MakeAvailable(tl_expected)
add_library(tl_expected_hdr INTERFACE)
target_include_directories(tl_expected_hdr
INTERFACE ${tl_expected_SOURCE_DIR}/include)
target_link_libraries(avclan PUBLIC tl_expected_hdr)
endif()
# std/tl::optional not currently used
# if(NOT HAVE_STD_OPTIONAL)
# FetchContent_Declare(
# tl_optional
# GIT_REPOSITORY https://github.com/TartanLlama/optional.git
# GIT_TAG 3a1209de8370bf5fe16362934956144b49591565
# PATCH_COMMAND ${CMAKE_COMMAND}
# -DGIT_EXECUTABLE=${GIT_EXECUTABLE}
# -DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/tl-optional-no-exception-header.patch
# -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/apply-patch.cmake
# SYSTEM
# )
# set(OPTIONAL_BUILD_TESTS OFF)
# set(OPTIONAL_BUILD_PACKAGE OFF)
# FetchContent_MakeAvailable(tl_optional)
# add_library(tl_optional_hdr INTERFACE)
# target_include_directories(tl_optional_hdr
# INTERFACE ${tl_optional_SOURCE_DIR}/include)
# target_link_libraries(avclan PUBLIC tl_optional_hdr)
# endif()
+85 -17
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,
@@ -22,7 +40,7 @@
"hidden": true, "hidden": true,
"description": "AVR ATtiny3216 cross-compile toolchain + matching port", "description": "AVR ATtiny3216 cross-compile toolchain + matching port",
"toolchainFile": "${sourceDir}/cmake/avr-gcc-toolchain.cmake", "toolchainFile": "${sourceDir}/cmake/avr-gcc-toolchain.cmake",
"binaryDir": "${sourceDir}/out/build/attiny3216", "binaryDir": "${sourceDir}/out/build/avr-attiny3216",
"cacheVariables": { "cacheVariables": {
"AVCLAN_TARGET": "avr-attiny3216", "AVCLAN_TARGET": "avr-attiny3216",
"FREQSEL": "20MHz", "FREQSEL": "20MHz",
@@ -31,31 +49,64 @@
} }
}, },
{ {
"name": "debug-base", "name": "pico2",
"hidden": true, "hidden": true,
"description": "Debug build settings", "binaryDir": "${sourceDir}/out/build/pico2",
"generator": "Unix Makefiles", "inherits": "debug-base",
"binaryDir": "${sourceDir}/out/build",
"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"
}
},
{
"name": "attiny3216-relwithdebinfo",
"hidden": true,
"description": "ATtiny3216 RelWithDebInfo build,",
"inherits": [
"avr-attiny3216",
"relwithdebinfo-base"
]
},
{
"name": "attiny3216-debug",
"hidden": true,
"description": "ATtiny3216 Debug build",
"inherits": [
"avr-attiny3216",
"debug-base"
]
},
{ {
"name": "attiny3216-relwithdebinfo-usb0", "name": "attiny3216-relwithdebinfo-usb0",
"displayName": "ATtiny3216 RelWithDebInfo (ttyUSB0)", "displayName": "ATtiny3216 RelWithDebInfo (ttyUSB0)",
"description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB0", "description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB0",
"inherits": [ "inherits": [
"avr-attiny3216", "attiny3216-relwithdebinfo",
"relwithdebinfo-base",
"usb0" "usb0"
] ]
}, },
@@ -64,8 +115,25 @@
"displayName": "ATtiny3216 RelWithDebInfo (ttyUSB1)", "displayName": "ATtiny3216 RelWithDebInfo (ttyUSB1)",
"description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB1", "description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB1",
"inherits": [ "inherits": [
"avr-attiny3216", "attiny3216-relwithdebinfo",
"relwithdebinfo-base", "usb1"
]
},
{
"name": "attiny3216-debug-usb0",
"displayName": "ATtiny3216 Debug (ttyUSB0)",
"description": "ATtiny3216 Debug build, program over /dev/ttyUSB0",
"inherits": [
"attiny3216-debug",
"usb0"
]
},
{
"name": "attiny3216-debug-usb1",
"displayName": "ATtiny3216 Debug (ttyUSB1)",
"description": "ATtiny3216 Debug build, program over /dev/ttyUSB1",
"inherits": [
"attiny3216-debug",
"usb1" "usb1"
] ]
} }
+18
View File
@@ -0,0 +1,18 @@
# Idempotent patch driver for FetchContent PATCH_COMMAND steps.
#
# Usage: cmake -DGIT_EXECUTABLE=<git> -DPATCH_FILE=<patch> -P apply-patch.cmake
# (run with CWD = the populated source directory, as PATCH_COMMAND does).
#
# FetchContent re-runs PATCH_COMMAND after its update step on reconfigure, so a
# bare `git apply` fails the second time around. Skip if the patch is already
# applied (the reverse-apply dry run succeeds); otherwise apply it for real.
execute_process(
COMMAND "${GIT_EXECUTABLE}" apply --reverse --check --ignore-whitespace
"${PATCH_FILE}"
RESULT_VARIABLE _already_applied
OUTPUT_QUIET ERROR_QUIET)
if(NOT _already_applied EQUAL 0)
execute_process(
COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace "${PATCH_FILE}"
COMMAND_ERROR_IS_FATAL ANY)
endif()
+3
View File
@@ -13,6 +13,9 @@
########################################################################## ##########################################################################
find_program(AVR_CC avr-gcc REQUIRED) find_program(AVR_CC avr-gcc REQUIRED)
find_program(AVR_CXX avr-g++ REQUIRED) find_program(AVR_CXX avr-g++ REQUIRED)
# Section-size reporter; consumed by an optional POST_BUILD in the top-level
# CMakeLists (left unset -> no size report) so the top level stays HW-agnostic.
find_program(CMAKE_SIZE avr-size)
set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR avr) set(CMAKE_SYSTEM_PROCESSOR avr)
+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})
@@ -0,0 +1,36 @@
diff --git a/include/tl/expected.hpp b/include/tl/expected.hpp
index 59e59aa..75857bd 100644
--- a/include/tl/expected.hpp
+++ b/include/tl/expected.hpp
@@ -20,7 +20,7 @@
#define TL_EXPECTED_VERSION_MINOR 3
#define TL_EXPECTED_VERSION_PATCH 1
-#include <exception>
+#include <cstdlib>
#include <functional>
#include <type_traits>
#include <utility>
@@ -222,7 +222,7 @@ static constexpr unexpect_t unexpect{};
#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
#define TL_EXPECTED_THROW_EXCEPTION(e) throw((e));
#else
-#define TL_EXPECTED_THROW_EXCEPTION(e) std::terminate();
+#define TL_EXPECTED_THROW_EXCEPTION(e) std::abort();
#endif
namespace detail {
@@ -1249,11 +1249,11 @@ template <class T, class E> struct expected_default_ctor_base<T, E, false> {
};
} // namespace detail
-template <class E> class bad_expected_access : public std::exception {
+template <class E> class bad_expected_access {
public:
explicit bad_expected_access(E e) : m_val(std::move(e)) {}
- virtual const char *what() const noexcept override {
+ virtual const char *what() const noexcept {
return "Bad expected access";
}
@@ -0,0 +1,21 @@
diff --git a/include/tl/optional.hpp b/include/tl/optional.hpp
index e9c59c2..1ff3d4a 100644
--- a/include/tl/optional.hpp
+++ b/include/tl/optional.hpp
@@ -21,7 +21,6 @@
#define TL_OPTIONAL_VERSION_MINOR 1
#define TL_OPTIONAL_VERSION_PATCH 0
-#include <exception>
#include <functional>
#include <new>
#include <type_traits>
@@ -664,7 +663,7 @@ struct nullopt_t {
static constexpr nullopt_t nullopt{nullopt_t::do_not_use{},
nullopt_t::do_not_use{}};
-class bad_optional_access : public std::exception {
+class bad_optional_access {
public:
bad_optional_access() = default;
const char *what() const noexcept { return "Optional has no value"; }
+1 -1
View File
@@ -1,2 +1,2 @@
sudo apk add make cmake avrdude avr-libc gcc-avr tshark sudo apk add make cmake avrdude avr-libc gcc-avr tshark clang-extra-tools
+34 -6
View File
@@ -203,14 +203,16 @@ local function bcd2dec(b)
end end
-- Format a BCD-encoded mm:ss pair, falling back to raw hex when a byte isn't -- Format a BCD-encoded mm:ss pair, falling back to raw hex when a byte isn't
-- valid BCD (e.g. a 0xff sentinel for "time unknown"). -- valid BCD (e.g. a 0xff sentinel for "time unknown"). `negative` (the CD
local function bcd_time(min_b, sec_b) -- NEGATIVE flag) only controls the sign, so it applies to the raw fallback too.
local function bcd_time(min_b, sec_b, negative)
local sign = negative and "-" or ""
local m = bcd2dec(min_b) local m = bcd2dec(min_b)
local s = bcd2dec(sec_b) local s = bcd2dec(sec_b)
if m == nil or s == nil then if m == nil or s == nil then
return string.format("%02x:%02x (raw)", min_b, sec_b) return string.format("%s%02x:%02x (raw)", sign, min_b, sec_b)
end end
return string.format("%02d:%02d", m, s) return string.format("%s%02d:%02d", sign, m, s)
end end
local known_devices = { local known_devices = {
@@ -478,6 +480,15 @@ local f_cd_flag_repeat = ProtoField.bool("avclan.cd.flags.repeat", "REPEAT", 8,
local f_cd_flag_disk_scan = ProtoField.bool("avclan.cd.flags.disk_scan", "DISK_SCAN", 8, nil, 0x20) local f_cd_flag_disk_scan = ProtoField.bool("avclan.cd.flags.disk_scan", "DISK_SCAN", 8, nil, 0x20)
local f_cd_flag_scan = ProtoField.bool("avclan.cd.flags.scan", "SCAN", 8, nil, 0x40) local f_cd_flag_scan = ProtoField.bool("avclan.cd.flags.scan", "SCAN", 8, nil, 0x40)
-- Second CD flags byte (last byte of the playback status frame). Resting value
-- is 0x80; 0x40 (NEGATIVE) makes the reported play time count below zero, as
-- seen when rewinding past the start of a track: ..., 00:01, 00:00, then the
-- byte flips to 0xc0 and the time climbs again as -00:01, -00:02, ...
-- [msgdumps/rewind-negative.txt].
local f_cd_flags2 = ProtoField.uint8("avclan.cd.flags2", "CD player flags (byte 2)")
local f_cd_flag_negative = ProtoField.bool("avclan.cd.flags2.negative", "NEGATIVE", 8, nil, 0x40)
local f_cd_flag2_unknown7 = ProtoField.bool("avclan.cd.flags2.unknown7", "UNKNOWN7", 8, nil, 0x80)
local f_tape_present = ProtoField.uint8("avclan.tape.present", "Tape deck slot", base.HEX, {[0x01] = "FILLED", [0x00] = "EMPTY"}) local f_tape_present = ProtoField.uint8("avclan.tape.present", "Tape deck slot", base.HEX, {[0x01] = "FILLED", [0x00] = "EMPTY"})
local f_tape_state = ProtoField.uint8("avclan.tape.state", "Tape deck state") local f_tape_state = ProtoField.uint8("avclan.tape.state", "Tape deck state")
local f_tape_seeking_rev = ProtoField.bool("avclan.tape.state.seeking_rev", "SEEKING_REVERSE", 8, nil, 0x01) local f_tape_seeking_rev = ProtoField.bool("avclan.tape.state.seeking_rev", "SEEKING_REVERSE", 8, nil, 0x01)
@@ -553,6 +564,9 @@ avclanproto.fields = {
f_cd_flag_repeat, f_cd_flag_repeat,
f_cd_flag_disk_scan, f_cd_flag_disk_scan,
f_cd_flag_scan, f_cd_flag_scan,
f_cd_flags2,
f_cd_flag_negative,
f_cd_flag2_unknown7,
f_tape_present, f_tape_present,
f_tape_state, f_tape_state,
f_tape_seeking_rev, f_tape_seeking_rev,
@@ -809,7 +823,7 @@ end
local function decode_cd(subtree, buffer, offset, action) local function decode_cd(subtree, buffer, offset, action)
if action == known_actions_names["PLAYBACK_STATUS"] or if action == known_actions_names["PLAYBACK_STATUS"] or
action == known_actions_names["PLAYBACK_RESP"] then action == known_actions_names["PLAYBACK_RESP"] then
local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player") local cdtree = subtree:add(avclanproto, buffer(offset,-1), "Device: CD player")
local cd_slots = cdtree:add(f_cd_slots, buffer(offset+3,1)) local cd_slots = cdtree:add(f_cd_slots, buffer(offset+3,1))
cd_slots:add(f_cd_slot1, buffer(offset+3,1)) cd_slots:add(f_cd_slot1, buffer(offset+3,1))
cd_slots:add(f_cd_slot2, buffer(offset+3,1)) cd_slots:add(f_cd_slot2, buffer(offset+3,1))
@@ -825,6 +839,12 @@ local function decode_cd(subtree, buffer, offset, action)
cd_state:add(f_cd_playback, buffer(offset+4,1)) cd_state:add(f_cd_playback, buffer(offset+4,1))
cd_state:add(f_cd_seeking_track, buffer(offset+4,1)) cd_state:add(f_cd_seeking_track, buffer(offset+4,1))
cd_state:add(f_cd_loading, buffer(offset+4,1)) cd_state:add(f_cd_loading, buffer(offset+4,1))
-- The sign of the play time lives in the trailing flags byte, so read it
-- before formatting the time (older/shorter reports may omit it).
local negative = false
if buffer:len() > offset+10 then
negative = bit.band(buffer(offset+10,1):uint(), 0x40) ~= 0
end
local cd_status = cdtree:add(avclanproto, buffer(offset+5,-1), "") local cd_status = cdtree:add(avclanproto, buffer(offset+5,-1), "")
cd_status:add(f_cd_disc, buffer(offset+5,1)) cd_status:add(f_cd_disc, buffer(offset+5,1))
cd_status:add(f_cd_track, buffer(offset+6,1)) cd_status:add(f_cd_track, buffer(offset+6,1))
@@ -832,7 +852,7 @@ local function decode_cd(subtree, buffer, offset, action)
cd_status:add(f_cd_sec, buffer(offset+8,1)) cd_status:add(f_cd_sec, buffer(offset+8,1))
cd_status:append_text("Disc " .. field_cd_disc().value .. ", ") cd_status:append_text("Disc " .. field_cd_disc().value .. ", ")
cd_status:append_text("track " .. tostring(buffer(offset+6,1)):gsub("(.)(.)", "%1%2") .. ", ") cd_status:append_text("track " .. tostring(buffer(offset+6,1)):gsub("(.)(.)", "%1%2") .. ", ")
cd_status:append_text("time " .. bcd_time(buffer(offset+7,1):uint(), buffer(offset+8,1):uint())) cd_status:append_text("time " .. bcd_time(buffer(offset+7,1):uint(), buffer(offset+8,1):uint(), negative))
local cd_flags = cdtree:add(f_cd_flags, buffer(offset+9,1)) local cd_flags = cdtree:add(f_cd_flags, buffer(offset+9,1))
cd_flags:add(f_cd_flag_disk_random, buffer(offset+9,1)) cd_flags:add(f_cd_flag_disk_random, buffer(offset+9,1))
cd_flags:add(f_cd_flag_random, buffer(offset+9,1)) cd_flags:add(f_cd_flag_random, buffer(offset+9,1))
@@ -840,6 +860,14 @@ local function decode_cd(subtree, buffer, offset, action)
cd_flags:add(f_cd_flag_repeat, buffer(offset+9,1)) cd_flags:add(f_cd_flag_repeat, buffer(offset+9,1))
cd_flags:add(f_cd_flag_disk_scan, buffer(offset+9,1)) cd_flags:add(f_cd_flag_disk_scan, buffer(offset+9,1))
cd_flags:add(f_cd_flag_scan, buffer(offset+9,1)) cd_flags:add(f_cd_flag_scan, buffer(offset+9,1))
if buffer:len() > offset+10 then
local cd_flags2 = cdtree:add(f_cd_flags2, buffer(offset+10,1))
cd_flags2:add(f_cd_flag_negative, buffer(offset+10,1))
cd_flags2:add(f_cd_flag2_unknown7, buffer(offset+10,1))
if negative then
cd_flags2:append_text(" (play time is negative)")
end
end
elseif action == known_actions_names["LOADING_STATUS"] or elseif action == known_actions_names["LOADING_STATUS"] or
action == known_actions_names["LOADING_RESP"] then action == known_actions_names["LOADING_RESP"] then
local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player") local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player")
+22 -12
View File
@@ -49,6 +49,7 @@ still bypasses it.
import argparse import argparse
import os import os
import re import re
import subprocess
import sys import sys
# ---- locations ------------------------------------------------------------- # ---- locations -------------------------------------------------------------
@@ -227,10 +228,16 @@ def find_address_spec(root):
def iter_src_files(root): def iter_src_files(root):
for dirpath, _dirs, files in os.walk(os.path.join(root, SRC_REL)): """List source files under src/, via `git ls-files` so paths excluded by
for fn in sorted(files): .gitignore (e.g. the ESP32 port's CMake build directory, which vendors
if fn.endswith(SRC_EXTS): FetchContent'd third-party code) are never walked into."""
yield os.path.join(dirpath, fn) out = subprocess.run(
["git", "-C", root, "ls-files", "-z", "--cached", "--others",
"--exclude-standard", "--", SRC_REL],
capture_output=True, check=True, text=True,
).stdout
paths = sorted(p for p in out.split("\0") if p.endswith(SRC_EXTS))
return [os.path.join(root, p) for p in paths]
# ---- lint ------------------------------------------------------------------ # ---- lint ------------------------------------------------------------------
@@ -471,8 +478,6 @@ def main():
lua_path = os.path.join(root, LUA_REL) lua_path = os.path.join(root, LUA_REL)
lua_lines = read_lines(lua_path) lua_lines = read_lines(lua_path)
specs, have_address = build_specs(root) specs, have_address = build_specs(root)
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
# ---- lint (all modes) ---- # ---- lint (all modes) ----
lint_failed = False lint_failed = False
@@ -487,6 +492,8 @@ def main():
print_lint(spec, LUA_REL, problems) print_lint(spec, LUA_REL, problems)
if args.lint: if args.lint:
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
if lint_failed: if lint_failed:
print("lint: FAILED") print("lint: FAILED")
return 1 return 1
@@ -503,6 +510,8 @@ def main():
has_drift = any(not d.clean() for _s, _e, d in drifts) has_drift = any(not d.clean() for _s, _e, d in drifts)
if args.fix: if args.fix:
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
if lint_failed: if lint_failed:
print("fix aborted: resolve the dissector name problems above first") print("fix aborted: resolve the dissector name problems above first")
return 1 return 1
@@ -525,16 +534,17 @@ def main():
print("fix: applied" if has_drift else "fix: already in sync") print("fix: applied" if has_drift else "fix: already in sync")
return 0 return 0
# ---- default: report ---- # ---- default: report (silent on success -- this is the pre-commit hook) ----
if not (lint_failed or has_drift):
return 0
if not have_address:
print("note: no Address enum found in src/ -- addresses not synced")
for spec, _entries, drift in drifts: for spec, _entries, drift in drifts:
if not drift.clean(): if not drift.clean():
print("%s drift:" % spec["name"]) print("%s drift:" % spec["name"])
print_drift(drift) print_drift(drift)
if lint_failed or has_drift: print("out of sync -- run: scripts/sync_avclan_enums.py --fix")
print("out of sync -- run: scripts/sync_avclan_enums.py --fix") return 1
return 1
print("enums in sync with the dissector")
return 0
if __name__ == "__main__": if __name__ == "__main__":
+18 -6
View File
@@ -121,6 +121,12 @@ enum AVCLAN_ENUM_CLASS MediaAction : uint8_t {
}; };
#ifdef __cplusplus #ifdef __cplusplus
enum class Device : uint8_t;
// Sentinel for a frame with no owning device.
// Compile-time collision check lives in device.hpp
inline constexpr Device NoDevice = Device{0xFF};
namespace detail { namespace detail {
struct Error { struct Error {
#endif #endif
@@ -137,9 +143,12 @@ struct Error {
BAD_CONTROLLER_PARITY, BAD_CONTROLLER_PARITY,
BAD_CONTROL_PARITY, BAD_CONTROL_PARITY,
BAD_PARITY, // generic bad parity has max severity BAD_PARITY, // generic bad parity has max severity
STARTBIT_TOO_SHORT, STARTBIT_MISSED,
STARTBIT_MALFORMED,
STARTBIT_TOO_LONG, STARTBIT_TOO_LONG,
BAD_STARTBIT, BAD_STARTBIT,
NO_FRAME,
POOL_EMPTY, // non-bus error
}; };
enum AVCLAN_ENUM_CLASS Send : uint8_t { enum AVCLAN_ENUM_CLASS Send : uint8_t {
@@ -147,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,
}; };
@@ -160,13 +172,13 @@ struct Error {
#ifdef __cplusplus #ifdef __cplusplus
}; };
#endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t { struct SendError {
bit_zero = 0x00, Device owning_device;
bit_one = 0x01, uint8_t reaction;
bit_start = 0x10 Error::Send err;
}; };
#endif
#ifdef __cplusplus #ifdef __cplusplus
} // namespace detail } // namespace detail
+161 -209
View File
@@ -26,30 +26,31 @@
| 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 "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/phy.h" #include "hal/phy.h"
#include "stdshim.hpp"
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 {
@@ -68,217 +69,142 @@ 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 *in, Frame::Print print) -> Read { auto Bus::read(Frame::Print print)
-> 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 = Frame::acquire();
if (!in) {
err.type = POOL_EMPTY;
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 BAD_STARTBIT: fputs("bad start bit (other)", stdout); break; case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_TOO_SHORT: fputs("bad start bit (short)", stdout); break; case STARTBIT_MISSED: fputs("missed start bit", stdout); break;
case STARTBIT_MALFORMED:
fputs("malformed start bit (external cause)", stdout);
break;
case STARTBIT_TOO_LONG: fputs("bad start bit (long)", stdout); break; case STARTBIT_TOO_LONG: fputs("bad start bit (long)", stdout); break;
case BAD_CONTROLLER_PARITY: case BAD_CONTROLLER_PARITY:
fputs("reading controller addr.", stdout); fputs("reading controller addr.", stdout);
@@ -288,109 +214,120 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
goto VERBOSE; goto VERBOSE;
case BAD_CONTROL_PARITY: fputs("reading control", stdout); goto VERBOSE; case BAD_CONTROL_PARITY: fputs("reading control", stdout); goto VERBOSE;
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:
if (print.verbose) { if (print.verbose) {
printf("; read 0x%02X", err.val); printf("; read 0x%02X:", err.val);
} }
} }
putchar('\n'); putchar('\n');
} }
// Only print if some data has been correctly received // Only print if some data has been correctly received
if (print.print && (err.errno < STARTBIT_TOO_SHORT)) { 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);
} }
return err.errno; if (err.type != Read{0})
return unexpected(err.type);
return in;
} }
auto Bus::send(const Frame *out, Frame::Print print) -> Send { 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++) {
// Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem
// necessary (i.e. This deviates from the previous broadcast specific
// function that sent an extra `1` bit after each byte/parity)
// Explanation for why audio-group broadcast state report isn't working?
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;
@@ -399,18 +336,33 @@ 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}; };
#ifndef NDEBUG #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
+15 -4
View File
@@ -48,9 +48,11 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
namespace avclan { namespace avclan {
@@ -67,18 +69,27 @@ 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
Error::Read read(uint16_t address, Frame *in, Frame::Print print); expected<std::unique_ptr<Frame>, Error::Read> read(Frame::Print print);
Error::Send send(const Frame *out, Frame::Print print); Error::Send send(const Frame &out, Frame::Print print);
private: private:
class Handle; class Handle;
+256 -214
View File
@@ -3,18 +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 <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),
@@ -27,73 +31,83 @@ 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
const uint8_t *data = &in->data[1]; std::optional<MediaAction> media;
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++;
const auto action = static_cast<Action>(*data++); const auto action = static_cast<Action>(*data++);
static const uint8_t function_change_resp[] = { static const uint8_t function_change_resp[] = {
0x00, to_underlying(Device::CD_CHANGER), to_underlying(from), 0xFF, 0x01}; 0x00, to_underlying(Device::CD_CHANGER), 0xFF, 0xFF, 0x01};
using enum Action; using enum Action;
#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);
memcpy(out->data, function_change_resp, sizeof(function_change_resp)); memcpy(out.data, function_change_resp, sizeof(function_change_resp));
out->data[3] = to_underlying(Enable_Function_Resp); out.data[2] = to_underlying(from);
out.data[3] = to_underlying(Enable_Function_Resp);
state = 0; state = 0;
flags2 = 0x80; flags2 = 0x80;
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case Disable_Function_Req: case Disable_Function_Req:
// No change/response needed if we're already not playing // Head unit always expects a response, but the state change can be
// conditional
out.is_unicast = true;
out.length = sizeof(function_change_resp);
memcpy(out.data, function_change_resp, sizeof(function_change_resp));
out.data[2] = to_underlying(from);
out.data[3] = to_underlying(Disable_Function_Resp);
if (isPlaying()) { if (isPlaying()) {
stopPlaying(); media = stopPlaying();
out->length = sizeof(function_change_resp);
memcpy(out->data, function_change_resp, sizeof(function_change_resp));
out->data[3] = to_underlying(Disable_Function_Resp);
state = 0; state = 0;
flags2 = 0x80; flags2 = 0x80;
out->is_unicast = true; out.reaction = r_StatusReport;
out->reaction = r_StatusReport; } else
} out.reaction = r_SendOnly;
break; break;
case Eject: { case Eject: {
// "Eject" label is multiply wrong; proper meaning unclear: // "Eject" label is multiply wrong; proper meaning unclear:
@@ -106,20 +120,20 @@ void CDChanger::handle(const Frame *in, Frame *out) {
if (static_cast<bool>(state & SEEKING)) { // FF/RW button released if (static_cast<bool>(state & SEEKING)) { // FF/RW button released
state &= ~SEEKING; state &= ~SEEKING;
} else { } else {
out->is_unicast = true; out.is_unicast = true;
{ {
const uint8_t msg[] = {0x00, to_underlying(Device::CD_CHANGER), const uint8_t msg[] = {0x00, to_underlying(Device::CD_CHANGER),
to_underlying(Device::CMD_SW), to_underlying(Device::CMD_SW),
to_underlying(Insertion), 0x01}; to_underlying(Insertion), 0x01};
out->length = sizeof(msg); out.length = sizeof(msg);
memcpy(out->data, msg, sizeof(msg)); memcpy(out.data, msg, sizeof(msg));
} }
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
} }
break; break;
} }
case Initial_Report_Req: { case Initial_Report_Req: {
out->is_unicast = true; out.is_unicast = true;
// No knowledge/understanding of field meaning/interpretation // No knowledge/understanding of field meaning/interpretation
const uint8_t cdinitreport_resp[] = {0x00, const uint8_t cdinitreport_resp[] = {0x00,
to_underlying(Device::CD_CHANGER), to_underlying(Device::CD_CHANGER),
@@ -130,29 +144,29 @@ void CDChanger::handle(const Frame *in, Frame *out) {
0x10, 0x10,
0x01, 0x01,
0x01}; 0x01};
out->length = sizeof(cdinitreport_resp); out.length = sizeof(cdinitreport_resp);
memcpy(out->data, cdinitreport_resp, sizeof(cdinitreport_resp)); memcpy(out.data, cdinitreport_resp, sizeof(cdinitreport_resp));
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
case Playback_Req: case Playback_Req:
out->data[0] = 0x00; out.data[0] = 0x00;
out->data[1] = to_underlying(Device::CD_CHANGER); out.data[1] = to_underlying(Device::CD_CHANGER);
out->data[2] = to_underlying(from); out.data[2] = to_underlying(from);
out->data[3] = to_underlying(Playback_Resp); out.data[3] = to_underlying(Playback_Resp);
out->length = WIRE_SIZE + 4; out.length = WIRE_SIZE + 4;
serialize(&out->data[4]); serialize(&out.data[4]);
out->is_unicast = true; out.is_unicast = true;
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
case Loading_Req: case Loading_Req:
out->data[0] = 0x00; out.data[0] = 0x00;
out->length = sizeof(cdloading_resp) + 1; out.length = sizeof(cdloading_resp) + 1;
memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp)); memcpy(&out.data[1], cdloading_resp, sizeof(cdloading_resp));
out->data[2] = to_underlying(from); out.data[2] = to_underlying(from);
out->data[3] = to_underlying(Loading_Resp); out.data[3] = to_underlying(Loading_Resp);
out->is_unicast = true; out.is_unicast = true;
out->reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
case Track_Seek_Up: case Track_Seek_Up:
state = SEEKING_TRACK; state = SEEKING_TRACK;
@@ -160,256 +174,287 @@ void CDChanger::handle(const Frame *in, Frame *out) {
++track; ++track;
else else
track = 1; track = 1;
mins = 0xff; time.reset();
secs = 0x7f;
flags2 = 0xc0;
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 (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 = 0xc0;
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;
secs += TIME_SKIP; seek(TIME_SKIP);
if (secs > 59) {
secs -= 60;
++mins;
}
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 out.reaction = r_SendOnly;
// is ~1 sec from now
out->reaction = r_SendOnly;
break; break;
} }
case Track_Rewind: { case Track_Rewind: {
state |= SEEKING; state |= SEEKING;
if (secs < TIME_SKIP) { seek(-TIME_SKIP);
if (mins > 0) {
const uint8_t dif = TIME_SKIP - secs;
secs = 60 - dif;
--mins;
} else {
mins = 0;
secs = 0;
}
} else
secs -= 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 out.reaction = r_SendOnly;
// is ~1 sec from now
out->reaction = r_SendOnly;
break; break;
} }
case CD_Enable_Random: case CD_Enable_Random:
flags |= RANDOM; flags |= RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Random: case CD_Disable_Random:
flags &= ~RANDOM; flags &= ~RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_Repeat: case CD_Enable_Repeat:
flags |= REPEAT; flags |= REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Repeat: case CD_Disable_Repeat:
flags &= ~REPEAT; flags &= ~REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_Disk_Random: case CD_Enable_Disk_Random:
flags |= DISK_RANDOM; flags |= DISK_RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Disk_Random: case CD_Disable_Disk_Random:
flags &= ~DISK_RANDOM; flags &= ~DISK_RANDOM;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_Disk_Repeat: case CD_Enable_Disk_Repeat:
flags |= DISK_REPEAT; flags |= DISK_REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Disable_Disk_Repeat: case CD_Disable_Disk_Repeat:
flags &= ~DISK_REPEAT; flags &= ~DISK_REPEAT;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out->reaction = r_StatusReport; out.reaction = r_StatusReport;
break;
case CD_Enable_Scan:
flags |= SCAN;
generateStatus(out, true, Device::CMD_SW);
out.reaction = r_StatusReport;
break;
case CD_Disable_Scan:
flags &= ~SCAN;
generateStatus(out, true, Device::CMD_SW);
out.reaction = r_StatusReport;
break;
case CD_Enable_Disk_Scan:
flags |= DISK_SCAN;
generateStatus(out, true, Device::CMD_SW);
out.reaction = r_StatusReport;
break;
case CD_Disable_Disk_Scan:
flags &= ~DISK_SCAN;
generateStatus(out, true, Device::CMD_SW);
out.reaction = r_StatusReport;
break; break;
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);
} }
void CDChanger::react(Frame *out, detail::Error::Send err) { std::unique_ptr<Frame>
auto resp = static_cast<reaction_t>(out->reaction); CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_Nothing; std::optional<MediaAction> media;
switch (resp) { std::unique_ptr<Frame> next;
case r_StateReport:
if (err == detail::Error::Send::NAK_ADDRESS && if (const std::lock_guard lock(mutex); !exp) {
++failedStatusReports > 1) { if (exp.error().reaction == to_underlying(r_StateReport) &&
failedStatusReports = 0; exp.error().err == detail::Error::Send::NAK_ADDRESS &&
stopPlaying(); // Disable periodic updates if e.g. no-one's ++failedStatusReports > 1) {
// listening (car was turned off?) failedStatusReports = 0;
} media = stopPlaying(); // Disable periodic updates if e.g. no-one's
break; // listening (car was turned off?)
case r_Ejection: {
const uint8_t play[] = {0x00,
to_underlying(Device::COMM_CTRL),
to_underlying(Device::COMMUNICATION_V1),
to_underlying(Action::Insertion),
to_underlying(Device::CD_CHANGER),
0x01};
out->length = sizeof(play);
memcpy(out->data, play, sizeof(play));
} }
out->reaction = r_Report_Load; } else {
break; auto out = std::move(exp.value());
case r_Report_Load: auto resp = static_cast<reaction_t>(out->reaction);
out->is_unicast = false; out->reaction = r_Nothing;
out->peripheral_addr = 0x1FF; switch (resp) {
out->length = sizeof(cdloading_resp) + 1; case r_Ejection: {
memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); const uint8_t play[] = {0x00,
out->data[1] = to_underlying(Device::STATUS); to_underlying(Device::COMM_CTRL),
out->data[2] = to_underlying(Action::Loading_Status); to_underlying(Device::COMMUNICATION_V1),
out->reaction = r_SendOnly; to_underlying(Action::Insertion),
break; to_underlying(Device::CD_CHANGER),
case r_TrackChange: 0x01};
setTime(0, 0); out->length = sizeof(play);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is memcpy(out->data, play, sizeof(play));
// ~1 sec from now }
[[fallthrough]]; out->reaction = r_Report_Load;
case r_NormalizeState: break;
normalizeState(); case r_Report_Load:
generateStatus(out, false, Device::STATUS); out->is_unicast = false;
out->reaction = r_SendOnly; out->peripheral_addr = 0x1FF;
break; out->length = sizeof(cdloading_resp) + 1;
case r_StartPlaying: memcpy(out->data, cdloading_resp, sizeof(cdloading_resp));
normalizeState(); out->data[1] = to_underlying(Device::STATUS);
generateStatus(out, false, Device::STATUS); out->data[2] = to_underlying(Action::Loading_Status);
out->reaction = r_BeganPlaying; out->reaction = r_SendOnly;
break; break;
case r_BeganPlaying: case r_TrackChange:
startPlaying(); // only start PIT after normalizing state setTime(0ms);
out->reaction = r_Nothing; // Track starts at a whole sec; next status is ~1 sec out
break; xTimerReset(statusTimer, 0);
case r_StatusReport: [[fallthrough]];
generateStatus(out, false, Device::STATUS); case r_NormalizeState:
out->reaction = r_SendOnly; normalizeState();
break; generateStatus(*out, false, Device::STATUS);
case r_SendOnly: [[fallthrough]]; out->reaction = r_SendOnly;
case r_Nothing: [[fallthrough]]; break;
default: out->reaction = r_Nothing; case r_StartPlaying:
normalizeState();
generateStatus(*out, false, Device::STATUS);
out->reaction = r_BeganPlaying;
break;
case r_BeganPlaying:
media = startPlaying(); // only start status timer after normalizing
out->reaction = r_Nothing;
break;
case r_StatusReport:
generateStatus(*out, false, Device::STATUS);
out->reaction = r_SendOnly;
break;
case r_StateReport: [[fallthrough]];
case r_SendOnly: [[fallthrough]];
case r_Nothing: [[fallthrough]];
default: break;
}
if (out->reaction > r_Nothing)
next = std::move(out);
} }
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 = 0xc0; flags2 = 0x80;
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out->reaction = r_StartPlaying; out.reaction = r_StartPlaying;
} }
} }
bool CDChanger::pending() { return cdtimer_pending(); } void CDChanger::emit(Frame &out, uint32_t /*payload*/) {
void CDChanger::resolvepending() { cdtimer_clear(); } statusQueued = false; // Clear first so a tick during emit() queues anew
const std::lock_guard lock(mutex);
void CDChanger::emit(Frame *out, uint16_t peripheral) {
out->owning_device = id; // so react() routes r_StateReport back here
out->peripheral_addr = peripheral;
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out->reaction = r_StateReport; out.reaction = r_StateReport;
} }
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;
}
void CDChanger::incrementTime() {
// Sentinel values (>TWODIGIT_MAX) mean "no time"; leave them alone until
// setTime() replaces them with a real count.
if (secs > TWODIGIT_MAX)
return;
if (secs == 59) {
secs = 0;
if (mins == TWODIGIT_MAX)
mins = 0;
else
mins++;
} else
secs++;
} }
// Used for changed status messages // Used for changed status messages
void CDChanger::generateStatus(Frame *status, bool is_unicast, void CDChanger::generateStatus(Frame &status, bool is_unicast,
Device to) const { Device to) const {
status->is_unicast = is_unicast; status.is_unicast = is_unicast;
if (!is_unicast) if (!is_unicast)
status->peripheral_addr = 0x1FF; status.peripheral_addr = 0x1FF;
status->control = 0xF; status.control = 0xF;
status->length = WIRE_SIZE + ((is_unicast) ? 4 : 3); status.length = WIRE_SIZE + ((is_unicast) ? 4 : 3);
uint8_t *data = status->data; uint8_t *data = status.data;
if (is_unicast) if (is_unicast)
*data++ = 0x00; *data++ = 0x00;
*data++ = to_underlying(Device::CD_CHANGER); *data++ = to_underlying(Device::CD_CHANGER);
@@ -419,13 +464,10 @@ 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);
flags2 = 0x80;
} }
#ifndef NDEBUG #ifndef NDEBUG
+49 -17
View File
@@ -5,7 +5,15 @@
#pragma once #pragma once
#include <atomic>
#include <chrono>
#include <cstdint> #include <cstdint>
#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"
@@ -38,6 +46,7 @@ public:
REPEAT = 1 << 4, REPEAT = 1 << 4,
DISK_SCAN = 1 << 5, DISK_SCAN = 1 << 5,
SCAN = 1 << 6, SCAN = 1 << 6,
NEGATIVE = 1 << 6,
}; };
/// Message state machine /// Message state machine
@@ -58,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 handle(const Frame *in, Frame *out); void init(Notifier notifier);
void react(Frame *out, detail::Error::Send err);
void enable(Frame *out); void handle(const Frame &in, Frame &out);
void disable(Frame *out); std::unique_ptr<Frame>
static bool pending(); react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
static void resolvepending(); void enable(Frame &out);
void emit(Frame *out, uint16_t peripheral); void disable(Frame &out);
void incrementTime(); void emit(Frame &out, uint32_t payload);
bool isPlaying() const; bool isPlaying() const;
#ifndef NDEBUG #ifndef NDEBUG
void media_action(MediaAction action); void media_action(MediaAction action);
@@ -76,23 +86,45 @@ 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 = 0xC0; uint8_t flags2 = 0x80;
}; };
} // namespace avclan } // namespace avclan
+29 -7
View File
@@ -5,9 +5,14 @@
#include <concepts> #include <concepts>
#include <cstdint> #include <cstdint>
#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"
namespace avclan { namespace avclan {
@@ -45,17 +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, detail::Error::Send err, requires(T dev, Notifier notifier, const Frame &in, Frame &out,
uint16_t peripheral) { 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(out, err); { dev.react(std::move(exp)) } -> std::same_as<std::unique_ptr<Frame>>;
{ dev.pending() } -> std::convertible_to<bool>; dev.emit(out, payload);
dev.resolvepending();
dev.emit(out, peripheral);
}; };
} // namespace avclan } // namespace avclan
+157 -29
View File
@@ -6,8 +6,53 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory>
#include <new>
#include <type_traits>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "frame.hpp" #include "frame.hpp"
#include "hal/stdio.h"
#include "stdshim.hpp"
#if defined(AVCLAN_FRAME_POOL_N)
#include <array>
#include <cstddef>
#include <limits>
namespace {
template <class T, std::uint8_t N>
requires(N >= 1 && N <= std::numeric_limits<uint8_t>::max())
class Pool {
public:
constexpr Pool() {
for (uint8_t i = 0; i < N; ++i)
ptrs_[i] = &storage_[i];
}
T *acquire() {
if (top_ == 0)
return nullptr;
return ptrs_[--top_];
}
void release(T *ptr) {
// Properly would need an origin check/confirmation if this was used more
// generally
ptrs_[top_++] = ptr;
}
private:
std::array<T, N> storage_;
std::array<T *, N> ptrs_;
uint8_t top_ = N;
};
Pool<avclan::Frame, AVCLAN_FRAME_POOL_N> pool;
} // namespace
#endif
namespace { namespace {
using Error = avclan::detail::Error; using Error = avclan::detail::Error;
@@ -15,52 +60,135 @@ using enum Error::Parse;
} // namespace } // namespace
namespace avclan { namespace avclan {
#if defined(AVCLAN_FRAME_POOL_N)
void *Frame::operator new(std::size_t /*count*/,
const std::nothrow_t & /*tag*/) noexcept {
taskENTER_CRITICAL();
Frame *frame = pool.acquire();
taskEXIT_CRITICAL();
return frame;
}
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
void Frame::operator delete(void *ptr) noexcept {
taskENTER_CRITICAL();
pool.release(static_cast<Frame *>(ptr));
taskEXIT_CRITICAL();
}
#endif
std::unique_ptr<Frame> Frame::acquire() {
std::unique_ptr<Frame> frame(new (std::nothrow) Frame);
for (uint8_t retries = 0; !frame && retries < 3; retries++) {
vTaskDelay(pdMS_TO_TICKS(2));
frame.reset(new (std::nothrow) Frame);
}
return frame;
}
namespace {
// Emit `value` as at least `width` (lowercase, as to_chars emits) hex digits,
// zero-padded. `width` must be <= 3 (see the padding below); every call site
// reserves its field's worst case in the destination buffer, so this can't
// overflow.
template <class T>
requires std::is_unsigned_v<T>
char *put_hex(char *dest, T value, uint8_t width) {
// to_chars emits the minimum number of digits, so work out up front how many
// that will be to know how much zero padding goes in front of them. At most
// two iterations for the field widths used here.
uint8_t ndigits = 1;
for (T rest = value; rest >= 16; rest /= 16)
ndigits++;
// At most two pad digits (width <= 3). Written out rather than as a counted
// loop because GCC turns that into a memset() call — call overhead an order
// of magnitude above the one or two stores it replaces.
if (ndigits < width) {
*dest++ = '0';
if (ndigits + 1 < width)
*dest++ = '0';
}
return to_chars(dest, dest + ndigits, value, 16).ptr;
}
// " 0xNNN" for each field, plus the leading unicast digit and trailing newline
constexpr uint8_t LINE_MAX = 1 + (4 * 6) + (Frame::MAXLENGTH * 5) + 1;
// The same buffer serves the binary branch, which is the shorter of the two
static_assert(LINE_MAX >= 8 + Frame::MAXLENGTH + 2);
} // namespace
void Frame::print(Frame::Print print) const { void Frame::print(Frame::Print print) const {
// The AVC-LAN read loop polls for start bits between calls, so emitting per
// character (~3 µs each through stdio) would blow past a start bit's ~169 µs
// and lose the next frame.
char buffer[LINE_MAX];
char *bptr = buffer;
if (print.binary) { if (print.binary) {
uint8_t buffer[8];
uint8_t *bptr = buffer;
*bptr++ = 0x10; // Data Link Escape, signaling binary data forthcoming *bptr++ = 0x10; // Data Link Escape, signaling binary data forthcoming
*bptr++ = static_cast<uint8_t>(is_unicast); *bptr++ = static_cast<char>(is_unicast);
// Send addresses in big-endian order // Send addresses in big-endian order
*bptr++ = static_cast<uint8_t>(controller_addr >> 8); *bptr++ = static_cast<char>(controller_addr >> 8);
*bptr++ = static_cast<uint8_t>(controller_addr); *bptr++ = static_cast<char>(controller_addr);
*bptr++ = static_cast<uint8_t>(peripheral_addr >> 8); *bptr++ = static_cast<char>(peripheral_addr >> 8);
*bptr++ = static_cast<uint8_t>(peripheral_addr); *bptr++ = static_cast<char>(peripheral_addr);
*bptr++ = control; *bptr++ = static_cast<char>(control);
*bptr++ = length; *bptr++ = static_cast<char>(length);
fwrite(buffer, 1, 8, stdout);
fwrite(data, 1, length, stdout); memcpy(bptr, data, length);
bptr += length;
bptr = buffer;
*bptr++ = 0x17; // End of transmission block *bptr++ = 0x17; // End of transmission block
*bptr++ = 0x0A; // \n *bptr++ = 0x0A; // \n
fwrite(buffer, 1, 2, stdout);
} else {
printf("%X", static_cast<unsigned>(is_unicast));
printf(" 0x%03X", static_cast<unsigned>(controller_addr & 0x0FFF));
printf(" 0x%03X", static_cast<unsigned>(peripheral_addr & 0x0FFF));
printf(" 0x%X", static_cast<unsigned>(control & 0x0F));
printf(" 0x%X", static_cast<unsigned>(length & 0x0F));
for (uint8_t i = 0; i < length; i++) { stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
printf(" 0x%02X", static_cast<unsigned>(data[i])); return;
}
putchar('\n');
} }
struct field_t {
uint16_t value;
uint8_t width; // minimum digits, matching the old %03x / %x formats
};
*bptr++ = is_unicast ? '1' : '0';
for (const field_t field :
{field_t{.value = static_cast<uint16_t>(controller_addr & 0x0FFF),
.width = 3},
field_t{.value = static_cast<uint16_t>(peripheral_addr & 0x0FFF),
.width = 3},
field_t{.value = static_cast<uint16_t>(control & 0x0F), .width = 1},
field_t{.value = static_cast<uint16_t>(length & 0x0F), .width = 1}}) {
*bptr++ = ' ';
*bptr++ = '0';
*bptr++ = 'x';
bptr = put_hex(bptr, field.value, field.width);
}
for (uint8_t i = 0; i < length; i++) {
*bptr++ = ' ';
*bptr++ = '0';
*bptr++ = 'x';
bptr = put_hex(bptr, data[i], 2);
}
*bptr++ = '\n';
stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
} }
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) { Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
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;
} }
@@ -73,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;
} }
@@ -81,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");
@@ -101,6 +229,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
} }
} }
return err.errno; return err.type;
} }
} // namespace avclan } // namespace avclan
+23 -3
View File
@@ -6,9 +6,15 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#if defined(AVCLAN_FRAME_POOL_N)
#include <cstddef>
#include <new>
#endif
namespace avclan { namespace avclan {
enum class Device : uint8_t; enum class Device : uint8_t;
@@ -25,13 +31,27 @@ 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;
uint8_t reaction; // Retries every 2ms up to 3 times (max 6ms wait); null if still exhausted
Device owning_device; static std::unique_ptr<Frame> acquire();
#if defined(AVCLAN_FRAME_POOL_N)
// O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported.
static void *operator new(std::size_t) = delete;
static void *operator new(std::size_t, const std::nothrow_t &) noexcept;
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
static void operator delete(void *) noexcept;
#endif
// reaction has a Device-defined interpretation, with the sole invariant that
// 0 == inactive/non-sendable frame
uint8_t reaction = 0;
Device owning_device = NoDevice;
bool is_unicast; bool is_unicast;
uint16_t controller_addr; // formerly "master" uint16_t controller_addr; // formerly "master"
uint16_t peripheral_addr; // formerly "slave" uint16_t peripheral_addr; // formerly "slave"
uint8_t control = 0xF; uint8_t control = 0xF;
uint8_t length; uint8_t length = 0;
uint8_t data[MAXLENGTH]; uint8_t data[MAXLENGTH];
}; };
} // namespace avclan } // namespace avclan
-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
+68 -39
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);
// Start-bit handling, factored out of read/sendframe so the framing layer holds /* Per-field frame I/O.
// no bus-timing or hardware-recovery logic. *
// - phy_read_startbit waits for and validates an incoming start bit, doing * All send functions may have asynchronous implementations (e.g. return before
// any target-specific bus recovery; see avclan::detail::Error::Read. * the send has completed on the bus). Success is indicated by a zero value
// - phy_send_startbit acquires the bus and emits a start bit; may return BUSY * `Read` or `Send` enum. Non-zero error codes indicate a synchronously
Read phy_read_startbit(void); * completed send failure. Otherwise, `phy_send_done` must be called to block
Send phy_send_startbit(void); * until all queued send's have completed, and may return the error code for a
* previous (queued) send failure; a success return value indicates that all
* 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.
*/
/* Returns 0 (`(Send)0`) if the peripheral sent an ACK bit, otherwise returns // Returns NO_FRAME when there is no frame to read.
NAK. An ACK bit is a cooperative bit, where the sender starts (drives the bus) Read phy_read_header(bool *is_unicast);
for the sync period, and allows the receiver to drive the bus (or not) to Read phy_read_controller_addr(uint16_t *addr);
finish a "1" bit. Read phy_read_peripheral_addr(uint16_t *addr);
*/ Read phy_read_control(uint8_t *control);
Send phy_read_ack(void); Read phy_read_length(uint8_t *length);
void phy_send_ack(void); // Read `length` data bytes. `data_index` is only written to on error, with the
// index of the failed byte.
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index);
// Per-symbol I/O. The send* helpers return the even parity of the bits sent; // Send start and broadcast bits. Returns success or one of these error values:
// the read* helpers return the even parity of the bits read. The _u8/_u16 // MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
// suffixes name the source-operand width. The function implementations need not // a broadcast).
// all be separate/independent (e.g. all send functions could be redirect to a Send phy_send_header(bool is_unicast);
// single phy_send_bits_u16, etc).
// N.B: `len` is the number of bits to send. The
// C++ send/readbits templates are the only consumers and use constraints to
// enforce valid len values, so runtime checks are unnecessary.
// Intended for sending parity bits // Send the controller address. Returns success or LOST_ARBITRATION (a device
void phy_send_bit(Bit bit); // with a lower device is sending a frame).
Send phy_send_controller_addr(uint16_t addr);
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack);
Send phy_send_control(uint8_t control, bool expect_ack);
Send phy_send_length(uint8_t length, bool expect_ack);
// Send `length` data bytes. `data_index` is only written to for NAK_DATA.
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index);
// Variants available to minimize unnecessary work for max runtime efficiency // Allows asynchronous ports to block until the phy has finished sending the
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len); // frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS,
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len); // etc) or CONTENDED_BUS; a port that queues the header and controller address
Bit phy_send_byte(const uint8_t *byte); // also reports their BUSY or LOST_ARBITRATION here. `data_index` is only
// written to for NAK_DATA. A fully synchronous port should always report
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len); // success.
Bit phy_read_bits_u16(uint16_t *bits, int8_t len); Send phy_send_done(uint8_t *data_index);
Bit phy_read_byte(uint8_t *byte);
#ifndef NDEBUG #ifndef NDEBUG
// Hold the bus at a level until the matching call.
void phy_set_dominant(void);
void phy_set_recessive(void);
#ifdef MEASURE_BUS
// Sample and dump bus bit timing over the serial link (REPL `M`). // Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void); void phy_measure(void);
#endif
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus
+22 -4
View File
@@ -3,18 +3,36 @@
#pragma once #pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
// 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* — no '\n' -> "\r\n" translation: bare LF and binary frame // - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
// payloads (fwrite) are emitted verbatim // change a bare LF or a binary frame payload. Writes through
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
// stream, in order.
void stdio_init(void); void stdio_init(void);
// Queue a buffer onto stdout--if it has space, or return false.
//
// If stdout cannot accept all `len` bytes now, the function queues
// a "\n!\n" indicator onto stdout, overwriting the last 3 chars if stdout is
// full. The caller must not assume that the buffer went out.
//
// A port MUST emit that indicator for every dropped buffer, and MUST be able to
// emit it even if stdout is full. A reader detects the loss as a line that
// holds only '!'. In the overwrite case the buffer in front of that line is
// truncated, so a reader MUST tolerate one malformed buffer there. The return
// value is useful only for diagnostics.
bool stdio_write_nonblock(const void *buf, uint8_t len);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+134 -54
View File
@@ -4,73 +4,125 @@
// 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 <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <tuple>
#include <type_traits>
#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"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
#include <cstdint>
#include <cstring>
#include <tuple>
namespace avclan { namespace avclan {
namespace detail {
enum class Party : uint8_t { Sender, Recipient };
}
template <DeviceInterface... Devs> class Peripheral { template <DeviceInterface... Devs> class Peripheral {
using Party = detail::Party;
using enum Party;
public: public:
using Error = detail::Error; using Error = detail::Error;
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} { // lack of static reflection until C++26 limits our methods of doing a
bus.init(); // compile-time collision check. Within Peripheral, we can at least assert no
(std::get<Devs>(devices_).init(), ...); // collision with Devices *used in a particular instantiation* but it is not a
// universal check against all Device enum values
static_assert(((Devs::id != NoDevice) && ...),
"a registered Device id collides with the NoDevice sentinel; "
"update the sentinel value in avclan.h");
static_assert(
sizeof...(Devs) <= 0x100,
"too many devices: Notifier packs the device index into 8 bits");
static constexpr UBaseType_t EMIT_QUEUE_LEN = 2 * sizeof...(Devs);
Peripheral(Bus &bus, uint16_t address)
: bus{bus}, address_{address},
emit_requests_{xQueueCreate(EMIT_QUEUE_LEN, sizeof(uint32_t))} {
configASSERT(emit_requests_);
bus.init(address);
(std::get<Devs>(devices_).init(Notifier{emit_requests_, index_of<Devs>()}),
...);
} }
Peripheral(const Peripheral &) = delete;
uint16_t controller() const { return controller_; }; 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
Error::Read read(Frame *in, Frame::Print print) { expected<std::unique_ptr<Frame>, Error::Read>
return bus.read(address_, in, print); read(Frame::Print print = Frame::Print{}) {
return bus.read(print);
}; };
Error::Send send(Frame *out, Frame::Print print) {
expected<std::unique_ptr<Frame>, detail::SendError>
send(std::unique_ptr<Frame> out, Frame::Print print = Frame::Print{}) {
// To "forge" a controller_addr, instantiate a new/different Peripheral // To "forge" a controller_addr, instantiate a new/different Peripheral
postmark(out); stamp<Sender>(*out);
return bus.send(out, print); out->control = 0xF;
auto err = bus.send(*out, print);
if (err != Error::Send{0})
return unexpected{detail::SendError{.owning_device = out->owning_device,
.reaction = out->reaction,
.err = err}};
return out;
}; };
#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) #define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c))
void route(const Frame *in, Frame *out) { // expected needed to distinguish don't vs can't respond
expected<std::unique_ptr<Frame>, Error::Read> route(const Frame &in) {
using enum Device; using enum Device;
using enum Action; using enum Action;
out->reaction = 0;
if (is_muted() || in->length < 3) if (is_muted() || in.length < 3)
return; return {};
std::unique_ptr<Frame> out = Frame::acquire();
if (!out) {
puts("!! failed Frame alloc in route !!");
return unexpected{Error::Read::POOL_EMPTY};
}
// 0xFF placeholders are variant bytes filled by writing directly to // 0xFF placeholders are variant bytes filled by writing directly to
// out->data[N] after memcpy. // out->data[N] after memcpy.
static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL), static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL),
to_underlying(LAN), 0xFF, 0xFF}; to_underlying(LAN), 0xFF, 0xFF};
out->peripheral_addr = controller_; stamp<Recipient>(*out);
const uint8_t *data = in->data; const uint8_t *data = in.data;
const uint8_t b0 = *data++; const uint8_t b0 = *data++;
const uint8_t b1 = *data++; const uint8_t b1 = *data++;
const uint8_t b2 = *data++; const uint8_t b2 = *data++;
uint8_t b3 = 0;
if (in->length > 3) // the shortest known/valid messages are 3 bytes long
b3 = *data++;
if (!in->is_unicast) { // the shortest known/valid messages are 3 bytes long
const uint8_t b3 = (in.length > 3) ? *data++ : 0;
if (!in.is_unicast) {
const auto from = b0; const auto from = b0;
const auto to = b1; const auto to = b1;
const auto action = b2; const auto action = b2;
@@ -103,15 +155,14 @@ public:
case PACK3(COMMUNICATION_V1, COMM_CTRL, case PACK3(COMMUNICATION_V1, COMM_CTRL,
to_underlying(Advertise_Function)): to_underlying(Advertise_Function)):
case PACK3(COMMUNICATION_V2, COMM_CTRL, case PACK3(COMMUNICATION_V2, COMM_CTRL,
to_underlying(Advertise_Function)): to_underlying(Advertise_Function)): {
auto enable_d = [](auto &d, auto &out) { d.enable(out); };
((Devs::id == static_cast<Device>(b3) ((Devs::id == static_cast<Device>(b3)
? [&] { ? originate(std::get<Devs>(devices_), *out, enable_d)
out->owning_device = Devs::id; : void()),
std::get<Devs>(devices_).enable(out);
}()
: void()),
...); ...);
break; break;
}
case PACK3(COMMUNICATION_V1, COMM_CTRL, to_underlying(Ping_Req)): case PACK3(COMMUNICATION_V1, COMM_CTRL, to_underlying(Ping_Req)):
case PACK3(COMMUNICATION_V2, COMM_CTRL, to_underlying(Ping_Req)): { case PACK3(COMMUNICATION_V2, COMM_CTRL, to_underlying(Ping_Req)): {
out->is_unicast = true; out->is_unicast = true;
@@ -127,8 +178,8 @@ public:
to_underlying(List_Functions_Req)): to_underlying(List_Functions_Req)):
case PACK3(COMMUNICATION_V2, COMM_CTRL, case PACK3(COMMUNICATION_V2, COMM_CTRL,
to_underlying(List_Functions_Req)): { to_underlying(List_Functions_Req)): {
controller_ = in->controller_addr; controller_ = in.controller_addr;
out->peripheral_addr = controller_; stamp<Recipient>(*out); // re-stamp now that controller_ is known
out->is_unicast = true; out->is_unicast = true;
const uint8_t list_functions_resp[] = { const uint8_t list_functions_resp[] = {
0x00, to_underlying(COMM_CTRL), from, 0x00, to_underlying(COMM_CTRL), from,
@@ -141,49 +192,78 @@ public:
// case Restart_Lan: not handled // case Restart_Lan: not handled
default: break; default: break;
} }
} else if (in->peripheral_addr == address_ && b0 == 0x00) { } else if (in.peripheral_addr == address_ && b0 == 0x00) {
auto handle_d = [&](auto &d, auto &out) { d.handle(in, out); };
((Devs::id == static_cast<Device>(b2) ((Devs::id == static_cast<Device>(b2)
? device_preroute(std::get<Devs>(devices_), in, out), ? originate(std::get<Devs>(devices_), *out, handle_d)
0 : 0), : void()),
...); ...);
} }
if (out->reaction > 0)
return out;
return {};
} }
#undef PACK3 #undef PACK3
void react(Frame *out, Error::Send err) { std::unique_ptr<Frame>
if (((Devs::id == out->owning_device) || ...)) react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
((Devs::id == out->owning_device const Device from =
? std::get<Devs>(devices_).react(out, err), exp ? exp.value()->owning_device : exp.error().owning_device;
0 : 0),
...); std::unique_ptr<Frame> next;
else ((Devs::id == from &&
out->reaction = 0; (next = std::get<Devs>(devices_).react(std::move(exp)))) ||
...);
return next;
} }
template <class F> void poll_devices(F &&fun) { // Blocks until a device requests an emit; requests are served in order.
(poller(std::get<Devs>(devices_), fun), ...); std::unique_ptr<Frame> poll() {
std::unique_ptr<Frame> out = Frame::acquire();
if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
uint32_t val;
xQueueReceive(emit_requests_, &val, portMAX_DELAY);
const uint8_t index = val & 0xFF;
auto emit_d = [val](auto &d, auto &out) { d.emit(out, val >> 8); };
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
((Is == index ? originate(std::get<Is>(devices_), *out, emit_d) : void()),
...);
}(std::index_sequence_for<Devs...>{});
return out;
} }
private: private:
void postmark(Frame *out) const { // Dev's position in Devs
out->controller_addr = address_; template <class Dev> static constexpr uint8_t index_of() {
out->control = 0xF; constexpr std::array is_dev{std::is_same_v<Dev, Devs>...};
return std::ranges::find(is_dev, true) - is_dev.begin();
} }
template <DeviceInterface Dev, class F> void poller(Dev &dev, F &&fun) { template <Party P> void stamp(Frame &out) const {
if (dev.pending() && fun(dev)) if constexpr (P == Sender)
dev.resolvepending(); out.controller_addr = address_;
else
out.peripheral_addr = controller_;
} }
template <DeviceInterface Dev>
void device_preroute(Dev &dev, const Frame *in, Frame *out) { void originate(DeviceInterface auto &dev, Frame &out, auto &&fill) {
out->owning_device = Dev::id; out.owning_device = std::remove_reference_t<decltype(dev)>::id;
dev.handle(in, out); stamp<Recipient>(out); // default set FIRST; fill() may override
fill(dev, out);
} }
Bus &bus; Bus &bus;
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
+72
View File
@@ -0,0 +1,72 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <version>
#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L
#include <tl/expected.hpp>
namespace avclan {
template <class T, class E> using expected = tl::expected<T, E>;
template <class E> using unexpected = tl::unexpected<E>;
using tl::unexpect;
using tl::unexpect_t;
} // namespace avclan
#else
#include <expected>
namespace avclan {
template <class T, class E> using expected = std::expected<T, E>;
template <class E> using unexpected = std::unexpected<E>;
using std::unexpect;
using std::unexpect_t;
} // namespace avclan
#endif
#if !defined(__cpp_lib_to_chars) || __cpp_lib_to_chars < 201611L
#include <cstdint>
#include <type_traits>
namespace avclan {
// No <charconv> (nor the <system_error> its result type needs) in the
// freestanding stdlib, so mirror the std API surface for the unsigned-integral
// overload — the only one used here. Values are those of std::errc, so call
// sites written against either spelling behave the same.
enum class errc : uint8_t { value_too_large = 75 };
struct to_chars_result {
char *ptr;
errc ec;
};
template <class T>
requires std::is_integral_v<T> && std::is_unsigned_v<T>
constexpr to_chars_result to_chars(char *first, char *last, T value,
int base = 10) {
char digits[sizeof(T) * 8]; // worst case: base 2
uint8_t ndigits = 0;
do {
const auto digit = static_cast<unsigned>(value % static_cast<T>(base));
digits[ndigits++] =
static_cast<char>(digit < 10 ? '0' + digit : 'a' + (digit - 10));
value = static_cast<T>(value / static_cast<T>(base));
} while (value != 0);
if ((last - first) < ndigits)
return {.ptr = last, .ec = errc::value_too_large};
while (ndigits > 0)
*first++ = digits[--ndigits];
return {.ptr = first, .ec = errc{}};
}
} // namespace avclan
#else
#include <charconv>
namespace avclan {
using errc = std::errc;
using std::to_chars;
using std::to_chars_result;
} // namespace avclan
#endif
@@ -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")
@@ -85,11 +90,14 @@ FetchContent_Declare(
avr_libstdcpp avr_libstdcpp
GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git
GIT_TAG 5354296040a2289c911062daa82336762231e897 GIT_TAG 5354296040a2289c911062daa82336762231e897
SYSTEM
) )
FetchContent_MakeAvailable(avr_libstdcpp) FetchContent_MakeAvailable(avr_libstdcpp)
add_library(libstdcpp INTERFACE) add_library(libstdcpp STATIC ${avr_libstdcpp_SOURCE_DIR}/src/new_handler.cc)
target_include_directories(libstdcpp SYSTEM target_include_directories(libstdcpp
INTERFACE ${avr_libstdcpp_SOURCE_DIR}/include) PUBLIC ${avr_libstdcpp_SOURCE_DIR}/include)
# Let --gc-sections drop the unused set/get_new_handler functions.
target_compile_options(libstdcpp PRIVATE -ffunction-sections -fdata-sections)
target_link_libraries(avclan PUBLIC libstdcpp) target_link_libraries(avclan PUBLIC libstdcpp)
if(NOT LIBC_VERSION_TEST) if(NOT LIBC_VERSION_TEST)
@@ -131,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)
+266 -40
View File
@@ -9,13 +9,17 @@
#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)
// F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by // F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by
// avr-libc. // avr-libc.
#include "timing_avr.h" #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.
@@ -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; } 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) {
@@ -112,11 +120,9 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) {
AVCLAN_setBusDriven(); AVCLAN_setBusDriven();
} }
while (TCB1.CNT <= period) {}; while (TCB1.CNT <= period) {};
return;
} }
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:
@@ -137,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
@@ -151,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
@@ -166,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;
@@ -191,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;
@@ -225,19 +233,22 @@ ISR(TCB0_INT_vect) {
// last_tcb1 = cur_tcb1; // last_tcb1 = cur_tcb1;
// #endif // #endif
READING_BYTE <<= 1;
// If the logical `0` pulse was less than the sync + data period threshold, // If the logical `0` pulse was less than the sync + data period threshold,
// bit was a 1 // bit was a 1
pulsewidth = TCB0.CCMP; uint16_t tmp = pulsewidth = TCB0.CCMP; // non-volatile tmp saves re-read in
if (pulsewidth < (uint16_t)AVCLAN_READBIT_THRESHOLD) { // read-bit-threshold comparison
READING_BYTE++; if (READING_NBITS) {
READING_PARITY++; READING_BYTE <<= 1;
if (tmp < (uint16_t)AVCLAN_READBIT_THRESHOLD) {
READING_BYTE++;
READING_PARITY++;
}
READING_NBITS--;
} }
READING_NBITS--;
} }
// 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;
@@ -264,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;
@@ -277,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;
@@ -303,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
@@ -338,15 +358,36 @@ void phy_init() {
phy_mute(false); // unmute AVCLAN bus TX phy_mute(false); // unmute AVCLAN bus TX
} }
// Wait for and validate an incoming start bit. On an over-long "driven" bus static Read phy_read_startbit() {
// (AC2 latched high because the bus is actually floating) this kicks PA7 hard // Following HAL header docs, this function is only called after a driven bus
// high to unlatch the comparator. The framing layer maps the result to its own // was ~recently detected.
// error reporting; no printing happens here. // Two main (designed) entry flows, depending on timing of pulse end w.r.t.
Read phy_read_startbit() { // beginning this function:
uint16_t startbitlen = TCB1.CNT = 0; // - Bus is still driven or ISR is pending
// - Wait for pulse to end if needed
// - On an over-long "driven" bus (AC2 latched high when bus is floating)
// kick PA7 hard high to try unlatching the comparator
// - Grab updated `pulsewidth`
// - Bus is idle
// - `pulsewidth` holds duration of most recent pulse.
TCB1.CNT = 0;
// "stale"ness indicates that the current `pulsewidth` value is out of date
bool stale = true;
uint16_t startbitlen;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
// pulsewidth won't/can't be updated during atomic block, but pulse may end
// and store an IRQ. If bus is idle and no pending ISR, then pulsewidth
// currrently holds the duration of the most recent pulse (stale = false).
stale = (((!BUS_IS_IDLE) | (TCB0.INTFLAGS & TCB_CAPT_bm)) != 0);
// Read ~atomically, to prevent torn reads
startbitlen = pulsewidth;
}
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
startbitlen = TCB1.CNT; if (TCB1.CNT > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
Read result = STARTBIT_TOO_LONG; Read result = STARTBIT_TOO_LONG;
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
// If bus is "driven" too long, assume the AC2 is latched (e.g. // If bus is "driven" too long, assume the AC2 is latched (e.g.
@@ -367,24 +408,48 @@ Read phy_read_startbit() {
return result; return result;
} }
} }
TCB1.CNT = 0;
while (stale) { // bus was driven, or a capture was still unconsumed, at entry
uint16_t old_pulsewidth = startbitlen;
// `pulsewidth` updates once the TCB0_INT_vect ISR runs for this pulse.
// Read ~atomically, to prevent torn reads
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { startbitlen = pulsewidth; }
if (startbitlen != old_pulsewidth)
break;
// Exit for potential false-negative: ISR writes for identical mid-frame
// bits (e.g. double 0 or 1 bits) can't be detected, but the ISR should
// definitely have run after ~6μs (i.e. startbitlen is actually current).
// The delay is ultimately free since mid-frame entry is directed to the
// "wait-for-message-completion" stall loop anyways
if (TCB1.CNT > (uint16_t)AVCLAN_BIT0_LOGIC_1)
break;
}
if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) { if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) {
// We missed the beginning of this message; wait for it to finish (bus // Not a start bit; wait for the message to finish (bus continuously idle
// continuously idle for >1 bit length) before returning, so we don't have // for >1 bit length) before returning, so we only report one error (instead
// multiple false-starts while the in-progress message keeps sending more // of e.g. repeated "bad (short) start bit" errors)
// bits.
TCB1.CNT = 0; TCB1.CNT = 0;
while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) { while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) {
if (!BUS_IS_IDLE) if (!BUS_IS_IDLE)
TCB1.CNT = 0; TCB1.CNT = 0; // (Re)start count from when bus was last driven
} }
return STARTBIT_TOO_SHORT; // A pulse no wider than a normal bit means we merely tuned in mid-frame and
// this was a data bit; a wider-but-still-sub-start pulse means some other
// device emitted a wonky pulse.
return (startbitlen < (uint16_t)AVCLAN_BIT_LENGTH_MAX) ? STARTBIT_MISSED
: STARTBIT_MALFORMED;
} }
if (startbitlen > (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 1.2))
return STARTBIT_TOO_LONG;
return (Read)0; // that was a start bit return (Read)0; // that was a start bit
} }
// 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) {
@@ -413,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. */
@@ -433,9 +636,30 @@ void phy_guard_leave() {
} }
#ifndef NDEBUG #ifndef NDEBUG
// Only used immediately below
#define XSTR(x) #x // Hold the bus at a given level.
#define STR(x) XSTR(x) // Overrides and restores mute state upon release (i.e. set recessive).
static void phy_set_state(bool dominant) {
static bool saved_mute = false;
if (dominant) {
saved_mute = phy_is_muted();
AVCLAN_setBusDriven();
phy_mute(false);
} else {
AVCLAN_setBusIdle();
phy_mute(saved_mute);
}
}
void phy_set_dominant(void) { phy_set_state(true); }
void phy_set_recessive(void) { phy_set_state(false); }
#ifdef MEASURE_BUS
#include <stdio.h> // phy_measure() reporting (debug builds only)
// Only used immediately below
#define XSTR(x) #x
#define STR(x) XSTR(x)
static uint16_t pulses[100]; static uint16_t pulses[100];
static uint16_t periods[100]; static uint16_t periods[100];
@@ -468,4 +692,6 @@ void phy_measure() {
phy_guard_leave(); phy_guard_leave();
} }
#endif
#endif #endif
+82 -5
View File
@@ -4,11 +4,27 @@
#include <avr/io.h> #include <avr/io.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <util/atomic.h>
#include "hal/stdio.h" #include "hal/stdio.h"
#include "timing_avr.h" // IWYU pragma: export #include "timing_avr.h" // IWYU pragma: export
#include "usart.h" // jnk0le AVR-UART-lib #include "usart.h" // jnk0le AVR-UART-lib
// TX ring owned by the jnk0le lib. `tx0_Head` is the index of the last byte
// written, so the next write goes to head+1. The DRE ISR reads tail+1 and then
// advances `tx0_Tail`. The ring is empty when the two indexes are equal. The
// ring holds at most TX0_BUFFER_SIZE-1 bytes.
extern char tx0_buffer[TX0_BUFFER_SIZE];
// stdio_write_nonblock() is all-or-nothing. If the ring cannot hold the largest
// buffer that the app writes, the function drops every buffer and queues none.
// The largest buffer is a text frame log line (Frame::print). That line is
// ~186 bytes at MAXLENGTH=32. The ring therefore needs headroom above this
// size.
static_assert(TX0_BUFFER_SIZE - 1 >= 192,
"TX ring too small to hold a whole frame log line");
// Raw stdout backend: emit the byte verbatim, no '\n' -> "\r\n" translation // Raw stdout backend: emit the byte verbatim, no '\n' -> "\r\n" translation
// (unlike the lib's uart_putchar). Keeps binary frame payloads intact. // (unlike the lib's uart_putchar). Keeps binary frame payloads intact.
static int stdio_putchar(char data, FILE *stream) { static int stdio_putchar(char data, FILE *stream) {
@@ -17,6 +33,67 @@ static int stdio_putchar(char data, FILE *stream) {
return 0; return 0;
} }
bool stdio_write_nonblock(const void *buf, uint8_t len) {
if (len == 0)
return true;
// A single unguarded read of each index is enough:
// - Only main writes tx0_Head.
// - Only the DRE ISR writes tx0_Tail.
// - 8-bit accesses are atomic on AVR.
// If the ISR advances the tail during this function, the ring has more free
// space than this function measured. That direction is safe.
const uint8_t head = tx0_Head;
const uint8_t space = (uint8_t)((tx0_Tail - head - 1) & TX0_BUFFER_MASK);
if (len > space) {
// The ring cannot take the whole buffer, so drop it and queue the indicator
// instead. A buffer may be refused because it is larger than
// available space or because the ring is full. For the former case, the
// full indicator can be appended without truncating previously queued
// bytes.
// Goal is for the bang-indicator to be on its own line
uint8_t tmp = head;
if (space >= 3) {
// Room for 3 normally implies the current head ends with a newline
// (avoids an unneeded double newline in most cases)
tmp = (tmp + 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '!';
tmp = (tmp + 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '\n';
tx0_Head = tmp;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { USART0.CTRLA |= USART_DREIE_bm; }
} else {
// No room for even the indicator, so it goes over the last three queued
// bytes. The ISR already drains the ring, so DREIE needs no change.
// Write in reverse order to more efficiently decrement
tx0_buffer[tmp] = '\n';
tmp = (tmp - 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '!';
tmp = (tmp - 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '\n';
}
return false;
}
const uint8_t start = (uint8_t)((head + 1) & TX0_BUFFER_MASK);
const uint8_t contiguous = TX0_BUFFER_SIZE - start;
if (len <= contiguous) {
memcpy(&tx0_buffer[start], buf, len);
} else { // wraps the end of the ring
memcpy(&tx0_buffer[start], buf, contiguous);
memcpy(&tx0_buffer[0], (const char *)buf + contiguous, len - contiguous);
}
// Publish the bytes only after all of them are in place. This is a single
// store, so the ISR never sees a partly filled buffer.
tx0_Head = (uint8_t)((head + len) & TX0_BUFFER_MASK);
// The ISR also writes CTRLA (to clear DREIE when it drains the last byte).
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { USART0.CTRLA |= USART_DREIE_bm; }
return true;
}
// Non-blocking stdin backend: next received byte, or _FDEV_EOF when the RX ring // Non-blocking stdin backend: next received byte, or _FDEV_EOF when the RX ring
// is empty (uart0_getData() returns a negative value when there is no data). // is empty (uart0_getData() returns a negative value when there is no data).
static int stdio_getchar(FILE *stream) { static int stdio_getchar(FILE *stream) {
@@ -34,14 +111,14 @@ void stdio_init(void) {
// The lib's uart0_init configures the USART registers/baud but not the pins; // The lib's uart0_init configures the USART registers/baud but not the pins;
// keep the ATtiny3216 pin-mux the old driver did. // keep the ATtiny3216 pin-mux the old driver did.
PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2 PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2
PORTA.DIRSET = PIN1_bm; // TxD output PORTA.DIRSET = PIN1_bm; // TxD output
PORTA.DIRCLR = PIN2_bm; // RxD input PORTA.DIRCLR = PIN2_bm; // RxD input
#ifdef USE_DOUBLE_SPEED #ifdef USE_DOUBLE_SPEED
uart0_init(DOUBLE_BAUD_CALC(1200000)); uart0_init(DOUBLE_BAUD_CALC(1200000));
#else #else
uart0_init(BAUD_CALC(1200000)); uart0_init(BAUD_CALC(1200000));
#endif #endif
stdout = stdin = &stdio_stream; // printf/fputs/fwrite + non-blocking getchar stdout = stdin = &stdio_stream; // printf/fputs/fwrite + non-blocking getchar
} }
@@ -63,4 +63,4 @@
// TCB_TICK is nanoseconds/tick; the generic timing.h wants microseconds/tick. // TCB_TICK is nanoseconds/tick; the generic timing.h wants microseconds/tick.
#define TICK_US (TCB_TICK / 1000.0) #define TICK_US (TCB_TICK / 1000.0)
#include "timing.h" #include "timing.h" // IWYU pragma: export
@@ -108,11 +108,11 @@
// Size of the ring buffers, must be power of 2 // Size of the ring buffers, must be power of 2
// default 32 // default 32
#define RX_BUFFER_SIZE 64 #define RX_BUFFER_SIZE 128
// Size of the ring buffers, must be power of 2 // Size of the ring buffers, must be power of 2
// default 32 // default 32
#define TX_BUFFER_SIZE 128 #define TX_BUFFER_SIZE 256
/*******************config for multiple USART * mcu's*************************/ /*******************config for multiple USART * mcu's*************************/
+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 -84
View File
@@ -3,101 +3,39 @@
#pragma once #pragma once
#include <array>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory> #include <memory>
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>
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max())
class Queue {
using Deleter = detail::template Deleter<T, N>;
friend Deleter;
// Owning FIFO of `T`s, safe to share between tasks
template <class T> class Queue {
public: public:
// Only full and copy-convert-from-full construction is allowed explicit Queue(UBaseType_t length)
Queue() = delete; : handle{xQueueCreate(length, sizeof(T *))} {
configASSERT(handle);
}
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);
constexpr Queue(T (&items)[N])
requires(Owning)
: owner{this}, write{N} {
for (uint8_t i = 0; i < N; ++i)
buf[i] = &items[i];
}
constexpr Queue(Queue<T, N, true> &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)
{
// structurally unnecessary; empty construction from same size parent
// guarantees
// !isFull assert(!isFull());
if (!x || !x.get_deleter().is_owned_by(owner))
return 1;
reclaim(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 (Owning) return ptr;
return {buf[mask(read++)], {this}};
else
return {buf[mask(read++)], {owner}};
} }
private: private:
uint8_t mask(uint8_t pos) const { return pos & (N - 1); } QueueHandle_t handle;
void reclaim(T *x) { buf[mask(write++)] = x; }
std::array<T *, N> buf = {};
Queue<T, N, true> *const owner;
uint8_t read = 0;
uint8_t write = 0;
}; };
template <class T, auto N> Queue(Queue<T, N, true> &) -> Queue<T, N, false>;
template <class T, auto N> Queue(T (&items)[N]) -> Queue<T, N, true>;
namespace detail {
template <class T, auto N> struct Deleter {
Deleter() = default;
constexpr Deleter(Queue<T, N, true> *owner) : owner{owner} {}
void operator()(T *x) const { owner->reclaim(x); }
constexpr bool is_owned_by(const Queue<T, N, true> *parent) const {
return owner == parent;
}
private:
Queue<T, N, true> *const owner = nullptr;
};
} // namespace detail
+175 -78
View File
@@ -7,6 +7,10 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "frame.hpp" #include "frame.hpp"
@@ -14,19 +18,32 @@
#include "hal/stdio.h" #include "hal/stdio.h"
#include "peripheral.hpp" #include "peripheral.hpp"
#include "queue.hpp" #include "queue.hpp"
#include "stdshim.hpp"
const char *const offon[] = {"OFF", "ON"};
constexpr uint8_t CACHE_SIZE = 32;
using namespace avclan; using namespace avclan;
namespace { namespace {
Frame frames[CACHE_SIZE]; using Periph = Peripheral<CDChanger>;
constinit Queue cache(frames); const char *const offon[] = {"OFF", "ON"};
constinit Queue incoming = cache;
constinit Queue outgoing = cache; 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
@@ -59,63 +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 Print = Frame::Print;
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (auto msg = peripheral.read(Print{.print = printAllFrames,
.binary = printBinary,
.verbose = verbose}))
incoming.push(std::move(*msg));
}
}
[[noreturn]] void vRoutingTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
const Frame *in = incoming.peek();
if (auto resp = peripheral.route(*in)) {
incoming.pop();
if (*resp)
outgoing.push(std::move(*resp));
}
}
}
[[noreturn]] void vPollTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (auto msg = peripheral.poll())
outgoing.push(std::move(msg));
}
}
[[noreturn]] void vSenderTask(void *pvParameters) {
using Print = Frame::Print;
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
auto result = peripheral.send(
outgoing.pop(), Print{.print = printAllFrames, .binary = printBinary});
if (auto next = peripheral.react(std::move(result)))
outgoing.push(std::move(next));
}
}
[[noreturn]] void vREPLTask(void *pvParameters) {
using enum Action; using enum Action;
using enum Device; using enum Device;
Peripheral<CDChanger> peripheral(phy, 0x360); auto &[peripheral, incoming, outgoing] =
using Error = decltype(peripheral)::Error; *static_cast<Context *>(pvParameters);
using Print = Frame::Print;
Setup();
print_help();
while (true) { while (true) {
if (peripheral.bus_is_active()) { if (int readkey = fgetc(stdin); readkey != EOF) {
if (auto msg = cache.pop()) {
auto err = peripheral.read(msg.get(), Print{.print = printAllFrames,
.binary = printBinary,
.verbose = verbose});
if (err == Error::Read{0x00})
incoming.push(std::move(msg));
} else {
puts("!! Dropping an incoming message; cache is empty !!");
}
}
if (const auto *in = incoming.peek()) {
if (auto out = cache.pop()) {
peripheral.route(in, out.get());
incoming.pop();
if (out->reaction > 0)
outgoing.push(std::move(out));
} else {
puts("!! Unable to respond; cache is empty !!");
}
}
peripheral.poll_devices([&](auto &dev) {
if (auto status = cache.pop()) {
dev.emit(status.get(), peripheral.controller());
outgoing.push(std::move(status));
return true;
}
return false;
});
if (auto out = outgoing.pop()) {
auto err = peripheral.send(
out.get(), Print{.print = printAllFrames, .binary = printBinary});
peripheral.react(out.get(), err);
if (out->reaction > 0)
outgoing.push(std::move(out));
}
// stdin must be non-blocking: yielding EOF when idle/empty
if (int readkey = getchar(); 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;
@@ -133,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 = cache.pop()) { if (auto out = Frame::acquire()) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -145,10 +159,10 @@ int main() {
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} else } else
puts("!! Cache empty; unable to queue beep request"); puts("!! failed Frame alloc for Beep !! ");
break; break;
case 'P': case 'P':
if (auto out = cache.pop()) { if (auto out = Frame::acquire()) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -163,7 +177,8 @@ int main() {
} }
out->reaction = CDChanger::reaction_t::r_Ejection; out->reaction = CDChanger::reaction_t::r_Ejection;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} } else
puts("!! failed Frame alloc for Play !! ");
break; break;
#ifndef NDEBUG #ifndef NDEBUG
@@ -189,7 +204,19 @@ 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
case 'M': peripheral.get_bus().measure(); break; case 'M': peripheral.get_bus().measure(); break;
#endif
#endif #endif
case 0x10: // Signals binary sequence incoming case 0x10: // Signals binary sequence incoming
@@ -218,41 +245,80 @@ 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 = cache.pop()) { if (auto out = Frame::acquire()) {
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;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
readSeq = readBinary = false;
} }
} } else
readSeq = readBinary = false; puts("!! failed Frame alloc for input message !!");
} else } else
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 if (seqIdx <= Frame::MAXLENGTH) { } else { // ASCII message
if (auto out = cache.pop()) { if (auto out = Frame::acquire()) {
const uint8_t sendLen =
seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH;
out->is_unicast = seqIsUnicast; out->is_unicast = seqIsUnicast;
out->peripheral_addr = out->peripheral_addr =
seqIsUnicast ? peripheral.controller() : 0x1FF; seqIsUnicast ? peripheral.controller() : 0x1FF;
out->length = seqIdx; out->length = sendLen;
memcpy(out->data, data_tmp, seqIdx); memcpy(out->data, data_tmp, sendLen);
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
}
printAllFrames = lastPrintAllFrames; if (seqIdx > Frame::MAXLENGTH)
printf("!! sequence too long (%u > %u), truncated !!\n",
static_cast<unsigned>(seqIdx),
static_cast<unsigned>(Frame::MAXLENGTH));
// Only leave hex-entry mode and restore logging once the
// message actually sent, so a failed alloc can be retried
// with '\n' instead of silently dropping the entry.
readSeq = false;
seqIdx = hexDigit = 0;
printAllFrames = lastPrintAllFrames;
} else
puts("!! failed Frame alloc for input message !!");
} }
break; break;
} }
DEFAULT: DEFAULT:
default: default:
if (readSeq && seqIdx < (Frame::MAXLENGTH + sizeof(Frame))) { if (readSeq) {
// Binary mode carries the full wire preamble; hex mode is payload
// only, so it stops one past MAXLENGTH to let '\n' report overflow.
if (seqIdx >= (readBinary ? sizeof(data_tmp)
: uint8_t{Frame::MAXLENGTH + 1})) {
puts("!! sequence buffer full, ignoring further input !!");
break;
}
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) {
@@ -277,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 {
@@ -291,8 +386,8 @@ void Setup() {
void print_help() { void print_help() {
puts("AVCLAN Mockingboard v1"); puts("AVCLAN Mockingboard v1");
puts("U - begin reading for unicast message\n" puts("U - begin reading for unicast message (send with Enter key)\n"
"B - begin reading for broadcast message\n" "B - begin reading for broadcast message (send with Enter key)\n"
"m - Toggle mute for mockingboard bus activity\n" "m - Toggle mute for mockingboard bus activity\n"
"v - Toggle verbose error logging\n" "v - Toggle verbose error logging\n"
"l - Toggle message logging\n" "l - Toggle message logging\n"
@@ -307,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");
} }