Based on Pflichtenheft v2.2, documents the full system design including daemon, API, frontend, and infrastructure with agreed deviations (Axum 0.8, SQLx 0.8, Vite 6, Tailwind 4). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
515 lines
16 KiB
C++
515 lines
16 KiB
C++
/*******************************************************************************
|
|
* Copyright (c) 2015 Matthijs Kooijman
|
|
* Copyright (c) 2018-2019 MCCI Corporation
|
|
*
|
|
* All rights reserved. This program and the accompanying materials
|
|
* are made available under the terms of the Eclipse Public License v1.0
|
|
* which accompanies this distribution, and is available at
|
|
* http://www.eclipse.org/legal/epl-v10.html
|
|
*
|
|
* This the HAL to run LMIC on top of the Arduino environment.
|
|
*******************************************************************************/
|
|
|
|
#include <Arduino.h>
|
|
#include <SPI.h>
|
|
// include all the lmic header files, including ../lmic/hal.h
|
|
#include "../lmic.h"
|
|
// include the C++ hal.h
|
|
#include "hal.h"
|
|
// we may need some things from stdio.
|
|
#include <stdio.h>
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// I/O
|
|
|
|
static const Arduino_LMIC::HalPinmap_t *plmic_pins;
|
|
static Arduino_LMIC::HalConfiguration_t *pHalConfig;
|
|
static Arduino_LMIC::HalConfiguration_t nullHalConig;
|
|
static hal_failure_handler_t* custom_hal_failure_handler = NULL;
|
|
|
|
static void hal_interrupt_init(); // Fwd declaration
|
|
|
|
static void hal_io_init () {
|
|
// NSS and DIO0 are required, DIO1 is required for LoRa, DIO2 for FSK
|
|
ASSERT(plmic_pins->nss != LMIC_UNUSED_PIN);
|
|
ASSERT(plmic_pins->dio[0] != LMIC_UNUSED_PIN);
|
|
ASSERT(plmic_pins->dio[1] != LMIC_UNUSED_PIN || plmic_pins->dio[2] != LMIC_UNUSED_PIN);
|
|
|
|
// Serial.print("nss: "); Serial.println(plmic_pins->nss);
|
|
// Serial.print("rst: "); Serial.println(plmic_pins->rst);
|
|
// Serial.print("dio[0]: "); Serial.println(plmic_pins->dio[0]);
|
|
// Serial.print("dio[1]: "); Serial.println(plmic_pins->dio[1]);
|
|
// Serial.print("dio[2]: "); Serial.println(plmic_pins->dio[2]);
|
|
|
|
// initialize SPI chip select to high (it's active low)
|
|
digitalWrite(plmic_pins->nss, HIGH);
|
|
pinMode(plmic_pins->nss, OUTPUT);
|
|
|
|
if (plmic_pins->rxtx != LMIC_UNUSED_PIN) {
|
|
// initialize to RX
|
|
digitalWrite(plmic_pins->rxtx, LOW != plmic_pins->rxtx_rx_active);
|
|
pinMode(plmic_pins->rxtx, OUTPUT);
|
|
}
|
|
if (plmic_pins->rst != LMIC_UNUSED_PIN) {
|
|
// initialize RST to floating
|
|
pinMode(plmic_pins->rst, INPUT);
|
|
}
|
|
|
|
hal_interrupt_init();
|
|
}
|
|
|
|
// val == 1 => tx
|
|
void hal_pin_rxtx (u1_t val) {
|
|
if (plmic_pins->rxtx != LMIC_UNUSED_PIN)
|
|
digitalWrite(plmic_pins->rxtx, val != plmic_pins->rxtx_rx_active);
|
|
}
|
|
|
|
// set radio RST pin to given value (or keep floating!)
|
|
void hal_pin_rst (u1_t val) {
|
|
if (plmic_pins->rst == LMIC_UNUSED_PIN)
|
|
return;
|
|
|
|
if(val == 0 || val == 1) { // drive pin
|
|
digitalWrite(plmic_pins->rst, val);
|
|
pinMode(plmic_pins->rst, OUTPUT);
|
|
} else { // keep pin floating
|
|
pinMode(plmic_pins->rst, INPUT);
|
|
}
|
|
}
|
|
|
|
s1_t hal_getRssiCal (void) {
|
|
return plmic_pins->rssi_cal;
|
|
}
|
|
|
|
//--------------------
|
|
// Interrupt handling
|
|
//--------------------
|
|
static constexpr unsigned NUM_DIO_INTERRUPT = 3;
|
|
static_assert(NUM_DIO_INTERRUPT <= NUM_DIO, "Number of interrupt-sensitive lines must be less than number of GPIOs");
|
|
static ostime_t interrupt_time[NUM_DIO_INTERRUPT] = {0};
|
|
|
|
#if !defined(LMIC_USE_INTERRUPTS)
|
|
static void hal_interrupt_init() {
|
|
pinMode(plmic_pins->dio[0], INPUT);
|
|
if (plmic_pins->dio[1] != LMIC_UNUSED_PIN)
|
|
pinMode(plmic_pins->dio[1], INPUT);
|
|
if (plmic_pins->dio[2] != LMIC_UNUSED_PIN)
|
|
pinMode(plmic_pins->dio[2], INPUT);
|
|
static_assert(NUM_DIO_INTERRUPT == 3, "Number of interrupt lines must be set to 3");
|
|
}
|
|
|
|
static bool dio_states[NUM_DIO_INTERRUPT] = {0};
|
|
void hal_pollPendingIRQs_helper() {
|
|
uint8_t i;
|
|
for (i = 0; i < NUM_DIO_INTERRUPT; ++i) {
|
|
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
|
|
continue;
|
|
|
|
if (dio_states[i] != digitalRead(plmic_pins->dio[i])) {
|
|
dio_states[i] = !dio_states[i];
|
|
if (dio_states[i] && interrupt_time[i] == 0) {
|
|
ostime_t const now = os_getTime();
|
|
interrupt_time[i] = now ? now : 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#else
|
|
// Interrupt handlers
|
|
|
|
static void hal_isrPin0() {
|
|
if (interrupt_time[0] == 0) {
|
|
ostime_t now = os_getTime();
|
|
interrupt_time[0] = now ? now : 1;
|
|
}
|
|
}
|
|
static void hal_isrPin1() {
|
|
if (interrupt_time[1] == 0) {
|
|
ostime_t now = os_getTime();
|
|
interrupt_time[1] = now ? now : 1;
|
|
}
|
|
}
|
|
static void hal_isrPin2() {
|
|
if (interrupt_time[2] == 0) {
|
|
ostime_t now = os_getTime();
|
|
interrupt_time[2] = now ? now : 1;
|
|
}
|
|
}
|
|
|
|
typedef void (*isr_t)();
|
|
static const isr_t interrupt_fns[NUM_DIO_INTERRUPT] = {hal_isrPin0, hal_isrPin1, hal_isrPin2};
|
|
static_assert(NUM_DIO_INTERRUPT == 3, "number of interrupts must be 3 for initializing interrupt_fns[]");
|
|
|
|
static void hal_interrupt_init() {
|
|
for (uint8_t i = 0; i < NUM_DIO_INTERRUPT; ++i) {
|
|
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
|
|
continue;
|
|
|
|
pinMode(plmic_pins->dio[i], INPUT);
|
|
attachInterrupt(digitalPinToInterrupt(plmic_pins->dio[i]), interrupt_fns[i], RISING);
|
|
}
|
|
}
|
|
#endif // LMIC_USE_INTERRUPTS
|
|
|
|
void hal_processPendingIRQs() {
|
|
uint8_t i;
|
|
for (i = 0; i < NUM_DIO_INTERRUPT; ++i) {
|
|
ostime_t iTime;
|
|
if (plmic_pins->dio[i] == LMIC_UNUSED_PIN)
|
|
continue;
|
|
|
|
// NOTE(tmm@mcci.com): if using interrupts, this next step
|
|
// assumes uniprocessor and fairly strict memory ordering
|
|
// semantics relative to ISRs. It would be better to use
|
|
// interlocked-exchange, but that's really far beyond
|
|
// Arduino semantics. Because our ISRs use "first time
|
|
// stamp" semantics, we don't have a value-race. But if
|
|
// we were to disable ints here, we might observe a second
|
|
// edge that we'll otherwise miss. Not a problem in this
|
|
// use case, as the radio won't release IRQs until we
|
|
// explicitly clear them.
|
|
iTime = interrupt_time[i];
|
|
if (iTime) {
|
|
interrupt_time[i] = 0;
|
|
radio_irq_handler_v2(i, iTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SPI
|
|
|
|
static void hal_spi_init () {
|
|
SPI.begin();
|
|
}
|
|
|
|
static void hal_spi_trx(u1_t cmd, u1_t* buf, size_t len, bit_t is_read) {
|
|
uint32_t spi_freq;
|
|
u1_t nss = plmic_pins->nss;
|
|
|
|
if ((spi_freq = plmic_pins->spi_freq) == 0)
|
|
spi_freq = LMIC_SPI_FREQ;
|
|
|
|
SPISettings settings(spi_freq, MSBFIRST, SPI_MODE0);
|
|
SPI.beginTransaction(settings);
|
|
digitalWrite(nss, 0);
|
|
|
|
SPI.transfer(cmd);
|
|
|
|
for (; len > 0; --len, ++buf) {
|
|
u1_t data = is_read ? 0x00 : *buf;
|
|
data = SPI.transfer(data);
|
|
if (is_read)
|
|
*buf = data;
|
|
}
|
|
|
|
digitalWrite(nss, 1);
|
|
SPI.endTransaction();
|
|
}
|
|
|
|
void hal_spi_write(u1_t cmd, const u1_t* buf, size_t len) {
|
|
hal_spi_trx(cmd, (u1_t*)buf, len, 0);
|
|
}
|
|
|
|
void hal_spi_read(u1_t cmd, u1_t* buf, size_t len) {
|
|
hal_spi_trx(cmd, buf, len, 1);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// TIME
|
|
|
|
static void hal_time_init () {
|
|
// Nothing to do
|
|
}
|
|
|
|
u4_t hal_ticks () {
|
|
// Because micros() is scaled down in this function, micros() will
|
|
// overflow before the tick timer should, causing the tick timer to
|
|
// miss a significant part of its values if not corrected. To fix
|
|
// this, the "overflow" serves as an overflow area for the micros()
|
|
// counter. It consists of three parts:
|
|
// - The US_PER_OSTICK upper bits are effectively an extension for
|
|
// the micros() counter and are added to the result of this
|
|
// function.
|
|
// - The next bit overlaps with the most significant bit of
|
|
// micros(). This is used to detect micros() overflows.
|
|
// - The remaining bits are always zero.
|
|
//
|
|
// By comparing the overlapping bit with the corresponding bit in
|
|
// the micros() return value, overflows can be detected and the
|
|
// upper bits are incremented. This is done using some clever
|
|
// bitwise operations, to remove the need for comparisons and a
|
|
// jumps, which should result in efficient code. By avoiding shifts
|
|
// other than by multiples of 8 as much as possible, this is also
|
|
// efficient on AVR (which only has 1-bit shifts).
|
|
static uint8_t overflow = 0;
|
|
|
|
// Scaled down timestamp. The top US_PER_OSTICK_EXPONENT bits are 0,
|
|
// the others will be the lower bits of our return value.
|
|
uint32_t scaled = micros() >> US_PER_OSTICK_EXPONENT;
|
|
// Most significant byte of scaled
|
|
uint8_t msb = scaled >> 24;
|
|
// Mask pointing to the overlapping bit in msb and overflow.
|
|
const uint8_t mask = (1 << (7 - US_PER_OSTICK_EXPONENT));
|
|
// Update overflow. If the overlapping bit is different
|
|
// between overflow and msb, it is added to the stored value,
|
|
// so the overlapping bit becomes equal again and, if it changed
|
|
// from 1 to 0, the upper bits are incremented.
|
|
overflow += (msb ^ overflow) & mask;
|
|
|
|
// Return the scaled value with the upper bits of stored added. The
|
|
// overlapping bit will be equal and the lower bits will be 0, so
|
|
// bitwise or is a no-op for them.
|
|
return scaled | ((uint32_t)overflow << 24);
|
|
|
|
// 0 leads to correct, but overly complex code (it could just return
|
|
// micros() unmodified), 8 leaves no room for the overlapping bit.
|
|
static_assert(US_PER_OSTICK_EXPONENT > 0 && US_PER_OSTICK_EXPONENT < 8, "Invalid US_PER_OSTICK_EXPONENT value");
|
|
}
|
|
|
|
// Returns the number of ticks until time. Negative values indicate that
|
|
// time has already passed.
|
|
static s4_t delta_time(u4_t time) {
|
|
return (s4_t)(time - hal_ticks());
|
|
}
|
|
|
|
// deal with boards that are stressed by no-interrupt delays #529, etc.
|
|
#if defined(ARDUINO_DISCO_L072CZ_LRWAN1)
|
|
# define HAL_WAITUNTIL_DOWNCOUNT_MS 16 // on this board, 16 ms works better
|
|
# define HAL_WAITUNTIL_DOWNCOUNT_THRESH ms2osticks(16) // as does this threashold.
|
|
#else
|
|
# define HAL_WAITUNTIL_DOWNCOUNT_MS 8 // on most boards, delay for 8 ms
|
|
# define HAL_WAITUNTIL_DOWNCOUNT_THRESH ms2osticks(9) // but try to leave a little slack for final timing.
|
|
#endif
|
|
|
|
u4_t hal_waitUntil (u4_t time) {
|
|
s4_t delta = delta_time(time);
|
|
// check for already too late.
|
|
if (delta < 0)
|
|
return -delta;
|
|
|
|
// From delayMicroseconds docs: Currently, the largest value that
|
|
// will produce an accurate delay is 16383. Also, STM32 does a better
|
|
// job with delay is less than 10,000 us; so reduce in steps.
|
|
// It's nice to use delay() for the longer times.
|
|
while (delta > HAL_WAITUNTIL_DOWNCOUNT_THRESH) {
|
|
// deliberately delay 8ms rather than 9ms, so we
|
|
// will exit loop with delta typically positive.
|
|
// Depends on BSP keeping time accurately even if interrupts
|
|
// are disabled.
|
|
delay(HAL_WAITUNTIL_DOWNCOUNT_MS);
|
|
// re-synchronize.
|
|
delta = delta_time(time);
|
|
}
|
|
|
|
// The radio driver runs with interrupt disabled, and this can
|
|
// mess up timing APIs on some platforms. If we know the BSP feature
|
|
// set, we can decide whether to use delta_time() [more exact,
|
|
// but not always possible with interrupts off], or fall back to
|
|
// delay_microseconds() [less exact, but more universal]
|
|
|
|
#if defined(_mcci_arduino_version)
|
|
// unluckily, delayMicroseconds() isn't very accurate.
|
|
// but delta_time() works with interrupts disabled.
|
|
// so spin using delta_time().
|
|
while (delta_time(time) > 0)
|
|
/* loop */;
|
|
#else // ! defined(_mcci_arduino_version)
|
|
// on other BSPs, we need to stick with the older way,
|
|
// until we fix the radio driver to run with interrupts
|
|
// enabled.
|
|
if (delta > 0)
|
|
delayMicroseconds(delta * US_PER_OSTICK);
|
|
#endif // ! defined(_mcci_arduino_version)
|
|
|
|
// we aren't "late". Callers are interested in gross delays, not
|
|
// necessarily delays due to poor timekeeping here.
|
|
return 0;
|
|
}
|
|
|
|
// check and rewind for target time
|
|
u1_t hal_checkTimer (u4_t time) {
|
|
// No need to schedule wakeup, since we're not sleeping
|
|
return delta_time(time) <= 0;
|
|
}
|
|
|
|
static uint8_t irqlevel = 0;
|
|
|
|
void hal_disableIRQs () {
|
|
noInterrupts();
|
|
irqlevel++;
|
|
}
|
|
|
|
void hal_enableIRQs () {
|
|
if(--irqlevel == 0) {
|
|
interrupts();
|
|
|
|
#if !defined(LMIC_USE_INTERRUPTS)
|
|
// Instead of using proper interrupts (which are a bit tricky
|
|
// and/or not available on all pins on AVR), just poll the pin
|
|
// values. Since os_runloop disables and re-enables interrupts,
|
|
// putting this here makes sure we check at least once every
|
|
// loop.
|
|
//
|
|
// As an additional bonus, this prevents the can of worms that
|
|
// we would otherwise get for running SPI transfers inside ISRs.
|
|
// We merely collect the edges and timestamps here; we wait for
|
|
// a call to hal_processPendingIRQs() before dispatching.
|
|
hal_pollPendingIRQs_helper();
|
|
#endif /* !defined(LMIC_USE_INTERRUPTS) */
|
|
}
|
|
}
|
|
|
|
uint8_t hal_getIrqLevel(void) {
|
|
return irqlevel;
|
|
}
|
|
|
|
void hal_sleep () {
|
|
// Not implemented
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#if defined(LMIC_PRINTF_TO)
|
|
#if !defined(__AVR)
|
|
static ssize_t uart_putchar (void *, const char *buf, size_t len) {
|
|
return LMIC_PRINTF_TO.write((const uint8_t *)buf, len);
|
|
}
|
|
|
|
static cookie_io_functions_t functions =
|
|
{
|
|
.read = NULL,
|
|
.write = uart_putchar,
|
|
.seek = NULL,
|
|
.close = NULL
|
|
};
|
|
|
|
void hal_printf_init() {
|
|
stdout = fopencookie(NULL, "w", functions);
|
|
if (stdout != nullptr) {
|
|
setvbuf(stdout, NULL, _IONBF, 0);
|
|
}
|
|
}
|
|
#else // defined(__AVR)
|
|
static int uart_putchar (char c, FILE *)
|
|
{
|
|
LMIC_PRINTF_TO.write(c) ;
|
|
return 0 ;
|
|
}
|
|
|
|
void hal_printf_init() {
|
|
// create a FILE structure to reference our UART output function
|
|
static FILE uartout;
|
|
memset(&uartout, 0, sizeof(uartout));
|
|
|
|
// fill in the UART file descriptor with pointer to writer.
|
|
fdev_setup_stream (&uartout, uart_putchar, NULL, _FDEV_SETUP_WRITE);
|
|
|
|
// The uart is the standard output device STDOUT.
|
|
stdout = &uartout ;
|
|
}
|
|
|
|
#endif // !defined(ESP8266) || defined(ESP31B) || defined(ESP32)
|
|
#endif // defined(LMIC_PRINTF_TO)
|
|
|
|
//void hal_init (void) {
|
|
void hal_init_lmic() {
|
|
|
|
// use the global constant
|
|
Arduino_LMIC::hal_init_with_pinmap(&lmic_pins);
|
|
}
|
|
|
|
// hal_init_ex is a C API routine, written in C++, and it's called
|
|
// with a pointer to an lmic_pinmap.
|
|
void hal_init_ex (const void *pContext) {
|
|
const lmic_pinmap * const pHalPinmap = (const lmic_pinmap *) pContext;
|
|
if (! Arduino_LMIC::hal_init_with_pinmap(pHalPinmap)) {
|
|
hal_failed(__FILE__, __LINE__);
|
|
}
|
|
}
|
|
|
|
// C++ API: initialize the HAL properly with a configuration object
|
|
namespace Arduino_LMIC {
|
|
bool hal_init_with_pinmap(const HalPinmap_t *pPinmap)
|
|
{
|
|
if (pPinmap == nullptr)
|
|
return false;
|
|
|
|
// set the static pinmap pointer.
|
|
plmic_pins = pPinmap;
|
|
|
|
// set the static HalConfiguration pointer.
|
|
HalConfiguration_t * const pThisHalConfig = pPinmap->pConfig;
|
|
|
|
if (pThisHalConfig != nullptr)
|
|
pHalConfig = pThisHalConfig;
|
|
else
|
|
pHalConfig = &nullHalConig;
|
|
|
|
pHalConfig->begin();
|
|
|
|
// configure radio I/O and interrupt handler
|
|
hal_io_init();
|
|
// configure radio SPI
|
|
hal_spi_init();
|
|
// configure timer and interrupt handler
|
|
hal_time_init();
|
|
#if defined(LMIC_PRINTF_TO)
|
|
// printf support
|
|
hal_printf_init();
|
|
#endif
|
|
// declare success
|
|
return true;
|
|
}
|
|
}; // namespace Arduino_LMIC
|
|
|
|
|
|
void hal_failed (const char *file, u2_t line) {
|
|
if (custom_hal_failure_handler != NULL) {
|
|
(*custom_hal_failure_handler)(file, line);
|
|
}
|
|
|
|
#if defined(LMIC_FAILURE_TO)
|
|
LMIC_FAILURE_TO.println("FAILURE ");
|
|
LMIC_FAILURE_TO.print(file);
|
|
LMIC_FAILURE_TO.print(':');
|
|
LMIC_FAILURE_TO.println(line);
|
|
LMIC_FAILURE_TO.flush();
|
|
#endif
|
|
|
|
hal_disableIRQs();
|
|
|
|
// Infinite loop
|
|
while (1) {
|
|
;
|
|
}
|
|
}
|
|
|
|
void hal_set_failure_handler(const hal_failure_handler_t* const handler) {
|
|
custom_hal_failure_handler = handler;
|
|
}
|
|
|
|
ostime_t hal_setModuleActive (bit_t val) {
|
|
// setModuleActive() takes a c++ bool, so
|
|
// it effectively says "val != 0". We
|
|
// don't have to.
|
|
return pHalConfig->setModuleActive(val);
|
|
}
|
|
|
|
bit_t hal_queryUsingTcxo(void) {
|
|
return pHalConfig->queryUsingTcxo();
|
|
}
|
|
|
|
uint8_t hal_getTxPowerPolicy(
|
|
u1_t inputPolicy,
|
|
s1_t requestedPower,
|
|
u4_t frequency
|
|
) {
|
|
return (uint8_t) pHalConfig->getTxPowerPolicy(
|
|
Arduino_LMIC::HalConfiguration_t::TxPowerPolicy_t(inputPolicy),
|
|
requestedPower,
|
|
frequency
|
|
);
|
|
}
|