diff options
| -rw-r--r-- | protocol.cpp | 124 | ||||
| -rw-r--r-- | protocol.hpp | 88 | ||||
| -rw-r--r-- | system.cpp | 11 | ||||
| -rw-r--r-- | system.hpp | 18 | ||||
| -rw-r--r-- | throttle-quadrant.ino | 32 | ||||
| -rw-r--r-- | utilities.cpp | 5 | ||||
| -rw-r--r-- | utilities.hpp | 67 |
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 |
