mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-10-31 22:47:38 +00:00
Compare commits
10 Commits
master
...
5b9cb1da8b
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b9cb1da8b | |||
| fbf5811496 | |||
| 7252e6bfdd | |||
| 6b22f14e6d | |||
| 2c85305c0d | |||
| 8a47b5b776 | |||
| 9efbb01ccf | |||
| 53c5abb3e9 | |||
| ac47cebe86 | |||
| e30dbc501d |
+4
-2
@@ -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
|
||||||
|
|||||||
@@ -8,3 +8,7 @@ CompileFlags:
|
|||||||
- -ffixed-r2
|
- -ffixed-r2
|
||||||
- -ffixed-r3
|
- -ffixed-r3
|
||||||
- -ffixed-r4
|
- -ffixed-r4
|
||||||
|
- -fzero-init-padding-bits
|
||||||
|
- -fno-malloc-dce
|
||||||
|
Add:
|
||||||
|
- -Wno-missing-field-initializers
|
||||||
|
|||||||
+38
-25
@@ -1,25 +1,32 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.30)
|
||||||
|
|
||||||
project(avclan-mockingboard VERSION 1.1 LANGUAGES C CXX)
|
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 +59,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,6 +68,21 @@ add_library(avclan STATIC
|
|||||||
src/avclan/bus.cc
|
src/avclan/bus.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(avclan PUBLIC
|
||||||
|
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
|
||||||
|
AVCLAN_MSG_QUEUE_SIZE=${AVCLAN_MSG_QUEUE_SIZE}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_options(avclan PUBLIC
|
||||||
|
-Wall -Wswitch-enum -Werror
|
||||||
|
|
||||||
|
# Debug must not be -O0: both the vendored usart.h and avr-libc's
|
||||||
|
# <util/delay.h> #warning when __OPTIMIZE__ is undefined, and -Werror makes
|
||||||
|
# that fatal.
|
||||||
|
$<$<CONFIG:Debug>:-Og>
|
||||||
|
$<$<CONFIG:Debug>:-fanalyzer>
|
||||||
|
$<$<CONFIG:Debug>:-Wno-analyzer-use-of-uninitialized-value>)
|
||||||
|
|
||||||
# avclan exports its public generic headers (src/avclan) to consumers and
|
# avclan exports its public generic headers (src/avclan) to consumers and
|
||||||
# reaches into src/ for sibling headers (stdio.h, board.h) during its own
|
# reaches into src/ for sibling headers (stdio.h, board.h) during its own
|
||||||
# build. The selected target adds its own per-target include path.
|
# build. The selected target adds its own per-target include path.
|
||||||
|
|||||||
+46
-10
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
|
||||||
|
|
||||||
|
# This can be dropped into an external project to help locate this SDK
|
||||||
|
# It should be include()ed prior to project()
|
||||||
|
|
||||||
|
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
||||||
|
# following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
||||||
|
# disclaimer.
|
||||||
|
#
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
||||||
|
# disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
|
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
|
||||||
|
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
|
||||||
|
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
|
||||||
|
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
|
||||||
|
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
|
||||||
|
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
|
||||||
|
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
|
||||||
|
|
||||||
|
if (NOT PICO_SDK_PATH)
|
||||||
|
if (PICO_SDK_FETCH_FROM_GIT)
|
||||||
|
include(FetchContent)
|
||||||
|
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
|
||||||
|
if (PICO_SDK_FETCH_FROM_GIT_PATH)
|
||||||
|
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||||
|
endif ()
|
||||||
|
FetchContent_Declare(
|
||||||
|
pico_sdk
|
||||||
|
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||||
|
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (NOT pico_sdk)
|
||||||
|
message("Downloading Raspberry Pi Pico SDK")
|
||||||
|
# GIT_SUBMODULES_RECURSE was added in 3.17
|
||||||
|
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
|
||||||
|
FetchContent_Populate(
|
||||||
|
pico_sdk
|
||||||
|
QUIET
|
||||||
|
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||||
|
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||||
|
GIT_SUBMODULES_RECURSE FALSE
|
||||||
|
|
||||||
|
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||||
|
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||||
|
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||||
|
)
|
||||||
|
else ()
|
||||||
|
FetchContent_Populate(
|
||||||
|
pico_sdk
|
||||||
|
QUIET
|
||||||
|
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||||
|
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||||
|
|
||||||
|
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||||
|
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||||
|
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
|
||||||
|
endif ()
|
||||||
|
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
|
||||||
|
else ()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||||
|
if (NOT EXISTS ${PICO_SDK_PATH})
|
||||||
|
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
|
||||||
|
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
|
||||||
|
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
|
||||||
|
|
||||||
|
include(${PICO_SDK_INIT_CMAKE_FILE})
|
||||||
+4
-6
@@ -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
-202
@@ -26,10 +26,12 @@
|
|||||||
| 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 <concepts>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <new>
|
#include <new>
|
||||||
@@ -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,133 +69,69 @@ 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);
|
};
|
||||||
|
Send send_peripheral_addr(uint16_t addr, bool expect_ack) {
|
||||||
if (expect_ack)
|
return phy_send_peripheral_addr(addr, expect_ack);
|
||||||
return read_ACK();
|
};
|
||||||
|
Send send_control(uint8_t control, bool expect_ack) {
|
||||||
sendbits<1>(1U); // still need to fill the ack bit slot
|
return phy_send_control(control, expect_ack);
|
||||||
return Send{0};
|
};
|
||||||
|
Send send_length(uint8_t length, bool expect_ack) {
|
||||||
|
return phy_send_length(length, expect_ack);
|
||||||
|
};
|
||||||
|
Send send_data(const uint8_t *data, uint8_t length, bool expect_ack,
|
||||||
|
uint8_t *data_index) {
|
||||||
|
return phy_send_data(data, length, expect_ack, data_index);
|
||||||
|
};
|
||||||
|
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||||
};
|
};
|
||||||
|
|
||||||
template <auto N, std::unsigned_integral T,
|
void Bus::init(uint16_t address) {
|
||||||
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};
|
|
||||||
};
|
|
||||||
|
|
||||||
template <auto N, std::unsigned_integral T, class F>
|
|
||||||
requires(sizeof(T) < 3 && N < 16)
|
|
||||||
Read read(T *bits, with_ack_t /*tag*/, F &&ack) {
|
|
||||||
if (read<N>(bits, with_parity) == Read::BAD_PARITY)
|
|
||||||
return Read::BAD_PARITY;
|
|
||||||
|
|
||||||
if (ack()) {
|
|
||||||
send_ACK();
|
|
||||||
} else {
|
|
||||||
uint8_t slot;
|
|
||||||
readbits<1>(&slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Read{0};
|
|
||||||
};
|
|
||||||
template <auto N, std::unsigned_integral T>
|
|
||||||
requires(sizeof(T) < 3 && N < 16)
|
|
||||||
Read read(T *bits, with_ack_t /*tag*/, bool ack) {
|
|
||||||
return read<N>(bits, with_ack, [=]() { return ack; });
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static void send_ACK() { phy_send_ack(); };
|
|
||||||
static Send read_ACK() { return phy_read_ack(); };
|
|
||||||
|
|
||||||
template <auto N, class T> Bit sendbits(T bits);
|
|
||||||
template <auto N, class T> Bit readbits(T *bits);
|
|
||||||
|
|
||||||
template <auto N>
|
|
||||||
requires(N > 1 && N < 8)
|
|
||||||
Bit sendbits(uint8_t bits) {
|
|
||||||
return phy_send_bits_u8(&bits, N);
|
|
||||||
};
|
|
||||||
template <auto N>
|
|
||||||
requires(N <= 16)
|
|
||||||
Bit sendbits(uint16_t bits) {
|
|
||||||
return phy_send_bits_u16(&bits, N);
|
|
||||||
};
|
|
||||||
template <auto N>
|
|
||||||
requires(N < 8)
|
|
||||||
Bit readbits(uint8_t *bits) {
|
|
||||||
return static_cast<Bit>(phy_read_bits_u8(bits, N));
|
|
||||||
};
|
|
||||||
template <auto N>
|
|
||||||
requires(N <= 16)
|
|
||||||
Bit readbits(uint16_t *bits) {
|
|
||||||
return static_cast<Bit>(phy_read_bits_u16(bits, N));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
template <> inline Bit Bus::Handle::sendbits<8>(uint8_t bits) {
|
|
||||||
return phy_send_byte(&bits);
|
|
||||||
};
|
|
||||||
template <> inline Bit Bus::Handle::sendbits<1>(uint8_t bits) {
|
|
||||||
const Bit bit{static_cast<Bit>(bits & 1U)};
|
|
||||||
phy_send_bit(bit);
|
|
||||||
return bit;
|
|
||||||
};
|
|
||||||
template <> inline Bit Bus::Handle::readbits<8>(uint8_t *bits) {
|
|
||||||
return phy_read_byte(bits);
|
|
||||||
};
|
|
||||||
|
|
||||||
void Bus::init() {
|
|
||||||
// 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
|
||||||
|
deafened_ = false; // Default to listening
|
||||||
inited_ = true;
|
inited_ = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||||
bool Bus::is_active() const { return phy_active(); };
|
bool Bus::is_active() const { return !deafened_ && phy_frame_pending(); };
|
||||||
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
|
||||||
};
|
};
|
||||||
|
void Bus::deafen(bool deaf) {
|
||||||
|
phy_deafen(deaf);
|
||||||
|
deafened_ = deaf; // Only update deafened_ *AFTER* the phy has stopped acking
|
||||||
|
}
|
||||||
|
|
||||||
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 = {};
|
||||||
|
|
||||||
@@ -205,88 +139,67 @@ auto Bus::read(uint16_t address, Frame::Print print)
|
|||||||
|
|
||||||
std::unique_ptr<Frame> in(new (std::nothrow) Frame);
|
std::unique_ptr<Frame> in(new (std::nothrow) Frame);
|
||||||
if (!in) {
|
if (!in) {
|
||||||
err.errno = POOL_EMPTY;
|
err.type = POOL_EMPTY;
|
||||||
goto handle_err;
|
goto handle_err;
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // 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[i];
|
err.val = in->data[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:;
|
||||||
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 +217,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 +229,14 @@ auto Bus::read(uint16_t address, Frame::Print print)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only print if some data has been correctly received
|
// Only print if some data has been correctly received
|
||||||
if (print.print && (err.errno < STARTBIT_MISSED)) {
|
if (print.print && (err.type < STARTBIT_MISSED)) {
|
||||||
if (err.errno > BAD_DATA_PARITY)
|
if (err.type > BAD_DATA_PARITY)
|
||||||
in->length = 0;
|
in->length = 0;
|
||||||
in->print(print);
|
in->print(print);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.errno != Read{0})
|
if (err.type != Read{0})
|
||||||
return unexpected(err.errno);
|
return unexpected(err.type);
|
||||||
|
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
@@ -331,79 +245,91 @@ 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 (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);
|
if (err.type != Send{0})
|
||||||
serr == NAK) {
|
goto handle_err; // Outbid (or busy) on every attempt
|
||||||
err.errno = NAK_CONTROL;
|
|
||||||
goto handle_err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto serr = handle.send<8>(out.length, with_ack, out.is_unicast);
|
|
||||||
serr == NAK) {
|
|
||||||
err.errno = NAK_MESSAGE_LENGTH;
|
|
||||||
goto handle_err;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < out.length; i++) {
|
|
||||||
if (auto serr = handle.send<8>(out.data[i], with_ack, out.is_unicast);
|
|
||||||
serr == NAK) {
|
|
||||||
err.errno = NAK_DATA;
|
|
||||||
err.val = i;
|
|
||||||
goto handle_err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // destroy handle
|
|
||||||
|
|
||||||
// back to read mode
|
// back to read mode
|
||||||
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
||||||
handle_err:;
|
handle_err:;
|
||||||
fputs("Error", stdout);
|
fputs("Error", stdout);
|
||||||
switch (err.errno) {
|
switch (err.type) {
|
||||||
case MUTED: fputs(": Device muted", stdout); break;
|
case MUTED: fputs(": Device muted", stdout); break;
|
||||||
case BUSY: fputs(": Busy bus", stdout); break;
|
case BUSY: fputs(": Busy bus", stdout); break;
|
||||||
|
case CONTENDED_BUS:
|
||||||
|
fputs(": bus contended after arbitration", stdout);
|
||||||
|
break;
|
||||||
|
case LOST_ARBITRATION: fputs(": lost arbitration", stdout); break;
|
||||||
case NAK_ADDRESS:
|
case NAK_ADDRESS:
|
||||||
case NAK_CONTROL:
|
case NAK_CONTROL:
|
||||||
case NAK_MESSAGE_LENGTH:
|
case NAK_MESSAGE_LENGTH:
|
||||||
case NAK_DATA:
|
case NAK_DATA:
|
||||||
|
case NAK_TOO_LONG:
|
||||||
case NAK:
|
case NAK:
|
||||||
fputs(" NAK: ", stdout);
|
fputs(" NAK: ", stdout);
|
||||||
switch (err.errno) {
|
switch (err.type) {
|
||||||
case NAK_ADDRESS: fputs("address", stdout); break;
|
case NAK_ADDRESS: fputs("address", stdout); break;
|
||||||
case NAK_CONTROL: fputs("Control", stdout); break;
|
case NAK_CONTROL: fputs("Control", stdout); break;
|
||||||
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
|
case NAK_MESSAGE_LENGTH: fputs("Message length", stdout); break;
|
||||||
case NAK_DATA: printf(" data[%u]", err.val); break;
|
case NAK_DATA: printf(" data[%u]", err.val); break;
|
||||||
|
case NAK_TOO_LONG: fputs("too long", stdout); break;
|
||||||
case NAK:
|
case NAK:
|
||||||
case MUTED:
|
case MUTED:
|
||||||
|
case CONTENDED_BUS:
|
||||||
|
case LOST_ARBITRATION:
|
||||||
case BUSY: __builtin_unreachable();
|
case BUSY: __builtin_unreachable();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -414,16 +340,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 for
|
||||||
|
// the same reason as is_active().
|
||||||
// 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
|
||||||
|
|||||||
+16
-3
@@ -69,18 +69,30 @@ 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);
|
||||||
|
|
||||||
|
// True when there is a frame to read and we aren't deafened. Depending on the
|
||||||
|
// target that means the bus has gone dominant or a frame is already buffered.
|
||||||
bool is_active() const;
|
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:
|
||||||
@@ -89,6 +101,7 @@ private:
|
|||||||
|
|
||||||
// Assume mute after default ctor; only viable after init call
|
// Assume mute after default ctor; only viable after init call
|
||||||
bool muted_ = true;
|
bool muted_ = true;
|
||||||
|
bool deafened_ = false;
|
||||||
bool inited_ = false;
|
bool inited_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -163,14 +163,14 @@ void Frame::print(Frame::Print print) const {
|
|||||||
|
|
||||||
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
||||||
struct errtype {
|
struct errtype {
|
||||||
Error::Parse errno;
|
Error::Parse type;
|
||||||
uint8_t val;
|
uint8_t val;
|
||||||
} err = {};
|
} err = {};
|
||||||
|
|
||||||
const uint8_t *last = bytes + len;
|
const uint8_t *last = bytes + len;
|
||||||
|
|
||||||
if (len < Frame::MIN_SIZE) {
|
if (len < Frame::MIN_SIZE) {
|
||||||
err.errno = TOO_SHORT;
|
err.type = TOO_SHORT;
|
||||||
goto handle_err;
|
goto handle_err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
|||||||
length = *bytes++;
|
length = *bytes++;
|
||||||
|
|
||||||
if (length > MAXLENGTH) {
|
if (length > MAXLENGTH) {
|
||||||
err.errno = LENGTH_TOO_BIG;
|
err.type = LENGTH_TOO_BIG;
|
||||||
err.val = length;
|
err.val = length;
|
||||||
goto handle_err;
|
goto handle_err;
|
||||||
}
|
}
|
||||||
@@ -191,14 +191,14 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
|||||||
if ((bytes + length) <= last) {
|
if ((bytes + length) <= last) {
|
||||||
memcpy(data, bytes, length);
|
memcpy(data, bytes, length);
|
||||||
} else {
|
} else {
|
||||||
err.errno = MISMATCH_LENGTH;
|
err.type = MISMATCH_LENGTH;
|
||||||
goto handle_err;
|
goto handle_err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
if (false) { // NOLINT(readability-simplify-boolean-expr)
|
||||||
handle_err:;
|
handle_err:;
|
||||||
fputs("ERR(parse): ", stdout);
|
fputs("ERR(parse): ", stdout);
|
||||||
switch (err.errno) {
|
switch (err.type) {
|
||||||
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
|
case TOO_SHORT: puts("not enough bytes too fill AVCLAN frame"); break;
|
||||||
case MISMATCH_LENGTH:
|
case MISMATCH_LENGTH:
|
||||||
puts("frame->length is longer than remaining data");
|
puts("frame->length is longer than remaining data");
|
||||||
@@ -211,6 +211,6 @@ Error::Parse Frame::parse(const uint8_t *bytes, uint8_t len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err.errno;
|
return err.type;
|
||||||
}
|
}
|
||||||
} // namespace avclan
|
} // namespace avclan
|
||||||
|
|||||||
+70
-39
@@ -12,16 +12,16 @@
|
|||||||
#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,57 +30,88 @@ 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)
|
// Withhold acknowledgement. Unlike mute this leaves TX alone: a deaf device
|
||||||
bool phy_active(void);
|
// can still manipulate the bus/send frames, it just never responds (e.g. ACK,
|
||||||
|
// etc). An empty implementation is sufficient for synchronous ports.
|
||||||
|
void phy_deafen(bool deaf);
|
||||||
|
|
||||||
|
// True when there is a frame to read. This may reflect current bus state (e.g.
|
||||||
|
// a frame can be synchronously read from the bus) or indicate that a buffered
|
||||||
|
// frame is available to "read".
|
||||||
|
bool phy_frame_pending(void);
|
||||||
|
|
||||||
// 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 contention isn't a concern.
|
||||||
|
// - May acquire a hardware lock to prevent concurrent use
|
||||||
void phy_guard_enter(void);
|
void phy_guard_enter(void);
|
||||||
void phy_guard_leave(void);
|
void phy_guard_leave(void);
|
||||||
|
|
||||||
// Validates an incoming start bit; see avclan::detail::Error::Read.
|
/* Per-field frame I/O.
|
||||||
// Invariants:
|
*
|
||||||
// - Must only be called after positive phy_active() call.
|
* All send functions may have asynchronous implementations (e.g. return before
|
||||||
Read phy_read_startbit(void);
|
* the send has completed on the bus). Success is indicated by a zero value
|
||||||
|
* `Read` or `Send` enum. Non-zero error codes indicate a synchronously
|
||||||
// Acquire the bus and emit a start bit; may return BUSY
|
* 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
|
||||||
/* Returns 0 (`(Send)0`) if the peripheral sent an ACK bit, otherwise returns
|
* queued send's have finished sending over the bus.
|
||||||
NAK. An ACK bit is a cooperative bit, where the sender starts (drives the bus)
|
*
|
||||||
for the sync period, and allows the receiver to drive the bus (or not) to
|
* The read functions are similarly optionally asynchronous, and may return the
|
||||||
finish a "1" bit.
|
* 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.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
Send phy_read_ack(void);
|
Read phy_read_header(bool *is_unicast);
|
||||||
void phy_send_ack(void);
|
Read phy_read_controller_addr(uint16_t *addr);
|
||||||
|
Read phy_read_peripheral_addr(uint16_t *addr);
|
||||||
|
Read phy_read_control(uint8_t *control);
|
||||||
|
Read phy_read_length(uint8_t *length);
|
||||||
|
// Read `length` data bytes. `data_index` is only written to on error, with the
|
||||||
|
// index of the failed byte.
|
||||||
|
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index);
|
||||||
|
|
||||||
// Per-symbol I/O. The send* helpers return the even parity of the bits sent;
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -11,8 +12,8 @@ extern "C" {
|
|||||||
|
|
||||||
// Generic stdio interface initialization. All user I/O goes through <stdio.h>
|
// Generic stdio interface initialization. All user I/O goes through <stdio.h>
|
||||||
// functions. Assumptions/invariants:
|
// functions. Assumptions/invariants:
|
||||||
// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when
|
// - stdin MUST be non-blocking (ie. a libc read yields EOF immediately when no
|
||||||
// empty). Necessary to avoid stalling the REPL poll loop.
|
// input is buffered). Necessary to avoid stalling the REPL poll loop.
|
||||||
// - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
|
// - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not
|
||||||
// change a bare LF or a binary frame payload. Writes through
|
// change a bare LF or a binary frame payload. Writes through
|
||||||
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
|
// <stdio.h> and writes through stdio_write_nonblock() must reach the same
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public:
|
|||||||
"update the sentinel value in avclan.h");
|
"update the sentinel value in avclan.h");
|
||||||
|
|
||||||
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
|
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
|
||||||
bus.init();
|
bus.init(address);
|
||||||
(std::get<Devs>(devices_).init(), ...);
|
(std::get<Devs>(devices_).init(), ...);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,10 @@ public:
|
|||||||
|
|
||||||
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);
|
if (!bus.is_active())
|
||||||
|
return unexpected{detail::Error::Read::NO_FRAME};
|
||||||
|
|
||||||
|
return bus.read(print);
|
||||||
};
|
};
|
||||||
|
|
||||||
expected<std::unique_ptr<Frame>, detail::SendError>
|
expected<std::unique_ptr<Frame>, detail::SendError>
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ target_sources(avclan PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(avclan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_compile_options(avclan PUBLIC
|
||||||
|
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
|
||||||
|
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
|
||||||
|
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
|
||||||
|
)
|
||||||
|
|
||||||
find_program(AVRDUDE avrdude)
|
find_program(AVRDUDE avrdude)
|
||||||
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
|
set(AVR_PROGRAMMER serialupdi CACHE STRING "avrdude programmer hardware")
|
||||||
@@ -76,9 +81,6 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
|
|||||||
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
|
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
|
||||||
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
|
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
|
||||||
|
|
||||||
# --- Firmware configuration options ----------------------------------------
|
|
||||||
set(AVCLAN_FRAME_POOL_N 14 CACHE STRING "Pool allocator capacity for avclan::Frame (must be <= incoming/outgoing queue size)")
|
|
||||||
|
|
||||||
try_compile(LIBC_VERSION_TEST
|
try_compile(LIBC_VERSION_TEST
|
||||||
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
|
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
|
||||||
COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
|
COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
|
||||||
@@ -137,6 +139,7 @@ target_compile_definitions(jnk0le_uart
|
|||||||
PRIVATE F_CPU=${FREQSEL}
|
PRIVATE F_CPU=${FREQSEL}
|
||||||
)
|
)
|
||||||
target_compile_options(jnk0le_uart PUBLIC
|
target_compile_options(jnk0le_uart PUBLIC
|
||||||
|
$<$<CONFIG:Debug>:-Og>
|
||||||
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
|
-ffixed-r2 -ffixed-r3 -ffixed-r4) # reserved ISR scratch (SREG=r4, Z=r2:r3)
|
||||||
target_link_libraries(avclan PUBLIC jnk0le_uart)
|
target_link_libraries(avclan PUBLIC jnk0le_uart)
|
||||||
|
|
||||||
@@ -148,7 +151,6 @@ target_compile_definitions(avclan PUBLIC
|
|||||||
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
|
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
|
||||||
TCB_CLKSEL=${TCB_CLKSEL}
|
TCB_CLKSEL=${TCB_CLKSEL}
|
||||||
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
|
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
|
||||||
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
|
|
||||||
)
|
)
|
||||||
target_compile_options(avclan PUBLIC
|
target_compile_options(avclan PUBLIC
|
||||||
--param=min-pagesize=0
|
--param=min-pagesize=0
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <util/atomic.h>
|
#include <util/atomic.h>
|
||||||
|
|
||||||
|
#include "avclan.h"
|
||||||
#include "hal/cd_timer.h" // statustimer_enable/disable (guard)
|
#include "hal/cd_timer.h" // statustimer_enable/disable (guard)
|
||||||
#include "hal/phy.h"
|
#include "hal/phy.h"
|
||||||
#include "media_avr.h" // media_sync_during_mask (guard)
|
#include "media_avr.h" // media_sync_during_mask (guard)
|
||||||
@@ -17,6 +18,9 @@
|
|||||||
// avr-libc.
|
// avr-libc.
|
||||||
#include "timing_avr.h" // IWYU pragma: keep
|
#include "timing_avr.h" // IWYU pragma: keep
|
||||||
|
|
||||||
|
enum bit_t : uint8_t { bit_zero = 0x00, bit_one = 0x01, bit_start = 0x10 };
|
||||||
|
typedef enum bit_t Bit;
|
||||||
|
|
||||||
// USART0 TX ring indices owned by the jnk0le lib; the guard consults them to
|
// USART0 TX ring indices owned by the jnk0le lib; the guard consults them to
|
||||||
// decide whether to resume the TX drain (DRE interrupt) on leave.
|
// decide whether to resume the TX drain (DRE interrupt) on leave.
|
||||||
extern volatile uint8_t tx0_Head, tx0_Tail;
|
extern volatile uint8_t tx0_Head, tx0_Tail;
|
||||||
@@ -79,8 +83,12 @@ bool phy_is_muted() {
|
|||||||
return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0);
|
return (((VPORTA_DIR & PIN4_bm) | (VPORTA_DIR & PIN0_bm)) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No-op: acks are driven from inside the synchronous read path, which a deaf
|
||||||
|
// caller never enters.
|
||||||
|
void phy_deafen([[maybe_unused]] bool deaf) {}
|
||||||
|
|
||||||
// True when the bus is being driven (i.e. not idle/floating).
|
// True when the bus is being driven (i.e. not idle/floating).
|
||||||
bool phy_active() { return (!BUS_IS_IDLE) != 0; }
|
bool phy_frame_pending() { return (!BUS_IS_IDLE) != 0; }
|
||||||
|
|
||||||
// Mute device TX on AVCLAN bus
|
// Mute device TX on AVCLAN bus
|
||||||
void phy_mute(bool mute) {
|
void phy_mute(bool mute) {
|
||||||
@@ -114,7 +122,7 @@ static void set_AVC_logic_for(uint8_t val, uint16_t period) {
|
|||||||
while (TCB1.CNT <= period) {};
|
while (TCB1.CNT <= period) {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void phy_send_bit(Bit bit) {
|
static void phy_send_bit(Bit bit) {
|
||||||
uint16_t zero_length, one_length;
|
uint16_t zero_length, one_length;
|
||||||
switch (bit) {
|
switch (bit) {
|
||||||
case bit_zero:
|
case bit_zero:
|
||||||
@@ -135,7 +143,7 @@ void phy_send_bit(Bit bit) {
|
|||||||
set_AVC_logic_for(1, one_length);
|
set_AVC_logic_for(1, one_length);
|
||||||
}
|
}
|
||||||
|
|
||||||
void phy_send_ack() {
|
static void phy_send_ack() {
|
||||||
TCB1.CNT = 0;
|
TCB1.CNT = 0;
|
||||||
|
|
||||||
// Wait for controller to begin ACK bit
|
// Wait for controller to begin ACK bit
|
||||||
@@ -149,7 +157,7 @@ void phy_send_ack() {
|
|||||||
phy_send_bit(bit_zero);
|
phy_send_bit(bit_zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
Send phy_read_ack() {
|
static Send phy_read_ack() {
|
||||||
TCB1.CNT = 0; // Double reset of TCB1.CNT: here
|
TCB1.CNT = 0; // Double reset of TCB1.CNT: here
|
||||||
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
|
set_AVC_logic_for(0, AVCLAN_BIT1_LOGIC_0); // And here (within)
|
||||||
AVCLAN_setBusIdle(); // Stop driving bus
|
AVCLAN_setBusIdle(); // Stop driving bus
|
||||||
@@ -164,13 +172,13 @@ Send phy_read_ack() {
|
|||||||
// Check/wait in case we get here before peripheral finishes ACK bit
|
// Check/wait in case we get here before peripheral finishes ACK bit
|
||||||
while (!BUS_IS_IDLE) {
|
while (!BUS_IS_IDLE) {
|
||||||
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
|
if (TCB1.CNT > AVCLAN_BIT_LENGTH_MAX)
|
||||||
return NAK;
|
return NAK_TOO_LONG;
|
||||||
}
|
}
|
||||||
return (Send)0;
|
return (Send)0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send `len` bits on the AVCLAN bus; returns the even parity
|
// Send `len` bits on the AVCLAN bus; returns the even parity
|
||||||
Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
|
static Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
|
||||||
uint8_t b = *bits;
|
uint8_t b = *bits;
|
||||||
uint8_t parity = 0;
|
uint8_t parity = 0;
|
||||||
int8_t len_mod8 = 8;
|
int8_t len_mod8 = 8;
|
||||||
@@ -189,17 +197,19 @@ Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) {
|
|||||||
b <<= 1;
|
b <<= 1;
|
||||||
}
|
}
|
||||||
len_mod8 = 8;
|
len_mod8 = 8;
|
||||||
|
// Avoid under-read for last byte
|
||||||
|
if (len > 0)
|
||||||
b = *--bits;
|
b = *--bits;
|
||||||
}
|
}
|
||||||
return (parity & 1);
|
return (parity & 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send `len` bits on the AVCLAN bus; returns the even parity
|
// Send `len` bits on the AVCLAN bus; returns the even parity
|
||||||
Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
|
static Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) {
|
||||||
return phy_send_bits_u8((const uint8_t *)bits + 1, len);
|
return phy_send_bits_u8((const uint8_t *)bits + 1, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bit phy_send_byte(const uint8_t *byte) {
|
static Bit phy_send_byte(const uint8_t *byte) {
|
||||||
uint8_t b = *byte;
|
uint8_t b = *byte;
|
||||||
uint8_t parity = 0;
|
uint8_t parity = 0;
|
||||||
|
|
||||||
@@ -238,7 +248,7 @@ ISR(TCB0_INT_vect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read `len` bits on the AVCLAN bus; returns the even parity
|
// Read `len` bits on the AVCLAN bus; returns the even parity
|
||||||
Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
|
static Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
|
||||||
uint8_t parity;
|
uint8_t parity;
|
||||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||||
READING_BYTE = 0;
|
READING_BYTE = 0;
|
||||||
@@ -265,7 +275,7 @@ Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read `len` bits on the AVCLAN bus; returns the even parity
|
// Read `len` bits on the AVCLAN bus; returns the even parity
|
||||||
Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
|
static Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
|
||||||
uint8_t parity = 0;
|
uint8_t parity = 0;
|
||||||
if (len > 8) {
|
if (len > 8) {
|
||||||
uint8_t over = len - 8;
|
uint8_t over = len - 8;
|
||||||
@@ -278,7 +288,7 @@ Bit phy_read_bits_u16(uint16_t *bits, int8_t len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read a byte on the AVCLAN bus
|
// Read a byte on the AVCLAN bus
|
||||||
Bit phy_read_byte(uint8_t *byte) {
|
static Bit phy_read_byte(uint8_t *byte) {
|
||||||
uint8_t parity;
|
uint8_t parity;
|
||||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||||
READING_BYTE = 0;
|
READING_BYTE = 0;
|
||||||
@@ -304,7 +314,16 @@ Bit phy_read_byte(uint8_t *byte) {
|
|||||||
return (Bit)(parity & 1);
|
return (Bit)(parity & 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void phy_init() {
|
// Our own peripheral address, and whether the frame being read is addressed to
|
||||||
|
// it. hal/phy.h puts the ACK decision in the phy, so the latch that used to
|
||||||
|
// live in Bus::read lives here now: set when the peripheral address comes in,
|
||||||
|
// and reused by every field after it.
|
||||||
|
static uint16_t own_address_;
|
||||||
|
static bool acking_;
|
||||||
|
|
||||||
|
void phy_init(uint16_t address) {
|
||||||
|
own_address_ = address;
|
||||||
|
|
||||||
// Set pin 6 and 7 as input
|
// Set pin 6 and 7 as input
|
||||||
PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
|
PORTA.DIRCLR = (PIN6_bm | PIN7_bm);
|
||||||
// Disable input buffer; recommended when using AC
|
// Disable input buffer; recommended when using AC
|
||||||
@@ -339,7 +358,7 @@ void phy_init() {
|
|||||||
phy_mute(false); // unmute AVCLAN bus TX
|
phy_mute(false); // unmute AVCLAN bus TX
|
||||||
}
|
}
|
||||||
|
|
||||||
Read phy_read_startbit() {
|
static Read phy_read_startbit() {
|
||||||
// Following HAL header docs, this function is only called after a driven bus
|
// Following HAL header docs, this function is only called after a driven bus
|
||||||
// was ~recently detected.
|
// was ~recently detected.
|
||||||
// Two main (designed) entry flows, depending on timing of pulse end w.r.t.
|
// Two main (designed) entry flows, depending on timing of pulse end w.r.t.
|
||||||
@@ -430,7 +449,7 @@ Read phy_read_startbit() {
|
|||||||
|
|
||||||
// Acquire the bus and emit a start bit. Returns false if another device is
|
// Acquire the bus and emit a start bit. Returns false if another device is
|
||||||
// already driving the bus (we can't yet do proper CSMA/CD).
|
// already driving the bus (we can't yet do proper CSMA/CD).
|
||||||
Send phy_send_startbit() {
|
static Send phy_send_startbit() {
|
||||||
// wait for free line
|
// wait for free line
|
||||||
TCB1.CNT = 0;
|
TCB1.CNT = 0;
|
||||||
while (BUS_IS_IDLE) {
|
while (BUS_IS_IDLE) {
|
||||||
@@ -459,6 +478,144 @@ Send phy_send_startbit() {
|
|||||||
return (Send)0;
|
return (Send)0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field bits, then the wire's parity bit, checked against what we counted.
|
||||||
|
static Read read_parity(Bit calc, Read bad) {
|
||||||
|
uint8_t wire = 0;
|
||||||
|
phy_read_bits_u8(&wire, 1);
|
||||||
|
return ((uint8_t)calc != wire) ? bad : (Read)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An acknowledge slot follows the field whether or not we drive it, so a frame
|
||||||
|
// that isn't ours still has to have the slot read away.
|
||||||
|
static void ack_slot(void) {
|
||||||
|
if (acking_) {
|
||||||
|
phy_send_ack();
|
||||||
|
} else {
|
||||||
|
uint8_t slot = 0;
|
||||||
|
phy_read_bits_u8(&slot, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Read phy_read_header(bool *is_unicast) {
|
||||||
|
acking_ = false; // New frame; nothing is addressed to us yet
|
||||||
|
|
||||||
|
const Read err = phy_read_startbit();
|
||||||
|
if (err != (Read)0)
|
||||||
|
return err;
|
||||||
|
|
||||||
|
uint8_t bit = 0;
|
||||||
|
phy_read_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
|
||||||
|
*is_unicast = (bit != 0);
|
||||||
|
return (Read)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Read phy_read_controller_addr(uint16_t *addr) {
|
||||||
|
// No acknowledge slot: this field is the arbitration window.
|
||||||
|
return read_parity(phy_read_bits_u16(addr, 12), BAD_CONTROLLER_PARITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Read phy_read_peripheral_addr(uint16_t *addr) {
|
||||||
|
const Read err =
|
||||||
|
read_parity(phy_read_bits_u16(addr, 12), BAD_PERIPHERAL_PARITY);
|
||||||
|
if (err != (Read)0)
|
||||||
|
return err;
|
||||||
|
|
||||||
|
// The one field that decides the ack; every field after it reuses the answer.
|
||||||
|
acking_ = !phy_is_muted() && (*addr == own_address_);
|
||||||
|
ack_slot();
|
||||||
|
return (Read)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Read phy_read_control(uint8_t *control) {
|
||||||
|
const Read err =
|
||||||
|
read_parity(phy_read_bits_u8(control, 4), BAD_CONTROL_PARITY);
|
||||||
|
if (err != (Read)0)
|
||||||
|
return err;
|
||||||
|
ack_slot();
|
||||||
|
return (Read)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Read phy_read_length(uint8_t *length) {
|
||||||
|
const Read err = read_parity(phy_read_byte(length), BAD_LENGTH_PARITY);
|
||||||
|
if (err != (Read)0)
|
||||||
|
return err;
|
||||||
|
ack_slot();
|
||||||
|
return (Read)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
|
||||||
|
for (uint8_t i = 0; i < length; i++) {
|
||||||
|
const Read err = read_parity(phy_read_byte(&data[i]), BAD_DATA_PARITY);
|
||||||
|
if (err != (Read)0) {
|
||||||
|
*data_index = i;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
ack_slot();
|
||||||
|
}
|
||||||
|
return (Read)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `expect_ack` is "a NAK here is worth reporting", not "emit the slot": the
|
||||||
|
// slot goes out either way, and a broadcast frame simply has nobody to fill it.
|
||||||
|
static Send send_ack_slot(bool expect_ack, Send nak) {
|
||||||
|
if (expect_ack) {
|
||||||
|
const Send err = phy_read_ack();
|
||||||
|
return err == NAK ? nak : err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t fill = 1U;
|
||||||
|
phy_send_bits_u8(&fill, 1);
|
||||||
|
return (Send)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Send phy_send_header(bool is_unicast) {
|
||||||
|
const Send err = phy_send_startbit();
|
||||||
|
if (err != (Send)0)
|
||||||
|
return err;
|
||||||
|
|
||||||
|
const uint8_t bit = (is_unicast) ? 1U : 0U;
|
||||||
|
phy_send_bits_u8(&bit, 1); // Broadcast bit: one bare bit, no parity
|
||||||
|
return (Send)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Send phy_send_controller_addr(uint16_t addr) {
|
||||||
|
// No acknowledge slot: this field is the arbitration window.
|
||||||
|
phy_send_bit(phy_send_bits_u16(&addr, 12));
|
||||||
|
return (Send)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
|
||||||
|
phy_send_bit(phy_send_bits_u16(&addr, 12));
|
||||||
|
return send_ack_slot(expect_ack, NAK_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
Send phy_send_control(uint8_t control, bool expect_ack) {
|
||||||
|
phy_send_bit(phy_send_bits_u8(&control, 4));
|
||||||
|
return send_ack_slot(expect_ack, NAK_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
Send phy_send_length(uint8_t length, bool expect_ack) {
|
||||||
|
phy_send_bit(phy_send_byte(&length));
|
||||||
|
return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
|
||||||
|
uint8_t *data_index) {
|
||||||
|
for (uint8_t i = 0; i < length; i++) {
|
||||||
|
phy_send_bit(phy_send_byte(&data[i]));
|
||||||
|
const Send err = send_ack_slot(expect_ack, NAK_DATA);
|
||||||
|
if (err != (Send)0) {
|
||||||
|
*data_index = i;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (Send)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every field above is on the wire, and has reported its own outcome, by the
|
||||||
|
// time it returns: nothing is left to wait out or attribute.
|
||||||
|
Send phy_send_done([[maybe_unused]] uint8_t *data_index) { return (Send)0; }
|
||||||
|
|
||||||
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
|
/* Disable non-read related interrupts (USART RX, RTC status tick, mic timer)
|
||||||
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
|
during AVCLAN bus transactions so framing isn't disturbed. TCB0 must remain
|
||||||
enabled. */
|
enabled. */
|
||||||
@@ -478,7 +635,26 @@ void phy_guard_leave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(NDEBUG) && defined(MEASURE_BUS)
|
#ifndef NDEBUG
|
||||||
|
|
||||||
|
// Hold the bus at a given level.
|
||||||
|
// Overrides and restores mute state upon release (i.e. set recessive).
|
||||||
|
static void phy_set_state(bool dominant) {
|
||||||
|
static bool saved_mute = false;
|
||||||
|
if (dominant) {
|
||||||
|
saved_mute = phy_is_muted();
|
||||||
|
AVCLAN_setBusDriven();
|
||||||
|
phy_mute(false);
|
||||||
|
} else {
|
||||||
|
AVCLAN_setBusIdle();
|
||||||
|
phy_mute(saved_mute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void phy_set_dominant(void) { phy_set_state(true); }
|
||||||
|
void phy_set_recessive(void) { phy_set_state(false); }
|
||||||
|
|
||||||
|
#ifdef MEASURE_BUS
|
||||||
#include <stdio.h> // phy_measure() reporting (debug builds only)
|
#include <stdio.h> // phy_measure() reporting (debug builds only)
|
||||||
|
|
||||||
// Only used immediately below
|
// Only used immediately below
|
||||||
@@ -516,4 +692,6 @@ void phy_measure() {
|
|||||||
|
|
||||||
phy_guard_leave();
|
phy_guard_leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
pico_sdk_init()
|
||||||
|
|
||||||
|
target_sources(avclan PRIVATE
|
||||||
|
phy.cc
|
||||||
|
media.cc
|
||||||
|
cd_timer.cc
|
||||||
|
board.cc
|
||||||
|
stdio.cc
|
||||||
|
)
|
||||||
|
pico_generate_pio_header(avclan ${CMAKE_CURRENT_LIST_DIR}/iebus.pio)
|
||||||
|
|
||||||
|
target_link_libraries(avclan PUBLIC
|
||||||
|
pico_stdlib pico_stdio_usb hardware_pio hardware_dma pico_multicore
|
||||||
|
)
|
||||||
|
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_pico` 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_pico
|
||||||
|
${avclan-mockingboard_SOURCE_DIR}/src/sniffer.cc
|
||||||
|
)
|
||||||
|
target_link_libraries(mockingboard_pico 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_pico
|
||||||
|
PROPERTIES COMPILE_OPTIONS "-w")
|
||||||
|
unset(_tinyusb_sources)
|
||||||
|
|
||||||
|
pico_add_extra_outputs(mockingboard_pico)
|
||||||
|
pico_set_float_implementation(mockingboard_pico none)
|
||||||
|
pico_set_double_implementation(mockingboard_pico none)
|
||||||
|
|
||||||
|
pico_set_program_name(mockingboard_pico mockingboard)
|
||||||
|
pico_set_program_description(mockingboard_pico
|
||||||
|
"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_pico
|
||||||
|
"${avclan-mockingboard_VERSION}")
|
||||||
|
pico_set_program_url(mockingboard_pico "https://github.com/halleysfifthinc/AVCLAN-Mockingboard")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#include "hal/board.h"
|
||||||
|
|
||||||
|
extern "C" void board_init() {}
|
||||||
|
|
||||||
|
extern "C" void board_enable_interrupts() {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#include "hal/cd_timer.h"
|
||||||
|
|
||||||
|
extern "C" void cdtimer_init(void *ptr, void(clbk)(void *),
|
||||||
|
bool(isplay)(void *)) {}
|
||||||
|
|
||||||
|
extern "C" void cdtimer_reset() {}
|
||||||
|
|
||||||
|
extern "C" void cdtimer_restore() {}
|
||||||
|
|
||||||
|
extern "C" void cdtimer_disable() {}
|
||||||
|
|
||||||
|
volatile bool cdtimer_pending_flag;
|
||||||
@@ -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 | 1 bit | 1 bit | 26 - N bits |
|
||||||
|
; | Length (N) | Data+P | has ACK | NAK ok | padding |
|
||||||
|
;
|
||||||
|
; Parity is generated by the driver and appended as the last (least significant)
|
||||||
|
; bit of the data field
|
||||||
|
|
||||||
|
.in 1 left
|
||||||
|
.out 1 left
|
||||||
|
|
||||||
|
.define public nak_irq 7
|
||||||
|
.define public lost_arb_irq 6
|
||||||
|
|
||||||
|
;;; 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 [12] ; 15 cycles from the release to `set pins, 0` below
|
||||||
|
; (~AVCLAN_BIT0_LOGIC_1)
|
||||||
|
|
||||||
|
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 x, 1
|
||||||
|
jmp !x reset ; Fall through to read ACK if "has ACK" is set
|
||||||
|
|
||||||
|
;;; 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:
|
||||||
|
;;; The recessive ("prep") period in the send_bit loop is 2 instr's shorter than
|
||||||
|
; the non-"has ACK" branch and
|
||||||
|
jmp send_bit [3]
|
||||||
|
|
||||||
|
;;; 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,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 true; }
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,885 @@
|
|||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <hardware/clocks.h>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "avclan.h"
|
||||||
|
#include "hal/phy.h"
|
||||||
|
#include "hardware/gpio.h"
|
||||||
|
#include "hardware/pio.h"
|
||||||
|
#include "iebus.pio.h"
|
||||||
|
|
||||||
|
#define TICK_US 1000000
|
||||||
|
#include "timing.h"
|
||||||
|
|
||||||
|
using namespace avclan;
|
||||||
|
using enum detail::Error::Read;
|
||||||
|
using enum detail::Error::Send;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int IEBUS_TX = 16;
|
||||||
|
constexpr int IEBUS_RX = 17;
|
||||||
|
|
||||||
|
// Activity indicators: each follows its bus pin exactly, so an LED wired
|
||||||
|
// supply -> resistor -> pin is lit while that line is dominant (low). The bus
|
||||||
|
// pins themselves stay unloaded.
|
||||||
|
constexpr int LED_RX = 8;
|
||||||
|
constexpr int LED_TX = 26;
|
||||||
|
|
||||||
|
constexpr uint32_t parity(uint32_t val) {
|
||||||
|
static_assert(sizeof(uint32_t) == sizeof(unsigned int));
|
||||||
|
if consteval {
|
||||||
|
int N = 0;
|
||||||
|
for (; val != 0UL; N++) {
|
||||||
|
val = val & (val - 1);
|
||||||
|
}
|
||||||
|
return (N & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return __builtin_parity(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CYCLES_PER_READBIT_PERIOD PIO cycles should take ~AVCLAN_READBIT_THRESHOLD
|
||||||
|
// μs. All three bus SMs share it: iebus_rx and iebus_ack are cross-PIO IRQ
|
||||||
|
// partners, which the datasheet (S11.4) requires to have equal dividers,
|
||||||
|
// synchronised by a single CTRL write -- see the enable in Phy::init.
|
||||||
|
inline float bus_clkdiv() {
|
||||||
|
return clock_get_hz(clk_sys) /
|
||||||
|
(CYCLES_PER_READBIT_PERIOD / (float)AVCLAN_READBIT_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reader runs from the PIO's RX-FIFO interrupt rather than from the calling
|
||||||
|
// thread, so slots are drained and the next script queued no matter what the
|
||||||
|
// main loop is doing. That is not polish: the RX FIFO is 4 deep and printf over
|
||||||
|
// USB CDC blocks for milliseconds, which is exactly the failure rx_log_push
|
||||||
|
// exists to dodge. It is also what lets a frame we lost arbitration to still
|
||||||
|
// arrive -- the engine never stopped receiving.
|
||||||
|
//
|
||||||
|
// The frames it completes are handed out whole; hal/phy.h's read calls pick
|
||||||
|
// their field out of one.
|
||||||
|
class IEBusRx {
|
||||||
|
// Field widths, in the SM's "data bits" terms. The broadcast bit carries no
|
||||||
|
// parity, so it is the one field read as a bare bit (count 0 => 1 bit).
|
||||||
|
static constexpr uint8_t W_BROADCAST = 0;
|
||||||
|
static constexpr uint8_t W_ADDR = 12;
|
||||||
|
static constexpr uint8_t W_CONTROL = 4;
|
||||||
|
static constexpr uint8_t W_BYTE = 8;
|
||||||
|
|
||||||
|
enum class RxField : uint8_t {
|
||||||
|
Broadcast,
|
||||||
|
Controller,
|
||||||
|
Peripheral,
|
||||||
|
Control,
|
||||||
|
Length,
|
||||||
|
Data,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Must be a power of two (the index wrap is a mask).
|
||||||
|
static constexpr uint32_t RXQ_N = 4;
|
||||||
|
static_assert((RXQ_N & (RXQ_N - 1)) == 0,
|
||||||
|
"RX buffer size must be a power of 2");
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct RxFrame {
|
||||||
|
uint16_t controller_addr;
|
||||||
|
uint16_t peripheral_addr;
|
||||||
|
uint8_t control;
|
||||||
|
uint8_t length;
|
||||||
|
uint8_t data[32];
|
||||||
|
bool is_unicast;
|
||||||
|
Read err;
|
||||||
|
};
|
||||||
|
|
||||||
|
IEBusRx() = default;
|
||||||
|
IEBusRx(const IEBusRx &) = delete;
|
||||||
|
IEBusRx &operator=(const IEBusRx &) = delete;
|
||||||
|
|
||||||
|
~IEBusRx() {
|
||||||
|
if (instance_ != this)
|
||||||
|
return;
|
||||||
|
irq_set_enabled(irq_, false);
|
||||||
|
irq_remove_handler(irq_, irq_handler);
|
||||||
|
pio_set_irq0_source_enabled(
|
||||||
|
pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), false);
|
||||||
|
pio_sm_set_enabled(pio_, sm_, false);
|
||||||
|
pio_remove_program_and_unclaim_sm(&iebus_rx_program, pio_, sm_, offset_);
|
||||||
|
instance_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims and configures the iebus_rx SM and its interrupt, but doesn't start
|
||||||
|
// the SM.
|
||||||
|
void init(PIO pio, uint pin_rx, uint16_t address) {
|
||||||
|
hard_assert(instance_ == nullptr);
|
||||||
|
instance_ = this;
|
||||||
|
pio_ = pio;
|
||||||
|
pin_ = pin_rx;
|
||||||
|
self_addrp_ = ((uint32_t)address << 1) | parity(address);
|
||||||
|
offset_ = (uint)pio_add_program(pio_, &iebus_rx_program);
|
||||||
|
sm_ = (uint)pio_claim_unused_sm(pio_, true);
|
||||||
|
|
||||||
|
gpio_init(pin_);
|
||||||
|
gpio_set_dir(pin_, false); // CAN/AVCLAN RX; PIO reads it without a claim
|
||||||
|
gpio_set_input_enabled(pin_, true);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio_, sm_, pin_, 1, false);
|
||||||
|
|
||||||
|
pio_sm_config cfg = iebus_rx_program_get_default_config(offset_);
|
||||||
|
sm_config_set_in_pins(&cfg, pin_);
|
||||||
|
sm_config_set_jmp_pin(&cfg, pin_);
|
||||||
|
sm_config_set_clkdiv(&cfg, bus_clkdiv());
|
||||||
|
|
||||||
|
// Entry is `wait_read`, not the program start -- offset 0 is rx_startbit,
|
||||||
|
// which would fall through into the header reads before the driver's own
|
||||||
|
// jmp to it, leaving every later field one read out of step.
|
||||||
|
pio_sm_init(pio_, sm_, offset_ + iebus_rx_wrap_target, &cfg);
|
||||||
|
prepare_ack();
|
||||||
|
begin_frame();
|
||||||
|
|
||||||
|
pio_set_irq0_source_enabled(
|
||||||
|
pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), true);
|
||||||
|
irq_ = (uint)pio_get_irq_num(pio_, 0);
|
||||||
|
irq_set_exclusive_handler(irq_, irq_handler);
|
||||||
|
irq_set_priority(irq_, PICO_HIGHEST_IRQ_PRIORITY);
|
||||||
|
irq_set_enabled(irq_, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
PIO pio() const { return pio_; }
|
||||||
|
uint32_t sm_mask() const { return 1U << sm_; }
|
||||||
|
|
||||||
|
// Muted means we still listen, we just don't answer: no ack, ever. Disarming
|
||||||
|
// the latch is what makes that true of the SM as well.
|
||||||
|
void mute(bool mute) {
|
||||||
|
muted_ = mute;
|
||||||
|
sync_ack_arming();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deafen(bool deaf) {
|
||||||
|
deafened_ = deaf;
|
||||||
|
sync_ack_arming();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disarm RX ACK'ing behavior; called prior to frame TX to prevent
|
||||||
|
// self-ACK'ing.
|
||||||
|
void disarm_ack() {
|
||||||
|
transmitting_ = true;
|
||||||
|
sync_ack_arming();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rearm RX ACK'ing behavior; called once our frame is over or lost.
|
||||||
|
void rearm_ack() {
|
||||||
|
transmitting_ = false;
|
||||||
|
sync_ack_arming();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffered frames exist to be read
|
||||||
|
bool frame_pending() const { return rxq_tail_ != rxq_head_; }
|
||||||
|
|
||||||
|
// Only valid while frame_pending().
|
||||||
|
const RxFrame &frame() const { return rxq_[rxq_tail_]; }
|
||||||
|
|
||||||
|
void release() { rxq_tail_ = (rxq_tail_ + 1) & (RXQ_N - 1); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// True when publish() would have nowhere to put the frame being received.
|
||||||
|
bool queue_full() const {
|
||||||
|
return ((rxq_head_ + 1) & (RXQ_N - 1)) == rxq_tail_;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __time_critical_func(irq_handler)() { instance_->isr(); }
|
||||||
|
|
||||||
|
void __time_critical_func(isr)() {
|
||||||
|
// Every field after the controller address is followed by an ack slot,
|
||||||
|
// which the SM consumes whether or not it drives it.
|
||||||
|
const auto next_after_ack = [this](RxField next, uint8_t bits) {
|
||||||
|
state_ = next;
|
||||||
|
enqueue_rx(bits, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
while (!pio_sm_is_rx_fifo_empty(pio_, sm_)) {
|
||||||
|
const auto slot = (uint16_t)pio_sm_get(pio_, sm_);
|
||||||
|
uint16_t value = 0;
|
||||||
|
|
||||||
|
switch (state_) {
|
||||||
|
case RxField::Broadcast:
|
||||||
|
building_.is_unicast = (slot & 1U) != 0U;
|
||||||
|
state_ = RxField::Controller;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RxField::Controller:
|
||||||
|
// Judged on the peripheral slot: that read is already running, and a
|
||||||
|
// restart now would take its slot for the next frame's broadcast bit.
|
||||||
|
controller_ok_ = update_value<W_ADDR>(slot, &value);
|
||||||
|
building_.controller_addr = value;
|
||||||
|
state_ = RxField::Peripheral;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RxField::Peripheral: {
|
||||||
|
// Past arbitration our own frame is the tx SM's business: it reads
|
||||||
|
// its own acks, and a copy here would only cost a queue slot. A frame
|
||||||
|
// we owe an ack for is refused when there is no room for it -- the
|
||||||
|
// NAK asks the sender to send it again, rather than losing it behind
|
||||||
|
// an ack we can't honour. Both come before the field checks: a frame
|
||||||
|
// being given up needs no parity verdict, and reporting one would
|
||||||
|
// name the wrong cause for the same NAK.
|
||||||
|
const bool ours =
|
||||||
|
building_.controller_addr == (uint16_t)(self_addrp_ >> 1);
|
||||||
|
const bool refuse =
|
||||||
|
!ours && queue_full() && pio_interrupt_get(pio_, ack_latch);
|
||||||
|
|
||||||
|
if (ours || refuse) {
|
||||||
|
if (refuse)
|
||||||
|
rxq_refused_ = rxq_refused_ + 1;
|
||||||
|
// Withdrawing the ack is part of giving the frame up; its slot is
|
||||||
|
// still ahead of the SM. Re-dispatching the start-bit block hands
|
||||||
|
// the rest of the frame back to it: those bits are far too short to
|
||||||
|
// read as a start bit, so it re-syncs on the next real one by
|
||||||
|
// itself.
|
||||||
|
pio_interrupt_clear(pio_, ack_latch);
|
||||||
|
begin_frame();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!controller_ok_) {
|
||||||
|
publish(BAD_CONTROLLER_PARITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!update_value<W_ADDR>(slot, &value)) {
|
||||||
|
publish(BAD_PERIPHERAL_PARITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
building_.peripheral_addr = value;
|
||||||
|
next_after_ack(RxField::Control, W_CONTROL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RxField::Control:
|
||||||
|
if (!update_value<W_CONTROL>(slot, &value)) {
|
||||||
|
publish(BAD_CONTROL_PARITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
building_.control = (uint8_t)value;
|
||||||
|
next_after_ack(RxField::Length, W_BYTE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RxField::Length:
|
||||||
|
if (!update_value<W_BYTE>(slot, &value)) {
|
||||||
|
publish(BAD_LENGTH_PARITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
building_.length = (uint8_t)value;
|
||||||
|
if (value == 0 || value > sizeof(building_.data)) {
|
||||||
|
publish(BAD_LENGTH_RANGE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
next_after_ack(RxField::Data, W_BYTE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RxField::Data:
|
||||||
|
if (!update_value<W_BYTE>(slot, &value)) {
|
||||||
|
publish(BAD_DATA_PARITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
building_.data[data_i_++] = (uint8_t)value;
|
||||||
|
if (data_i_ >= building_.length)
|
||||||
|
publish(Read{0});
|
||||||
|
else
|
||||||
|
next_after_ack(RxField::Data, W_BYTE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start (or restart) a frame. The start-bit block is dispatched like any
|
||||||
|
// other field, except the streamed instruction is a jmp instead of a read's
|
||||||
|
// setup. It only falls through on a start bit, so the header's reads queue
|
||||||
|
// right behind it. Four words fill the FIFO, which is empty here: the SM
|
||||||
|
// takes each word before pushing the slot that ends a frame.
|
||||||
|
void __time_critical_func(begin_frame)() {
|
||||||
|
state_ = RxField::Broadcast;
|
||||||
|
building_ = {};
|
||||||
|
data_i_ = 0;
|
||||||
|
pio_sm_put(pio_, sm_,
|
||||||
|
pio_encode_jmp(offset_ + iebus_rx_offset_rx_startbit));
|
||||||
|
enqueue_rx(W_BROADCAST, false);
|
||||||
|
enqueue_rx(W_ADDR, false);
|
||||||
|
enqueue_rx(W_ADDR, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load our address (with parity) into the SM's Y register.
|
||||||
|
// The SM compares read values to Y to set the ack latch and trigger ACK'ing.
|
||||||
|
// SM must be stopped with an empty FIFO.
|
||||||
|
void prepare_ack() {
|
||||||
|
pio_sm_put(pio_, sm_, self_addrp_);
|
||||||
|
pio_sm_exec(pio_, sm_, pio_encode_pull(false, true));
|
||||||
|
pio_sm_exec(pio_, sm_, pio_encode_out(pio_y, 32));
|
||||||
|
ack_armed_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bring the latch's arming in line with the reasons to withhold an ack.
|
||||||
|
// Disarming inverts the Y register (self address + parity), which leaves the
|
||||||
|
// upper 19 bits set and so prevents any read from matching.
|
||||||
|
void sync_ack_arming() {
|
||||||
|
const bool arm = !muted_ && !deafened_ && !transmitting_;
|
||||||
|
if (arm == ack_armed_)
|
||||||
|
return;
|
||||||
|
pio_sm_exec_wait_blocking(pio_, sm_, pio_encode_mov_not(pio_y, pio_y));
|
||||||
|
ack_armed_ = arm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One read's script word: [has_ack 31][zeros 30:16][instr 15:0]. The SM reads
|
||||||
|
// bits + 1 (the field plus its parity); the instruction zeroes its parity
|
||||||
|
// accumulator and leaves that many bits before the OSR threshold. `bits` must
|
||||||
|
// be <= 13: at 14 the instruction would encode as `out x, 0`, which the ISA
|
||||||
|
// reads as 32. `has_ack` has the SM consume the ack slot after the field.
|
||||||
|
static constexpr uint32_t encode_rx(uint8_t bits, bool has_ack) {
|
||||||
|
return ((uint32_t)has_ack << 31) | pio_encode_out(pio_x, 14 - bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update `*value` with the `read` value. Returns true for a correct parity.
|
||||||
|
template <auto N> static bool update_value(uint16_t read, uint16_t *value) {
|
||||||
|
*value = (uint16_t)((read >> 1) & ((1U << N) - 1U));
|
||||||
|
return parity(*value) == (unsigned)(read & 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void __time_critical_func(enqueue_rx)(uint8_t bits, bool has_ack) {
|
||||||
|
pio_sm_put_blocking(pio_, sm_, encode_rx(bits, has_ack));
|
||||||
|
}
|
||||||
|
|
||||||
|
// New frames are dropped if the rx queue is full. A frame we acked is not
|
||||||
|
// among them: there was room for it at the peripheral slot, and from here the
|
||||||
|
// queue only gains space. What is left to lose is what no ack was owed for.
|
||||||
|
void __time_critical_func(publish)(Read err) {
|
||||||
|
// The SM NAKs bad parity itself; this vetoes what only we can judge
|
||||||
|
// (controller parity, length range) before its late read of the latch.
|
||||||
|
if (err != Read{0})
|
||||||
|
pio_interrupt_clear(pio_, ack_latch);
|
||||||
|
building_.err = err;
|
||||||
|
if (queue_full()) {
|
||||||
|
rxq_drops_ = rxq_drops_ + 1; // Never stall the bus for a lagging reader
|
||||||
|
} else {
|
||||||
|
rxq_[rxq_head_] = building_;
|
||||||
|
rxq_head_ = (rxq_head_ + 1) & (RXQ_N - 1);
|
||||||
|
}
|
||||||
|
begin_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The initialized instance, for irq_handler: SDK IRQ handlers take no
|
||||||
|
// context.
|
||||||
|
static inline IEBusRx *instance_ = nullptr;
|
||||||
|
|
||||||
|
PIO pio_;
|
||||||
|
uint sm_;
|
||||||
|
uint offset_;
|
||||||
|
uint pin_;
|
||||||
|
uint irq_;
|
||||||
|
|
||||||
|
// Our address as the rx SM sees it: a slot is the field plus its parity bit,
|
||||||
|
// and parity is a function of the field, so there is exactly one legal slot
|
||||||
|
// value for us.
|
||||||
|
uint32_t self_addrp_;
|
||||||
|
|
||||||
|
bool ack_armed_ = false; // hardware: y holds self_addrp_, or its complement
|
||||||
|
bool muted_ = false; // we don't answer on the bus
|
||||||
|
bool deafened_ = false; // we don't answer, but may still transmit
|
||||||
|
bool transmitting_ = false; // our own frame is on the wire
|
||||||
|
|
||||||
|
std::array<RxFrame, RXQ_N> rxq_ = {};
|
||||||
|
volatile uint32_t rxq_head_ = 0; // written by the ISR only
|
||||||
|
volatile uint32_t rxq_tail_ = 0; // written by the reader only
|
||||||
|
// Lost: no ack was owed, so there was nothing to refuse -- a broadcast, or a
|
||||||
|
// unicast addressed elsewhere.
|
||||||
|
volatile uint32_t rxq_drops_ = 0;
|
||||||
|
// NAK'd for want of room; the sender still owns the frame and sends it again.
|
||||||
|
volatile uint32_t rxq_refused_ = 0;
|
||||||
|
|
||||||
|
RxFrame building_ = {};
|
||||||
|
RxField state_ = RxField::Broadcast;
|
||||||
|
uint8_t data_i_ = 0;
|
||||||
|
bool controller_ok_ = false; // parity verdict, held until the peripheral slot
|
||||||
|
};
|
||||||
|
|
||||||
|
// The transmit engine: the iebus_tx SM, plus the ack SM that drives the ack
|
||||||
|
// slot on the rx engine's behalf. Both run iebus_tx, sharing its ack-bit drive
|
||||||
|
// and the TX pin.
|
||||||
|
//
|
||||||
|
// Every field, arbitration included, is queued, not sent, so the fields report
|
||||||
|
// nothing and the frame's verdict comes from send_done.
|
||||||
|
class IEBusTx {
|
||||||
|
// Word positions in a frame
|
||||||
|
static constexpr uint8_t WORD_PERIPHERAL = 2; // After broadcast, controller
|
||||||
|
static constexpr uint8_t WORD_CONTROL = 3;
|
||||||
|
static constexpr uint8_t WORD_LENGTH = 4;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// The ack match lives in the rx SM but exists for our sake: it has to be
|
||||||
|
// parked while our own address is on the wire, and put back the moment we
|
||||||
|
// stop transmitting -- including when we lose arbitration mid-field.
|
||||||
|
explicit IEBusTx(IEBusRx &rx) : rx_(rx) {}
|
||||||
|
IEBusTx(const IEBusTx &) = delete;
|
||||||
|
IEBusTx &operator=(const IEBusTx &) = delete;
|
||||||
|
|
||||||
|
// Hands both SMs and their program memory back. Stopping them leaves the pad
|
||||||
|
// at its last driven level, which is recessive -- the same property mute
|
||||||
|
// relies on. The pad keeps its PIO function select: handing it back to SIO
|
||||||
|
// would float the line, and a floating bus reads dominant.
|
||||||
|
~IEBusTx() {
|
||||||
|
if (!claimed_)
|
||||||
|
return;
|
||||||
|
pio_set_sm_mask_enabled(pio_, sm_mask(), false);
|
||||||
|
pio_remove_program_and_unclaim_sm(&iebus_tx_program, pio_, tx_sm_,
|
||||||
|
tx_offset_);
|
||||||
|
pio_sm_unclaim(pio_, ack_sm_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims and configures both SMs, but doesn't start them.
|
||||||
|
void init(PIO pio, uint pin_rx, uint pin_tx) {
|
||||||
|
pio_ = pio;
|
||||||
|
pin_tx_ = pin_tx;
|
||||||
|
tx_offset_ = (uint)pio_add_program(pio_, &iebus_tx_program);
|
||||||
|
tx_sm_ = (uint)pio_claim_unused_sm(pio_, true);
|
||||||
|
ack_sm_ = (uint)pio_claim_unused_sm(pio_, true);
|
||||||
|
|
||||||
|
// Drive the pad recessive *before* handing its function select to this PIO,
|
||||||
|
// so the handover cannot glitch the bus dominant. Level and direction are
|
||||||
|
// block-wide registers, so setting them through one SM covers both.
|
||||||
|
pio_sm_set_pins_with_mask(pio_, tx_sm_, 1U << pin_tx, 1U << pin_tx);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio_, tx_sm_, pin_tx, 1, true);
|
||||||
|
pio_gpio_init(pio_, pin_tx);
|
||||||
|
|
||||||
|
pio_sm_config tx_cfg = iebus_tx_program_get_default_config(tx_offset_);
|
||||||
|
sm_config_set_out_pins(&tx_cfg, pin_tx, 1);
|
||||||
|
sm_config_set_set_pins(&tx_cfg, pin_tx, 1);
|
||||||
|
// Both the bit value and the arbitration check come from the readback.
|
||||||
|
sm_config_set_jmp_pin(&tx_cfg, pin_rx);
|
||||||
|
// The start bit's trailing-edge `wait` indexes the IN mapping, not JMP_PIN.
|
||||||
|
sm_config_set_in_pins(&tx_cfg, pin_rx);
|
||||||
|
sm_config_set_clkdiv(&tx_cfg, bus_clkdiv());
|
||||||
|
|
||||||
|
// Entry is `reset`; offset 0 is the start-bit block.
|
||||||
|
pio_sm_init(pio_, tx_sm_, tx_offset_ + iebus_tx_wrap_target, &tx_cfg);
|
||||||
|
|
||||||
|
pio_sm_config ack_cfg = iebus_tx_program_get_default_config(tx_offset_);
|
||||||
|
sm_config_set_wrap(&ack_cfg, tx_offset_ + iebus_tx_offset_ack_entry,
|
||||||
|
tx_offset_ + iebus_tx_offset_ack_release);
|
||||||
|
sm_config_set_set_pins(&ack_cfg, pin_tx, 1);
|
||||||
|
sm_config_set_clkdiv(&ack_cfg, bus_clkdiv());
|
||||||
|
|
||||||
|
pio_sm_init(pio_, ack_sm_, tx_offset_ + iebus_tx_offset_ack_entry,
|
||||||
|
&ack_cfg);
|
||||||
|
claimed_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PIO pio() const { return pio_; }
|
||||||
|
uint32_t sm_mask() const { return (1U << tx_sm_) | (1U << ack_sm_); }
|
||||||
|
|
||||||
|
// Must only be called between transactions to ensure the TX pin is left
|
||||||
|
// high/recessive.
|
||||||
|
void mute(bool mute) {
|
||||||
|
pio_set_sm_mask_enabled(pio_, sm_mask(), !mute);
|
||||||
|
muted_ = mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_muted() const { return muted_; }
|
||||||
|
|
||||||
|
// Hold the bus at a given level.
|
||||||
|
// Overrides and restores mute state upon release (i.e. set recessive).
|
||||||
|
void set_state(bool dominant) {
|
||||||
|
static bool saved_mute = false;
|
||||||
|
if (dominant) {
|
||||||
|
saved_mute = muted_;
|
||||||
|
mute(true);
|
||||||
|
pio_sm_set_pins_with_mask(pio_, tx_sm_, 0U, 1U << pin_tx_);
|
||||||
|
} else {
|
||||||
|
pio_sm_set_pins_with_mask(pio_, tx_sm_, 1U << pin_tx_, 1U << pin_tx_);
|
||||||
|
// The ack SM parks on `wait 1 irq`, which consumes a flag raised while it
|
||||||
|
// was down and drives the slot immediately -- Phy::mute's ordering.
|
||||||
|
pio_interrupt_clear(pio_, ack_irq);
|
||||||
|
mute(saved_mute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send start and broadcast bits.
|
||||||
|
Send send_header(bool is_unicast) {
|
||||||
|
if (muted_)
|
||||||
|
return MUTED;
|
||||||
|
|
||||||
|
rx_.disarm_ack();
|
||||||
|
words_ = 0;
|
||||||
|
data_start_ = std::numeric_limits<uint8_t>::max();
|
||||||
|
|
||||||
|
// Jump to the tx_startbit section from the default "reset" stall on pull,
|
||||||
|
// with the start-bit block's sample counts loaded
|
||||||
|
pio_sm_exec_wait_blocking(pio_, tx_sm_,
|
||||||
|
pio_encode_set(pio_x, iebus_tx_JOIN_SAMPLES));
|
||||||
|
pio_sm_exec_wait_blocking(pio_, tx_sm_,
|
||||||
|
pio_encode_set(pio_y, iebus_tx_IDLE_SAMPLES));
|
||||||
|
pio_sm_exec_wait_blocking(
|
||||||
|
pio_, tx_sm_, pio_encode_jmp(tx_offset_ + iebus_tx_offset_tx_startbit));
|
||||||
|
|
||||||
|
// The broadcast bit carries no parity, so it is sent as a bare bit: length
|
||||||
|
// 0 (one bit) with the parity slot standing in for the bit itself.
|
||||||
|
return send_field(0, is_unicast ? 1U : 0U, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A field is only queued, and its outcome left to send_done. Once one has
|
||||||
|
// failed, the rest of the frame is dropped.
|
||||||
|
Send send_field(size_t len, uint32_t bits, bool has_ack, bool expect_ack) {
|
||||||
|
if (muted_)
|
||||||
|
return MUTED;
|
||||||
|
if (!check())
|
||||||
|
put(encode_tx(len, (uint16_t)bits, has_ack, expect_ack));
|
||||||
|
return Send{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
Send send_data(const uint8_t *data, uint8_t length, bool expect_ack) {
|
||||||
|
if (muted_)
|
||||||
|
return MUTED;
|
||||||
|
data_start_ = words_;
|
||||||
|
for (uint8_t i = 0; i < length && !check(); i++)
|
||||||
|
put(encode_tx(8, data[i], true, expect_ack));
|
||||||
|
return Send{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
Send send_done(uint8_t *data_index) {
|
||||||
|
const auto flagged = [this] {
|
||||||
|
return pio_interrupt_get(pio_, iebus_tx_lost_arb_irq) ||
|
||||||
|
pio_interrupt_get(pio_, iebus_tx_nak_irq);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The second PC test confirms that SM has finished sending last value and
|
||||||
|
// not in-progress (i.e. pulled the last FIFO value and still mid-send)
|
||||||
|
const auto idle = [this] {
|
||||||
|
return pio_sm_is_tx_fifo_empty(pio_, tx_sm_) && // Read before the pc
|
||||||
|
pio_sm_get_pc(pio_, tx_sm_) == tx_offset_ + iebus_tx_wrap_target;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (!(flagged() || idle()))
|
||||||
|
tight_loop_contents();
|
||||||
|
|
||||||
|
check();
|
||||||
|
// Our frame is over, won or not.
|
||||||
|
rx_.rearm_ack();
|
||||||
|
words_ = 0;
|
||||||
|
const Send err = fault_;
|
||||||
|
fault_ = Send{0};
|
||||||
|
if (err == NAK_DATA)
|
||||||
|
*data_index = (uint8_t)(failed_word_ - data_start_);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// One field's word: [len:4][data+parity:len+1][has_ack:1][nak_ok:1][pad]. The
|
||||||
|
// SM sends count + 1 bits, so the count is `len` and the parity bit the
|
||||||
|
// driver appends rides along as the extra one. `has_ack` emits the
|
||||||
|
// acknowledge slot; a NAK in it raises nak_irq unless `nak_ok`, i.e. unless
|
||||||
|
// we don't `expect_ack`.
|
||||||
|
static constexpr uint32_t encode_tx(size_t len, uint16_t bits, bool has_ack,
|
||||||
|
bool expect_ack) {
|
||||||
|
const auto n = (uint8_t)(len + 1);
|
||||||
|
// Masked so a bit above the field (the broadcast bit's copy of itself, when
|
||||||
|
// len is 0) can't spill into the count.
|
||||||
|
const uint16_t with_parity =
|
||||||
|
((bits << 1) | parity(bits)) & ((1U << n) - 1U);
|
||||||
|
uint32_t word = (uint32_t)len << 28;
|
||||||
|
word |= with_parity << (28 - n);
|
||||||
|
word |= (uint32_t)has_ack << (27 - n);
|
||||||
|
word |= (uint32_t)!expect_ack << (26 - n);
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect a flag if the SM has raised one, without waiting. Returns whether
|
||||||
|
// the frame has failed, now or earlier.
|
||||||
|
bool check() {
|
||||||
|
const bool lost_arb = pio_interrupt_get(pio_, iebus_tx_lost_arb_irq);
|
||||||
|
if (!lost_arb && !pio_interrupt_get(pio_, iebus_tx_nak_irq))
|
||||||
|
return fault_ != Send{0};
|
||||||
|
|
||||||
|
// Read the level before the clear throws it away, and clear before the
|
||||||
|
// flag: releasing the SM with words still queued would send the rest of the
|
||||||
|
// frame.
|
||||||
|
const auto pulled =
|
||||||
|
(uint8_t)(words_ - pio_sm_get_tx_fifo_level(pio_, tx_sm_));
|
||||||
|
failed_word_ = (uint8_t)(pulled - 1U);
|
||||||
|
pio_sm_clear_fifos(pio_, tx_sm_);
|
||||||
|
|
||||||
|
if (lost_arb) {
|
||||||
|
pio_interrupt_clear(pio_, iebus_tx_lost_arb_irq);
|
||||||
|
// The other frame is still arriving; put the ack match back so we can
|
||||||
|
// answer it if it turns out to be addressed to us.
|
||||||
|
rx_.rearm_ack();
|
||||||
|
// A bid lost before any word was pulled: the start-bit block saw the bus
|
||||||
|
// dominant with something other than a start bit.
|
||||||
|
if (pulled == 0)
|
||||||
|
fault_ = BUSY;
|
||||||
|
else if (failed_word_ < WORD_PERIPHERAL && failed_word_ < data_start_)
|
||||||
|
fault_ = LOST_ARBITRATION;
|
||||||
|
else
|
||||||
|
fault_ = CONTENDED_BUS;
|
||||||
|
} else {
|
||||||
|
pio_interrupt_clear(pio_, iebus_tx_nak_irq);
|
||||||
|
if (failed_word_ >= data_start_)
|
||||||
|
fault_ = NAK_DATA;
|
||||||
|
else if (failed_word_ == WORD_PERIPHERAL)
|
||||||
|
fault_ = NAK_ADDRESS;
|
||||||
|
else if (failed_word_ == WORD_CONTROL)
|
||||||
|
fault_ = NAK_CONTROL;
|
||||||
|
else if (failed_word_ == WORD_LENGTH)
|
||||||
|
fault_ = NAK_MESSAGE_LENGTH;
|
||||||
|
else
|
||||||
|
fault_ = NAK;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only a full FIFO waits, and not on a parked SM: it never frees a slot,
|
||||||
|
// which is why this isn't pio_sm_put_blocking.
|
||||||
|
void put(uint32_t word) {
|
||||||
|
while (pio_sm_is_tx_fifo_full(pio_, tx_sm_)) {
|
||||||
|
if (check())
|
||||||
|
return;
|
||||||
|
tight_loop_contents();
|
||||||
|
}
|
||||||
|
pio_sm_put(pio_, tx_sm_, word);
|
||||||
|
words_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEBusRx &rx_;
|
||||||
|
|
||||||
|
PIO pio_;
|
||||||
|
uint pin_tx_;
|
||||||
|
uint tx_sm_;
|
||||||
|
uint tx_offset_;
|
||||||
|
uint ack_sm_;
|
||||||
|
bool claimed_ = false; // init() ran, so the destructor has something to undo
|
||||||
|
|
||||||
|
bool muted_ = false;
|
||||||
|
|
||||||
|
// Words put since the header, taken by the SM or not. Its flags park it,
|
||||||
|
// which freezes the FIFO, so the word it failed on is the last one it pulled:
|
||||||
|
// words_ - level - 1. Wrapping is harmless; the FIFO holds at most 8.
|
||||||
|
uint8_t words_;
|
||||||
|
uint8_t data_start_; // Position of the first data word
|
||||||
|
uint8_t failed_word_;
|
||||||
|
// Sticky until send_done reports it; nothing more is queued meanwhile.
|
||||||
|
Send fault_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The bus as a whole. Only it can hold the invariants that span the two
|
||||||
|
// engines: they sit on different PIOs and so must be started by one
|
||||||
|
// synchronised CTRL write, and muting has to reach both.
|
||||||
|
class Phy {
|
||||||
|
public:
|
||||||
|
Phy() = default;
|
||||||
|
Phy(const Phy &) = delete;
|
||||||
|
Phy &operator=(const Phy &) = delete;
|
||||||
|
|
||||||
|
~Phy() { activity_leds_deinit(); }
|
||||||
|
|
||||||
|
void init(uint16_t address) {
|
||||||
|
rx_.init(pio0, IEBUS_RX, address);
|
||||||
|
tx_.init(pio1, IEBUS_RX, IEBUS_TX);
|
||||||
|
|
||||||
|
// Cross-PIO IRQ partners must share a clock divider *and* have it restarted
|
||||||
|
// in the same cycle (S11.4). One CTRL write starts all three in step.
|
||||||
|
// Reception runs from here on, independent of the main loop.
|
||||||
|
pio_enable_sm_multi_mask_in_sync(rx_.pio(), 0, rx_.sm_mask(),
|
||||||
|
tx_.sm_mask());
|
||||||
|
|
||||||
|
activity_leds_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Muted" means we still listen but neither transmit nor ACK. Ordered so no
|
||||||
|
// ack request can be stranded across the transition: the rx engine stops
|
||||||
|
// asking before its driver goes away, and any flag it did leave behind is
|
||||||
|
// dropped before that driver comes back -- the iebus_ack SM's `wait 1 irq`
|
||||||
|
// clears an already-set flag and drives the slot immediately.
|
||||||
|
void mute(bool mute) {
|
||||||
|
muted_ = mute;
|
||||||
|
if (mute) {
|
||||||
|
rx_.mute(true);
|
||||||
|
tx_.mute(true);
|
||||||
|
} else {
|
||||||
|
pio_interrupt_clear(tx_.pio(), ack_irq);
|
||||||
|
tx_.mute(false);
|
||||||
|
rx_.mute(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_muted() const { return muted_; }
|
||||||
|
|
||||||
|
IEBusRx &rx() { return rx_; }
|
||||||
|
IEBusTx &tx() { return tx_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Point one mirror SM at one src -> dst pair. Only the destination gets
|
||||||
|
// `pio_gpio_init`: taking the function select of a source pin would hand it
|
||||||
|
// to this PIO block and cut whoever actually drives it (IEBUS_TX) loose.
|
||||||
|
// Reading needs nothing but the pad's input buffer, which is on for both bus
|
||||||
|
// pins already -- asserted here so the mirror cannot go dark if that changes.
|
||||||
|
void pin_mirror_sm_init(uint sm, uint offset, uint src, uint dst) {
|
||||||
|
PIO pio = pio2;
|
||||||
|
gpio_set_input_enabled(src, true);
|
||||||
|
pio_gpio_init(pio, dst);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio, sm, dst, 1, true);
|
||||||
|
|
||||||
|
pio_sm_config cfg = pin_mirror_program_get_default_config(offset);
|
||||||
|
sm_config_set_in_pins(&cfg, src);
|
||||||
|
sm_config_set_out_pins(&cfg, dst, 1);
|
||||||
|
// Default clkdiv: one copy per system clock, so the LED tracks the line far
|
||||||
|
// faster than a bit period.
|
||||||
|
|
||||||
|
pio_sm_init(pio, sm, offset, &cfg);
|
||||||
|
pio_sm_set_enabled(pio, sm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize hardware to display bus TX/RX activity on two LEDs
|
||||||
|
// The bus idles HIGH, so the PIO program mirrors inverted pin state from the
|
||||||
|
// IEBUS TX/RX pins to the LED pins so that dominant bus activity (i.e. LOW
|
||||||
|
// state for IEBUS TX/RX pins) lights the respective LED
|
||||||
|
void activity_leds_init() {
|
||||||
|
PIO pio = pio2; // iebus_rx and iebus_tx each fill a block
|
||||||
|
// Indicators are cosmetic; never fail the bus bring-up for them.
|
||||||
|
if (!pio_can_add_program(pio, &pin_mirror_program))
|
||||||
|
return;
|
||||||
|
const int msm_rx = pio_claim_unused_sm(pio, false);
|
||||||
|
if (msm_rx < 0)
|
||||||
|
return;
|
||||||
|
const int msm_tx = pio_claim_unused_sm(pio, false);
|
||||||
|
if (msm_tx < 0) {
|
||||||
|
pio_sm_unclaim(pio, (uint)msm_rx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
led_offset_ = (uint)pio_add_program(pio, &pin_mirror_program);
|
||||||
|
led_sm_rx_ = (uint)msm_rx;
|
||||||
|
led_sm_tx_ = (uint)msm_tx;
|
||||||
|
leds_claimed_ = true;
|
||||||
|
pin_mirror_sm_init(led_sm_rx_, led_offset_, IEBUS_RX, LED_RX);
|
||||||
|
pin_mirror_sm_init(led_sm_tx_, led_offset_, IEBUS_TX, LED_TX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two mirrors share one copy of the program, so the slots go back
|
||||||
|
// individually but the program memory only once -- which is why this isn't
|
||||||
|
// two pio_remove_program_and_unclaim_sm calls.
|
||||||
|
void activity_leds_deinit() {
|
||||||
|
if (!leds_claimed_)
|
||||||
|
return;
|
||||||
|
PIO pio = pio2;
|
||||||
|
pio_sm_set_enabled(pio, led_sm_rx_, false);
|
||||||
|
pio_sm_set_enabled(pio, led_sm_tx_, false);
|
||||||
|
pio_sm_unclaim(pio, led_sm_rx_);
|
||||||
|
pio_sm_unclaim(pio, led_sm_tx_);
|
||||||
|
pio_remove_program(pio, &pin_mirror_program, led_offset_);
|
||||||
|
leds_claimed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEBusRx rx_;
|
||||||
|
IEBusTx tx_{rx_};
|
||||||
|
bool muted_ = false;
|
||||||
|
|
||||||
|
uint led_sm_rx_;
|
||||||
|
uint led_sm_tx_;
|
||||||
|
uint led_offset_;
|
||||||
|
bool leds_claimed_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
Phy phy;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void phy_init(uint16_t address) { phy.init(address); }
|
||||||
|
|
||||||
|
extern "C" void phy_mute(bool mute) { phy.mute(mute); }
|
||||||
|
|
||||||
|
extern "C" bool phy_is_muted() { return phy.is_muted(); }
|
||||||
|
|
||||||
|
extern "C" void phy_deafen(bool deaf) { phy.rx().deafen(deaf); }
|
||||||
|
|
||||||
|
extern "C" bool phy_frame_pending() { return phy.rx().frame_pending(); }
|
||||||
|
|
||||||
|
extern "C" void phy_guard_enter() {}
|
||||||
|
extern "C" void phy_guard_leave() {}
|
||||||
|
|
||||||
|
// --- Reads: served from the buffered frame ----------------------------------
|
||||||
|
//
|
||||||
|
// The engine has already checked every parity bit and driven every ack slot, so
|
||||||
|
// these only hand back fields. The error it recorded is reported by the first
|
||||||
|
// call of the frame; the caller abandons the frame on it, which releases it.
|
||||||
|
|
||||||
|
extern "C" Read phy_read_header(bool *is_unicast) {
|
||||||
|
const IEBusRx::RxFrame &frame = phy.rx().frame();
|
||||||
|
if (frame.err != Read{0}) {
|
||||||
|
phy.rx().release();
|
||||||
|
return frame.err;
|
||||||
|
}
|
||||||
|
*is_unicast = frame.is_unicast;
|
||||||
|
return Read{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Read phy_read_controller_addr(uint16_t *addr) {
|
||||||
|
*addr = phy.rx().frame().controller_addr;
|
||||||
|
return Read{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Read phy_read_peripheral_addr(uint16_t *addr) {
|
||||||
|
*addr = phy.rx().frame().peripheral_addr;
|
||||||
|
return Read{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Read phy_read_control(uint8_t *control) {
|
||||||
|
*control = phy.rx().frame().control;
|
||||||
|
return Read{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Read phy_read_length(uint8_t *length) {
|
||||||
|
*length = phy.rx().frame().length;
|
||||||
|
return Read{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Read phy_read_data(uint8_t *data, uint8_t length,
|
||||||
|
[[maybe_unused]] uint8_t *data_index) {
|
||||||
|
memcpy(data, phy.rx().frame().data, length);
|
||||||
|
phy.rx().release();
|
||||||
|
return Read{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_header(bool is_unicast) {
|
||||||
|
return phy.tx().send_header(is_unicast);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_controller_addr(uint16_t addr) {
|
||||||
|
return phy.tx().send_field(12, addr, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
|
||||||
|
return phy.tx().send_field(12, addr, true, expect_ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_control(uint8_t control, bool expect_ack) {
|
||||||
|
return phy.tx().send_field(4, control, true, expect_ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_length(uint8_t length, bool expect_ack) {
|
||||||
|
return phy.tx().send_field(8, length, true, expect_ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_data(const uint8_t *data, uint8_t length,
|
||||||
|
bool expect_ack,
|
||||||
|
[[maybe_unused]] uint8_t *data_index) {
|
||||||
|
return phy.tx().send_data(data, length, expect_ack);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" Send phy_send_done(uint8_t *data_index) {
|
||||||
|
return phy.tx().send_done(data_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
|
||||||
|
extern "C" void phy_set_dominant() { phy.tx().set_state(true); }
|
||||||
|
extern "C" void phy_set_recessive() { phy.tx().set_state(false); }
|
||||||
|
|
||||||
|
#ifdef MEASURE_BUS
|
||||||
|
// Sample and dump bus bit timing over the serial link (REPL `M`).
|
||||||
|
void phy_measure(void) {};
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "hal/stdio.h"
|
||||||
|
#include "pico/stdio.h"
|
||||||
|
#include "pico/stdio_usb.h"
|
||||||
|
#include "pico/time.h"
|
||||||
|
#include "tusb.h" // IWYU pragma: keep
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void stdio_init() {
|
||||||
|
stdio_usb_init();
|
||||||
|
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 waits forever. EAGAIN rather than
|
||||||
|
// a 0-length read: newlib's refill skips a stream that has ever seen EOF.
|
||||||
|
extern "C" int _read(int handle, char *buffer, int length) {
|
||||||
|
if (handle != STDIN_FILENO) {
|
||||||
|
errno = EBADF;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int count = stdio_get_until(buffer, length, make_timeout_time_us(0));
|
||||||
|
if (count < 0) {
|
||||||
|
errno = EAGAIN;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
+32
-4
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const char *const offon[] = {"OFF", "ON"};
|
const char *const offon[] = {"OFF", "ON"};
|
||||||
|
|
||||||
constexpr uint8_t CACHE_SIZE = 16;
|
constexpr uint8_t CACHE_SIZE = AVCLAN_MSG_QUEUE_SIZE;
|
||||||
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
|
static_assert(CACHE_SIZE >= AVCLAN_FRAME_POOL_N,
|
||||||
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
|
"CACHE_SIZE must be >= avclan::Frame allocator pool capacity");
|
||||||
|
|
||||||
@@ -74,12 +74,10 @@ int main() {
|
|||||||
print_help();
|
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()) {
|
if (const auto *in = incoming.peek()) {
|
||||||
if (auto resp = peripheral.route(*in)) {
|
if (auto resp = peripheral.route(*in)) {
|
||||||
@@ -105,7 +103,7 @@ int main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stdin must be non-blocking: yielding EOF when idle/empty
|
// stdin must be non-blocking: yielding EOF when idle/empty
|
||||||
if (int readkey = getchar(); readkey != EOF) {
|
if (int readkey = fgetc(stdin); readkey != EOF) {
|
||||||
switch (readkey) {
|
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;
|
||||||
@@ -180,6 +178,16 @@ int main() {
|
|||||||
while (peripheral.device<CDChanger>().media_busy()) {}
|
while (peripheral.device<CDChanger>().media_busy()) {}
|
||||||
puts("end");
|
puts("end");
|
||||||
break;
|
break;
|
||||||
|
case '+':
|
||||||
|
peripheral.get_bus().deafen(true);
|
||||||
|
peripheral.get_bus().set_dominant();
|
||||||
|
puts("Set bus dominant...");
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
peripheral.get_bus().set_recessive();
|
||||||
|
peripheral.get_bus().deafen(false);
|
||||||
|
puts("Set bus recessive...");
|
||||||
|
break;
|
||||||
#ifdef MEASURE_BUS
|
#ifdef MEASURE_BUS
|
||||||
case 'M': peripheral.get_bus().measure(); break;
|
case 'M': peripheral.get_bus().measure(); break;
|
||||||
#endif
|
#endif
|
||||||
@@ -211,7 +219,19 @@ 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) {
|
||||||
@@ -270,6 +290,10 @@ int main() {
|
|||||||
if (readBinary) {
|
if (readBinary) {
|
||||||
data_tmp[seqIdx++] = readkey;
|
data_tmp[seqIdx++] = readkey;
|
||||||
} else {
|
} else {
|
||||||
|
// Only take valid hex digits
|
||||||
|
if (isxdigit(readkey) == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
hexChars[hexDigit++] = readkey;
|
hexChars[hexDigit++] = readkey;
|
||||||
|
|
||||||
if (hexDigit == 2) {
|
if (hexDigit == 2) {
|
||||||
@@ -294,6 +318,8 @@ int main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // switch (readkey)
|
} // switch (readkey)
|
||||||
|
} else {
|
||||||
|
clearerr(stdin);
|
||||||
} // if (readkey != EOF)
|
} // if (readkey != EOF)
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -324,6 +350,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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user