summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2026-06-20 22:23:48 +0200
committerFelix Morgner <felix.morgner@gmail.com>2026-06-20 22:44:43 +0200
commitd7c2a8029c4aefc295719174a863129645d6ab99 (patch)
treed2f36ab8721dd32235dc63a60218378eb35922c8
downloadthrottle-quadrant-d7c2a8029c4aefc295719174a863129645d6ab99.tar.xz
throttle-quadrant-d7c2a8029c4aefc295719174a863129645d6ab99.zip
initial commitHEADmaster
-rw-r--r--protocol.cpp124
-rw-r--r--protocol.hpp88
-rw-r--r--system.cpp11
-rw-r--r--system.hpp18
-rw-r--r--throttle-quadrant.ino32
-rw-r--r--utilities.cpp5
-rw-r--r--utilities.hpp67
7 files changed, 345 insertions, 0 deletions
diff --git a/protocol.cpp b/protocol.cpp
new file mode 100644
index 0000000..5ad0975
--- /dev/null
+++ b/protocol.cpp
@@ -0,0 +1,124 @@
+#include "protocol.hpp"
+#include "system.hpp"
+
+#include <Arduino.h>
+#include <TimerOne.h>
+#include <SPI.h>
+
+// ============================================================================
+// 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<message> {
+ 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<message>{ 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
new file mode 100644
index 0000000..c71b7d3
--- /dev/null
+++ b/protocol.hpp
@@ -0,0 +1,88 @@
+#ifndef THROTTLE_QUADRANT_PROTOCOL_HPP
+#define THROTTLE_QUADRANT_PROTOCOL_HPP
+
+#include "utilities.hpp"
+
+#include <Arduino.h>
+
+//! 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<message>;
+
+//! @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
new file mode 100644
index 0000000..d130d66
--- /dev/null
+++ b/system.cpp
@@ -0,0 +1,11 @@
+#include "system.hpp"
+
+#include <Arduino.h>
+
+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
new file mode 100644
index 0000000..e39b79e
--- /dev/null
+++ b/system.hpp
@@ -0,0 +1,18 @@
+#ifndef THROTTLE_QUADRANT_SYSTEM_HPP
+#define THROTTLE_QUADRANT_SYSTEM_HPP
+
+#include <Arduino.h>
+
+//! 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
new file mode 100644
index 0000000..7c213a1
--- /dev/null
+++ b/throttle-quadrant.ino
@@ -0,0 +1,32 @@
+#include "protocol.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();
+
+ // Initialize the serial output stream.
+ Serial.begin(9600);
+}
+
+//! 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();
+
+ if (maybe_message.has_value()) {
+ // Extract the received message.
+ auto const& message = maybe_message.value();
+
+ Serial.write("new message: ");
+ Serial.write("throttle == ");
+ Serial.print(static_cast<int>(message.throttle));
+ }
+} \ No newline at end of file
diff --git a/utilities.cpp b/utilities.cpp
new file mode 100644
index 0000000..d05e67c
--- /dev/null
+++ b/utilities.cpp
@@ -0,0 +1,5 @@
+#include <Arduino.h>
+
+void* operator new(size_t size, void* ptr) {
+ return ptr;
+} \ No newline at end of file
diff --git a/utilities.hpp b/utilities.hpp
new file mode 100644
index 0000000..819c170
--- /dev/null
+++ b/utilities.hpp
@@ -0,0 +1,67 @@
+#ifndef THROTTLE_QUADRANT_UTILITIES_HPP
+#define THROTTLE_QUADRANT_UTILITIES_HPP
+
+#include <Arduino.h>
+
+void* operator new(size_t size, void* ptr);
+
+namespace tq {
+
+template<typename ValueType>
+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<ValueType const*>(m_storage));
+ }
+
+private:
+ auto construct_from(ValueType const& value) -> void {
+ new (static_cast<byte*>(m_storage)) ValueType{ value };
+ }
+
+ auto destroy() -> void {
+ if (has_value()) {
+ (reinterpret_cast<ValueType const*>(m_storage))->~ValueType();
+ }
+ }
+
+ alignas(ValueType) byte m_storage[sizeof(ValueType)];
+ bool m_engaged;
+};
+
+
+}
+
+#endif \ No newline at end of file