#include "lora_radio.h" #include #include "esp_check.h" #include "esp_log.h" #include "esp_timer.h" #include "driver/gpio.h" #include "driver/spi_master.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_lora_1121.h" #include "lr1121_common/lr1121_common.h" #include "lr11xx_driver/lr11xx_hal.h" #include "lr11xx_driver/lr11xx_radio.h" #include "lr11xx_driver/lr11xx_regmem.h" #include "lr11xx_driver/lr11xx_system.h" static const char *TAG = "lora_radio"; #define LR1121_SPI_FREQ_HZ (8 * 1000 * 1000) #define LR1121_SPI_MODE (0) #define LR1121_RX_CONTINUOUS (0xFFFFFF) #define LR1121_SYNC_WORD (0x12) // Private network #define LR1121_PA_RAMP (LR11XX_RADIO_RAMP_48_US) static lr1121_t s_lr = {0}; static spi_device_handle_t s_spi = NULL; static bool s_is_tx = false; static lora_params_t s_params; static lora_metrics_t s_metrics = { .rssi_dbm = -120, .snr_db = -20, .last_status = 0 }; static char s_last_payload[96] = "(no data)"; static size_t s_last_payload_len = 0; static int64_t s_last_rx_restart_us = 0; static bool s_tx_in_progress = false; static int64_t s_last_tx_us = 0; static inline esp_err_t lr_status_to_esp(lr11xx_status_t st) { return (st == LR11XX_STATUS_OK) ? ESP_OK : ESP_FAIL; } static uint32_t cmhz_to_hz(int cmhz) { if (cmhz <= 0) { return 0; } return (uint32_t)cmhz * 10000U; } static lr11xx_radio_lora_sf_t map_sf(int sf) { switch (sf) { case 5: return LR11XX_RADIO_LORA_SF5; case 6: return LR11XX_RADIO_LORA_SF6; case 7: return LR11XX_RADIO_LORA_SF7; case 8: return LR11XX_RADIO_LORA_SF8; case 9: return LR11XX_RADIO_LORA_SF9; case 10: return LR11XX_RADIO_LORA_SF10; case 11: return LR11XX_RADIO_LORA_SF11; case 12: return LR11XX_RADIO_LORA_SF12; default: return LR11XX_RADIO_LORA_SF7; } } static lr11xx_radio_lora_bw_t map_bw(int bw_khz) { switch (bw_khz) { case 125: return LR11XX_RADIO_LORA_BW_125; case 250: return LR11XX_RADIO_LORA_BW_250; case 500: return LR11XX_RADIO_LORA_BW_500; default: return LR11XX_RADIO_LORA_BW_125; } } static lr11xx_radio_lora_cr_t map_cr(int cr) { switch (cr) { case 5: return LR11XX_RADIO_LORA_CR_4_5; case 6: return LR11XX_RADIO_LORA_CR_4_6; case 7: return LR11XX_RADIO_LORA_CR_4_7; case 8: return LR11XX_RADIO_LORA_CR_4_8; default: return LR11XX_RADIO_LORA_CR_4_5; } } static void sanitize_params(lora_params_t *p) { if (!p) { return; } if (p->freq_centi_mhz < 15000) p->freq_centi_mhz = 15000; // 150 MHz мінімум if (p->freq_centi_mhz > 250000) p->freq_centi_mhz = 250000; // 2.5 GHz максимум if (p->bw_khz != 125 && p->bw_khz != 250 && p->bw_khz != 500) { p->bw_khz = 125; } if (p->sf < 5) p->sf = 5; if (p->sf > 12) p->sf = 12; if (p->cr < 5) p->cr = 5; if (p->cr > 8) p->cr = 8; int max_dbm = 22; // HF port (L/S/2.4 GHz) має нижчу допустиму потужність if (p->freq_centi_mhz >= 150000) { max_dbm = 13; } if (p->tx_power_dbm < -17) p->tx_power_dbm = -17; if (p->tx_power_dbm > max_dbm) p->tx_power_dbm = max_dbm; if (p->preamble_syms == 0) p->preamble_syms = 8; if (p->payload_len == 0 && p->header_implicit) { p->payload_len = 1; } } static void lr1121_calibrate_image_for_freq(uint32_t freq_hz) { // Використовуємо універсальне калібрування за частотою в MHz ±10 MHz uint16_t mhz = (uint16_t)(freq_hz / 1000000U); uint16_t f1 = (mhz > 10) ? (uint16_t)(mhz - 10) : mhz; uint16_t f2 = (uint16_t)(mhz + 10); lr11xx_system_calibrate_image_in_mhz(&s_lr, f1, f2); } static esp_err_t radio_setup_spi(void) { spi_bus_config_t buscfg = { .mosi_io_num = CONFIG_LORA_PIN_MOSI, .miso_io_num = CONFIG_LORA_PIN_MISO, .sclk_io_num = CONFIG_LORA_PIN_SCK, .quadwp_io_num = -1, .quadhd_io_num = -1, }; esp_err_t err = spi_bus_initialize(CONFIG_LORA_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { return err; } if (s_spi) { return ESP_OK; } spi_device_interface_config_t devcfg = { .clock_speed_hz = LR1121_SPI_FREQ_HZ, .mode = LR1121_SPI_MODE, .spics_io_num = -1, // CS керуємо вручну в HAL .queue_size = 2, }; err = spi_bus_add_device(CONFIG_LORA_SPI_HOST, &devcfg, &s_spi); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { return err; } return ESP_OK; } static esp_err_t lr1121_hw_init(void) { lora_init_io_context(&s_lr, CONFIG_LORA_PIN_CS, CONFIG_LORA_PIN_RST, CONFIG_LORA_PIN_BUSY, CONFIG_LORA_PIN_DIO1); lora_init_io(&s_lr); lora_spi_init(&s_lr, s_spi); return ESP_OK; } static esp_err_t lr1121_system_boot(void) { ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_reset(&s_lr)), TAG, "reset"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_hal_wakeup(&s_lr)), TAG, "wakeup"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_set_standby(&s_lr, LR11XX_SYSTEM_STANDBY_CFG_XOSC)), TAG, "standby"); const lr11xx_system_reg_mode_t reg_mode = smtc_shield_lr11xx_common_get_reg_mode(); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_set_reg_mode(&s_lr, reg_mode)), TAG, "reg mode"); const lr11xx_system_rfswitch_cfg_t *rf_switch = smtc_shield_lr11xx_common_get_rf_switch_cfg(); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_set_dio_as_rf_switch(&s_lr, rf_switch)), TAG, "rf switch"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_set_tcxo_mode(&s_lr, LR11XX_SYSTEM_TCXO_CTRL_3_0V, 300)), TAG, "tcxo"); const smtc_shield_lr11xx_lfclk_cfg_t *lf = smtc_shield_lr11xx_common_get_lfclk_cfg(); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_cfg_lfclk(&s_lr, lf->lf_clk_cfg, lf->wait_32k_ready)), TAG, "lfclk"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_clear_errors(&s_lr)), TAG, "clear errors"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_calibrate(&s_lr, 0x3F)), TAG, "calibrate"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_clear_irq_status(&s_lr, LR11XX_SYSTEM_IRQ_ALL_MASK)), TAG, "clear irq"); lr11xx_system_version_t ver = {0}; if (lr11xx_system_get_version(&s_lr, &ver) == LR11XX_STATUS_OK) { ESP_LOGI(TAG, "LR1121 ver hw=0x%02X type=0x%02X fw=0x%04X", ver.hw, ver.type, ver.fw); } return ESP_OK; } static esp_err_t lr1121_apply_lora(const lora_params_t *p) { uint32_t freq_hz = cmhz_to_hz(p->freq_centi_mhz); lr11xx_radio_lora_bw_t bw = map_bw(p->bw_khz); lr11xx_radio_lora_sf_t sf = map_sf(p->sf); lr11xx_radio_lora_cr_t cr = map_cr(p->cr); lr11xx_radio_mod_params_lora_t mod = { .sf = sf, .bw = bw, .cr = cr, .ldro = smtc_shield_lr11xx_common_compute_lora_ldro(sf, bw), }; lr11xx_radio_pkt_params_lora_t pkt = { .preamble_len_in_symb = p->preamble_syms, .header_type = p->header_implicit ? LR11XX_RADIO_LORA_PKT_IMPLICIT : LR11XX_RADIO_LORA_PKT_EXPLICIT, .pld_len_in_bytes = p->payload_len, .crc = p->crc_on ? LR11XX_RADIO_LORA_CRC_ON : LR11XX_RADIO_LORA_CRC_OFF, .iq = p->iq_invert ? LR11XX_RADIO_LORA_IQ_INVERTED : LR11XX_RADIO_LORA_IQ_STANDARD, }; const lr11xx_radio_rssi_calibration_table_t *rssi_cal = smtc_shield_lr11xx_get_rssi_calibration_table(freq_hz); const smtc_shield_lr11xx_pa_pwr_cfg_t *pa_cfg = smtc_shield_lr1121mb1gis_get_pa_pwr_cfg(freq_hz, p->tx_power_dbm); if (pa_cfg == NULL || rssi_cal == NULL) { return ESP_ERR_INVALID_ARG; } lr1121_calibrate_image_for_freq(freq_hz); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_pkt_type(&s_lr, LR11XX_RADIO_PKT_TYPE_LORA)), TAG, "pkt type"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_rf_freq(&s_lr, freq_hz)), TAG, "freq"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_rssi_calibration(&s_lr, rssi_cal)), TAG, "rssi cal"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_pa_cfg(&s_lr, &pa_cfg->pa_config)), TAG, "pa cfg"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_tx_params(&s_lr, pa_cfg->power, LR1121_PA_RAMP)), TAG, "tx params"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_rx_tx_fallback_mode(&s_lr, LR11XX_RADIO_FALLBACK_STDBY_RC)), TAG, "fallback"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_cfg_rx_boosted(&s_lr, true)), TAG, "rx boost"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_lora_mod_params(&s_lr, &mod)), TAG, "mod params"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_lora_pkt_params(&s_lr, &pkt)), TAG, "pkt params"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_lora_sync_word(&s_lr, LR1121_SYNC_WORD)), TAG, "sync word"); lr11xx_system_irq_mask_t irq_mask = LR11XX_SYSTEM_IRQ_RX_DONE | LR11XX_SYSTEM_IRQ_TIMEOUT | LR11XX_SYSTEM_IRQ_HEADER_ERROR | LR11XX_SYSTEM_IRQ_CRC_ERROR; if (s_is_tx) { irq_mask |= LR11XX_SYSTEM_IRQ_TX_DONE; } ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_set_dio_irq_params(&s_lr, irq_mask, 0)), TAG, "irq mask"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_system_clear_irq_status(&s_lr, LR11XX_SYSTEM_IRQ_ALL_MASK)), TAG, "clr irq"); return ESP_OK; } static esp_err_t lr1121_enter_rx_continuous(void) { s_last_rx_restart_us = esp_timer_get_time(); return lr_status_to_esp(lr11xx_radio_set_rx_with_timeout_in_rtc_step(&s_lr, LR1121_RX_CONTINUOUS)); } bool lora_radio_init(bool is_tx_role) { s_is_tx = is_tx_role; s_params.freq_centi_mhz = CONFIG_LORA_FREQ_MHZ * 100; s_params.bw_khz = CONFIG_LORA_BW_KHZ; s_params.sf = CONFIG_LORA_SF; s_params.cr = CONFIG_LORA_CR; s_params.tx_power_dbm = 14; s_params.preamble_syms = 8; s_params.payload_len = 0; s_params.crc_on = true; s_params.iq_invert = false; s_params.header_implicit = false; sanitize_params(&s_params); ESP_LOGI(TAG, "Init LR1121 over SPI host %d (MOSI=%d, MISO=%d, SCK=%d, CS=%d, RST=%d, BUSY=%d, DIO1=%d)", CONFIG_LORA_SPI_HOST, CONFIG_LORA_PIN_MOSI, CONFIG_LORA_PIN_MISO, CONFIG_LORA_PIN_SCK, CONFIG_LORA_PIN_CS, CONFIG_LORA_PIN_RST, CONFIG_LORA_PIN_BUSY, CONFIG_LORA_PIN_DIO1); if (CONFIG_LORA_PIN_BUSY < 0) { ESP_LOGE(TAG, "BUSY pin обов'язковий для LR1121, вкажіть коректний GPIO"); return false; } if (radio_setup_spi() != ESP_OK) { ESP_LOGE(TAG, "SPI init failed"); return false; } if (lr1121_hw_init() != ESP_OK) { ESP_LOGE(TAG, "IO init failed"); return false; } if (lr1121_system_boot() != ESP_OK) { ESP_LOGE(TAG, "System boot failed"); return false; } if (lr1121_apply_lora(&s_params) != ESP_OK) { ESP_LOGE(TAG, "Initial params failed"); return false; } if (!s_is_tx) { lr1121_enter_rx_continuous(); } ESP_LOGI(TAG, "Radio ready: %.2f MHz BW %d kHz SF %d CR 4/%d TX %d dBm", s_params.freq_centi_mhz / 100.0f, s_params.bw_khz, s_params.sf, s_params.cr, s_params.tx_power_dbm); return true; } esp_err_t lora_radio_apply_params(const lora_params_t *params) { if (!params) { return ESP_ERR_INVALID_ARG; } lora_params_t clean = *params; sanitize_params(&clean); esp_err_t err = lr1121_apply_lora(&clean); if (err == ESP_OK) { s_params = clean; if (!s_is_tx) { lr1121_enter_rx_continuous(); } ESP_LOGI(TAG, "Apply LoRa params: %.2f MHz, BW %d kHz, SF %d, CR 4/%d, TX %d dBm", s_params.freq_centi_mhz / 100.0f, s_params.bw_khz, s_params.sf, s_params.cr, s_params.tx_power_dbm); } return err; } esp_err_t lora_radio_set_tx_power_dbm(int dbm) { lora_params_t next = s_params; next.tx_power_dbm = dbm; return lora_radio_apply_params(&next); } esp_err_t lora_radio_set_frequency_centi_mhz(int freq_centi_mhz) { lora_params_t next = s_params; next.freq_centi_mhz = freq_centi_mhz; return lora_radio_apply_params(&next); } void lora_radio_get_params(lora_params_t *out) { if (!out) { return; } *out = s_params; } void lora_radio_tick_tx(void) { if (!s_is_tx) { return; } lr11xx_system_irq_mask_t irq_status = 0; if (lr11xx_system_get_and_clear_irq_status(&s_lr, &irq_status) != LR11XX_STATUS_OK) { return; } if (irq_status == 0) { return; } s_metrics.last_status = (uint8_t)(irq_status & 0xFF); if (irq_status & LR11XX_SYSTEM_IRQ_TX_DONE) { s_tx_in_progress = false; ESP_LOGI(TAG, "TX done"); } if (irq_status & LR11XX_SYSTEM_IRQ_TIMEOUT) { s_tx_in_progress = false; ESP_LOGW(TAG, "TX timeout"); } } void lora_radio_tick_rx(void) { if (s_is_tx) { return; } lr11xx_system_irq_mask_t irq_status = 0; if (lr11xx_system_get_and_clear_irq_status(&s_lr, &irq_status) != LR11XX_STATUS_OK) { return; } if (irq_status == 0) { int64_t now = esp_timer_get_time(); if (now - s_last_rx_restart_us > 1000000) { lr1121_enter_rx_continuous(); } return; } s_metrics.last_status = (uint8_t)(irq_status & 0xFF); if (irq_status & LR11XX_SYSTEM_IRQ_RX_DONE) { lr11xx_radio_rx_buffer_status_t rx_status = {0}; if (lr11xx_radio_get_rx_buffer_status(&s_lr, &rx_status) == LR11XX_STATUS_OK && rx_status.pld_len_in_bytes > 0) { uint8_t buf[sizeof(s_last_payload)]; uint8_t copy_len = rx_status.pld_len_in_bytes; if (copy_len >= sizeof(buf)) { copy_len = sizeof(buf) - 1; } if (lr11xx_regmem_read_buffer8(&s_lr, buf, rx_status.buffer_start_pointer, copy_len) == LR11XX_STATUS_OK) { memcpy(s_last_payload, buf, copy_len); s_last_payload[copy_len] = '\0'; s_last_payload_len = copy_len; lr11xx_radio_pkt_status_lora_t pkt = {0}; if (lr11xx_radio_get_lora_pkt_status(&s_lr, &pkt) == LR11XX_STATUS_OK) { s_metrics.rssi_dbm = pkt.rssi_pkt_in_dbm; s_metrics.snr_db = pkt.snr_pkt_in_db; } ESP_LOGI(TAG, "RX %u bytes RSSI=%ddBm SNR=%ddB \"%s\"", (unsigned)copy_len, s_metrics.rssi_dbm, s_metrics.snr_db, s_last_payload); } } lr1121_enter_rx_continuous(); } if (irq_status & (LR11XX_SYSTEM_IRQ_TIMEOUT | LR11XX_SYSTEM_IRQ_HEADER_ERROR | LR11XX_SYSTEM_IRQ_CRC_ERROR)) { ESP_LOGW(TAG, "RX irq=0x%04X (timeout/header/crc)", irq_status); lr1121_enter_rx_continuous(); } } void lora_radio_get_metrics(lora_metrics_t *out) { if (!out) { return; } *out = s_metrics; } void lora_radio_get_last_payload(char *out, size_t out_len) { if (!out || out_len == 0) { return; } if (s_last_payload_len == 0) { strlcpy(out, "(no data)", out_len); return; } strlcpy(out, s_last_payload, out_len); } esp_err_t lora_radio_send(const uint8_t *data, size_t len) { if (!s_is_tx) { return ESP_ERR_INVALID_STATE; } if (!data || len == 0) { return ESP_ERR_INVALID_ARG; } if (len > 255) { len = 255; // обмеження довжини пакета } // Оновити pkt params під фактичну довжину lr11xx_radio_pkt_params_lora_t pkt = { .preamble_len_in_symb = s_params.preamble_syms, .header_type = s_params.header_implicit ? LR11XX_RADIO_LORA_PKT_IMPLICIT : LR11XX_RADIO_LORA_PKT_EXPLICIT, .pld_len_in_bytes = (uint8_t)len, .crc = s_params.crc_on ? LR11XX_RADIO_LORA_CRC_ON : LR11XX_RADIO_LORA_CRC_OFF, .iq = s_params.iq_invert ? LR11XX_RADIO_LORA_IQ_INVERTED : LR11XX_RADIO_LORA_IQ_STANDARD, }; ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_lora_pkt_params(&s_lr, &pkt)), TAG, "pkt params tx"); ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_regmem_write_buffer8(&s_lr, data, len)), TAG, "write buf"); // timeout 0 => без тайм-ауту, чекаємо TX_DONE ESP_RETURN_ON_ERROR(lr_status_to_esp(lr11xx_radio_set_tx(&s_lr, 0)), TAG, "set tx"); s_tx_in_progress = true; s_last_tx_us = esp_timer_get_time(); s_params.payload_len = (uint8_t)len; return ESP_OK; }