简体中文
English
简体中文
日本語

Arduino 上手教程

2. 设备开发 & 案例程序

5. 扩展模块

6. 应用案例

PaperMono LoRa Arduino 使用教程

PaperMono LoRa 案例程序。

案例程序

编译要求

  • M5Stack 板管理版本 >= 3.3.9
  • 开发板选项 = M5PaperMono
  • M5Unified 库版本 >= 0.2.20
  • M5GFX 库版本 >= 0.2.27
  • RadioLib 库版本 >= 7.2.1
  • M5PM1 库版本 >= 1.0.7
  • M5IOE1 库版本 >= 1.0.9
注意
PaperMono 板载 SX1262 使用 SCK=39MISO=40MOSI=38NSS=41 的独立 SPI 引脚。代码必须在创建 RadioLib Module 时传入 loraSpi 和对应的 SPISettings,并在调用 radio.begin() 前执行 loraSpi.begin(kSckPin, kMisoPin, kMosiPin, kNssPin)。省略这些设置可能使 RadioLib 使用默认 SPI 引脚,导致无法与 SX1262 通信并出现初始化失败,常见错误码为 RADIOLIB_ERR_CHIP_NOT_FOUND (-2)。

PaperMono 的 LoRa 供电使能信号 LoRa_EN 连接至 M5PM1 GPIO2;SX1262 复位信号 SX_NRST 和天线开关信号 SX_ANT_SW 分别连接至 M5IOE1 GPIO10 (M5IOE1_PIN_10) 和 GPIO2 (M5IOE1_PIN_2)。下方案例程序会先拉高 M5PM1 GPIO2 打开 LoRa 供电,再通过 M5IOE1 GPIO10 复位 SX1262,并根据收发状态控制 M5IOE1 GPIO2 开关天线通道;如果未正确配置这些引脚,SX1262 可能无法正常初始化或收发。

发送端

程序通过控制 M5PM1 打开 LoRa 供电、天线开关并完成 SX1262 复位。SX1262 初始化后,每隔 3 秒发送一条带递增编号的 Hello PaperMono #X 消息,并在屏幕和串口监视器中显示发送结果。

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 = 22;
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();
}

接收端

将下方程序烧录到另一台 PaperMono,接收参数需与发送端保持一致。

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 = 22;
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();
}

接收端会持续监听 LoRa 数据。收到数据后,屏幕会显示消息内容、累计接收数量、RSSI 和 SNR,串口监视器还会输出频率误差;出现 CRC 校验错误或读取失败时会显示对应状态,随后自动恢复接收。

驱动库

Page Tools
PDF
On This Page