Słomkowski's technical musings

Playing with software, hardware and touching the sky with a paraglider.

Getting into Meshtastic with Orange Pi Zero 3 and E22-900M30S LoRa module


Testing a Meshtastic node with an Orange Pi Zero 3 and an E22-900M30S (SX1262) LoRa module. I describe Armbian, SPI, GPIO configuration, and building an 868 MHz ground-plane antenna.

I was browsing the loranet.pl and lorastats.pl maps one day. These services gather information about Meshtastic nodes. I noticed there are quite a lot of nodes in the city where I live. So it struck me: maybe it’s finally time to get into this mesh network thing?

My ideas revolved around building an outdoor node powered with PoE, but I decided to start small and build a node into my Octoglow device. That would match its post-apocalyptic theme nicely. But before I went to KiCad, I needed to do some research. I present the results here.

I wanted to stick with the Orange Pi Zero line of SBCs. Making a new PCB gives me an opportunity to upgrade the OPi Zero to the third generation. I bought a used 2 GB variant for the equivalent of 20 euros. Since most Meshtastic traffic in Poland is on 868 MHz, I ordered an EBYTE E22-900M30S LoRa module on AliExpress. The package was missing an 868 MHz antenna, which prompted me to build one myself. I also found a nice tutorial about Meshtastic on Orange Pi Zero 3, but it uses DietPi instead of Armbian and a ready-made LoRa HAT instead of a bare E22-900M30S module.

Orange Pi Zero 3 hardware setup

Orange Pi Zero 3 (in short, OPi Zero 3) is one of the cheap, fruit-themed Raspberry Pi alternatives, equipped with a 64-bit ARM Cortex-A53 CPU and 1/1.5/2/4 gigabytes of RAM. I installed Armbian Linux 25.11.2 on it (the Debian 12 bookworm version). The device’s low price is offset by a relative lack of community support and documentation. The pinout:

Orange Pi Zero 3 pinout.

The pinout is important because you’ll be connecting the LoRa module to it. The GPIO header on the left has 26 pins, which makes it incompatible with the more common 40-pin Raspberry Pi header. So you can’t use popular LoRa HATs directly.

Enabling SPI

The E22-900M30S LoRa module requires SPI communication. Configuring it on the Orange Pi Zero 3 took me a surprisingly long time to figure out. Because of the lack of documentation, I struggled to apply the proper device tree overlays to enable SPI and the hardware CS signal on the OPi Zero 3 pin.

The armbian-config tool warns that bookworm is not an officially supported distro. If you go into its menu anyway (System→Kernel→Manage device tree overlays), it lists a lot of overlays, including ones named spi-spidev, spidev0_0, etc. Don’t use them! The proper ones are, quite surprisingly, prefixed with bananapi-m4-. There’s another fruit-themed SBC called Banana Pi, so perhaps the overlays happen to match. Overlay support seems shaky, at least it worked with Armbian 25.11.2 at the time of writing. Anyway, it’s better to skip armbian-config and edit /boot/armbianEnv.txt directly, so the relevant fragment looks like this:

overlays=bananapi-m4-spi1-cs0-cs1-spidev bananapi-m4-spi1-cs1-spidev
param_spidev_spi_bus=1
param_spidev_max_freq=8000000

To be on the safe side and avoid potential transmission errors, I limited the SPI clock frequency to 8 MHz. Software usually sets this value much lower anyway. After a reboot, two new devices should be visible: /dev/spidev1.0 and /dev/spidev1.1. The first number corresponds to the SPI bus number, and the second to the CS pin. Only CS pin 1 is exposed on the GPIO header, so for the LoRa module you’ll use spidev1.1.

Before attaching the LoRa module, you can check whether SPI is working correctly (at least the MISO and MOSI signals) with a short Python script:

# requires: apt install python3-spidev

import spidev

spi = spidev.SpiDev()
spi.open(1, 1)  # bus, CS
spi.max_speed_hz = 500000
data_out = [12, 34, 56]
data_in = spi.xfer2(data_out)
print("Received:", data_in)
spi.close()

Connect the MISO and MOSI pins with a piece of wire and run the script. If everything is fine, you should see Received: [12, 34, 56] printed on the screen. Values like Received: [255, 255, 255] or Received: [0, 0, 0] indicate a problem: a misconfigured SPI setup or no connection between the MISO and MOSI pins.

Connecting EBYTE E22-900M30S LoRa module

Warning

Never operate the LoRa module without an antenna attached. Because of reflected power, the module may be damaged.

EBYTE E22-900M30S is a popular LoRa module based on the Semtech SX1262, which can output up to +30 dBm (1 watt) of RF power in the 850–930 MHz range. That covers Europe’s 868 MHz ISM band.

To function, the module requires a +5 V power supply, but the data signals must be 3.3 V. Communication is done over SPI, but additional GPIO lines are required as well. The connections are listed in the table:

No: E22: Meshtastic config: OPi Zero 3 SoC pin name: OPi Zero 3 GPIO line: Notes:
1 MISO PH8/MISO
2 MOSI PH7/MOSI
3 SCK PC6/CLK
4 NSS PH9/CS 233 CS can be driven either by hardware SPI or as ordinary GPIO.
5 BUSY Busy PC10 74
6 DIO1 IRQ PC7 71 Needs to support interrupts.
7 NRST Reset PC14 78
8 RXEN RXen PC15 79
9 TXEN TXen PC11 75 Optional, see text.

The E22 module’s MISO, MOSI, and SCK signals are driven by the Orange Pi Zero’s hardware SPI interface, so they must be connected to their respective pins on the OPi Zero 3 GPIO header. With the other signals, however, you have much more freedom: they can use any GPIOs on the header. Note that the GPIO connected to IRQ needs to support interrupts, but all GPIOs on the OPi Zero 3 header do anyway. So, for further testing, I picked a few GPIO pins and listed them in the table above. They can be changed, of course.

The LoRa module’s TXEN pin can be driven directly by an OPi Zero 3 GPIO pin. But if you want to minimize the number of GPIOs you use, you can connect TXEN to DIO2 (on the E22 module) instead. In that case, the DIO2_AS_RF_SWITCH flag should be set to true in the Meshtastic configuration file.

Meshtastic flag: Description:
DIO2_AS_RF_SWITCH Should be set to true if E22’s DIO2 pin is connected to the TXEN pin. If false, a dedicated OPi Zero 3 GPIO pin must drive TXEN.
DIO3_TCXO_VOLTAGE Should be true because the E22-900M30S has an integrated Temperature-Compensated Crystal Oscillator.
SX126X_MAX_POWER Output power setting; you have to add +7 dB (for example, 20 results in 27 dBm at the output).

GPIO Line Calculation

On the pinout diagrams, the pins are marked with SoC labels like PH8, PC15, etc. But under Linux, the SoC pins are assigned numerical identifiers called GPIO lines. Each GPIO-capable device driver registers a /dev/gpiochip* device, and each device exposes several GPIO lines. You can list the available GPIO chips and lines with the gpioinfo command.

On the OPi Zero 3, all external GPIOs are exposed on gpiochip1. There are 288 GPIO lines on it, but only a handful are available on the OPi Zero 3 GPIO header. So how do you calculate the GPIO line number from the SoC pin label? On Allwinner, given the SoC pin name P{X}{Y} (for example PC10), where X is the port letter and Y is the index within that port:

Variable Description
L Linux GPIO line number (within a given gpiochip).
X Group (0–8: A through I):
A B C D E F G H I
0 1 2 3 4 5 6 7 8
Y Pin index within the port (0–31).

the GPIO line number is calculated as:

L = ( X × 32 ) + Y

For example, PC10 means port C X = 2 , and Y = 10 , so L = 74 . Other SoC pin labels and their respective GPIO lines are listed in the table.

Setting maximum transmit power

In the config, set the maximum output power for the SX1262 chip. The E22-900M30S module, apart from the SX1262 chip, has an integrated output amplifier that boosts the signal by 7 dB. So the setting 22 corresponds to ~30 dBm (1 watt) of output power. More detailed measurements are gathered here.

Keep in mind the radio-frequency laws in your country. In Europe, 27 dBm (500 mW) EIRP is the limit for radiated power on the 868 MHz band. That includes the whole transmission budget, so if the antenna has a gain of 2.15 dBi and the cable/connector losses are 0.1 dB, the transmitter power shouldn’t exceed 25 dBm. That is, if you want to stay below the legal limit, you should set the SX1262 output power setting to 18.

Ground Plane antenna for 868 MHz band

The package I received from AliExpress was missing an antenna. Fortunately, my past interests in amateur radio left me with some SMA connectors and pigtails, so I could quickly build an antenna for the 868 MHz band. I chose a Ground Plane antenna because it’s simple enough. It has gain comparable to a dipole, about 2.15 dBi. The antenna description and calculator are available on M0UKD’s website. I took an SMA female connector and soldered pieces of Φ1.7 mm copper wire to it. I connected it to the LoRa module with an SMA–U.FL cable. Then I attached the contraption to a plant scaffold made of plastic.

Note

Keep in mind that SMA is not the same as RP-SMA, which is usually used for Wi‑Fi equipment! Reverse Polarity SMA simply has its center-pin gender swapped compared to standard SMA. This evil connector was devised specifically for Wi‑Fi and meant to prevent people from attaching their own antennas to Wi‑Fi cards.

For a center frequency of 869 MHz, the calculated length of the radiator is 82 mm, and the length of the counterweights is 92 mm. I cut the counterweights exactly to the calculated length, but I left ~5 mm of slack on the radiator so I could tune it.

For tuning the antenna, I finally made some use of the NanoVNA I had bought several years ago. For the uninitiated in RF design or amateur radio: the NanoVNA is a popular and inexpensive vector network analyzer, a tremendously useful tool that, very roughly speaking, lets you measure a component’s impedance over a wide range of frequencies. Leveraging this power requires a bit of knowledge, though; you have to understand concepts like complex impedance, SWR, S11, S12, Return Loss, etc.

I started tuning by attaching the NanoVNA to the antenna and, for convenience, also to the PC. Despite the NanoVNA’s color touchscreen, using it directly is rather cumbersome. In NanoVNA Saver, I enabled the S11 Return Loss (dB) and S11 R+jX charts for the 750–1000 MHz range. Of course, the NanoVNA requires calibration beforehand. You can also use the S11 VSWR chart for similar results.

NanoVNA Saver screenshot: Return Loss.

The goal is to make the return loss chart as low as possible at the desired frequency, and to get the impedance close to 50 Ω real + 0j Ω imaginary. A rule of thumb is that changing the radiator length shifts the resonance frequency, while changing the counterweight angle affects the impedance. By cutting the radiator bit by bit and bending/changing the angle of the counterweights, I achieved the result shown in the screenshots. BTW, there’s a comprehensive article about matching antenna impedance and the myth of the 1:1 SWR requirement.

NanoVNA Saver screenshot: Complex Impedance.

Meshtastic demon setup

Fortunately, Meshtastic supports 64-bit ARM Debian out of the box. To install it on Armbian, simply follow the official tutorial and assume you’re on an ARM Debian distro. Ignore the Raspberry Pi hardware configuration, and apply the one below instead.

You need to make sure the LoRa module configuration matches your hardware. Place the following file at /etc/meshtasticd/config.d/lora.yaml:

Lora:
  Module: sx1262
  DIO2_AS_RF_SWITCH: true
  DIO3_TCXO_VOLTAGE: true
  SX126X_MAX_POWER: 22
  spidev: spidev1.1
  gpiochip: 1
  Busy:           # BUSY, PC10
    pin: 26
    line: 74
  Reset:          # NRST, PC14 
    pin: 18
    line: 78
  IRQ:            # DIO1, PC7
    pin: 22
    line: 71
# TXen:           # TXEN, PC11
#   pin: 12
#   line: 75
  RXen:           # RXEN, PC15
    pin: 16
    line: 79

If you’ve read the section about GPIO lines above, the YAML config will be almost self-explanatory. The only strange thing is the pin entry. These are used on Raspberry Pi. Raspberry Pi has a vast ecosystem where header pin numbering is treated as a standard interface. Libraries commonly ship a mapping table such as: header physical pin number → SoC pin label → gpiochip + line.

So a config field named pin can mean header pin number, and the library can resolve it transparently. On the OPi Zero 3 (and many other SBCs), however, there isn’t one universally adopted mapping layer, so you have to specify gpiochip and line manually.

But Meshtastic demon insists that the pin entries are defined, and it throws an error otherwise. I didn’t explore the firmware source code deeply, but it seems the pin entries have to be unique numbers despite not being used (because line overrides them). I simply set them to the header pin numbers.

The TXen entry should be defined if you want to drive it from GPIO. The max transmit power setting should be chosen according to your jurisdiction. My setup is indoors, so I set it to maximum power.

During the initial setup, to make debug messages readily visible in the terminal, you can run the server directly instead of using the systemd service:

meshtasticd -v

After everything is proven to work, one can enable the service and start it:

systemctl start meshtasticd.service
systemctl enable meshtasticd.service

Further configuration can be done via the official Meshtastic mobile app. Enter the IP address of your OPi Zero 3 in the app and follow one of the tutorials online. These configuration steps are not specific to the OPi Zero 3.

Ideas for further fun

As I mentioned in the introduction, I’m planning to build a Meshtastic node into my Octoglow VFD display. Another idea I’m entertaining is building a node into the car. It might provide info about the current GPS position, battery voltage, and any other data available on the CAN bus through the OBD-II connector. It could also launch the Webasto heater. I hope the dynamically expanding mesh network in Poland will bring even more fun!