mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-11-01 23:07:41 +00:00
Compare commits
39 Commits
681450f4ad
..
pico2
| Author | SHA1 | Date | |
|---|---|---|---|
| e64fe519d6 | |||
| 598f6ede0e | |||
| c24bab7a80 | |||
| 5b9cb1da8b | |||
| fbf5811496 | |||
| 7252e6bfdd | |||
| 6b22f14e6d | |||
| 2c85305c0d | |||
| 8a47b5b776 | |||
| 9efbb01ccf | |||
| 53c5abb3e9 | |||
| ac47cebe86 | |||
| e30dbc501d | |||
| 6c460aa3a0 | |||
| 6ce4c2cc21 | |||
| b9f7978d3e | |||
| cd68ee7267 | |||
| ee5b4a955c | |||
| 5d6b5f4ce0 | |||
| 0bef565564 | |||
| 66dd441610 | |||
| 3ea7c2d44a | |||
| 95dae572a0 | |||
| f45a0f5e92 | |||
| 994759dd39 | |||
| eab535db98 | |||
| f3d0d99ce4 | |||
| 76f5da0125 | |||
| 7871e92dbe | |||
| fb17b7e75b | |||
| ae539e27f6 | |||
| a99fe56aae | |||
| 909915730b | |||
| 5bbbb6c011 | |||
| 8950bd6917 | |||
| 56936aed10 | |||
| 5208e083f3 | |||
| 200a225add | |||
| 568c2af9e6 |
+6
-2
@@ -16,13 +16,17 @@ Checks: |
|
||||
-modernize-avoid-c-arrays,
|
||||
-modernize-use-std-print,
|
||||
-readability-magic-numbers,
|
||||
-readability-function-cognitive-complexity,
|
||||
-misc-non-private-member-variables-in-classes
|
||||
WarningsAsErrors: ""
|
||||
ExcludeHeaderFilterRegex: 'out/build'
|
||||
HeaderFilterRegex: '^src/.*'
|
||||
ExcludeHeaderFilterRegex: "out/build"
|
||||
HeaderFilterRegex: "^src/.*"
|
||||
SystemHeaders: false
|
||||
# AnalyzeTemporaryDtors: false
|
||||
FormatStyle: none
|
||||
CheckOptions:
|
||||
- key: modernize-deprecated-headers.CheckHeaderFile
|
||||
value: "false"
|
||||
- key: readability-magic-numbers.IgnorePowersOf2IntegerValues
|
||||
value: "true"
|
||||
- key: readability-magic-numbers.IgnoredIntegerValues
|
||||
|
||||
@@ -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
|
||||
@@ -5,7 +5,17 @@
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/base:alpine-3.22",
|
||||
// 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.
|
||||
// "forwardPorts": [],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
@@ -17,28 +27,25 @@
|
||||
"extensions": [
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"rockcat.avr-support",
|
||||
"harikrishnan94.cxx-compiler-explorer"
|
||||
"harikrishnan94.cxx-compiler-explorer",
|
||||
"llvm-vs-code-extensions.vscode-clangd",
|
||||
"Anthropic.claude-code"
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": true,
|
||||
"C_Cpp.codeAnalysis.clangTidy.checks.disabled": [
|
||||
"clang-analyzer-core.NullDereference"
|
||||
],
|
||||
"C_Cpp.files.exclude": {
|
||||
"build/**": true
|
||||
},
|
||||
"files.associations": {
|
||||
"*.S": "avr"
|
||||
},
|
||||
"cmake.configureOnOpen": true,
|
||||
"compilerexplorer.compilationDirectory": "${workspaceFolder}/build"
|
||||
"clangd.path": "/usr/bin/clangd",
|
||||
"clangd.arguments": [
|
||||
"--compile-commands-dir=${workspaceFolder}/out/build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
// "runArgs": [
|
||||
// "--device=/dev/ttyUSB0",
|
||||
// "--device=/dev/ttyUSB1"
|
||||
// ]
|
||||
},
|
||||
"runArgs": [
|
||||
"--device=/dev/ttyUSB0",
|
||||
"--network=host"
|
||||
]
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ __pycache__/
|
||||
~*
|
||||
\#*
|
||||
/CMakeUserPresets.json
|
||||
.cache/
|
||||
|
||||
+127
-24
@@ -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_CXX_STANDARD 23)
|
||||
set(CMAKE_C_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_VERSION VERSION_LESS "13")
|
||||
@@ -47,15 +61,6 @@ if(INSTALL_GIT_HOOKS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Hardware target (port) selection. The chosen target/<name>/ directory supplies
|
||||
# the port implementation sources, the per-target include path, and all
|
||||
# hardware-specific build configuration (compile defs/options, device-pack
|
||||
# handling, flashing) via its own CMakeLists, pulled in with add_subdirectory
|
||||
# below. The matching cross-compiler is selected separately by a toolchain file
|
||||
# (e.g. cmake/avr-gcc-toolchain.cmake) named in a CMake preset. Adding a new
|
||||
# board = a sibling target/ directory + a toolchain file + a preset wiring them.
|
||||
set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build")
|
||||
|
||||
# The AVC-LAN stack as a static library: the target-agnostic generic core (no
|
||||
# <avr/...>, no register access). The selected target's port sources and flags
|
||||
# are contributed by its subdirectory.
|
||||
@@ -65,6 +70,21 @@ add_library(avclan STATIC
|
||||
src/avclan/bus.cc
|
||||
)
|
||||
|
||||
target_compile_definitions(avclan PUBLIC
|
||||
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
|
||||
AVCLAN_MSG_QUEUE_SIZE=${AVCLAN_MSG_QUEUE_SIZE}
|
||||
)
|
||||
|
||||
target_compile_options(avclan PUBLIC
|
||||
-Wall -Wswitch-enum -Werror
|
||||
|
||||
# Debug must not be -O0: both the vendored usart.h and avr-libc's
|
||||
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
|
||||
# that fatal.
|
||||
$<$<CONFIG:Debug>:-Og>
|
||||
$<$<CONFIG:Debug>:-fanalyzer>
|
||||
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
|
||||
|
||||
# avclan exports its public generic headers (src/avclan) to consumers and
|
||||
# reaches into src/ for sibling headers (stdio.h, board.h) during its own
|
||||
# build. The selected target adds its own per-target include path.
|
||||
@@ -76,8 +96,91 @@ target_include_directories(avclan PUBLIC
|
||||
# flashing target. Added after the targets above so it can extend them.
|
||||
add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
|
||||
|
||||
add_executable(mockingboard
|
||||
src/sniffer.cc
|
||||
)
|
||||
if(NOT TARGET mockingboard)
|
||||
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
@@ -1,6 +1,24 @@
|
||||
{
|
||||
"version": 3,
|
||||
"version": 9,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "debug-base",
|
||||
"hidden": true,
|
||||
"description": "Debug build settings",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "relwithdebinfo-base",
|
||||
"hidden": true,
|
||||
"description": "RelWithDebInfo build settings",
|
||||
"inherits": "debug-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "usb0",
|
||||
"hidden": true,
|
||||
@@ -22,7 +40,7 @@
|
||||
"hidden": true,
|
||||
"description": "AVR ATtiny3216 cross-compile toolchain + matching port",
|
||||
"toolchainFile": "${sourceDir}/cmake/avr-gcc-toolchain.cmake",
|
||||
"binaryDir": "${sourceDir}/out/build/attiny3216",
|
||||
"binaryDir": "${sourceDir}/out/build/avr-attiny3216",
|
||||
"cacheVariables": {
|
||||
"AVCLAN_TARGET": "avr-attiny3216",
|
||||
"FREQSEL": "20MHz",
|
||||
@@ -31,31 +49,64 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "debug-base",
|
||||
"name": "pico2",
|
||||
"hidden": true,
|
||||
"description": "Debug build settings",
|
||||
"generator": "Unix Makefiles",
|
||||
"binaryDir": "${sourceDir}/out/build",
|
||||
"binaryDir": "${sourceDir}/out/build/pico2",
|
||||
"inherits": "debug-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
"AVCLAN_TARGET": "pico2",
|
||||
"AVCLAN_FRAME_POOL_N": "14",
|
||||
"AVCLAN_TARGET_SDK_FILE": "${sourceDir}/cmake/pico_sdk_import.cmake",
|
||||
"PICO_SDK_FETCH_FROM_GIT": true
|
||||
},
|
||||
"environment": {
|
||||
"PICO_PLATFORM": "rp2350-arm-s",
|
||||
"PICO_BOARD": "pimoroni_pico_plus2_w_rp2350"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "relwithdebinfo-base",
|
||||
"hidden": true,
|
||||
"description": "RelWithDebInfo build settings",
|
||||
"inherits": "debug-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
|
||||
"name": "pico2-gcc",
|
||||
"displayName": "Pico 2W (GCC)",
|
||||
"inherits": "pico2",
|
||||
"description": "",
|
||||
"generator": "Ninja",
|
||||
"environment": {
|
||||
"PICO_COMPILER": "pico_arm_gcc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pico2-clang",
|
||||
"displayName": "Pico 2W (Clang)",
|
||||
"inherits": "pico2",
|
||||
"description": "",
|
||||
"environment": {
|
||||
"PICO_COMPILER": "pico_arm_clang"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"displayName": "ATtiny3216 RelWithDebInfo (ttyUSB0)",
|
||||
"description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB0",
|
||||
"inherits": [
|
||||
"avr-attiny3216",
|
||||
"relwithdebinfo-base",
|
||||
"attiny3216-relwithdebinfo",
|
||||
"usb0"
|
||||
]
|
||||
},
|
||||
@@ -64,8 +115,25 @@
|
||||
"displayName": "ATtiny3216 RelWithDebInfo (ttyUSB1)",
|
||||
"description": "ATtiny3216 RelWithDebInfo build, program over /dev/ttyUSB1",
|
||||
"inherits": [
|
||||
"avr-attiny3216",
|
||||
"relwithdebinfo-base",
|
||||
"attiny3216-relwithdebinfo",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -13,6 +13,9 @@
|
||||
##########################################################################
|
||||
find_program(AVR_CC avr-gcc 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_PROCESSOR avr)
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -203,14 +203,16 @@ local function bcd2dec(b)
|
||||
end
|
||||
|
||||
-- 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").
|
||||
local function bcd_time(min_b, sec_b)
|
||||
-- valid BCD (e.g. a 0xff sentinel for "time unknown"). `negative` (the CD
|
||||
-- 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 s = bcd2dec(sec_b)
|
||||
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
|
||||
return string.format("%02d:%02d", m, s)
|
||||
return string.format("%s%02d:%02d", sign, m, s)
|
||||
end
|
||||
|
||||
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_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_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)
|
||||
@@ -553,6 +564,9 @@ avclanproto.fields = {
|
||||
f_cd_flag_repeat,
|
||||
f_cd_flag_disk_scan,
|
||||
f_cd_flag_scan,
|
||||
f_cd_flags2,
|
||||
f_cd_flag_negative,
|
||||
f_cd_flag2_unknown7,
|
||||
f_tape_present,
|
||||
f_tape_state,
|
||||
f_tape_seeking_rev,
|
||||
@@ -809,7 +823,7 @@ end
|
||||
local function decode_cd(subtree, buffer, offset, action)
|
||||
if action == known_actions_names["PLAYBACK_STATUS"] or
|
||||
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))
|
||||
cd_slots:add(f_cd_slot1, 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_seeking_track, 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), "")
|
||||
cd_status:add(f_cd_disc, buffer(offset+5,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:append_text("Disc " .. field_cd_disc().value .. ", ")
|
||||
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))
|
||||
cd_flags:add(f_cd_flag_disk_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_disk_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
|
||||
action == known_actions_names["LOADING_RESP"] then
|
||||
local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player")
|
||||
|
||||
@@ -49,6 +49,7 @@ still bypasses it.
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ---- locations -------------------------------------------------------------
|
||||
@@ -227,10 +228,16 @@ def find_address_spec(root):
|
||||
|
||||
|
||||
def iter_src_files(root):
|
||||
for dirpath, _dirs, files in os.walk(os.path.join(root, SRC_REL)):
|
||||
for fn in sorted(files):
|
||||
if fn.endswith(SRC_EXTS):
|
||||
yield os.path.join(dirpath, fn)
|
||||
"""List source files under src/, via `git ls-files` so paths excluded by
|
||||
.gitignore (e.g. the ESP32 port's CMake build directory, which vendors
|
||||
FetchContent'd third-party code) are never walked into."""
|
||||
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 ------------------------------------------------------------------
|
||||
|
||||
@@ -471,8 +478,6 @@ def main():
|
||||
lua_path = os.path.join(root, LUA_REL)
|
||||
lua_lines = read_lines(lua_path)
|
||||
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_failed = False
|
||||
@@ -487,6 +492,8 @@ def main():
|
||||
print_lint(spec, LUA_REL, problems)
|
||||
|
||||
if args.lint:
|
||||
if not have_address:
|
||||
print("note: no Address enum found in src/ -- addresses not synced")
|
||||
if lint_failed:
|
||||
print("lint: FAILED")
|
||||
return 1
|
||||
@@ -503,6 +510,8 @@ def main():
|
||||
has_drift = any(not d.clean() for _s, _e, d in drifts)
|
||||
|
||||
if args.fix:
|
||||
if not have_address:
|
||||
print("note: no Address enum found in src/ -- addresses not synced")
|
||||
if lint_failed:
|
||||
print("fix aborted: resolve the dissector name problems above first")
|
||||
return 1
|
||||
@@ -525,16 +534,17 @@ def main():
|
||||
print("fix: applied" if has_drift else "fix: already in sync")
|
||||
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:
|
||||
if not drift.clean():
|
||||
print("%s drift:" % spec["name"])
|
||||
print_drift(drift)
|
||||
if lint_failed or has_drift:
|
||||
print("out of sync -- run: scripts/sync_avclan_enums.py --fix")
|
||||
return 1
|
||||
print("enums in sync with the dissector")
|
||||
return 0
|
||||
print("out of sync -- run: scripts/sync_avclan_enums.py --fix")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+18
-6
@@ -121,6 +121,12 @@ enum AVCLAN_ENUM_CLASS MediaAction : uint8_t {
|
||||
};
|
||||
|
||||
#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 {
|
||||
struct Error {
|
||||
#endif
|
||||
@@ -137,9 +143,12 @@ struct Error {
|
||||
BAD_CONTROLLER_PARITY,
|
||||
BAD_CONTROL_PARITY,
|
||||
BAD_PARITY, // generic bad parity has max severity
|
||||
STARTBIT_TOO_SHORT,
|
||||
STARTBIT_MISSED,
|
||||
STARTBIT_MALFORMED,
|
||||
STARTBIT_TOO_LONG,
|
||||
BAD_STARTBIT,
|
||||
NO_FRAME,
|
||||
POOL_EMPTY, // non-bus error
|
||||
};
|
||||
|
||||
enum AVCLAN_ENUM_CLASS Send : uint8_t {
|
||||
@@ -147,7 +156,10 @@ struct Error {
|
||||
NAK_MESSAGE_LENGTH,
|
||||
NAK_CONTROL,
|
||||
NAK_ADDRESS,
|
||||
NAK_TOO_LONG,
|
||||
NAK, // generic NAK has max severity
|
||||
CONTENDED_BUS,
|
||||
LOST_ARBITRATION,
|
||||
BUSY,
|
||||
MUTED,
|
||||
};
|
||||
@@ -160,13 +172,13 @@ struct Error {
|
||||
|
||||
#ifdef __cplusplus
|
||||
};
|
||||
#endif
|
||||
|
||||
enum AVCLAN_ENUM_CLASS Bit : uint8_t {
|
||||
bit_zero = 0x00,
|
||||
bit_one = 0x01,
|
||||
bit_start = 0x10
|
||||
struct SendError {
|
||||
Device owning_device;
|
||||
uint8_t reaction;
|
||||
Error::Send err;
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // namespace detail
|
||||
|
||||
+161
-209
@@ -26,30 +26,31 @@
|
||||
| 1 │ *Acknowledge*
|
||||
*repeat `n` times*
|
||||
|
||||
No acknowledge bits are sent for broadcast frames.
|
||||
For broadcast frames the acknowledge (one) bit is sent, but an ACK response
|
||||
(zero) is not expected.
|
||||
*/
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
|
||||
#include "avclan.h"
|
||||
#include "bus.hpp"
|
||||
#include "frame.hpp"
|
||||
#include "hal/phy.h"
|
||||
#include "stdshim.hpp"
|
||||
|
||||
namespace {
|
||||
using Read = avclan::detail::Error::Read;
|
||||
using Send = avclan::detail::Error::Send;
|
||||
using Bit = avclan::detail::Bit;
|
||||
|
||||
struct trailer_bits_t {};
|
||||
struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit)
|
||||
struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address)
|
||||
struct with_ack_t : trailer_bits_t {
|
||||
}; // bits + parity + ACK slot (all other fields)
|
||||
inline constexpr no_parity_t no_parity{};
|
||||
inline constexpr with_parity_t with_parity{};
|
||||
inline constexpr with_ack_t with_ack{};
|
||||
// The bus spec has a unit that loses arbitration retry rather than fail: "if
|
||||
// the unit loses in arbitration, the frame is automatically reset up twice
|
||||
// (three times in total)". Only an attempt that is outbid every time is an
|
||||
// error worth reporting.
|
||||
constexpr uint8_t SEND_ATTEMPTS = 3;
|
||||
} // namespace
|
||||
|
||||
namespace avclan {
|
||||
@@ -68,217 +69,142 @@ public:
|
||||
Handle(const Handle &) = delete;
|
||||
Handle(Handle &&) = delete;
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
Send sendstartbit() { return phy_send_startbit(); };
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
Read readstartbit() { return phy_read_startbit(); };
|
||||
|
||||
template <auto N, std::unsigned_integral T,
|
||||
std::derived_from<trailer_bits_t> Trailer>
|
||||
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
|
||||
Send send(T bits, Trailer /*tag*/) {
|
||||
const Bit parity = sendbits<N>(bits);
|
||||
|
||||
if constexpr (std::is_same_v<Trailer, with_parity_t>)
|
||||
sendbits<1>(to_underlying(parity));
|
||||
|
||||
return Send{0};
|
||||
// Forward to phy API (organized so that hal/phy.h isn't public/visible at the
|
||||
// C++/library level)
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
Read read_header(bool *is_unicast) { return phy_read_header(is_unicast); };
|
||||
Read read_controller_addr(uint16_t *addr) {
|
||||
return phy_read_controller_addr(addr);
|
||||
};
|
||||
Read read_peripheral_addr(uint16_t *addr) {
|
||||
return phy_read_peripheral_addr(addr);
|
||||
};
|
||||
Read read_control(uint8_t *control) { return phy_read_control(control); };
|
||||
Read read_length(uint8_t *length) { return phy_read_length(length); };
|
||||
Read read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
|
||||
return phy_read_data(data, length, data_index);
|
||||
};
|
||||
|
||||
template <auto N, std::unsigned_integral T>
|
||||
requires(sizeof(T) < 3 && N < 16)
|
||||
Send send(T bits, with_ack_t /*tag*/, bool expect_ack) {
|
||||
send<N>(bits, with_parity);
|
||||
|
||||
if (expect_ack)
|
||||
return read_ACK();
|
||||
|
||||
sendbits<1>(1U); // still need to fill the ack bit slot
|
||||
return Send{0};
|
||||
Send send_header(bool is_unicast) { return phy_send_header(is_unicast); };
|
||||
Send send_controller_addr(uint16_t addr) {
|
||||
return phy_send_controller_addr(addr);
|
||||
};
|
||||
|
||||
template <auto N, std::unsigned_integral T,
|
||||
std::derived_from<trailer_bits_t> Trailer>
|
||||
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
|
||||
Read read(T *bits, Trailer /*tag*/) {
|
||||
const Bit calc_parity = readbits<N>(bits);
|
||||
if constexpr (std::is_same_v<Trailer, with_parity_t>) {
|
||||
uint8_t read_parity;
|
||||
readbits<1>(&read_parity);
|
||||
if (to_underlying(calc_parity) != read_parity)
|
||||
return Read::BAD_PARITY;
|
||||
}
|
||||
return Read{0};
|
||||
Send send_peripheral_addr(uint16_t addr, bool expect_ack) {
|
||||
return phy_send_peripheral_addr(addr, expect_ack);
|
||||
};
|
||||
|
||||
template <auto N, std::unsigned_integral T, class F>
|
||||
requires(sizeof(T) < 3 && N < 16)
|
||||
Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
|
||||
if (read<N>(bits, with_parity) == Read::BAD_PARITY)
|
||||
return Read::BAD_PARITY;
|
||||
|
||||
if (ack()) {
|
||||
send_ACK();
|
||||
} else {
|
||||
uint8_t slot;
|
||||
readbits<1>(&slot);
|
||||
}
|
||||
|
||||
return Read{0};
|
||||
Send send_control(uint8_t control, bool expect_ack) {
|
||||
return phy_send_control(control, expect_ack);
|
||||
};
|
||||
template <auto N, std::unsigned_integral T>
|
||||
requires(sizeof(T) < 3 && N < 16)
|
||||
Read read(T *bits, with_ack_t /*tag*/, bool ack) {
|
||||
return read<N>(bits, with_ack, [=]() { return ack; });
|
||||
}
|
||||
|
||||
private:
|
||||
static void send_ACK() { phy_send_ack(); };
|
||||
static Send read_ACK() { return phy_read_ack(); };
|
||||
|
||||
template <auto N, class T> Bit sendbits(T bits);
|
||||
template <auto N, class T> Bit readbits(T *bits);
|
||||
|
||||
template <auto N>
|
||||
requires(N > 1 && N < 8)
|
||||
Bit sendbits(uint8_t bits) {
|
||||
return phy_send_bits_u8(&bits, N);
|
||||
Send send_length(uint8_t length, bool expect_ack) {
|
||||
return phy_send_length(length, expect_ack);
|
||||
};
|
||||
template <auto N>
|
||||
requires(N <= 16)
|
||||
Bit sendbits(uint16_t bits) {
|
||||
return phy_send_bits_u16(&bits, N);
|
||||
};
|
||||
template <auto N>
|
||||
requires(N < 8)
|
||||
Bit readbits(uint8_t *bits) {
|
||||
return static_cast<Bit>(phy_read_bits_u8(bits, N));
|
||||
};
|
||||
template <auto N>
|
||||
requires(N <= 16)
|
||||
Bit readbits(uint16_t *bits) {
|
||||
return static_cast<Bit>(phy_read_bits_u16(bits, N));
|
||||
Send send_data(const uint8_t *data, uint8_t length, bool expect_ack,
|
||||
uint8_t *data_index) {
|
||||
return phy_send_data(data, length, expect_ack, data_index);
|
||||
};
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
};
|
||||
|
||||
template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) {
|
||||
return phy_send_byte(&bits);
|
||||
};
|
||||
template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) {
|
||||
const Bit bit{static_cast<Bit>(bits & 1U)};
|
||||
phy_send_bit(bit);
|
||||
return bit;
|
||||
};
|
||||
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
|
||||
return phy_read_byte(bits);
|
||||
};
|
||||
|
||||
void Bus::init() {
|
||||
void Bus::init(uint16_t address) {
|
||||
// Idempotent: the single Bus is shared by reference, so every Peripheral's
|
||||
// ctor calls init() on it — but the hardware must be brought up exactly once
|
||||
// (phy_init is not assumed re-entrant/idempotent).
|
||||
if (inited_)
|
||||
return;
|
||||
phy_init();
|
||||
phy_init(address);
|
||||
muted_ = false; // phy_init leaves the bus TX unmuted
|
||||
inited_ = true;
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
bool Bus::is_active() const { return phy_active(); };
|
||||
void Bus::mute(bool mute) {
|
||||
phy_mute(mute);
|
||||
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
|
||||
};
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
void Bus::deafen(bool deaf) { phy_deafen(deaf); }
|
||||
|
||||
auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
|
||||
auto Bus::read(Frame::Print print)
|
||||
-> expected<std::unique_ptr<Frame>, Error::Read> {
|
||||
struct errtype {
|
||||
Read errno;
|
||||
Read type;
|
||||
uint16_t val;
|
||||
} err = {};
|
||||
|
||||
using enum Read;
|
||||
|
||||
std::unique_ptr<Frame> in = Frame::acquire();
|
||||
if (!in) {
|
||||
err.type = POOL_EMPTY;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
phy_wait_frame(portMAX_DELAY);
|
||||
|
||||
{ // bound handle lifetime
|
||||
auto handle = get();
|
||||
|
||||
bool shouldACK = false;
|
||||
uint8_t tmp = 0;
|
||||
|
||||
err.errno = handle.readstartbit();
|
||||
if (err.errno != Read{0})
|
||||
err.type = handle.read_header(&in->is_unicast);
|
||||
if (err.type != Read{0})
|
||||
goto handle_err;
|
||||
|
||||
handle.read<1>(&tmp, no_parity);
|
||||
in->is_unicast = (tmp != 0U);
|
||||
|
||||
if (auto rerr = handle.read<12>(&in->controller_addr, with_parity);
|
||||
rerr == BAD_PARITY) {
|
||||
err.errno = BAD_CONTROLLER_PARITY;
|
||||
err.type = handle.read_controller_addr(&in->controller_addr);
|
||||
if (err.type != Read{0}) {
|
||||
if (print.verbose)
|
||||
err.val = in->controller_addr;
|
||||
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
// Using lambda for delayed evaluation of peripheral_addr field
|
||||
// deref, which will be written by the time the lambda is evaluated
|
||||
auto should_ack_lambda = [&]() {
|
||||
shouldACK = !is_muted() && (in->peripheral_addr == address);
|
||||
return shouldACK;
|
||||
};
|
||||
if (auto rerr =
|
||||
handle.read<12>(&in->peripheral_addr, with_ack, should_ack_lambda);
|
||||
rerr == BAD_PARITY) {
|
||||
err.errno = BAD_PERIPHERAL_PARITY;
|
||||
err.type = handle.read_peripheral_addr(&in->peripheral_addr);
|
||||
if (err.type != Read{0}) {
|
||||
if (print.verbose)
|
||||
err.val = in->peripheral_addr;
|
||||
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
if (auto rerr = handle.read<4>(&in->control, with_ack, shouldACK);
|
||||
rerr == BAD_PARITY) {
|
||||
err.errno = BAD_CONTROL_PARITY;
|
||||
err.type = handle.read_control(&in->control);
|
||||
if (err.type != Read{0}) {
|
||||
if (print.verbose)
|
||||
err.val = in->control;
|
||||
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
if (auto rerr = handle.read<8>(&in->length, with_ack, shouldACK);
|
||||
rerr == BAD_PARITY) {
|
||||
err.errno = BAD_LENGTH_PARITY;
|
||||
err.type = handle.read_length(&in->length);
|
||||
if (err.type != Read{0}) {
|
||||
if (print.verbose)
|
||||
err.val = in->length;
|
||||
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
if (in->length == 0 || in->length > Frame::MAXLENGTH) {
|
||||
err.errno = BAD_LENGTH_RANGE;
|
||||
} else if (in->length == 0 || in->length > Frame::MAXLENGTH) {
|
||||
err.type = BAD_LENGTH_RANGE;
|
||||
err.val = in->length;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < in->length; i++) {
|
||||
if (auto rerr = handle.read<8>(&in->data[i], with_ack, shouldACK);
|
||||
rerr == BAD_PARITY) {
|
||||
err.errno = BAD_DATA_PARITY;
|
||||
if (print.verbose)
|
||||
err.val = in->data[i];
|
||||
uint8_t data_i = 0;
|
||||
err.type = handle.read_data(in->data, in->length, &data_i);
|
||||
if (err.type != Read{0}) {
|
||||
if (print.verbose)
|
||||
err.val = in->data[data_i];
|
||||
|
||||
goto handle_err;
|
||||
}
|
||||
goto handle_err;
|
||||
}
|
||||
} // destroy handle
|
||||
|
||||
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
||||
handle_err:;
|
||||
if (err.type == NO_FRAME)
|
||||
return unexpected{NO_FRAME};
|
||||
fputs("ERR(read): ", stdout);
|
||||
switch (err.errno) {
|
||||
switch (err.type) {
|
||||
case POOL_EMPTY: puts("failed Frame alloc"); break;
|
||||
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
|
||||
case STARTBIT_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 BAD_CONTROLLER_PARITY:
|
||||
fputs("reading controller addr.", stdout);
|
||||
@@ -288,109 +214,120 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
|
||||
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_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 NO_FRAME:
|
||||
case BAD_PARITY:
|
||||
__builtin_unreachable();
|
||||
VERBOSE:
|
||||
if (print.verbose) {
|
||||
printf("; read 0x%02X", err.val);
|
||||
printf("; read 0x%02X:", err.val);
|
||||
}
|
||||
}
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
// Only print if some data has been correctly received
|
||||
if (print.print && (err.errno < STARTBIT_TOO_SHORT)) {
|
||||
if (err.errno > BAD_DATA_PARITY)
|
||||
if (print.print && (err.type < STARTBIT_MISSED)) {
|
||||
if (err.type > BAD_DATA_PARITY)
|
||||
in->length = 0;
|
||||
in->print(print);
|
||||
}
|
||||
|
||||
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 {
|
||||
// Error enum is ordered such that a lower numeric value corresponds to
|
||||
// more success
|
||||
Send errno;
|
||||
Send type;
|
||||
uint8_t val;
|
||||
} err = {};
|
||||
|
||||
using enum Send;
|
||||
|
||||
if (is_muted()) {
|
||||
err.errno = MUTED;
|
||||
err.type = MUTED;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
{ // bound handle lifetime
|
||||
auto handle = get();
|
||||
for (uint8_t attempt = 0; attempt < SEND_ATTEMPTS; attempt++) {
|
||||
auto handle = get(); // bound handle lifetime
|
||||
|
||||
if (handle.sendstartbit() == BUSY) {
|
||||
// Some other device is already driving the bus
|
||||
err.errno = BUSY;
|
||||
err.type = Send{0};
|
||||
|
||||
err.type = handle.send_header(out.is_unicast);
|
||||
if (err.type != Send{0}) // BUSY or LOST_ARBITRATION (broadcast)
|
||||
continue;
|
||||
|
||||
err.type = handle.send_controller_addr(out.controller_addr);
|
||||
if (err.type != Send{0}) // LOST_ARBITRATION: other device has lower address
|
||||
continue;
|
||||
|
||||
err.type = handle.send_peripheral_addr(out.peripheral_addr, out.is_unicast);
|
||||
if (err.type != Send{0})
|
||||
goto handle_err;
|
||||
|
||||
err.type = handle.send_control(out.control, out.is_unicast);
|
||||
if (err.type != Send{0})
|
||||
goto handle_err;
|
||||
|
||||
err.type = handle.send_length(out.length, out.is_unicast);
|
||||
if (err.type != Send{0})
|
||||
goto handle_err;
|
||||
|
||||
err.type = handle.send_data(out.data, out.length, out.is_unicast, &err.val);
|
||||
if (err.type != Send{0})
|
||||
goto handle_err;
|
||||
|
||||
// A phy that only queued the fields above settles them here
|
||||
uint8_t data_i = 0;
|
||||
err.type = phy_send_done(&data_i);
|
||||
if (err.type == BUSY || err.type == LOST_ARBITRATION) // Queued header
|
||||
continue;
|
||||
if (err.type != Send{0}) {
|
||||
err.val = data_i;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
handle.send<1>(static_cast<uint8_t>(out->is_unicast), no_parity);
|
||||
break; // Sent
|
||||
}
|
||||
|
||||
handle.send<12>(out->controller_addr, with_parity);
|
||||
|
||||
if (auto serr =
|
||||
handle.send<12>(out->peripheral_addr, with_ack, out->is_unicast);
|
||||
serr == NAK) {
|
||||
err.errno = NAK_ADDRESS;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
if (auto serr = handle.send<4>(out->control, with_ack, out->is_unicast);
|
||||
serr == NAK) {
|
||||
err.errno = NAK_CONTROL;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
if (auto serr = handle.send<8>(out->length, with_ack, out->is_unicast);
|
||||
serr == NAK) {
|
||||
err.errno = NAK_MESSAGE_LENGTH;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < out->length; i++) {
|
||||
// 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
|
||||
if (err.type != Send{0})
|
||||
goto handle_err; // Outbid (or busy) on every attempt
|
||||
|
||||
// back to read mode
|
||||
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
||||
handle_err:;
|
||||
fputs("Error", stdout);
|
||||
switch (err.errno) {
|
||||
switch (err.type) {
|
||||
case MUTED: fputs(": Device muted", stdout); break;
|
||||
case BUSY: fputs(": Busy bus", stdout); break;
|
||||
case CONTENDED_BUS:
|
||||
fputs(": bus contended after arbitration", stdout);
|
||||
break;
|
||||
case LOST_ARBITRATION: fputs(": lost arbitration", stdout); break;
|
||||
case NAK_ADDRESS:
|
||||
case NAK_CONTROL:
|
||||
case NAK_MESSAGE_LENGTH:
|
||||
case NAK_DATA:
|
||||
case NAK_TOO_LONG:
|
||||
case NAK:
|
||||
fputs(" NAK: ", stdout);
|
||||
switch (err.errno) {
|
||||
switch (err.type) {
|
||||
case NAK_ADDRESS: fputs("address", stdout); break;
|
||||
case NAK_CONTROL: fputs("Control", stdout); break;
|
||||
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
|
||||
case NAK_DATA: printf(" data[%u]", err.val); break;
|
||||
case NAK_TOO_LONG: fputs("too long", stdout); break;
|
||||
case NAK:
|
||||
case MUTED:
|
||||
case CONTENDED_BUS:
|
||||
case LOST_ARBITRATION:
|
||||
case BUSY: __builtin_unreachable();
|
||||
}
|
||||
break;
|
||||
@@ -399,18 +336,33 @@ auto Bus::send(const Frame *out, Frame::Print print) -> Send {
|
||||
}
|
||||
|
||||
if (print.print)
|
||||
out->print(print);
|
||||
out.print(print);
|
||||
|
||||
return err.errno;
|
||||
return err.type;
|
||||
}
|
||||
|
||||
Bus::Handle Bus::get() { return Handle{*this}; };
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Debug bit-timing measurement on the one physical bus; instance-scoped for the
|
||||
// same reason as is_active().
|
||||
#if !defined(NDEBUG)
|
||||
|
||||
void Bus::set_dominant() { phy_set_dominant(); }
|
||||
void Bus::set_recessive() { phy_set_recessive(); }
|
||||
|
||||
Send Bus::sendbyte(uint8_t byte, bool ack) {
|
||||
auto handle = get();
|
||||
uint8_t data_i = 0;
|
||||
if (const Send serr = handle.send_data(&byte, 1, ack, &data_i);
|
||||
serr != Send{0})
|
||||
return serr;
|
||||
return phy_send_done(&data_i);
|
||||
}
|
||||
|
||||
#ifdef MEASURE_BUS
|
||||
// Debug bit-timing measurement on the one physical bus; instance-scoped like
|
||||
// deafen().
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
void Bus::measure() { phy_measure(); }
|
||||
#endif
|
||||
#endif
|
||||
|
||||
} // namespace avclan
|
||||
|
||||
+15
-4
@@ -48,9 +48,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "avclan.h"
|
||||
#include "frame.hpp"
|
||||
#include "stdshim.hpp"
|
||||
|
||||
namespace avclan {
|
||||
|
||||
@@ -67,18 +69,27 @@ public:
|
||||
Bus(const Bus &) = delete;
|
||||
Bus &operator=(const Bus &) = delete;
|
||||
|
||||
void init();
|
||||
// `address` is our own peripheral address; the phy keeps it and acknowledges
|
||||
// frames addressed to it without further instruction. One address per bus.
|
||||
void init(uint16_t address);
|
||||
|
||||
bool is_active() const;
|
||||
// Prevent the device from being active on the bus
|
||||
void mute(bool mute);
|
||||
bool is_muted() const { return muted_; };
|
||||
|
||||
// Set the device to be deaf to (ie don't react to) bus activity. It is
|
||||
// orthogonal to mute — a deaf device can still send frames.
|
||||
void deafen(bool deaf);
|
||||
|
||||
#ifndef NDEBUG
|
||||
void measure();
|
||||
detail::Error::Send sendbyte(uint8_t byte, bool ack = false);
|
||||
void set_dominant();
|
||||
void set_recessive();
|
||||
#endif
|
||||
|
||||
Error::Read read(uint16_t address, Frame *in, Frame::Print print);
|
||||
Error::Send send(const Frame *out, Frame::Print print);
|
||||
expected<std::unique_ptr<Frame>, Error::Read> read(Frame::Print print);
|
||||
Error::Send send(const Frame &out, Frame::Print print);
|
||||
|
||||
private:
|
||||
class Handle;
|
||||
|
||||
+256
-214
@@ -3,18 +3,22 @@
|
||||
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "avclan.h"
|
||||
#include "cdchanger.hpp"
|
||||
#include "device.hpp"
|
||||
#include "frame.hpp"
|
||||
#include "hal/cd_timer.h"
|
||||
#include "hal/media.h"
|
||||
|
||||
namespace {
|
||||
using namespace avclan;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
|
||||
to_underlying(Device::STATUS),
|
||||
@@ -27,73 +31,83 @@ constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
|
||||
0x01,
|
||||
0x02};
|
||||
|
||||
constexpr int WIRE_SIZE = 8; // cd state report size in bytes
|
||||
constexpr int TIME_SKIP = 15; // seconds
|
||||
constexpr int WIRE_SIZE = 8; // cd state report size in bytes
|
||||
constexpr auto TIME_SKIP = 15s;
|
||||
constexpr int TWODIGIT_MAX = 99;
|
||||
constexpr auto MAX_TIME = std::chrono::minutes{TWODIGIT_MAX} + 59s;
|
||||
|
||||
/* Pack a 0–TWODIGIT_MAX count into 2-digit BCD. Values >TWODIGIT_MAX (sentinels
|
||||
such as 0xFF / 0x7F meaning "no time") pass through unchanged so they survive
|
||||
the wire round-trip. */
|
||||
// Pack a 0–TWODIGIT_MAX count into 2-digit BCD
|
||||
constexpr uint8_t toBCD(uint8_t val) {
|
||||
if (val > TWODIGIT_MAX)
|
||||
return val;
|
||||
return (uint8_t)(((val / 10) << 4) | (val % 10));
|
||||
}
|
||||
|
||||
extern "C" void incrementTime_callback(void *self) {
|
||||
static_cast<avclan::CDChanger *>(self)->incrementTime();
|
||||
}
|
||||
|
||||
extern "C" bool isPlaying_callback(void *self) {
|
||||
return static_cast<avclan::CDChanger *>(self)->isPlaying();
|
||||
// Whole seconds, saturated to the displayable range
|
||||
constexpr std::chrono::seconds displaySeconds(std::chrono::milliseconds t) {
|
||||
return std::clamp(std::chrono::floor<std::chrono::seconds>(t), -MAX_TIME,
|
||||
MAX_TIME);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace avclan {
|
||||
|
||||
void CDChanger::init() {
|
||||
void CDChanger::init(Notifier notif) {
|
||||
media_init();
|
||||
cdtimer_init(this, &incrementTime_callback, &isPlaying_callback);
|
||||
notifier = notif;
|
||||
statusTimer = xTimerCreate(
|
||||
"cd status", pdMS_TO_TICKS(1000), true, this, [](TimerHandle_t timer) {
|
||||
auto &self = *static_cast<CDChanger *>(pvTimerGetTimerID(timer));
|
||||
// Timer callbacks must not block; a refused request is re-made next
|
||||
// tick
|
||||
if (!self.statusQueued.exchange(true) && !self.notifier.notify(0, 0))
|
||||
self.statusQueued = false;
|
||||
});
|
||||
configASSERT(statusTimer);
|
||||
}
|
||||
|
||||
void CDChanger::handle(const Frame *in, Frame *out) {
|
||||
if (in->length < 4)
|
||||
void CDChanger::handle(const Frame &in, Frame &out) {
|
||||
if (in.length < 4)
|
||||
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 to = */ data++;
|
||||
const auto action = static_cast<Action>(*data++);
|
||||
|
||||
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;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wswitch-enum"
|
||||
// Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]).
|
||||
switch (action) {
|
||||
switch (const std::lock_guard lock(mutex); action) {
|
||||
case Enable_Function_Req:
|
||||
out->is_unicast = true;
|
||||
out->length = sizeof(function_change_resp);
|
||||
memcpy(out->data, function_change_resp, sizeof(function_change_resp));
|
||||
out->data[3] = to_underlying(Enable_Function_Resp);
|
||||
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(Enable_Function_Resp);
|
||||
state = 0;
|
||||
flags2 = 0x80;
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
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()) {
|
||||
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);
|
||||
media = stopPlaying();
|
||||
state = 0;
|
||||
flags2 = 0x80;
|
||||
out->is_unicast = true;
|
||||
out->reaction = r_StatusReport;
|
||||
}
|
||||
out.reaction = r_StatusReport;
|
||||
} else
|
||||
out.reaction = r_SendOnly;
|
||||
break;
|
||||
case Eject: {
|
||||
// "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
|
||||
state &= ~SEEKING;
|
||||
} else {
|
||||
out->is_unicast = true;
|
||||
out.is_unicast = true;
|
||||
{
|
||||
const uint8_t msg[] = {0x00, to_underlying(Device::CD_CHANGER),
|
||||
to_underlying(Device::CMD_SW),
|
||||
to_underlying(Insertion), 0x01};
|
||||
out->length = sizeof(msg);
|
||||
memcpy(out->data, msg, sizeof(msg));
|
||||
out.length = sizeof(msg);
|
||||
memcpy(out.data, msg, sizeof(msg));
|
||||
}
|
||||
out->reaction = r_SendOnly;
|
||||
out.reaction = r_SendOnly;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Initial_Report_Req: {
|
||||
out->is_unicast = true;
|
||||
out.is_unicast = true;
|
||||
// No knowledge/understanding of field meaning/interpretation
|
||||
const uint8_t cdinitreport_resp[] = {0x00,
|
||||
to_underlying(Device::CD_CHANGER),
|
||||
@@ -130,29 +144,29 @@ void CDChanger::handle(const Frame *in, Frame *out) {
|
||||
0x10,
|
||||
0x01,
|
||||
0x01};
|
||||
out->length = sizeof(cdinitreport_resp);
|
||||
memcpy(out->data, cdinitreport_resp, sizeof(cdinitreport_resp));
|
||||
out->reaction = r_SendOnly;
|
||||
out.length = sizeof(cdinitreport_resp);
|
||||
memcpy(out.data, cdinitreport_resp, sizeof(cdinitreport_resp));
|
||||
out.reaction = r_SendOnly;
|
||||
break;
|
||||
}
|
||||
case Playback_Req:
|
||||
out->data[0] = 0x00;
|
||||
out->data[1] = to_underlying(Device::CD_CHANGER);
|
||||
out->data[2] = to_underlying(from);
|
||||
out->data[3] = to_underlying(Playback_Resp);
|
||||
out->length = WIRE_SIZE + 4;
|
||||
serialize(&out->data[4]);
|
||||
out->is_unicast = true;
|
||||
out->reaction = r_SendOnly;
|
||||
out.data[0] = 0x00;
|
||||
out.data[1] = to_underlying(Device::CD_CHANGER);
|
||||
out.data[2] = to_underlying(from);
|
||||
out.data[3] = to_underlying(Playback_Resp);
|
||||
out.length = WIRE_SIZE + 4;
|
||||
serialize(&out.data[4]);
|
||||
out.is_unicast = true;
|
||||
out.reaction = r_SendOnly;
|
||||
break;
|
||||
case Loading_Req:
|
||||
out->data[0] = 0x00;
|
||||
out->length = sizeof(cdloading_resp) + 1;
|
||||
memcpy(&out->data[1], cdloading_resp, sizeof(cdloading_resp));
|
||||
out->data[2] = to_underlying(from);
|
||||
out->data[3] = to_underlying(Loading_Resp);
|
||||
out->is_unicast = true;
|
||||
out->reaction = r_SendOnly;
|
||||
out.data[0] = 0x00;
|
||||
out.length = sizeof(cdloading_resp) + 1;
|
||||
memcpy(&out.data[1], cdloading_resp, sizeof(cdloading_resp));
|
||||
out.data[2] = to_underlying(from);
|
||||
out.data[3] = to_underlying(Loading_Resp);
|
||||
out.is_unicast = true;
|
||||
out.reaction = r_SendOnly;
|
||||
break;
|
||||
case Track_Seek_Up:
|
||||
state = SEEKING_TRACK;
|
||||
@@ -160,256 +174,287 @@ void CDChanger::handle(const Frame *in, Frame *out) {
|
||||
++track;
|
||||
else
|
||||
track = 1;
|
||||
mins = 0xff;
|
||||
secs = 0x7f;
|
||||
flags2 = 0xc0;
|
||||
time.reset();
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
media_action(MediaAction::Track_Next);
|
||||
out->reaction = r_TrackChange;
|
||||
media = MediaAction::Track_Next;
|
||||
out.reaction = r_TrackChange;
|
||||
break;
|
||||
case Track_Seek_Down:
|
||||
state = SEEKING_TRACK;
|
||||
// Track down returns to track beginning if in ~middle of song
|
||||
if (mins == 0 && secs < 5) {
|
||||
if (const auto t = trackTime(); t && *t < 5s) {
|
||||
if (track > 1)
|
||||
--track;
|
||||
else
|
||||
track = TWODIGIT_MAX;
|
||||
|
||||
media_action(MediaAction::Track_Prev);
|
||||
media = MediaAction::Track_Prev;
|
||||
}
|
||||
mins = 0xff;
|
||||
secs = 0x7f;
|
||||
flags2 = 0xc0;
|
||||
time.reset();
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_TrackChange;
|
||||
out.reaction = r_TrackChange;
|
||||
break;
|
||||
case Track_Fast_Forward: {
|
||||
state |= SEEKING;
|
||||
secs += TIME_SKIP;
|
||||
if (secs > 59) {
|
||||
secs -= 60;
|
||||
++mins;
|
||||
}
|
||||
seek(TIME_SKIP);
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
media_action(MediaAction::Skip_Forward);
|
||||
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
|
||||
// is ~1 sec from now
|
||||
out->reaction = r_SendOnly;
|
||||
media = MediaAction::Skip_Forward;
|
||||
out.reaction = r_SendOnly;
|
||||
break;
|
||||
}
|
||||
case Track_Rewind: {
|
||||
state |= SEEKING;
|
||||
if (secs < 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;
|
||||
seek(-TIME_SKIP);
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
media_action(MediaAction::Skip_Backward);
|
||||
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
|
||||
// is ~1 sec from now
|
||||
out->reaction = r_SendOnly;
|
||||
media = MediaAction::Skip_Backward;
|
||||
out.reaction = r_SendOnly;
|
||||
break;
|
||||
}
|
||||
case CD_Enable_Random:
|
||||
flags |= RANDOM;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Disable_Random:
|
||||
flags &= ~RANDOM;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Enable_Repeat:
|
||||
flags |= REPEAT;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Disable_Repeat:
|
||||
flags &= ~REPEAT;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Enable_Disk_Random:
|
||||
flags |= DISK_RANDOM;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Disable_Disk_Random:
|
||||
flags &= ~DISK_RANDOM;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Enable_Disk_Repeat:
|
||||
flags |= DISK_REPEAT;
|
||||
generateStatus(out, true, Device::CMD_SW);
|
||||
out->reaction = r_StatusReport;
|
||||
out.reaction = r_StatusReport;
|
||||
break;
|
||||
case CD_Disable_Disk_Repeat:
|
||||
flags &= ~DISK_REPEAT;
|
||||
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;
|
||||
default: break;
|
||||
}
|
||||
#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) {
|
||||
auto resp = static_cast<reaction_t>(out->reaction);
|
||||
out->reaction = r_Nothing;
|
||||
switch (resp) {
|
||||
case r_StateReport:
|
||||
if (err == detail::Error::Send::NAK_ADDRESS &&
|
||||
++failedStatusReports > 1) {
|
||||
failedStatusReports = 0;
|
||||
stopPlaying(); // Disable periodic updates if e.g. no-one's
|
||||
// listening (car was turned off?)
|
||||
}
|
||||
break;
|
||||
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));
|
||||
std::unique_ptr<Frame>
|
||||
CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
|
||||
std::optional<MediaAction> media;
|
||||
std::unique_ptr<Frame> next;
|
||||
|
||||
if (const std::lock_guard lock(mutex); !exp) {
|
||||
if (exp.error().reaction == to_underlying(r_StateReport) &&
|
||||
exp.error().err == detail::Error::Send::NAK_ADDRESS &&
|
||||
++failedStatusReports > 1) {
|
||||
failedStatusReports = 0;
|
||||
media = stopPlaying(); // Disable periodic updates if e.g. no-one's
|
||||
// listening (car was turned off?)
|
||||
}
|
||||
out->reaction = r_Report_Load;
|
||||
break;
|
||||
case r_Report_Load:
|
||||
out->is_unicast = false;
|
||||
out->peripheral_addr = 0x1FF;
|
||||
out->length = sizeof(cdloading_resp) + 1;
|
||||
memcpy(out->data, cdloading_resp, sizeof(cdloading_resp));
|
||||
out->data[1] = to_underlying(Device::STATUS);
|
||||
out->data[2] = to_underlying(Action::Loading_Status);
|
||||
out->reaction = r_SendOnly;
|
||||
break;
|
||||
case r_TrackChange:
|
||||
setTime(0, 0);
|
||||
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is
|
||||
// ~1 sec from now
|
||||
[[fallthrough]];
|
||||
case r_NormalizeState:
|
||||
normalizeState();
|
||||
generateStatus(out, false, Device::STATUS);
|
||||
out->reaction = r_SendOnly;
|
||||
break;
|
||||
case r_StartPlaying:
|
||||
normalizeState();
|
||||
generateStatus(out, false, Device::STATUS);
|
||||
out->reaction = r_BeganPlaying;
|
||||
break;
|
||||
case r_BeganPlaying:
|
||||
startPlaying(); // only start PIT after normalizing state
|
||||
out->reaction = r_Nothing;
|
||||
break;
|
||||
case r_StatusReport:
|
||||
generateStatus(out, false, Device::STATUS);
|
||||
out->reaction = r_SendOnly;
|
||||
break;
|
||||
case r_SendOnly: [[fallthrough]];
|
||||
case r_Nothing: [[fallthrough]];
|
||||
default: out->reaction = r_Nothing;
|
||||
} else {
|
||||
auto out = std::move(exp.value());
|
||||
auto resp = static_cast<reaction_t>(out->reaction);
|
||||
out->reaction = r_Nothing;
|
||||
switch (resp) {
|
||||
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;
|
||||
break;
|
||||
case r_Report_Load:
|
||||
out->is_unicast = false;
|
||||
out->peripheral_addr = 0x1FF;
|
||||
out->length = sizeof(cdloading_resp) + 1;
|
||||
memcpy(out->data, cdloading_resp, sizeof(cdloading_resp));
|
||||
out->data[1] = to_underlying(Device::STATUS);
|
||||
out->data[2] = to_underlying(Action::Loading_Status);
|
||||
out->reaction = r_SendOnly;
|
||||
break;
|
||||
case r_TrackChange:
|
||||
setTime(0ms);
|
||||
// Track starts at a whole sec; next status is ~1 sec out
|
||||
xTimerReset(statusTimer, 0);
|
||||
[[fallthrough]];
|
||||
case r_NormalizeState:
|
||||
normalizeState();
|
||||
generateStatus(*out, false, Device::STATUS);
|
||||
out->reaction = r_SendOnly;
|
||||
break;
|
||||
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 (mins > TWODIGIT_MAX)
|
||||
mins = 0;
|
||||
if (secs > TWODIGIT_MAX)
|
||||
secs = 0;
|
||||
const std::lock_guard lock(mutex);
|
||||
if (!time)
|
||||
setTime(0ms);
|
||||
state = SEEKING | SEEKING_TRACK;
|
||||
flags2 = 0xc0;
|
||||
flags2 = 0x80;
|
||||
generateStatus(out, false, Device::STATUS);
|
||||
out->reaction = r_StartPlaying;
|
||||
out.reaction = r_StartPlaying;
|
||||
}
|
||||
}
|
||||
|
||||
bool CDChanger::pending() { return cdtimer_pending(); }
|
||||
void CDChanger::resolvepending() { cdtimer_clear(); }
|
||||
|
||||
void CDChanger::emit(Frame *out, uint16_t peripheral) {
|
||||
out->owning_device = id; // so react() routes r_StateReport back here
|
||||
out->peripheral_addr = peripheral;
|
||||
void CDChanger::emit(Frame &out, uint32_t /*payload*/) {
|
||||
statusQueued = false; // Clear first so a tick during emit() queues anew
|
||||
const std::lock_guard lock(mutex);
|
||||
generateStatus(out, false, Device::STATUS);
|
||||
out->reaction = r_StateReport;
|
||||
out.reaction = r_StateReport;
|
||||
}
|
||||
|
||||
bool CDChanger::isPlaying() const { return playing; }
|
||||
|
||||
// Sets CD_mode to play and resets timer count (so that the next interrupt is in
|
||||
// 1 sec)
|
||||
void CDChanger::startPlaying() {
|
||||
// Starts track time and the ~1 Hz status report (the first ~1 sec from now)
|
||||
std::optional<MediaAction> CDChanger::startPlaying() {
|
||||
static bool havePlayed = false;
|
||||
std::optional<MediaAction> media;
|
||||
if (havePlayed)
|
||||
media_action(MediaAction::Play);
|
||||
media = MediaAction::Play;
|
||||
havePlayed |= true;
|
||||
if (!playing)
|
||||
refTick = xTaskGetTickCount();
|
||||
playing = true;
|
||||
cdtimer_reset();
|
||||
xTimerReset(statusTimer, 0);
|
||||
return media;
|
||||
}
|
||||
|
||||
void CDChanger::stopPlaying() {
|
||||
cdtimer_disable();
|
||||
MediaAction CDChanger::stopPlaying() {
|
||||
xTimerStop(statusTimer, 0);
|
||||
time = trackTime();
|
||||
playing = false;
|
||||
media_action(MediaAction::Pause);
|
||||
return MediaAction::Pause;
|
||||
}
|
||||
|
||||
std::optional<std::chrono::milliseconds> CDChanger::trackTime() const {
|
||||
if (!time || !playing)
|
||||
return time;
|
||||
return *time + std::chrono::milliseconds{
|
||||
pdTICKS_TO_MS(xTaskGetTickCount() - refTick)};
|
||||
}
|
||||
|
||||
void CDChanger::setTime(std::chrono::milliseconds t) {
|
||||
time = t;
|
||||
refTick = xTaskGetTickCount();
|
||||
}
|
||||
|
||||
// Seek `by` from the current whole second; no-op while no time is shown
|
||||
void CDChanger::seek(std::chrono::seconds by) {
|
||||
const auto t = trackTime();
|
||||
if (!t)
|
||||
return;
|
||||
setTime(displaySeconds(*t + by));
|
||||
if (playing) // Seeked to a whole sec; next status is ~1 sec out
|
||||
xTimerReset(statusTimer, 0);
|
||||
}
|
||||
|
||||
// Serialize cd_status into the wire format.
|
||||
// track/mins/secs, need converted from decimal to BCD
|
||||
void CDChanger::serialize(uint8_t *dst) const {
|
||||
uint8_t mins = 0xFF; // "no time" sentinels
|
||||
uint8_t secs = 0x7F;
|
||||
bool negative = false;
|
||||
if (const auto t = trackTime()) {
|
||||
const auto sec = displaySeconds(*t);
|
||||
const auto magnitude = std::chrono::abs(sec);
|
||||
const auto wholeMins = std::chrono::floor<std::chrono::minutes>(magnitude);
|
||||
mins = toBCD((uint8_t)wholeMins.count());
|
||||
secs = toBCD((uint8_t)(magnitude - wholeMins).count());
|
||||
negative = sec < 0s;
|
||||
}
|
||||
|
||||
*dst++ = cds;
|
||||
*dst++ = state;
|
||||
*dst++ = disc;
|
||||
*dst++ = toBCD(track);
|
||||
*dst++ = toBCD(mins);
|
||||
*dst++ = toBCD(secs);
|
||||
*dst++ = mins;
|
||||
*dst++ = secs;
|
||||
*dst++ = flags;
|
||||
*dst++ = flags2;
|
||||
}
|
||||
|
||||
void CDChanger::setTime(uint8_t min, uint8_t sec) {
|
||||
mins = min;
|
||||
secs = sec;
|
||||
}
|
||||
|
||||
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++;
|
||||
*dst++ = negative ? (flags2 | NEGATIVE) : (flags2 & ~NEGATIVE);
|
||||
}
|
||||
|
||||
// Used for changed status messages
|
||||
void CDChanger::generateStatus(Frame *status, bool is_unicast,
|
||||
void CDChanger::generateStatus(Frame &status, bool is_unicast,
|
||||
Device to) const {
|
||||
status->is_unicast = is_unicast;
|
||||
status.is_unicast = is_unicast;
|
||||
if (!is_unicast)
|
||||
status->peripheral_addr = 0x1FF;
|
||||
status->control = 0xF;
|
||||
status->length = WIRE_SIZE + ((is_unicast) ? 4 : 3);
|
||||
status.peripheral_addr = 0x1FF;
|
||||
status.control = 0xF;
|
||||
status.length = WIRE_SIZE + ((is_unicast) ? 4 : 3);
|
||||
|
||||
uint8_t *data = status->data;
|
||||
uint8_t *data = status.data;
|
||||
if (is_unicast)
|
||||
*data++ = 0x00;
|
||||
*data++ = to_underlying(Device::CD_CHANGER);
|
||||
@@ -419,13 +464,10 @@ void CDChanger::generateStatus(Frame *status, bool is_unicast,
|
||||
}
|
||||
|
||||
void CDChanger::normalizeState() {
|
||||
if (mins > TWODIGIT_MAX)
|
||||
mins = 0;
|
||||
if (secs > TWODIGIT_MAX)
|
||||
secs = 0;
|
||||
if (!time)
|
||||
setTime(0ms);
|
||||
state = PLAYBACK;
|
||||
flags &= (uint8_t)~(DISK_SCAN | SCAN);
|
||||
flags2 = 0x80;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
||||
+49
-17
@@ -5,7 +5,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
#include "semphr.h"
|
||||
#include "timers.h"
|
||||
|
||||
#include "avclan.h"
|
||||
#include "device.hpp"
|
||||
@@ -38,6 +46,7 @@ public:
|
||||
REPEAT = 1 << 4,
|
||||
DISK_SCAN = 1 << 5,
|
||||
SCAN = 1 << 6,
|
||||
NEGATIVE = 1 << 6,
|
||||
};
|
||||
|
||||
/// Message state machine
|
||||
@@ -58,16 +67,17 @@ public:
|
||||
};
|
||||
|
||||
static constexpr Device id = Device::CD_CHANGER;
|
||||
void init();
|
||||
CDChanger() : mutex(xSemaphoreCreateMutex()) {}
|
||||
CDChanger(const CDChanger &) = delete;
|
||||
|
||||
void handle(const Frame *in, Frame *out);
|
||||
void react(Frame *out, detail::Error::Send err);
|
||||
void enable(Frame *out);
|
||||
void disable(Frame *out);
|
||||
static bool pending();
|
||||
static void resolvepending();
|
||||
void emit(Frame *out, uint16_t peripheral);
|
||||
void incrementTime();
|
||||
void init(Notifier notifier);
|
||||
|
||||
void handle(const Frame &in, Frame &out);
|
||||
std::unique_ptr<Frame>
|
||||
react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
|
||||
void enable(Frame &out);
|
||||
void disable(Frame &out);
|
||||
void emit(Frame &out, uint32_t payload);
|
||||
bool isPlaying() const;
|
||||
#ifndef NDEBUG
|
||||
void media_action(MediaAction action);
|
||||
@@ -76,23 +86,45 @@ public:
|
||||
#endif
|
||||
|
||||
private:
|
||||
void startPlaying();
|
||||
void stopPlaying();
|
||||
// Implements the BasicLockable named requirements for an xSemaphore
|
||||
class xMutex {
|
||||
public:
|
||||
explicit xMutex(SemaphoreHandle_t mutex) : mutex_{mutex} {}
|
||||
xMutex(const xMutex &) = delete;
|
||||
|
||||
void lock() noexcept { xSemaphoreTake(mutex_, portMAX_DELAY); }
|
||||
void unlock() noexcept { xSemaphoreGive(mutex_); }
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t mutex_ = nullptr;
|
||||
};
|
||||
|
||||
std::optional<MediaAction> startPlaying();
|
||||
MediaAction stopPlaying();
|
||||
std::optional<std::chrono::milliseconds> trackTime() const;
|
||||
void setTime(std::chrono::milliseconds t);
|
||||
void seek(std::chrono::seconds by);
|
||||
void serialize(uint8_t *dst) const;
|
||||
void setTime(uint8_t mins, uint8_t secs);
|
||||
void generateStatus(Frame *status, bool is_unicast, Device to) const;
|
||||
void generateStatus(Frame &status, bool is_unicast, Device to) const;
|
||||
void normalizeState();
|
||||
|
||||
Notifier notifier{};
|
||||
TimerHandle_t statusTimer = nullptr;
|
||||
std::atomic<bool> statusQueued = false; // coalesces status emit requests
|
||||
xMutex mutex;
|
||||
|
||||
// Track time is a stopwatch: `time` is relative to `refTick`, and
|
||||
// advances while `playing`. nullopt means no time is shown.
|
||||
std::optional<std::chrono::milliseconds> time;
|
||||
TickType_t refTick = 0;
|
||||
bool playing = false;
|
||||
int failedStatusReports = 0;
|
||||
uint8_t cds = CD1;
|
||||
uint8_t state = SEEKING | SEEKING_TRACK;
|
||||
uint8_t disc = 1;
|
||||
uint8_t track = 1; // Decimal storage; serialize to BCD
|
||||
uint8_t mins = 0xFF; // Decimal storage; serialize to BCD
|
||||
uint8_t secs = 0x7F; // Decimal storage; serialize to BCD
|
||||
uint8_t track = 1; // Decimal storage; serialize to BCD
|
||||
uint8_t flags = 0;
|
||||
uint8_t flags2 = 0xC0;
|
||||
uint8_t flags2 = 0x80;
|
||||
};
|
||||
|
||||
} // namespace avclan
|
||||
|
||||
+29
-7
@@ -5,9 +5,14 @@
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
#include "queue.h"
|
||||
|
||||
#include "avclan.h"
|
||||
#include "frame.hpp"
|
||||
#include "stdshim.hpp"
|
||||
|
||||
namespace avclan {
|
||||
|
||||
@@ -45,17 +50,34 @@ enum class Device : uint8_t {
|
||||
TRIP_INFO = 0xE5,
|
||||
};
|
||||
|
||||
// Notifies the Peripheral to emit for a device at `index`. Requests are served
|
||||
// in order, one `emit()` each; coalescing is up to the device. `val` (24 bits)
|
||||
// is device-defined. Returns whether the request was accepted within `wait`.
|
||||
class Notifier {
|
||||
public:
|
||||
Notifier() = default;
|
||||
Notifier(QueueHandle_t queue, uint8_t index) : queue{queue}, index{index} {}
|
||||
|
||||
bool notify(uint32_t val = 0, TickType_t wait = portMAX_DELAY) const {
|
||||
const uint32_t item = (val << 8) | index;
|
||||
return xQueueSend(queue, &item, wait) == pdPASS;
|
||||
}
|
||||
|
||||
private:
|
||||
QueueHandle_t queue = nullptr;
|
||||
uint8_t index = 0;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept DeviceInterface =
|
||||
requires { std::integral_constant<Device, T::id>{}; } &&
|
||||
requires(T dev, const Frame *in, Frame *out, detail::Error::Send err,
|
||||
uint16_t peripheral) {
|
||||
dev.init();
|
||||
requires(T dev, Notifier notifier, const Frame &in, Frame &out,
|
||||
expected<std::unique_ptr<Frame>, detail::SendError> exp,
|
||||
uint32_t payload) {
|
||||
dev.init(notifier);
|
||||
dev.handle(in, out);
|
||||
dev.enable(out);
|
||||
dev.react(out, err);
|
||||
{ dev.pending() } -> std::convertible_to<bool>;
|
||||
dev.resolvepending();
|
||||
dev.emit(out, peripheral);
|
||||
{ dev.react(std::move(exp)) } -> std::same_as<std::unique_ptr<Frame>>;
|
||||
dev.emit(out, payload);
|
||||
};
|
||||
} // namespace avclan
|
||||
|
||||
+157
-29
@@ -6,8 +6,53 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
#include "task.h"
|
||||
|
||||
#include "frame.hpp"
|
||||
#include "hal/stdio.h"
|
||||
#include "stdshim.hpp"
|
||||
|
||||
#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 {
|
||||
using Error = avclan::detail::Error;
|
||||
@@ -15,52 +60,135 @@ using enum Error::Parse;
|
||||
} // namespace
|
||||
|
||||
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 {
|
||||
// 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) {
|
||||
uint8_t buffer[8];
|
||||
uint8_t *bptr = buffer;
|
||||
*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
|
||||
*bptr++ = static_cast<uint8_t>(controller_addr >> 8);
|
||||
*bptr++ = static_cast<uint8_t>(controller_addr);
|
||||
*bptr++ = static_cast<uint8_t>(peripheral_addr >> 8);
|
||||
*bptr++ = static_cast<uint8_t>(peripheral_addr);
|
||||
*bptr++ = static_cast<char>(controller_addr >> 8);
|
||||
*bptr++ = static_cast<char>(controller_addr);
|
||||
*bptr++ = static_cast<char>(peripheral_addr >> 8);
|
||||
*bptr++ = static_cast<char>(peripheral_addr);
|
||||
|
||||
*bptr++ = control;
|
||||
*bptr++ = length;
|
||||
fwrite(buffer, 1, 8, stdout);
|
||||
fwrite(data, 1, length, stdout);
|
||||
*bptr++ = static_cast<char>(control);
|
||||
*bptr++ = static_cast<char>(length);
|
||||
|
||||
memcpy(bptr, data, length);
|
||||
bptr += length;
|
||||
|
||||
bptr = buffer;
|
||||
*bptr++ = 0x17; // End of transmission block
|
||||
*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++) {
|
||||
printf(" 0x%02X", static_cast<unsigned>(data[i]));
|
||||
}
|
||||
putchar('\n');
|
||||
stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
struct errtype {
|
||||
Error::Parse errno;
|
||||
Error::Parse type;
|
||||
uint8_t val;
|
||||
} err = {};
|
||||
|
||||
const uint8_t *last = bytes + len;
|
||||
|
||||
if (len < Frame::MIN_SIZE) {
|
||||
err.errno = TOO_SHORT;
|
||||
err.type = TOO_SHORT;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
@@ -73,7 +201,7 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
||||
length = *bytes++;
|
||||
|
||||
if (length > MAXLENGTH) {
|
||||
err.errno = LENGTH_TOO_BIG;
|
||||
err.type = LENGTH_TOO_BIG;
|
||||
err.val = length;
|
||||
goto handle_err;
|
||||
}
|
||||
@@ -81,14 +209,14 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
||||
if ((bytes + length) <= last) {
|
||||
memcpy(data, bytes, length);
|
||||
} else {
|
||||
err.errno = MISMATCH_LENGTH;
|
||||
err.type = MISMATCH_LENGTH;
|
||||
goto handle_err;
|
||||
}
|
||||
|
||||
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
||||
handle_err:;
|
||||
fputs("ERR(parse): ", stdout);
|
||||
switch (err.errno) {
|
||||
switch (err.type) {
|
||||
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
|
||||
case MISMATCH_LENGTH:
|
||||
puts("frame->length is longer than remaining data");
|
||||
@@ -101,6 +229,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
return err.errno;
|
||||
return err.type;
|
||||
}
|
||||
} // namespace avclan
|
||||
|
||||
+23
-3
@@ -6,9 +6,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "avclan.h"
|
||||
|
||||
#if defined(AVCLAN_FRAME_POOL_N)
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#endif
|
||||
|
||||
namespace avclan {
|
||||
enum class Device : uint8_t;
|
||||
|
||||
@@ -25,13 +31,27 @@ struct Frame {
|
||||
Error::Parse parse(const uint8_t *bytes, uint8_t len);
|
||||
void print(Print print) const;
|
||||
|
||||
uint8_t reaction;
|
||||
Device owning_device;
|
||||
// Retries every 2ms up to 3 times (max 6ms wait); null if still exhausted
|
||||
static std::unique_ptr<Frame> acquire();
|
||||
|
||||
#if defined(AVCLAN_FRAME_POOL_N)
|
||||
// O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
|
||||
// supported.
|
||||
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;
|
||||
uint16_t controller_addr; // formerly "master"
|
||||
uint16_t peripheral_addr; // formerly "slave"
|
||||
uint8_t control = 0xF;
|
||||
uint8_t length;
|
||||
uint8_t length = 0;
|
||||
uint8_t data[MAXLENGTH];
|
||||
};
|
||||
} // namespace avclan
|
||||
|
||||
@@ -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
@@ -7,21 +7,23 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
|
||||
#include "avclan.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
using Read = avclan::detail::Error::Read;
|
||||
using Send = avclan::detail::Error::Send;
|
||||
using Bit = avclan::detail::Bit;
|
||||
extern "C" {
|
||||
#else
|
||||
typedef enum Read Read;
|
||||
typedef enum Send Send;
|
||||
typedef enum Bit Bit;
|
||||
#endif
|
||||
|
||||
// One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted.
|
||||
void phy_init(void);
|
||||
//
|
||||
// `address` is our device address.
|
||||
void phy_init(uint16_t address);
|
||||
|
||||
// Mute/unmute device TX. "Muted" means transmission is disabled (RX is
|
||||
// unchanged/still allowed)
|
||||
@@ -30,56 +32,83 @@ void phy_mute(bool mute);
|
||||
// Non-mutating (e.g. theoretically const qualified/-able)
|
||||
bool phy_is_muted(void);
|
||||
|
||||
// True when bus is driven/"dominant" (logical 0)
|
||||
bool phy_active(void);
|
||||
// Do not respond to or log incoming frames. Can still send frames.
|
||||
void phy_deafen(bool deaf);
|
||||
|
||||
// Blocks until a frame is ready
|
||||
void phy_wait_frame(TickType_t xTicksToWait);
|
||||
|
||||
// Bus-transaction guard: quiesce the other async sources (e.g. interrupts)
|
||||
// so that bus read/send timing isn't disturbed. Re-enable relevant async
|
||||
// sources with `phy_guard_leave`. May be a no-op on a target where contention
|
||||
// isn't a concern.
|
||||
// sources with `phy_guard_leave`.
|
||||
// - May be a no-op on a target where concurrency/preemption isn't a concern.
|
||||
void phy_guard_enter(void);
|
||||
void phy_guard_leave(void);
|
||||
|
||||
// Start-bit handling, factored out of read/sendframe so the framing layer holds
|
||||
// no bus-timing or hardware-recovery logic.
|
||||
// - phy_read_startbit waits for and validates an incoming start bit, doing
|
||||
// any target-specific bus recovery; see avclan::detail::Error::Read.
|
||||
// - phy_send_startbit acquires the bus and emits a start bit; may return BUSY
|
||||
Read phy_read_startbit(void);
|
||||
Send phy_send_startbit(void);
|
||||
/* Per-field frame I/O.
|
||||
*
|
||||
* All send functions may have asynchronous implementations (e.g. return before
|
||||
* the send has completed on the bus). Success is indicated by a zero value
|
||||
* `Read` or `Send` enum. Non-zero error codes indicate a synchronously
|
||||
* completed send failure. Otherwise, `phy_send_done` must be called to block
|
||||
* until all queued send's have completed, and may return the error code for a
|
||||
* previous (queued) send failure; a success return value indicates that all
|
||||
* queued send's have finished sending over the bus.
|
||||
*
|
||||
* The read functions are similarly optionally asynchronous, and may return the
|
||||
* results of buffered reads. When this is the case, phy_read_header returns the
|
||||
* relevant error for the entire frame.
|
||||
*
|
||||
* It is invalid to call any later read or send function after getting an
|
||||
* error (e.g. calling `phy_read_data` after `phy_read_length` errored).
|
||||
*
|
||||
* `expect_ack` indicates whether the recipient should be ACK'ing; false for
|
||||
* broadcast frames which don't have a single recipient.
|
||||
*/
|
||||
|
||||
/* Returns 0 (`(Send)0`) if the peripheral sent an ACK bit, otherwise returns
|
||||
NAK. An ACK bit is a cooperative bit, where the sender starts (drives the bus)
|
||||
for the sync period, and allows the receiver to drive the bus (or not) to
|
||||
finish a "1" bit.
|
||||
*/
|
||||
Send phy_read_ack(void);
|
||||
void phy_send_ack(void);
|
||||
// Returns NO_FRAME when there is no frame to read.
|
||||
Read phy_read_header(bool *is_unicast);
|
||||
Read phy_read_controller_addr(uint16_t *addr);
|
||||
Read phy_read_peripheral_addr(uint16_t *addr);
|
||||
Read phy_read_control(uint8_t *control);
|
||||
Read phy_read_length(uint8_t *length);
|
||||
// Read `length` data bytes. `data_index` is only written to on error, with the
|
||||
// index of the failed byte.
|
||||
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index);
|
||||
|
||||
// Per-symbol I/O. The send* helpers return the even parity of the bits sent;
|
||||
// the read* helpers return the even parity of the bits read. The _u8/_u16
|
||||
// suffixes name the source-operand width. The function implementations need not
|
||||
// all be separate/independent (e.g. all send functions could be redirect to a
|
||||
// single phy_send_bits_u16, etc).
|
||||
// N.B: `len` is the number of bits to send. The
|
||||
// C++ send/readbits templates are the only consumers and use constraints to
|
||||
// enforce valid len values, so runtime checks are unnecessary.
|
||||
// Send start and broadcast bits. Returns success or one of these error values:
|
||||
// MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
|
||||
// a broadcast).
|
||||
Send phy_send_header(bool is_unicast);
|
||||
|
||||
// Intended for sending parity bits
|
||||
void phy_send_bit(Bit bit);
|
||||
// Send the controller address. Returns success or LOST_ARBITRATION (a device
|
||||
// with a lower device is sending a frame).
|
||||
Send phy_send_controller_addr(uint16_t addr);
|
||||
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack);
|
||||
Send phy_send_control(uint8_t control, bool expect_ack);
|
||||
Send phy_send_length(uint8_t length, bool expect_ack);
|
||||
// Send `length` data bytes. `data_index` is only written to for NAK_DATA.
|
||||
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
|
||||
uint8_t *data_index);
|
||||
|
||||
// Variants available to minimize unnecessary work for max runtime efficiency
|
||||
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len);
|
||||
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len);
|
||||
Bit phy_send_byte(const uint8_t *byte);
|
||||
|
||||
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len);
|
||||
Bit phy_read_bits_u16(uint16_t *bits, int8_t len);
|
||||
Bit phy_read_byte(uint8_t *byte);
|
||||
// Allows asynchronous ports to block until the phy has finished sending the
|
||||
// frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS,
|
||||
// etc) or CONTENDED_BUS; a port that queues the header and controller address
|
||||
// also reports their BUSY or LOST_ARBITRATION here. `data_index` is only
|
||||
// written to for NAK_DATA. A fully synchronous port should always report
|
||||
// success.
|
||||
Send phy_send_done(uint8_t *data_index);
|
||||
|
||||
#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`).
|
||||
void phy_measure(void);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+22
-4
@@ -3,18 +3,36 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Generic stdio interface initialization. All user I/O goes through <stdio.h>
|
||||
// functions. Assumptions/invariants:
|
||||
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when
|
||||
// empty). Necessary to avoid stalling the REPL poll loop.
|
||||
// - stdout is *raw* — no '\n' -> "\r\n" translation: bare LF and binary frame
|
||||
// payloads (fwrite) are emitted verbatim
|
||||
// - A libc read of stdin blocks the calling task (not the CPU) until at least
|
||||
// one byte is available.
|
||||
// - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
|
||||
// change a bare LF or a binary frame payload. Writes through
|
||||
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
|
||||
// stream, in order.
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
||||
+134
-54
@@ -4,73 +4,125 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
#include "queue.h"
|
||||
|
||||
#include "avclan.h"
|
||||
#include "bus.hpp"
|
||||
#include "device.hpp"
|
||||
#include "frame.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <tuple>
|
||||
#include "stdshim.hpp"
|
||||
|
||||
namespace avclan {
|
||||
namespace detail {
|
||||
enum class Party : uint8_t { Sender, Recipient };
|
||||
}
|
||||
|
||||
template <DeviceInterface... Devs> class Peripheral {
|
||||
using Party = detail::Party;
|
||||
using enum Party;
|
||||
|
||||
public:
|
||||
using Error = detail::Error;
|
||||
|
||||
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
|
||||
bus.init();
|
||||
(std::get<Devs>(devices_).init(), ...);
|
||||
// lack of static reflection until C++26 limits our methods of doing a
|
||||
// compile-time collision check. Within Peripheral, we can at least assert no
|
||||
// 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_; };
|
||||
template <DeviceInterface Dev> Dev &device() {
|
||||
return std::get<Dev>(devices_);
|
||||
}
|
||||
|
||||
bool bus_is_active() const { return bus.is_active(); };
|
||||
void mute(bool mute) { bus.mute(mute); };
|
||||
bool is_muted() const { return bus.is_muted(); };
|
||||
|
||||
#ifndef NDEBUG
|
||||
Bus &get_bus() { return bus; }
|
||||
void set_controller(uint16_t addr) { controller_ = addr; }
|
||||
#endif
|
||||
|
||||
Error::Read read(Frame *in, Frame::Print print) {
|
||||
return bus.read(address_, in, print);
|
||||
expected<std::unique_ptr<Frame>, Error::Read>
|
||||
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
|
||||
postmark(out);
|
||||
return bus.send(out, print);
|
||||
stamp<Sender>(*out);
|
||||
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))
|
||||
|
||||
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 Action;
|
||||
out->reaction = 0;
|
||||
|
||||
if (is_muted() || in->length < 3)
|
||||
return;
|
||||
if (is_muted() || in.length < 3)
|
||||
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
|
||||
// out->data[N] after memcpy.
|
||||
static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL),
|
||||
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 b1 = *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 to = b1;
|
||||
const auto action = b2;
|
||||
@@ -103,15 +155,14 @@ public:
|
||||
case PACK3(COMMUNICATION_V1, COMM_CTRL,
|
||||
to_underlying(Advertise_Function)):
|
||||
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)
|
||||
? [&] {
|
||||
out->owning_device = Devs::id;
|
||||
std::get<Devs>(devices_).enable(out);
|
||||
}()
|
||||
: void()),
|
||||
? originate(std::get<Devs>(devices_), *out, enable_d)
|
||||
: void()),
|
||||
...);
|
||||
break;
|
||||
}
|
||||
case PACK3(COMMUNICATION_V1, COMM_CTRL, to_underlying(Ping_Req)):
|
||||
case PACK3(COMMUNICATION_V2, COMM_CTRL, to_underlying(Ping_Req)): {
|
||||
out->is_unicast = true;
|
||||
@@ -127,8 +178,8 @@ public:
|
||||
to_underlying(List_Functions_Req)):
|
||||
case PACK3(COMMUNICATION_V2, COMM_CTRL,
|
||||
to_underlying(List_Functions_Req)): {
|
||||
controller_ = in->controller_addr;
|
||||
out->peripheral_addr = controller_;
|
||||
controller_ = in.controller_addr;
|
||||
stamp<Recipient>(*out); // re-stamp now that controller_ is known
|
||||
out->is_unicast = true;
|
||||
const uint8_t list_functions_resp[] = {
|
||||
0x00, to_underlying(COMM_CTRL), from,
|
||||
@@ -141,49 +192,78 @@ public:
|
||||
// case Restart_Lan: not handled
|
||||
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)
|
||||
? device_preroute(std::get<Devs>(devices_), in, out),
|
||||
0 : 0),
|
||||
? originate(std::get<Devs>(devices_), *out, handle_d)
|
||||
: void()),
|
||||
...);
|
||||
}
|
||||
|
||||
if (out->reaction > 0)
|
||||
return out;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
#undef PACK3
|
||||
|
||||
void react(Frame *out, Error::Send err) {
|
||||
if (((Devs::id == out->owning_device) || ...))
|
||||
((Devs::id == out->owning_device
|
||||
? std::get<Devs>(devices_).react(out, err),
|
||||
0 : 0),
|
||||
...);
|
||||
else
|
||||
out->reaction = 0;
|
||||
std::unique_ptr<Frame>
|
||||
react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
|
||||
const Device from =
|
||||
exp ? exp.value()->owning_device : exp.error().owning_device;
|
||||
|
||||
std::unique_ptr<Frame> next;
|
||||
((Devs::id == from &&
|
||||
(next = std::get<Devs>(devices_).react(std::move(exp)))) ||
|
||||
...);
|
||||
return next;
|
||||
}
|
||||
|
||||
template <class F> void poll_devices(F &&fun) {
|
||||
(poller(std::get<Devs>(devices_), fun), ...);
|
||||
// Blocks until a device requests an emit; requests are served in order.
|
||||
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:
|
||||
void postmark(Frame *out) const {
|
||||
out->controller_addr = address_;
|
||||
out->control = 0xF;
|
||||
// Dev's position in Devs
|
||||
template <class Dev> static constexpr uint8_t index_of() {
|
||||
constexpr std::array is_dev{std::is_same_v<Dev, Devs>...};
|
||||
return std::ranges::find(is_dev, true) - is_dev.begin();
|
||||
}
|
||||
|
||||
template <DeviceInterface Dev, class F> void poller(Dev &dev, F &&fun) {
|
||||
if (dev.pending() && fun(dev))
|
||||
dev.resolvepending();
|
||||
template <Party P> void stamp(Frame &out) const {
|
||||
if constexpr (P == Sender)
|
||||
out.controller_addr = address_;
|
||||
else
|
||||
out.peripheral_addr = controller_;
|
||||
}
|
||||
template <DeviceInterface Dev>
|
||||
void device_preroute(Dev &dev, const Frame *in, Frame *out) {
|
||||
out->owning_device = Dev::id;
|
||||
dev.handle(in, out);
|
||||
|
||||
void originate(DeviceInterface auto &dev, Frame &out, auto &&fill) {
|
||||
out.owning_device = std::remove_reference_t<decltype(dev)>::id;
|
||||
stamp<Recipient>(out); // default set FIRST; fill() may override
|
||||
fill(dev, out);
|
||||
}
|
||||
|
||||
Bus &bus;
|
||||
uint16_t controller_ = 0;
|
||||
const uint16_t address_;
|
||||
std::tuple<Devs...> devices_;
|
||||
QueueHandle_t emit_requests_;
|
||||
};
|
||||
} // namespace avclan
|
||||
|
||||
@@ -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_compile_options(avclan PUBLIC
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
|
||||
)
|
||||
|
||||
find_program(AVRDUDE avrdude)
|
||||
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
|
||||
@@ -85,11 +90,14 @@ FetchContent_Declare(
|
||||
avr_libstdcpp
|
||||
GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git
|
||||
GIT_TAG 5354296040a2289c911062daa82336762231e897
|
||||
SYSTEM
|
||||
)
|
||||
FetchContent_MakeAvailable(avr_libstdcpp)
|
||||
add_library(libstdcpp INTERFACE)
|
||||
target_include_directories(libstdcpp SYSTEM
|
||||
INTERFACE ${avr_libstdcpp_SOURCE_DIR}/include)
|
||||
add_library(libstdcpp STATIC ${avr_libstdcpp_SOURCE_DIR}/src/new_handler.cc)
|
||||
target_include_directories(libstdcpp
|
||||
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)
|
||||
|
||||
if(NOT LIBC_VERSION_TEST)
|
||||
@@ -131,6 +139,7 @@ target_compile_definitions(jnk0le_uart
|
||||
PRIVATE F_CPU=${FREQSEL}
|
||||
)
|
||||
target_compile_options(jnk0le_uart PUBLIC
|
||||
$<$<CONFIG:Debug>:-Og>
|
||||
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
|
||||
target_link_libraries(avclan PUBLIC jnk0le_uart)
|
||||
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
#include <stdint.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
#include "avclan.h"
|
||||
#include "hal/cd_timer.h" // statustimer_enable/disable (guard)
|
||||
#include "hal/phy.h"
|
||||
#include "media_avr.h" // media_sync_during_mask (guard)
|
||||
|
||||
// F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by
|
||||
// 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
|
||||
// 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);
|
||||
}
|
||||
|
||||
// No-op: acks are driven from inside the synchronous read path, which a deaf
|
||||
// caller never enters.
|
||||
void phy_deafen([[maybe_unused]] bool deaf) {}
|
||||
|
||||
// True when the bus is being driven (i.e. not idle/floating).
|
||||
bool phy_active() { return !BUS_IS_IDLE; }
|
||||
bool phy_frame_pending() { return (!BUS_IS_IDLE) != 0; }
|
||||
|
||||
// Mute device TX on AVCLAN bus
|
||||
void phy_mute(bool mute) {
|
||||
@@ -112,11 +120,9 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) {
|
||||
AVCLAN_setBusDriven();
|
||||
}
|
||||
while (TCB1.CNT <= period) {};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void phy_send_bit(Bit bit) {
|
||||
static void phy_send_bit(Bit bit) {
|
||||
uint16_t zero_length, one_length;
|
||||
switch (bit) {
|
||||
case bit_zero:
|
||||
@@ -137,7 +143,7 @@ void phy_send_bit(Bit bit) {
|
||||
set_AVC_logic_for(1, one_length);
|
||||
}
|
||||
|
||||
void phy_send_ack() {
|
||||
static void phy_send_ack() {
|
||||
TCB1.CNT = 0;
|
||||
|
||||
// Wait for controller to begin ACK bit
|
||||
@@ -151,7 +157,7 @@ void phy_send_ack() {
|
||||
phy_send_bit(bit_zero);
|
||||
}
|
||||
|
||||
Send phy_read_ack() {
|
||||
static Send phy_read_ack() {
|
||||
TCB1.CNT = 0; // Double reset of TCB1.CNT: here
|
||||
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
|
||||
AVCLAN_setBusIdle(); // Stop driving bus
|
||||
@@ -166,13 +172,13 @@ Send phy_read_ack() {
|
||||
// Check/wait in case we get here before peripheral finishes ACK bit
|
||||
while (!BUS_IS_IDLE) {
|
||||
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
|
||||
return NAK;
|
||||
return NAK_TOO_LONG;
|
||||
}
|
||||
return (Send)0;
|
||||
}
|
||||
|
||||
// Send `len` bits on the AVCLAN bus; returns the even parity
|
||||
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
|
||||
static Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
|
||||
uint8_t b = *bits;
|
||||
uint8_t parity = 0;
|
||||
int8_t len_mod8 = 8;
|
||||
@@ -191,17 +197,19 @@ Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
|
||||
b <<= 1;
|
||||
}
|
||||
len_mod8 = 8;
|
||||
b = *--bits;
|
||||
// Avoid under-read for last byte
|
||||
if (len > 0)
|
||||
b = *--bits;
|
||||
}
|
||||
return (parity & 1);
|
||||
}
|
||||
|
||||
// Send `len` bits on the AVCLAN bus; returns the even parity
|
||||
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
|
||||
static Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
|
||||
return phy_send_bits_u8((const uint8_t *)bits + 1, len);
|
||||
}
|
||||
|
||||
Bit phy_send_byte(const uint8_t *byte) {
|
||||
static Bit phy_send_byte(const uint8_t *byte) {
|
||||
uint8_t b = *byte;
|
||||
uint8_t parity = 0;
|
||||
|
||||
@@ -225,19 +233,22 @@ ISR(TCB0_INT_vect) {
|
||||
// last_tcb1 = cur_tcb1;
|
||||
// #endif
|
||||
|
||||
READING_BYTE <<= 1;
|
||||
// If the logical `0` pulse was less than the sync + data period threshold,
|
||||
// bit was a 1
|
||||
pulsewidth = TCB0.CCMP;
|
||||
if (pulsewidth < (uint16_t)AVCLAN_READBIT_THRESHOLD) {
|
||||
READING_BYTE++;
|
||||
READING_PARITY++;
|
||||
uint16_t tmp = pulsewidth = TCB0.CCMP; // non-volatile tmp saves re-read in
|
||||
// read-bit-threshold comparison
|
||||
if (READING_NBITS) {
|
||||
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
|
||||
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
|
||||
static Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
|
||||
uint8_t parity;
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
READING_BYTE = 0;
|
||||
@@ -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
|
||||
Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
|
||||
static Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
|
||||
uint8_t parity = 0;
|
||||
if (len > 8) {
|
||||
uint8_t over = len - 8;
|
||||
@@ -277,7 +288,7 @@ Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
|
||||
}
|
||||
|
||||
// Read a byte on the AVCLAN bus
|
||||
Bit phy_read_byte(uint8_t *byte) {
|
||||
static Bit phy_read_byte(uint8_t *byte) {
|
||||
uint8_t parity;
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
READING_BYTE = 0;
|
||||
@@ -303,7 +314,16 @@ Bit phy_read_byte(uint8_t *byte) {
|
||||
return (Bit)(parity & 1);
|
||||
}
|
||||
|
||||
void phy_init() {
|
||||
// Our own peripheral address, and whether the frame being read is addressed to
|
||||
// it. hal/phy.h puts the ACK decision in the phy, so the latch that used to
|
||||
// live in Bus::read lives here now: set when the peripheral address comes in,
|
||||
// and reused by every field after it.
|
||||
static uint16_t own_address_;
|
||||
static bool acking_;
|
||||
|
||||
void phy_init(uint16_t address) {
|
||||
own_address_ = address;
|
||||
|
||||
// Set pin 6 and 7 as input
|
||||
PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
|
||||
// Disable input buffer; recommended when using AC
|
||||
@@ -338,15 +358,36 @@ void phy_init() {
|
||||
phy_mute(false); // unmute AVCLAN bus TX
|
||||
}
|
||||
|
||||
// Wait for and validate an incoming start bit. On an over-long "driven" bus
|
||||
// (AC2 latched high because the bus is actually floating) this kicks PA7 hard
|
||||
// high to unlatch the comparator. The framing layer maps the result to its own
|
||||
// error reporting; no printing happens here.
|
||||
Read phy_read_startbit() {
|
||||
uint16_t startbitlen = TCB1.CNT = 0;
|
||||
static Read phy_read_startbit() {
|
||||
// Following HAL header docs, this function is only called after a driven bus
|
||||
// was ~recently detected.
|
||||
// Two main (designed) entry flows, depending on timing of pulse end w.r.t.
|
||||
// beginning this function:
|
||||
// - 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) {
|
||||
startbitlen = TCB1.CNT;
|
||||
if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
|
||||
if (TCB1.CNT > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
|
||||
Read result = STARTBIT_TOO_LONG;
|
||||
while (!BUS_IS_IDLE) {
|
||||
// If bus is "driven" too long, assume the AC2 is latched (e.g.
|
||||
@@ -367,24 +408,48 @@ Read phy_read_startbit() {
|
||||
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)) {
|
||||
// We missed the beginning of this message; wait for it to finish (bus
|
||||
// continuously idle for >1 bit length) before returning, so we don't have
|
||||
// multiple false-starts while the in-progress message keeps sending more
|
||||
// bits.
|
||||
// Not a start bit; wait for the message to finish (bus continuously idle
|
||||
// for >1 bit length) before returning, so we only report one error (instead
|
||||
// of e.g. repeated "bad (short) start bit" errors)
|
||||
TCB1.CNT = 0;
|
||||
while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) {
|
||||
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
|
||||
}
|
||||
|
||||
// Acquire the bus and emit a start bit. Returns false if another device is
|
||||
// already driving the bus (we can't yet do proper CSMA/CD).
|
||||
Send phy_send_startbit() {
|
||||
static Send phy_send_startbit() {
|
||||
// wait for free line
|
||||
TCB1.CNT = 0;
|
||||
while (BUS_IS_IDLE) {
|
||||
@@ -413,6 +478,144 @@ Send phy_send_startbit() {
|
||||
return (Send)0;
|
||||
}
|
||||
|
||||
// Field bits, then the wire's parity bit, checked against what we counted.
|
||||
static Read read_parity(Bit calc, Read bad) {
|
||||
uint8_t wire = 0;
|
||||
phy_read_bits_u8(&wire, 1);
|
||||
return ((uint8_t)calc != wire) ? bad : (Read)0;
|
||||
}
|
||||
|
||||
// An acknowledge slot follows the field whether or not we drive it, so a frame
|
||||
// that isn't ours still has to have the slot read away.
|
||||
static void ack_slot(void) {
|
||||
if (acking_) {
|
||||
phy_send_ack();
|
||||
} else {
|
||||
uint8_t slot = 0;
|
||||
phy_read_bits_u8(&slot, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Read phy_read_header(bool *is_unicast) {
|
||||
acking_ = false; // New frame; nothing is addressed to us yet
|
||||
|
||||
const Read err = phy_read_startbit();
|
||||
if (err != (Read)0)
|
||||
return err;
|
||||
|
||||
uint8_t bit = 0;
|
||||
phy_read_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
|
||||
*is_unicast = (bit != 0);
|
||||
return (Read)0;
|
||||
}
|
||||
|
||||
Read phy_read_controller_addr(uint16_t *addr) {
|
||||
// No acknowledge slot: this field is the arbitration window.
|
||||
return read_parity(phy_read_bits_u16(addr, 12), BAD_CONTROLLER_PARITY);
|
||||
}
|
||||
|
||||
Read phy_read_peripheral_addr(uint16_t *addr) {
|
||||
const Read err =
|
||||
read_parity(phy_read_bits_u16(addr, 12), BAD_PERIPHERAL_PARITY);
|
||||
if (err != (Read)0)
|
||||
return err;
|
||||
|
||||
// The one field that decides the ack; every field after it reuses the answer.
|
||||
acking_ = !phy_is_muted() && (*addr == own_address_);
|
||||
ack_slot();
|
||||
return (Read)0;
|
||||
}
|
||||
|
||||
Read phy_read_control(uint8_t *control) {
|
||||
const Read err =
|
||||
read_parity(phy_read_bits_u8(control, 4), BAD_CONTROL_PARITY);
|
||||
if (err != (Read)0)
|
||||
return err;
|
||||
ack_slot();
|
||||
return (Read)0;
|
||||
}
|
||||
|
||||
Read phy_read_length(uint8_t *length) {
|
||||
const Read err = read_parity(phy_read_byte(length), BAD_LENGTH_PARITY);
|
||||
if (err != (Read)0)
|
||||
return err;
|
||||
ack_slot();
|
||||
return (Read)0;
|
||||
}
|
||||
|
||||
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
const Read err = read_parity(phy_read_byte(&data[i]), BAD_DATA_PARITY);
|
||||
if (err != (Read)0) {
|
||||
*data_index = i;
|
||||
return err;
|
||||
}
|
||||
ack_slot();
|
||||
}
|
||||
return (Read)0;
|
||||
}
|
||||
|
||||
// `expect_ack` is "a NAK here is worth reporting", not "emit the slot": the
|
||||
// slot goes out either way, and a broadcast frame simply has nobody to fill it.
|
||||
static Send send_ack_slot(bool expect_ack, Send nak) {
|
||||
if (expect_ack) {
|
||||
const Send err = phy_read_ack();
|
||||
return err == NAK ? nak : err;
|
||||
}
|
||||
|
||||
const uint8_t fill = 1U;
|
||||
phy_send_bits_u8(&fill, 1);
|
||||
return (Send)0;
|
||||
}
|
||||
|
||||
Send phy_send_header(bool is_unicast) {
|
||||
const Send err = phy_send_startbit();
|
||||
if (err != (Send)0)
|
||||
return err;
|
||||
|
||||
const uint8_t bit = (is_unicast) ? 1U : 0U;
|
||||
phy_send_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
|
||||
return (Send)0;
|
||||
}
|
||||
|
||||
Send phy_send_controller_addr(uint16_t addr) {
|
||||
// No acknowledge slot: this field is the arbitration window.
|
||||
phy_send_bit(phy_send_bits_u16(&addr, 12));
|
||||
return (Send)0;
|
||||
}
|
||||
|
||||
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
|
||||
phy_send_bit(phy_send_bits_u16(&addr, 12));
|
||||
return send_ack_slot(expect_ack, NAK_ADDRESS);
|
||||
}
|
||||
|
||||
Send phy_send_control(uint8_t control, bool expect_ack) {
|
||||
phy_send_bit(phy_send_bits_u8(&control, 4));
|
||||
return send_ack_slot(expect_ack, NAK_CONTROL);
|
||||
}
|
||||
|
||||
Send phy_send_length(uint8_t length, bool expect_ack) {
|
||||
phy_send_bit(phy_send_byte(&length));
|
||||
return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH);
|
||||
}
|
||||
|
||||
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
|
||||
uint8_t *data_index) {
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
phy_send_bit(phy_send_byte(&data[i]));
|
||||
const Send err = send_ack_slot(expect_ack, NAK_DATA);
|
||||
if (err != (Send)0) {
|
||||
*data_index = i;
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return (Send)0;
|
||||
}
|
||||
|
||||
// Every field above is on the wire, and has reported its own outcome, by the
|
||||
// time it returns: nothing is left to wait out or attribute.
|
||||
Send phy_send_done([[maybe_unused]] uint8_t *data_index) { return (Send)0; }
|
||||
|
||||
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
|
||||
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
|
||||
enabled. */
|
||||
@@ -433,9 +636,30 @@ void phy_guard_leave() {
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Only used immediately below
|
||||
#define XSTR(x) #x
|
||||
#define STR(x) XSTR(x)
|
||||
|
||||
// Hold the bus at a given level.
|
||||
// Overrides and restores mute state upon release (i.e. set recessive).
|
||||
static void phy_set_state(bool dominant) {
|
||||
static bool saved_mute = false;
|
||||
if (dominant) {
|
||||
saved_mute = phy_is_muted();
|
||||
AVCLAN_setBusDriven();
|
||||
phy_mute(false);
|
||||
} else {
|
||||
AVCLAN_setBusIdle();
|
||||
phy_mute(saved_mute);
|
||||
}
|
||||
}
|
||||
|
||||
void phy_set_dominant(void) { phy_set_state(true); }
|
||||
void phy_set_recessive(void) { phy_set_state(false); }
|
||||
|
||||
#ifdef MEASURE_BUS
|
||||
#include <stdio.h> // phy_measure() reporting (debug builds only)
|
||||
|
||||
// Only used immediately below
|
||||
#define XSTR(x) #x
|
||||
#define STR(x) XSTR(x)
|
||||
|
||||
static uint16_t pulses[100];
|
||||
static uint16_t periods[100];
|
||||
@@ -468,4 +692,6 @@ void phy_measure() {
|
||||
|
||||
phy_guard_leave();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -4,11 +4,27 @@
|
||||
#include <avr/io.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
#include "hal/stdio.h"
|
||||
#include "timing_avr.h" // IWYU pragma: export
|
||||
#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
|
||||
// (unlike the lib's uart_putchar). Keeps binary frame payloads intact.
|
||||
static int stdio_putchar(char data, FILE *stream) {
|
||||
@@ -17,6 +33,67 @@ static int stdio_putchar(char data, FILE *stream) {
|
||||
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
|
||||
// is empty (uart0_getData() returns a negative value when there is no data).
|
||||
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;
|
||||
// keep the ATtiny3216 pin-mux the old driver did.
|
||||
PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2
|
||||
PORTA.DIRSET = PIN1_bm; // TxD output
|
||||
PORTA.DIRCLR = PIN2_bm; // RxD input
|
||||
PORTA.DIRSET = PIN1_bm; // TxD output
|
||||
PORTA.DIRCLR = PIN2_bm; // RxD input
|
||||
|
||||
#ifdef USE_DOUBLE_SPEED
|
||||
#ifdef USE_DOUBLE_SPEED
|
||||
uart0_init(DOUBLE_BAUD_CALC(1200000));
|
||||
#else
|
||||
#else
|
||||
uart0_init(BAUD_CALC(1200000));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
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.
|
||||
#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
|
||||
// default 32
|
||||
#define RX_BUFFER_SIZE 64
|
||||
#define RX_BUFFER_SIZE 128
|
||||
|
||||
// Size of the ring buffers, must be power of 2
|
||||
// default 32
|
||||
#define TX_BUFFER_SIZE 128
|
||||
#define TX_BUFFER_SIZE 256
|
||||
|
||||
/*******************config for multiple USART * mcu's*************************/
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "hal/board.h"
|
||||
|
||||
extern "C" void board_init() {}
|
||||
|
||||
extern "C" void board_enable_interrupts() {}
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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
@@ -3,101 +3,39 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
namespace detail {
|
||||
template <class T, auto N> struct Deleter;
|
||||
}
|
||||
|
||||
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;
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
#include "queue.h"
|
||||
|
||||
// Owning FIFO of `T`s, safe to share between tasks
|
||||
template <class T> class Queue {
|
||||
public:
|
||||
// Only full and copy-convert-from-full construction is allowed
|
||||
Queue() = delete;
|
||||
explicit Queue(UBaseType_t length)
|
||||
: handle{xQueueCreate(length, sizeof(T *))} {
|
||||
configASSERT(handle);
|
||||
}
|
||||
Queue(const Queue &) = delete;
|
||||
|
||||
// Moving is unsupported due to being self-referential
|
||||
Queue(Queue &&) = delete;
|
||||
Queue &operator=(Queue &&) = delete;
|
||||
|
||||
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;
|
||||
// Blocks until accepted (INCLUDE_vTaskSuspend: portMAX_DELAY never times out)
|
||||
void push(std::unique_ptr<T> x) {
|
||||
T *ptr = x.release();
|
||||
xQueueSend(handle, &ptr, portMAX_DELAY);
|
||||
}
|
||||
|
||||
const T *peek() const {
|
||||
if (isEmpty())
|
||||
return nullptr;
|
||||
|
||||
return buf[mask(read)];
|
||||
std::unique_ptr<T> pop(TickType_t wait = portMAX_DELAY) {
|
||||
T *ptr = nullptr;
|
||||
xQueueReceive(handle, &ptr, wait);
|
||||
return std::unique_ptr<T>(ptr);
|
||||
}
|
||||
|
||||
std::unique_ptr<T, Deleter> pop() {
|
||||
if (isEmpty())
|
||||
return nullptr;
|
||||
|
||||
if constexpr (Owning)
|
||||
return {buf[mask(read++)], {this}};
|
||||
else
|
||||
return {buf[mask(read++)], {owner}};
|
||||
// The front item stays owned by the queue
|
||||
const T *peek(TickType_t wait = portMAX_DELAY) const {
|
||||
T *ptr = nullptr;
|
||||
xQueuePeek(handle, &ptr, wait);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t mask(uint8_t pos) const { return pos & (N - 1); }
|
||||
void 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;
|
||||
QueueHandle_t handle;
|
||||
};
|
||||
|
||||
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
@@ -7,6 +7,10 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include "FreeRTOS.h" // IWYU pragma: export
|
||||
#include "task.h"
|
||||
|
||||
#include "cdchanger.hpp"
|
||||
#include "frame.hpp"
|
||||
@@ -14,19 +18,32 @@
|
||||
#include "hal/stdio.h"
|
||||
#include "peripheral.hpp"
|
||||
#include "queue.hpp"
|
||||
|
||||
const char *const offon[] = {"OFF", "ON"};
|
||||
|
||||
constexpr uint8_t CACHE_SIZE = 32;
|
||||
#include "stdshim.hpp"
|
||||
|
||||
using namespace avclan;
|
||||
|
||||
namespace {
|
||||
Frame frames[CACHE_SIZE];
|
||||
using Periph = Peripheral<CDChanger>;
|
||||
|
||||
constinit Queue cache(frames);
|
||||
constinit Queue incoming = cache;
|
||||
constinit Queue outgoing = cache;
|
||||
const char *const offon[] = {"OFF", "ON"};
|
||||
|
||||
constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE;
|
||||
// A queue that can hold the entire Frame pool is never full, so the sender's
|
||||
// blocking push of a follow-up frame onto its own queue can't deadlock
|
||||
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
|
||||
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
|
||||
|
||||
// Must be ranked such that queues deterministically tend to empty
|
||||
constexpr UBaseType_t SendPriority = tskIDLE_PRIORITY + 3;
|
||||
constexpr UBaseType_t RoutePriority = tskIDLE_PRIORITY + 2;
|
||||
constexpr UBaseType_t PollPriority = tskIDLE_PRIORITY + 2;
|
||||
constexpr UBaseType_t ReceivePriority = tskIDLE_PRIORITY + 1;
|
||||
|
||||
struct Context {
|
||||
Periph &peripheral;
|
||||
Queue<Frame> &incoming;
|
||||
Queue<Frame> &outgoing;
|
||||
};
|
||||
|
||||
uint8_t hexChars[2];
|
||||
uint8_t hexDigit = 0; // current digit being written to hexChars
|
||||
@@ -59,63 +76,60 @@ void set_flag(bool *flag, bool val, const char *msg) {
|
||||
|
||||
void Setup();
|
||||
void print_help();
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Bus phy;
|
||||
[[noreturn]] void vReceiverTask(void *pvParameters) {
|
||||
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 Device;
|
||||
Peripheral<CDChanger> peripheral(phy, 0x360);
|
||||
using Error = decltype(peripheral)::Error;
|
||||
using Print = Frame::Print;
|
||||
|
||||
Setup();
|
||||
print_help();
|
||||
|
||||
auto &[peripheral, incoming, outgoing] =
|
||||
*static_cast<Context *>(pvParameters);
|
||||
while (true) {
|
||||
if (peripheral.bus_is_active()) {
|
||||
if (auto msg = 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) {
|
||||
if (int readkey = fgetc(stdin); readkey != EOF) {
|
||||
switch (readkey) {
|
||||
case '?': print_help(); break;
|
||||
case 'v': toggle_flag(&verbose, "Verbose errors:"); break;
|
||||
@@ -133,7 +147,7 @@ int main() {
|
||||
case 'x': set_flag(&printBinary, false, "Binary:"); break;
|
||||
|
||||
case 'E': // Beep
|
||||
if (auto out = cache.pop()) {
|
||||
if (auto out = Frame::acquire()) {
|
||||
out->is_unicast = true;
|
||||
out->peripheral_addr = peripheral.controller();
|
||||
{
|
||||
@@ -145,10 +159,10 @@ int main() {
|
||||
out->reaction = 1;
|
||||
outgoing.push(std::move(out));
|
||||
} else
|
||||
puts("!! Cache empty; unable to queue beep request");
|
||||
puts("!! failed Frame alloc for Beep !! ");
|
||||
break;
|
||||
case 'P':
|
||||
if (auto out = cache.pop()) {
|
||||
if (auto out = Frame::acquire()) {
|
||||
out->is_unicast = true;
|
||||
out->peripheral_addr = peripheral.controller();
|
||||
{
|
||||
@@ -163,7 +177,8 @@ int main() {
|
||||
}
|
||||
out->reaction = CDChanger::reaction_t::r_Ejection;
|
||||
outgoing.push(std::move(out));
|
||||
}
|
||||
} else
|
||||
puts("!! failed Frame alloc for Play !! ");
|
||||
break;
|
||||
|
||||
#ifndef NDEBUG
|
||||
@@ -189,7 +204,19 @@ int main() {
|
||||
while (peripheral.device<CDChanger>().media_busy()) {}
|
||||
puts("end");
|
||||
break;
|
||||
case '+':
|
||||
peripheral.get_bus().deafen(true);
|
||||
peripheral.get_bus().set_dominant();
|
||||
puts("Set bus dominant...");
|
||||
break;
|
||||
case '-':
|
||||
peripheral.get_bus().set_recessive();
|
||||
peripheral.get_bus().deafen(false);
|
||||
puts("Set bus recessive...");
|
||||
break;
|
||||
#ifdef MEASURE_BUS
|
||||
case 'M': peripheral.get_bus().measure(); break;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
case 0x10: // Signals binary sequence incoming
|
||||
@@ -218,41 +245,80 @@ int main() {
|
||||
hexChars[0] = hexChars[1] = 0;
|
||||
seqIsUnicast = false;
|
||||
break;
|
||||
case '\r': // Eat carriage return too
|
||||
if (readBinary)
|
||||
goto DEFAULT;
|
||||
[[fallthrough]];
|
||||
case '\n':
|
||||
if (readSeq && !readBinary && seqIdx == 0) {
|
||||
// Nothing to send, so leave the mode instead: an escape for an
|
||||
// entry started by accident.
|
||||
readSeq = false;
|
||||
hexDigit = hexChars[0] = hexChars[1] = 0;
|
||||
printAllFrames = lastPrintAllFrames;
|
||||
break;
|
||||
}
|
||||
if (readSeq && seqIdx > 0) {
|
||||
if (readBinary) {
|
||||
if (data_tmp[seqIdx - 1] == 0x17) {
|
||||
if (auto out = cache.pop()) {
|
||||
if (auto out = Frame::acquire()) {
|
||||
if (out->parse(data_tmp, --seqIdx) ==
|
||||
Frame::Error::Parse{0}) {
|
||||
out->reaction = 1;
|
||||
outgoing.push(std::move(out));
|
||||
readSeq = readBinary = false;
|
||||
}
|
||||
}
|
||||
readSeq = readBinary = false;
|
||||
} else
|
||||
puts("!! failed Frame alloc for input message !!");
|
||||
} else
|
||||
goto DEFAULT; // reading binary and this is a real data byte;
|
||||
// fall through to default
|
||||
} else if (seqIdx <= Frame::MAXLENGTH) {
|
||||
if (auto out = cache.pop()) {
|
||||
} else { // ASCII message
|
||||
if (auto out = Frame::acquire()) {
|
||||
const uint8_t sendLen =
|
||||
seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH;
|
||||
out->is_unicast = seqIsUnicast;
|
||||
out->peripheral_addr =
|
||||
seqIsUnicast ? peripheral.controller() : 0x1FF;
|
||||
out->length = seqIdx;
|
||||
memcpy(out->data, data_tmp, seqIdx);
|
||||
out->length = sendLen;
|
||||
memcpy(out->data, data_tmp, sendLen);
|
||||
out->reaction = 1;
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
data_tmp[seqIdx++] = readkey;
|
||||
} else {
|
||||
// Only take valid hex digits
|
||||
if (isxdigit(readkey) == 0)
|
||||
break;
|
||||
|
||||
hexChars[hexDigit++] = readkey;
|
||||
|
||||
if (hexDigit == 2) {
|
||||
@@ -277,9 +343,38 @@ int main() {
|
||||
}
|
||||
}
|
||||
} // switch (readkey)
|
||||
} else {
|
||||
clearerr(stdin);
|
||||
} // if (readkey != EOF)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Setup();
|
||||
print_help();
|
||||
|
||||
// Static: the scheduler reclaims main's stack for ISRs
|
||||
static Bus phy;
|
||||
static Periph peripheral(phy, 0x360);
|
||||
static Queue<Frame> incoming(CACHE_SIZE);
|
||||
static Queue<Frame> outgoing(CACHE_SIZE);
|
||||
static Context ctx{
|
||||
.peripheral = peripheral, .incoming = incoming, .outgoing = outgoing};
|
||||
|
||||
xTaskCreateAffinitySet(vSenderTask, "send/react", configMINIMAL_STACK_SIZE,
|
||||
&ctx, SendPriority, 0b01, nullptr);
|
||||
xTaskCreateAffinitySet(vRoutingTask, "router", configMINIMAL_STACK_SIZE, &ctx,
|
||||
RoutePriority, 0b01, nullptr);
|
||||
xTaskCreateAffinitySet(vPollTask, "poll", configMINIMAL_STACK_SIZE, &ctx,
|
||||
PollPriority, 0b01, nullptr);
|
||||
xTaskCreateAffinitySet(vReceiverTask, "receiver", configMINIMAL_STACK_SIZE,
|
||||
&ctx, ReceivePriority, 0b01, nullptr);
|
||||
xTaskCreateAffinitySet(vREPLTask, "repl", configMINIMAL_STACK_SIZE, &ctx,
|
||||
tskIDLE_PRIORITY, 0b01, nullptr);
|
||||
|
||||
vTaskStartScheduler();
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -291,8 +386,8 @@ void Setup() {
|
||||
|
||||
void print_help() {
|
||||
puts("AVCLAN Mockingboard v1");
|
||||
puts("U - begin reading for unicast message\n"
|
||||
"B - begin reading for broadcast message\n"
|
||||
puts("U - begin reading for unicast message (send with Enter key)\n"
|
||||
"B - begin reading for broadcast message (send with Enter key)\n"
|
||||
"m - Toggle mute for mockingboard bus activity\n"
|
||||
"v - Toggle verbose error logging\n"
|
||||
"l - Toggle message logging\n"
|
||||
@@ -307,6 +402,8 @@ void print_help() {
|
||||
"s - MIC skip forward\n"
|
||||
"b - MIC skip backward\n"
|
||||
"M - Measure bit-timing (pulse-widths and periods)\n"
|
||||
"+ - Set bus driven/dominant\n"
|
||||
"- - Set bus idle/recessive\n"
|
||||
#endif
|
||||
"? - Print this message");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user