From 82620f74d6e40617b725dcb8e50cdd94eb3e3e86 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Mon, 29 Jun 2026 22:00:26 +0200 Subject: tq: rewrite for shift register semantics --- ThrottleQuadrant.cpp | 59 ++++++++++++++++++++++++ ThrottleQuadrant.hpp | 49 ++++++++++++++++++++ protocol.cpp | 124 -------------------------------------------------- protocol.hpp | 88 ----------------------------------- system.cpp | 11 ----- system.hpp | 18 -------- throttle-quadrant.ino | 42 +++++++---------- utilities.cpp | 5 -- utilities.hpp | 67 --------------------------- 9 files changed, 125 insertions(+), 338 deletions(-) create mode 100644 ThrottleQuadrant.cpp create mode 100644 ThrottleQuadrant.hpp delete mode 100644 protocol.cpp delete mode 100644 protocol.hpp delete mode 100644 system.cpp delete mode 100644 system.hpp delete mode 100644 utilities.cpp delete mode 100644 utilities.hpp diff --git a/ThrottleQuadrant.cpp b/ThrottleQuadrant.cpp new file mode 100644 index 0000000..4e4404a --- /dev/null +++ b/ThrottleQuadrant.cpp @@ -0,0 +1,59 @@ +#include "ThrottleQuadrant.hpp" + +#include + +#include + +auto SaitekThrottleQuadrant::beginTransaction() -> void { + noInterrupts(); + digitalWrite(latch_pin, LOW); + delayMicroseconds(latchDelayMicros); +} + +auto SaitekThrottleQuadrant::endTransaction() -> void { + digitalWrite(latch_pin, HIGH); + delayMicroseconds(latchDelayMicros); + interrupts(); +} + +template +auto SaitekThrottleQuadrant::doRead() -> T { + auto value = T{}; + + for (auto i = 0u; i < Bits; ++i) { + digitalWrite(clock_pin, LOW); + delayMicroseconds(clockDelayMicros); + + value |= digitalRead(data_pin) << i; + + digitalWrite(clock_pin, HIGH); + delayMicroseconds(clockDelayMicros); + } + + return value; +} + +auto SaitekThrottleQuadrant::begin(unsigned clock_pin, unsigned latch_pin, unsigned data_pin) -> void { + clock_pin = clock_pin; + latch_pin = latch_pin; + data_pin = data_pin; + + pinMode(clock_pin, OUTPUT); + pinMode(latch_pin, OUTPUT); + pinMode(data_pin, INPUT); +} + +auto SaitekThrottleQuadrant::read() noexcept -> Data { + auto result = Data{}; + + beginTransaction(); + + result.left = doRead(); + result.middle = doRead(); + result.right = doRead(); + result.buttons = doRead(); + + endTransaction(); + + return result; +} diff --git a/ThrottleQuadrant.hpp b/ThrottleQuadrant.hpp new file mode 100644 index 0000000..8776aab --- /dev/null +++ b/ThrottleQuadrant.hpp @@ -0,0 +1,49 @@ +#ifndef SAITEK_THROTTLE_QUADRANT_HPP +#define SAITEK_THROTTLE_QUADRANT_HPP + +#include +#include +#include + +struct SaitekThrottleQuadrant { + + //! The data read from the throttle quadrant. + struct [[gnu::packed]] Data { + uint8_t left; + uint8_t right; + uint8_t middle; + uint16_t buttons : 9; + uint16_t : 0; + }; + + //! The minimum delay between successive read operations. + auto constexpr static readDelayMillis = 25u; + + //! Initialize the throttle quadrant interface. + auto begin(unsigned clock_pin, unsigned latch_pin, unsigned data_pin) noexcept -> void; + + //! Read the current state of the throttle quadrant. + auto read() noexcept -> Data; + +private: + //! The time for the clock to propagate. + auto constexpr static clockDelayMicros = 25u; + + //! The time for the latch to settle. + auto constexpr static latchDelayMicros = 5u; + + //! The number of buttons on the throttle quadrant. + auto constexpr static numberOfButtons = 9u; + + auto beginTransaction() -> void; + auto endTransaction() -> void; + + template + auto doRead() -> T; + + unsigned clock_pin{}; + unsigned latch_pin{}; + unsigned data_pin{}; +} static ThrottleQuadrant; + +#endif \ No newline at end of file diff --git a/protocol.cpp b/protocol.cpp deleted file mode 100644 index 5ad0975..0000000 --- a/protocol.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "protocol.hpp" -#include "system.hpp" - -#include -#include -#include - -// ============================================================================ -// The throttle qudrant requires a specific protocol for communication: -// 1. At time 0: the "data ready" line will be pulled low. -// 2. At 0.7ms: the line will is release. -// 3. At 1.9ms: the data is ready to be read. -// -// The data can be read out at about 20 kHz. It is presented LSB first and comprises 5 -// bytes. -// ============================================================================ - -namespace tq::protocol { - -namespace state { - -//! Whether or not an SPI transfer is pending. -auto volatile spi_transfer_pending = false; - -//! Whether we are currently in a protocol loop. -auto volatile protocol_loop_running = false; - -} - -namespace settings { - -//! The SPI bus configuration for data transfers from the throttle quadrant. -auto const spi_configuration = SPISettings{ 20000, LSBFIRST, SPI_MODE3 }; - -} - -//! Start a transfer of the throttle quadrant data. -//! -//! This function schedules the next step in the protocol. -auto begin_transfer_cycle() -> void { - // Disable the "data ready" interrupt, since we need to reuse it for the next step. - detachInterrupt(digitalPinToInterrupt(system::data_ready_signal_pin)); - - // Attach an inverted "data ready" interrupt, so we know when to start the timer. - attachInterrupt(digitalPinToInterrupt(system::data_ready_signal_pin), - on_start_delay, - RISING); -} - -auto init() -> void { - // Ensure we are not starting spurious transfers. - state::spi_transfer_pending = false; - state::protocol_loop_running = false; - - // Prepare the protocol timer. - Timer1.initialize(); - - // Prepare the "data ready" signal pin. - pinMode(system::data_ready_signal_pin, INPUT_PULLUP); -} - -auto on_data_ready() -> void { - // Read out the current state of the "data ready" signal pin. - auto pin_state = digitalRead(system::data_ready_signal_pin); - - // Check if we actually are at the start of a transfer. - if (pin_state == LOW) { - begin_transfer_cycle(); - } -} - -auto on_start_delay() -> void { - // Disable the interrupt so we are not interrupted during a transfer. - detachInterrupt(digitalPinToInterrupt(system::data_ready_signal_pin)); - - // Wait 1.2ms until we start the actual SPI transfer. - Timer1.attachInterrupt(on_start_delay_passed, 1200); - Timer1.start(); -} - -auto on_start_delay_passed() -> void { - Timer1.detachInterrupt(); - state::spi_transfer_pending = true; -} - -auto perform_transfer() -> optional { - byte buffer[sizeof(message)] = {}; - - if (!state::spi_transfer_pending) { - return {}; - } - - // Record that we are done with one loop. - state::spi_transfer_pending = false; - state::protocol_loop_running = false; - - SPI.beginTransaction(settings::spi_configuration); - SPI.transfer(buffer, sizeof(buffer)); - SPI.endTransaction(); - - auto parsed = message{}; - memcpy(&parsed, buffer, sizeof(message)); - - return optional{ parsed }; -} - -auto start() -> bool { - // If we are already in a loop, don't start a new one. - if (state::protocol_loop_running) { - return false; - } - - // Record that a loop has started - state::protocol_loop_running = true; - - // Attach the protocol handling subsystem entry point to the correct input line. - attachInterrupt(digitalPinToInterrupt(system::data_ready_signal_pin), - tq::protocol::on_data_ready, - FALLING); - - return true; -} - -} \ No newline at end of file diff --git a/protocol.hpp b/protocol.hpp deleted file mode 100644 index c71b7d3..0000000 --- a/protocol.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef THROTTLE_QUADRANT_PROTOCOL_HPP -#define THROTTLE_QUADRANT_PROTOCOL_HPP - -#include "utilities.hpp" - -#include - -//! All our custom code lives in its own name space to avoid collisions. -namespace tq::protocol { - -//! A message as received from the throttle quadrant. -struct [[gnu::packed]] message { - //! The current throttle value (unit TBD). - byte throttle; - //! The current propeller speed value (unit TBD). - byte propeller_speed; - //! The current mixture value (unit TBD). - byte mixture; - - //! The state (pressed/released) of the first trigger. - byte trigger_1 : 1; - //! The state (pressed/released) of the second trigger. - byte trigger_2 : 1; - //! The state (pressed/released) of the third trigger. - byte trigger_3 : 1; - //! The state (pressed/released) of the fourth trigger. - byte trigger_4 : 1; - //! The state (pressed/released) of the fifth trigger. - byte trigger_5 : 1; - //! The state (pressed/released) of the sixth trigger. - byte trigger_6 : 1; - - //! Whether the throttle is idle or not. - byte throttle_idle : 1; - - //! Whether the throttle is feathered or not (?). - byte propeller_feather : 1; - - //! Whether the mixture is cut off or not. - byte mixture_cutoff : 1; - - //! Reserved bits. - byte : 7; -}; - -//! Initialize the protocol handling subsystem. -auto init() -> void; - -//! Start a run of the protocol handling subsystem. -//! -//! A new run is only started if there is not current run ongoing. -//! -//! @return true iff. a new run was started, false otherwise. -auto start() -> bool; - -//! Perform the synchronous SPI transfer, if one is pending. -//! -//! Perform the final synchronous transfer from the throttle quadrant. If no -//! new data is available, an empty optional is returned. -//! -//! @return An optional object containing a new message if one is available. -auto perform_transfer() -> optional; - -//! @internal -//! Process the "data ready" signal coming from the throttle quadrant. -//! -//! When the throttle quadrant pulls the "data ready" line low, we know that -//! there is new data available to be read out. We must adhere to a somewhat -//! strict timing protocol, as detailed in the implemenation of this function. -auto on_data_ready() -> void; - -//! @internal -//! Process the "start delay" signal coming from the throttle quadrant. -//! -//! After notifying us about the data being ready, the "data ready" line will -//! be deasserted. However, we must wait an additional 1.2ms before we start -//! the transfer. -auto on_start_delay() -> void; - -//! @internal -//! Process the end of the start delay. -//! -//! The data is of the throttle quadrant is now ready to be read. -auto on_start_delay_passed() -> void; - -} - -#endif \ No newline at end of file diff --git a/system.cpp b/system.cpp deleted file mode 100644 index d130d66..0000000 --- a/system.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "system.hpp" - -#include - -namespace tq::system { - -// Abort compilation if the interrupt pin is not valid -static_assert(digitalPinToInterrupt(data_ready_signal_pin) != -1, - "The selected pin is not a valid interrupt source!"); - -} \ No newline at end of file diff --git a/system.hpp b/system.hpp deleted file mode 100644 index e39b79e..0000000 --- a/system.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef THROTTLE_QUADRANT_SYSTEM_HPP -#define THROTTLE_QUADRANT_SYSTEM_HPP - -#include - -//! All our custom code lives in its own name space to avoid collisions. -namespace tq::system { - -//! The pin to which the "data ready" line of the throttle quadrant is connected. -//! -//! Since the throttle qudrant runs its logic asynchronously to the execution -//! of our code, it exposes a signal to inform us that it has acquired new -//! data. It does so by pulling the "data ready" line low. -auto constexpr data_ready_signal_pin = 2; - -} - -#endif \ No newline at end of file diff --git a/throttle-quadrant.ino b/throttle-quadrant.ino index 7c213a1..54740d9 100644 --- a/throttle-quadrant.ino +++ b/throttle-quadrant.ino @@ -1,32 +1,24 @@ -#include "protocol.hpp" +#include "ThrottleQuadrant.hpp" -//! Set up any global state. -//! -//! This function is executed once during the start of the firmware. -auto setup() -> void { - // Initialize the protocol subsystem. - tq::protocol::init(); +namespace { - // Initialize the serial output stream. - Serial.begin(9600); -} +//! The time point (in milliseconds since boot) when we last read out the data of the throttle quadrant. +auto lastReadTimePoint = 0ul; -//! Perform one iteration through the firmare logic. -//! -//! This function is called in an endless loop after the firmware has started. -auto loop() -> void { - // Always try to start a new transfer. - tq::protocol::start(); +} - // Receive a new message if one is available. - auto maybe_message = tq::protocol::perform_transfer(); +void setup() { + Serial.begin(115200); + ThrottleQuadrant.begin(2, 3, 4); +} - if (maybe_message.has_value()) { - // Extract the received message. - auto const& message = maybe_message.value(); +void loop() { + auto currentTimePoint = millis(); - Serial.write("new message: "); - Serial.write("throttle == "); - Serial.print(static_cast(message.throttle)); + // Read out the throttle quadrant if the minimum amount of wait time has passed. + if ((currentTimePoint - lastReadTimePoint) >= ThrottleQuadrant.readDelayMillis) { + auto data = ThrottleQuadrant.read(); + lastReadTimePoint = millis(); + Serial.write(reinterpret_cast(&data), sizeof(data)); } -} \ No newline at end of file +} diff --git a/utilities.cpp b/utilities.cpp deleted file mode 100644 index d05e67c..0000000 --- a/utilities.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include - -void* operator new(size_t size, void* ptr) { - return ptr; -} \ No newline at end of file diff --git a/utilities.hpp b/utilities.hpp deleted file mode 100644 index 819c170..0000000 --- a/utilities.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef THROTTLE_QUADRANT_UTILITIES_HPP -#define THROTTLE_QUADRANT_UTILITIES_HPP - -#include - -void* operator new(size_t size, void* ptr); - -namespace tq { - -template -struct optional { - - optional() - : m_engaged{ false } { - } - - optional(optional const& other) - : m_engaged{ other.has_value() } { - if (m_engaged) { - construct_from(other.value()); - } - } - - explicit optional(ValueType const& value) - : m_engaged{ true } { - construct_from(value); - } - - ~optional() { - destroy(); - } - - auto operator=(optional const& other) -> optional& { - destroy(); - m_engaged = other.m_engaged; - if (m_engaged) { - construct_from(other.value()); - } - } - - auto has_value() const -> bool { - return m_engaged; - } - - auto value() const -> ValueType const& { - return *(reinterpret_cast(m_storage)); - } - -private: - auto construct_from(ValueType const& value) -> void { - new (static_cast(m_storage)) ValueType{ value }; - } - - auto destroy() -> void { - if (has_value()) { - (reinterpret_cast(m_storage))->~ValueType(); - } - } - - alignas(ValueType) byte m_storage[sizeof(ValueType)]; - bool m_engaged; -}; - - -} - -#endif \ No newline at end of file -- cgit v1.2.3