summaryrefslogtreecommitdiff
path: root/protocol.cpp
blob: 5ad0975ad69f6494dc1911acf48ea011b434a929 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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;
}

}