-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathLinkIR.hpp
369 lines (315 loc) · 11.2 KB
/
LinkIR.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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#ifndef LINK_IR_H
#define LINK_IR_H
// --------------------------------------------------------------------------
// A driver for the Infrared Adapter (AGB-006).
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
// LinkIR* linkIR = new LinkIR();
// - 2) Add the interrupt service routines:
// interrupt_init();
// interrupt_add(INTR_SERIAL, LINK_IR_ISR_SERIAL);
// interrupt_add(INTR_TIMER2, []() {});
// interrupt_add(INTR_TIMER3, []() {});
// - 3) Initialize the library with:
// linkIR->activate();
// - 4) Send NEC signals:
// linkIR->sendNEC(0x04, 0x03);
// - 5) Receive NEC signals:
// u8 address, u8 command;
// linkIR->receiveNEC(address, command);
// - 6) Send 38kHz signals:
// linkIR->send({9000, 4500, 560, 560, 560, 1690, 0});
// // u16 array, numbers are microseconds
// // even indices: marks (IR on)
// // odd indices: spaces (IR off)
// // 0 = EOS
// - 7) Receive 38kHz signals:
// u16 pulses[2000];
// linkIR->receive(pulses, 2000);
// // out array, max entries, timeout (in microseconds)
// - 8) Bitbang the LED manually:
// linkIR->setLight(true);
// - 9) Receive manually:
// bool ledOn = linkIR->isDetectingLight();
// --------------------------------------------------------------------------
// considerations:
// - wait at least 1 microsecond between `send(...)` and `receive(...)` calls!
// --------------------------------------------------------------------------
#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif
#include "LinkGPIO.hpp"
#include "_link_common.hpp"
LINK_VERSION_TAG LINK_IR_VERSION = "vLinkIR/v8.0.3";
#define LINK_IR_SIGNAL_END 0
#define LINK_IR_DEFAULT_PRIMARY_TIMER_ID 2
#define LINK_IR_DEFAULT_SECONDARY_TIMER_ID 3
/**
* @brief A driver for the Infrared Adapter (AGB-006).
* \warning To use this library, make sure that `lib/iwram_code/LinkIR.cpp` gets
* compiled! For example, in a Makefile-based project, verify that the directory
* is in your `SRCDIRS` list.
*/
class LinkIR {
private:
using u32 = Link::u32;
using u16 = Link::u16;
using u8 = Link::u8;
using vu32 = Link::vu32;
using Pin = LinkGPIO::Pin;
using Direction = LinkGPIO::Direction;
static constexpr int CYCLES_PER_MICROSECOND = 17;
static constexpr int DETECTION_TIMEOUT = 1000;
static constexpr int DEMODULATION_38KHZ_PERIOD = 1000000 / 38000;
static constexpr int DEMODULATION_MARK_MIN_TRANSITIONS = 3;
static constexpr int DEMODULATION_SPACE_PERIODS = 3;
static constexpr int DEMODULATION_SPACE_THRESHOLD =
DEMODULATION_38KHZ_PERIOD * DEMODULATION_SPACE_PERIODS;
static constexpr int DEMODULATION_HYSTERESIS_DELAY = 10;
static constexpr int NEC_TOLERANCE_PERCENTAGE = 15;
static constexpr int NEC_TOTAL_PULSES = 68;
static constexpr int NEC_LEADER_MARK = 9000;
static constexpr int NEC_LEADER_SPACE = 4500;
static constexpr int NEC_PULSE = 560;
static constexpr int NEC_SPACE_1 = 1690;
static constexpr int NEC_SPACE_0 = 560;
static constexpr int DEFAULT_RECEIVE_TIMEOUT = 15000;
static constexpr u32 NO_TIMEOUT = 0xFFFFFFFF;
public:
/**
* @brief Constructs a new LinkIR object.
* @param primaryTimerId `(0~3)` GBA Timer to use for measuring time (1/2).
* @param secondaryTimerId `(0~3)` GBA Timer to use for measuring time (2/2).
*/
explicit LinkIR(u8 primaryTimerId = LINK_IR_DEFAULT_PRIMARY_TIMER_ID,
u8 secondaryTimerId = LINK_IR_DEFAULT_SECONDARY_TIMER_ID) {
config.primaryTimerId = primaryTimerId;
config.secondaryTimerId = secondaryTimerId;
}
/**
* @brief Returns whether the library is active or not.
*/
[[nodiscard]] bool isActive() { return isEnabled; }
/**
* @brief Activates the library. Returns whether the adapter is connected or
* not.
*/
bool activate() {
LINK_READ_TAG(LINK_IR_VERSION);
LINK_BARRIER;
isEnabled = false;
LINK_BARRIER;
resetState();
linkGPIO.reset();
LINK_BARRIER;
isEnabled = true;
LINK_BARRIER;
linkGPIO.setMode(Pin::SC, Direction::OUTPUT);
linkGPIO.writePin(Pin::SC, false);
linkGPIO.setMode(Pin::SD, Direction::OUTPUT);
linkGPIO.writePin(Pin::SD, true);
linkGPIO.setMode(Pin::SO, Direction::OUTPUT);
linkGPIO.writePin(Pin::SO, false);
linkGPIO.setSIInterrupts(true);
waitMicroseconds(DETECTION_TIMEOUT);
setLight(true);
waitMicroseconds(DETECTION_TIMEOUT);
setLight(false);
linkGPIO.setSIInterrupts(false);
return detected;
}
/**
* @brief Deactivates the library.
*/
void deactivate() {
isEnabled = false;
linkGPIO.reset();
}
/**
* Sends a NEC signal.
* @param address An 8-bit address, to specify the device.
* @param command An 8-bit command, to specify the action.
*/
void sendNEC(u8 address, u8 command) {
if (!isEnabled)
return;
u16 pulses[NEC_TOTAL_PULSES];
u32 i = 0;
pulses[i++] = NEC_LEADER_MARK;
pulses[i++] = NEC_LEADER_SPACE;
addNECByte(pulses, i, address);
addNECByte(pulses, i, (u8)~address);
addNECByte(pulses, i, command);
addNECByte(pulses, i, (u8)~command);
pulses[i++] = NEC_PULSE;
pulses[i++] = LINK_IR_SIGNAL_END;
send(pulses);
}
/**
* Receives a signal and returns whether it's a NEC signal or not. If it is,
* the `address` and `command` will be filled. Returns `true` on success.
* @param address The read 8-bit address, specifying the device.
* @param command The read 8-bit command, specifying the action.
* @param startTimeout Number of microseconds before the first *mark* to abort
* the reception.
*/
bool receiveNEC(u8& address, u8& command, u32 startTimeout = NO_TIMEOUT) {
if (!isEnabled)
return false;
u16 pulses[NEC_TOTAL_PULSES];
if (!receive(pulses, NEC_TOTAL_PULSES, startTimeout))
return false;
return parseNEC(pulses, address, command);
}
/**
* Tries to interpret an already received array of `pulses` as a NEC signal.
* On success, returns `true` and fills the `address` and `command`
* parameters.
* @param pulses The pulses to interpret. Returns `true` on success.
* @param address The read 8-bit address, specifying the device.
* @param command The read 8-bit command, specifying the action.
*/
bool parseNEC(u16 pulses[], u8& address, u8& command) {
if (!isEnabled)
return false;
if (!isWithinNECTolerance(pulses[0], NEC_LEADER_MARK))
return false;
if (!isWithinNECTolerance(pulses[1], NEC_LEADER_SPACE))
return false;
u32 data = 0;
for (u32 bit = 0; bit < 32; bit++) {
u32 markIndex = 2 + bit * 2;
u32 spaceIndex = markIndex + 1;
if (!isWithinNECTolerance(pulses[markIndex], NEC_PULSE))
return false;
u16 space = pulses[spaceIndex];
if (isWithinNECTolerance(space, NEC_SPACE_1))
data |= 1 << bit;
else if (!isWithinNECTolerance(space, NEC_SPACE_0))
return false;
}
if (!isWithinNECTolerance(pulses[66], NEC_PULSE))
return false;
u8 addr = data & 0xFF;
u8 invAddr = (data >> 8) & 0xFF;
u8 cmd = (data >> 16) & 0xFF;
u8 invCmd = (data >> 24) & 0xFF;
if ((u8)~addr != invAddr || (u8)~cmd != invCmd)
return false;
address = addr;
command = cmd;
return true;
}
/**
* Sends a generic IR signal, modulating at standard 38kHz.
* @param pulses An array of u16 numbers describing the signal. Even indices
* are *marks* (IR on), odd indices are *spaces* (IR off), and `0` ends the
* signal.
* \warning The carrier frequency is tied to the ASM code. To transmit in
* other frequencies, you'll have to bitbang the `SO` pin yourself with
* `setLight(...)`.
*/
void send(u16 pulses[]); // defined in `LinkIR.cpp`
/**
* Receives a generic IR signal modulated at standard 38kHz. Returns whether
* something was received or not.
* @param pulses An array to be filled with u16 numbers describing the signal.
* Even indices are *marks* (IR on), odd indices are *spaces* (IR off), and
* `0` ends the signal.
* @param maxEntries Maximum capacity of the `pulses` array.
* @param startTimeout Number of microseconds before the first *mark* to abort
* the reception.
* @param signalTimeout Number of microseconds inside a *space* after
* terminating the reception. It doesn't start to count until the first
* *mark*.
*/
bool receive(
u16 pulses[],
u32 maxEntries,
u32 startTimeout = NO_TIMEOUT,
u32 signalTimeout = DEFAULT_RECEIVE_TIMEOUT); // defined in `LinkIR.cpp`
/**
* Turns the output IR LED ON/OFF through the `SO` pin (HIGH = ON).
* @param on Whether the light should be ON.
* \warning Add some pauses after every 10µs!
*/
void setLight(bool on) { linkGPIO.writePin(Pin::SO, on); }
/**
* Returns whether the output IR LED is ON or OFF.
*/
bool isEmittingLight() { return linkGPIO.readPin(Pin::SO); }
/**
* Returns whether a remote light signal is detected through the `SI` pin
* (LOW = DETECTED) or not.
*/
bool isDetectingLight() { return !linkGPIO.readPin(Pin::SI); }
/**
* @brief This method is called by the SERIAL interrupt handler.
* \warning This is internal API!
*/
void _onSerial(); // defined in `LinkIR.cpp`
struct Config {
u8 primaryTimerId;
u8 secondaryTimerId;
};
/**
* @brief LinkIR configuration.
* \warning `deactivate()` first, change the config, and `activate()` again!
*/
Config config;
private:
LinkGPIO linkGPIO;
volatile bool isEnabled = false;
volatile bool detected = false;
vu32 firstLightTime = 0;
vu32 lastLightTime = 0;
vu32 transitionCount = 0;
void addNECByte(u16 pulses[], u32& i, u8 value) {
for (u32 b = 0; b < 8; b++) {
pulses[i++] = NEC_PULSE;
pulses[i++] = (value >> b) & 1 ? NEC_SPACE_1 : NEC_SPACE_0;
}
}
bool isWithinNECTolerance(u16 measured, u16 expected) {
if (measured == 0)
return false;
u32 tolerance = (expected * NEC_TOLERANCE_PERCENTAGE) / 100;
return measured >= expected - tolerance && measured <= expected + tolerance;
}
void generate38kHzSignal(u32 microseconds); // defined in ASM (`LinkIR.cpp`)
void waitMicroseconds(u32 microseconds); // defined in ASM (`LinkIR.cpp`)
void resetState() {
detected = false;
firstLightTime = 0;
lastLightTime = 0;
transitionCount = 0;
}
void startCount() {
Link::_REG_TM[config.primaryTimerId].start = 0;
Link::_REG_TM[config.secondaryTimerId].start = 0;
Link::_REG_TM[config.primaryTimerId].cnt = 0;
Link::_REG_TM[config.secondaryTimerId].cnt = 0;
Link::_REG_TM[config.secondaryTimerId].cnt =
Link::_TM_ENABLE | Link::_TM_CASCADE;
Link::_REG_TM[config.primaryTimerId].cnt =
Link::_TM_ENABLE | Link::_TM_FREQ_1;
}
LINK_INLINE u32 getCount() {
return (Link::_REG_TM[config.primaryTimerId].count |
(Link::_REG_TM[config.secondaryTimerId].count << 16));
}
u32 stopCount() {
Link::_REG_TM[config.primaryTimerId].cnt = 0;
Link::_REG_TM[config.secondaryTimerId].cnt = 0;
return getCount();
}
};
extern LinkIR* linkIR;
/**
* @brief SERIAL interrupt handler.
*/
inline void LINK_IR_ISR_SERIAL() {
linkIR->_onSerial();
}
#endif // LINK_IR_H