PaperMono LoRa example programs.
M5PaperMonoSCK=39, MISO=40, MOSI=38, and NSS=41. When creating the RadioLib Module, the code must pass loraSpi and the corresponding SPISettings, and must call loraSpi.begin(kSckPin, kMisoPin, kMosiPin, kNssPin) before radio.begin(). Omitting these settings may cause RadioLib to use the default SPI pins, preventing communication with the SX1262 and resulting in initialization failure. A common error code is RADIOLIB_ERR_CHIP_NOT_FOUND (-2).The LoRa power-enable signal LoRa_EN on PaperMono is connected to M5PM1 GPIO2; the SX1262 reset signal SX_NRST and antenna switch signal SX_ANT_SW are connected to M5IOE1 GPIO10 (M5IOE1_PIN_10) and GPIO2 (M5IOE1_PIN_2), respectively. The example first drives M5PM1 GPIO2 high to enable the LoRa power rail, then resets the SX1262 through M5IOE1 GPIO10 and controls the antenna switch channel through M5IOE1 GPIO2 according to the transmit/receive state. If these pins are not configured correctly, the SX1262 may fail to initialize or transmit/receive normally.
The program enables LoRa power through the M5PM1 and uses the M5IOE1 to reset the SX1262 and control the antenna switch. After initializing the SX1262 for 868.0 MHz, a 125.0 kHz bandwidth, SF12, and 16 dBm, it sends a numbered Hello PaperMono message every 3 seconds and displays the transmission result on the screen and in the Serial Monitor.
#include <Arduino.h>
#include <M5IOE1.h>
#include <M5PM1.h>
#include <M5Unified.h>
#include <RadioLib.h>
#include <SPI.h>
namespace {
constexpr float kFrequencyMhz = 868.0f;
constexpr float kBandwidthKhz = 125.0f;
constexpr uint8_t kSpreadingFactor = 12;
constexpr uint8_t kCodingRate = 5;
constexpr uint8_t kSyncWord = 0x34;
constexpr int8_t kTxPowerDbm = 16;
constexpr uint16_t kPreambleLength = 20;
constexpr uint32_t kSendIntervalMs = 3000;
constexpr int kMosiPin = 38;
constexpr int kMisoPin = 40;
constexpr int kSckPin = 39;
constexpr int kNssPin = 41;
constexpr int kIrqPin = 5;
constexpr int kBusyPin = 21;
constexpr uint32_t kSpiFrequencyHz = 8000000;
constexpr auto kResetPin = M5IOE1_PIN_10;
constexpr auto kAntennaSwitchPin = M5IOE1_PIN_2;
SPIClass loraSpi(HSPI);
SX1262 radio = new Module(
kNssPin, kIrqPin, RADIOLIB_NC, kBusyPin, loraSpi,
SPISettings(kSpiFrequencyHz, MSBFIRST, SPI_MODE0));
M5Canvas canvas(&M5.Display);
M5PM1 pm1;
M5IOE1 ioe1;
volatile bool transmissionFinished = false;
int transmissionState = RADIOLIB_ERR_NONE;
uint32_t messageNumber = 1;
void IRAM_ATTR onTransmissionFinished()
{
transmissionFinished = true;
}
void drawScreen(const String& state, const String& message,
const String& detail = "")
{
canvas.fillSprite(TFT_WHITE);
canvas.setTextDatum(middle_center);
canvas.setTextColor(TFT_BLACK, TFT_WHITE);
canvas.setFont(&fonts::FreeSansBold24pt7b);
canvas.drawString("LoRa TX", 240, 95);
canvas.drawFastHLine(40, 150, 400, TFT_BLACK);
canvas.setFont(&fonts::FreeSansBold18pt7b);
canvas.drawString(state, 240, 220);
canvas.setFont(&fonts::FreeSansBold12pt7b);
canvas.drawString(message, 240, 330);
canvas.setFont(&fonts::FreeSans12pt7b);
canvas.drawString(detail, 240, 405);
canvas.drawString("868.0 MHz | SF12 | 16 dBm", 240, 665);
canvas.pushSprite(0, 0);
}
[[noreturn]] void stopWithError(const String& message, int code)
{
Serial.printf("%s, code: %d\n", message.c_str(), code);
drawScreen("ERROR", message, "Code: " + String(code));
while (true) {
delay(1000);
}
}
bool enableLoRaHardware()
{
const m5pm1_err_t pm1Error =
pm1.begin(&M5.In_I2C, M5PM1_DEFAULT_ADDR, M5PM1_I2C_FREQ_100K);
if (pm1Error != M5PM1_OK) {
Serial.printf("M5PM1 init failed, code: %d\n", pm1Error);
return false;
}
if (pm1.gpioSetFunc(M5PM1_GPIO_NUM_2, M5PM1_GPIO_FUNC_GPIO) != M5PM1_OK ||
pm1.gpioSet(M5PM1_GPIO_NUM_2, M5PM1_GPIO_MODE_OUTPUT, HIGH,
M5PM1_GPIO_PULL_NONE,
M5PM1_GPIO_DRIVE_PUSHPULL) != M5PM1_OK) {
Serial.println("LoRa power enable failed");
return false;
}
Serial.println("LoRa power: enabled");
const m5ioe1_err_t ioeError = ioe1.begin(
&M5.In_I2C, M5IOE1_DEFAULT_ADDR_2, M5IOE1_I2C_FREQ_100K);
if (ioeError != M5IOE1_OK) {
Serial.printf("M5IOE1 init failed, code: %d\n", ioeError);
return false;
}
ioe1.pinMode(kResetPin, OUTPUT);
ioe1.pinMode(kAntennaSwitchPin, OUTPUT);
if (ioe1.setDriveMode(kResetPin, M5IOE1_DRIVE_PUSHPULL) != M5IOE1_OK ||
ioe1.setDriveMode(kAntennaSwitchPin, M5IOE1_DRIVE_PUSHPULL) != M5IOE1_OK) {
Serial.println("LoRa IO drive setup failed");
return false;
}
delay(200);
m5ioe1_err_t ioeWriteError = M5IOE1_OK;
ioe1.digitalWriteWithRes(kResetPin, LOW, &ioeWriteError);
delay(100);
ioe1.digitalWriteWithRes(kResetPin, HIGH, &ioeWriteError);
delay(200);
if (ioeWriteError != M5IOE1_OK) {
Serial.printf("LoRa reset failed, code: %d\n", ioeWriteError);
return false;
}
pinMode(kBusyPin, INPUT);
Serial.printf("LoRa reset: released, BUSY=%d\n", digitalRead(kBusyPin));
return true;
}
void startPacket()
{
String message = "Hello PaperMono #" + String(messageNumber);
Serial.printf("[LoRa TX] Sending: %s\n", message.c_str());
ioe1.digitalWrite(kAntennaSwitchPin, HIGH);
transmissionState = radio.startTransmit(message);
if (transmissionState != RADIOLIB_ERR_NONE) {
transmissionFinished = true;
}
}
} // namespace
void setup()
{
Serial.begin(115200);
delay(100);
Serial.println("PaperMONO LoRa sender");
auto config = M5.config();
config.clear_display = false;
config.internal_rtc = false;
config.internal_imu = false;
config.internal_mic = false;
M5.begin(config);
M5.Display.setRotation(0);
M5.Display.setBrightness(160);
M5.Display.setEpdMode(epd_mode_t::epd_fast);
canvas.setColorDepth(16);
if (!canvas.createSprite(M5.Display.width(), M5.Display.height())) {
Serial.println("Display canvas allocation failed");
while (true) delay(1000);
}
drawScreen("INITIALIZING", "SX1262");
if (!enableLoRaHardware()) {
stopWithError("LoRa power init failed", -1);
}
loraSpi.begin(kSckPin, kMisoPin, kMosiPin, kNssPin);
Serial.println("LoRa SPI: HSPI/SPI3 at 8 MHz");
const int state = radio.begin(kFrequencyMhz, kBandwidthKhz,
kSpreadingFactor, kCodingRate, kSyncWord,
kTxPowerDbm, kPreambleLength, 3.0f, true);
if (state != RADIOLIB_ERR_NONE) {
stopWithError("SX1262 init failed", state);
}
radio.setCurrentLimit(140);
radio.setPacketSentAction(onTransmissionFinished);
drawScreen("READY", "Sending every 3 seconds");
startPacket();
}
void loop()
{
if (!transmissionFinished) {
delay(5);
return;
}
transmissionFinished = false;
const String message = "Hello PaperMono #" + String(messageNumber);
if (transmissionState == RADIOLIB_ERR_NONE) {
Serial.printf("[LoRa TX] Sent: %s\n", message.c_str());
drawScreen("SENT", message,
"Message " + String(messageNumber));
} else {
Serial.printf("[LoRa TX] Failed, code: %d\n", transmissionState);
drawScreen("SEND FAILED", message,
"Code: " + String(transmissionState));
}
radio.finishTransmit();
delay(kSendIntervalMs);
++messageNumber;
startPacket();
}Flash the following program to another PaperMono. Its reception parameters must match those of the transmitter.
#include <Arduino.h>
#include <M5IOE1.h>
#include <M5PM1.h>
#include <M5Unified.h>
#include <RadioLib.h>
#include <SPI.h>
namespace {
constexpr float kFrequencyMhz = 868.0f;
constexpr float kBandwidthKhz = 125.0f;
constexpr uint8_t kSpreadingFactor = 12;
constexpr uint8_t kCodingRate = 5;
constexpr uint8_t kSyncWord = 0x34;
constexpr int8_t kTxPowerDbm = 16;
constexpr uint16_t kPreambleLength = 20;
constexpr int kMosiPin = 38;
constexpr int kMisoPin = 40;
constexpr int kSckPin = 39;
constexpr int kNssPin = 41;
constexpr int kIrqPin = 5;
constexpr int kBusyPin = 21;
constexpr uint32_t kSpiFrequencyHz = 8000000;
constexpr auto kResetPin = M5IOE1_PIN_10;
constexpr auto kAntennaSwitchPin = M5IOE1_PIN_2;
SPIClass loraSpi(HSPI);
SX1262 radio = new Module(
kNssPin, kIrqPin, RADIOLIB_NC, kBusyPin, loraSpi,
SPISettings(kSpiFrequencyHz, MSBFIRST, SPI_MODE0));
M5Canvas canvas(&M5.Display);
M5PM1 pm1;
M5IOE1 ioe1;
volatile bool packetReceived = false;
uint32_t receivedCount = 0;
void IRAM_ATTR onPacketReceived()
{
packetReceived = true;
}
void drawScreen(const String& state, const String& message,
const String& rssi = "", const String& snr = "",
const String& detail = "")
{
canvas.fillSprite(TFT_WHITE);
canvas.setTextDatum(middle_center);
canvas.setTextColor(TFT_BLACK, TFT_WHITE);
canvas.setFont(&fonts::FreeSansBold24pt7b);
canvas.drawString("LoRa RX", 240, 95);
canvas.drawFastHLine(40, 150, 400, TFT_BLACK);
canvas.setFont(&fonts::FreeSansBold18pt7b);
canvas.drawString(state, 240, 215);
canvas.setFont(&fonts::FreeSansBold12pt7b);
canvas.drawString(message, 240, 310);
canvas.setFont(&fonts::FreeSans12pt7b);
canvas.drawString(rssi, 240, 390);
canvas.drawString(snr, 240, 445);
canvas.drawString(detail, 240, 515);
canvas.drawString("868.0 MHz | SF12 | CR 4/5", 240, 665);
canvas.pushSprite(0, 0);
}
[[noreturn]] void stopWithError(const String& message, int code)
{
Serial.printf("%s, code: %d\n", message.c_str(), code);
drawScreen("ERROR", message, "Code: " + String(code));
while (true) {
delay(1000);
}
}
bool enableLoRaHardware()
{
const m5pm1_err_t pm1Error =
pm1.begin(&M5.In_I2C, M5PM1_DEFAULT_ADDR, M5PM1_I2C_FREQ_100K);
if (pm1Error != M5PM1_OK) {
Serial.printf("M5PM1 init failed, code: %d\n", pm1Error);
return false;
}
if (pm1.gpioSetFunc(M5PM1_GPIO_NUM_2, M5PM1_GPIO_FUNC_GPIO) != M5PM1_OK ||
pm1.gpioSet(M5PM1_GPIO_NUM_2, M5PM1_GPIO_MODE_OUTPUT, HIGH,
M5PM1_GPIO_PULL_NONE,
M5PM1_GPIO_DRIVE_PUSHPULL) != M5PM1_OK) {
Serial.println("LoRa power enable failed");
return false;
}
Serial.println("LoRa power: enabled");
const m5ioe1_err_t ioeError = ioe1.begin(
&M5.In_I2C, M5IOE1_DEFAULT_ADDR_2, M5IOE1_I2C_FREQ_100K);
if (ioeError != M5IOE1_OK) {
Serial.printf("M5IOE1 init failed, code: %d\n", ioeError);
return false;
}
ioe1.pinMode(kResetPin, OUTPUT);
ioe1.pinMode(kAntennaSwitchPin, OUTPUT);
if (ioe1.setDriveMode(kResetPin, M5IOE1_DRIVE_PUSHPULL) != M5IOE1_OK ||
ioe1.setDriveMode(kAntennaSwitchPin, M5IOE1_DRIVE_PUSHPULL) != M5IOE1_OK) {
Serial.println("LoRa IO drive setup failed");
return false;
}
ioe1.digitalWrite(kAntennaSwitchPin, LOW);
delay(200);
m5ioe1_err_t ioeWriteError = M5IOE1_OK;
ioe1.digitalWriteWithRes(kResetPin, LOW, &ioeWriteError);
delay(100);
ioe1.digitalWriteWithRes(kResetPin, HIGH, &ioeWriteError);
delay(200);
if (ioeWriteError != M5IOE1_OK) {
Serial.printf("LoRa reset failed, code: %d\n", ioeWriteError);
return false;
}
pinMode(kBusyPin, INPUT);
Serial.printf("LoRa reset: released, BUSY=%d\n", digitalRead(kBusyPin));
return true;
}
void resumeReceive()
{
ioe1.digitalWrite(kAntennaSwitchPin, LOW);
const int state = radio.startReceive();
if (state != RADIOLIB_ERR_NONE) {
stopWithError("Listen failed", state);
}
}
} // namespace
void setup()
{
Serial.begin(115200);
delay(100);
Serial.println("PaperMONO LoRa receiver");
auto config = M5.config();
config.clear_display = false;
config.internal_rtc = false;
config.internal_imu = false;
config.internal_mic = false;
M5.begin(config);
M5.Display.setRotation(0);
M5.Display.setBrightness(160);
M5.Display.setEpdMode(epd_mode_t::epd_fast);
canvas.setColorDepth(16);
if (!canvas.createSprite(M5.Display.width(), M5.Display.height())) {
Serial.println("Display canvas allocation failed");
while (true) delay(1000);
}
drawScreen("INITIALIZING", "SX1262");
if (!enableLoRaHardware()) {
stopWithError("LoRa power init failed", -1);
}
loraSpi.begin(kSckPin, kMisoPin, kMosiPin, kNssPin);
Serial.println("LoRa SPI: HSPI/SPI3 at 8 MHz");
const int state = radio.begin(kFrequencyMhz, kBandwidthKhz,
kSpreadingFactor, kCodingRate, kSyncWord,
kTxPowerDbm, kPreambleLength, 3.0f, true);
if (state != RADIOLIB_ERR_NONE) {
stopWithError("SX1262 init failed", state);
}
radio.setCurrentLimit(140);
radio.setPacketReceivedAction(onPacketReceived);
resumeReceive();
Serial.println("[LoRa RX] Listening...");
drawScreen("LISTENING", "Waiting for a packet");
}
void loop()
{
if (!packetReceived) {
delay(5);
return;
}
packetReceived = false;
String message;
const int state = radio.readData(message);
if (state == RADIOLIB_ERR_NONE) {
++receivedCount;
const float rssi = radio.getRSSI();
const float snr = radio.getSNR();
const float frequencyError = radio.getFrequencyError();
Serial.printf("[LoRa RX] Data: %s\n", message.c_str());
Serial.printf("[LoRa RX] RSSI: %.2f dBm, SNR: %.2f dB\n", rssi, snr);
Serial.printf("[LoRa RX] Frequency error: %.2f Hz\n", frequencyError);
drawScreen("RECEIVED", message,
"RSSI " + String(rssi, 2) + " dBm",
"SNR " + String(snr, 2) + " dB",
"Packets " + String(receivedCount));
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
Serial.println("[LoRa RX] CRC mismatch");
drawScreen("CRC ERROR", "Packet discarded");
} else {
Serial.printf("[LoRa RX] Read failed, code: %d\n", state);
drawScreen("READ FAILED", "Code: " + String(state));
}
resumeReceive();
}The receiver continuously listens for LoRa data. When data is received, the screen displays the message, total number of received packets, RSSI, and SNR, while the Serial Monitor also outputs the frequency error. If a CRC error or read failure occurs, the corresponding status is displayed, after which reception resumes automatically.