Skip to content

Commit 0e67c4e

Browse files
authored
Merge pull request #571 from mcci-catena/issue568
The primary purpose of this PR is to fix #524, allowing the LMIC to run without disabling interrupts at all, and without requiring any changes to underlying BSPs. When configured for interrupts, interrupts simply cause the current time stamp to be captured. The secondary ISR is run as part of os_runloop_once(). This should also fix #503, and address #528, #558, #548, and others. In addition, since we're updating the radio driver, I addressed #524. In testing, I discovered a subtle bug with one of our applications that uses LMIC_sendAlive() -- there was a path when sending piggybacked MAC data with a poll that would result in trying to take the port 0 path instead. This caused problems with ChirpStack and TTN, at least. (This is #570.) Finally, updated to use Arduino IDE 1.8.12 in test. Version of library changes to v3.1.0.20.
2 parents 59df2fa + c2e1f64 commit 0e67c4e

File tree

9 files changed

+175
-63
lines changed

9 files changed

+175
-63
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ sudo: true
1919

2020
env:
2121
global:
22-
- IDE_VERSION=1.8.10
22+
- IDE_VERSION=1.8.12
2323
matrix:
2424
- TARGET=samd
2525
- TARGET=stm32l0

README.md

Lines changed: 35 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,9 @@ function uflt12f(rawUflt12)
11951215

11961216
- HEAD has the following changes:
11971217

1218+
- [#570](https://github.com/mcci-catena/arduino-lmic/issue/570) corrects handling of piggy-back MAC responses when sending an `LMIC_sendAlive()` (`OPMODE_POLL`) message.
1219+
- [#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.
1220+
- [#568](https://github.com/mcci-catena/arduino-lmic/issue/568) improves documentation for the radio driver.
11981221
- [#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.
11991222

12001223
- 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, "Number of interrupt-sensitive lines must be less than number of GPIOs");
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, "Number of interrupt lines must be set to 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 (interrupt_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, "number of interrupts must be 3 for initializing interrupt_fns[]");
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.c

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -889,10 +889,14 @@ scan_mac_cmds(
889889
uint8_t cmd;
890890

891891
LMIC.pendMacLen = 0;
892-
if (port == 0)
892+
if (port == 0) {
893+
// port zero: mac data is in the normal payload, and there can't be
894+
// piggyback mac data.
893895
LMIC.pendMacPiggyback = 0;
894-
else
896+
} else {
897+
// port is either -1 (no port) or non-zero (piggyback): treat as piggyback.
895898
LMIC.pendMacPiggyback = 1;
899+
}
896900

897901
while( oidx < olen ) {
898902
bit_t response_fit;
@@ -1861,7 +1865,8 @@ static bit_t buildDataFrame (void) {
18611865
// highest importance are the ones in the pendMac buffer.
18621866
int end = OFF_DAT_OPTS;
18631867

1864-
if (LMIC.pendTxPort != 0 && LMIC.pendMacPiggyback && LMIC.pendMacLen != 0) {
1868+
// Send piggyback data if: !txdata or txport != 0
1869+
if ((! txdata || LMIC.pendTxPort != 0) && LMIC.pendMacPiggyback && LMIC.pendMacLen != 0) {
18651870
os_copyMem(LMIC.frame + end, LMIC.pendMacData, LMIC.pendMacLen);
18661871
end += LMIC.pendMacLen;
18671872
}
@@ -2786,6 +2791,11 @@ void LMIC_reset (void) {
27862791
LMIC.adrEnabled = FCT_ADREN;
27872792
resetJoinParams();
27882793
LMIC.rxDelay = DELAY_DNW1;
2794+
// LMIC.pendMacLen = 0;
2795+
// LMIC.pendMacPiggyback = 0;
2796+
// LMIC.dn2Ans = 0;
2797+
// LMIC.macDlChannelAns = 0;
2798+
// LMIC.macRxTimingSetupAns = 0;
27892799
#if !defined(DISABLE_PING)
27902800
LMIC.ping.freq = FREQ_PING; // defaults for ping
27912801
LMIC.ping.dr = DR_PING; // ditto

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/lmic_compliance.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ static void acSendUplink(void) {
727727
__func__,
728728
eSend,
729729
(unsigned) downlink & 0xFFFF,
730-
LMIC.client.txMessageCb
730+
(unsigned) sizeof(payload)
731731
);
732732
LMIC_Compliance.eventflags |= LMIC_COMPLIANCE_EVENT_UPLINK_COMPLETE;
733733
fsmEval();

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) {

0 commit comments

Comments
 (0)