Skip to content

Commit e2d6f48

Browse files
committed
Fix #524: simplify interrupt handling
1 parent 690ea58 commit e2d6f48

File tree

6 files changed

+102
-50
lines changed

6 files changed

+102
-50
lines changed

README.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ requires C99 mode to be enabled by default.
8484
- [LoRa Nexus by Ideetron](#lora-nexus-by-ideetron)
8585
- [Example Sketches](#example-sketches)
8686
- [Timing](#timing)
87+
- [Controlling protocol timing](#controlling-protocol-timing)
8788
- [`LMIC_setClockError()`](#lmic_setclockerror)
89+
- [Interrupts and Arduino system timing](#interrupts-and-arduino-system-timing)
8890
- [Downlink data rate](#downlink-data-rate)
8991
- [Encoding Utilities](#encoding-utilities)
9092
- [sflt16](#sflt16)
@@ -290,7 +292,7 @@ Configures the library for use with an sx1276 transceiver.
290292

291293
`#define LMIC_USE_INTERRUPTS`
292294

293-
If defined, configures the library to use interrupts for detecting events from the transceiver. If left undefined, the library will poll for events from the transceiver. See [Timing](#timing) for more info.
295+
If defined, configures the library to use interrupts for detecting events from the transceiver. If left undefined, the library will poll for events from the transceiver. See [Timing](#timing) for more info. Be aware that interrupts are not tested or supported on many platforms.
294296

295297
### Disabling PING
296298

@@ -812,32 +814,36 @@ This library provides several examples.
812814
813815
The library is
814816
responsible for keeping track of time of certain network events, and scheduling
815-
other events relative to those events. In particular, the library must note
817+
other events relative to those events. For Class A uplink transmissions, the library must note
816818
when a packet finishes transmitting, so it can open up the RX1 and RX2
817819
receive windows at a fixed time after the end of transmission. The library does this
818820
by watching for rising edges on the DIO0 output of the SX127x, and noting the time.
819821
820822
The library observes and processes rising edges on the pins as part of `os_runloop()` processing.
821823
This can be configured in one of two ways (see
822-
[Controlling use of interrupts](#controlling-use-of-interrupts)).
824+
[Controlling use of interrupts](#controlling-use-of-interrupts)). See [Interrupts and Arduino system timing](#interrupts-and-arduino-system-timing) for implementation details.
823825
824-
By default, the routine `hal_io_check()`
826+
By default, the library
825827
polls the enabled pins to determine whether an event has occurred. This approach
826828
allows use of any CPU pin to sense the DIOs, and makes no assumptions about
827829
interrupts. However, it means that the end-of-transmit event is not observed
828-
(and time-stamped) until `os_runloop()` is called.
830+
(and time-stamped) until `os_runloop_once()` is called.
829831
830832
Optionally, you can configure the LMIC library to use interrupts. The
831833
interrupt handlers capture the time of
832-
the event. Actual processing is done the next time that `os_runloop()`
834+
the event. Actual processing is done the next time that `os_runloop_once()`
833835
is called, using the captured time. However, this requires that the
834-
DIO pins be wired to Arduino pins that support rising-edge interrupts.
836+
DIO pins be wired to Arduino pins that support rising-edge interrupts,
837+
and it may require custom initialization code on your platform to
838+
hook up the interrupts.
835839
836-
Fortunately, LoRa is a fairly slow protocol and the timing of the
837-
receive windows is not super critical. To synchronize transmitter and
838-
receiver, a preamble is first transmitted. Using LoRaWAN, this preamble
839-
consists of 8 symbols, of which the receiver needs to see 4 symbols to
840-
lock on. The current implementation tries to enable the receiver for 6
840+
### Controlling protocol timing
841+
842+
The timing of end-of-transmit interrupts is used to determine when to open the downlink receive window. Because of the considerations above, some inaccuracy in the time stamp for the end-of-transmit interrupt is inevitable.
843+
844+
Fortunately, the timing of the receive windows at the device need not be extremely accurate; the LMIC has to turn on the receiver early enough to capture a downlink
845+
from the gateway and must leave the receiver on long enough to compensate for timing
846+
errors due to various inaccuracies. To make it easier for the device to catch downlinks, the gateway first transmits a preamble consisting of 8 symbols. The SX127x receiver needs to see at least 4 symbols to detect a message. The Arduino LMIC tries to enable the receiver for 6
841847
symbol times slightly before the start of the receive window.
842848
843849
The HAL bases all timing on the Arduino `micros()` timer, which has a platform-specific
@@ -886,6 +892,20 @@ For a variety of reasons, the LMIC normally ignores clock errors greater than 40
886892
887893
This clock error is not reset by `LMIC_reset()`.
888894
895+
### Interrupts and Arduino system timing
896+
897+
The IBM LMIC used as the basis for this code disables interrupts while the radio driver is active, to prevent reentrancy via `radio_irq_handler()` at unexpected moments. It uses `os_getTime()`, and assumes that `os_getTime()` still works when interrupts were disabled. This causes problems on Arduino platforms. Most board support packages use interrupts to advance `millis()` and `micros()`, and with these BSPs, `millis()` and `micros()` return incorrect values while interrupts are disabled. Although some BSPs (like the ones provided by MCCI) provide real time correctly while interrupts are disabled, this is not portable. It's not practical to make such changes in every BSP.
898+
899+
To avoid this, the LMIC processes events in several steps; these steps ensure that `radio_irq_handler_v2()` is only called at predictable times.
900+
901+
1. If interrupts are enabled via `LMIC_USE_INTERRUPTS`, hardware interrupts catch the time of the interrupt and record that the interrupt occurred. These routines rely on hardware edge-sensitive interrupts. If your hardware interrupts are level-sensitive, you must mask the interrupt somehow at the ISR. You can't use SPI routines to talk to the radio, because this may leave the SPI system and the radio in undefined states. In this configuration, `hal_io_pollIRQs()` exists but is a no-op.
902+
903+
2. If interrupts are not enabled via `LMIC_USE_INTERRUPTS`, the digital I/O lines are polled every so often by calling the routine `hal_io_pollIRQs()`. This routine watches for edges on the relevant digital I/O lines, and records the time of transition.
904+
905+
3. The LMIC `os_runloop_once()` routine calls `hal_processPendingIRQs()`. This routine uses the timestamps captured by the hardware ISRs and `hal_io_pollIRQs()` to invoke `radio_irq_hander_v2()` with the appropriate information. `hal_processPendingIRQs()` in turn calls `hal_io_pollIRQs()` (in case interrupts are not configured).
906+
907+
4. For compatibility with older versions of the Arduino LMIC, `hal_enableIRQs()` also calls `hal_io_pollIRQs()` when enabling interrupts. However, it does not dispatch the interrupts to `radio_irq_handler_v2()`; this must be done by a subsequent call to `hal_processPendingIRQs()`.
908+
889909
## Downlink data rate
890910
891911
Note that the data rate used for downlink packets in the RX2 window varies by region. Consult your network's manual for any divergences from the LoRaWAN Regional Parameters. This library assumes that the network follows the regional default.
@@ -1195,6 +1215,8 @@ function uflt12f(rawUflt12)
11951215

11961216
- HEAD has the following changes:
11971217

1218+
- [#524](https://github.com/mcci-catena/arduino-lmic/issue/524) corrects handling of interrupt disable, and slightly refactors the low-level interrupt handling wrappers for clarity. With this change, `radio_irq_handler_v2()` is never called except from the run loop, and so the radio driver need not (and does not) disable interrupts. Version is v3.1.0.20.
1219+
- [#568](https://github.com/mcci-catena/arduino-lmic/issue/568) improves documentation for the radio driver.
11981220
- [#537](https://github.com/mcci-catena/arduino-lmic/pull/537) fixes a compile error in SX1272 support. (Thanks @ricaun.) Version is v3.1.0.10.
11991221

12001222
- v3.1.0 officially adopts the changes from v3.0.99. There were dozens of changes; check the GitHub issue logs and change logs. This was a breaking release (due to changes in data layout in the LMIC structure; the structure is accessed by apps).

src/hal/hal.cpp

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,75 +81,101 @@ s1_t hal_getRssiCal (void) {
8181
return plmic_pins->rssi_cal;
8282
}
8383

84+
//--------------------
85+
// Interrupt handling
86+
//--------------------
87+
static constexpr unsigned NUM_DIO_INTERRUPT = 3;
88+
static_assert(NUM_DIO_INTERRUPT <= NUM_DIO);
89+
static ostime_t interrupt_time[NUM_DIO_INTERRUPT] = {0};
90+
8491
#if !defined(LMIC_USE_INTERRUPTS)
8592
static void hal_interrupt_init() {
8693
pinMode(plmic_pins->dio[0], INPUT);
8794
if (plmic_pins->dio[1] != LMIC_UNUSED_PIN)
8895
pinMode(plmic_pins->dio[1], INPUT);
8996
if (plmic_pins->dio[2] != LMIC_UNUSED_PIN)
9097
pinMode(plmic_pins->dio[2], INPUT);
98+
static_assert(NUM_DIO_INTERRUPT == 3);
9199
}
92100

93-
static bool dio_states[NUM_DIO] = {0};
94-
static void hal_io_check() {
101+
static bool dio_states[NUM_DIO_INTERRUPT] = {0};
102+
void hal_pollPendingIRQs_helper() {
95103
uint8_t i;
96-
for (i = 0; i < NUM_DIO; ++i) {
104+
for (i = 0; i < NUM_DIO_INTERRUPT; ++i) {
97105
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
98106
continue;
99107

100108
if (dio_states[i] != digitalRead(plmic_pins->dio[i])) {
101109
dio_states[i] = !dio_states[i];
102-
if (dio_states[i])
103-
radio_irq_handler(i);
110+
if (dio_states[i] && interrupt_time[i] == 0) {
111+
ostime_t const now = os_getTime();
112+
interrupt_time[i] = now ? now : 1;
113+
}
104114
}
105115
}
106116
}
107117

108118
#else
109119
// Interrupt handlers
110-
static ostime_t interrupt_time[NUM_DIO] = {0};
111120

112121
static void hal_isrPin0() {
113-
ostime_t now = os_getTime();
114-
interrupt_time[0] = now ? now : 1;
122+
if (interrupt_time[0] == 0) {
123+
ostime_t now = os_getTime();
124+
interrupt_time[0] = now ? now : 1;
125+
}
115126
}
116127
static void hal_isrPin1() {
117-
ostime_t now = os_getTime();
118-
interrupt_time[1] = now ? now : 1;
128+
if (interrupt_time[1] == 0) {
129+
ostime_t now = os_getTime();
130+
interrupt_time[1] = now ? now : 1;
131+
}
119132
}
120133
static void hal_isrPin2() {
121-
ostime_t now = os_getTime();
122-
interrupt_time[2] = now ? now : 1;
134+
if (interupt_time[2] == 0) {
135+
ostime_t now = os_getTime();
136+
interrupt_time[2] = now ? now : 1;
137+
}
123138
}
124139

125140
typedef void (*isr_t)();
126-
static isr_t interrupt_fns[NUM_DIO] = {hal_isrPin0, hal_isrPin1, hal_isrPin2};
141+
static const isr_t interrupt_fns[NUM_DIO_INTERRUPT] = {hal_isrPin0, hal_isrPin1, hal_isrPin2};
142+
static_assert(NUM_DIO_INTERRUPT == 3);
127143

128144
static void hal_interrupt_init() {
129-
for (uint8_t i = 0; i < NUM_DIO; ++i) {
145+
for (uint8_t i = 0; i < NUM_DIO_INTERRUPT; ++i) {
130146
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
131147
continue;
132148

133149
pinMode(plmic_pins->dio[i], INPUT);
134150
attachInterrupt(digitalPinToInterrupt(plmic_pins->dio[i]), interrupt_fns[i], RISING);
135151
}
136152
}
153+
#endif // LMIC_USE_INTERRUPTS
137154

138-
static void hal_io_check() {
155+
void hal_processPendingIRQs() {
139156
uint8_t i;
140-
for (i = 0; i < NUM_DIO; ++i) {
157+
for (i = 0; i < NUM_DIO_INTERRUPT; ++i) {
141158
ostime_t iTime;
142159
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
143160
continue;
144161

162+
// NOTE(tmm@mcci.com): if using interrupts, this next step
163+
// assumes uniprocessor and fairly strict memory ordering
164+
// semantics relative to ISRs. It would be better to use
165+
// interlocked-exchange, but that's really far beyond
166+
// Arduino semantics. Because our ISRs use "first time
167+
// stamp" semantics, we don't have a value-race. But if
168+
// we were to disable ints here, we might observe a second
169+
// edge that we'll otherwise miss. Not a problem in this
170+
// use case, as the radio won't release IRQs until we
171+
// explicitly clear them.
145172
iTime = interrupt_time[i];
146173
if (iTime) {
147174
interrupt_time[i] = 0;
148175
radio_irq_handler_v2(i, iTime);
149176
}
150177
}
151178
}
152-
#endif // LMIC_USE_INTERRUPTS
153179

154180
// -----------------------------------------------------------------------------
155181
// SPI
@@ -319,15 +345,19 @@ void hal_enableIRQs () {
319345
if(--irqlevel == 0) {
320346
interrupts();
321347

348+
#if !defined(LMIC_USE_INTERRUPTS)
322349
// Instead of using proper interrupts (which are a bit tricky
323350
// and/or not available on all pins on AVR), just poll the pin
324351
// values. Since os_runloop disables and re-enables interrupts,
325352
// putting this here makes sure we check at least once every
326353
// loop.
327354
//
328355
// As an additional bonus, this prevents the can of worms that
329-
// we would otherwise get for running SPI transfers inside ISRs
330-
hal_io_check();
356+
// we would otherwise get for running SPI transfers inside ISRs.
357+
// We merely collect the edges and timestamps here; we wait for
358+
// a call to hal_processPendingIRQs() before dispatching.
359+
hal_pollPendingIRQs_helper();
360+
#endif /* !defined(LMIC_USE_INTERRUPTS) */
331361
}
332362
}
333363

src/lmic/hal.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ uint8_t hal_getTxPowerPolicy(
169169
u4_t freq
170170
);
171171

172+
void hal_pollPendingIRQs_helper();
173+
void hal_processPendingIRQs(void);
174+
175+
/// \brief check for any pending interrupts: stub if interrupts are enabled.
176+
static void inline hal_pollPendingIRQs(void)
177+
{
178+
#if !defined(LMIC_USE_INTERRUPTS)
179+
hal_pollPendingIRQs_helper();
180+
#endif /* !defined(LMIC_USE_INTERRUPTS) */
181+
}
182+
172183
#ifdef __cplusplus
173184
} // extern "C"
174185
#endif

src/lmic/lmic.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* Copyright (c) 2014-2016 IBM Corporation.
33
* Copyright (c) 2016 Matthijs Kooijman.
4-
* Copyright (c) 2016-2019 MCCI Corporation.
4+
* Copyright (c) 2016-2020 MCCI Corporation.
55
* All rights reserved.
66
*
77
* Redistribution and use in source and binary forms, with or without
@@ -105,7 +105,7 @@ extern "C"{
105105
#define ARDUINO_LMIC_VERSION_CALC(major, minor, patch, local) \
106106
((((major)*UINT32_C(1)) << 24) | (((minor)*UINT32_C(1)) << 16) | (((patch)*UINT32_C(1)) << 8) | (((local)*UINT32_C(1)) << 0))
107107

108-
#define ARDUINO_LMIC_VERSION ARDUINO_LMIC_VERSION_CALC(3, 1, 0, 10) /* v3.1.0.10 */
108+
#define ARDUINO_LMIC_VERSION ARDUINO_LMIC_VERSION_CALC(3, 1, 0, 20) /* v3.1.0.20 */
109109

110110
#define ARDUINO_LMIC_VERSION_GET_MAJOR(v) \
111111
((((v)*UINT32_C(1)) >> 24u) & 0xFFu)

src/lmic/oslmic.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ void os_runloop () {
139139

140140
void os_runloop_once() {
141141
osjob_t* j = NULL;
142+
hal_processPendingIRQs();
143+
142144
hal_disableIRQs();
143145
// check for runnable jobs
144146
if(OS.runnablejobs) {

src/lmic/radio.c

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,9 +1089,9 @@ static void startrx (u1_t rxmode) {
10891089
//! are enabled.
10901090
//! - The `hal_spi_..()` functions must be ready for use.
10911091
//!
1092+
//! Generally, all these are satisfied by a call to `hal_init_with_pinmap()`.
1093+
//!
10921094
int radio_init () {
1093-
hal_disableIRQs();
1094-
10951095
requestModuleActive(1);
10961096

10971097
// manually reset radio
@@ -1154,7 +1154,6 @@ int radio_init () {
11541154

11551155
opmode(OPMODE_SLEEP);
11561156

1157-
hal_enableIRQs();
11581157
return 1;
11591158
}
11601159

@@ -1173,9 +1172,7 @@ u1_t radio_rand1 () {
11731172
}
11741173

11751174
u1_t radio_rssi () {
1176-
hal_disableIRQs();
11771175
u1_t r = readReg(LORARegRssiValue);
1178-
hal_enableIRQs();
11791176
return r;
11801177
}
11811178

@@ -1229,13 +1226,8 @@ void radio_monitor_rssi(ostime_t nTicks, oslmic_radio_rssi_t *pRssi) {
12291226
tBegin = os_getTime();
12301227
rssiMax = 0;
12311228

1232-
/* XXX(tanupoo)
1233-
* In this loop, micros() in os_getTime() returns a past time sometimes.
1234-
* At least, it happens on Dragino LoRa Mini.
1235-
* the return value of micros() looks not to be stable in IRQ disabled.
1236-
* Once it happens, this loop never exit infinitely.
1237-
* In order to prevent it, it enables IRQ before calling os_getTime(),
1238-
* disable IRQ again after that.
1229+
/* Per bug report from tanupoo, it's critical that interrupts be enabled
1230+
* in the loop below so that `os_getTime()` always advances.
12391231
*/
12401232
do {
12411233
ostime_t now;
@@ -1248,10 +1240,7 @@ void radio_monitor_rssi(ostime_t nTicks, oslmic_radio_rssi_t *pRssi) {
12481240
rssiMin = rssiNow;
12491241
rssiSum += rssiNow;
12501242
++rssiN;
1251-
// TODO(tmm@mcci.com) move this to os_getTime().
1252-
hal_enableIRQs();
12531243
now = os_getTime();
1254-
hal_disableIRQs();
12551244
notDone = now - (tBegin + nTicks) < 0;
12561245
} while (notDone);
12571246

@@ -1419,7 +1408,6 @@ which will cause `LMIC.osjob` to be scheduled with its current function.
14191408
*/
14201409

14211410
void os_radio (u1_t mode) {
1422-
hal_disableIRQs();
14231411
switch (mode) {
14241412
case RADIO_RST:
14251413
// put radio to sleep
@@ -1448,7 +1436,6 @@ void os_radio (u1_t mode) {
14481436
startrx(RXMODE_SCAN); // buf=LMIC.frame
14491437
break;
14501438
}
1451-
hal_enableIRQs();
14521439
}
14531440

14541441
ostime_t os_getRadioRxRampup (void) {

0 commit comments

Comments
 (0)