25 Commits

Author SHA1 Message Date
Allen Hill e64fe519d6 Add debug method to manually set controller address (e.g. for bench testing) 2026-09-28 11:36:01 -07:00
Allen Hill 598f6ede0e Offload TX data section to DMA (releases up to 12.5ms back to RTOS) 2026-09-28 11:36:01 -07:00
Allen Hill c24bab7a80 Migrate to FreeRTOS 2026-09-28 11:36:01 -07:00
Allen Hill 5b9cb1da8b Add single byte send debug function 2026-09-20 15:08:19 -07:00
Allen Hill fbf5811496 Refine (further) the Phy API
- Refactor send/read data to read entire array instead of byte-oriented
  reads
- Simplify/improve consistency of asynchrony (all send functions now
  optionally async)
- Fix start-bit TX (now actually validate joinable start-bits)

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

Assisted-by: Claude Code (Opus 5)
2026-09-20 15:07:42 -07:00
Allen Hill 9efbb01ccf PIO proof-of-concept 2026-09-20 15:03:31 -07:00
Allen Hill 53c5abb3e9 Fix compile errors due to non-portable code 2026-09-03 16:07:27 -07:00
Allen Hill ac47cebe86 Add ~empty implementation of HAL for pico2 2026-09-03 16:06:57 -07:00
Allen Hill e30dbc501d initial build system tweaks for pico hardware 2026-09-03 12:50:00 -07:00
Allen Hill 6c460aa3a0 more infra/clangd updates 2026-08-19 11:45:06 -07:00
Allen Hill 6ce4c2cc21 Improve stdio_write_nonblock perf and indicator printing 2026-08-19 11:38:08 -07:00
Allen Hill b9f7978d3e react to [disk] scan button presses 2026-08-13 20:46:57 -07:00
Allen Hill cd68ee7267 misc infra/clangd updates 2026-08-13 17:55:46 -07:00
Allen Hill ee5b4a955c Update Lua dissector for newly understood bit flag (NEGATIVE time)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:50:21 -07:00
Allen Hill 5d6b5f4ce0 Handle flag for NEGATIVE time; unify time incrementing 2026-08-13 17:47:31 -07:00
Allen Hill 0bef565564 Speed up Frame printing to (finally) solve missing AVCLAN bus frames
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 17:34:15 -07:00
Allen Hill 66dd441610 Max the UART TX buffer size 2026-08-13 16:48:44 -07:00
Allen Hill 3ea7c2d44a Improve correctness of phy_read_startbit invariants/assumptions 2026-08-13 16:31:03 -07:00
Allen Hill 95dae572a0 Tighten up non-reading TCB0 ISR timing 2026-08-13 16:27:22 -07:00
Allen Hill f45a0f5e92 Shorten time to next phy_active check (early continue) 2026-08-13 16:25:50 -07:00
Allen Hill 994759dd39 Free up some ram 2026-08-13 16:24:55 -07:00
35 changed files with 3072 additions and 783 deletions
+4 -2
View File
@@ -19,12 +19,14 @@ Checks: |
-readability-function-cognitive-complexity, -readability-function-cognitive-complexity,
-misc-non-private-member-variables-in-classes -misc-non-private-member-variables-in-classes
WarningsAsErrors: "" WarningsAsErrors: ""
ExcludeHeaderFilterRegex: 'out/build' ExcludeHeaderFilterRegex: "out/build"
HeaderFilterRegex: '^src/.*' HeaderFilterRegex: "^src/.*"
SystemHeaders: false SystemHeaders: false
# AnalyzeTemporaryDtors: false # AnalyzeTemporaryDtors: false
FormatStyle: none FormatStyle: none
CheckOptions: CheckOptions:
- key: modernize-deprecated-headers.CheckHeaderFile
value: "false"
- key: readability-magic-numbers.IgnorePowersOf2IntegerValues - key: readability-magic-numbers.IgnorePowersOf2IntegerValues
value: "true" value: "true"
- key: readability-magic-numbers.IgnoredIntegerValues - key: readability-magic-numbers.IgnoredIntegerValues
+14
View File
@@ -0,0 +1,14 @@
CompileFlags:
Remove:
- -mlongcalls
- -fno-shrink-wrap
- -fstrict-volatile-bitfields
- -fno-tree-switch-conversion
- -fanalyzer
- -ffixed-r2
- -ffixed-r3
- -ffixed-r4
- -fzero-init-padding-bits
- -fno-malloc-dce
Add:
- -Wno-missing-field-initializers
+23 -16
View File
@@ -5,7 +5,17 @@
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:alpine-3.22", "image": "mcr.microsoft.com/devcontainers/base:alpine-3.22",
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
"features": {}, "features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {}
},
"mounts": [
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached",
// ~/.claude/skills/* are symlinks into ~/.agents/skills — mount it too or they dangle
"source=${localEnv:HOME}/.agents,target=/home/vscode/.agents,type=bind,consistency=cached"
],
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
@@ -17,28 +27,25 @@
"extensions": [ "extensions": [
"ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-extension-pack",
"rockcat.avr-support", "rockcat.avr-support",
"harikrishnan94.cxx-compiler-explorer" "harikrishnan94.cxx-compiler-explorer",
"llvm-vs-code-extensions.vscode-clangd",
"Anthropic.claude-code"
], ],
"settings": { "settings": {
"editor.formatOnSave": true,
"C_Cpp.codeAnalysis.clangTidy.checks.disabled": [
"clang-analyzer-core.NullDereference"
],
"C_Cpp.files.exclude": {
"build/**": true
},
"files.associations": { "files.associations": {
"*.S": "avr" "*.S": "avr"
}, },
"cmake.configureOnOpen": true, "clangd.path": "/usr/bin/clangd",
"compilerexplorer.compilationDirectory": "${workspaceFolder}/build" "clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/out/build"
]
} }
} }
} },
// "runArgs": [ "runArgs": [
// "--device=/dev/ttyUSB0", "--device=/dev/ttyUSB0",
// "--device=/dev/ttyUSB1" "--network=host"
// ] ]
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root" // "remoteUser": "root"
} }
+53 -37
View File
@@ -1,25 +1,34 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.30)
project(avclan-mockingboard VERSION 1.1 LANGUAGES C CXX) include(FetchContent)
if(AVCLAN_TARGET_SDK_FILE)
include(${AVCLAN_TARGET_SDK_FILE})
endif()
project(avclan-mockingboard VERSION 1.2 LANGUAGES C CXX ASM)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
# Hardware target (port) selection. The chosen target/<name>/ directory supplies
# the port implementation sources, the per-target include path, and all
# hardware-specific build configuration (compile defs/options, device-pack
# handling, flashing) via its own CMakeLists, pulled in with add_subdirectory
# below. The matching cross-compiler is selected separately by a toolchain file
# (e.g. cmake/avr-gcc-toolchain.cmake) named in a CMake preset. Adding a new
# board = a sibling target/ directory + a toolchain file + a preset wiring them.
set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build")
set_property(CACHE AVCLAN_TARGET PROPERTY STRINGS "avr-attiny3216" "pico2" "esp32")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_MSG_QUEUE_SIZE 16 CACHE STRING "Size of incoming/outgoing queues")
set(AVCLAN_FRAME_POOL_N 14 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
set(CMAKE_C_STANDARD 23) set(CMAKE_C_STANDARD 23)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
add_compile_options(
-Wall -Wswitch-enum -Werror
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13")
@@ -52,15 +61,6 @@ if(INSTALL_GIT_HOOKS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
endif() endif()
endif() endif()
# Hardware target (port) selection. The chosen target/<name>/ directory supplies
# the port implementation sources, the per-target include path, and all
# hardware-specific build configuration (compile defs/options, device-pack
# handling, flashing) via its own CMakeLists, pulled in with add_subdirectory
# below. The matching cross-compiler is selected separately by a toolchain file
# (e.g. cmake/avr-gcc-toolchain.cmake) named in a CMake preset. Adding a new
# board = a sibling target/ directory + a toolchain file + a preset wiring them.
set(AVCLAN_TARGET avr-attiny3216 CACHE STRING "Hardware target (port) to build")
# The AVC-LAN stack as a static library: the target-agnostic generic core (no # The AVC-LAN stack as a static library: the target-agnostic generic core (no
# <avr/...>, no register access). The selected target's port sources and flags # <avr/...>, no register access). The selected target's port sources and flags
# are contributed by its subdirectory. # are contributed by its subdirectory.
@@ -70,23 +70,45 @@ add_library(avclan STATIC
src/avclan/bus.cc src/avclan/bus.cc
) )
target_compile_definitions(avclan PUBLIC
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
AVCLAN_MSG_QUEUE_SIZE=${AVCLAN_MSG_QUEUE_SIZE}
)
target_compile_options(avclan PUBLIC
-Wall -Wswitch-enum -Werror
# Debug must not be -O0: both the vendored usart.h and avr-libc's
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
# that fatal.
$<$<CONFIG:Debug>:-Og>
$<$<CONFIG:Debug>:-fanalyzer>
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
# avclan exports its public generic headers (src/avclan) to consumers and # avclan exports its public generic headers (src/avclan) to consumers and
# reaches into src/ for sibling headers (stdio.h, board.h) during its own # reaches into src/ for sibling headers (stdio.h, board.h) during its own
# build. The selected target adds its own per-target include path. # build. The selected target adds its own per-target include path.
target_include_directories(avclan PUBLIC target_include_directories(avclan PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/avclan) ${CMAKE_CURRENT_SOURCE_DIR}/src/avclan)
add_executable(mockingboard # Pull in the selected hardware target: its port sources, per-target headers,
src/sniffer.cc # hardware-specific compile options/definitions, device-pack handling, and the
) # flashing target. Added after the targets above so it can extend them.
target_link_libraries(mockingboard avclan) add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
if(NOT TARGET mockingboard)
add_executable(mockingboard
src/sniffer.cc
)
target_link_libraries(mockingboard avclan)
endif()
# Report firmware section sizes on every build, when the toolchain provides a # Report firmware section sizes on every build, when the toolchain provides a
# size tool (CMAKE_SIZE). A standalone custom target (rather than a POST_BUILD # size tool (CMAKE_SIZE). A standalone custom target (rather than a POST_BUILD
# command on mockingboard) so it prints even when the link is up to date: # command on mockingboard) so it prints even when the link is up to date:
# custom targets with no output are always considered stale and re-run. # custom targets with no output are always considered stale and re-run.
# add_dependencies orders it after the link. # add_dependencies orders it after the link.
if(CMAKE_SIZE) if(TARGET mockingboard AND CMAKE_SIZE)
add_custom_target(mockingboard-size ALL add_custom_target(mockingboard-size ALL
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard> COMMAND ${CMAKE_SIZE} $<TARGET_FILE:mockingboard>
COMMENT "Section sizes: mockingboard firmware" COMMENT "Section sizes: mockingboard firmware"
@@ -95,18 +117,12 @@ if(CMAKE_SIZE)
add_dependencies(mockingboard-size mockingboard) add_dependencies(mockingboard-size mockingboard)
endif() endif()
# Pull in the selected hardware target: its port sources, per-target headers,
# hardware-specific compile options/definitions, device-pack handling, and the
# flashing target. Added after the targets above so it can extend them.
add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
# --- Polyfill headers for std facilities the toolchain may lack ------------- # --- Polyfill headers for std facilities the toolchain may lack -------------
# tl::expected / tl::optional back-fill std::expected / std::optional (with # tl::expected / tl::optional back-fill std::expected / std::optional (with
# C++23 monadic ops) when the build's stdlib predates them. Runs after the port # C++23 monadic ops) when the build's stdlib predates them. Runs after the port
# subdirectory to provide a port-supplied freestanding stdlib via # subdirectory to provide a port-supplied freestanding stdlib via
# CMAKE_REQUIRED_INCLUDES, if necessary. # CMAKE_REQUIRED_INCLUDES, if necessary.
include(CheckCXXSourceCompiles) include(CheckCXXSourceCompiles)
include(FetchContent)
find_program(GIT_EXECUTABLE git REQUIRED) find_program(GIT_EXECUTABLE git REQUIRED)
if(TARGET libstdcpp) if(TARGET libstdcpp)
+46 -10
View File
@@ -1,6 +1,24 @@
{ {
"version": 3, "version": 9,
"configurePresets": [ "configurePresets": [
{
"name": "debug-base",
"hidden": true,
"description": "Debug build settings",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "relwithdebinfo-base",
"hidden": true,
"description": "RelWithDebInfo build settings",
"inherits": "debug-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{ {
"name": "usb0", "name": "usb0",
"hidden": true, "hidden": true,
@@ -31,20 +49,38 @@
} }
}, },
{ {
"name": "debug-base", "name": "pico2",
"hidden": true, "hidden": true,
"description": "Debug build settings", "binaryDir": "${sourceDir}/out/build/pico2",
"inherits": "debug-base",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug" "AVCLAN_TARGET": "pico2",
"AVCLAN_FRAME_POOL_N": "14",
"AVCLAN_TARGET_SDK_FILE": "${sourceDir}/cmake/pico_sdk_import.cmake",
"PICO_SDK_FETCH_FROM_GIT": true
},
"environment": {
"PICO_PLATFORM": "rp2350-arm-s",
"PICO_BOARD": "pimoroni_pico_plus2_w_rp2350"
} }
}, },
{ {
"name": "relwithdebinfo-base", "name": "pico2-gcc",
"hidden": true, "displayName": "Pico 2W (GCC)",
"description": "RelWithDebInfo build settings", "inherits": "pico2",
"inherits": "debug-base", "description": "",
"cacheVariables": { "generator": "Ninja",
"CMAKE_BUILD_TYPE": "RelWithDebInfo" "environment": {
"PICO_COMPILER": "pico_arm_gcc"
}
},
{
"name": "pico2-clang",
"displayName": "Pico 2W (Clang)",
"inherits": "pico2",
"description": "",
"environment": {
"PICO_COMPILER": "pico_arm_clang"
} }
}, },
{ {
+121
View File
@@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})
+1 -1
View File
@@ -1,2 +1,2 @@
sudo apk add make cmake avrdude avr-libc gcc-avr tshark sudo apk add make cmake avrdude avr-libc gcc-avr tshark clang-extra-tools
+34 -6
View File
@@ -203,14 +203,16 @@ local function bcd2dec(b)
end end
-- Format a BCD-encoded mm:ss pair, falling back to raw hex when a byte isn't -- Format a BCD-encoded mm:ss pair, falling back to raw hex when a byte isn't
-- valid BCD (e.g. a 0xff sentinel for "time unknown"). -- valid BCD (e.g. a 0xff sentinel for "time unknown"). `negative` (the CD
local function bcd_time(min_b, sec_b) -- NEGATIVE flag) only controls the sign, so it applies to the raw fallback too.
local function bcd_time(min_b, sec_b, negative)
local sign = negative and "-" or ""
local m = bcd2dec(min_b) local m = bcd2dec(min_b)
local s = bcd2dec(sec_b) local s = bcd2dec(sec_b)
if m == nil or s == nil then if m == nil or s == nil then
return string.format("%02x:%02x (raw)", min_b, sec_b) return string.format("%s%02x:%02x (raw)", sign, min_b, sec_b)
end end
return string.format("%02d:%02d", m, s) return string.format("%s%02d:%02d", sign, m, s)
end end
local known_devices = { local known_devices = {
@@ -478,6 +480,15 @@ local f_cd_flag_repeat = ProtoField.bool("avclan.cd.flags.repeat", "REPEAT", 8,
local f_cd_flag_disk_scan = ProtoField.bool("avclan.cd.flags.disk_scan", "DISK_SCAN", 8, nil, 0x20) local f_cd_flag_disk_scan = ProtoField.bool("avclan.cd.flags.disk_scan", "DISK_SCAN", 8, nil, 0x20)
local f_cd_flag_scan = ProtoField.bool("avclan.cd.flags.scan", "SCAN", 8, nil, 0x40) local f_cd_flag_scan = ProtoField.bool("avclan.cd.flags.scan", "SCAN", 8, nil, 0x40)
-- Second CD flags byte (last byte of the playback status frame). Resting value
-- is 0x80; 0x40 (NEGATIVE) makes the reported play time count below zero, as
-- seen when rewinding past the start of a track: ..., 00:01, 00:00, then the
-- byte flips to 0xc0 and the time climbs again as -00:01, -00:02, ...
-- [msgdumps/rewind-negative.txt].
local f_cd_flags2 = ProtoField.uint8("avclan.cd.flags2", "CD player flags (byte 2)")
local f_cd_flag_negative = ProtoField.bool("avclan.cd.flags2.negative", "NEGATIVE", 8, nil, 0x40)
local f_cd_flag2_unknown7 = ProtoField.bool("avclan.cd.flags2.unknown7", "UNKNOWN7", 8, nil, 0x80)
local f_tape_present = ProtoField.uint8("avclan.tape.present", "Tape deck slot", base.HEX, {[0x01] = "FILLED", [0x00] = "EMPTY"}) local f_tape_present = ProtoField.uint8("avclan.tape.present", "Tape deck slot", base.HEX, {[0x01] = "FILLED", [0x00] = "EMPTY"})
local f_tape_state = ProtoField.uint8("avclan.tape.state", "Tape deck state") local f_tape_state = ProtoField.uint8("avclan.tape.state", "Tape deck state")
local f_tape_seeking_rev = ProtoField.bool("avclan.tape.state.seeking_rev", "SEEKING_REVERSE", 8, nil, 0x01) local f_tape_seeking_rev = ProtoField.bool("avclan.tape.state.seeking_rev", "SEEKING_REVERSE", 8, nil, 0x01)
@@ -553,6 +564,9 @@ avclanproto.fields = {
f_cd_flag_repeat, f_cd_flag_repeat,
f_cd_flag_disk_scan, f_cd_flag_disk_scan,
f_cd_flag_scan, f_cd_flag_scan,
f_cd_flags2,
f_cd_flag_negative,
f_cd_flag2_unknown7,
f_tape_present, f_tape_present,
f_tape_state, f_tape_state,
f_tape_seeking_rev, f_tape_seeking_rev,
@@ -809,7 +823,7 @@ end
local function decode_cd(subtree, buffer, offset, action) local function decode_cd(subtree, buffer, offset, action)
if action == known_actions_names["PLAYBACK_STATUS"] or if action == known_actions_names["PLAYBACK_STATUS"] or
action == known_actions_names["PLAYBACK_RESP"] then action == known_actions_names["PLAYBACK_RESP"] then
local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player") local cdtree = subtree:add(avclanproto, buffer(offset,-1), "Device: CD player")
local cd_slots = cdtree:add(f_cd_slots, buffer(offset+3,1)) local cd_slots = cdtree:add(f_cd_slots, buffer(offset+3,1))
cd_slots:add(f_cd_slot1, buffer(offset+3,1)) cd_slots:add(f_cd_slot1, buffer(offset+3,1))
cd_slots:add(f_cd_slot2, buffer(offset+3,1)) cd_slots:add(f_cd_slot2, buffer(offset+3,1))
@@ -825,6 +839,12 @@ local function decode_cd(subtree, buffer, offset, action)
cd_state:add(f_cd_playback, buffer(offset+4,1)) cd_state:add(f_cd_playback, buffer(offset+4,1))
cd_state:add(f_cd_seeking_track, buffer(offset+4,1)) cd_state:add(f_cd_seeking_track, buffer(offset+4,1))
cd_state:add(f_cd_loading, buffer(offset+4,1)) cd_state:add(f_cd_loading, buffer(offset+4,1))
-- The sign of the play time lives in the trailing flags byte, so read it
-- before formatting the time (older/shorter reports may omit it).
local negative = false
if buffer:len() > offset+10 then
negative = bit.band(buffer(offset+10,1):uint(), 0x40) ~= 0
end
local cd_status = cdtree:add(avclanproto, buffer(offset+5,-1), "") local cd_status = cdtree:add(avclanproto, buffer(offset+5,-1), "")
cd_status:add(f_cd_disc, buffer(offset+5,1)) cd_status:add(f_cd_disc, buffer(offset+5,1))
cd_status:add(f_cd_track, buffer(offset+6,1)) cd_status:add(f_cd_track, buffer(offset+6,1))
@@ -832,7 +852,7 @@ local function decode_cd(subtree, buffer, offset, action)
cd_status:add(f_cd_sec, buffer(offset+8,1)) cd_status:add(f_cd_sec, buffer(offset+8,1))
cd_status:append_text("Disc " .. field_cd_disc().value .. ", ") cd_status:append_text("Disc " .. field_cd_disc().value .. ", ")
cd_status:append_text("track " .. tostring(buffer(offset+6,1)):gsub("(.)(.)", "%1%2") .. ", ") cd_status:append_text("track " .. tostring(buffer(offset+6,1)):gsub("(.)(.)", "%1%2") .. ", ")
cd_status:append_text("time " .. bcd_time(buffer(offset+7,1):uint(), buffer(offset+8,1):uint())) cd_status:append_text("time " .. bcd_time(buffer(offset+7,1):uint(), buffer(offset+8,1):uint(), negative))
local cd_flags = cdtree:add(f_cd_flags, buffer(offset+9,1)) local cd_flags = cdtree:add(f_cd_flags, buffer(offset+9,1))
cd_flags:add(f_cd_flag_disk_random, buffer(offset+9,1)) cd_flags:add(f_cd_flag_disk_random, buffer(offset+9,1))
cd_flags:add(f_cd_flag_random, buffer(offset+9,1)) cd_flags:add(f_cd_flag_random, buffer(offset+9,1))
@@ -840,6 +860,14 @@ local function decode_cd(subtree, buffer, offset, action)
cd_flags:add(f_cd_flag_repeat, buffer(offset+9,1)) cd_flags:add(f_cd_flag_repeat, buffer(offset+9,1))
cd_flags:add(f_cd_flag_disk_scan, buffer(offset+9,1)) cd_flags:add(f_cd_flag_disk_scan, buffer(offset+9,1))
cd_flags:add(f_cd_flag_scan, buffer(offset+9,1)) cd_flags:add(f_cd_flag_scan, buffer(offset+9,1))
if buffer:len() > offset+10 then
local cd_flags2 = cdtree:add(f_cd_flags2, buffer(offset+10,1))
cd_flags2:add(f_cd_flag_negative, buffer(offset+10,1))
cd_flags2:add(f_cd_flag2_unknown7, buffer(offset+10,1))
if negative then
cd_flags2:append_text(" (play time is negative)")
end
end
elseif action == known_actions_names["LOADING_STATUS"] or elseif action == known_actions_names["LOADING_STATUS"] or
action == known_actions_names["LOADING_RESP"] then action == known_actions_names["LOADING_RESP"] then
local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player") local cdtree = subtree:add(avclanproto, buffer(offset,9), "Device: CD player")
+4 -6
View File
@@ -147,6 +147,7 @@ struct Error {
STARTBIT_MALFORMED, STARTBIT_MALFORMED,
STARTBIT_TOO_LONG, STARTBIT_TOO_LONG,
BAD_STARTBIT, BAD_STARTBIT,
NO_FRAME,
POOL_EMPTY, // non-bus error POOL_EMPTY, // non-bus error
}; };
@@ -155,7 +156,10 @@ struct Error {
NAK_MESSAGE_LENGTH, NAK_MESSAGE_LENGTH,
NAK_CONTROL, NAK_CONTROL,
NAK_ADDRESS, NAK_ADDRESS,
NAK_TOO_LONG,
NAK, // generic NAK has max severity NAK, // generic NAK has max severity
CONTENDED_BUS,
LOST_ARBITRATION,
BUSY, BUSY,
MUTED, MUTED,
}; };
@@ -176,12 +180,6 @@ struct SendError {
}; };
#endif #endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t {
bit_zero = 0x00,
bit_one = 0x01,
bit_start = 0x10
};
#ifdef __cplusplus #ifdef __cplusplus
} // namespace detail } // namespace detail
} // namespace avclan } // namespace avclan
+143 -208
View File
@@ -26,13 +26,15 @@
| 1 │ *Acknowledge* | 1 │ *Acknowledge*
*repeat `n` times* *repeat `n` times*
No acknowledge bits are sent for broadcast frames. For broadcast frames the acknowledge (one) bit is sent, but an ACK response
(zero) is not expected.
*/ */
#include <concepts> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <new>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
@@ -43,16 +45,12 @@
namespace { namespace {
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send; using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
struct trailer_bits_t {}; // The bus spec has a unit that loses arbitration retry rather than fail: "if
struct no_parity_t : trailer_bits_t {}; // raw bits (the broadcast bit) // the unit loses in arbitration, the frame is automatically reset up twice
struct with_parity_t : trailer_bits_t {}; // bits + parity (controller address) // (three times in total)". Only an attempt that is outbid every time is an
struct with_ack_t : trailer_bits_t { // error worth reporting.
}; // bits + parity + ACK slot (all other fields) constexpr uint8_t SEND_ATTEMPTS = 3;
inline constexpr no_parity_t no_parity{};
inline constexpr with_parity_t with_parity{};
inline constexpr with_ack_t with_ack{};
} // namespace } // namespace
namespace avclan { namespace avclan {
@@ -71,222 +69,136 @@ public:
Handle(const Handle &) = delete; Handle(const Handle &) = delete;
Handle(Handle &&) = delete; Handle(Handle &&) = delete;
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // Forward to phy API (organized so that hal/phy.h isn't public/visible at the
Send sendstartbit() { return phy_send_startbit(); }; // C++/library level)
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTBEGIN(readability-convert-member-functions-to-static)
Read readstartbit() { return phy_read_startbit(); }; Read read_header(bool *is_unicast) { return phy_read_header(is_unicast); };
Read read_controller_addr(uint16_t *addr) {
template <auto N, std::unsigned_integral T, return phy_read_controller_addr(addr);
std::derived_from<trailer_bits_t> Trailer> };
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>) Read read_peripheral_addr(uint16_t *addr) {
Send send(T bits, Trailer /*tag*/) { return phy_read_peripheral_addr(addr);
const Bit parity = sendbits<N>(bits); };
Read read_control(uint8_t *control) { return phy_read_control(control); };
if constexpr (std::is_same_v<Trailer, with_parity_t>) Read read_length(uint8_t *length) { return phy_read_length(length); };
sendbits<1>(to_underlying(parity)); Read read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
return phy_read_data(data, length, data_index);
return Send{0};
}; };
template <auto N, std::unsigned_integral T> Send send_header(bool is_unicast) { return phy_send_header(is_unicast); };
requires(sizeof(T) < 3 && N < 16) Send send_controller_addr(uint16_t addr) {
Send send(T bits, with_ack_t /*tag*/, bool expect_ack) { return phy_send_controller_addr(addr);
send<N>(bits, with_parity);
if (expect_ack)
return read_ACK();
sendbits<1>(1U); // still need to fill the ack bit slot
return Send{0};
}; };
Send send_peripheral_addr(uint16_t addr, bool expect_ack) {
template <auto N, std::unsigned_integral T, return phy_send_peripheral_addr(addr, expect_ack);
std::derived_from<trailer_bits_t> Trailer>
requires(sizeof(T) < 3 && N < 16 && !std::same_as<Trailer, with_ack_t>)
Read read(T *bits, Trailer /*tag*/) {
const Bit calc_parity = readbits<N>(bits);
if constexpr (std::is_same_v<Trailer, with_parity_t>) {
uint8_t read_parity;
readbits<1>(&read_parity);
if (to_underlying(calc_parity) != read_parity)
return Read::BAD_PARITY;
}
return Read{0};
}; };
Send send_control(uint8_t control, bool expect_ack) {
template <auto N, std::unsigned_integral T, class F> return phy_send_control(control, expect_ack);
requires(sizeof(T) < 3 && N < 16)
Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
if (read<N>(bits, with_parity) == Read::BAD_PARITY)
return Read::BAD_PARITY;
if (ack()) {
send_ACK();
} else {
uint8_t slot;
readbits<1>(&slot);
}
return Read{0};
}; };
template <auto N, std::unsigned_integral T> Send send_length(uint8_t length, bool expect_ack) {
requires(sizeof(T) < 3 && N < 16) return phy_send_length(length, expect_ack);
Read read(T *bits, with_ack_t /*tag*/, bool ack) {
return read<N>(bits, with_ack, [=]() { return ack; });
}
private:
static void send_ACK() { phy_send_ack(); };
static Send read_ACK() { return phy_read_ack(); };
template <auto N, class T> Bit sendbits(T bits);
template <auto N, class T> Bit readbits(T *bits);
template <auto N>
requires(N > 1 && N < 8)
Bit sendbits(uint8_t bits) {
return phy_send_bits_u8(&bits, N);
}; };
template <auto N> Send send_data(const uint8_t *data, uint8_t length, bool expect_ack,
requires(N <= 16) uint8_t *data_index) {
Bit sendbits(uint16_t bits) { return phy_send_data(data, length, expect_ack, data_index);
return phy_send_bits_u16(&bits, N);
};
template <auto N>
requires(N < 8)
Bit readbits(uint8_t *bits) {
return static_cast<Bit>(phy_read_bits_u8(bits, N));
};
template <auto N>
requires(N <= 16)
Bit readbits(uint16_t *bits) {
return static_cast<Bit>(phy_read_bits_u16(bits, N));
}; };
// NOLINTEND(readability-convert-member-functions-to-static)
}; };
template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) { void Bus::init(uint16_t address) {
return phy_send_byte(&bits);
};
template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) {
const Bit bit{static_cast<Bit>(bits & 1U)};
phy_send_bit(bit);
return bit;
};
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
return phy_read_byte(bits);
};
void Bus::init() {
// Idempotent: the single Bus is shared by reference, so every Peripheral's // Idempotent: the single Bus is shared by reference, so every Peripheral's
// ctor calls init() on it — but the hardware must be brought up exactly once // ctor calls init() on it — but the hardware must be brought up exactly once
// (phy_init is not assumed re-entrant/idempotent). // (phy_init is not assumed re-entrant/idempotent).
if (inited_) if (inited_)
return; return;
phy_init(); phy_init(address);
muted_ = false; // phy_init leaves the bus TX unmuted muted_ = false; // phy_init leaves the bus TX unmuted
inited_ = true; inited_ = true;
}; };
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
bool Bus::is_active() const { return phy_active(); };
void Bus::mute(bool mute) { void Bus::mute(bool mute) {
phy_mute(mute); phy_mute(mute);
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
}; };
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void Bus::deafen(bool deaf) { phy_deafen(deaf); }
auto Bus::read(uint16_t address, Frame::Print print) auto Bus::read(Frame::Print print)
-> expected<std::unique_ptr<Frame>, Error::Read> { -> expected<std::unique_ptr<Frame>, Error::Read> {
struct errtype { struct errtype {
Read errno; Read type;
uint16_t val; uint16_t val;
} err = {}; } err = {};
using enum Read; using enum Read;
std::unique_ptr<Frame> in(new (std::nothrow) Frame); std::unique_ptr<Frame> in = Frame::acquire();
if (!in) { if (!in) {
err.errno = POOL_EMPTY; err.type = POOL_EMPTY;
goto handle_err; goto handle_err;
} }
phy_wait_frame(portMAX_DELAY);
{ // bound handle lifetime { // bound handle lifetime
auto handle = get(); auto handle = get();
bool shouldACK = false; err.type = handle.read_header(&in->is_unicast);
uint8_t tmp = 0; if (err.type != Read{0})
err.errno = handle.readstartbit();
if (err.errno != Read{0})
goto handle_err; goto handle_err;
handle.read<1>(&tmp, no_parity); err.type = handle.read_controller_addr(&in->controller_addr);
in->is_unicast = (tmp != 0U); if (err.type != Read{0}) {
if (auto rerr = handle.read<12>(&in->controller_addr, with_parity);
rerr == BAD_PARITY) {
err.errno = BAD_CONTROLLER_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->controller_addr; err.val = in->controller_addr;
goto handle_err; goto handle_err;
} }
// Using lambda for delayed evaluation of peripheral_addr field err.type = handle.read_peripheral_addr(&in->peripheral_addr);
// deref, which will be written by the time the lambda is evaluated if (err.type != Read{0}) {
auto should_ack_lambda = [&]() {
shouldACK = !is_muted() && (in->peripheral_addr == address);
return shouldACK;
};
if (auto rerr =
handle.read<12>(&in->peripheral_addr, with_ack, should_ack_lambda);
rerr == BAD_PARITY) {
err.errno = BAD_PERIPHERAL_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->peripheral_addr; err.val = in->peripheral_addr;
goto handle_err; goto handle_err;
} }
if (auto rerr = handle.read<4>(&in->control, with_ack, shouldACK); err.type = handle.read_control(&in->control);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_CONTROL_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->control; err.val = in->control;
goto handle_err; goto handle_err;
} }
if (auto rerr = handle.read<8>(&in->length, with_ack, shouldACK); err.type = handle.read_length(&in->length);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_LENGTH_PARITY;
if (print.verbose) if (print.verbose)
err.val = in->length; err.val = in->length;
goto handle_err; goto handle_err;
} } else if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.type = BAD_LENGTH_RANGE;
if (in->length == 0 || in->length > Frame::MAXLENGTH) {
err.errno = BAD_LENGTH_RANGE;
err.val = in->length; err.val = in->length;
goto handle_err; goto handle_err;
} }
for (uint8_t i = 0; i < in->length; i++) { uint8_t data_i = 0;
if (auto rerr = handle.read<8>(&in->data[i], with_ack, shouldACK); err.type = handle.read_data(in->data, in->length, &data_i);
rerr == BAD_PARITY) { if (err.type != Read{0}) {
err.errno = BAD_DATA_PARITY; if (print.verbose)
if (print.verbose) err.val = in->data[data_i];
err.val = in->data[i];
goto handle_err; goto handle_err;
}
} }
} // destroy handle } // destroy handle
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
if (err.type == NO_FRAME)
return unexpected{NO_FRAME};
fputs("ERR(read): ", stdout); fputs("ERR(read): ", stdout);
switch (err.errno) { switch (err.type) {
case POOL_EMPTY: puts("failed Frame alloc"); break; case POOL_EMPTY: puts("failed Frame alloc"); break;
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break; case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_MISSED: fputs("missed start bit", stdout); break; case STARTBIT_MISSED: fputs("missed start bit", stdout); break;
@@ -304,6 +216,7 @@ auto Bus::read(uint16_t address, Frame::Print print)
case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE; case BAD_LENGTH_PARITY: fputs("reading length", stdout); goto VERBOSE;
case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break; case BAD_LENGTH_RANGE: printf("bad length 0x%02X:", err.val); break;
case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE; case BAD_DATA_PARITY: fputs("reading data", stdout); goto VERBOSE;
case NO_FRAME:
case BAD_PARITY: case BAD_PARITY:
__builtin_unreachable(); __builtin_unreachable();
VERBOSE: VERBOSE:
@@ -315,14 +228,14 @@ auto Bus::read(uint16_t address, Frame::Print print)
} }
// Only print if some data has been correctly received // Only print if some data has been correctly received
if (print.print && (err.errno < STARTBIT_MISSED)) { if (print.print && (err.type < STARTBIT_MISSED)) {
if (err.errno > BAD_DATA_PARITY) if (err.type > BAD_DATA_PARITY)
in->length = 0; in->length = 0;
in->print(print); in->print(print);
} }
if (err.errno != Read{0}) if (err.type != Read{0})
return unexpected(err.errno); return unexpected(err.type);
return in; return in;
} }
@@ -331,83 +244,90 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
struct errtype { struct errtype {
// Error enum is ordered such that a lower numeric value corresponds to // Error enum is ordered such that a lower numeric value corresponds to
// more success // more success
Send errno; Send type;
uint8_t val; uint8_t val;
} err = {}; } err = {};
using enum Send; using enum Send;
if (is_muted()) { if (is_muted()) {
err.errno = MUTED; err.type = MUTED;
goto handle_err; goto handle_err;
} }
{ // bound handle lifetime for (uint8_t attempt = 0; attempt < SEND_ATTEMPTS; attempt++) {
auto handle = get(); auto handle = get(); // bound handle lifetime
if (handle.sendstartbit() == BUSY) { err.type = Send{0};
// Some other device is already driving the bus
err.errno = BUSY; err.type = handle.send_header(out.is_unicast);
if (err.type != Send{0}) // BUSY or LOST_ARBITRATION (broadcast)
continue;
err.type = handle.send_controller_addr(out.controller_addr);
if (err.type != Send{0}) // LOST_ARBITRATION: other device has lower address
continue;
err.type = handle.send_peripheral_addr(out.peripheral_addr, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_control(out.control, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_length(out.length, out.is_unicast);
if (err.type != Send{0})
goto handle_err;
err.type = handle.send_data(out.data, out.length, out.is_unicast, &err.val);
if (err.type != Send{0})
goto handle_err;
// A phy that only queued the fields above settles them here
uint8_t data_i = 0;
err.type = phy_send_done(&data_i);
if (err.type == BUSY || err.type == LOST_ARBITRATION) // Queued header
continue;
if (err.type != Send{0}) {
err.val = data_i;
goto handle_err; goto handle_err;
} }
handle.send<1>(static_cast<uint8_t>(out.is_unicast), no_parity); break; // Sent
}
handle.send<12>(out.controller_addr, with_parity); if (err.type != Send{0})
goto handle_err; // Outbid (or busy) on every attempt
if (auto serr =
handle.send<12>(out.peripheral_addr, with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_ADDRESS;
goto handle_err;
}
if (auto serr = handle.send<4>(out.control, with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_CONTROL;
goto handle_err;
}
if (auto serr = handle.send<8>(out.length, with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_MESSAGE_LENGTH;
goto handle_err;
}
for (uint8_t i = 0; i < out.length; i++) {
// Based on the µPD6708 datasheet, ACK bit for broadcast doesn't seem
// necessary (i.e. This deviates from the previous broadcast specific
// function that sent an extra `1` bit after each byte/parity)
// Explanation for why audio-group broadcast state report isn't working?
if (auto serr = handle.send<8>(out.data[i], with_ack, out.is_unicast);
serr == NAK) {
err.errno = NAK_DATA;
err.val = i;
goto handle_err;
}
}
} // destroy handle
// back to read mode // back to read mode
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("Error", stdout); fputs("Error", stdout);
switch (err.errno) { switch (err.type) {
case MUTED: fputs(": Device muted", stdout); break; case MUTED: fputs(": Device muted", stdout); break;
case BUSY: fputs(": Busy bus", stdout); break; case BUSY: fputs(": Busy bus", stdout); break;
case CONTENDED_BUS:
fputs(": bus contended after arbitration", stdout);
break;
case LOST_ARBITRATION: fputs(": lost arbitration", stdout); break;
case NAK_ADDRESS: case NAK_ADDRESS:
case NAK_CONTROL: case NAK_CONTROL:
case NAK_MESSAGE_LENGTH: case NAK_MESSAGE_LENGTH:
case NAK_DATA: case NAK_DATA:
case NAK_TOO_LONG:
case NAK: case NAK:
fputs(" NAK: ", stdout); fputs(" NAK: ", stdout);
switch (err.errno) { switch (err.type) {
case NAK_ADDRESS: fputs("address", stdout); break; case NAK_ADDRESS: fputs("address", stdout); break;
case NAK_CONTROL: fputs("Control", stdout); break; case NAK_CONTROL: fputs("Control", stdout); break;
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break; case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
case NAK_DATA: printf(" data[%u]", err.val); break; case NAK_DATA: printf(" data[%u]", err.val); break;
case NAK_TOO_LONG: fputs("too long", stdout); break;
case NAK: case NAK:
case MUTED: case MUTED:
case CONTENDED_BUS:
case LOST_ARBITRATION:
case BUSY: __builtin_unreachable(); case BUSY: __builtin_unreachable();
} }
break; break;
@@ -418,16 +338,31 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
if (print.print) if (print.print)
out.print(print); out.print(print);
return err.errno; return err.type;
} }
Bus::Handle Bus::get() { return Handle{*this}; }; Bus::Handle Bus::get() { return Handle{*this}; };
#if !defined(NDEBUG) && defined(MEASURE_BUS) #if !defined(NDEBUG)
// Debug bit-timing measurement on the one physical bus; instance-scoped for the
// same reason as is_active(). void Bus::set_dominant() { phy_set_dominant(); }
void Bus::set_recessive() { phy_set_recessive(); }
Send Bus::sendbyte(uint8_t byte, bool ack) {
auto handle = get();
uint8_t data_i = 0;
if (const Send serr = handle.send_data(&byte, 1, ack, &data_i);
serr != Send{0})
return serr;
return phy_send_done(&data_i);
}
#ifdef MEASURE_BUS
// Debug bit-timing measurement on the one physical bus; instance-scoped like
// deafen().
// NOLINTNEXTLINE(readability-convert-member-functions-to-static) // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void Bus::measure() { phy_measure(); } void Bus::measure() { phy_measure(); }
#endif
#endif #endif
} // namespace avclan } // namespace avclan
+12 -4
View File
@@ -69,18 +69,26 @@ public:
Bus(const Bus &) = delete; Bus(const Bus &) = delete;
Bus &operator=(const Bus &) = delete; Bus &operator=(const Bus &) = delete;
void init(); // `address` is our own peripheral address; the phy keeps it and acknowledges
// frames addressed to it without further instruction. One address per bus.
void init(uint16_t address);
bool is_active() const; // Prevent the device from being active on the bus
void mute(bool mute); void mute(bool mute);
bool is_muted() const { return muted_; }; bool is_muted() const { return muted_; };
// Set the device to be deaf to (ie don't react to) bus activity. It is
// orthogonal to mute — a deaf device can still send frames.
void deafen(bool deaf);
#ifndef NDEBUG #ifndef NDEBUG
void measure(); void measure();
detail::Error::Send sendbyte(uint8_t byte, bool ack = false);
void set_dominant();
void set_recessive();
#endif #endif
expected<std::unique_ptr<Frame>, Error::Read> read(uint16_t address, expected<std::unique_ptr<Frame>, Error::Read> read(Frame::Print print);
Frame::Print print);
Error::Send send(const Frame &out, Frame::Print print); Error::Send send(const Frame &out, Frame::Print print);
private: private:
+133 -105
View File
@@ -3,19 +3,22 @@
// Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com> // Copyright (C) 2015 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <mutex>
#include "avclan.h" #include "avclan.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "device.hpp" #include "device.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/cd_timer.h"
#include "hal/media.h" #include "hal/media.h"
namespace { namespace {
using namespace avclan; using namespace avclan;
using namespace std::chrono_literals;
constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER), constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
to_underlying(Device::STATUS), to_underlying(Device::STATUS),
@@ -28,39 +31,45 @@ constexpr uint8_t cdloading_resp[] = {to_underlying(Device::CD_CHANGER),
0x01, 0x01,
0x02}; 0x02};
constexpr int WIRE_SIZE = 8; // cd state report size in bytes constexpr int WIRE_SIZE = 8; // cd state report size in bytes
constexpr int TIME_SKIP = 15; // seconds constexpr auto TIME_SKIP = 15s;
constexpr int TWODIGIT_MAX = 99; constexpr int TWODIGIT_MAX = 99;
constexpr auto MAX_TIME = std::chrono::minutes{TWODIGIT_MAX} + 59s;
/* Pack a 0–TWODIGIT_MAX count into 2-digit BCD. Values >TWODIGIT_MAX (sentinels // Pack a 0–TWODIGIT_MAX count into 2-digit BCD
such as 0xFF / 0x7F meaning "no time") pass through unchanged so they survive
the wire round-trip. */
constexpr uint8_t toBCD(uint8_t val) { constexpr uint8_t toBCD(uint8_t val) {
if (val > TWODIGIT_MAX)
return val;
return (uint8_t)(((val / 10) << 4) | (val % 10)); return (uint8_t)(((val / 10) << 4) | (val % 10));
} }
extern "C" void incrementTime_callback(void *self) { // Whole seconds, saturated to the displayable range
static_cast<avclan::CDChanger *>(self)->incrementTime(); constexpr std::chrono::seconds displaySeconds(std::chrono::milliseconds t) {
} return std::clamp(std::chrono::floor<std::chrono::seconds>(t), -MAX_TIME,
MAX_TIME);
extern "C" bool isPlaying_callback(void *self) {
return static_cast<avclan::CDChanger *>(self)->isPlaying();
} }
} // namespace } // namespace
namespace avclan { namespace avclan {
void CDChanger::init() { void CDChanger::init(Notifier notif) {
media_init(); media_init();
cdtimer_init(this, &incrementTime_callback, &isPlaying_callback); notifier = notif;
statusTimer = xTimerCreate(
"cd status", pdMS_TO_TICKS(1000), true, this, [](TimerHandle_t timer) {
auto &self = *static_cast<CDChanger *>(pvTimerGetTimerID(timer));
// Timer callbacks must not block; a refused request is re-made next
// tick
if (!self.statusQueued.exchange(true) && !self.notifier.notify(0, 0))
self.statusQueued = false;
});
configASSERT(statusTimer);
} }
void CDChanger::handle(const Frame &in, Frame &out) { void CDChanger::handle(const Frame &in, Frame &out) {
if (in.length < 4) if (in.length < 4)
return; // [Currently known] valid CDChanger frames have at least 4 bytes return; // [Currently known] valid CDChanger frames have at least 4 bytes
std::optional<MediaAction> media;
const uint8_t *data = &in.data[1]; const uint8_t *data = &in.data[1];
const auto from = static_cast<Device>(*data++); const auto from = static_cast<Device>(*data++);
/* const auto to = */ data++; /* const auto to = */ data++;
@@ -73,7 +82,7 @@ void CDChanger::handle(const Frame &in, Frame &out) {
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum" #pragma GCC diagnostic ignored "-Wswitch-enum"
// Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]). // Unicast to CD changer: bytes are (0x00, from, to, action, [extra...]).
switch (action) { switch (const std::lock_guard lock(mutex); action) {
case Enable_Function_Req: case Enable_Function_Req:
out.is_unicast = true; out.is_unicast = true;
out.length = sizeof(function_change_resp); out.length = sizeof(function_change_resp);
@@ -93,7 +102,7 @@ void CDChanger::handle(const Frame &in, Frame &out) {
out.data[2] = to_underlying(from); out.data[2] = to_underlying(from);
out.data[3] = to_underlying(Disable_Function_Resp); out.data[3] = to_underlying(Disable_Function_Resp);
if (isPlaying()) { if (isPlaying()) {
stopPlaying(); media = stopPlaying();
state = 0; state = 0;
flags2 = 0x80; flags2 = 0x80;
out.reaction = r_StatusReport; out.reaction = r_StatusReport;
@@ -165,61 +174,39 @@ void CDChanger::handle(const Frame &in, Frame &out) {
++track; ++track;
else else
track = 1; track = 1;
mins = 0xff; time.reset();
secs = 0x7f;
flags2 = 0xc0;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Track_Next); media = MediaAction::Track_Next;
out.reaction = r_TrackChange; out.reaction = r_TrackChange;
break; break;
case Track_Seek_Down: case Track_Seek_Down:
state = SEEKING_TRACK; state = SEEKING_TRACK;
// Track down returns to track beginning if in ~middle of song // Track down returns to track beginning if in ~middle of song
if (mins == 0 && secs < 5) { if (const auto t = trackTime(); t && *t < 5s) {
if (track > 1) if (track > 1)
--track; --track;
else else
track = TWODIGIT_MAX; track = TWODIGIT_MAX;
media_action(MediaAction::Track_Prev); media = MediaAction::Track_Prev;
} }
mins = 0xff; time.reset();
secs = 0x7f;
flags2 = 0xc0;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out.reaction = r_TrackChange; out.reaction = r_TrackChange;
break; break;
case Track_Fast_Forward: { case Track_Fast_Forward: {
state |= SEEKING; state |= SEEKING;
secs += TIME_SKIP; seek(TIME_SKIP);
if (secs > 59) {
secs -= 60;
++mins;
}
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Forward); media = MediaAction::Skip_Forward;
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now
out.reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
case Track_Rewind: { case Track_Rewind: {
state |= SEEKING; state |= SEEKING;
if (secs < TIME_SKIP) { seek(-TIME_SKIP);
if (mins > 0) {
const uint8_t dif = TIME_SKIP - secs;
secs = 60 - dif;
--mins;
} else {
mins = 0;
secs = 0;
}
} else
secs -= TIME_SKIP;
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
media_action(MediaAction::Skip_Backward); media = MediaAction::Skip_Backward;
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick
// is ~1 sec from now
out.reaction = r_SendOnly; out.reaction = r_SendOnly;
break; break;
} }
@@ -263,21 +250,47 @@ void CDChanger::handle(const Frame &in, Frame &out) {
generateStatus(out, true, Device::CMD_SW); generateStatus(out, true, Device::CMD_SW);
out.reaction = r_StatusReport; out.reaction = r_StatusReport;
break; break;
case CD_Enable_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; default: break;
} }
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
// Media action handled outside of switch to minimize lock duration
if (media)
media_action(*media);
} }
std::unique_ptr<Frame> CDChanger::react( std::unique_ptr<Frame>
expected<std::unique_ptr<Frame>, detail::SendError> exp) { CDChanger::react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
std::optional<MediaAction> media;
std::unique_ptr<Frame> next;
if (!exp) { if (const std::lock_guard lock(mutex); !exp) {
if (exp.error().reaction == to_underlying(r_StateReport) && if (exp.error().reaction == to_underlying(r_StateReport) &&
exp.error().err == detail::Error::Send::NAK_ADDRESS && exp.error().err == detail::Error::Send::NAK_ADDRESS &&
++failedStatusReports > 1) { ++failedStatusReports > 1) {
failedStatusReports = 0; failedStatusReports = 0;
stopPlaying(); // Disable periodic updates if e.g. no-one's media = stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?) // listening (car was turned off?)
} }
} else { } else {
auto out = std::move(exp.value()); auto out = std::move(exp.value());
@@ -306,9 +319,9 @@ std::unique_ptr<Frame> CDChanger::react(
out->reaction = r_SendOnly; out->reaction = r_SendOnly;
break; break;
case r_TrackChange: case r_TrackChange:
setTime(0, 0); setTime(0ms);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is // Track starts at a whole sec; next status is ~1 sec out
// ~1 sec from now xTimerReset(statusTimer, 0);
[[fallthrough]]; [[fallthrough]];
case r_NormalizeState: case r_NormalizeState:
normalizeState(); normalizeState();
@@ -321,7 +334,7 @@ std::unique_ptr<Frame> CDChanger::react(
out->reaction = r_BeganPlaying; out->reaction = r_BeganPlaying;
break; break;
case r_BeganPlaying: case r_BeganPlaying:
startPlaying(); // only start PIT after normalizing state media = startPlaying(); // only start status timer after normalizing
out->reaction = r_Nothing; out->reaction = r_Nothing;
break; break;
case r_StatusReport: case r_StatusReport:
@@ -335,83 +348,101 @@ std::unique_ptr<Frame> CDChanger::react(
} }
if (out->reaction > r_Nothing) if (out->reaction > r_Nothing)
return out; next = std::move(out);
} }
return {}; if (media)
media_action(*media);
return next;
} }
void CDChanger::enable(Frame &out) { void CDChanger::enable(Frame &out) {
if (!isPlaying()) { if (!isPlaying()) {
if (mins > TWODIGIT_MAX) const std::lock_guard lock(mutex);
mins = 0; if (!time)
if (secs > TWODIGIT_MAX) setTime(0ms);
secs = 0;
state = SEEKING | SEEKING_TRACK; state = SEEKING | SEEKING_TRACK;
flags2 = 0xc0; flags2 = 0x80;
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out.reaction = r_StartPlaying; out.reaction = r_StartPlaying;
} }
} }
bool CDChanger::pending() { return cdtimer_pending(); } void CDChanger::emit(Frame &out, uint32_t /*payload*/) {
statusQueued = false; // Clear first so a tick during emit() queues anew
void CDChanger::emit(Frame &out) { const std::lock_guard lock(mutex);
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out.reaction = r_StateReport; out.reaction = r_StateReport;
cdtimer_clear();
} }
bool CDChanger::isPlaying() const { return playing; } bool CDChanger::isPlaying() const { return playing; }
// Sets CD_mode to play and resets timer count (so that the next interrupt is in // Starts track time and the ~1 Hz status report (the first ~1 sec from now)
// 1 sec) std::optional<MediaAction> CDChanger::startPlaying() {
void CDChanger::startPlaying() {
static bool havePlayed = false; static bool havePlayed = false;
std::optional<MediaAction> media;
if (havePlayed) if (havePlayed)
media_action(MediaAction::Play); media = MediaAction::Play;
havePlayed |= true; havePlayed |= true;
if (!playing)
refTick = xTaskGetTickCount();
playing = true; playing = true;
cdtimer_reset(); xTimerReset(statusTimer, 0);
return media;
} }
void CDChanger::stopPlaying() { MediaAction CDChanger::stopPlaying() {
cdtimer_disable(); xTimerStop(statusTimer, 0);
time = trackTime();
playing = false; playing = false;
media_action(MediaAction::Pause); return MediaAction::Pause;
}
std::optional<std::chrono::milliseconds> CDChanger::trackTime() const {
if (!time || !playing)
return time;
return *time + std::chrono::milliseconds{
pdTICKS_TO_MS(xTaskGetTickCount() - refTick)};
}
void CDChanger::setTime(std::chrono::milliseconds t) {
time = t;
refTick = xTaskGetTickCount();
}
// Seek `by` from the current whole second; no-op while no time is shown
void CDChanger::seek(std::chrono::seconds by) {
const auto t = trackTime();
if (!t)
return;
setTime(displaySeconds(*t + by));
if (playing) // Seeked to a whole sec; next status is ~1 sec out
xTimerReset(statusTimer, 0);
} }
// Serialize cd_status into the wire format. // Serialize cd_status into the wire format.
// track/mins/secs, need converted from decimal to BCD // track/mins/secs, need converted from decimal to BCD
void CDChanger::serialize(uint8_t *dst) const { void CDChanger::serialize(uint8_t *dst) const {
uint8_t mins = 0xFF; // "no time" sentinels
uint8_t secs = 0x7F;
bool negative = false;
if (const auto t = trackTime()) {
const auto sec = displaySeconds(*t);
const auto magnitude = std::chrono::abs(sec);
const auto wholeMins = std::chrono::floor<std::chrono::minutes>(magnitude);
mins = toBCD((uint8_t)wholeMins.count());
secs = toBCD((uint8_t)(magnitude - wholeMins).count());
negative = sec < 0s;
}
*dst++ = cds; *dst++ = cds;
*dst++ = state; *dst++ = state;
*dst++ = disc; *dst++ = disc;
*dst++ = toBCD(track); *dst++ = toBCD(track);
*dst++ = toBCD(mins); *dst++ = mins;
*dst++ = toBCD(secs); *dst++ = secs;
*dst++ = flags; *dst++ = flags;
*dst++ = flags2; *dst++ = negative ? (flags2 | NEGATIVE) : (flags2 & ~NEGATIVE);
}
void CDChanger::setTime(uint8_t min, uint8_t sec) {
mins = min;
secs = sec;
}
void CDChanger::incrementTime() {
// Sentinel values (>TWODIGIT_MAX) mean "no time"; leave them alone until
// setTime() replaces them with a real count.
if (secs > TWODIGIT_MAX)
return;
if (secs == 59) {
secs = 0;
if (mins == TWODIGIT_MAX)
mins = 0;
else
mins++;
} else
secs++;
} }
// Used for changed status messages // Used for changed status messages
@@ -433,13 +464,10 @@ void CDChanger::generateStatus(Frame &status, bool is_unicast,
} }
void CDChanger::normalizeState() { void CDChanger::normalizeState() {
if (mins > TWODIGIT_MAX) if (!time)
mins = 0; setTime(0ms);
if (secs > TWODIGIT_MAX)
secs = 0;
state = PLAYBACK; state = PLAYBACK;
flags &= (uint8_t)~(DISK_SCAN | SCAN); flags &= (uint8_t)~(DISK_SCAN | SCAN);
flags2 = 0x80;
} }
#ifndef NDEBUG #ifndef NDEBUG
+42 -11
View File
@@ -5,8 +5,15 @@
#pragma once #pragma once
#include <atomic>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <optional>
#include "FreeRTOS.h" // IWYU pragma: export
#include "semphr.h"
#include "timers.h"
#include "avclan.h" #include "avclan.h"
#include "device.hpp" #include "device.hpp"
@@ -39,6 +46,7 @@ public:
REPEAT = 1 << 4, REPEAT = 1 << 4,
DISK_SCAN = 1 << 5, DISK_SCAN = 1 << 5,
SCAN = 1 << 6, SCAN = 1 << 6,
NEGATIVE = 1 << 6,
}; };
/// Message state machine /// Message state machine
@@ -59,16 +67,17 @@ public:
}; };
static constexpr Device id = Device::CD_CHANGER; static constexpr Device id = Device::CD_CHANGER;
void init(); CDChanger() : mutex(xSemaphoreCreateMutex()) {}
CDChanger(const CDChanger &) = delete;
void init(Notifier notifier);
void handle(const Frame &in, Frame &out); void handle(const Frame &in, Frame &out);
std::unique_ptr<Frame> std::unique_ptr<Frame>
react(expected<std::unique_ptr<Frame>, detail::SendError> exp); react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
void enable(Frame &out); void enable(Frame &out);
void disable(Frame &out); void disable(Frame &out);
static bool pending(); void emit(Frame &out, uint32_t payload);
void emit(Frame &out);
void incrementTime();
bool isPlaying() const; bool isPlaying() const;
#ifndef NDEBUG #ifndef NDEBUG
void media_action(MediaAction action); void media_action(MediaAction action);
@@ -77,23 +86,45 @@ public:
#endif #endif
private: private:
void startPlaying(); // Implements the BasicLockable named requirements for an xSemaphore
void stopPlaying(); class xMutex {
public:
explicit xMutex(SemaphoreHandle_t mutex) : mutex_{mutex} {}
xMutex(const xMutex &) = delete;
void lock() noexcept { xSemaphoreTake(mutex_, portMAX_DELAY); }
void unlock() noexcept { xSemaphoreGive(mutex_); }
private:
SemaphoreHandle_t mutex_ = nullptr;
};
std::optional<MediaAction> startPlaying();
MediaAction stopPlaying();
std::optional<std::chrono::milliseconds> trackTime() const;
void setTime(std::chrono::milliseconds t);
void seek(std::chrono::seconds by);
void serialize(uint8_t *dst) const; void serialize(uint8_t *dst) const;
void setTime(uint8_t mins, uint8_t secs);
void generateStatus(Frame &status, bool is_unicast, Device to) const; void generateStatus(Frame &status, bool is_unicast, Device to) const;
void normalizeState(); void normalizeState();
Notifier notifier{};
TimerHandle_t statusTimer = nullptr;
std::atomic<bool> statusQueued = false; // coalesces status emit requests
xMutex mutex;
// Track time is a stopwatch: `time` is relative to `refTick`, and
// advances while `playing`. nullopt means no time is shown.
std::optional<std::chrono::milliseconds> time;
TickType_t refTick = 0;
bool playing = false; bool playing = false;
int failedStatusReports = 0; int failedStatusReports = 0;
uint8_t cds = CD1; uint8_t cds = CD1;
uint8_t state = SEEKING | SEEKING_TRACK; uint8_t state = SEEKING | SEEKING_TRACK;
uint8_t disc = 1; uint8_t disc = 1;
uint8_t track = 1; // Decimal storage; serialize to BCD uint8_t track = 1; // Decimal storage; serialize to BCD
uint8_t mins = 0xFF; // Decimal storage; serialize to BCD
uint8_t secs = 0x7F; // Decimal storage; serialize to BCD
uint8_t flags = 0; uint8_t flags = 0;
uint8_t flags2 = 0xC0; uint8_t flags2 = 0x80;
}; };
} // namespace avclan } // namespace avclan
+27 -10
View File
@@ -7,6 +7,9 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp" #include "stdshim.hpp"
@@ -47,20 +50,34 @@ enum class Device : uint8_t {
TRIP_INFO = 0xE5, TRIP_INFO = 0xE5,
}; };
// Notifies the Peripheral to emit for a device at `index`. Requests are served
// in order, one `emit()` each; coalescing is up to the device. `val` (24 bits)
// is device-defined. Returns whether the request was accepted within `wait`.
class Notifier {
public:
Notifier() = default;
Notifier(QueueHandle_t queue, uint8_t index) : queue{queue}, index{index} {}
bool notify(uint32_t val = 0, TickType_t wait = portMAX_DELAY) const {
const uint32_t item = (val << 8) | index;
return xQueueSend(queue, &item, wait) == pdPASS;
}
private:
QueueHandle_t queue = nullptr;
uint8_t index = 0;
};
template <class T> template <class T>
concept DeviceInterface = concept DeviceInterface =
requires { std::integral_constant<Device, T::id>{}; } && requires { std::integral_constant<Device, T::id>{}; } &&
requires(T dev, const Frame &in, Frame &out, requires(T dev, Notifier notifier, const Frame &in, Frame &out,
expected<std::unique_ptr<Frame>, detail::SendError> exp) { expected<std::unique_ptr<Frame>, detail::SendError> exp,
dev.init(); uint32_t payload) {
dev.init(notifier);
dev.handle(in, out); dev.handle(in, out);
dev.enable(out); dev.enable(out);
{ { dev.react(std::move(exp)) } -> std::same_as<std::unique_ptr<Frame>>;
dev.react(std::move(exp)) dev.emit(out, payload);
} -> std::same_as<std::unique_ptr<Frame>>;
{ dev.pending() } -> std::convertible_to<bool>;
// Devices must clear `pending()` after `emit()` is called
dev.emit(out);
}; };
} // namespace avclan } // namespace avclan
+109 -31
View File
@@ -6,14 +6,21 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory>
#include <new>
#include <type_traits>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "frame.hpp" #include "frame.hpp"
#include "hal/stdio.h"
#include "stdshim.hpp"
#if defined(AVCLAN_FRAME_POOL_N) #if defined(AVCLAN_FRAME_POOL_N)
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <limits> #include <limits>
#include <new>
namespace { namespace {
template <class T, std::uint8_t N> template <class T, std::uint8_t N>
@@ -57,60 +64,131 @@ namespace avclan {
#if defined(AVCLAN_FRAME_POOL_N) #if defined(AVCLAN_FRAME_POOL_N)
void *Frame::operator new(std::size_t /*count*/, void *Frame::operator new(std::size_t /*count*/,
const std::nothrow_t & /*tag*/) noexcept { const std::nothrow_t & /*tag*/) noexcept {
return pool.acquire(); taskENTER_CRITICAL();
Frame *frame = pool.acquire();
taskEXIT_CRITICAL();
return frame;
} }
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive // NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
void Frame::operator delete(void *ptr) noexcept { void Frame::operator delete(void *ptr) noexcept {
taskENTER_CRITICAL();
pool.release(static_cast<Frame *>(ptr)); pool.release(static_cast<Frame *>(ptr));
taskEXIT_CRITICAL();
} }
#endif #endif
std::unique_ptr<Frame> Frame::acquire() {
std::unique_ptr<Frame> frame(new (std::nothrow) Frame);
for (uint8_t retries = 0; !frame && retries < 3; retries++) {
vTaskDelay(pdMS_TO_TICKS(2));
frame.reset(new (std::nothrow) Frame);
}
return frame;
}
namespace {
// Emit `value` as at least `width` (lowercase, as to_chars emits) hex digits,
// zero-padded. `width` must be <= 3 (see the padding below); every call site
// reserves its field's worst case in the destination buffer, so this can't
// overflow.
template <class T>
requires std::is_unsigned_v<T>
char *put_hex(char *dest, T value, uint8_t width) {
// to_chars emits the minimum number of digits, so work out up front how many
// that will be to know how much zero padding goes in front of them. At most
// two iterations for the field widths used here.
uint8_t ndigits = 1;
for (T rest = value; rest >= 16; rest /= 16)
ndigits++;
// At most two pad digits (width <= 3). Written out rather than as a counted
// loop because GCC turns that into a memset() call — call overhead an order
// of magnitude above the one or two stores it replaces.
if (ndigits < width) {
*dest++ = '0';
if (ndigits + 1 < width)
*dest++ = '0';
}
return to_chars(dest, dest + ndigits, value, 16).ptr;
}
// " 0xNNN" for each field, plus the leading unicast digit and trailing newline
constexpr uint8_t LINE_MAX = 1 + (4 * 6) + (Frame::MAXLENGTH * 5) + 1;
// The same buffer serves the binary branch, which is the shorter of the two
static_assert(LINE_MAX >= 8 + Frame::MAXLENGTH + 2);
} // namespace
void Frame::print(Frame::Print print) const { void Frame::print(Frame::Print print) const {
// The AVC-LAN read loop polls for start bits between calls, so emitting per
// character (~3 µs each through stdio) would blow past a start bit's ~169 µs
// and lose the next frame.
char buffer[LINE_MAX];
char *bptr = buffer;
if (print.binary) { if (print.binary) {
uint8_t buffer[8];
uint8_t *bptr = buffer;
*bptr++ = 0x10; // Data Link Escape, signaling binary data forthcoming *bptr++ = 0x10; // Data Link Escape, signaling binary data forthcoming
*bptr++ = static_cast<uint8_t>(is_unicast); *bptr++ = static_cast<char>(is_unicast);
// Send addresses in big-endian order // Send addresses in big-endian order
*bptr++ = static_cast<uint8_t>(controller_addr >> 8); *bptr++ = static_cast<char>(controller_addr >> 8);
*bptr++ = static_cast<uint8_t>(controller_addr); *bptr++ = static_cast<char>(controller_addr);
*bptr++ = static_cast<uint8_t>(peripheral_addr >> 8); *bptr++ = static_cast<char>(peripheral_addr >> 8);
*bptr++ = static_cast<uint8_t>(peripheral_addr); *bptr++ = static_cast<char>(peripheral_addr);
*bptr++ = control; *bptr++ = static_cast<char>(control);
*bptr++ = length; *bptr++ = static_cast<char>(length);
fwrite(buffer, 1, 8, stdout);
fwrite(data, 1, length, stdout); memcpy(bptr, data, length);
bptr += length;
bptr = buffer;
*bptr++ = 0x17; // End of transmission block *bptr++ = 0x17; // End of transmission block
*bptr++ = 0x0A; // \n *bptr++ = 0x0A; // \n
fwrite(buffer, 1, 2, stdout);
} else {
printf("%X", static_cast<unsigned>(is_unicast));
printf(" 0x%03X", static_cast<unsigned>(controller_addr & 0x0FFF));
printf(" 0x%03X", static_cast<unsigned>(peripheral_addr & 0x0FFF));
printf(" 0x%X", static_cast<unsigned>(control & 0x0F));
printf(" 0x%X", static_cast<unsigned>(length & 0x0F));
for (uint8_t i = 0; i < length; i++) { stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
printf(" 0x%02X", static_cast<unsigned>(data[i])); return;
}
putchar('\n');
} }
struct field_t {
uint16_t value;
uint8_t width; // minimum digits, matching the old %03x / %x formats
};
*bptr++ = is_unicast ? '1' : '0';
for (const field_t field :
{field_t{.value = static_cast<uint16_t>(controller_addr & 0x0FFF),
.width = 3},
field_t{.value = static_cast<uint16_t>(peripheral_addr & 0x0FFF),
.width = 3},
field_t{.value = static_cast<uint16_t>(control & 0x0F), .width = 1},
field_t{.value = static_cast<uint16_t>(length & 0x0F), .width = 1}}) {
*bptr++ = ' ';
*bptr++ = '0';
*bptr++ = 'x';
bptr = put_hex(bptr, field.value, field.width);
}
for (uint8_t i = 0; i < length; i++) {
*bptr++ = ' ';
*bptr++ = '0';
*bptr++ = 'x';
bptr = put_hex(bptr, data[i], 2);
}
*bptr++ = '\n';
stdio_write_nonblock(buffer, static_cast<size_t>(bptr - buffer));
} }
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) { Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
struct errtype { struct errtype {
Error::Parse errno; Error::Parse type;
uint8_t val; uint8_t val;
} err = {}; } err = {};
const uint8_t *last = bytes + len; const uint8_t *last = bytes + len;
if (len < Frame::MIN_SIZE) { if (len < Frame::MIN_SIZE) {
err.errno = TOO_SHORT; err.type = TOO_SHORT;
goto handle_err; goto handle_err;
} }
@@ -123,7 +201,7 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
length = *bytes++; length = *bytes++;
if (length > MAXLENGTH) { if (length > MAXLENGTH) {
err.errno = LENGTH_TOO_BIG; err.type = LENGTH_TOO_BIG;
err.val = length; err.val = length;
goto handle_err; goto handle_err;
} }
@@ -131,14 +209,14 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
if ((bytes + length) <= last) { if ((bytes + length) <= last) {
memcpy(data, bytes, length); memcpy(data, bytes, length);
} else { } else {
err.errno = MISMATCH_LENGTH; err.type = MISMATCH_LENGTH;
goto handle_err; goto handle_err;
} }
if (false) { // NOLINT(readability-simplify-boolean-expr) if (false) { // NOLINT(readability-simplify-boolean-expr)
handle_err:; handle_err:;
fputs("ERR(parse): ", stdout); fputs("ERR(parse): ", stdout);
switch (err.errno) { switch (err.type) {
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break; case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
case MISMATCH_LENGTH: case MISMATCH_LENGTH:
puts("frame->length is longer than remaining data"); puts("frame->length is longer than remaining data");
@@ -151,6 +229,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
} }
} }
return err.errno; return err.type;
} }
} // namespace avclan } // namespace avclan
+4
View File
@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
@@ -30,6 +31,9 @@ struct Frame {
Error::Parse parse(const uint8_t *bytes, uint8_t len); Error::Parse parse(const uint8_t *bytes, uint8_t len);
void print(Print print) const; void print(Print print) const;
// Retries every 2ms up to 3 times (max 6ms wait); null if still exhausted
static std::unique_ptr<Frame> acquire();
#if defined(AVCLAN_FRAME_POOL_N) #if defined(AVCLAN_FRAME_POOL_N)
// O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is // O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported. // supported.
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
// CDChanger periodic-timer HAL: the ~1 Hz tick that drives the CD changer's
// status updates. The timer hardware is target-specific; the CD changer owns
// what a tick means (see cdchanger.cc). The device polls cdtimer_pending() and
// clears the tick with cdtimer_clear().
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// One-time hardware bring-up. Leaves the tick disabled.
void cdtimer_init(void *ptr, void(clbk)(void *), bool(isplay)(void *));
// Reset the count so the next tick is ~1 s out, and enable the tick.
void cdtimer_reset(void);
// Restore / disable the ~1 Hz tick.
void cdtimer_restore(void);
void cdtimer_disable(void);
extern volatile bool cdtimer_pending_flag;
static inline bool cdtimer_pending() { return cdtimer_pending_flag; }
static inline void cdtimer_clear() { cdtimer_pending_flag = false; }
#ifdef __cplusplus
}
#endif
+68 -39
View File
@@ -7,21 +7,23 @@
#include <stdint.h> #include <stdint.h>
#include "FreeRTOS.h" // IWYU pragma: export
#include "avclan.h" #include "avclan.h"
#ifdef __cplusplus #ifdef __cplusplus
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
using Send = avclan::detail::Error::Send; using Send = avclan::detail::Error::Send;
using Bit = avclan::detail::Bit;
extern "C" { extern "C" {
#else #else
typedef enum Read Read; typedef enum Read Read;
typedef enum Send Send; typedef enum Send Send;
typedef enum Bit Bit;
#endif #endif
// One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted. // One-time bring-up of the bus hardware. Leaves the bus idle and TX unmuted.
void phy_init(void); //
// `address` is our device address.
void phy_init(uint16_t address);
// Mute/unmute device TX. "Muted" means transmission is disabled (RX is // Mute/unmute device TX. "Muted" means transmission is disabled (RX is
// unchanged/still allowed) // unchanged/still allowed)
@@ -30,56 +32,83 @@ void phy_mute(bool mute);
// Non-mutating (e.g. theoretically const qualified/-able) // Non-mutating (e.g. theoretically const qualified/-able)
bool phy_is_muted(void); bool phy_is_muted(void);
// True when bus is driven/"dominant" (logical 0) // Do not respond to or log incoming frames. Can still send frames.
bool phy_active(void); void phy_deafen(bool deaf);
// Blocks until a frame is ready
void phy_wait_frame(TickType_t xTicksToWait);
// Bus-transaction guard: quiesce the other async sources (e.g. interrupts) // Bus-transaction guard: quiesce the other async sources (e.g. interrupts)
// so that bus read/send timing isn't disturbed. Re-enable relevant async // so that bus read/send timing isn't disturbed. Re-enable relevant async
// sources with `phy_guard_leave`. May be a no-op on a target where contention // sources with `phy_guard_leave`.
// isn't a concern. // - May be a no-op on a target where concurrency/preemption isn't a concern.
void phy_guard_enter(void); void phy_guard_enter(void);
void phy_guard_leave(void); void phy_guard_leave(void);
// Start-bit handling, factored out of read/sendframe so the framing layer holds /* Per-field frame I/O.
// no bus-timing or hardware-recovery logic. *
// - phy_read_startbit waits for and validates an incoming start bit, doing * All send functions may have asynchronous implementations (e.g. return before
// any target-specific bus recovery; see avclan::detail::Error::Read. * the send has completed on the bus). Success is indicated by a zero value
// - phy_send_startbit acquires the bus and emits a start bit; may return BUSY * `Read` or `Send` enum. Non-zero error codes indicate a synchronously
Read phy_read_startbit(void); * completed send failure. Otherwise, `phy_send_done` must be called to block
Send phy_send_startbit(void); * until all queued send's have completed, and may return the error code for a
* previous (queued) send failure; a success return value indicates that all
* queued send's have finished sending over the bus.
*
* The read functions are similarly optionally asynchronous, and may return the
* results of buffered reads. When this is the case, phy_read_header returns the
* relevant error for the entire frame.
*
* It is invalid to call any later read or send function after getting an
* error (e.g. calling `phy_read_data` after `phy_read_length` errored).
*
* `expect_ack` indicates whether the recipient should be ACK'ing; false for
* broadcast frames which don't have a single recipient.
*/
/* Returns 0 (`(Send)0`) if the peripheral sent an ACK bit, otherwise returns // Returns NO_FRAME when there is no frame to read.
NAK. An ACK bit is a cooperative bit, where the sender starts (drives the bus) Read phy_read_header(bool *is_unicast);
for the sync period, and allows the receiver to drive the bus (or not) to Read phy_read_controller_addr(uint16_t *addr);
finish a "1" bit. Read phy_read_peripheral_addr(uint16_t *addr);
*/ Read phy_read_control(uint8_t *control);
Send phy_read_ack(void); Read phy_read_length(uint8_t *length);
void phy_send_ack(void); // Read `length` data bytes. `data_index` is only written to on error, with the
// index of the failed byte.
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index);
// Per-symbol I/O. The send* helpers return the even parity of the bits sent; // Send start and broadcast bits. Returns success or one of these error values:
// the read* helpers return the even parity of the bits read. The _u8/_u16 // MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
// suffixes name the source-operand width. The function implementations need not // a broadcast).
// all be separate/independent (e.g. all send functions could be redirect to a Send phy_send_header(bool is_unicast);
// single phy_send_bits_u16, etc).
// N.B: `len` is the number of bits to send. The
// C++ send/readbits templates are the only consumers and use constraints to
// enforce valid len values, so runtime checks are unnecessary.
// Intended for sending parity bits // Send the controller address. Returns success or LOST_ARBITRATION (a device
void phy_send_bit(Bit bit); // with a lower device is sending a frame).
Send phy_send_controller_addr(uint16_t addr);
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack);
Send phy_send_control(uint8_t control, bool expect_ack);
Send phy_send_length(uint8_t length, bool expect_ack);
// Send `length` data bytes. `data_index` is only written to for NAK_DATA.
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index);
// Variants available to minimize unnecessary work for max runtime efficiency // Allows asynchronous ports to block until the phy has finished sending the
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len); // frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS,
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len); // etc) or CONTENDED_BUS; a port that queues the header and controller address
Bit phy_send_byte(const uint8_t *byte); // also reports their BUSY or LOST_ARBITRATION here. `data_index` is only
// written to for NAK_DATA. A fully synchronous port should always report
// success.
Send phy_send_done(uint8_t *data_index);
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len); #ifndef NDEBUG
Bit phy_read_bits_u16(uint16_t *bits, int8_t len);
Bit phy_read_byte(uint8_t *byte);
#if !defined(NDEBUG) && defined(MEASURE_BUS) // Hold the bus at a level until the matching call.
void phy_set_dominant(void);
void phy_set_recessive(void);
#ifdef MEASURE_BUS
// Sample and dump bus bit timing over the serial link (REPL `M`). // Sample and dump bus bit timing over the serial link (REPL `M`).
void phy_measure(void); void phy_measure(void);
#endif
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus
+22 -4
View File
@@ -3,18 +3,36 @@
#pragma once #pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
// Generic stdio interface initialization. All user I/O goes through <stdio.h> // Generic stdio interface initialization. All user I/O goes through <stdio.h>
// functions. Assumptions/invariants: // functions. Assumptions/invariants:
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when // - A libc read of stdin blocks the calling task (not the CPU) until at least
// empty). Necessary to avoid stalling the REPL poll loop. // one byte is available.
// - stdout is *raw* — no '\n' -> "\r\n" translation: bare LF and binary frame // - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
// payloads (fwrite) are emitted verbatim // change a bare LF or a binary frame payload. Writes through
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
// stream, in order.
void stdio_init(void); void stdio_init(void);
// Queue a buffer onto stdout--if it has space, or return false.
//
// If stdout cannot accept all `len` bytes now, the function queues
// a "\n!\n" indicator onto stdout, overwriting the last 3 chars if stdout is
// full. The caller must not assume that the buffer went out.
//
// A port MUST emit that indicator for every dropped buffer, and MUST be able to
// emit it even if stdout is full. A reader detects the loss as a line that
// holds only '!'. In the overwrite case the buffer in front of that line is
// truncated, so a reader MUST tolerate one malformed buffer there. The return
// value is useful only for diagnostics.
bool stdio_write_nonblock(const void *buf, uint8_t len);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+47 -50
View File
@@ -4,15 +4,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
#include <algorithm>
#include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <new>
#include <tuple> #include <tuple>
#include <type_traits>
#include <utility> #include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "queue.h"
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "device.hpp" #include "device.hpp"
@@ -38,28 +43,38 @@ public:
static_assert(((Devs::id != NoDevice) && ...), static_assert(((Devs::id != NoDevice) && ...),
"a registered Device id collides with the NoDevice sentinel; " "a registered Device id collides with the NoDevice sentinel; "
"update the sentinel value in avclan.h"); "update the sentinel value in avclan.h");
static_assert(
sizeof...(Devs) <= 0x100,
"too many devices: Notifier packs the device index into 8 bits");
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} { static constexpr UBaseType_t EMIT_QUEUE_LEN = 2 * sizeof...(Devs);
bus.init();
(std::get<Devs>(devices_).init(), ...); Peripheral(Bus &bus, uint16_t address)
: bus{bus}, address_{address},
emit_requests_{xQueueCreate(EMIT_QUEUE_LEN, sizeof(uint32_t))} {
configASSERT(emit_requests_);
bus.init(address);
(std::get<Devs>(devices_).init(Notifier{emit_requests_, index_of<Devs>()}),
...);
} }
Peripheral(const Peripheral &) = delete;
uint16_t controller() const { return controller_; }; uint16_t controller() const { return controller_; };
template <DeviceInterface Dev> Dev &device() { template <DeviceInterface Dev> Dev &device() {
return std::get<Dev>(devices_); return std::get<Dev>(devices_);
} }
bool bus_is_active() const { return bus.is_active(); };
void mute(bool mute) { bus.mute(mute); }; void mute(bool mute) { bus.mute(mute); };
bool is_muted() const { return bus.is_muted(); }; bool is_muted() const { return bus.is_muted(); };
#ifndef NDEBUG #ifndef NDEBUG
Bus &get_bus() { return bus; } Bus &get_bus() { return bus; }
void set_controller(uint16_t addr) { controller_ = addr; }
#endif #endif
expected<std::unique_ptr<Frame>, Error::Read> expected<std::unique_ptr<Frame>, Error::Read>
read(Frame::Print print = Frame::Print{}) { read(Frame::Print print = Frame::Print{}) {
return bus.read(address_, print); return bus.read(print);
}; };
expected<std::unique_ptr<Frame>, detail::SendError> expected<std::unique_ptr<Frame>, detail::SendError>
@@ -69,8 +84,9 @@ public:
out->control = 0xF; out->control = 0xF;
auto err = bus.send(*out, print); auto err = bus.send(*out, print);
if (err != Error::Send{0}) if (err != Error::Send{0})
return unexpected{ return unexpected{detail::SendError{.owning_device = out->owning_device,
detail::SendError{out->owning_device, out->reaction, err}}; .reaction = out->reaction,
.err = err}};
return out; return out;
}; };
@@ -85,7 +101,7 @@ public:
if (is_muted() || in.length < 3) if (is_muted() || in.length < 3)
return {}; return {};
std::unique_ptr<Frame> out(new (std::nothrow) Frame); std::unique_ptr<Frame> out = Frame::acquire();
if (!out) { if (!out) {
puts("!! failed Frame alloc in route !!"); puts("!! failed Frame alloc in route !!");
return unexpected{Error::Read::POOL_EMPTY}; return unexpected{Error::Read::POOL_EMPTY};
@@ -204,53 +220,33 @@ public:
return next; return next;
} }
// Service ready devices in round-robin order // Blocks until a device requests an emit; requests are served in order.
std::unique_ptr<Frame> poll() { std::unique_ptr<Frame> poll() {
using U = std::unique_ptr<Frame>; std::unique_ptr<Frame> out = Frame::acquire();
auto does_emit = [&](DeviceInterface auto &dev) -> U { if (!out) {
if (!dev.pending()) puts("!! failed Frame alloc in poll !!");
return {};
U out(new (std::nothrow) Frame);
if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
originate(dev, *out, [](auto &d, auto &out) { d.emit(out); });
return out;
};
// Runtime tuple index helper
auto does_index_emit = [&](std::size_t t) -> U {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> U {
U out;
((Is == t && (out = does_emit(std::get<Is>(devices_)))) || ...);
return out;
}(std::index_sequence_for<Devs...>{});
};
constexpr std::size_t N = sizeof...(Devs);
if constexpr (N == 1) { // round-robin not needed
return does_emit(std::get<0>(devices_));
} else {
static uint8_t rr_ = 0; // round-robin cursor
const std::size_t start = rr_;
for (std::size_t t = start; t < N; ++t) // [start, N)
if (auto out = does_index_emit(t)) {
rr_ = (t + 1 == N) ? 0 : t + 1;
return out;
}
for (std::size_t t = 0; t < start; ++t) // [0, start); t+1 <= start < N
if (auto out = does_index_emit(t)) {
rr_ = t + 1;
return out;
}
return {}; return {};
} }
uint32_t val;
xQueueReceive(emit_requests_, &val, portMAX_DELAY);
const uint8_t index = val & 0xFF;
auto emit_d = [val](auto &d, auto &out) { d.emit(out, val >> 8); };
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
((Is == index ? originate(std::get<Is>(devices_), *out, emit_d) : void()),
...);
}(std::index_sequence_for<Devs...>{});
return out;
} }
private: private:
// Dev's position in Devs
template <class Dev> static constexpr uint8_t index_of() {
constexpr std::array is_dev{std::is_same_v<Dev, Devs>...};
return std::ranges::find(is_dev, true) - is_dev.begin();
}
template <Party P> void stamp(Frame &out) const { template <Party P> void stamp(Frame &out) const {
if constexpr (P == Sender) if constexpr (P == Sender)
out.controller_addr = address_; out.controller_addr = address_;
@@ -268,5 +264,6 @@ private:
uint16_t controller_ = 0; uint16_t controller_ = 0;
const uint16_t address_; const uint16_t address_;
std::tuple<Devs...> devices_; std::tuple<Devs...> devices_;
QueueHandle_t emit_requests_;
}; };
} // namespace avclan } // namespace avclan
+47
View File
@@ -23,3 +23,50 @@ using std::unexpect;
using std::unexpect_t; using std::unexpect_t;
} // namespace avclan } // namespace avclan
#endif #endif
#if !defined(__cpp_lib_to_chars) || __cpp_lib_to_chars < 201611L
#include <cstdint>
#include <type_traits>
namespace avclan {
// No <charconv> (nor the <system_error> its result type needs) in the
// freestanding stdlib, so mirror the std API surface for the unsigned-integral
// overload — the only one used here. Values are those of std::errc, so call
// sites written against either spelling behave the same.
enum class errc : uint8_t { value_too_large = 75 };
struct to_chars_result {
char *ptr;
errc ec;
};
template <class T>
requires std::is_integral_v<T> && std::is_unsigned_v<T>
constexpr to_chars_result to_chars(char *first, char *last, T value,
int base = 10) {
char digits[sizeof(T) * 8]; // worst case: base 2
uint8_t ndigits = 0;
do {
const auto digit = static_cast<unsigned>(value % static_cast<T>(base));
digits[ndigits++] =
static_cast<char>(digit < 10 ? '0' + digit : 'a' + (digit - 10));
value = static_cast<T>(value / static_cast<T>(base));
} while (value != 0);
if ((last - first) < ndigits)
return {.ptr = last, .ec = errc::value_too_large};
while (ndigits > 0)
*first++ = digits[--ndigits];
return {.ptr = first, .ec = errc{}};
}
} // namespace avclan
#else
#include <charconv>
namespace avclan {
using errc = std::errc;
using std::to_chars;
using std::to_chars_result;
} // namespace avclan
#endif
@@ -12,6 +12,11 @@ target_sources(avclan PRIVATE
) )
target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(avclan PUBLIC
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
)
find_program(AVRDUDE avrdude) find_program(AVRDUDE avrdude)
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware") set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
@@ -76,9 +81,6 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json. # tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)") set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_FRAME_POOL_N 24 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
try_compile(LIBC_VERSION_TEST try_compile(LIBC_VERSION_TEST
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp" SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
COMPILE_DEFINITIONS -mmcu=${AVR_MCU} COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
@@ -137,6 +139,7 @@ target_compile_definitions(jnk0le_uart
PRIVATE F_CPU=${FREQSEL} PRIVATE F_CPU=${FREQSEL}
) )
target_compile_options(jnk0le_uart PUBLIC target_compile_options(jnk0le_uart PUBLIC
$<$<CONFIG:Debug>:-Og>
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3) -ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
target_link_libraries(avclan PUBLIC jnk0le_uart) target_link_libraries(avclan PUBLIC jnk0le_uart)
@@ -148,7 +151,6 @@ target_compile_definitions(avclan PUBLIC
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV} __CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
TCB_CLKSEL=${TCB_CLKSEL} TCB_CLKSEL=${TCB_CLKSEL}
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
) )
target_compile_options(avclan PUBLIC target_compile_options(avclan PUBLIC
--param=min-pagesize=0 --param=min-pagesize=0
+248 -46
View File
@@ -9,13 +9,17 @@
#include <stdint.h> #include <stdint.h>
#include <util/atomic.h> #include <util/atomic.h>
#include "avclan.h"
#include "hal/cd_timer.h" // statustimer_enable/disable (guard) #include "hal/cd_timer.h" // statustimer_enable/disable (guard)
#include "hal/phy.h" #include "hal/phy.h"
#include "media_avr.h" // media_sync_during_mask (guard) #include "media_avr.h" // media_sync_during_mask (guard)
// F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by // F_CPU + TICK_US (timing.h) defined here; F_CPU potentially needed by
// avr-libc. // avr-libc.
#include "timing_avr.h" #include "timing_avr.h" // IWYU pragma: keep
enum bit_t : uint8_t { bit_zero = 0x00, bit_one = 0x01, bit_start = 0x10 };
typedef enum bit_t Bit;
// USART0 TX ring indices owned by the jnk0le lib; the guard consults them to // USART0 TX ring indices owned by the jnk0le lib; the guard consults them to
// decide whether to resume the TX drain (DRE interrupt) on leave. // decide whether to resume the TX drain (DRE interrupt) on leave.
@@ -79,8 +83,12 @@ bool phy_is_muted() {
return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0); return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0);
} }
// No-op: acks are driven from inside the synchronous read path, which a deaf
// caller never enters.
void phy_deafen([[maybe_unused]] bool deaf) {}
// True when the bus is being driven (i.e. not idle/floating). // True when the bus is being driven (i.e. not idle/floating).
bool phy_active() { return !BUS_IS_IDLE; } bool phy_frame_pending() { return (!BUS_IS_IDLE) != 0; }
// Mute device TX on AVCLAN bus // Mute device TX on AVCLAN bus
void phy_mute(bool mute) { void phy_mute(bool mute) {
@@ -112,11 +120,9 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) {
AVCLAN_setBusDriven(); AVCLAN_setBusDriven();
} }
while (TCB1.CNT <= period) {}; while (TCB1.CNT <= period) {};
return;
} }
void phy_send_bit(Bit bit) { static void phy_send_bit(Bit bit) {
uint16_t zero_length, one_length; uint16_t zero_length, one_length;
switch (bit) { switch (bit) {
case bit_zero: case bit_zero:
@@ -137,7 +143,7 @@ void phy_send_bit(Bit bit) {
set_AVC_logic_for(1, one_length); set_AVC_logic_for(1, one_length);
} }
void phy_send_ack() { static void phy_send_ack() {
TCB1.CNT = 0; TCB1.CNT = 0;
// Wait for controller to begin ACK bit // Wait for controller to begin ACK bit
@@ -151,7 +157,7 @@ void phy_send_ack() {
phy_send_bit(bit_zero); phy_send_bit(bit_zero);
} }
Send phy_read_ack() { static Send phy_read_ack() {
TCB1.CNT = 0; // Double reset of TCB1.CNT: here TCB1.CNT = 0; // Double reset of TCB1.CNT: here
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within) set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
AVCLAN_setBusIdle(); // Stop driving bus AVCLAN_setBusIdle(); // Stop driving bus
@@ -166,13 +172,13 @@ Send phy_read_ack() {
// Check/wait in case we get here before peripheral finishes ACK bit // Check/wait in case we get here before peripheral finishes ACK bit
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX) if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
return NAK; return NAK_TOO_LONG;
} }
return (Send)0; return (Send)0;
} }
// Send `len` bits on the AVCLAN bus; returns the even parity // Send `len` bits on the AVCLAN bus; returns the even parity
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) { static Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
uint8_t b = *bits; uint8_t b = *bits;
uint8_t parity = 0; uint8_t parity = 0;
int8_t len_mod8 = 8; int8_t len_mod8 = 8;
@@ -191,17 +197,19 @@ Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
b <<= 1; b <<= 1;
} }
len_mod8 = 8; len_mod8 = 8;
b = *--bits; // Avoid under-read for last byte
if (len > 0)
b = *--bits;
} }
return (parity & 1); return (parity & 1);
} }
// Send `len` bits on the AVCLAN bus; returns the even parity // Send `len` bits on the AVCLAN bus; returns the even parity
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) { static Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
return phy_send_bits_u8((const uint8_t *)bits + 1, len); return phy_send_bits_u8((const uint8_t *)bits + 1, len);
} }
Bit phy_send_byte(const uint8_t *byte) { static Bit phy_send_byte(const uint8_t *byte) {
uint8_t b = *byte; uint8_t b = *byte;
uint8_t parity = 0; uint8_t parity = 0;
@@ -225,19 +233,22 @@ ISR(TCB0_INT_vect) {
// last_tcb1 = cur_tcb1; // last_tcb1 = cur_tcb1;
// #endif // #endif
READING_BYTE <<= 1;
// If the logical `0` pulse was less than the sync + data period threshold, // If the logical `0` pulse was less than the sync + data period threshold,
// bit was a 1 // bit was a 1
pulsewidth = TCB0.CCMP; uint16_t tmp = pulsewidth = TCB0.CCMP; // non-volatile tmp saves re-read in
if (pulsewidth < (uint16_t)AVCLAN_READBIT_THRESHOLD) { // read-bit-threshold comparison
READING_BYTE++; if (READING_NBITS) {
READING_PARITY++; READING_BYTE <<= 1;
if (tmp < (uint16_t)AVCLAN_READBIT_THRESHOLD) {
READING_BYTE++;
READING_PARITY++;
}
READING_NBITS--;
} }
READING_NBITS--;
} }
// Read `len` bits on the AVCLAN bus; returns the even parity // Read `len` bits on the AVCLAN bus; returns the even parity
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) { static Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
uint8_t parity; uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0; READING_BYTE = 0;
@@ -264,7 +275,7 @@ Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
} }
// Read `len` bits on the AVCLAN bus; returns the even parity // Read `len` bits on the AVCLAN bus; returns the even parity
Bit phy_read_bits_u16(uint16_t *bits, int8_t len) { static Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
uint8_t parity = 0; uint8_t parity = 0;
if (len > 8) { if (len > 8) {
uint8_t over = len - 8; uint8_t over = len - 8;
@@ -277,7 +288,7 @@ Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
} }
// Read a byte on the AVCLAN bus // Read a byte on the AVCLAN bus
Bit phy_read_byte(uint8_t *byte) { static Bit phy_read_byte(uint8_t *byte) {
uint8_t parity; uint8_t parity;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
READING_BYTE = 0; READING_BYTE = 0;
@@ -303,7 +314,16 @@ Bit phy_read_byte(uint8_t *byte) {
return (Bit)(parity & 1); return (Bit)(parity & 1);
} }
void phy_init() { // Our own peripheral address, and whether the frame being read is addressed to
// it. hal/phy.h puts the ACK decision in the phy, so the latch that used to
// live in Bus::read lives here now: set when the peripheral address comes in,
// and reused by every field after it.
static uint16_t own_address_;
static bool acking_;
void phy_init(uint16_t address) {
own_address_ = address;
// Set pin 6 and 7 as input // Set pin 6 and 7 as input
PORTA.DIRCLR = (PIN6_bm | PIN7_bm); PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
// Disable input buffer; recommended when using AC // Disable input buffer; recommended when using AC
@@ -338,23 +358,36 @@ void phy_init() {
phy_mute(false); // unmute AVCLAN bus TX phy_mute(false); // unmute AVCLAN bus TX
} }
// Wait for and validate an incoming start bit. On an over-long "driven" bus static Read phy_read_startbit() {
// (AC2 latched high because the bus is actually floating) this kicks PA7 hard // Following HAL header docs, this function is only called after a driven bus
// high to unlatch the comparator. The framing layer maps the result to its own // was ~recently detected.
// error reporting; no printing happens here. // Two main (designed) entry flows, depending on timing of pulse end w.r.t.
Read phy_read_startbit() { // beginning this function:
uint16_t startbitlen = TCB1.CNT = 0; // - Bus is still driven or ISR is pending
// - Wait for pulse to end if needed
// - On an over-long "driven" bus (AC2 latched high when bus is floating)
// kick PA7 hard high to try unlatching the comparator
// - Grab updated `pulsewidth`
// - Bus is idle
// - `pulsewidth` holds duration of most recent pulse.
// Reset the ~atomic `pulsewidth` variable to detect the post-pulse update TCB1.CNT = 0;
// from the TCB0_INT_vect ISR
// "stale"ness indicates that the current `pulsewidth` value is out of date
bool stale = true;
uint16_t startbitlen;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
if (!BUS_IS_IDLE) // Only reset if bus is actively driven (i.e. current // pulsewidth won't/can't be updated during atomic block, but pulse may end
pulsewidth = 0; // value is stale/already been used) // and store an IRQ. If bus is idle and no pending ISR, then pulsewidth
// currrently holds the duration of the most recent pulse (stale = false).
stale = (((!BUS_IS_IDLE) | (TCB0.INTFLAGS & TCB_CAPT_bm)) != 0);
// Read ~atomically, to prevent torn reads
startbitlen = pulsewidth;
} }
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
startbitlen = TCB1.CNT; if (TCB1.CNT > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
if (startbitlen > (uint16_t)AVCLAN_STARTBIT_LOGIC_0 * 1.2) {
Read result = STARTBIT_TOO_LONG; Read result = STARTBIT_TOO_LONG;
while (!BUS_IS_IDLE) { while (!BUS_IS_IDLE) {
// If bus is "driven" too long, assume the AC2 is latched (e.g. // If bus is "driven" too long, assume the AC2 is latched (e.g.
@@ -376,15 +409,23 @@ Read phy_read_startbit() {
} }
} }
// `pulsewidth` updates once the TCB0_INT_vect ISR runs for this pulse.
TCB1.CNT = 0; TCB1.CNT = 0;
do { while (stale) { // bus was driven, or a capture was still unconsumed, at entry
if (TCB1.CNT > (uint16_t)AVCLAN_BIT0_LOGIC_1) // Wait a max of ~6μs for ISR uint16_t old_pulsewidth = startbitlen;
return BAD_STARTBIT; // ISR/other implementation bug; abort // `pulsewidth` updates once the TCB0_INT_vect ISR runs for this pulse.
// Read ~atomically, to prevent torn reads // Read ~atomically, to prevent torn reads
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { startbitlen = pulsewidth; } ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { startbitlen = pulsewidth; }
} while (startbitlen == 0); if (startbitlen != old_pulsewidth)
break;
// Exit for potential false-negative: ISR writes for identical mid-frame
// bits (e.g. double 0 or 1 bits) can't be detected, but the ISR should
// definitely have run after ~6μs (i.e. startbitlen is actually current).
// The delay is ultimately free since mid-frame entry is directed to the
// "wait-for-message-completion" stall loop anyways
if (TCB1.CNT > (uint16_t)AVCLAN_BIT0_LOGIC_1)
break;
}
if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) { if (startbitlen < (uint16_t)(AVCLAN_STARTBIT_LOGIC_0 * 0.8)) {
// Not a start bit; wait for the message to finish (bus continuously idle // Not a start bit; wait for the message to finish (bus continuously idle
@@ -393,7 +434,7 @@ Read phy_read_startbit() {
TCB1.CNT = 0; TCB1.CNT = 0;
while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) { while (TCB1.CNT < (uint16_t)(AVCLAN_BIT_LENGTH_MAX * 1.2)) {
if (!BUS_IS_IDLE) if (!BUS_IS_IDLE)
TCB1.CNT = 0; // Reset counter after each bit pulse TCB1.CNT = 0; // (Re)start count from when bus was last driven
} }
// A pulse no wider than a normal bit means we merely tuned in mid-frame and // 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 // this was a data bit; a wider-but-still-sub-start pulse means some other
@@ -408,7 +449,7 @@ Read phy_read_startbit() {
// Acquire the bus and emit a start bit. Returns false if another device is // Acquire the bus and emit a start bit. Returns false if another device is
// already driving the bus (we can't yet do proper CSMA/CD). // already driving the bus (we can't yet do proper CSMA/CD).
Send phy_send_startbit() { static Send phy_send_startbit() {
// wait for free line // wait for free line
TCB1.CNT = 0; TCB1.CNT = 0;
while (BUS_IS_IDLE) { while (BUS_IS_IDLE) {
@@ -437,6 +478,144 @@ Send phy_send_startbit() {
return (Send)0; return (Send)0;
} }
// Field bits, then the wire's parity bit, checked against what we counted.
static Read read_parity(Bit calc, Read bad) {
uint8_t wire = 0;
phy_read_bits_u8(&wire, 1);
return ((uint8_t)calc != wire) ? bad : (Read)0;
}
// An acknowledge slot follows the field whether or not we drive it, so a frame
// that isn't ours still has to have the slot read away.
static void ack_slot(void) {
if (acking_) {
phy_send_ack();
} else {
uint8_t slot = 0;
phy_read_bits_u8(&slot, 1);
}
}
Read phy_read_header(bool *is_unicast) {
acking_ = false; // New frame; nothing is addressed to us yet
const Read err = phy_read_startbit();
if (err != (Read)0)
return err;
uint8_t bit = 0;
phy_read_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
*is_unicast = (bit != 0);
return (Read)0;
}
Read phy_read_controller_addr(uint16_t *addr) {
// No acknowledge slot: this field is the arbitration window.
return read_parity(phy_read_bits_u16(addr, 12), BAD_CONTROLLER_PARITY);
}
Read phy_read_peripheral_addr(uint16_t *addr) {
const Read err =
read_parity(phy_read_bits_u16(addr, 12), BAD_PERIPHERAL_PARITY);
if (err != (Read)0)
return err;
// The one field that decides the ack; every field after it reuses the answer.
acking_ = !phy_is_muted() && (*addr == own_address_);
ack_slot();
return (Read)0;
}
Read phy_read_control(uint8_t *control) {
const Read err =
read_parity(phy_read_bits_u8(control, 4), BAD_CONTROL_PARITY);
if (err != (Read)0)
return err;
ack_slot();
return (Read)0;
}
Read phy_read_length(uint8_t *length) {
const Read err = read_parity(phy_read_byte(length), BAD_LENGTH_PARITY);
if (err != (Read)0)
return err;
ack_slot();
return (Read)0;
}
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
for (uint8_t i = 0; i < length; i++) {
const Read err = read_parity(phy_read_byte(&data[i]), BAD_DATA_PARITY);
if (err != (Read)0) {
*data_index = i;
return err;
}
ack_slot();
}
return (Read)0;
}
// `expect_ack` is "a NAK here is worth reporting", not "emit the slot": the
// slot goes out either way, and a broadcast frame simply has nobody to fill it.
static Send send_ack_slot(bool expect_ack, Send nak) {
if (expect_ack) {
const Send err = phy_read_ack();
return err == NAK ? nak : err;
}
const uint8_t fill = 1U;
phy_send_bits_u8(&fill, 1);
return (Send)0;
}
Send phy_send_header(bool is_unicast) {
const Send err = phy_send_startbit();
if (err != (Send)0)
return err;
const uint8_t bit = (is_unicast) ? 1U : 0U;
phy_send_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
return (Send)0;
}
Send phy_send_controller_addr(uint16_t addr) {
// No acknowledge slot: this field is the arbitration window.
phy_send_bit(phy_send_bits_u16(&addr, 12));
return (Send)0;
}
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
phy_send_bit(phy_send_bits_u16(&addr, 12));
return send_ack_slot(expect_ack, NAK_ADDRESS);
}
Send phy_send_control(uint8_t control, bool expect_ack) {
phy_send_bit(phy_send_bits_u8(&control, 4));
return send_ack_slot(expect_ack, NAK_CONTROL);
}
Send phy_send_length(uint8_t length, bool expect_ack) {
phy_send_bit(phy_send_byte(&length));
return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH);
}
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index) {
for (uint8_t i = 0; i < length; i++) {
phy_send_bit(phy_send_byte(&data[i]));
const Send err = send_ack_slot(expect_ack, NAK_DATA);
if (err != (Send)0) {
*data_index = i;
return err;
}
}
return (Send)0;
}
// Every field above is on the wire, and has reported its own outcome, by the
// time it returns: nothing is left to wait out or attribute.
Send phy_send_done([[maybe_unused]] uint8_t *data_index) { return (Send)0; }
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer) /* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
enabled. */ enabled. */
@@ -456,10 +635,31 @@ void phy_guard_leave() {
} }
} }
#if !defined(NDEBUG) && defined(MEASURE_BUS) #ifndef NDEBUG
// Only used immediately below
#define XSTR(x) #x // Hold the bus at a given level.
#define STR(x) XSTR(x) // Overrides and restores mute state upon release (i.e. set recessive).
static void phy_set_state(bool dominant) {
static bool saved_mute = false;
if (dominant) {
saved_mute = phy_is_muted();
AVCLAN_setBusDriven();
phy_mute(false);
} else {
AVCLAN_setBusIdle();
phy_mute(saved_mute);
}
}
void phy_set_dominant(void) { phy_set_state(true); }
void phy_set_recessive(void) { phy_set_state(false); }
#ifdef MEASURE_BUS
#include <stdio.h> // phy_measure() reporting (debug builds only)
// Only used immediately below
#define XSTR(x) #x
#define STR(x) XSTR(x)
static uint16_t pulses[100]; static uint16_t pulses[100];
static uint16_t periods[100]; static uint16_t periods[100];
@@ -492,4 +692,6 @@ void phy_measure() {
phy_guard_leave(); phy_guard_leave();
} }
#endif
#endif #endif
+82 -5
View File
@@ -4,11 +4,27 @@
#include <avr/io.h> #include <avr/io.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <util/atomic.h>
#include "hal/stdio.h" #include "hal/stdio.h"
#include "timing_avr.h" // IWYU pragma: export #include "timing_avr.h" // IWYU pragma: export
#include "usart.h" // jnk0le AVR-UART-lib #include "usart.h" // jnk0le AVR-UART-lib
// TX ring owned by the jnk0le lib. `tx0_Head` is the index of the last byte
// written, so the next write goes to head+1. The DRE ISR reads tail+1 and then
// advances `tx0_Tail`. The ring is empty when the two indexes are equal. The
// ring holds at most TX0_BUFFER_SIZE-1 bytes.
extern char tx0_buffer[TX0_BUFFER_SIZE];
// stdio_write_nonblock() is all-or-nothing. If the ring cannot hold the largest
// buffer that the app writes, the function drops every buffer and queues none.
// The largest buffer is a text frame log line (Frame::print). That line is
// ~186 bytes at MAXLENGTH=32. The ring therefore needs headroom above this
// size.
static_assert(TX0_BUFFER_SIZE - 1 >= 192,
"TX ring too small to hold a whole frame log line");
// Raw stdout backend: emit the byte verbatim, no '\n' -> "\r\n" translation // Raw stdout backend: emit the byte verbatim, no '\n' -> "\r\n" translation
// (unlike the lib's uart_putchar). Keeps binary frame payloads intact. // (unlike the lib's uart_putchar). Keeps binary frame payloads intact.
static int stdio_putchar(char data, FILE *stream) { static int stdio_putchar(char data, FILE *stream) {
@@ -17,6 +33,67 @@ static int stdio_putchar(char data, FILE *stream) {
return 0; return 0;
} }
bool stdio_write_nonblock(const void *buf, uint8_t len) {
if (len == 0)
return true;
// A single unguarded read of each index is enough:
// - Only main writes tx0_Head.
// - Only the DRE ISR writes tx0_Tail.
// - 8-bit accesses are atomic on AVR.
// If the ISR advances the tail during this function, the ring has more free
// space than this function measured. That direction is safe.
const uint8_t head = tx0_Head;
const uint8_t space = (uint8_t)((tx0_Tail - head - 1) & TX0_BUFFER_MASK);
if (len > space) {
// The ring cannot take the whole buffer, so drop it and queue the indicator
// instead. A buffer may be refused because it is larger than
// available space or because the ring is full. For the former case, the
// full indicator can be appended without truncating previously queued
// bytes.
// Goal is for the bang-indicator to be on its own line
uint8_t tmp = head;
if (space >= 3) {
// Room for 3 normally implies the current head ends with a newline
// (avoids an unneeded double newline in most cases)
tmp = (tmp + 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '!';
tmp = (tmp + 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '\n';
tx0_Head = tmp;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { USART0.CTRLA |= USART_DREIE_bm; }
} else {
// No room for even the indicator, so it goes over the last three queued
// bytes. The ISR already drains the ring, so DREIE needs no change.
// Write in reverse order to more efficiently decrement
tx0_buffer[tmp] = '\n';
tmp = (tmp - 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '!';
tmp = (tmp - 1) & TX0_BUFFER_MASK;
tx0_buffer[tmp] = '\n';
}
return false;
}
const uint8_t start = (uint8_t)((head + 1) & TX0_BUFFER_MASK);
const uint8_t contiguous = TX0_BUFFER_SIZE - start;
if (len <= contiguous) {
memcpy(&tx0_buffer[start], buf, len);
} else { // wraps the end of the ring
memcpy(&tx0_buffer[start], buf, contiguous);
memcpy(&tx0_buffer[0], (const char *)buf + contiguous, len - contiguous);
}
// Publish the bytes only after all of them are in place. This is a single
// store, so the ISR never sees a partly filled buffer.
tx0_Head = (uint8_t)((head + len) & TX0_BUFFER_MASK);
// The ISR also writes CTRLA (to clear DREIE when it drains the last byte).
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { USART0.CTRLA |= USART_DREIE_bm; }
return true;
}
// Non-blocking stdin backend: next received byte, or _FDEV_EOF when the RX ring // Non-blocking stdin backend: next received byte, or _FDEV_EOF when the RX ring
// is empty (uart0_getData() returns a negative value when there is no data). // is empty (uart0_getData() returns a negative value when there is no data).
static int stdio_getchar(FILE *stream) { static int stdio_getchar(FILE *stream) {
@@ -34,14 +111,14 @@ void stdio_init(void) {
// The lib's uart0_init configures the USART registers/baud but not the pins; // The lib's uart0_init configures the USART registers/baud but not the pins;
// keep the ATtiny3216 pin-mux the old driver did. // keep the ATtiny3216 pin-mux the old driver did.
PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2 PORTMUX.CTRLB = PORTMUX_USART0_ALTERNATE_gc; // TxD/RxD on PA1/PA2
PORTA.DIRSET = PIN1_bm; // TxD output PORTA.DIRSET = PIN1_bm; // TxD output
PORTA.DIRCLR = PIN2_bm; // RxD input PORTA.DIRCLR = PIN2_bm; // RxD input
#ifdef USE_DOUBLE_SPEED #ifdef USE_DOUBLE_SPEED
uart0_init(DOUBLE_BAUD_CALC(1200000)); uart0_init(DOUBLE_BAUD_CALC(1200000));
#else #else
uart0_init(BAUD_CALC(1200000)); uart0_init(BAUD_CALC(1200000));
#endif #endif
stdout = stdin = &stdio_stream; // printf/fputs/fwrite + non-blocking getchar stdout = stdin = &stdio_stream; // printf/fputs/fwrite + non-blocking getchar
} }
@@ -63,4 +63,4 @@
// TCB_TICK is nanoseconds/tick; the generic timing.h wants microseconds/tick. // TCB_TICK is nanoseconds/tick; the generic timing.h wants microseconds/tick.
#define TICK_US (TCB_TICK / 1000.0) #define TICK_US (TCB_TICK / 1000.0)
#include "timing.h" #include "timing.h" // IWYU pragma: export
@@ -108,11 +108,11 @@
// Size of the ring buffers, must be power of 2 // Size of the ring buffers, must be power of 2
// default 32 // default 32
#define RX_BUFFER_SIZE 64 #define RX_BUFFER_SIZE 128
// Size of the ring buffers, must be power of 2 // Size of the ring buffers, must be power of 2
// default 32 // default 32
#define TX_BUFFER_SIZE 128 #define TX_BUFFER_SIZE 256
/*******************config for multiple USART * mcu's*************************/ /*******************config for multiple USART * mcu's*************************/
+79
View File
@@ -0,0 +1,79 @@
pico_sdk_init()
include_directories(
${CMAKE_CURRENT_LIST_DIR}
include
)
FetchContent_Declare( freertos_kernel
EXCLUDE_FROM_ALL
GIT_REPOSITORY https://github.com/FreeRTOS/FreeRTOS-Kernel.git
GIT_TAG 3a22924e0a9ddbbc8b0758881c33b3422a5cc20d # v11.3.1
)
add_library(freertos_config INTERFACE)
target_include_directories(freertos_config SYSTEM
INTERFACE
include
)
target_compile_definitions(freertos_config
INTERFACE
projCOVERAGE_TEST=0
)
target_compile_options(freertos_config
INTERFACE
$<$<COMPILE_LANGUAGE:CXX>:-Wno-volatile>
)
set( FREERTOS_PORT "GCC_RP2040" CACHE STRING "" FORCE)
FetchContent_MakeAvailable(freertos_kernel)
set(FREERTOS_KERNEL_PATH "${freertos_kernel_SOURCE_DIR}" CACHE STRING "" FORCE)
include(${freertos_kernel_SOURCE_DIR}/portable/ThirdParty/GCC/RP2040/FreeRTOS_Kernel_import.cmake)
target_sources(avclan PRIVATE
phy.cc
media.cc
board.cc
stdio.cc
)
pico_generate_pio_header(avclan ${CMAKE_CURRENT_LIST_DIR}/iebus.pio)
target_link_libraries(avclan PUBLIC
hardware_pio
hardware_dma
pico_stdlib
pico_stdio_usb
pico_multicore
pico_async_context_freertos
freertos_config
FreeRTOS-Kernel-Heap4
)
pico_enable_stdio_usb(avclan 1)
# stdio_write_nonblock() is all-or-nothing and the largest buffer is a ~186 byte
# frame log line; the SDK default of 64 would drop every one. PUBLIC so the
# TinyUSB sources compiled into `mockingboard` agree on the FIFO size.
target_compile_definitions(avclan PUBLIC CFG_TUD_CDC_TX_BUFSIZE=256)
# Needs separate/new executable target because `pico_add_extra_outputs` only
# works on locally (to this file/directory) defined targets
add_executable(mockingboard
${avclan-mockingboard_SOURCE_DIR}/src/sniffer.cc
)
target_link_libraries(mockingboard PUBLIC
avclan
)
# Silence warnings in TinyUSB, and only there.
file(GLOB_RECURSE _tinyusb_sources ${PICO_TINYUSB_PATH}/src/*.c)
set_source_files_properties(${_tinyusb_sources}
TARGET_DIRECTORY avclan mockingboard
PROPERTIES COMPILE_OPTIONS "-w")
unset(_tinyusb_sources)
pico_add_extra_outputs(mockingboard)
pico_set_program_description(mockingboard
"Emulate an external CD Changer on an AVCLAN bus to add Bluetooth and aux-input audio to older Toyota's"
)
pico_set_program_version(mockingboard
"${avclan-mockingboard_VERSION}")
pico_set_program_url(mockingboard "https://github.com/halleysfifthinc/AVCLAN-Mockingboard")
+5
View File
@@ -0,0 +1,5 @@
#include "hal/board.h"
extern "C" void board_init() {}
extern "C" void board_enable_interrupts() {}
+268
View File
@@ -0,0 +1,268 @@
.define public ack_latch 4 ; (PIO0, internal) frame is addressed to us
.define public ack_irq 5 ; (PIO1) rx -> iebus_ack: hold this slot low
; This is used to set the clkdiv.
; The delay cycle counts in read_bit (AVCLAN_READBIT_THRESHOLD) and iebus_tx's
; ack_drive (AVCLAN_BIT0_LOGIC_0) must be updated in sync with this variable
.define public CYCLES_PER_READBIT_PERIOD 64
.pio_version 1
.program iebus_rx
; TX encoding: one read per word
; | 31 | 30:16 | 15:0 |
; | has ACK | zeros | Instr |
;
; Instr must be `out x, (14 - N)` for a read of N bits + parity, or `jmp rx_startbit`.
; The `out` zeroes x (the parity accumulator) and leaves exactly N + 1 bits before
; the OSR reaches its threshold, which ends the read loop. "has ACK" marks a field
; followed by an acknowledge slot.
;
; RX encoding: one read per word (RX FIFO can hold max 4 reads)
; | 31:16 | 15:0 |
; | zeros | slot |
;
; where each slot is encoded as
; | MSB .................... LSB |
; | 15-N zeros | N bits | parity |
;
; Parity is validated by this SM, but is still reported in the LSB. `y` is reserved
; for the address match below.
.in 1 left
; OSR threshold set to 31 so that the OSRE is reached when all the data+parity bits
; have been read
.out 0 right 31
.mov_status irq set ack_latch
;;; Start-bit read (auto-resets until a proper start bit is observed)
; A start bit is dominant for AVCLAN_STARTBIT_LOGIC_0 (~169us) and recessive for
; AVCLAN_STARTBIT_LOGIC_0 (~20us) more.
; The bus state is polled as fast as target bounds (80% and 120%) can be evenly
; divided (must be faster than shortest recessive period AVCLAN_BIT0_LOGIC_1 to
; detect/distinguish normal bits mid-frame).
public rx_startbit:
wait 1 pin 0
leading_edge:
wait 0 pin 0
set x 29
dominant:
jmp pin leading_edge [10] ; Pin HIGH == released before 0.8x: not a start bit
; Polled every 11 cycles, starting 2 cycles after leading edge
; Last poll at 332 cycles (~80% * AVCLAN_STARTBIT_LOGIC_0)
jmp x-- dominant ; Cycle 333 on fall-through
set x 18 ; 334
recessive:
jmp pin valid_startbit ; Pin HIGH == released within bounds
jmp x-- recessive [8] ; + 19 * 9 = 506 total (~120% * AVCLAN_STARTBIT_LOGIC_0)
; Delay on decrement jmp to keep the release jmp quick
jmp rx_startbit ; Still dominant: too long to be proper start bit
valid_startbit:
irq clear ack_latch ; Once per frame, before any address can set it
.wrap_target
wait_read:
pull
out exec, 16 ; Set up the read (zero x and set up output shift
; count as bit count), or jmp to rx_startbit
read_bit:
;;; Resynchronize at each leading (recessive => dominant) edge
wait 1 pin 0
; Worst-case (slowest) path to return here is for non-ACK flow.
wait 0 pin 0 [31]
out null, 1 [31] ; Count this bit
; Read bus state ~AVCLAN_READBIT_THRESHOLD after bus goes dominant
in pins, 1
jmp pin counted ; Fall through for recessive (i.e. a one bit)
mov x, ~x ; Accumulate parity over the zero bits
counted:
jmp !osre read_bit
;;; Check parity
; Every parity-checked field is an odd number of bits, so even parity (an even
; number of ones) is an odd number of zeros: x == ~0. The broadcast bit has no
; parity; its verdict can only clear a latch that was never set.
jmp x-- parity_ok
irq clear ack_latch ; NAK this field and the rest of the frame
;;; Check if we're the frame recipient
; Our peripheral address can only appear in individual 12-bit reads as the
; controller or peripheral address. We should only ack if we're the frame recipient.
; y is preset in phy_init to our peripheral address + parity, and temporarily
; inverted when we are sending (so we don't self-ACK)
parity_ok:
mov x, isr ; Save read value before clobbering the ISR with push
jmp x!=y push_slot ; Fall-through to set ACK latch only if the read matches
; our address + parity
irq set ack_latch ; Set before the push, so the driver's veto can't be overwritten
push_slot:
push
;;; Consume the ack slot if has ACK, driving it if ACK latch is set
out x, 1 ; Load "has ACK"
jmp !x wait_read
wait 1 pin 0
wait 0 pin 0 [30] ; Sender begins the ACK bit, and will hold it dominant
; for ~48 cycles (~AVCLAN_BIT1_LOGIC_0) before releasing
; for peripheral to take over
; Delay reloading ack_latch to give the driver as much
; time as possible to veto an ACK (i.e. clear it for
; e.g. a bad length)
mov x, status ; Load ack_latch state into x
jmp !x wait_read
irq next set ack_irq ; The ack SM (iebus_tx's ack_entry, on the other (TX)
; PIO) is released 32 cycles after sender began bit,
; and its jmp to ack_drive takes the slot over at 34
; (~70% of AVCLAN_BIT1_LOGIC_0), holding it dominant.
.wrap
; Can execute one streamed instruction every 3 cycles
; An even number of instructions must be executed to maintain the correct shift-count/autopull
; when returning to normal reads after exec'ing. Pad instructions with `nop` as needed.
; public do_exec:
; out exec, 16 ; Cycle 1: Load the streamed instruction
; ; Cycle 2: Execute the streamed instruction
; jmp x-- do_exec ; Cycle 3: Restart loop (if x != 0)
; jmp wait_read
.program iebus_tx
.fifo tx
; TX Encoding (8 deep TX FIFO):
; | MSB ............................................. LSB |
; | 4 bits | N bits | 5 bits | 1 bit | 22 - N bits |
; | Length (N) | Data+P | Next | NAK ok | padding |
;
; Parity is generated by the driver and appended as the last (least significant)
; bit of the data field. "Next" should be the address of `reset` or `ack_drive`
.in 1 left
.out 1 left
.define public nak_irq 7
.define public lost_arb_irq 6
.define public eof_irq 3 ; An acknowledge slot is over
;;; Emit or synchronize to a start bit
; Reached by a CPU initiated force exec jmp to tx_startbit, with x = JOIN_SAMPLES
; and y = IDLE_SAMPLES. We originate once the bus has stayed recessive for the
; idle window. A dominant pulse is joined only if it outlasts every bit's
; dominant period (i.e. it's a start bit); otherwise the bus is busy, reported as
; a lost bid before any word is pulled.
.define public IDLE_SAMPLES 11 ; 12 * 16 = 192 cycles (~2 * AVCLAN_BIT_LENGTH_MAX)
; TODO: Explore tightening the idle window to 3 * AVCLAN_STARTBIT_LOGIC_1 (the
; longest recessive period within a frame)
.define public JOIN_SAMPLES 8 ; 9 * 12 = 108 cycles (> 1.2 * AVCLAN_BIT0_LOGIC_0)
idle_ok:
jmp y-- tx_startbit [14] ; Sampled every 16 cycles
set x 31 ; Idle: originate
set pins, 0 [30]
public tx_startbit:
jmp pin idle_ok
hold:
jmp pin lost ; Released too soon for a start bit (never while we drive)
jmp x-- hold [10] ; Sampled every 12 cycles, within a zero bit's recessive
; period (~AVCLAN_BIT0_LOGIC_1). Ours is 31 + 1 + 32 * 12
; = 416 total (~AVCLAN_STARTBIT_LOGIC_0)
set pins, 1 ; Release
wait 1 pin 0 [31] ; The start bit's trailing edge, whoever drove it
jmp reset [16] ; 51 cycles from that edge to reset's first
; `set pins, 0` (~AVCLAN_STARTBIT_LOGIC_1)
handle_nak:
jmp x-- ack_done ; Slot is over either way if a NAK was expected
irq wait nak_irq
;;; Close the acknowledge slot
; The peripheral owns this slot's trailing edge, so the recessive period that
; ends the slot is timed from its release rather than from our leading edge --
; the one bit in a frame we don't drive to completion ourselves. A slot nobody
; answered is already recessive and falls straight through.
ack_done:
wait 1 pin 0 [11] ; 15 cycles from the release to `set pins, 0` below
; (~AVCLAN_BIT0_LOGIC_1)
irq set eof_irq
reset:
.wrap_target
pull
out x, 4
send_bit:
set pins, 0 [31] ; OUT pin remains low/dominant for 32+16 cycles (~AVCLAN_BIT1_LOGIC_0)
out y, 1 [15] ; Save output bit to y
mov pins, y [15] ; Output the bit
; Read bus state ~AVCLAN_READBIT_THRESHOLD after bus goes dominant
jmp pin bit_end [13] ; JMP_PIN must be the IEBUS_RX pin (TX activity will mirror
; back to the RX pin). The delay burns the common duration
; between the one and zero bits.
bit_zero:
; This branch catches a difference between what we intended to send and what is actually
; present on the bus (a difference is only expected/allowed during send arbitration,
; where lower addresses have higher priority: since one bits are recessive, another
; device sending a zero will "override" the value of that bit)
jmp !y bit_end [2] ; Delay 3 more cycles before releasing bus at ~AVCLAN_BIT0_LOGIC_0
; Fall through means we lost arbitration (we read a zero, when we expected a one)
lost:
irq wait lost_arb_irq
.wrap
bit_end:
set pins, 1 [9] ; Delay must be common to all following control flow paths
jmp x-- slow_jmp ; Fall through once x (the bit count) is exhausted
out pc, 5 [1] ; "Next": reset, or ack_drive to read ACK
;;; read ack
public ack_drive:
set pins, 0 [31] ; Start ACK bit with a AVCLAN_BIT1_LOGIC_0 length pulse
out x, 1 [15] ; Preload NAK ok
public ack_release:
set pins, 1 [15] ; Release bus
jmp pin handle_nak [13] ; Sampled at 64 cycles (~AVCLAN_READBIT_THRESHOLD);
; both flow paths reach ack_done one instr later
jmp ack_done
slow_jmp:
jmp send_bit [3] ; 15 cycles from bit_end to `set pins, 0`, the same
; as the last bit's path through `out pc` and reset
;;; Send an ack for iebus_rx
; Run by its own SM, wrapping from ack_release back to ack_entry: it borrows
; ack_drive's 48-cycle pulse to hold the slot dominant for *less than* a complete
; AVCLAN_BIT0_LOGIC_0, releasing at ~AVCLAN_BIT0_LOGIC_0 after the slot's
; leading edge.
public ack_entry:
wait 1 irq ack_irq ; Released by iebus_rx after bus goes dominant
jmp ack_drive
; `mov pins, pins` reads the IN mapping (bit 0 == in base) and writes the OUT
; mapping (out base, 1 pin), so one program instance serves any src->dst pair;
; give each pair its own SM with its own pin mapping.
.program pin_mirror
mov pins, ~pins
; === IDIOMS ===
;;; Isolate first bit (for left shifting)
; mov osr, ::isr ; Reverse ISR so its LSB becomes the MSB in osr
; out x, 1 ; Set LSB of x from MSB of osr
;;; Incrementing (counting up)
; set x 0
; set x ~x ; x == 0xffffffff
; jmp x-- label ; "Increment"
; set x ~x ; Re-inversing gives the actual count
;;; Multiplying delays/counts (e.g. 2*20)
; set y 1
; loop1:
; set x 19
; loop2:
; jmp x-- loop2
@@ -0,0 +1,161 @@
/*
* FreeRTOS V202111.00
* Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* http://www.FreeRTOS.org
* http://aws.amazon.com/freertos
*
* 1 tab == 4 spaces!
*/
#ifndef FREERTOS_CONFIG_EXAMPLES_COMMON_H
#define FREERTOS_CONFIG_EXAMPLES_COMMON_H
/*-----------------------------------------------------------
* Application specific definitions.
*
* These definitions should be adjusted for your particular hardware and
* application requirements.
*
* THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
* FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
*
* See http://www.freertos.org/a00110.html
*----------------------------------------------------------*/
/* Scheduler Related */
#define configUSE_PREEMPTION 1
#define configUSE_TICKLESS_IDLE 0
#define configCPU_CLOCK_HZ (150000000UL)
#define configTICK_RATE_HZ ((TickType_t)1000)
#define configMAX_PRIORITIES 32
#define configMINIMAL_STACK_SIZE (configSTACK_DEPTH_TYPE)512
#define configIDLE_SHOULD_YIELD 1
#define configUSE_16_BIT_TICKS 0
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
/* Synchronization Related */
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_APPLICATION_TASK_TAG 0
#define configUSE_COUNTING_SEMAPHORES 1
#define configQUEUE_REGISTRY_SIZE 8
#define configUSE_QUEUE_SETS 1
#define configUSE_TIME_SLICING 1
#define configUSE_NEWLIB_REENTRANT 0
#define configENABLE_BACKWARD_COMPATIBILITY 0
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
/* System */
#define configSTACK_DEPTH_TYPE uint32_t
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
/* Memory allocation related definitions. */
#ifndef configSUPPORT_STATIC_ALLOCATION
#define configSUPPORT_STATIC_ALLOCATION 0
#endif
#ifndef configSUPPORT_DYNAMIC_ALLOCATION
#define configSUPPORT_DYNAMIC_ALLOCATION 1
#endif
#define configTOTAL_HEAP_SIZE (128 * 1024)
#define configAPPLICATION_ALLOCATED_HEAP 0
/* Hook function related definitions. */
#define configCHECK_FOR_STACK_OVERFLOW 0
#define configUSE_MALLOC_FAILED_HOOK 0
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
/* Run time and task stats gathering related definitions. */
#define configGENERATE_RUN_TIME_STATS 0
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 0
/* Co-routine related definitions. */
#define configUSE_CO_ROUTINES 0
#define configMAX_CO_ROUTINE_PRIORITIES 1
/* Software timer related definitions. */
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 1024
/* Interrupt nesting behaviour configuration. */
/*
#define configKERNEL_INTERRUPT_PRIORITY [dependent of processor]
#define configMAX_SYSCALL_INTERRUPT_PRIORITY [dependent on processor and
application] #define configMAX_API_CALL_INTERRUPT_PRIORITY [dependent on
processor and application]
*/
#if FREE_RTOS_KERNEL_SMP // set by the RP2xxx SMP port of FreeRTOS
/* SMP port only */
#ifndef configNUMBER_OF_CORES
#define configNUMBER_OF_CORES 2
#endif
#define configNUM_CORES configNUMBER_OF_CORES
#define configTICK_CORE 0
#define configRUN_MULTIPLE_PRIORITIES 1
#if configNUMBER_OF_CORES > 1
#define configUSE_CORE_AFFINITY 1
#endif
#define configUSE_PASSIVE_IDLE_HOOK 0
#endif
/* RP2040 specific */
#define configSUPPORT_PICO_SYNC_INTEROP 1
#define configSUPPORT_PICO_TIME_INTEROP 1
#include <assert.h>
/* Define to trap errors during development. */
#define configASSERT(x) assert(x)
/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_uxTaskPriorityGet 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_vTaskDelay 1
#define INCLUDE_xTaskDelayUntil 1
#define INCLUDE_xTaskGetSchedulerState 1
#define INCLUDE_xTaskGetCurrentTaskHandle 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#define INCLUDE_xTaskGetIdleTaskHandle 1
#define INCLUDE_eTaskGetState 1
#define INCLUDE_xTimerPendFunctionCall 1
#define INCLUDE_xTaskAbortDelay 1
#define INCLUDE_xTaskGetHandle 1
#define INCLUDE_xTaskResumeFromISR 1
#define INCLUDE_xQueueGetMutexHolder 1
#if PICO_RP2350
#define configENABLE_MPU 0
#define configENABLE_TRUSTZONE 0
#define configRUN_FREERTOS_SECURE_ONLY 1
#define configENABLE_FPU 1
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 16
#endif
/* A header file that defines trace macro can be included here. */
#endif /* FREERTOS_CONFIG_H */
+10
View File
@@ -0,0 +1,10 @@
#include "hal/media.h"
extern "C" void media_init(void) {}
extern "C" void media_action(MediaAction fn) {}
#ifndef NDEBUG
extern "C" bool media_mic_toggle(void) { return false; }
extern "C" bool media_busy(void) { return false; }
#endif
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#include "FreeRTOS.h" // IWYU pragma: export
#include "semphr.h"
#include "hal/stdio.h"
#include "pico/stdio.h"
#include "pico/stdio_usb.h"
#include "pico/time.h"
#include "tusb.h" // IWYU pragma: export
// stdio_write_nonblock() is all-or-nothing. The CDC TX FIFO must hold the
// largest buffer the app writes; that is a text frame log line (Frame::print),
// ~186 bytes at MAXLENGTH=32.
static_assert(CFG_TUD_CDC_TX_BUFSIZE >= 256,
"CDC TX FIFO too small to hold a whole frame log line");
namespace {
// Set when a dropped buffer had no room for its indicator; the next call emits
// it. The AVR port instead overwrites the last three queued bytes, which
// collapses a burst of drops to a single '!' line in exactly the same way.
bool drop_indicator_pending = false;
// stdout's only backend here is the usb driver, which writes into the same CDC
// TX FIFO measured below, so printf and this share one stream and one order.
void queue(const char *str, int len) {
stdio_put_string(str, len, false, false);
}
SemaphoreHandle_t rx_ready = nullptr;
// Called from the stdio_usb background IRQ after each tud_task() with RX data
// pending.
void on_rx([[maybe_unused]] void *param) {
BaseType_t woken = false;
xSemaphoreGiveFromISR(rx_ready, &woken);
portYIELD_FROM_ISR(woken);
}
} // namespace
extern "C" void stdio_init() {
rx_ready = xSemaphoreCreateBinary();
stdio_usb_init();
stdio_set_chars_available_callback(on_rx, nullptr);
stdio_set_translate_crlf(&stdio_usb, false);
// pico_stdio wraps printf/puts/putchar straight onto the CDC, but not
// fputs/fwrite. Unbuffered stdout keeps the newlib path in step with them
// instead of stranding whole strings in the FILE buffer.
setvbuf(stdout, nullptr, _IONBF, 0);
}
// Overrides the SDK's weak newlib hook, which busy-waits.
extern "C" int _read(int handle, char *buffer, int length) {
if (handle != STDIN_FILENO) {
errno = EBADF;
return -1;
}
while (true) {
const int count = stdio_get_until(buffer, length, make_timeout_time_us(0));
if (count > 0)
return count;
xSemaphoreTake(rx_ready, portMAX_DELAY);
}
}
extern "C" bool stdio_write_nonblock(const void *buf, uint8_t len) {
uint32_t avail = tud_cdc_write_available();
if (drop_indicator_pending) {
if (avail < 3)
return false;
queue("!\n", 2);
drop_indicator_pending = false;
avail -= 2;
}
if (len == 0)
return true;
if (len <= avail) {
queue(static_cast<const char *>(buf), len);
return true;
}
// Every buffer ends with '\n', so the FIFO already sits at a line boundary.
if (avail >= 3)
queue("!\n", 2);
else
drop_indicator_pending = true;
return false;
}
+22 -107
View File
@@ -3,124 +3,39 @@
#pragma once #pragma once
#include <array>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory> #include <memory>
#include <type_traits>
namespace detail { #include "FreeRTOS.h" // IWYU pragma: export
template <class T, auto N> struct Deleter; #include "queue.h"
}
template <class T, std::integral auto N, bool Owning = false,
class Deleter = std::default_delete<T>>
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max() &&
// push() drops the passed-in deleter and pop() fabricates a fresh
// one, so a deleter must either derive its state from the queue
// itself (the pool Deleter) or carry no state at all
(std::is_same_v<Deleter, detail::Deleter<T, N>> ||
(std::is_empty_v<Deleter> &&
std::is_nothrow_default_constructible_v<Deleter>)))
class Queue {
friend detail::Deleter<T, N>;
// Owning FIFO of `T`s, safe to share between tasks
template <class T> class Queue {
public: public:
// Only empty construction is allowed for non-Owning, non-pool deleters explicit Queue(UBaseType_t length)
constexpr Queue() : handle{xQueueCreate(length, sizeof(T *))} {
requires(!Owning && !std::is_same_v<Deleter, detail::Deleter<T, N>>) configASSERT(handle);
: owner{nullptr} {} }
Queue(const Queue &) = delete; Queue(const Queue &) = delete;
// Moving is unsupported due to being self-referential // Blocks until accepted (INCLUDE_vTaskSuspend: portMAX_DELAY never times out)
Queue(Queue &&) = delete; void push(std::unique_ptr<T> x) {
Queue &operator=(Queue &&) = delete; T *ptr = x.release();
xQueueSend(handle, &ptr, portMAX_DELAY);
// Construct from pre-defined storage
constexpr Queue(T (&items)[N])
requires(Owning) && std::same_as<Deleter, detail::Deleter<T, N>>
: owner{this}, write{N} {
for (uint8_t i = 0; i < N; ++i)
buf[i] = &items[i];
}
// Construct non-Owning empty from Owning queue
constexpr Queue(Queue<T, N, true, Deleter> &queue)
requires(!Owning)
: owner{&queue} {}
bool isEmpty() const { return write == read; }
uint8_t size() const { return write - read; }
uint8_t capacity() const { return N; }
bool isFull() const { return size() == capacity(); }
uint8_t push(std::unique_ptr<T, Deleter> x)
requires(!Owning)
{
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) {
// structurally unnecessary for detail::Deleter, where empty construction
// is only from same size parent
if (isFull())
return 1;
}
if (!x)
return 1;
if constexpr (std::is_same_v<Deleter, detail::Deleter<T, N>>) {
if (!x.get_deleter().is_owned_by(owner))
return 1;
}
claim(x.release());
return 0;
} }
const T *peek() const { std::unique_ptr<T> pop(TickType_t wait = portMAX_DELAY) {
if (isEmpty()) T *ptr = nullptr;
return nullptr; xQueueReceive(handle, &ptr, wait);
return std::unique_ptr<T>(ptr);
return buf[mask(read)];
} }
std::unique_ptr<T, Deleter> pop() { // The front item stays owned by the queue
if (isEmpty()) const T *peek(TickType_t wait = portMAX_DELAY) const {
return nullptr; T *ptr = nullptr;
xQueuePeek(handle, &ptr, wait);
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) return ptr;
return {buf[mask(read++)], Deleter{}};
else if constexpr (Owning)
return {buf[mask(read++)], Deleter{this}};
else
return {buf[mask(read++)], Deleter{owner}};
} }
private: private:
uint8_t mask(uint8_t pos) const { return pos & (N - 1); } QueueHandle_t handle;
void claim(T *x) { buf[mask(write++)] = x; }
std::array<T *, N> buf = {};
Queue<T, N, true, Deleter> *const owner;
uint8_t read = 0;
uint8_t write = 0;
}; };
template <class T, auto N, class D>
Queue(Queue<T, N, true, D> &) -> Queue<T, N, false, D>;
template <class T, auto N>
Queue(T (&items)[N]) -> Queue<T, N, true, detail::Deleter<T, N>>;
namespace detail {
template <class T, auto N> struct Deleter {
Deleter() = default;
constexpr Deleter(Queue<T, N, true, Deleter> *owner) : owner{owner} {}
void operator()(T *x) const { owner->claim(x); }
constexpr bool is_owned_by(const Queue<T, N, true, Deleter> *parent) const {
return owner == parent;
}
private:
Queue<T, N, true, Deleter> *const owner = nullptr;
};
} // namespace detail
+130 -46
View File
@@ -7,9 +7,11 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <new>
#include <utility> #include <utility>
#include "FreeRTOS.h" // IWYU pragma: export
#include "task.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/board.h" #include "hal/board.h"
@@ -18,15 +20,30 @@
#include "queue.hpp" #include "queue.hpp"
#include "stdshim.hpp" #include "stdshim.hpp"
const char *const offon[] = {"OFF", "ON"};
constexpr uint8_t CACHE_SIZE = 32;
using namespace avclan; using namespace avclan;
namespace { namespace {
constinit Queue<Frame, CACHE_SIZE> incoming; using Periph = Peripheral<CDChanger>;
constinit Queue<Frame, CACHE_SIZE> outgoing;
const char *const offon[] = {"OFF", "ON"};
constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE;
// A queue that can hold the entire Frame pool is never full, so the sender's
// blocking push of a follow-up frame onto its own queue can't deadlock
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
// Must be ranked such that queues deterministically tend to empty
constexpr UBaseType_t SendPriority = tskIDLE_PRIORITY + 3;
constexpr UBaseType_t RoutePriority = tskIDLE_PRIORITY + 2;
constexpr UBaseType_t PollPriority = tskIDLE_PRIORITY + 2;
constexpr UBaseType_t ReceivePriority = tskIDLE_PRIORITY + 1;
struct Context {
Periph &peripheral;
Queue<Frame> &incoming;
Queue<Frame> &outgoing;
};
uint8_t hexChars[2]; uint8_t hexChars[2];
uint8_t hexDigit = 0; // current digit being written to hexChars uint8_t hexDigit = 0; // current digit being written to hexChars
@@ -59,49 +76,60 @@ void set_flag(bool *flag, bool val, const char *msg) {
void Setup(); void Setup();
void print_help(); void print_help();
} // namespace
int main() { [[noreturn]] void vReceiverTask(void *pvParameters) {
Bus phy;
using enum Action;
using enum Device;
Peripheral<CDChanger> peripheral(phy, 0x360);
using Print = Frame::Print; using Print = Frame::Print;
auto &[peripheral, incoming, outgoing] =
Setup(); *static_cast<Context *>(pvParameters);
print_help();
while (true) { while (true) {
if (peripheral.bus_is_active()) { if (auto msg = peripheral.read(Print{.print = printAllFrames,
if (auto msg = peripheral.read(Print{.print = printAllFrames, .binary = printBinary,
.binary = printBinary, .verbose = verbose}))
.verbose = verbose})) incoming.push(std::move(*msg));
incoming.push(std::move(*msg)); }
} }
if (const auto *in = incoming.peek()) { [[noreturn]] void vRoutingTask(void *pvParameters) {
if (auto resp = peripheral.route(*in)) { auto &[peripheral, incoming, outgoing] =
incoming.pop(); *static_cast<Context *>(pvParameters);
if (*resp) { while (true) {
outgoing.push(std::move(*resp)); const Frame *in = incoming.peek();
continue; // route can be long; re-check the bus before poll/send if (auto resp = peripheral.route(*in)) {
} incoming.pop();
} if (*resp)
outgoing.push(std::move(*resp));
} }
}
}
[[noreturn]] void vPollTask(void *pvParameters) {
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (auto msg = peripheral.poll()) if (auto msg = peripheral.poll())
outgoing.push(std::move(msg)); outgoing.push(std::move(msg));
}
}
if (auto out = outgoing.pop()) { [[noreturn]] void vSenderTask(void *pvParameters) {
auto result = using Print = Frame::Print;
peripheral.send(std::move(out), Print{.print = printAllFrames, auto &[peripheral, incoming, outgoing] =
.binary = printBinary}); *static_cast<Context *>(pvParameters);
if (auto next = peripheral.react(std::move(result))) while (true) {
outgoing.push(std::move(next)); auto result = peripheral.send(
} outgoing.pop(), Print{.print = printAllFrames, .binary = printBinary});
if (auto next = peripheral.react(std::move(result)))
outgoing.push(std::move(next));
}
}
// stdin must be non-blocking: yielding EOF when idle/empty [[noreturn]] void vREPLTask(void *pvParameters) {
if (int readkey = getchar(); readkey != EOF) { using enum Action;
using enum Device;
auto &[peripheral, incoming, outgoing] =
*static_cast<Context *>(pvParameters);
while (true) {
if (int readkey = fgetc(stdin); readkey != EOF) {
switch (readkey) { switch (readkey) {
case '?': print_help(); break; case '?': print_help(); break;
case 'v': toggle_flag(&verbose, "Verbose errors:"); break; case 'v': toggle_flag(&verbose, "Verbose errors:"); break;
@@ -119,7 +147,7 @@ int main() {
case 'x': set_flag(&printBinary, false, "Binary:"); break; case 'x': set_flag(&printBinary, false, "Binary:"); break;
case 'E': // Beep case 'E': // Beep
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) { if (auto out = Frame::acquire()) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -134,7 +162,7 @@ int main() {
puts("!! failed Frame alloc for Beep !! "); puts("!! failed Frame alloc for Beep !! ");
break; break;
case 'P': case 'P':
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) { if (auto out = Frame::acquire()) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -176,6 +204,16 @@ int main() {
while (peripheral.device<CDChanger>().media_busy()) {} while (peripheral.device<CDChanger>().media_busy()) {}
puts("end"); puts("end");
break; break;
case '+':
peripheral.get_bus().deafen(true);
peripheral.get_bus().set_dominant();
puts("Set bus dominant...");
break;
case '-':
peripheral.get_bus().set_recessive();
peripheral.get_bus().deafen(false);
puts("Set bus recessive...");
break;
#ifdef MEASURE_BUS #ifdef MEASURE_BUS
case 'M': peripheral.get_bus().measure(); break; case 'M': peripheral.get_bus().measure(); break;
#endif #endif
@@ -207,12 +245,23 @@ int main() {
hexChars[0] = hexChars[1] = 0; hexChars[0] = hexChars[1] = 0;
seqIsUnicast = false; seqIsUnicast = false;
break; break;
case '\r': // Eat carriage return too
if (readBinary)
goto DEFAULT;
[[fallthrough]];
case '\n': case '\n':
if (readSeq && !readBinary && seqIdx == 0) {
// Nothing to send, so leave the mode instead: an escape for an
// entry started by accident.
readSeq = false;
hexDigit = hexChars[0] = hexChars[1] = 0;
printAllFrames = lastPrintAllFrames;
break;
}
if (readSeq && seqIdx > 0) { if (readSeq && seqIdx > 0) {
if (readBinary) { if (readBinary) {
if (data_tmp[seqIdx - 1] == 0x17) { if (data_tmp[seqIdx - 1] == 0x17) {
if (auto out = if (auto out = Frame::acquire()) {
std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (out->parse(data_tmp, --seqIdx) == if (out->parse(data_tmp, --seqIdx) ==
Frame::Error::Parse{0}) { Frame::Error::Parse{0}) {
out->reaction = 1; out->reaction = 1;
@@ -225,7 +274,7 @@ int main() {
goto DEFAULT; // reading binary and this is a real data byte; goto DEFAULT; // reading binary and this is a real data byte;
// fall through to default // fall through to default
} else { // ASCII message } else { // ASCII message
if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) { if (auto out = Frame::acquire()) {
const uint8_t sendLen = const uint8_t sendLen =
seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH; seqIdx <= Frame::MAXLENGTH ? seqIdx : Frame::MAXLENGTH;
out->is_unicast = seqIsUnicast; out->is_unicast = seqIsUnicast;
@@ -266,6 +315,10 @@ int main() {
if (readBinary) { if (readBinary) {
data_tmp[seqIdx++] = readkey; data_tmp[seqIdx++] = readkey;
} else { } else {
// Only take valid hex digits
if (isxdigit(readkey) == 0)
break;
hexChars[hexDigit++] = readkey; hexChars[hexDigit++] = readkey;
if (hexDigit == 2) { if (hexDigit == 2) {
@@ -290,9 +343,38 @@ int main() {
} }
} }
} // switch (readkey) } // switch (readkey)
} else {
clearerr(stdin);
} // if (readkey != EOF) } // if (readkey != EOF)
} }
return 0; }
} // namespace
int main() {
Setup();
print_help();
// Static: the scheduler reclaims main's stack for ISRs
static Bus phy;
static Periph peripheral(phy, 0x360);
static Queue<Frame> incoming(CACHE_SIZE);
static Queue<Frame> outgoing(CACHE_SIZE);
static Context ctx{
.peripheral = peripheral, .incoming = incoming, .outgoing = outgoing};
xTaskCreateAffinitySet(vSenderTask, "send/react", configMINIMAL_STACK_SIZE,
&ctx, SendPriority, 0b01, nullptr);
xTaskCreateAffinitySet(vRoutingTask, "router", configMINIMAL_STACK_SIZE, &ctx,
RoutePriority, 0b01, nullptr);
xTaskCreateAffinitySet(vPollTask, "poll", configMINIMAL_STACK_SIZE, &ctx,
PollPriority, 0b01, nullptr);
xTaskCreateAffinitySet(vReceiverTask, "receiver", configMINIMAL_STACK_SIZE,
&ctx, ReceivePriority, 0b01, nullptr);
xTaskCreateAffinitySet(vREPLTask, "repl", configMINIMAL_STACK_SIZE, &ctx,
tskIDLE_PRIORITY, 0b01, nullptr);
vTaskStartScheduler();
} }
namespace { namespace {
@@ -320,6 +402,8 @@ void print_help() {
"s - MIC skip forward\n" "s - MIC skip forward\n"
"b - MIC skip backward\n" "b - MIC skip backward\n"
"M - Measure bit-timing (pulse-widths and periods)\n" "M - Measure bit-timing (pulse-widths and periods)\n"
"+ - Set bus driven/dominant\n"
"- - Set bus idle/recessive\n"
#endif #endif
"? - Print this message"); "? - Print this message");
} }