mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-17 23:55:04 +00:00
Compare commits
2 Commits
606b918f9c
...
94deee8875
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94deee8875 | ||
|
|
f13c18f398 |
@ -1,5 +1,6 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
- Drivers/sx126x-module
|
||||
- Drivers/xl9555-module
|
||||
- Drivers/cst66xx-module
|
||||
- Drivers/gdeq031t10-module
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/esp32_pwm_ledc.h>
|
||||
#include <tactility/bindings/pwm_backlight.h>
|
||||
#include <bindings/sx1262.h>
|
||||
#include <bindings/xl9555.h>
|
||||
#include <bindings/cst66xx.h>
|
||||
#include <bindings/gdeq031t10.h>
|
||||
@ -136,7 +137,7 @@
|
||||
host = <SPI2_HOST>;
|
||||
cs-gpios = <&gpio0 34 GPIO_FLAG_NONE>, // 0: EPD display
|
||||
<&gpio0 48 GPIO_FLAG_NONE>, // 1: SD card
|
||||
<&gpio0 3 GPIO_FLAG_NONE>; // 2: LoRa radio (SX1262, not wired up yet)
|
||||
<&gpio0 3 GPIO_FLAG_NONE>; // 2: LoRa radio (SX1262)
|
||||
pin-mosi = <&gpio0 33 GPIO_FLAG_NONE>;
|
||||
pin-miso = <&gpio0 47 GPIO_FLAG_NONE>;
|
||||
pin-sclk = <&gpio0 36 GPIO_FLAG_NONE>;
|
||||
@ -160,5 +161,22 @@
|
||||
status = "disabled";
|
||||
frequency-khz = <20000>;
|
||||
};
|
||||
|
||||
// SX1262 LoRa transceiver, wired per the vendor docs/pinmap.md and
|
||||
// examples/LoRa_sx1262: CS/RST/DIO1/BUSY = GPIO 3/4/5/6, TCXO 2.4 V,
|
||||
// DIO2 drives the antenna TX/RX switch. Module power (LORA_EN, P01)
|
||||
// and antenna select (LORA_SEL, P04: high = internal antenna) are
|
||||
// gated by the XL9555 IO expander.
|
||||
radio@2 {
|
||||
compatible = "semtech,sx1262";
|
||||
reg = <2>;
|
||||
pin-reset = <&gpio0 4 GPIO_FLAG_NONE>;
|
||||
pin-dio1 = <&gpio0 5 GPIO_FLAG_NONE>;
|
||||
pin-busy = <&gpio0 6 GPIO_FLAG_NONE>;
|
||||
pin-enable = <&xl9555 1 GPIO_FLAG_NONE>;
|
||||
pin-antenna-select = <&xl9555 4 GPIO_FLAG_NONE>;
|
||||
tcxo-millivolts = <2400>;
|
||||
dio2-as-rf-switch;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@ -132,6 +132,11 @@ static void hardware_reset(Gdeq031t10Internal* internal) {
|
||||
}
|
||||
|
||||
static bool init_full(Gdeq031t10Internal* internal, bool mirror_180) {
|
||||
// The reset pulse restores register defaults, which the init sequence below assumes:
|
||||
// a cold-booted controller never drives BUSY ready after CMD_POWER_ON without it, a
|
||||
// mode change needs partial mode's lingering VCOM/data-interval setting cleared, and
|
||||
// waking from deep sleep is only possible by toggling RST.
|
||||
hardware_reset(internal);
|
||||
bool ok = write_command(internal, CMD_PANEL_SETTING);
|
||||
ok = ok && write_data_byte(internal, mirror_180 ? 0x13 : 0x1F);
|
||||
ok = ok && write_command(internal, CMD_POWER_ON);
|
||||
|
||||
12
Drivers/sx126x-module/CMakeLists.txt
Normal file
12
Drivers/sx126x-module/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*" "private/*.c*")
|
||||
|
||||
tactility_add_module(sx126x-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
REQUIRES TactilityKernel platform-esp32 radiolib driver esp_timer
|
||||
)
|
||||
195
Drivers/sx126x-module/LICENSE-Apache-2.0.md
Normal file
195
Drivers/sx126x-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
69
Drivers/sx126x-module/README.md
Normal file
69
Drivers/sx126x-module/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# SX126x driver module
|
||||
|
||||
Kernel driver for the Semtech SX1262 sub-GHz LoRa and (G)FSK transceiver, built on
|
||||
[RadioLib](https://github.com/jgromes/RadioLib). It registers a `lora` device
|
||||
(see `<tactility/drivers/lora.h>`) that supports LoRa and FSK for TX/RX and LR-FHSS for TX.
|
||||
|
||||
The device is a child of an SPI controller. The chip-select line comes from the parent's
|
||||
`cs-gpios` entry matching the node's unit address; the `reg` property must match that
|
||||
unit address (checked at device start). The reset, busy and DIO1 lines must be
|
||||
SoC GPIOs (the RadioLib HAL drives them directly); the optional enable and antenna-select
|
||||
lines can live on any GPIO controller, including IO expanders, and are driven to their
|
||||
active level while the device is started.
|
||||
|
||||
Example (LilyGO T-Deck Max):
|
||||
|
||||
```dts
|
||||
spi0 {
|
||||
compatible = "espressif,esp32-spi";
|
||||
cs-gpios = <&gpio0 34 GPIO_FLAG_NONE>, // 0: EPD display
|
||||
<&gpio0 48 GPIO_FLAG_NONE>, // 1: SD card
|
||||
<&gpio0 3 GPIO_FLAG_NONE>; // 2: LoRa radio (SX1262)
|
||||
/* ... */
|
||||
|
||||
radio@2 {
|
||||
compatible = "semtech,sx1262";
|
||||
reg = <2>;
|
||||
pin-reset = <&gpio0 4 GPIO_FLAG_NONE>;
|
||||
pin-dio1 = <&gpio0 5 GPIO_FLAG_NONE>;
|
||||
pin-busy = <&gpio0 6 GPIO_FLAG_NONE>;
|
||||
pin-enable = <&xl9555 1 GPIO_FLAG_NONE>;
|
||||
pin-antenna-select = <&xl9555 4 GPIO_FLAG_NONE>;
|
||||
tcxo-millivolts = <2400>;
|
||||
dio2-as-rf-switch;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Starting the device selects the antenna path, powers the module and runs a GPIO-only
|
||||
probe (reset pulse, then wait for the chip to drive BUSY low), so an absent or
|
||||
unpowered module fails at device start. The modem itself is initialized when the
|
||||
radio is enabled via `lora_set_enabled()`, which requires a modulation to be set first.
|
||||
- Bus sharing: RadioLib drives the chip-select manually across multiple transfers, so
|
||||
each exchange must be atomic on the bus. It runs inside two nested locks — the kernel
|
||||
SPI controller lock (`spi_controller_lock`, the arbiter other kernel drivers on the
|
||||
host cooperate through) as the outer lock, and ESP-IDF's per-host bus lock
|
||||
(`spi_device_acquire_bus`) as the inner one, which additionally blocks the IDF-managed
|
||||
spi_master devices (displays, SD cards) that don't take the controller lock.
|
||||
- TX-done is waited for using the packet's time-on-air plus a fixed margin, so slow
|
||||
configurations (high spreading factor / narrow bandwidth) don't false-time-out.
|
||||
- Over-current protection defaults to RadioLib's fail-safe 60 mA, which caps the PA
|
||||
current into a bad or disconnected antenna but also caps output below +22 dBm. A
|
||||
board-aware consumer that knows its antenna and PA can raise it via
|
||||
`LORA_PARAMETER_CURRENT_LIMIT` (up to 140 mA) to reach full output power.
|
||||
- Radio activity is logged at INFO level (state changes, modem config, TX/RX events);
|
||||
per-call detail sits at DEBUG, which is compiled out unless the sdkconfig log level
|
||||
is raised.
|
||||
|
||||
## Roadmap / not yet implemented
|
||||
|
||||
- FSK addressed transmission is not exposed through the lora API yet.
|
||||
- Antenna-presence detection. The SX1262 has no hardware antenna-detect pin (unlike, e.g.,
|
||||
the u-blox SARA modules' dedicated ANT_DET ADC circuit), and the T-Deck Max antenna
|
||||
connector is not hot-swap-rated. The intended approach is a software heuristic on the
|
||||
radio thread — watching the RX noise floor / RSSI for the signature of a
|
||||
disconnected/open PA load and disabling TX at the driver layer when detected — so the
|
||||
protection lives below the app/OS. This is unproven and deliberately not built yet;
|
||||
feedback on the method is welcome before implementing it.
|
||||
47
Drivers/sx126x-module/bindings/semtech,sx1262.yaml
Normal file
47
Drivers/sx126x-module/bindings/semtech,sx1262.yaml
Normal file
@ -0,0 +1,47 @@
|
||||
description: Semtech SX1262 sub-GHz LoRa and (G)FSK transceiver
|
||||
|
||||
compatible: "semtech,sx1262"
|
||||
|
||||
bus: spi
|
||||
|
||||
properties:
|
||||
reg:
|
||||
type: int
|
||||
required: true
|
||||
description: Chip-select index on the parent SPI controller, must match the node's unit address
|
||||
spi-frequency-khz:
|
||||
type: int
|
||||
default: 4000
|
||||
description: SPI clock frequency in kHz
|
||||
pin-reset:
|
||||
type: phandles
|
||||
required: true
|
||||
description: NRESET line (must be an SoC GPIO)
|
||||
pin-busy:
|
||||
type: phandles
|
||||
required: true
|
||||
description: BUSY line (must be an SoC GPIO)
|
||||
pin-dio1:
|
||||
type: phandles
|
||||
required: true
|
||||
description: DIO1 interrupt line (must be an SoC GPIO)
|
||||
pin-enable:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Optional module power enable, driven active while the device is started (any GPIO controller)
|
||||
pin-antenna-select:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Optional antenna select, driven active while the device is started (any GPIO controller)
|
||||
tcxo-millivolts:
|
||||
type: int
|
||||
default: 0
|
||||
description: TCXO reference voltage on DIO3 in millivolts, 0 when no TCXO is fitted
|
||||
use-regulator-ldo:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Use the LDO regulator instead of the DC-DC converter
|
||||
dio2-as-rf-switch:
|
||||
type: boolean
|
||||
default: false
|
||||
description: DIO2 drives the antenna TX/RX switch
|
||||
3
Drivers/sx126x-module/devicetree.yaml
Normal file
3
Drivers/sx126x-module/devicetree.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
15
Drivers/sx126x-module/include/bindings/sx1262.h
Normal file
15
Drivers/sx126x-module/include/bindings/sx1262.h
Normal file
@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/sx1262.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(sx1262, struct Sx1262Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
38
Drivers/sx126x-module/include/drivers/sx1262.h
Normal file
38
Drivers/sx126x-module/include/drivers/sx1262.h
Normal file
@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Field order must match the property order in bindings/semtech,sx1262.yaml */
|
||||
struct Sx1262Config {
|
||||
/** Chip-select index on the parent SPI controller, must match the node's unit address */
|
||||
int32_t reg;
|
||||
/** SPI clock frequency in kHz */
|
||||
uint32_t spi_frequency_khz;
|
||||
/** NRESET line (must be an SoC GPIO) */
|
||||
struct GpioPinSpec pin_reset;
|
||||
/** BUSY line (must be an SoC GPIO) */
|
||||
struct GpioPinSpec pin_busy;
|
||||
/** DIO1 interrupt line (must be an SoC GPIO) */
|
||||
struct GpioPinSpec pin_dio1;
|
||||
/** Optional module power enable, driven active while the device is started */
|
||||
struct GpioPinSpec pin_enable;
|
||||
/** Optional antenna select, driven active while the device is started */
|
||||
struct GpioPinSpec pin_antenna_select;
|
||||
/** TCXO reference voltage on DIO3 in millivolts, 0 when no TCXO is fitted */
|
||||
uint32_t tcxo_millivolts;
|
||||
/** Use the LDO regulator instead of the DC-DC converter */
|
||||
bool use_regulator_ldo;
|
||||
/** DIO2 drives the antenna TX/RX switch */
|
||||
bool dio2_as_rf_switch;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
14
Drivers/sx126x-module/include/sx126x_module.h
Normal file
14
Drivers/sx126x-module/include/sx126x_module.h
Normal file
@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module sx126x_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
968
Drivers/sx126x-module/private/drivers/sx1262_radio.cpp
Normal file
968
Drivers/sx126x-module/private/drivers/sx1262_radio.cpp
Normal file
@ -0,0 +1,968 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "sx1262_radio.h"
|
||||
|
||||
#include "sx126x_radiolib_hal.h"
|
||||
|
||||
#include <tactility/concurrent/event_group.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
|
||||
#include <RadioLib.h>
|
||||
|
||||
#define TAG "sx1262"
|
||||
|
||||
namespace {
|
||||
|
||||
// TX-done wait is derived from the packet's time-on-air: fixed timeouts either
|
||||
// false-time-out on slow configs (high SF / narrow BW, airtime up to seconds) or
|
||||
// wait needlessly long on fast ones. The margin covers PA ramp and command latency;
|
||||
// the fallback is used only when RadioLib can't compute airtime for the modem config.
|
||||
constexpr auto SX1262_TX_TIMEOUT_MARGIN_MILLIS = 1000;
|
||||
constexpr auto SX1262_TX_TIMEOUT_FALLBACK_MILLIS = 2000;
|
||||
constexpr uint32_t SX1262_INTERRUPT_BIT = (1 << 0);
|
||||
constexpr uint32_t SX1262_DIO1_EVENT_BIT = (1 << 1);
|
||||
constexpr uint32_t SX1262_QUEUED_TX_BIT = (1 << 2);
|
||||
constexpr auto SX1262_IRQ_FLAGS = RADIOLIB_IRQ_RX_DEFAULT_FLAGS;
|
||||
// RX callbacks run on the radio thread and may do non-trivial work (e.g. packet decryption)
|
||||
constexpr size_t SX1262_THREAD_STACK_SIZE = 8192;
|
||||
|
||||
const char* toString(enum LoraRadioState state) {
|
||||
switch (state) {
|
||||
case LORA_RADIO_STATE_OFF:
|
||||
return "off";
|
||||
case LORA_RADIO_STATE_ON_PENDING:
|
||||
return "on-pending";
|
||||
case LORA_RADIO_STATE_ON:
|
||||
return "on";
|
||||
case LORA_RADIO_STATE_OFF_PENDING:
|
||||
return "off-pending";
|
||||
case LORA_RADIO_STATE_ERROR:
|
||||
return "error";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* toString(enum LoraModulation modulation) {
|
||||
switch (modulation) {
|
||||
case LORA_MODULATION_NONE:
|
||||
return "none";
|
||||
case LORA_MODULATION_FSK:
|
||||
return "FSK";
|
||||
case LORA_MODULATION_LORA:
|
||||
return "LoRa";
|
||||
case LORA_MODULATION_LR_FHSS:
|
||||
return "LR-FHSS";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* toString(enum LoraParameter parameter) {
|
||||
switch (parameter) {
|
||||
case LORA_PARAMETER_POWER:
|
||||
return "power";
|
||||
case LORA_PARAMETER_BOOSTED_GAIN:
|
||||
return "boosted gain";
|
||||
case LORA_PARAMETER_FREQUENCY:
|
||||
return "frequency";
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
return "bandwidth";
|
||||
case LORA_PARAMETER_SPREADING_FACTOR:
|
||||
return "spreading factor";
|
||||
case LORA_PARAMETER_CODING_RATE:
|
||||
return "coding rate";
|
||||
case LORA_PARAMETER_SYNC_WORD:
|
||||
return "sync word";
|
||||
case LORA_PARAMETER_PREAMBLE_LENGTH:
|
||||
return "preamble length";
|
||||
case LORA_PARAMETER_FREQUENCY_DEVIATION:
|
||||
return "frequency deviation";
|
||||
case LORA_PARAMETER_DATA_RATE:
|
||||
return "data rate";
|
||||
case LORA_PARAMETER_NARROW_GRID:
|
||||
return "narrow grid";
|
||||
case LORA_PARAMETER_CURRENT_LIMIT:
|
||||
return "current limit";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
constexpr error_t checkLimitsAndApply(T& target, const int32_t value, const int32_t lower, const int32_t upper, const int32_t step = 0) {
|
||||
if ((value >= lower) && (value <= upper)) {
|
||||
if ((step != 0) && ((value % step) != 0)) {
|
||||
return ERROR_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
target = static_cast<T>(value);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
return ERROR_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
constexpr error_t checkValuesAndApply(T& target, const int32_t value, std::initializer_list<int32_t> valids) {
|
||||
for (int32_t valid : valids) {
|
||||
if (value == valid) {
|
||||
target = static_cast<T>(value);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
}
|
||||
return ERROR_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Sx1262Radio::RadioParts {
|
||||
Sx126xRadiolibHal hal;
|
||||
Module radioModule;
|
||||
SX1262 radio;
|
||||
|
||||
explicit RadioParts(const Settings& settings)
|
||||
: hal(settings.spi_host, settings.spi_frequency_hz, settings.spi_controller)
|
||||
, radioModule(&hal, settings.pin_cs, RADIOLIB_NC, settings.pin_reset, settings.pin_busy)
|
||||
, radio(&radioModule) {}
|
||||
};
|
||||
|
||||
Sx1262Radio::Sx1262Radio(const Settings& settings)
|
||||
: settings(settings) {
|
||||
recursive_mutex_construct(&mutex);
|
||||
event_group_construct(&events);
|
||||
parts = new RadioParts(settings);
|
||||
}
|
||||
|
||||
Sx1262Radio::~Sx1262Radio() {
|
||||
setEnabled(false);
|
||||
delete parts;
|
||||
event_group_destruct(&events);
|
||||
recursive_mutex_destruct(&mutex);
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::probe() const {
|
||||
// NRESET output, idle high. BUSY input with a pull-up: an absent or unpowered
|
||||
// module leaves BUSY floating, and the pull-up parks it high so the ready
|
||||
// check below can't false-pass on a floating line.
|
||||
gpio_config_t reset_conf = {
|
||||
.pin_bit_mask = (1ULL << settings.pin_reset),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&reset_conf);
|
||||
gpio_set_level(settings.pin_reset, 1);
|
||||
|
||||
gpio_config_t busy_conf = {
|
||||
.pin_bit_mask = (1ULL << settings.pin_busy),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&busy_conf);
|
||||
|
||||
// Reset pulse (datasheet: NRESET low for >= 100 us triggers a full reset)
|
||||
gpio_set_level(settings.pin_reset, 0);
|
||||
delay_millis(2);
|
||||
gpio_set_level(settings.pin_reset, 1);
|
||||
|
||||
// After reset the chip boots and calibrates with BUSY high, then drives BUSY
|
||||
// low once it reaches STDBY_RC (datasheet: ~3.5 ms max). Allow a generous
|
||||
// margin; a line stuck high means no chip is answering.
|
||||
constexpr auto PROBE_TIMEOUT_MILLIS = 20;
|
||||
int elapsed = 0;
|
||||
while (gpio_get_level(settings.pin_busy) != 0) {
|
||||
if (elapsed >= PROBE_TIMEOUT_MILLIS) {
|
||||
LOG_E(TAG, "Probe failed: BUSY (GPIO %d) stuck high after reset — module absent or unpowered?", settings.pin_busy);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
delay_millis(1);
|
||||
elapsed++;
|
||||
}
|
||||
|
||||
// Drop the probe pull-up again: the chip actively drives BUSY when powered,
|
||||
// and RadioLib reconfigures the pin at begin() anyway.
|
||||
busy_conf.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
gpio_config(&busy_conf);
|
||||
|
||||
LOG_I(TAG, "Probe OK: SX1262 answered reset in ~%d ms (BUSY low)", elapsed);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// region Thread lifecycle
|
||||
|
||||
void Sx1262Radio::dio1Isr(void* context) {
|
||||
auto* self = static_cast<Sx1262Radio*>(context);
|
||||
// DIO1 is armed as a HIGH_LEVEL interrupt (edge types are unreliable on the
|
||||
// ESP32 per erratum 3.11). A level interrupt re-fires for as long as the line
|
||||
// is asserted, so mask it here and let the radio thread re-arm once it has
|
||||
// cleared the modem IRQ (which drops DIO1 low again).
|
||||
gpio_descriptor_disable_interrupt(self->settings.dio1);
|
||||
event_group_set(self->events, SX1262_DIO1_EVENT_BIT);
|
||||
}
|
||||
|
||||
int32_t Sx1262Radio::threadMainStatic(void* context) {
|
||||
return static_cast<Sx1262Radio*>(context)->threadMain();
|
||||
}
|
||||
|
||||
bool Sx1262Radio::isThreadInterrupted() const {
|
||||
lock();
|
||||
const bool interrupted = threadInterrupted;
|
||||
unlock();
|
||||
return interrupted;
|
||||
}
|
||||
|
||||
int32_t Sx1262Radio::threadMain() {
|
||||
int rc = doBegin(getModulation());
|
||||
bool hasRx = false;
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
setState(LORA_RADIO_STATE_ON);
|
||||
|
||||
while (!isThreadInterrupted()) {
|
||||
// Re-arm DIO1: the ISR masks the HIGH_LEVEL interrupt on each fire, so the
|
||||
// modem's next RX/TX-done needs it enabled again. DIO1 is low here (the
|
||||
// previous IRQ was cleared by doReceive()/doTransmit()); re-arming while it
|
||||
// were still asserted would just self-fire once and be absorbed by the
|
||||
// empty-read guard in doReceive().
|
||||
gpio_descriptor_enable_interrupt(settings.dio1);
|
||||
|
||||
hasRx = doListen();
|
||||
|
||||
// Thread might've been interrupted in the meanwhile
|
||||
if (isThreadInterrupted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Service a received packet before deciding to transmit: an RX-done and a
|
||||
// queued TX can coincide in the same iteration, and dropping the RX here would
|
||||
// lose the packet outright.
|
||||
if (hasRx) {
|
||||
doReceive();
|
||||
}
|
||||
if (getTxQueueSize() > 0) {
|
||||
doTransmit();
|
||||
}
|
||||
}
|
||||
|
||||
doEnd();
|
||||
return 0;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::setEnabled(bool enabled) {
|
||||
lock();
|
||||
|
||||
if (enabled) {
|
||||
if ((thread != nullptr) && (thread_get_state(thread) != THREAD_STATE_STOPPED)) {
|
||||
LOG_W(TAG, "Already started");
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (modulation == LORA_MODULATION_NONE) {
|
||||
LOG_E(TAG, "Cannot enable without a modulation set");
|
||||
unlock();
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (thread != nullptr) {
|
||||
thread_free(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
|
||||
threadInterrupted = false;
|
||||
setState(LORA_RADIO_STATE_ON_PENDING);
|
||||
|
||||
thread = thread_alloc_full("SX1262", SX1262_THREAD_STACK_SIZE, threadMainStatic, this, tskNO_AFFINITY);
|
||||
if (thread == nullptr) {
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
unlock();
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
thread_set_priority(thread, THREAD_PRIORITY_HIGH);
|
||||
|
||||
if (thread_start(thread) != ERROR_NONE) {
|
||||
thread_free(thread);
|
||||
thread = nullptr;
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
unlock();
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
} else {
|
||||
setState(LORA_RADIO_STATE_OFF_PENDING);
|
||||
|
||||
if (thread != nullptr) {
|
||||
threadInterrupted = true;
|
||||
event_group_set(events, SX1262_INTERRUPT_BIT);
|
||||
|
||||
Thread* oldThread = thread;
|
||||
thread = nullptr;
|
||||
|
||||
if (thread_get_state(oldThread) != THREAD_STATE_STOPPED) {
|
||||
// Unlock so the thread can lock
|
||||
unlock();
|
||||
// Wait for the thread to finish
|
||||
thread_join(oldThread, portMAX_DELAY, pdMS_TO_TICKS(10));
|
||||
// Re-lock to continue logic below
|
||||
lock();
|
||||
}
|
||||
|
||||
thread_free(oldThread);
|
||||
}
|
||||
|
||||
setState(LORA_RADIO_STATE_OFF);
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region State, modulation and callbacks
|
||||
|
||||
enum LoraRadioState Sx1262Radio::getState() const {
|
||||
lock();
|
||||
const auto result = state;
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Sx1262Radio::setState(enum LoraRadioState newState) {
|
||||
lock();
|
||||
if (state == newState) {
|
||||
unlock();
|
||||
return;
|
||||
}
|
||||
LOG_I(TAG, "State: %s -> %s", toString(state), toString(newState));
|
||||
state = newState;
|
||||
auto callbacks = stateCallbacks;
|
||||
unlock();
|
||||
|
||||
for (const auto& entry : callbacks) {
|
||||
entry.callback(settings.device, entry.context, newState);
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::setModulation(enum LoraModulation newModulation) {
|
||||
const auto currentState = getState();
|
||||
if ((currentState == LORA_RADIO_STATE_ON_PENDING) || (currentState == LORA_RADIO_STATE_ON)) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (!((newModulation == LORA_MODULATION_NONE) || canTransmit(newModulation) || canReceive(newModulation))) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
lock();
|
||||
LOG_I(TAG, "Modulation set to %s", toString(newModulation));
|
||||
modulation = newModulation;
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
enum LoraModulation Sx1262Radio::getModulation() const {
|
||||
lock();
|
||||
const auto result = modulation;
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Callbacks are invoked on a snapshot of the list, with the radio mutex released:
|
||||
// consumers take their own locks in callbacks and also call into this API while
|
||||
// holding those locks, so invoking under the radio mutex would set up an AB-BA
|
||||
// deadlock between the radio thread and any consumer thread.
|
||||
void Sx1262Radio::publishRx(const struct LoraRxPacket& packet) {
|
||||
lock();
|
||||
auto callbacks = rxCallbacks;
|
||||
unlock();
|
||||
|
||||
for (const auto& entry : callbacks) {
|
||||
entry.callback(settings.device, entry.context, &packet);
|
||||
}
|
||||
}
|
||||
|
||||
void Sx1262Radio::publishTx(LoraTxId id, enum LoraTransmissionState txState) {
|
||||
lock();
|
||||
auto callbacks = txCallbacks;
|
||||
unlock();
|
||||
|
||||
for (const auto& entry : callbacks) {
|
||||
entry.callback(settings.device, entry.context, id, txState);
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::addRxCallback(void* context, LoraRxCallback callback) {
|
||||
lock();
|
||||
rxCallbacks.push_back({context, callback});
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::removeRxCallback(LoraRxCallback callback) {
|
||||
lock();
|
||||
const auto old_size = rxCallbacks.size();
|
||||
std::erase_if(rxCallbacks, [callback](const auto& entry) { return entry.callback == callback; });
|
||||
const auto result = (rxCallbacks.size() == old_size) ? ERROR_NOT_FOUND : ERROR_NONE;
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::addStateCallback(void* context, LoraStateCallback callback) {
|
||||
lock();
|
||||
stateCallbacks.push_back({context, callback});
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::removeStateCallback(LoraStateCallback callback) {
|
||||
lock();
|
||||
const auto old_size = stateCallbacks.size();
|
||||
std::erase_if(stateCallbacks, [callback](const auto& entry) { return entry.callback == callback; });
|
||||
const auto result = (stateCallbacks.size() == old_size) ? ERROR_NOT_FOUND : ERROR_NONE;
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::addTxCallback(void* context, LoraTxCallback callback) {
|
||||
lock();
|
||||
txCallbacks.push_back({context, callback});
|
||||
unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::removeTxCallback(LoraTxCallback callback) {
|
||||
lock();
|
||||
const auto old_size = txCallbacks.size();
|
||||
std::erase_if(txCallbacks, [callback](const auto& entry) { return entry.callback == callback; });
|
||||
const auto result = (txCallbacks.size() == old_size) ? ERROR_NOT_FOUND : ERROR_NONE;
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region TX queue
|
||||
|
||||
size_t Sx1262Radio::getTxQueueSize() const {
|
||||
lock();
|
||||
const auto size = txQueue.size();
|
||||
unlock();
|
||||
return size;
|
||||
}
|
||||
|
||||
Sx1262Radio::TxItem Sx1262Radio::popNextQueuedTx() {
|
||||
lock();
|
||||
auto tx = std::move(txQueue.front());
|
||||
txQueue.pop_front();
|
||||
unlock();
|
||||
return tx;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::transmit(const uint8_t* data, size_t length, LoraTxId* id) {
|
||||
lock();
|
||||
const auto txId = lastTxId;
|
||||
lastTxId++;
|
||||
txQueue.push_back(TxItem {.id = txId, .data = std::vector<uint8_t>(data, data + length)});
|
||||
LOG_D(TAG, "TX id=%d queued: %u bytes (queue depth %u)", (int)txId, (unsigned)length, (unsigned)txQueue.size());
|
||||
unlock();
|
||||
|
||||
publishTx(txId, LORA_TRANSMISSION_STATE_QUEUED);
|
||||
event_group_set(events, SX1262_QUEUED_TX_BIT);
|
||||
|
||||
if (id != nullptr) {
|
||||
*id = txId;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Parameters
|
||||
|
||||
error_t Sx1262Radio::setBaseParameter(enum LoraParameter parameter, int32_t value) {
|
||||
switch (parameter) {
|
||||
case LORA_PARAMETER_POWER:
|
||||
return checkLimitsAndApply(power, value, -9, 22);
|
||||
case LORA_PARAMETER_BOOSTED_GAIN:
|
||||
return checkLimitsAndApply(boostedGain, value, 0, 1, 1);
|
||||
case LORA_PARAMETER_CURRENT_LIMIT:
|
||||
// SX1262 OCP range is 0..140 mA (RadioLib clamps to a 2.5 mA step internally).
|
||||
return checkLimitsAndApply(currentLimit, value, 0, 140);
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::setLoraParameter(enum LoraParameter parameter, int32_t value) {
|
||||
switch (parameter) {
|
||||
// Frequency in Hz (150..960 MHz)
|
||||
case LORA_PARAMETER_FREQUENCY:
|
||||
return checkLimitsAndApply(frequency, value, 150000000, 960000000);
|
||||
// Bandwidth in Hz (RadioLib's supported LoRa bandwidths, expressed in Hz)
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
return checkValuesAndApply(bandwidth, value, {7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000, 500000});
|
||||
case LORA_PARAMETER_SPREADING_FACTOR:
|
||||
return checkLimitsAndApply(spreadingFactor, value, 7, 12, 1);
|
||||
case LORA_PARAMETER_CODING_RATE:
|
||||
return checkLimitsAndApply(codingRate, value, 5, 8, 1);
|
||||
case LORA_PARAMETER_SYNC_WORD:
|
||||
return checkLimitsAndApply(syncWord, value, 0, 255);
|
||||
case LORA_PARAMETER_PREAMBLE_LENGTH:
|
||||
return checkLimitsAndApply(preambleLength, value, 0, 65535);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_W(TAG, "Tried to set unsupported LoRa parameter \"%s\" to %d", toString(parameter), (int)value);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::setFskParameter(enum LoraParameter parameter, int32_t value) {
|
||||
switch (parameter) {
|
||||
// Frequency in Hz (150..960 MHz)
|
||||
case LORA_PARAMETER_FREQUENCY:
|
||||
return checkLimitsAndApply(frequency, value, 150000000, 960000000);
|
||||
// RX bandwidth in Hz (RadioLib's supported FSK bandwidths, expressed in Hz)
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
return checkValuesAndApply(bandwidth, value, {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400, 29300, 39000, 46900, 58600, 78200});
|
||||
case LORA_PARAMETER_PREAMBLE_LENGTH:
|
||||
return checkLimitsAndApply(preambleLength, value, 0, 65535);
|
||||
// Bit rate in bit/s (0.6..300 kbps)
|
||||
case LORA_PARAMETER_DATA_RATE:
|
||||
return checkLimitsAndApply(bitRate, value, 600, 300000);
|
||||
// Frequency deviation in Hz (0..200 kHz)
|
||||
case LORA_PARAMETER_FREQUENCY_DEVIATION:
|
||||
return checkLimitsAndApply(frequencyDeviation, value, 0, 200000);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_W(TAG, "Tried to set unsupported FSK parameter \"%s\" to %d", toString(parameter), (int)value);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::setLrFhssParameter(enum LoraParameter parameter, int32_t value) {
|
||||
switch (parameter) {
|
||||
// Bandwidth in Hz (RadioLib's supported LR-FHSS bandwidths, expressed in Hz)
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
return checkValuesAndApply(bandwidth, value, {39060, 85940, 136720, 183590, 335940, 386720, 722660, 773440, 1523400, 1574200});
|
||||
case LORA_PARAMETER_CODING_RATE:
|
||||
return checkValuesAndApply(codingRate, value, {RADIOLIB_SX126X_LR_FHSS_CR_5_6, RADIOLIB_SX126X_LR_FHSS_CR_2_3, RADIOLIB_SX126X_LR_FHSS_CR_1_2, RADIOLIB_SX126X_LR_FHSS_CR_1_3});
|
||||
case LORA_PARAMETER_NARROW_GRID:
|
||||
return checkLimitsAndApply(narrowGrid, value, 0, 1, 1);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_W(TAG, "Tried to set unsupported LR-FHSS parameter \"%s\" to %d", toString(parameter), (int)value);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::setParameter(enum LoraParameter parameter, int32_t value) {
|
||||
lock();
|
||||
|
||||
error_t result = setBaseParameter(parameter, value);
|
||||
if (result == ERROR_NOT_SUPPORTED) {
|
||||
switch (modulation) {
|
||||
case LORA_MODULATION_LORA:
|
||||
result = setLoraParameter(parameter, value);
|
||||
break;
|
||||
case LORA_MODULATION_FSK:
|
||||
result = setFskParameter(parameter, value);
|
||||
break;
|
||||
case LORA_MODULATION_LR_FHSS:
|
||||
result = setLrFhssParameter(parameter, value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == ERROR_NONE) {
|
||||
LOG_D(TAG, "Parameter %s = %d", toString(parameter), (int)value);
|
||||
}
|
||||
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::getBaseParameter(enum LoraParameter parameter, int32_t* value) const {
|
||||
switch (parameter) {
|
||||
case LORA_PARAMETER_POWER:
|
||||
*value = power;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_BOOSTED_GAIN:
|
||||
*value = boostedGain;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_CURRENT_LIMIT:
|
||||
*value = currentLimit;
|
||||
return ERROR_NONE;
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::getLoraParameter(enum LoraParameter parameter, int32_t* value) const {
|
||||
switch (parameter) {
|
||||
case LORA_PARAMETER_FREQUENCY:
|
||||
*value = frequency;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
*value = bandwidth;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_SPREADING_FACTOR:
|
||||
*value = spreadingFactor;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_CODING_RATE:
|
||||
*value = codingRate;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_SYNC_WORD:
|
||||
*value = syncWord;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_PREAMBLE_LENGTH:
|
||||
*value = preambleLength;
|
||||
return ERROR_NONE;
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::getFskParameter(enum LoraParameter parameter, int32_t* value) const {
|
||||
switch (parameter) {
|
||||
case LORA_PARAMETER_FREQUENCY:
|
||||
*value = frequency;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
*value = bandwidth;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_DATA_RATE:
|
||||
*value = bitRate;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_FREQUENCY_DEVIATION:
|
||||
*value = frequencyDeviation;
|
||||
return ERROR_NONE;
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::getLrFhssParameter(enum LoraParameter parameter, int32_t* value) const {
|
||||
switch (parameter) {
|
||||
case LORA_PARAMETER_BANDWIDTH:
|
||||
*value = bandwidth;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_CODING_RATE:
|
||||
*value = codingRate;
|
||||
return ERROR_NONE;
|
||||
case LORA_PARAMETER_NARROW_GRID:
|
||||
*value = narrowGrid;
|
||||
return ERROR_NONE;
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
error_t Sx1262Radio::getParameter(enum LoraParameter parameter, int32_t* value) const {
|
||||
lock();
|
||||
|
||||
// No warnings are emitted to be able to discover parameters by return status
|
||||
error_t result = getBaseParameter(parameter, value);
|
||||
if (result == ERROR_NOT_SUPPORTED) {
|
||||
switch (modulation) {
|
||||
case LORA_MODULATION_LORA:
|
||||
result = getLoraParameter(parameter, value);
|
||||
break;
|
||||
case LORA_MODULATION_FSK:
|
||||
result = getFskParameter(parameter, value);
|
||||
break;
|
||||
case LORA_MODULATION_LR_FHSS:
|
||||
result = getLrFhssParameter(parameter, value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Radio operations (radio thread only)
|
||||
|
||||
// DIO1 uses the GPIO descriptor callback API in HIGH_LEVEL mode. The interrupt is
|
||||
// armed (enabled) per cycle by the radio thread loop and masked by the ISR on each
|
||||
// fire; the modem asserts DIO1 for RX/TX-done, which the driver clears by reading
|
||||
// the packet or finishing the transmission. Edge-triggered interrupts are avoided
|
||||
// on purpose (ESP32 erratum 3.11: subsequent edge interrupts may be missed, which
|
||||
// for a radio would drop an RX/TX-done and stall the thread).
|
||||
void Sx1262Radio::registerDio1Isr() {
|
||||
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT;
|
||||
flags = GPIO_FLAG_INTERRUPT_TO_OPTIONS(flags, GPIO_INTERRUPT_HIGH_LEVEL);
|
||||
if (gpio_descriptor_set_flags(settings.dio1, flags) != ERROR_NONE ||
|
||||
gpio_descriptor_add_callback(settings.dio1, dio1Isr, this) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to install DIO1 interrupt");
|
||||
}
|
||||
}
|
||||
|
||||
void Sx1262Radio::unregisterDio1Isr() {
|
||||
gpio_descriptor_disable_interrupt(settings.dio1);
|
||||
gpio_descriptor_remove_callback(settings.dio1);
|
||||
}
|
||||
|
||||
int Sx1262Radio::doBegin(enum LoraModulation beginModulation) {
|
||||
int16_t rc = RADIOLIB_ERR_NONE;
|
||||
auto& radio = parts->radio;
|
||||
|
||||
// RadioLib takes MHz/kHz/kbps floats; the driver stores Hz/bit/s integers.
|
||||
const float frequencyMhz = static_cast<float>(frequency) / 1000000.0f;
|
||||
const float bandwidthKhz = static_cast<float>(bandwidth) / 1000.0f;
|
||||
|
||||
if (beginModulation == LORA_MODULATION_LORA) {
|
||||
LOG_I(
|
||||
TAG,
|
||||
"Starting LoRa: %.3f MHz, BW %.2f kHz, SF%u, CR 4/%u, sync 0x%02X, preamble %u, %d dBm, TCXO %.1f V",
|
||||
frequencyMhz,
|
||||
bandwidthKhz,
|
||||
spreadingFactor,
|
||||
codingRate,
|
||||
syncWord,
|
||||
preambleLength,
|
||||
power,
|
||||
settings.tcxo_voltage
|
||||
);
|
||||
rc = radio.begin(
|
||||
frequencyMhz,
|
||||
bandwidthKhz,
|
||||
spreadingFactor,
|
||||
codingRate,
|
||||
syncWord,
|
||||
power,
|
||||
preambleLength,
|
||||
settings.tcxo_voltage,
|
||||
settings.use_regulator_ldo
|
||||
);
|
||||
} else if (beginModulation == LORA_MODULATION_FSK) {
|
||||
const float bitRateKbps = static_cast<float>(bitRate) / 1000.0f;
|
||||
const float frequencyDeviationKhz = static_cast<float>(frequencyDeviation) / 1000.0f;
|
||||
LOG_I(
|
||||
TAG,
|
||||
"Starting FSK: %.3f MHz, %.2f kbps, deviation %.1f kHz, BW %.1f kHz, preamble %u, %d dBm",
|
||||
frequencyMhz,
|
||||
bitRateKbps,
|
||||
frequencyDeviationKhz,
|
||||
bandwidthKhz,
|
||||
preambleLength,
|
||||
power
|
||||
);
|
||||
rc = radio.beginFSK(
|
||||
frequencyMhz,
|
||||
bitRateKbps,
|
||||
frequencyDeviationKhz,
|
||||
bandwidthKhz,
|
||||
power,
|
||||
preambleLength,
|
||||
settings.tcxo_voltage,
|
||||
settings.use_regulator_ldo
|
||||
);
|
||||
} else if (beginModulation == LORA_MODULATION_LR_FHSS) {
|
||||
// NOTE: LR-FHSS is unvalidated. RadioLib's beginLRFHSS() takes
|
||||
// (freq, bw-index, cr, narrowGrid, ...) where bw is a RADIOLIB_SX126X_LR_FHSS_BW_*
|
||||
// index, not a frequency; this call passes the stored bandwidth into the freq slot
|
||||
// and is known to be incomplete. Left as-is pending a dedicated LR-FHSS bring-up —
|
||||
// the LoRa and FSK paths above are the hardware-validated ones.
|
||||
LOG_I(TAG, "Starting LR-FHSS: BW %d Hz, CR %u, %s grid", (int)bandwidth, codingRate, narrowGrid ? "narrow" : "wide");
|
||||
rc = radio.beginLRFHSS(
|
||||
bandwidth,
|
||||
codingRate,
|
||||
narrowGrid,
|
||||
settings.tcxo_voltage,
|
||||
settings.use_regulator_ldo
|
||||
);
|
||||
} else {
|
||||
LOG_E(TAG, "SX1262 not capable of modulation \"%s\"", toString(beginModulation));
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_E(TAG, "RadioLib initialization failed with code %hi", rc);
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Apply the PA over-current protection limit. RadioLib's begin() already set its
|
||||
// fail-safe default (60 mA), so this is only meaningful when a consumer raised it
|
||||
// via LORA_PARAMETER_CURRENT_LIMIT to reach higher output power.
|
||||
rc = radio.setCurrentLimit(static_cast<float>(currentLimit));
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_E(TAG, "Setting current limit to %d mA failed with code %hi", (int)currentLimit, rc);
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Modules that wire the antenna TX/RX switch to DIO2 (e.g. LilyGO T-Deck Max)
|
||||
// must enable this or the RF path stays disconnected and no TX/RX gets through.
|
||||
if (settings.dio2_rf_switch) {
|
||||
rc = radio.setDio2AsRfSwitch(true);
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_E(TAG, "Setting DIO2 as RF switch failed with code %hi", rc);
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
rc = radio.setRxBoostedGainMode(boostedGain, true);
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_E(TAG, "Setting RX boosted gain to %s failed with code %hi", boostedGain ? "true" : "false", rc);
|
||||
setState(LORA_RADIO_STATE_ERROR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Modem initialized (chip verified by RadioLib)");
|
||||
registerDio1Isr();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Sx1262Radio::doEnd() {
|
||||
unregisterDio1Isr();
|
||||
// Leave the modem in its lowest-power state; the next enable runs a full begin()
|
||||
const int16_t rc = parts->radio.sleep();
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_W(TAG, "Putting modem to sleep failed with code %hi", rc);
|
||||
} else {
|
||||
LOG_I(TAG, "Modem put to sleep");
|
||||
}
|
||||
}
|
||||
|
||||
void Sx1262Radio::doTransmit() {
|
||||
currentTx = popNextQueuedTx();
|
||||
auto& radio = parts->radio;
|
||||
|
||||
int16_t rc = radio.standby();
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_W(TAG, "RadioLib returned %hi on TX standby", rc);
|
||||
}
|
||||
|
||||
LOG_I(TAG, "TX id=%d: %u bytes (%u more queued)", (int)currentTx.id, (unsigned)currentTx.data.size(), (unsigned)getTxQueueSize());
|
||||
rc = radio.startTransmit(currentTx.data.data(), currentTx.data.size());
|
||||
|
||||
if (rc == RADIOLIB_ERR_NONE) {
|
||||
publishTx(currentTx.id, LORA_TRANSMISSION_STATE_TRANSMIT_PENDING);
|
||||
|
||||
// Time-on-air (microseconds) for the current modem config; 0 if RadioLib can't
|
||||
// compute it, in which case fall back to a fixed timeout.
|
||||
const uint32_t airtimeMillis = radio.getTimeOnAir(currentTx.data.size()) / 1000;
|
||||
const uint32_t txTimeoutMillis = (airtimeMillis > 0)
|
||||
? (airtimeMillis + SX1262_TX_TIMEOUT_MARGIN_MILLIS)
|
||||
: SX1262_TX_TIMEOUT_FALLBACK_MILLIS;
|
||||
|
||||
// outFlags stays 0 on timeout, which routes to the Timeout branch below
|
||||
uint32_t txEventFlags = 0;
|
||||
event_group_wait(
|
||||
events,
|
||||
SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT,
|
||||
false,
|
||||
true,
|
||||
&txEventFlags,
|
||||
pdMS_TO_TICKS(txTimeoutMillis)
|
||||
);
|
||||
|
||||
// Clean up after transmission
|
||||
radio.finishTransmit();
|
||||
|
||||
// Thread might've been interrupted in the meanwhile. Publish a terminal state so a
|
||||
// caller that queued this TX still gets a final callback for its id when
|
||||
// setEnabled(false) races with an in-flight transmit.
|
||||
if (isThreadInterrupted()) {
|
||||
publishTx(currentTx.id, LORA_TRANSMISSION_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the DIO1 bit is unset, this means the wait timed out
|
||||
if (txEventFlags & SX1262_DIO1_EVENT_BIT) {
|
||||
LOG_I(TAG, "TX id=%d: done", (int)currentTx.id);
|
||||
publishTx(currentTx.id, LORA_TRANSMISSION_STATE_TRANSMITTED);
|
||||
} else {
|
||||
LOG_W(TAG, "TX id=%d: no TX-done IRQ within %u ms", (int)currentTx.id, (unsigned)txTimeoutMillis);
|
||||
publishTx(currentTx.id, LORA_TRANSMISSION_STATE_TIMEOUT);
|
||||
}
|
||||
} else {
|
||||
LOG_E(TAG, "Error transmitting id=%d, rc=%hi", (int)currentTx.id, rc);
|
||||
publishTx(currentTx.id, LORA_TRANSMISSION_STATE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
bool Sx1262Radio::doListen() {
|
||||
auto& radio = parts->radio;
|
||||
|
||||
if (getModulation() != LORA_MODULATION_LR_FHSS) {
|
||||
int16_t rc = radio.startReceiveDutyCycleAuto(preambleLength, 0, SX1262_IRQ_FLAGS);
|
||||
if (rc == RADIOLIB_ERR_NONE) {
|
||||
uint32_t flags = 0;
|
||||
event_group_wait(
|
||||
events,
|
||||
SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT | SX1262_QUEUED_TX_BIT,
|
||||
false,
|
||||
true,
|
||||
&flags,
|
||||
portMAX_DELAY
|
||||
);
|
||||
return (flags & SX1262_DIO1_EVENT_BIT) != 0;
|
||||
} else {
|
||||
LOG_E(TAG, "Error setting dutycycle RX, RadioLib returned %hi", rc);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// LR-FHSS modem only supports TX
|
||||
event_group_wait(
|
||||
events,
|
||||
SX1262_INTERRUPT_BIT | SX1262_QUEUED_TX_BIT,
|
||||
false,
|
||||
true,
|
||||
nullptr,
|
||||
portMAX_DELAY
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Sx1262Radio::doReceive() {
|
||||
// LR-FHSS modem only supports TX
|
||||
if (getModulation() == LORA_MODULATION_LR_FHSS) return;
|
||||
|
||||
auto& radio = parts->radio;
|
||||
|
||||
uint16_t rxSize = radio.getPacketLength(true);
|
||||
std::vector<uint8_t> data(rxSize);
|
||||
int16_t rc = radio.readData(data.data(), rxSize);
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc);
|
||||
} else if (rxSize == 0) {
|
||||
// Empty read: skip silently to avoid log flooding on spurious IRQs.
|
||||
} else {
|
||||
const struct LoraRxPacket packet = {
|
||||
.data = data.data(),
|
||||
.length = data.size(),
|
||||
.rssi = radio.getRSSI(),
|
||||
.snr = radio.getSNR(),
|
||||
};
|
||||
|
||||
LOG_I(TAG, "RX: %u bytes, RSSI %.1f dBm, SNR %.1f dB", (unsigned)packet.length, packet.rssi, packet.snr);
|
||||
publishRx(packet);
|
||||
radio.finishReceive();
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
175
Drivers/sx126x-module/private/drivers/sx1262_radio.h
Normal file
175
Drivers/sx126x-module/private/drivers/sx1262_radio.h
Normal file
@ -0,0 +1,175 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/concurrent/recursive_mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/drivers/lora.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/event_groups.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
struct Device;
|
||||
struct GpioDescriptor;
|
||||
|
||||
/**
|
||||
* SX1262 radio engine: owns the radio thread, the TX queue and the callback lists.
|
||||
* The public methods are thread-safe. Callbacks are invoked on a snapshot of the list with
|
||||
* the internal mutex released, either from the radio thread (RX, TX progress, state) or from
|
||||
* the caller of transmit() (QUEUED).
|
||||
*
|
||||
* The RadioLib types live behind the RadioParts indirection: RadioLib declares a global
|
||||
* `class Module` that collides with the kernel's `struct Module` when both are visible
|
||||
* in the same translation unit, so RadioLib headers must not leak out of the implementation.
|
||||
*/
|
||||
class Sx1262Radio final {
|
||||
public:
|
||||
struct Settings {
|
||||
/** The kernel device, passed to callbacks */
|
||||
Device* device;
|
||||
/** The parent SPI controller device, for the SPI controller bus lock */
|
||||
Device* spi_controller;
|
||||
spi_host_device_t spi_host;
|
||||
int spi_frequency_hz;
|
||||
// CS/RESET/BUSY are native SoC GPIO numbers: the RadioLib HAL drives them directly
|
||||
// through ESP-IDF, so they can't sit behind an IO expander (unlike enable/antenna-select,
|
||||
// which the driver-registration layer resolves through the GPIO descriptor API).
|
||||
gpio_num_t pin_cs;
|
||||
gpio_num_t pin_reset;
|
||||
gpio_num_t pin_busy;
|
||||
/** DIO1 IRQ line, owned by the driver. The radio thread arms it as a
|
||||
* HIGH_LEVEL one-shot via the GPIO descriptor callback API. */
|
||||
struct GpioDescriptor* dio1;
|
||||
float tcxo_voltage;
|
||||
bool use_regulator_ldo;
|
||||
bool dio2_rf_switch;
|
||||
};
|
||||
|
||||
private:
|
||||
struct RadioParts;
|
||||
|
||||
struct TxItem {
|
||||
LoraTxId id = 0;
|
||||
std::vector<uint8_t> data;
|
||||
};
|
||||
|
||||
template<typename Callback>
|
||||
struct CallbackEntry {
|
||||
void* context;
|
||||
Callback callback;
|
||||
};
|
||||
|
||||
const Settings settings;
|
||||
RadioParts* parts;
|
||||
mutable RecursiveMutex mutex = {};
|
||||
EventGroupHandle_t events = nullptr;
|
||||
|
||||
Thread* thread = nullptr;
|
||||
bool threadInterrupted = false;
|
||||
|
||||
enum LoraRadioState state = LORA_RADIO_STATE_OFF;
|
||||
enum LoraModulation modulation = LORA_MODULATION_NONE;
|
||||
|
||||
std::deque<TxItem> txQueue;
|
||||
TxItem currentTx;
|
||||
LoraTxId lastTxId = 0;
|
||||
|
||||
std::vector<CallbackEntry<LoraStateCallback>> stateCallbacks;
|
||||
std::vector<CallbackEntry<LoraRxCallback>> rxCallbacks;
|
||||
std::vector<CallbackEntry<LoraTxCallback>> txCallbacks;
|
||||
|
||||
// Parameter store, applied on the next doBegin(). Frequencies/rates are held in base SI
|
||||
// units (Hz, bit/s) and converted to RadioLib's MHz/kHz/kbps floats in doBegin().
|
||||
int8_t power = -9;
|
||||
int32_t frequency = 150000000; // Hz
|
||||
int32_t bandwidth = 0; // Hz
|
||||
uint8_t spreadingFactor = 0;
|
||||
uint8_t codingRate = 0;
|
||||
uint8_t syncWord = 0;
|
||||
uint16_t preambleLength = 0;
|
||||
int32_t bitRate = 0; // bit/s
|
||||
int32_t frequencyDeviation = 0; // Hz
|
||||
bool narrowGrid = false;
|
||||
bool boostedGain = false;
|
||||
// PA over-current protection limit in mA. Default matches RadioLib's fail-safe 60 mA,
|
||||
// which caps output below +22 dBm; a board-aware consumer can raise it (up to 140 mA).
|
||||
int32_t currentLimit = 60; // mA
|
||||
|
||||
static void dio1Isr(void* context);
|
||||
static int32_t threadMainStatic(void* context);
|
||||
|
||||
void lock() const { recursive_mutex_lock(&mutex); }
|
||||
void unlock() const { recursive_mutex_unlock(&mutex); }
|
||||
|
||||
bool isThreadInterrupted() const;
|
||||
int32_t threadMain();
|
||||
|
||||
void setState(enum LoraRadioState newState);
|
||||
void publishRx(const struct LoraRxPacket& packet);
|
||||
void publishTx(LoraTxId id, enum LoraTransmissionState txState);
|
||||
|
||||
size_t getTxQueueSize() const;
|
||||
TxItem popNextQueuedTx();
|
||||
|
||||
void registerDio1Isr();
|
||||
void unregisterDio1Isr();
|
||||
|
||||
error_t setBaseParameter(enum LoraParameter parameter, int32_t value);
|
||||
error_t setLoraParameter(enum LoraParameter parameter, int32_t value);
|
||||
error_t setFskParameter(enum LoraParameter parameter, int32_t value);
|
||||
error_t setLrFhssParameter(enum LoraParameter parameter, int32_t value);
|
||||
error_t getBaseParameter(enum LoraParameter parameter, int32_t* value) const;
|
||||
error_t getLoraParameter(enum LoraParameter parameter, int32_t* value) const;
|
||||
error_t getFskParameter(enum LoraParameter parameter, int32_t* value) const;
|
||||
error_t getLrFhssParameter(enum LoraParameter parameter, int32_t* value) const;
|
||||
|
||||
int doBegin(enum LoraModulation beginModulation);
|
||||
void doEnd();
|
||||
void doTransmit();
|
||||
bool doListen();
|
||||
void doReceive();
|
||||
|
||||
public:
|
||||
explicit Sx1262Radio(const Settings& settings);
|
||||
~Sx1262Radio();
|
||||
|
||||
/**
|
||||
* Verify a live SX1262 responds on the wired pins, using only GPIO (no SPI traffic):
|
||||
* pulse NRESET and expect the chip to drive BUSY low once it reaches standby.
|
||||
* @return ERROR_NONE when the chip responded
|
||||
*/
|
||||
error_t probe() const;
|
||||
|
||||
enum LoraRadioState getState() const;
|
||||
error_t setEnabled(bool enabled);
|
||||
error_t setModulation(enum LoraModulation newModulation);
|
||||
enum LoraModulation getModulation() const;
|
||||
|
||||
bool canTransmit(enum LoraModulation withModulation) const {
|
||||
return (withModulation == LORA_MODULATION_FSK) ||
|
||||
(withModulation == LORA_MODULATION_LORA) ||
|
||||
(withModulation == LORA_MODULATION_LR_FHSS);
|
||||
}
|
||||
|
||||
bool canReceive(enum LoraModulation withModulation) const {
|
||||
return (withModulation == LORA_MODULATION_FSK) || (withModulation == LORA_MODULATION_LORA);
|
||||
}
|
||||
|
||||
error_t setParameter(enum LoraParameter parameter, int32_t value);
|
||||
error_t getParameter(enum LoraParameter parameter, int32_t* value) const;
|
||||
|
||||
error_t transmit(const uint8_t* data, size_t length, LoraTxId* id);
|
||||
|
||||
error_t addRxCallback(void* context, LoraRxCallback callback);
|
||||
error_t removeRxCallback(LoraRxCallback callback);
|
||||
error_t addStateCallback(void* context, LoraStateCallback callback);
|
||||
error_t removeStateCallback(LoraStateCallback callback);
|
||||
error_t addTxCallback(void* context, LoraTxCallback callback);
|
||||
error_t removeTxCallback(LoraTxCallback callback);
|
||||
};
|
||||
141
Drivers/sx126x-module/private/drivers/sx126x_radiolib_hal.cpp
Normal file
141
Drivers/sx126x-module/private/drivers/sx126x_radiolib_hal.cpp
Normal file
@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "sx126x_radiolib_hal.h"
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <esp_rom_gpio.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
#define TAG "sx126x_hal"
|
||||
|
||||
void Sx126xRadiolibHal::init() {
|
||||
spiBegin();
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::term() {
|
||||
spiEnd();
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::pinMode(uint32_t pin, uint32_t mode) {
|
||||
if (pin == RADIOLIB_NC) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Not gpio_config(): that rewrites the pin's interrupt type along with everything
|
||||
// else, and DIO1's HIGH_LEVEL interrupt is owned by the kernel GPIO descriptor API
|
||||
// while RadioLib still calls pinMode() on that pin during begin(). Configure the pad
|
||||
// routing, direction and pulls through the per-aspect setters instead, which leave
|
||||
// the interrupt configuration untouched.
|
||||
esp_rom_gpio_pad_select_gpio(pin);
|
||||
gpio_set_direction((gpio_num_t)pin, (gpio_mode_t)mode);
|
||||
gpio_set_pull_mode((gpio_num_t)pin, GPIO_FLOATING);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::digitalWrite(uint32_t pin, uint32_t value) {
|
||||
if (pin == RADIOLIB_NC) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_set_level((gpio_num_t)pin, value);
|
||||
}
|
||||
|
||||
uint32_t Sx126xRadiolibHal::digitalRead(uint32_t pin) {
|
||||
if (pin == RADIOLIB_NC) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return gpio_get_level((gpio_num_t)pin);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) {
|
||||
LOG_E(TAG, "Interrupt registration via RadioLib is not supported");
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::detachInterrupt(uint32_t interruptNum) {
|
||||
LOG_E(TAG, "Interrupt registration via RadioLib is not supported");
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::delay(unsigned long ms) {
|
||||
delay_millis(ms);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::delayMicroseconds(unsigned long us) {
|
||||
delay_micros(us);
|
||||
}
|
||||
|
||||
unsigned long Sx126xRadiolibHal::millis() {
|
||||
return (unsigned long)(esp_timer_get_time() / 1000ULL);
|
||||
}
|
||||
|
||||
unsigned long Sx126xRadiolibHal::micros() {
|
||||
return (unsigned long)(esp_timer_get_time());
|
||||
}
|
||||
|
||||
long Sx126xRadiolibHal::pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) {
|
||||
if (pin == RADIOLIB_NC) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this->pinMode(pin, GPIO_MODE_INPUT);
|
||||
uint32_t start = this->micros();
|
||||
uint32_t curtick = this->micros();
|
||||
|
||||
while (this->digitalRead(pin) == state) {
|
||||
if ((this->micros() - curtick) > timeout) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (this->micros() - start);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::spiBegin() {
|
||||
if (!spiInitialized) {
|
||||
spi_device_interface_config_t devcfg = {};
|
||||
devcfg.clock_speed_hz = spiFrequency;
|
||||
devcfg.mode = 0;
|
||||
// CS is set to unused, as RadioLib sets it manually
|
||||
devcfg.spics_io_num = -1;
|
||||
devcfg.queue_size = 1;
|
||||
esp_err_t ret = spi_bus_add_device(spiHostDevice, &devcfg, &spiDeviceHandle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to add SPI device, error %s", esp_err_to_name(ret));
|
||||
}
|
||||
spiInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::spiBeginTransaction() {
|
||||
// RadioLib holds CS low across multiple transfers, so the whole exchange must
|
||||
// be atomic on the bus. Take the kernel SPI controller lock (the arbiter other
|
||||
// kernel drivers on this host cooperate through) as the outer lock, then
|
||||
// ESP-IDF's per-host bus lock to also block the IDF-managed spi_master devices
|
||||
// (display, SD) that don't take the controller lock.
|
||||
spi_controller_lock(spiController);
|
||||
spi_device_acquire_bus(spiDeviceHandle, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::spiTransfer(uint8_t* out, size_t len, uint8_t* in) {
|
||||
spi_transaction_t t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.length = len * 8;
|
||||
t.tx_buffer = out;
|
||||
t.rx_buffer = in;
|
||||
spi_device_polling_transmit(spiDeviceHandle, &t);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::spiEndTransaction() {
|
||||
spi_device_release_bus(spiDeviceHandle);
|
||||
spi_controller_unlock(spiController);
|
||||
}
|
||||
|
||||
void Sx126xRadiolibHal::spiEnd() {
|
||||
if (spiInitialized) {
|
||||
spi_bus_remove_device(spiDeviceHandle);
|
||||
spiInitialized = false;
|
||||
}
|
||||
}
|
||||
65
Drivers/sx126x-module/private/drivers/sx126x_radiolib_hal.h
Normal file
65
Drivers/sx126x-module/private/drivers/sx126x_radiolib_hal.h
Normal file
@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <RadioLib.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
/**
|
||||
* RadioLib HAL on top of ESP-IDF GPIO and SPI master.
|
||||
*
|
||||
* RadioLib drives the chip-select manually across multiple transfers, so every
|
||||
* command/response exchange must be atomic on the bus. Two locks wrap each
|
||||
* exchange: the kernel SPI controller lock (spi_controller_lock) is the
|
||||
* abstraction other kernel drivers on this host serialise through, and ESP-IDF's
|
||||
* per-host bus lock (spi_device_acquire_bus) additionally blocks the IDF-managed
|
||||
* spi_master devices (display, SD) that don't take the controller lock. The
|
||||
* controller lock is taken as the outer lock; nothing else takes both, so there
|
||||
* is no lock-ordering hazard.
|
||||
*/
|
||||
class Sx126xRadiolibHal final : public RadioLibHal {
|
||||
private:
|
||||
spi_host_device_t spiHostDevice;
|
||||
int spiFrequency;
|
||||
struct Device* spiController;
|
||||
spi_device_handle_t spiDeviceHandle = nullptr;
|
||||
bool spiInitialized = false;
|
||||
|
||||
public:
|
||||
Sx126xRadiolibHal(spi_host_device_t spiHostDevice, int spiFrequency, struct Device* spiController)
|
||||
: RadioLibHal(
|
||||
GPIO_MODE_INPUT,
|
||||
GPIO_MODE_OUTPUT,
|
||||
0, // LOW
|
||||
1, // HIGH
|
||||
GPIO_INTR_POSEDGE,
|
||||
GPIO_INTR_NEGEDGE
|
||||
)
|
||||
, spiHostDevice(spiHostDevice)
|
||||
, spiFrequency(spiFrequency)
|
||||
, spiController(spiController) {}
|
||||
|
||||
void init() override;
|
||||
void term() override;
|
||||
|
||||
void pinMode(uint32_t pin, uint32_t mode) override;
|
||||
void digitalWrite(uint32_t pin, uint32_t value) override;
|
||||
uint32_t digitalRead(uint32_t pin) override;
|
||||
void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override;
|
||||
void detachInterrupt(uint32_t interruptNum) override;
|
||||
|
||||
void delay(unsigned long ms) override;
|
||||
void delayMicroseconds(unsigned long us) override;
|
||||
unsigned long millis() override;
|
||||
unsigned long micros() override;
|
||||
long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override;
|
||||
|
||||
void spiBegin() override;
|
||||
void spiBeginTransaction() override;
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override;
|
||||
void spiEndTransaction() override;
|
||||
void spiEnd() override;
|
||||
};
|
||||
19
Drivers/sx126x-module/source/module.cpp
Normal file
19
Drivers/sx126x-module/source/module.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver sx1262_driver;
|
||||
|
||||
static Driver* const sx126x_drivers[] = {
|
||||
&sx1262_driver,
|
||||
nullptr
|
||||
};
|
||||
|
||||
Module sx126x_module = {
|
||||
.name = "sx126x",
|
||||
.drivers = sx126x_drivers
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
384
Drivers/sx126x-module/source/sx1262.cpp
Normal file
384
Drivers/sx126x-module/source/sx1262.cpp
Normal file
@ -0,0 +1,384 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <new>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/esp32_spi.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/lora.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
#include <drivers/sx1262.h>
|
||||
#include <drivers/sx1262_radio.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define TAG "sx1262"
|
||||
|
||||
#define GET_CONFIG(device) ((const struct Sx1262Config*)device->config)
|
||||
#define GET_DATA(device) ((struct Sx1262Internal*)device_get_driver_data(device))
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Sx1262Internal {
|
||||
Sx1262Radio* radio = nullptr;
|
||||
GpioDescriptor* pin_reset = nullptr;
|
||||
GpioDescriptor* pin_busy = nullptr;
|
||||
GpioDescriptor* pin_dio1 = nullptr;
|
||||
GpioDescriptor* pin_enable = nullptr;
|
||||
GpioDescriptor* pin_antenna_select = nullptr;
|
||||
|
||||
void release_pins() {
|
||||
release_pin(&pin_reset);
|
||||
release_pin(&pin_busy);
|
||||
release_pin(&pin_dio1);
|
||||
release_pin(&pin_enable);
|
||||
release_pin(&pin_antenna_select);
|
||||
}
|
||||
|
||||
private:
|
||||
static void release_pin(GpioDescriptor** descriptor) {
|
||||
if (*descriptor != nullptr) {
|
||||
gpio_descriptor_release(*descriptor);
|
||||
*descriptor = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Acquire a pin that must live on an SoC GPIO controller and resolve its native pin number.
|
||||
* The reset/busy/DIO1 lines are driven directly through ESP-IDF by the RadioLib HAL,
|
||||
* so pins behind an IO expander can't back them. The caller passes the pin's direction,
|
||||
* because the devicetree pin specs carry no direction of their own.
|
||||
*/
|
||||
static GpioDescriptor* acquire_native_pin(const struct GpioPinSpec& spec, const char* pin_name, gpio_flags_t flags, gpio_num_t* native_pin) {
|
||||
if (spec.gpio_controller == nullptr) {
|
||||
LOG_E(TAG, "Pin \"%s\" is not set", pin_name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* descriptor = gpio_descriptor_acquire(spec.gpio_controller, spec.pin, flags, GPIO_OWNER_GPIO);
|
||||
if (descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire pin \"%s\"", pin_name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (gpio_descriptor_get_native_pin_number(descriptor, native_pin) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Pin \"%s\" must be an SoC GPIO", pin_name);
|
||||
gpio_descriptor_release(descriptor);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an optional control pin (may sit behind an IO expander) and drive it to the given logical state.
|
||||
* Controllers may not support ACTIVE_LOW on outputs (e.g. xl9555), so polarity from the pin spec is applied here.
|
||||
*/
|
||||
static error_t acquire_and_drive_pin(const struct GpioPinSpec& spec, const char* pin_name, bool active, GpioDescriptor** out_descriptor) {
|
||||
if (spec.gpio_controller == nullptr) {
|
||||
*out_descriptor = nullptr;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// Polarity is applied by hand below, so the descriptor is acquired as a plain output.
|
||||
auto* descriptor = gpio_descriptor_acquire(spec.gpio_controller, spec.pin, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
|
||||
if (descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire pin \"%s\"", pin_name);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
const bool level = (spec.flags & GPIO_FLAG_ACTIVE_LOW) ? !active : active;
|
||||
if (gpio_descriptor_set_level(descriptor, level) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to drive pin \"%s\"", pin_name);
|
||||
gpio_descriptor_release(descriptor);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Pin \"%s\" (controller \"%s\" pin %u) driven %s", pin_name, spec.gpio_controller->name, spec.pin, level ? "high" : "low");
|
||||
*out_descriptor = descriptor;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static void set_pin_active(GpioDescriptor* descriptor, const struct GpioPinSpec& spec, bool active) {
|
||||
if (descriptor != nullptr) {
|
||||
const bool level = (spec.flags & GPIO_FLAG_ACTIVE_LOW) ? !active : active;
|
||||
gpio_descriptor_set_level(descriptor, level);
|
||||
}
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
LOG_I(TAG, "start %s", device->name);
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
if (device_get_type(parent) != &SPI_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent device is not an SPI controller");
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
auto* config = GET_CONFIG(device);
|
||||
|
||||
// The chip-select lookup uses the node's unit address; reg exists to satisfy the
|
||||
// devicetree convention for addressed nodes, so reject a board definition where
|
||||
// the two disagree instead of silently using one of them.
|
||||
if (config->reg != device->address) {
|
||||
LOG_E(TAG, "reg (%d) does not match the node's unit address (%d)", (int)config->reg, (int)device->address);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// DIO1's interrupt is set up later through the GPIO descriptor callback API
|
||||
// (see Sx1262Radio::registerDio1Isr), which installs the shared ISR service
|
||||
// on demand and reference-counts it against the other GPIO-interrupt users.
|
||||
|
||||
auto* data = new (std::nothrow) Sx1262Internal();
|
||||
if (data == nullptr) return ERROR_OUT_OF_MEMORY;
|
||||
|
||||
gpio_num_t pin_reset = GPIO_NUM_NC;
|
||||
gpio_num_t pin_busy = GPIO_NUM_NC;
|
||||
gpio_num_t pin_dio1 = GPIO_NUM_NC;
|
||||
|
||||
// DIO1 keeps a plain input here; registerDio1Isr() adds the interrupt flags when it
|
||||
// attaches the handler, so the line stays quiet until the radio thread is ready for it.
|
||||
data->pin_reset = acquire_native_pin(config->pin_reset, "pin-reset", GPIO_FLAG_DIRECTION_OUTPUT, &pin_reset);
|
||||
data->pin_busy = acquire_native_pin(config->pin_busy, "pin-busy", GPIO_FLAG_DIRECTION_INPUT, &pin_busy);
|
||||
data->pin_dio1 = acquire_native_pin(config->pin_dio1, "pin-dio1", GPIO_FLAG_DIRECTION_INPUT, &pin_dio1);
|
||||
|
||||
if (data->pin_reset == nullptr || data->pin_busy == nullptr || data->pin_dio1 == nullptr) {
|
||||
data->release_pins();
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Select the antenna path first, then power the module: the RF switch should
|
||||
// never be indeterminate while the radio is powered.
|
||||
if (acquire_and_drive_pin(config->pin_antenna_select, "pin-antenna-select", true, &data->pin_antenna_select) != ERROR_NONE ||
|
||||
acquire_and_drive_pin(config->pin_enable, "pin-enable", true, &data->pin_enable) != ERROR_NONE) {
|
||||
data->release_pins();
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Give the supply rail time to settle before poking the chip
|
||||
if (data->pin_enable != nullptr) {
|
||||
delay_millis(10);
|
||||
}
|
||||
|
||||
struct GpioPinSpec cs_pin_spec;
|
||||
if (esp32_spi_get_cs_pin(device, &cs_pin_spec) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to get CS pin from parent SPI controller");
|
||||
data->release_pins();
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
|
||||
|
||||
const Sx1262Radio::Settings settings = {
|
||||
.device = device,
|
||||
.spi_controller = parent,
|
||||
.spi_host = spi_config->host,
|
||||
.spi_frequency_hz = (int)config->spi_frequency_khz * 1000,
|
||||
.pin_cs = static_cast<gpio_num_t>(cs_pin_spec.pin),
|
||||
.pin_reset = pin_reset,
|
||||
.pin_busy = pin_busy,
|
||||
.dio1 = data->pin_dio1,
|
||||
.tcxo_voltage = (float)config->tcxo_millivolts / 1000.0f,
|
||||
.use_regulator_ldo = config->use_regulator_ldo,
|
||||
.dio2_rf_switch = config->dio2_as_rf_switch,
|
||||
};
|
||||
|
||||
LOG_I(
|
||||
TAG,
|
||||
"%s: CS=%d RST=%d BUSY=%d DIO1=%d, SPI %u kHz, TCXO %u mV, DIO2-RF-switch=%s, LDO=%s",
|
||||
device->name,
|
||||
(int)settings.pin_cs,
|
||||
(int)pin_reset,
|
||||
(int)pin_busy,
|
||||
(int)pin_dio1,
|
||||
(unsigned)config->spi_frequency_khz,
|
||||
(unsigned)config->tcxo_millivolts,
|
||||
config->dio2_as_rf_switch ? "yes" : "no",
|
||||
config->use_regulator_ldo ? "yes" : "no"
|
||||
);
|
||||
|
||||
data->radio = new (std::nothrow) Sx1262Radio(settings);
|
||||
if (data->radio == nullptr) {
|
||||
data->release_pins();
|
||||
delete data;
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// Cheap GPIO-only sanity check that a live chip answers on these pins,
|
||||
// so a miswired or unpowered module fails at device start instead of
|
||||
// surfacing later as an opaque RadioLib error on the radio thread.
|
||||
if (data->radio->probe() != ERROR_NONE) {
|
||||
delete data->radio;
|
||||
// Power down first, then release the antenna path (mirror of the start order)
|
||||
set_pin_active(data->pin_enable, config->pin_enable, false);
|
||||
set_pin_active(data->pin_antenna_select, config->pin_antenna_select, false);
|
||||
data->release_pins();
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
LOG_I(TAG, "stop %s", device->name);
|
||||
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) return ERROR_NONE;
|
||||
|
||||
// Stops the radio thread before teardown
|
||||
delete data->radio;
|
||||
data->radio = nullptr;
|
||||
|
||||
// Power down first, then release the antenna path (mirror of the start order)
|
||||
auto* config = GET_CONFIG(device);
|
||||
set_pin_active(data->pin_enable, config->pin_enable, false);
|
||||
set_pin_active(data->pin_antenna_select, config->pin_antenna_select, false);
|
||||
|
||||
data->release_pins();
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// region LoraApi
|
||||
|
||||
static Sx1262Radio* get_radio(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
return (data != nullptr) ? data->radio : nullptr;
|
||||
}
|
||||
|
||||
static error_t api_get_radio_state(Device* device, enum LoraRadioState* state) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
*state = radio->getState();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t api_set_enabled(Device* device, bool enabled) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->setEnabled(enabled);
|
||||
}
|
||||
|
||||
static error_t api_set_modulation(Device* device, enum LoraModulation modulation) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->setModulation(modulation);
|
||||
}
|
||||
|
||||
static error_t api_get_modulation(Device* device, enum LoraModulation* modulation) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
*modulation = radio->getModulation();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool api_can_transmit(Device* device, enum LoraModulation modulation) {
|
||||
auto* radio = get_radio(device);
|
||||
return (radio != nullptr) && radio->canTransmit(modulation);
|
||||
}
|
||||
|
||||
static bool api_can_receive(Device* device, enum LoraModulation modulation) {
|
||||
auto* radio = get_radio(device);
|
||||
return (radio != nullptr) && radio->canReceive(modulation);
|
||||
}
|
||||
|
||||
static error_t api_set_parameter(Device* device, enum LoraParameter parameter, int32_t value) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->setParameter(parameter, value);
|
||||
}
|
||||
|
||||
static error_t api_get_parameter(Device* device, enum LoraParameter parameter, int32_t* value) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->getParameter(parameter, value);
|
||||
}
|
||||
|
||||
static error_t api_transmit(Device* device, const uint8_t* data, size_t length, LoraTxId* id) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->transmit(data, length, id);
|
||||
}
|
||||
|
||||
static error_t api_add_rx_callback(Device* device, void* callback_context, LoraRxCallback callback) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->addRxCallback(callback_context, callback);
|
||||
}
|
||||
|
||||
static error_t api_remove_rx_callback(Device* device, LoraRxCallback callback) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->removeRxCallback(callback);
|
||||
}
|
||||
|
||||
static error_t api_add_state_callback(Device* device, void* callback_context, LoraStateCallback callback) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->addStateCallback(callback_context, callback);
|
||||
}
|
||||
|
||||
static error_t api_remove_state_callback(Device* device, LoraStateCallback callback) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->removeStateCallback(callback);
|
||||
}
|
||||
|
||||
static error_t api_add_tx_callback(Device* device, void* callback_context, LoraTxCallback callback) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->addTxCallback(callback_context, callback);
|
||||
}
|
||||
|
||||
static error_t api_remove_tx_callback(Device* device, LoraTxCallback callback) {
|
||||
auto* radio = get_radio(device);
|
||||
if (radio == nullptr) return ERROR_INVALID_STATE;
|
||||
return radio->removeTxCallback(callback);
|
||||
}
|
||||
|
||||
static const struct LoraApi sx1262_lora_api = {
|
||||
.get_radio_state = api_get_radio_state,
|
||||
.set_enabled = api_set_enabled,
|
||||
.set_modulation = api_set_modulation,
|
||||
.get_modulation = api_get_modulation,
|
||||
.can_transmit = api_can_transmit,
|
||||
.can_receive = api_can_receive,
|
||||
.set_parameter = api_set_parameter,
|
||||
.get_parameter = api_get_parameter,
|
||||
.transmit = api_transmit,
|
||||
.add_rx_callback = api_add_rx_callback,
|
||||
.remove_rx_callback = api_remove_rx_callback,
|
||||
.add_state_callback = api_add_state_callback,
|
||||
.remove_state_callback = api_remove_state_callback,
|
||||
.add_tx_callback = api_add_tx_callback,
|
||||
.remove_tx_callback = api_remove_tx_callback,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
extern Module sx126x_module;
|
||||
|
||||
Driver sx1262_driver = {
|
||||
.name = "sx1262",
|
||||
.compatible = (const char*[]) { "semtech,sx1262", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &sx1262_lora_api,
|
||||
.device_type = &LORA_TYPE,
|
||||
.owner = &sx126x_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@ -84,5 +84,9 @@ dependencies:
|
||||
version: "1.1.4"
|
||||
rules:
|
||||
- if: "target in [esp32s3, esp32p4]"
|
||||
jgromes/radiolib:
|
||||
version: "7.3.0"
|
||||
rules:
|
||||
- if: "target in [esp32, esp32s3, esp32p4]"
|
||||
idf: '5.5.2'
|
||||
|
||||
|
||||
277
TactilityKernel/include/tactility/drivers/lora.h
Normal file
277
TactilityKernel/include/tactility/drivers/lora.h
Normal file
@ -0,0 +1,277 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Device;
|
||||
|
||||
/**
|
||||
* Device type and API for sub-GHz packet transceivers such as the Semtech SX126x family.
|
||||
* The API is LoRa-centric but exposes the modem's other modulation schemes as well.
|
||||
*/
|
||||
|
||||
enum LoraModulation {
|
||||
LORA_MODULATION_NONE = 0,
|
||||
LORA_MODULATION_FSK,
|
||||
LORA_MODULATION_LORA,
|
||||
LORA_MODULATION_LR_FHSS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Tunable radio parameters. Values are int32_t; the unit is documented per parameter.
|
||||
* Frequencies and rates use their base SI unit (Hz, bit/s) on purpose: a float can't hold
|
||||
* a value like 906875000 Hz (906.875 MHz) exactly, so integers avoid the rounding a
|
||||
* fractional MHz/kHz representation would introduce.
|
||||
*
|
||||
* Which parameters are available depends on the driver and the selected modulation.
|
||||
*/
|
||||
enum LoraParameter {
|
||||
/** TX output power in dBm */
|
||||
LORA_PARAMETER_POWER = 0,
|
||||
/** Boosted RX gain mode: 0 = off, 1 = on */
|
||||
LORA_PARAMETER_BOOSTED_GAIN,
|
||||
/** Carrier frequency in Hz */
|
||||
LORA_PARAMETER_FREQUENCY,
|
||||
/** Bandwidth in Hz */
|
||||
LORA_PARAMETER_BANDWIDTH,
|
||||
/** LoRa spreading factor (7-12) */
|
||||
LORA_PARAMETER_SPREADING_FACTOR,
|
||||
/** LoRa coding rate denominator (5-8 for 4/5 to 4/8) */
|
||||
LORA_PARAMETER_CODING_RATE,
|
||||
/**
|
||||
* LoRa sync word (0x00-0xFF). Distinguishes otherwise-identical networks: a receiver
|
||||
* only accepts packets whose sync word matches. Conventionally 0x12 for private
|
||||
* networks and 0x34 for public/LoRaWAN. LoRa modulation only.
|
||||
*/
|
||||
LORA_PARAMETER_SYNC_WORD,
|
||||
/** Preamble length in symbols (LoRa) or bits (FSK) */
|
||||
LORA_PARAMETER_PREAMBLE_LENGTH,
|
||||
/** FSK frequency deviation from the carrier in Hz */
|
||||
LORA_PARAMETER_FREQUENCY_DEVIATION,
|
||||
/** FSK bit rate in bits per second */
|
||||
LORA_PARAMETER_DATA_RATE,
|
||||
/** LR-FHSS grid spacing: 0 = 25 kHz (wide), 1 = 3.9 kHz (narrow) */
|
||||
LORA_PARAMETER_NARROW_GRID,
|
||||
/**
|
||||
* PA over-current protection limit in mA. Fail-safe: a low limit caps the current the
|
||||
* PA can push into a bad or disconnected antenna, but also caps the achievable output
|
||||
* power. Drivers keep a conservative default; a board-aware consumer that knows its
|
||||
* antenna and PA can raise it to reach full output power.
|
||||
*/
|
||||
LORA_PARAMETER_CURRENT_LIMIT,
|
||||
};
|
||||
|
||||
enum LoraRadioState {
|
||||
LORA_RADIO_STATE_OFF,
|
||||
LORA_RADIO_STATE_ON_PENDING,
|
||||
LORA_RADIO_STATE_ON,
|
||||
LORA_RADIO_STATE_OFF_PENDING,
|
||||
LORA_RADIO_STATE_ERROR,
|
||||
};
|
||||
|
||||
/** Identifies a queued transmission. Unique per device until it wraps around. */
|
||||
typedef int32_t LoraTxId;
|
||||
|
||||
enum LoraTransmissionState {
|
||||
/** Accepted into the TX queue */
|
||||
LORA_TRANSMISSION_STATE_QUEUED,
|
||||
/** Handed to the modem, waiting for TX-done */
|
||||
LORA_TRANSMISSION_STATE_TRANSMIT_PENDING,
|
||||
/** TX-done confirmed by the modem */
|
||||
LORA_TRANSMISSION_STATE_TRANSMITTED,
|
||||
/** No TX-done within the driver's timeout */
|
||||
LORA_TRANSMISSION_STATE_TIMEOUT,
|
||||
/** The modem rejected the transmission */
|
||||
LORA_TRANSMISSION_STATE_ERROR,
|
||||
};
|
||||
|
||||
struct LoraRxPacket {
|
||||
/** Packet payload. Only valid for the duration of the RX callback. */
|
||||
const uint8_t* data;
|
||||
size_t length;
|
||||
/** Received signal strength in dBm */
|
||||
float rssi;
|
||||
/** Signal-to-noise ratio in dB */
|
||||
float snr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callbacks are invoked without any driver lock held, either from the driver's radio
|
||||
* thread (RX, TX progress, state) or from the thread calling transmit()/set_enabled()
|
||||
* (the QUEUED TX event and enable/disable state changes). Taking consumer locks in a
|
||||
* callback is therefore safe, but keep callbacks short: RX processing stalls while
|
||||
* they run. After remove_*_callback returns, a callback that was already in flight
|
||||
* may still complete once — disable the radio before destroying callback context.
|
||||
*/
|
||||
typedef void (*LoraStateCallback)(struct Device* device, void* context, enum LoraRadioState state);
|
||||
typedef void (*LoraRxCallback)(struct Device* device, void* context, const struct LoraRxPacket* packet);
|
||||
typedef void (*LoraTxCallback)(struct Device* device, void* context, LoraTxId id, enum LoraTransmissionState state);
|
||||
|
||||
struct LoraApi {
|
||||
/**
|
||||
* Get the radio state of the device.
|
||||
* @param[in] device the lora device
|
||||
* @param[out] state the radio state
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*get_radio_state)(struct Device* device, enum LoraRadioState* state);
|
||||
|
||||
/**
|
||||
* Turn the radio on or off. Requires a modulation to be set before enabling.
|
||||
* Turning on is asynchronous: observe the radio state to know when it's up.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] enabled true to turn the radio on
|
||||
* @return ERROR_NONE on success
|
||||
* @retval ERROR_INVALID_STATE when enabling without a modulation set
|
||||
*/
|
||||
error_t (*set_enabled)(struct Device* device, bool enabled);
|
||||
|
||||
/**
|
||||
* Set the modulation scheme. Only allowed while the radio is off.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] modulation the modulation scheme
|
||||
* @return ERROR_NONE on success
|
||||
* @retval ERROR_INVALID_STATE when the radio is on or turning on
|
||||
* @retval ERROR_NOT_SUPPORTED when the device supports neither TX nor RX for this modulation
|
||||
*/
|
||||
error_t (*set_modulation)(struct Device* device, enum LoraModulation modulation);
|
||||
|
||||
/**
|
||||
* Get the current modulation scheme.
|
||||
* @param[in] device the lora device
|
||||
* @param[out] modulation the modulation scheme
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*get_modulation)(struct Device* device, enum LoraModulation* modulation);
|
||||
|
||||
/**
|
||||
* @param[in] device the lora device
|
||||
* @param[in] modulation the modulation scheme
|
||||
* @return true when the device can transmit with the given modulation
|
||||
*/
|
||||
bool (*can_transmit)(struct Device* device, enum LoraModulation modulation);
|
||||
|
||||
/**
|
||||
* @param[in] device the lora device
|
||||
* @param[in] modulation the modulation scheme
|
||||
* @return true when the device can receive with the given modulation
|
||||
*/
|
||||
bool (*can_receive)(struct Device* device, enum LoraModulation modulation);
|
||||
|
||||
/**
|
||||
* Set a radio parameter. See LoraParameter for units.
|
||||
* Parameters apply to the current modulation and take effect the next time the radio turns on.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] parameter the parameter to set
|
||||
* @param[in] value the value to set
|
||||
* @return ERROR_NONE on success
|
||||
* @retval ERROR_NOT_SUPPORTED when the parameter doesn't apply to the device or modulation
|
||||
* @retval ERROR_OUT_OF_RANGE when the value is invalid for the parameter
|
||||
*/
|
||||
error_t (*set_parameter)(struct Device* device, enum LoraParameter parameter, int32_t value);
|
||||
|
||||
/**
|
||||
* Get a radio parameter. See LoraParameter for units.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] parameter the parameter to get
|
||||
* @param[out] value the current value
|
||||
* @return ERROR_NONE on success
|
||||
* @retval ERROR_NOT_SUPPORTED when the parameter doesn't apply to the device or modulation
|
||||
*/
|
||||
error_t (*get_parameter)(struct Device* device, enum LoraParameter parameter, int32_t* value);
|
||||
|
||||
/**
|
||||
* Queue a packet for transmission. The data is copied.
|
||||
* Progress is reported through the TX callbacks, starting with QUEUED.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] data the packet payload
|
||||
* @param[in] length the payload length in bytes
|
||||
* @param[out] id the id assigned to this transmission (optional, can be NULL)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*transmit)(struct Device* device, const uint8_t* data, size_t length, LoraTxId* id);
|
||||
|
||||
/**
|
||||
* Add a callback for received packets.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] callback_context the context to pass to the callback
|
||||
* @param[in] callback the callback function
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*add_rx_callback)(struct Device* device, void* callback_context, LoraRxCallback callback);
|
||||
|
||||
/**
|
||||
* Remove a callback for received packets.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] callback the callback function
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*remove_rx_callback)(struct Device* device, LoraRxCallback callback);
|
||||
|
||||
/**
|
||||
* Add a callback for radio state changes.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] callback_context the context to pass to the callback
|
||||
* @param[in] callback the callback function
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*add_state_callback)(struct Device* device, void* callback_context, LoraStateCallback callback);
|
||||
|
||||
/**
|
||||
* Remove a callback for radio state changes.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] callback the callback function
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*remove_state_callback)(struct Device* device, LoraStateCallback callback);
|
||||
|
||||
/**
|
||||
* Add a callback for transmission progress.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] callback_context the context to pass to the callback
|
||||
* @param[in] callback the callback function
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*add_tx_callback)(struct Device* device, void* callback_context, LoraTxCallback callback);
|
||||
|
||||
/**
|
||||
* Remove a callback for transmission progress.
|
||||
* @param[in] device the lora device
|
||||
* @param[in] callback the callback function
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*remove_tx_callback)(struct Device* device, LoraTxCallback callback);
|
||||
};
|
||||
|
||||
extern const struct DeviceType LORA_TYPE;
|
||||
|
||||
/** @return the first registered lora device, regardless of started state, or NULL if none exists */
|
||||
struct Device* lora_find_first_registered_device(void);
|
||||
|
||||
error_t lora_get_radio_state(struct Device* device, enum LoraRadioState* state);
|
||||
error_t lora_set_enabled(struct Device* device, bool enabled);
|
||||
error_t lora_set_modulation(struct Device* device, enum LoraModulation modulation);
|
||||
error_t lora_get_modulation(struct Device* device, enum LoraModulation* modulation);
|
||||
bool lora_can_transmit(struct Device* device, enum LoraModulation modulation);
|
||||
bool lora_can_receive(struct Device* device, enum LoraModulation modulation);
|
||||
error_t lora_set_parameter(struct Device* device, enum LoraParameter parameter, int32_t value);
|
||||
error_t lora_get_parameter(struct Device* device, enum LoraParameter parameter, int32_t* value);
|
||||
error_t lora_transmit(struct Device* device, const uint8_t* data, size_t length, LoraTxId* id);
|
||||
error_t lora_add_rx_callback(struct Device* device, void* callback_context, LoraRxCallback callback);
|
||||
error_t lora_remove_rx_callback(struct Device* device, LoraRxCallback callback);
|
||||
error_t lora_add_state_callback(struct Device* device, void* callback_context, LoraStateCallback callback);
|
||||
error_t lora_remove_state_callback(struct Device* device, LoraStateCallback callback);
|
||||
error_t lora_add_tx_callback(struct Device* device, void* callback_context, LoraTxCallback callback);
|
||||
error_t lora_remove_tx_callback(struct Device* device, LoraTxCallback callback);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
83
TactilityKernel/source/drivers/lora.cpp
Normal file
83
TactilityKernel/source/drivers/lora.cpp
Normal file
@ -0,0 +1,83 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/drivers/lora.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
|
||||
#define LORA_API(device) ((const struct LoraApi*)device_get_driver(device)->api)
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Device* lora_find_first_registered_device() {
|
||||
struct Device* found = nullptr;
|
||||
device_for_each_of_type(&LORA_TYPE, &found, [](struct Device* dev, void* ctx) -> bool {
|
||||
*static_cast<struct Device**>(ctx) = dev;
|
||||
return false;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
error_t lora_get_radio_state(struct Device* device, enum LoraRadioState* state) {
|
||||
return LORA_API(device)->get_radio_state(device, state);
|
||||
}
|
||||
|
||||
error_t lora_set_enabled(struct Device* device, bool enabled) {
|
||||
return LORA_API(device)->set_enabled(device, enabled);
|
||||
}
|
||||
|
||||
error_t lora_set_modulation(struct Device* device, enum LoraModulation modulation) {
|
||||
return LORA_API(device)->set_modulation(device, modulation);
|
||||
}
|
||||
|
||||
error_t lora_get_modulation(struct Device* device, enum LoraModulation* modulation) {
|
||||
return LORA_API(device)->get_modulation(device, modulation);
|
||||
}
|
||||
|
||||
bool lora_can_transmit(struct Device* device, enum LoraModulation modulation) {
|
||||
return LORA_API(device)->can_transmit(device, modulation);
|
||||
}
|
||||
|
||||
bool lora_can_receive(struct Device* device, enum LoraModulation modulation) {
|
||||
return LORA_API(device)->can_receive(device, modulation);
|
||||
}
|
||||
|
||||
error_t lora_set_parameter(struct Device* device, enum LoraParameter parameter, int32_t value) {
|
||||
return LORA_API(device)->set_parameter(device, parameter, value);
|
||||
}
|
||||
|
||||
error_t lora_get_parameter(struct Device* device, enum LoraParameter parameter, int32_t* value) {
|
||||
return LORA_API(device)->get_parameter(device, parameter, value);
|
||||
}
|
||||
|
||||
error_t lora_transmit(struct Device* device, const uint8_t* data, size_t length, LoraTxId* id) {
|
||||
return LORA_API(device)->transmit(device, data, length, id);
|
||||
}
|
||||
|
||||
error_t lora_add_rx_callback(struct Device* device, void* callback_context, LoraRxCallback callback) {
|
||||
return LORA_API(device)->add_rx_callback(device, callback_context, callback);
|
||||
}
|
||||
|
||||
error_t lora_remove_rx_callback(struct Device* device, LoraRxCallback callback) {
|
||||
return LORA_API(device)->remove_rx_callback(device, callback);
|
||||
}
|
||||
|
||||
error_t lora_add_state_callback(struct Device* device, void* callback_context, LoraStateCallback callback) {
|
||||
return LORA_API(device)->add_state_callback(device, callback_context, callback);
|
||||
}
|
||||
|
||||
error_t lora_remove_state_callback(struct Device* device, LoraStateCallback callback) {
|
||||
return LORA_API(device)->remove_state_callback(device, callback);
|
||||
}
|
||||
|
||||
error_t lora_add_tx_callback(struct Device* device, void* callback_context, LoraTxCallback callback) {
|
||||
return LORA_API(device)->add_tx_callback(device, callback_context, callback);
|
||||
}
|
||||
|
||||
error_t lora_remove_tx_callback(struct Device* device, LoraTxCallback callback) {
|
||||
return LORA_API(device)->remove_tx_callback(device, callback);
|
||||
}
|
||||
|
||||
const struct DeviceType LORA_TYPE = {
|
||||
.name = "lora"
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@ -21,6 +21,7 @@
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/drivers/i8080_controller.h>
|
||||
#include <tactility/drivers/keyboard.h>
|
||||
#include <tactility/drivers/lora.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/drivers/pwm.h>
|
||||
@ -387,6 +388,24 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(WIFI_TYPE),
|
||||
// wifi_auto_scan
|
||||
DEFINE_MODULE_SYMBOL(wifi_auto_scan_set_paused),
|
||||
// drivers/lora
|
||||
DEFINE_MODULE_SYMBOL(lora_find_first_registered_device),
|
||||
DEFINE_MODULE_SYMBOL(lora_get_radio_state),
|
||||
DEFINE_MODULE_SYMBOL(lora_set_enabled),
|
||||
DEFINE_MODULE_SYMBOL(lora_set_modulation),
|
||||
DEFINE_MODULE_SYMBOL(lora_get_modulation),
|
||||
DEFINE_MODULE_SYMBOL(lora_can_transmit),
|
||||
DEFINE_MODULE_SYMBOL(lora_can_receive),
|
||||
DEFINE_MODULE_SYMBOL(lora_set_parameter),
|
||||
DEFINE_MODULE_SYMBOL(lora_get_parameter),
|
||||
DEFINE_MODULE_SYMBOL(lora_transmit),
|
||||
DEFINE_MODULE_SYMBOL(lora_add_rx_callback),
|
||||
DEFINE_MODULE_SYMBOL(lora_remove_rx_callback),
|
||||
DEFINE_MODULE_SYMBOL(lora_add_state_callback),
|
||||
DEFINE_MODULE_SYMBOL(lora_remove_state_callback),
|
||||
DEFINE_MODULE_SYMBOL(lora_add_tx_callback),
|
||||
DEFINE_MODULE_SYMBOL(lora_remove_tx_callback),
|
||||
DEFINE_MODULE_SYMBOL(LORA_TYPE),
|
||||
// drivers/usb_host_hid
|
||||
DEFINE_MODULE_SYMBOL(usb_host_hid_is_connected),
|
||||
DEFINE_MODULE_SYMBOL(usb_host_hid_subscribe),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user