summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ThrottleQuadrant.cpp59
-rw-r--r--ThrottleQuadrant.hpp49
-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.ino42
-rw-r--r--utilities.cpp5
-rw-r--r--utilities.hpp67
9 files changed, 125 insertions, 338 deletions
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 <limits.h>
+
+#include <Arduino.h>
+
+auto SaitekThrottleQuadrant::beginTransaction() -> void {
+ noInterrupts();
+ digitalWrite(latch_pin, LOW);
+ delayMicroseconds(latchDelayMicros);
+}
+
+auto SaitekThrottleQuadrant::endTransaction() -> void {
+ digitalWrite(latch_pin, HIGH);
+ delayMicroseconds(latchDelayMicros);
+ interrupts();
+}
+
+template<typename T, size_t Bits>
+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<uint8_t>();
+ result.middle = doRead<uint8_t>();
+ result.right = doRead<uint8_t>();
+ result.buttons = doRead<uint16_t, numberOfButtons>();
+
+ 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 <limits.h>
+#include <stddef.h>
+#include <stdint.h>
+
+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<typename T, size_t Bits = sizeof(T) * CHAR_BIT>
+ 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 <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
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 <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
deleted file mode 100644
index d130d66..0000000
--- a/system.cpp
+++ /dev/null
@@ -1,11 +0,0 @@
-#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
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 <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
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<int>(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<byte const *>(&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 <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
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 <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