/******************************************************************************* * * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x * * Copyright (c) 2018-2019 Manuel Bleichenbacher * * Licensed under MIT License * https://opensource.org/licenses/MIT * * Hardware abstraction layer to run LMIC on a ESP32 using ESP-IDF. *******************************************************************************/ #include "hal_esp32.h" #include "../lmic/lmic.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_timer.h" #include "esp_log.h" #include #include #define LMIC_UNUSED_PIN 0xff #define NOTIFY_BIT_DIO 1 #define NOTIFY_BIT_TIMER 2 #define NOTIFY_BIT_WAKEUP 4 #define NOTIFY_BIT_STOP 8 #define TAG "ttn_hal" typedef enum { WAIT_KIND_NONE = 0, WAIT_KIND_CHECK_IO, WAIT_KIND_WAIT_FOR_ANY_EVENT, WAIT_KIND_WAIT_FOR_TIMER } wait_kind_e; static void lmic_background_task(void* pvParameter); static void qio_irq_handler(void* arg); static void timer_callback(void *arg); static int64_t os_time_to_esp_time(int64_t esp_now, uint32_t os_time); static int64_t get_current_time(); static void init_io(void); static void init_spi(void); static void assert_nss(spi_transaction_t* trans); static void deassert_nss(spi_transaction_t* trans); static void init_timer(void); static void set_next_alarm(int64_t time); static void arm_timer(int64_t esp_now); static void disarm_timer(void); static bool wait(wait_kind_e wait_kind); static spi_host_device_t spi_host; static gpio_num_t pin_nss; static gpio_num_t pin_rx_tx; static gpio_num_t pin_rst; static gpio_num_t pin_dio0; static gpio_num_t pin_dio1; static int8_t rssi_cal = 10; static TaskHandle_t lmic_task; static uint32_t dio_interrupt_time; static uint8_t dio_num; static spi_device_handle_t spi_handle; static spi_transaction_t spi_transaction; static SemaphoreHandle_t mutex; static esp_timer_handle_t timer; static int64_t time_offset; static int32_t initial_time_offset; static int64_t next_alarm; static volatile bool run_background_task; static volatile wait_kind_e current_wait_kind; // ----------------------------------------------------------------------------- // I/O void hal_esp32_configure_pins(spi_host_device_t host, uint8_t nss, uint8_t rxtx, uint8_t rst, uint8_t dio0, uint8_t dio1) { spi_host = host; pin_nss = (gpio_num_t)nss; pin_rx_tx = (gpio_num_t)rxtx; pin_rst = (gpio_num_t)rst; pin_dio0 = (gpio_num_t)dio0; pin_dio1 = (gpio_num_t)dio1; // Until the background process has been started, use the current task // for supporting calls like `hal_waitUntil()`. lmic_task = xTaskGetCurrentTaskHandle(); } void IRAM_ATTR qio_irq_handler(void *arg) { dio_interrupt_time = hal_ticks(); dio_num = (u1_t)(long)arg; BaseType_t higher_prio_task_woken = pdFALSE; xTaskNotifyFromISR(lmic_task, NOTIFY_BIT_DIO, eSetBits, &higher_prio_task_woken); if (higher_prio_task_woken) portYIELD_FROM_ISR(); } void init_io(void) { // pin_nss, pin_dio0 and pin_dio1 are required ASSERT(pin_nss != LMIC_UNUSED_PIN); ASSERT(pin_dio0 != LMIC_UNUSED_PIN); ASSERT(pin_dio1 != LMIC_UNUSED_PIN); gpio_config_t output_pin_config = { .pin_bit_mask = BIT64(pin_nss), .mode = GPIO_MODE_OUTPUT, .pull_up_en = false, .pull_down_en = false, .intr_type = GPIO_INTR_DISABLE }; if (pin_rx_tx != LMIC_UNUSED_PIN) output_pin_config.pin_bit_mask |= BIT64(pin_rx_tx); if (pin_rst != LMIC_UNUSED_PIN) output_pin_config.pin_bit_mask |= BIT64(pin_rst); gpio_config(&output_pin_config); gpio_set_level(pin_nss, 1); if (pin_rx_tx != LMIC_UNUSED_PIN) gpio_set_level(pin_rx_tx, 0); if (pin_rst != LMIC_UNUSED_PIN) gpio_set_level(pin_rst, 0); // DIO pins with interrupt handlers gpio_config_t input_pin_config = { .pin_bit_mask = BIT64(pin_dio0) | BIT64(pin_dio1), .mode = GPIO_MODE_INPUT, .pull_up_en = false, .pull_down_en = true, .intr_type = GPIO_INTR_POSEDGE, }; gpio_config(&input_pin_config); ESP_LOGI(TAG, "IO initialized"); } __attribute__((weak)) // duplicate this symbol if your pin is controlled by e.g. I2C void hal_pin_rxtx(u1_t val) { if (pin_rx_tx == LMIC_UNUSED_PIN) return; gpio_set_level(pin_rx_tx, val); } __attribute__((weak)) // duplicate this symbol if your pin is controlled by e.g. I2C void hal_pin_rst(u1_t val) { if (pin_rst == LMIC_UNUSED_PIN) return; if (val == 0 || val == 1) { // drive pin gpio_set_level(pin_rst, val); gpio_set_direction(pin_rst, GPIO_MODE_OUTPUT); } else { #if defined(CONFIG_TTN_RESET_STATES_ASSERTED) // drive up the pin because the hardware is nonstandard gpio_set_level(pin_rst, 1); gpio_set_direction(pin_rst, GPIO_MODE_OUTPUT); #else // keep pin floating gpio_set_direction(pin_rst, GPIO_MODE_INPUT); #endif } } s1_t hal_getRssiCal(void) { return rssi_cal; } ostime_t hal_setModuleActive(bit_t val) { return 0; } bit_t hal_queryUsingTcxo(void) { return false; } uint8_t hal_getTxPowerPolicy(u1_t inputPolicy, s1_t requestedPower, u4_t frequency) { return LMICHAL_radio_tx_power_policy_paboost; } // ----------------------------------------------------------------------------- // SPI void init_spi(void) { // init device spi_device_interface_config_t spi_config = { .mode = 0, .clock_speed_hz = CONFIG_TTN_SPI_FREQ, .command_bits = 0, .address_bits = 8, .spics_io_num = -1, .queue_size = 1, .pre_cb = assert_nss, .post_cb = deassert_nss, }; esp_err_t ret = spi_bus_add_device(spi_host, &spi_config, &spi_handle); ESP_ERROR_CHECK(ret); ESP_LOGI(TAG, "SPI initialized"); } void hal_spi_write(u1_t cmd, const u1_t *buf, size_t len) { memset(&spi_transaction, 0, sizeof(spi_transaction)); spi_transaction.addr = cmd; spi_transaction.length = 8 * len; spi_transaction.tx_buffer = buf; esp_err_t err = spi_device_transmit(spi_handle, &spi_transaction); ESP_ERROR_CHECK(err); } void hal_spi_read(u1_t cmd, u1_t *buf, size_t len) { memset(buf, 0, len); memset(&spi_transaction, 0, sizeof(spi_transaction)); spi_transaction.addr = cmd; spi_transaction.length = 8 * len; spi_transaction.rxlength = 8 * len; spi_transaction.tx_buffer = buf; spi_transaction.rx_buffer = buf; esp_err_t err = spi_device_transmit(spi_handle, &spi_transaction); ESP_ERROR_CHECK(err); } void IRAM_ATTR assert_nss(spi_transaction_t* trans) { gpio_set_level(pin_nss, 0); } void IRAM_ATTR deassert_nss(spi_transaction_t* trans) { gpio_set_level(pin_nss, 1); } // ----------------------------------------------------------------------------- // TIME /* * LIMIC uses a 32 bit time system (ostime_t) counting ticks. In this * implementation, each tick is 16µs. It will wrap arounnd every 19 hours. * * The ESP32 has a 64 bit timer counting microseconds. It will wrap around * every 584,000 years. So we don't need to bother. * * The time includes an offset initialized from `gettimeofday()` * to ensure the time correctly advances during deep sleep. * * Based on this timer, future callbacks can be scheduled. This is used to * schedule the next LMIC job. */ // Convert LMIC tick time (ostime_t) to ESP absolute time. // `os_time` is assumed to be somewhere between one hour in the past and // 18 hours into the future. int64_t os_time_to_esp_time(int64_t esp_now, uint32_t os_time) { int64_t esp_time; uint32_t os_now = (uint32_t)(esp_now >> 4); // unsigned difference: // 0x00000000 - 0xefffffff: future (0 to about 18 hours) // 0xf0000000 - 0xffffffff: past (about 1 to 0 hours) uint32_t os_diff = os_time - os_now; if (os_diff < 0xf0000000) { esp_time = esp_now + (((int64_t)os_diff) << 4); } else { os_diff = -os_diff; esp_time = esp_now - (((int64_t)os_diff) << 4); } return esp_time; } int64_t IRAM_ATTR get_current_time() { return esp_timer_get_time() + time_offset; } void init_timer(void) { esp_timer_create_args_t timer_config = { .callback = &timer_callback, .arg = NULL, .dispatch_method = ESP_TIMER_TASK, .name = "lmic_job" }; esp_err_t err = esp_timer_create(&timer_config, &timer); ESP_ERROR_CHECK(err); struct timeval now; gettimeofday(&now, NULL); time_offset += (int64_t)now.tv_sec * 1000000; initial_time_offset = 0; ESP_LOGI(TAG, "Timer initialized"); } uint32_t hal_esp32_get_time(void) { struct timeval now; gettimeofday(&now, NULL); return now.tv_sec + initial_time_offset; } void hal_esp32_set_time(uint32_t time_val) { initial_time_offset = time_val; time_offset = (int64_t)time_val * 1000000; } void set_next_alarm(int64_t time) { next_alarm = time; } void arm_timer(int64_t esp_now) { if (next_alarm == 0) return; int64_t timeout = next_alarm - get_current_time(); if (timeout < 0) timeout = 10; esp_timer_start_once(timer, timeout); } void disarm_timer(void) { esp_timer_stop(timer); } void timer_callback(void *arg) { xTaskNotify(lmic_task, NOTIFY_BIT_TIMER, eSetBits); } // Wait for the next external event. Either: // - scheduled timer due to scheduled job or waiting for a given time // - wake up event from the client code // - I/O interrupt (DIO0 or DIO1 pin) bool wait(wait_kind_e wait_kind) { TickType_t ticks_to_wait = wait_kind == WAIT_KIND_CHECK_IO ? 0 : portMAX_DELAY; while (true) { current_wait_kind = wait_kind; uint32_t bits = ulTaskNotifyTake(pdTRUE, ticks_to_wait); current_wait_kind = WAIT_KIND_NONE; if (bits == 0) return false; if ((bits & NOTIFY_BIT_STOP) != 0) return false; if ((bits & NOTIFY_BIT_WAKEUP) != 0) { if (wait_kind != WAIT_KIND_WAIT_FOR_TIMER) { disarm_timer(); return true; } } else if ((bits & NOTIFY_BIT_TIMER) != 0) { disarm_timer(); set_next_alarm(0); if (wait_kind != WAIT_KIND_CHECK_IO) return true; } else // IO interrupt { if (wait_kind != WAIT_KIND_WAIT_FOR_TIMER) disarm_timer(); hal_esp32_enter_critical_section(); radio_irq_handler_v2(dio_num, dio_interrupt_time); hal_esp32_leave_critical_section(); if (wait_kind != WAIT_KIND_WAIT_FOR_TIMER) return true; } } } TickType_t hal_esp32_get_timer_duration(void) { wait_kind_e wait_kind = current_wait_kind; int64_t alarm_time = next_alarm; if (wait_kind == WAIT_KIND_NONE || wait_kind == WAIT_KIND_CHECK_IO) return 1; // busy, not waiting if (alarm_time != 0) { TickType_t dur = pdMS_TO_TICKS((alarm_time - get_current_time() + 999) / 1000); if (dur > pdMS_TO_TICKS(30000)) dur = pdMS_TO_TICKS(200); return dur; } return 0; // waiting indefinitely } // Gets current time in LMIC ticks u4_t IRAM_ATTR hal_ticks(void) { // LMIC tick unit: 16µs // esp_timer unit: 1µs return (u4_t)(get_current_time() >> 4); } // Wait until the specified time. // Called if the LMIC code needs to wait for a precise time. // All other events are ignored and will be served later. u4_t hal_waitUntil(u4_t time) { int64_t esp_now = get_current_time(); int64_t esp_time = os_time_to_esp_time(esp_now, time); set_next_alarm(esp_time); arm_timer(esp_now); wait(WAIT_KIND_WAIT_FOR_TIMER); u4_t os_now = hal_ticks(); u4_t diff = os_now - time; return diff < 0x80000000U ? diff : 0; } // Called by client code to wake up LMIC to do something, // e.g. send a submitted messages. void hal_esp32_wake_up(void) { xTaskNotify(lmic_task, NOTIFY_BIT_WAKEUP, eSetBits); } // Check if the specified time has been reached or almost reached. // Otherwise, save it as alarm time. // LMIC calls this function with the scheduled time of the next job // in the queue. If the job is not due yet, LMIC will go to sleep. u1_t hal_checkTimer(uint32_t time) { int64_t esp_now = get_current_time(); int64_t esp_time = os_time_to_esp_time(esp_now, time); int64_t diff = esp_time - esp_now; if (diff < 100) return 1; // timer has expired or will expire very soon set_next_alarm(esp_time); return 0; } // Go to sleep until next event. // Called when LMIC is not busy and not job is due to be executed. void hal_sleep(void) { if (wait(WAIT_KIND_CHECK_IO)) return; arm_timer(get_current_time()); wait(WAIT_KIND_WAIT_FOR_ANY_EVENT); } // ----------------------------------------------------------------------------- // IRQ void hal_disableIRQs(void) { // nothing to do as interrupt handlers post message to queue // and don't access any shared data structures } void hal_enableIRQs(void) { // nothing to do as interrupt handlers post message to queue // and don't access any shared data structures } void hal_processPendingIRQs(void) { // nothing to do as interrupt handlers post message to queue // and don't access any shared data structures } // ----------------------------------------------------------------------------- // Synchronization between application code and background task void hal_esp32_init_critical_section(void) { mutex = xSemaphoreCreateRecursiveMutex(); } void hal_esp32_enter_critical_section(void) { xSemaphoreTakeRecursive(mutex, portMAX_DELAY); } void hal_esp32_leave_critical_section(void) { xSemaphoreGiveRecursive(mutex); } // ----------------------------------------------------------------------------- void lmic_background_task(void* pvParameter) { while (run_background_task) os_runloop_once(); vTaskDelete(NULL); } void hal_init_ex(const void *pContext) { // configure radio I/O and interrupt handler init_io(); // configure radio SPI init_spi(); // configure timer and alarm callback init_timer(); } void hal_esp32_start_lmic_task(void) { run_background_task = true; xTaskCreate(lmic_background_task, "ttn_lmic", 1024 * 4, NULL, CONFIG_TTN_BG_TASK_PRIO, &lmic_task); // enable interrupts gpio_isr_handler_add(pin_dio0, qio_irq_handler, (void *)0); gpio_isr_handler_add(pin_dio1, qio_irq_handler, (void *)1); } void hal_esp32_stop_lmic_task(void) { run_background_task = false; gpio_isr_handler_remove(pin_dio0); gpio_isr_handler_remove(pin_dio1); disarm_timer(); set_next_alarm(0); xTaskNotify(lmic_task, NOTIFY_BIT_STOP, eSetBits); lmic_task = xTaskGetCurrentTaskHandle(); } // ----------------------------------------------------------------------------- // Fatal failure static hal_failure_handler_t* custom_hal_failure_handler = NULL; void hal_set_failure_handler(hal_failure_handler_t* const handler) { custom_hal_failure_handler = handler; } void hal_failed(const char *file, u2_t line) { if (custom_hal_failure_handler != NULL) (*custom_hal_failure_handler)(file, line); ESP_LOGE(TAG, "LMIC failed and stopped: %s:%d", file, line); // go to sleep forever while (true) { vTaskDelay(portMAX_DELAY); } } // ----------------------------------------------------------------------------- // RSSI void hal_esp32_set_rssi_cal(int8_t cal) { rssi_cal = cal; }