Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:
- PicoW_repeater
# STM32
- wio-e5-mini_repeater
- LilyGo_T3_STM32_repeater
- LilyGo_T3_STM32_companion_radio_usb
- LilyGo_T3_STM32_repeater_ina219
- LilyGo_T3_STM32_companion_radio_usb_ina219
# ESP32-C6
- LilyGo_Tlora_C6_repeater_
# LR1110 (nRF52)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ For developers:

- Install [PlatformIO](https://docs.platformio.org) in [Visual Studio Code](https://code.visualstudio.com).
- Clone and open the MeshCore repository in Visual Studio Code.
- For the LilyGO T3 STM32, see the [board build and flashing guide](./variants/lilygo_t3_stm32/README.md).
- See the example applications you can modify and run:
- [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or Wi-Fi.
- [KISS Modem](./examples/kiss_modem) - Serial KISS protocol bridge for host applications. ([protocol docs](./docs/kiss_modem_protocol.md))
Expand Down
32 changes: 32 additions & 0 deletions boards/lilygo_t3_stm32.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"build": {
"arduino": {
"variant_h": "variant_generic.h"
},
"core": "stm32",
"cpu": "cortex-m4",
"extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WL55xx",
"f_cpu": "48000000L",
"framework_extra_flags": {
"arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_GENERIC_WL55CCUX"
},
"mcu": "stm32wl55ccu6",
"product_line": "STM32WL55xx",
"variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U"
},
"debug": {
"jlink_device": "STM32WL55CC",
"openocd_target": "stm32wlx",
"svd_path": "STM32WL5x_CM4.svd"
},
"frameworks": ["arduino"],
"name": "LilyGO T3 STM32 V1.0 (STM32WL55CCU6)",
"upload": {
"maximum_ram_size": 65536,
"maximum_size": 262144,
"protocol": "serial",
"protocols": ["serial", "stlink", "jlink"]
},
"url": "https://github.com/Xinyuan-LilyGO/T3-STM32",
"vendor": "LilyGO"
}
7 changes: 6 additions & 1 deletion src/helpers/ui/SSD1306Display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ bool SSD1306Display::begin() {
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS);
bool success = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false);
#if defined(PIN_OLED_CS) && defined(PIN_OLED_DC)
return success;
#else
return success && i2c_probe(Wire, DISPLAY_ADDRESS);
#endif
}

void SSD1306Display::turnOn() {
Expand Down
8 changes: 8 additions & 0 deletions src/helpers/ui/SSD1306Display.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
#define DISPLAY_ADDRESS 0x3C
#endif

#if defined(PIN_OLED_CS) != defined(PIN_OLED_DC)
#error "SPI OLED requires both PIN_OLED_CS and PIN_OLED_DC"
#endif

class SSD1306Display : public DisplayDriver {
Adafruit_SSD1306 display;
bool _isOn;
Expand All @@ -24,7 +28,11 @@ class SSD1306Display : public DisplayDriver {
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
#if defined(PIN_OLED_CS) && defined(PIN_OLED_DC)
display(128, 64, &SPI, PIN_OLED_DC, PIN_OLED_RESET, PIN_OLED_CS),
#else
display(128, 64, &Wire, PIN_OLED_RESET),
#endif
_peripher_power(peripher_power)
{
_isOn = false;
Expand Down
101 changes: 101 additions & 0 deletions variants/lilygo_t3_stm32/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# LilyGO T3 STM32 V1.0

MVP support for the STM32WL55CCU6 board with its integrated Sub-GHz radio and
128×64 SSD1315 SPI OLED. Supports a USB companion and a repeater, with a default
transmit power of **20 dBm**. The companion uses USB serial; the board has no BLE.

## Build

```sh
pio run -e LilyGo_T3_STM32_companion_radio_usb
pio run -e LilyGo_T3_STM32_repeater
```

The companion supports 100 contacts and eight channels. Radio settings inherit
the repository defaults; select settings appropriate to the board's RF band.
Existing saved preferences override build defaults.

## Optional INA219 telemetry

The INA219 builds use MeshCore's existing environment sensor driver to report
voltage, current, and power. Build either role with:

```sh
pio run -e LilyGo_T3_STM32_repeater_ina219
pio run -e LilyGo_T3_STM32_companion_radio_usb_ina219
```

Connect the sensor's logic supply to **3.3 V** and use a common ground:

| INA219 connection | T3 STM32 connection |
| --- | --- |
| VCC / logic supply | 3.3 V |
| GND | GND |
| SDA | PB7 (JP2 pin 9) |
| SCL | PB8 (JP2 pin 10) |

PB7/PB8 are the alternate I2C1 pins exposed on the board headers. The default
STM32 I2C pins, PA9/PA10, are reserved for USB serial. The OLED uses SPI.
The sensor is detected at startup at address **0x40**; to use another address,
override `TELEM_INA219_ADDRESS` in the build flags. An absent sensor produces no
additional telemetry. The base builds do not initialize I2C or include the driver.

The measurement terminals (VIN+ and VIN-) go in series with the positive supply
of the circuit being measured; these are separate from the sensor's logic supply.
See the [INA219 wiring guide](https://learn.adafruit.com/adafruit-ina219-current-sensor-breakout/wiring).
USB power can bypass a sensor placed only in the battery lead.

Readings use environment telemetry permissions. Existing telemetry encodes current
in 1 mA steps and power in whole watts; it does not provide accumulated mAh or
battery percentage. INA219 telemetry has been confirmed on hardware with the
repeater build; the USB companion sensor build has been compile-tested only.

## Flash and connect

Connect the USB serial port. Hold BOOT, press and release RST, then release BOOT:

```sh
pio run -e LilyGo_T3_STM32_companion_radio_usb -t upload --upload-port /dev/ttyACM0
```

Choose your actual serial port, then press RST after programming. The repeater
CLI and USB companion protocol both use USART1 at **115200 baud**.

The bundled `stm32flash` 0.7 may reject device ID `0x497` due to an
[upstream packaging issue](https://github.com/platformio/platform-ststm32/issues/899).
Use an updated [stm32flash](https://sourceforge.net/p/stm32flash/code/ci/master/tree/)
with STM32WL support, or STM32CubeProgrammer in UART mode. For CubeProgrammer,
use `.pio/build/<environment>/firmware.hex`, or `firmware.bin` at `0x08000000`.
ST-Link and J-Link uploads are also available through the board's SWD connector.

Application flash is limited to **224 KiB**. InternalFS occupies the final
32 KiB at `0x08038000`–`0x0803FFFF`; preserve it when updating to retain the
identity and settings.

## Hardware mapping

| Function | Pins / setting |
| --- | --- |
| USART1 | TX PA9, RX PA10 |
| Radio RF switch | PB2/PB6: idle 0/0, RX 1/0, HP TX 0/1 |
| Radio oscillator | 32 MHz crystal; TCXO mode disabled |
| SPI1 | SCK PA5, MISO PA6, MOSI PA7 |
| OLED | CS PB12, DC PA8, no software reset pin |
| SD card | CS PA4, held high |
| User button | PH3/BOOT, active high |
| LEDs | PA0/PA1, left off |

Initialization follows the manufacturer's
[hardware files](https://github.com/Xinyuan-LilyGO/T3-STM32/tree/master/hardware)
and [reference firmware](https://github.com/Xinyuan-LilyGO/T3-STM32/tree/master/examples/6_SubGHz_TXRX).

## MVP validation and limitations

Hardware checks covered OLED startup, USB companion communication, advertisement
reception, Public message transmission, and matching repeater trace replies at
20 dBm. Public packets were captured and decrypted independently. Some repeater
pings timed out; long-term reliability has not been established. Higher transmit
powers are outside the validated MVP setting.

The repeater builds successfully; forwarding has not been hardware-tested.
Built-in battery telemetry, SD storage, and low-power optimization are not implemented.
11 changes: 11 additions & 0 deletions variants/lilygo_t3_stm32/STM32ArduinoCompat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#ifdef __cplusplus
// Include the device headers first so subsequent Arduino includes cannot
// reintroduce the peripheral macro. It conflicts with both mesh::RNG and
// Crypto's global RNG object. Leave C HAL sources' register macros intact.
#include <Arduino.h>
#ifdef RNG
#undef RNG
#endif
#endif
81 changes: 81 additions & 0 deletions variants/lilygo_t3_stm32/platformio.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
[lilygo_t3_stm32]
extends = stm32_base
board = lilygo_t3_stm32
board_upload.maximum_size = 229376 ; final 32 KiB reserved for InternalFS
build_flags = ${stm32_base.build_flags}
-include variants/lilygo_t3_stm32/STM32ArduinoCompat.h
-I variants/lilygo_t3_stm32
-D RADIO_CLASS=CustomSTM32WLx
-D WRAPPER_CLASS=CustomSTM32WLxWrapper
-D LORA_TX_POWER=20
-D SX126X_CURRENT_LIMIT=140
-D RX_BOOSTED_GAIN=true
-D STM32WL_TCXO_VOLTAGE=0 ; crystal oscillator; manufacturer sets IS_TCXO_SUPPORTED=0
-D PIN_SERIAL_TX=PA9
-D PIN_SERIAL_RX=PA10
-D SERIAL_UART_INSTANCE=1
-D PIN_SPI_SCK=PA5
-D PIN_SPI_MISO=PA6
-D PIN_SPI_MOSI=PA7
-D PIN_OLED_CS=PB12
-D PIN_OLED_DC=PA8
-D PIN_OLED_RESET=-1
-D PIN_USER_BTN=PH3
-D USER_BTN_PRESSED=HIGH
-D DISPLAY_CLASS=SSD1306Display
build_src_filter = ${stm32_base.build_src_filter}
+<../variants/lilygo_t3_stm32>
+<helpers/ui/SSD1306Display.cpp>
lib_deps = ${stm32_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13

[env:LilyGo_T3_STM32_repeater]
extends = lilygo_t3_stm32
build_flags = ${lilygo_t3_stm32.build_flags}
-D ADVERT_NAME='"T3 STM32 Repeater"'
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
build_src_filter = ${lilygo_t3_stm32.build_src_filter}
+<../examples/simple_repeater/*.cpp>

[env:LilyGo_T3_STM32_companion_radio_usb]
extends = lilygo_t3_stm32
build_flags = ${lilygo_t3_stm32.build_flags}
-I examples/companion_radio/ui-orig
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D ENABLE_USB_INTERFACE
build_src_filter = ${lilygo_t3_stm32.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-orig/*.cpp>
lib_deps = ${lilygo_t3_stm32.lib_deps}
densaugeo/base64 @ ~1.4.0

; Optional INA219 telemetry on I2C1. Keep the base builds sensor-free.
[lilygo_t3_stm32_ina219]
build_flags =
-D ENV_INCLUDE_INA219=1
-D PIN_WIRE_SDA=PB7
-D PIN_WIRE_SCL=PB8
build_src_filter =
+<helpers/sensors/EnvironmentSensorManager.cpp>
lib_deps =
adafruit/Adafruit INA219 @ ^1.2.3

[env:LilyGo_T3_STM32_repeater_ina219]
extends = env:LilyGo_T3_STM32_repeater
build_flags = ${env:LilyGo_T3_STM32_repeater.build_flags}
${lilygo_t3_stm32_ina219.build_flags}
build_src_filter = ${env:LilyGo_T3_STM32_repeater.build_src_filter}
${lilygo_t3_stm32_ina219.build_src_filter}
lib_deps = ${lilygo_t3_stm32.lib_deps}
${lilygo_t3_stm32_ina219.lib_deps}

[env:LilyGo_T3_STM32_companion_radio_usb_ina219]
extends = env:LilyGo_T3_STM32_companion_radio_usb
build_flags = ${env:LilyGo_T3_STM32_companion_radio_usb.build_flags}
${lilygo_t3_stm32_ina219.build_flags}
build_src_filter = ${env:LilyGo_T3_STM32_companion_radio_usb.build_src_filter}
${lilygo_t3_stm32_ina219.build_src_filter}
lib_deps = ${env:LilyGo_T3_STM32_companion_radio_usb.lib_deps}
${lilygo_t3_stm32_ina219.lib_deps}
94 changes: 94 additions & 0 deletions variants/lilygo_t3_stm32/target.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include <Arduino.h>
#include <SPI.h>
#if ENV_INCLUDE_INA219
#include <Wire.h>
#endif
#include "target.h"

LilyGoT3STM32Board board;
RADIO_CLASS radio = new STM32WLx_Module();
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock rtc_clock;
#if ENV_INCLUDE_INA219
EnvironmentSensorManager sensors;
#else
SensorManager sensors;
#endif
DISPLAY_CLASS display;
// PH3 is also BOOT0: the board's button is active high with an external pull-down.
MomentaryButton user_btn(PIN_USER_BTN, 1000, false);

static const uint32_t rfswitch_pins[] = {
PB2, PB6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC
};
static const Module::RfSwitchMode_t rfswitch_table[] = {
{STM32WLx::MODE_IDLE, {LOW, LOW}},
{STM32WLx::MODE_RX, {HIGH, LOW}},
{STM32WLx::MODE_TX_HP, {LOW, HIGH}},
END_OF_MODE_TABLE,
};

void LilyGoT3STM32Board::begin() {
STM32Board::begin();

#if ENV_INCLUDE_INA219
// These builds remap Wire to PB7/PB8; PA9/PA10 are used by USART1.
Wire.begin();
#endif

// The SD card shares SPI1 with the OLED; keep it deselected.
digitalWrite(PA4, HIGH);
pinMode(PA4, OUTPUT);
digitalWrite(PIN_OLED_CS, HIGH);
pinMode(PIN_OLED_CS, OUTPUT);
pinMode(PIN_USER_BTN, INPUT);
// Both onboard LEDs are active high. Leave them off until explicitly needed.
digitalWrite(PA0, LOW);
pinMode(PA0, OUTPUT);
digitalWrite(PA1, LOW);
pinMode(PA1, OUTPUT);

SPI.setSCLK(PIN_SPI_SCK);
SPI.setMISO(PIN_SPI_MISO);
SPI.setMOSI(PIN_SPI_MOSI);
SPI.begin();
delay(200); // OLED power-on settling time used by the manufacturer's example.
}

#ifndef LORA_CR
#define LORA_CR 5
#endif

static bool check_radio_status(int status, const char* stage) {
if (status == RADIOLIB_ERR_NONE) return true;

Serial.print("ERROR: ");
Serial.print(stage);
Serial.print(": ");
Serial.println(status);
return false;
}

bool radio_init() {
radio.setRfSwitchTable(rfswitch_pins, rfswitch_table);
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR,
RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER,
16, STM32WL_TCXO_VOLTAGE, 0);
if (!check_radio_status(status, "radio.begin")) return false;

// Match the manufacturer's 3.3 V power-LDO trim (REG_DRV_CTRL).
const uint8_t power_ldo_3v3 = 0x0E;
status = radio.writeRegister(0x091F, &power_ldo_3v3, 1);
if (!check_radio_status(status, "power LDO trim")) return false;

status = radio.setCurrentLimit(SX126X_CURRENT_LIMIT);
if (!check_radio_status(status, "current limit")) return false;
status = radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
if (!check_radio_status(status, "RX gain")) return false;
return check_radio_status(radio.setCRC(1), "CRC");
}

mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}
Loading