-
Notifications
You must be signed in to change notification settings - Fork 1
/
PulsedOutput.hpp
174 lines (153 loc) · 8.01 KB
/
PulsedOutput.hpp
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#pragma once
#include "Const.hpp"
#include "IOpin.hpp"
#include "Nop.hpp"
namespace AVR {
// enum class InterruptHandling {
// None,
// Bit,
// Byte,
// Block,
// };
/**
* @brief Output that can handle a pulse length encoded bit digital protocol
*
* A single bit is a high "pulse" period and low "pulse" period.
*
* A longer pulse is a "1" bit and a shorter pulse is a "0" bit.
*
* @details
* _______________________
* 0 = |<-- ShortPulseNanos -->| <-- MinRecoveryNanos -->
* | |___________________________________
* __________________________________
* 1 = |<-------- LongPulseNanos -------->|<-- MinRecoveryNanos -->
* | |________________________
*
* @tparam Port The processor "Port" to use
* @tparam Pin The particular Pin of the "Port" to use
* @tparam ShortPulseNanos The length of the short pulse in nanoseconds
* @tparam InvertedOutput Whether to invert the output (false)
* @tparam LongPulseNanos The length of the long pulse in nanoseconds (2x short pulse)
* @tparam LittleEndian Whether to send the least significant bit first (false)
* @tparam InvertBits Whether to invert the long/short pulse meaning of bits (false)
* @tparam MinRecoveryNanos The minimum time to wait after sending a bit before sending the next bit (0)
* @tparam BalanceRecoveryTimes Make pulses start at regular intervals (false) [Not Yet Implemented]
*/
template <Ports Port, unsigned Pin, unsigned ShortPulseNanos, bool InvertedOutput = false,
bool BalanceRecoveryTimes = false, bool LittleEndian = false, bool InvertBits = false,
unsigned MinRecoveryNanos = ShortPulseNanos, unsigned LongPulseNanos = ShortPulseNanos * 2>
class PulsedOutput : protected Output<Port, Pin, InvertedOutput> {
protected:
using Output<Port, Pin, InvertedOutput>::on;
using Output<Port, Pin, InvertedOutput>::off;
using Output<Port, Pin, InvertedOutput>::input;
using Output<Port, Pin, InvertedOutput>::output;
public:
using Output<Port, Pin, InvertedOutput>::init;
/**
* @brief Group all math for pulse lengths into a single struct to de-clutter main namespace
*
*/
struct PulseMath {
// Positive length of output on for "0" bit (for non-inverted output/bits)
static constexpr double pulseLengthShort = ShortPulseNanos / 1e9;
// Positive length of output on for "1" bit
static constexpr double pulseLengthLong = LongPulseNanos / 1e9;
// Minimum length of off state
static constexpr double pulseLengthRecover = MinRecoveryNanos / 1e9;
// Cycles needed by microprocessor to achieve the desired periods
static constexpr unsigned cyclesShort = Const::round(F_CPU * pulseLengthShort);
static constexpr unsigned cyclesLong = Const::round(F_CPU * pulseLengthLong);
static constexpr unsigned cyclesRecover = Const::round(F_CPU * pulseLengthRecover);
/**
* In order to get accurate transition times, we need to know how long other instructions that are used/executed
* take. To get these values, we need to inspect the generated assembly and count the number of instructions the
* compiler generated. The remaining time is filled with NOPs.
*/
/**
* Let's step through the assembly code generated by the compiler.
*
* Each line is one clock cycle. It's a "0" bit (short pulse) followed by a "1" bit (long pulse).
*
* Out is either "Idle" or "Asserted" state (level depends on InvertedOutput)
*
* C++ // ASM simplified ; Out Notes
* --------- // -------------- ; --- --------
* byte <<= // lsl ; I Top of Loop - Shift data and store carry bit ("0" this time)
* on() // sbi Port,Pin ; A Turn on output
* delay(A) // nop x A ; A Delay A - adjust to get the "0" timing we want
* if (bit) // brcs off() ; A test bit. is low. needs no extra delay. Takes 1 cycle without branch.
* off() // cbi Port,Pin ; I Turn off
* delay(C) // nop x C ; I Delay C - adjust to get the minimum off timing we want
* len-- // subi ; I Decrement byte length counter
* if (len) // brne ; I if not zero, jump back to start of loop
* ; I brne takes 2 clock cycles when branching (looping)
* byte <<= // lsr ; I Top of Loop - Shift data and store carry bit ("1" this time)
* on() // sbi Port,Pin ; A Turn on output
* delay(A) // nop x A ; A Delay A - adjust to get the "0" timing we want
* if (bit) // brcs off() ; A test bit. is high. needs extra delay. branch to DelayB.
* ; A brcs takes 2 clock cycles when branching
* delay(B) // nop x B ; A Delay B - adjust to get the "1" timing we want
* goto // rjmp Off() ; A Jump to off
* ; A rjmp takes 2 clock cycles
* off() // cbi Port,Pin ; I Turn off
* ...
*
* So, now we count how many clock cycles the output stays on, for each value.
*
* The high time for a "0" is 3 lines, but includes one line for delay A. So minCyclesShort = 2
* The high time for a "1" is 7 lines, but includes lines delay A and B. So minCyclesLong = 5
* The low time for the inner loop is 6 lines, but includes the delay C. So minCyclesRecover = 5
*
* The outer loop (asm not shown) adds 7 clock cycles to the low period every byte. So outerLoopExtraCycles = 7
* So long as it doesn't stretch the low time too much, the data should not be corrupted.
*
* Let's use some basic arithmetic. Starting with...
* cyclesShort = minCyclesShort + A
* cyclesLong = minCyclesLong + A + B
* cyclesLow = minCyclesRecover + C
* cyclesLowLoop = cyclesLow + outerLoopExtraCycles
*
* We can rearrange the equations to get...
*
* delayCyclesA = cyclesShort - minCyclesShort
* delayCyclesB = cyclesLong - minCyclesLong - delayCyclesA
* delayCyclesC = cyclesLow - minCyclesRecover
*/
// The number of instructions it takes to turn on the output and possibly skip the second delay
static constexpr unsigned minCyclesShort = 2;
static constexpr unsigned minCyclesLong = 5;
static constexpr unsigned minCyclesRecover = 5;
static constexpr unsigned outerLoopExtraCycles = 7;
static constexpr unsigned delayCyclesA = Const::max<signed>(0, cyclesShort - minCyclesShort);
static constexpr unsigned delayCyclesB = Const::max<signed>(0, cyclesLong - minCyclesLong - delayCyclesA);
static constexpr unsigned delayCyclesC = Const::max<signed>(0, cyclesRecover - minCyclesRecover);
// Values that will actually be used. For developer inspection with modern editor. Not used in code.
static constexpr auto realHighCyclesShort = minCyclesShort + delayCyclesA;
static constexpr auto realHighCyclesLong = minCyclesLong + delayCyclesA + delayCyclesB;
static constexpr auto realLowCyclesMin = minCyclesRecover + delayCyclesC;
static constexpr auto realLowCyclesMax = realLowCyclesMin + outerLoopExtraCycles;
static constexpr auto realHighTimeShort = realHighCyclesShort / double(F_CPU);
static constexpr auto realHighTimeLong = realHighCyclesLong / double(F_CPU);
static constexpr auto realLowTimeMin = realLowCyclesMin / double(F_CPU);
static constexpr auto realLowTimeMax = realLowCyclesMax / double(F_CPU);
static constexpr auto realHighNanosecondsShort = realHighTimeShort * 1e9;
static constexpr auto realHighNanosecondsLong = realHighTimeLong * 1e9;
static constexpr auto realLowNanosecondsMin = realLowTimeMin * 1e9;
static constexpr auto realLowMicrosecondsMax = realLowTimeMax * 1e6;
};
public:
static void send(u1 byte, u1 bits = 8);
/**
* @brief Shift an array of bits (packed as bytes) out the specified pin
*
* @param data the bytes to send
* @param bytes the number of bytes to send
*/
static inline void send(u1 const *data, u1 bytes) {
while (bytes--)
send(*data++);
}
};
}; // namespace AVR