English
English
简体中文
日本語

Arduino Quick Start

2. Devices & Examples

5. Extensions

6. Applications

PaperMono LoRa Arduino Tutorial

PaperMono LoRa example programs.

Example Programs

Build Requirements

  • M5Stack Board Manager version >= 3.3.9
  • Board option = M5PaperMono
  • M5Unified library version >= 0.2.20
  • M5GFX library version >= 0.2.27
  • RadioLib library version >= 7.2.1
Note
The onboard SX1262 on PaperMono uses dedicated SPI pins: SCK=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.

Transmitter

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.

cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
#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();
}

Receiver

Flash the following program to another PaperMono. Its reception parameters must match those of the transmitter.

cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
#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.

Driver Library

Page Tools
PDF
On This Page