20 KiB
How to compile the RNode firmware yourself
This document describes my efforts to add a new board to the Reticulum RNode firmware. It is published in the hope that it is useful to other makers who want to make their own RNodes.
Required knowledge
To follow this manual, some basic programming knowledge like using text editors to edit source code and using basic commands like make
is required. Plus some basic Linux knowledge. But you don't have to be an expert.
Dependencies
Make sure git, make, python and pip are installed. Most likely these are already available on your system. Otherwise do:
$ sudo apt install git
$ sudo apt install make
$ sudo apt install pip
$ sudo apt install python
Install Reticulum:
$ pip install rns --break-system-packages
Add new directories to path:
$ nano ~/.bashrc
Add export PATH=~/.local/bin:~/bin:$PATH
to the end of the file. Exit with CTRL-x
and type y
to save the changes.
Now reload the shell so the changes to the path will take effect: exec $SHELL
. You can also logout and login again.
Download arduino-cli:
$ cd
$ curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
Clone git repo
$ cd
$ git clone https://github.com/liberatedsystems/RNode_Firmware_CE.git
Prepare
Install the required BSP and libraries for the ESP32 system.
$ cd RNode_Firmware_CE/
$ make prep-esp32
Sometimes this command stalls. If this happens, stop the command by hiting CTRL-c
and restarted it.
Test
To test if you can compile the firmware try it:
$ make firmware-heltec32_v3
Be patient, this can take up to 15 minutes to complete. Wait for the command prompt to return. You should not see any errors.
Define a new board
Go to the directory ~/RNode_Firmware_CE
. This is the source code of the RNode firmware. Here you will find all the files needed to compile the firmware. You can edit these files with any text editor you like.
Let's say we want to add support for our homebrew ESP32-S3 with an SX1278 LoRa transceiver. First we have to define a BOARD_MODEL. This is an 8 bit value that is used in the source code to select the right code for the hardware. Let's choose 0x61 as the BOARD_MODEL as this number is not used yet. Optional we can also define a BOARD_VARIANT. This defines the variant of the board. A board could come with different LoRa transceivers, for example. It is also a unique 8 bit number. For now, we ignore this BOARD_VARIANT and only use the BOARD_MODEL.
Makefile
The source code is compiled using make
and the file called Makefile.
Every target board has a section firmware-<board_name>
, upload-<board-name>
and release-<board-name>
. In the Makefile, three new sections for our new board have to be added. The exact place is not critical. But it is good practice to put them alongside the already present sections for firmware, upload and release.
# Added board from Mees Electronics
firmware-mees-esp32-s3:
arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\""
# Added board from Mees Electronics
upload-mees-esp32-s3:
arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:esp32s3
@sleep 1
rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware_CE.ino.bin)
@sleep 3
python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
release-mees-esp32-s3:
arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\""
cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_mees-esp32-s3.boot_app0
cp build/esp32.esp32.mees-esp32-s3/RNode_Firmware_CE.ino.bin build/rnode_firmware_mees-esp32-s3.bin
cp build/esp32.esp32.mees-esp32-s3/RNode_Firmware_CE.ino.bootloader.bin build/rnode_firmware_mees-esp32-s3.bootloader
cp build/esp32.esp32.mees-esp32-s3/RNode_Firmware_CE.ino.partitions.bin build/rnode_firmware_mees-esp32-s3.partitions
zip --junk-paths ./Release/rnode_firmware_mees-esp32-s3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_mees-esp32-s3.boot_app0 build/rnode_firmware_mees-esp32-s3.bin build/rnode_firmware_mees-esp32-s3.bootloader build/rnode_firmware_mees-esp32-s3.partitions
rm -r build
The upload-mees-esp32-s3
section assumes the ESP32 S3 is connected to serial port /dev/ttyACM0. On your system, this could be another port. Manual change all the /dev/ttyACM0
to the port on your system.
Boards.h
The file Board.h
is the most important file to edit. This is the place where the supported boards are defined. For our new board, a new section has to be added. This section defines the pinout of the SPI port and if it has certain peripherals such as a screen or BLE.
First, the new board has to be defined at the beginning of the file. Search for the line #define MODEL_FF 0xFF // Homebrew board, max 14dBm output power
and add the new definition below that line:
// Board added by Mees Electronics
#define BOARD_MEES_ESP32_S3 0x61 // Mees electronics ESP32 S3
Next, search for the line #if MCU_VARIANT == MCU_ESP32
. This is the part where all the ESP32 variant are defined. This section stops at the line #elif MCU_VARIANT == MCU_NRF52
. From here the definitions for the NRF52 board begin.
Somewhere between these to lines we have to add our board definition. It is good practice to add it at the end, just before the following code:
#else
#error An unsupported ESP32 board was selected. Cannot compile RNode firmware.
#endif
The new code:
// Board definition added by Mees Electronics
#elif BOARD_MODEL == BOARD_MEES_ESP32_S3
#define IS_ESP32S3 true
#define HAS_DISPLAY false
#define HAS_BLUETOOTH false
#define HAS_BLE true
#define HAS_PMU true
#define HAS_CONSOLE true
#define HAS_EEPROM true
#define INTERFACE_COUNT 1
#define HAS_NP true
const int pin_np = 21;
#if HAS_NP == false
#if defined(EXTERNAL_LEDS)
const int pin_led_rx = 16;
const int pin_led_tx = 17;
#else
const int pin_led_rx = 21;
const int pin_led_tx = 21;
#endif
#endif
const uint8_t interfaces[INTERFACE_COUNT] = {SX1278};
const bool interface_cfg[INTERFACE_COUNT][3] = {
// SX1278
{
false, // DEFAULT_SPI
false, // HAS_TCXO
false // DIO2_AS_RF_SWITCH
},
};
const int8_t interface_pins[INTERFACE_COUNT][10] = {
// SX1278
{
10, // pin_ss
12, // pin_sclk
11, // pin_mosi
13, // pin_miso
-1, // pin_busy
15, // pin_dio
14, // pin_reset
-1, // pin_txen
-1, // pin_rxen
-1 // pin_tcxo_enable
}
};
A lot of the abilities of the board is defined here. Most of it is self explanatory. The above values should be good enough to get you started.
Name | What does it do |
---|---|
IS_ESP32S3 | Is the board an ESP32 S3 |
HAS_DISPLAY | Has the board a display |
HAS_BLUETOOTH | Has the board Bluetooth |
HAS_BLE | Has the board Bluetooth LE |
HAS_PMU | Unknown yet |
HAS_CONSOLE | RNode Bootstrap Console |
HAS_EEPROM | All ESP32 have EEPROM |
INTERFACE_COUNT | Unknown, set to 1 |
HAS_NP | Has the board a NeoPixel |
EXTERNAL_LEDS | Has the board external LEDs |
The SPI bus is defined in the structs interfaces
, interface_cfg
and interface_pins
. The comments in the code explain it well enough, I think. When the value of interface_cfg[0]
is set to false, we can define all the IO pins in the struct interface_pins
. The numbers are the GPIO numbers of the ESP32 S3. So 10 means GPIO10. If the value is set to -1 the pin is not used. For example, the SX1278 transceiver doesn't have a busy pin, so it is set to -1.
Utilities.h
In this file, the routines for the optional external LEDs are defined. If the new board has a NeoPixel, you don't have to change these. But keep reading, because there are some other functions you have to change even if your board has a NeoPixel!
Search for the line #if MCU_VARIANT == MCU_ESP32
. Because there are several of these lines, make sure you have the right one: the next line should be #if HAS_NP == true
. Here you will find a bunch of #elif BOARD_MODEL ==
lines. Find the last one of this if block, which ends with #endif
. Add the new section for our board just before the #endif
:
#elif BOARD_MODEL == BOARD_MEES_ESP32_S3
void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
void led_id_on() { }
void led_id_off() { }
Search for the code bool eeprom_model_valid() {
. This code checks if the BOARD_MODEL, which is stored in the EEPROM corresponds with the running firmware version. If these are not the same, the firmware will not boot.
After adding our new board section, the code looks something like this:
#if BOARD_MODEL == BOARD_RNODE
if (model == MODEL_A4 || model == MODEL_A9 || model == MODEL_FF || model == MODEL_FE) {
#elif BOARD_MODEL == BOARD_RNODE_NG_20
if (model == MODEL_A3 || model == MODEL_A8) {
#elif BOARD_MODEL == BOARD_RNODE_NG_21
if (model == MODEL_A2 || model == MODEL_A7) {
#elif BOARD_MODEL == BOARD_T3S3
if (model == MODEL_A1 || model == MODEL_A6 || model == MODEL_A5 || model == MODEL_AA || model == MODEL_AC) {
#elif BOARD_MODEL == BOARD_HMBRW
if (model == MODEL_FF || model == MODEL_FE) {
#elif BOARD_MODEL == BOARD_TBEAM
if (model == MODEL_E4 || model == MODEL_E9 || model == MODEL_E3 || model == MODEL_E8) {
#elif BOARD_MODEL == BOARD_TDECK
if (model == MODEL_D4 || model == MODEL_D9) {
#elif BOARD_MODEL == BOARD_TECHO
if (model == MODEL_16 || model == MODEL_17) {
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
if (model == MODEL_DB || model == MODEL_DC) {
#elif BOARD_MODEL == BOARD_LORA32_V1_0
if (model == MODEL_BA || model == MODEL_BB) {
#elif BOARD_MODEL == BOARD_LORA32_V2_0
if (model == MODEL_B3 || model == MODEL_B8) {
#elif BOARD_MODEL == BOARD_LORA32_V2_1
if (model == MODEL_B4 || model == MODEL_B9) {
#elif BOARD_MODEL == BOARD_HELTEC32_V2
if (model == MODEL_C4 || model == MODEL_C9) {
#elif BOARD_MODEL == BOARD_HELTEC32_V3
if (model == MODEL_C5 || model == MODEL_CA) {
#elif BOARD_MODEL == BOARD_H_W_PAPER
if (model == MODEL_C8) {
#elif BOARD_MODEL == BOARD_HELTEC_T114
if (model == MODEL_C6 || model == MODEL_C7) {
#elif BOARD_MODEL == BOARD_RAK4631
if (model == MODEL_11 || model == MODEL_12 || model == MODEL_13 || model == MODEL_14) {
#elif BOARD_MODEL == BOARD_OPENCOM_XL
if (model == MODEL_21) {
#elif BOARD_MODEL == BOARD_HUZZAH32
if (model == MODEL_FF) {
#elif BOARD_MODEL == BOARD_HMBRW
if (model == MODEL_FF || model == MODEL_FE) {
#elif BOARD_MODEL == BOARD_GENERIC_ESP32
if (model == MODEL_FF || model == MODEL_FE) {
// Added by Mees Electronics
#elif BOARD_MODEL == BOARD_MEES_ESP32_S3
if (model == MODEL_A4) {
#else
Last, find the line void setTXPower(RadioInterface* radio, int txp) {
. This is the code that handles the RF power settings of the LoRa transceiver. Here you have to add the proper code for the LoRa module you use. After adding the code for the SX1278 module we use in this example the code should look something like this:
if (model == MODEL_A1) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_A2) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_A3) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
if (model == MODEL_A4) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
if (model == MODEL_A5) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
if (model == MODEL_A6) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_A7) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_A8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_A9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_AA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_AC) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_B3) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_B4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_B8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_B9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_BA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_BB) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_C4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_C9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_C5) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_C6) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
if (model == MODEL_C7) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
if (model == MODEL_CA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_D4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_D9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_DB) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_DC) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_E4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_E9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_E3) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_E8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_FE) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
if (model == MODEL_FF) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
// Added by Mees Electronics
if (model == MODEL_A4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
The MODEL_A4
is an 8 bit value read from the EEPROM. This is the value we will write to the EEPROM later in this document with the command rnodeconf /dev/ttyACM0 -r --platform ESP32 --model a4 --product f0 --hwrev 3
. The model number a4 is chosen from a list published in the file https://git.meezenest.nl/marcel/RNode_Firmware_CE/src/branch/master/Documentation/BUILDING.md. It is shared with another board and it is probably better to choose a unique value. But rnodeconf
uses this value and choosing a new number breaks Reticulum. Further research is needed. But for now let's use a4, because it works.
Display.h
The file display.h also has a board specific definition section. If the new board has a display, make sure there is a proper entry for it. Our example board doesn't have a display, so we don't have to change this file. If your board has a display, make sure you make the proper changes.
Power.h
The file Power.h must also have a board definition. And also proper definitions in void measure_battery() and bool init_pmu(). These are more elaborate and not yet fully understood, but only used when the board has a battery installed. Our example board doesn't have a battery, so we don't have to change this file. If your board has a display, make sure you make the proper changes.
Compiling the code and flashing the board
Make sure you are in the dialout group. If not:
$ sudo usermod -aG dialout <username>
If you are flashing a custom board, you will need to generate a signing key in rnodeconf prior to flashing if you do not already have one by running:
$ rnodeconf -k
Compile the code:
$ make firmware-mees-esp32-s3
Than flash the firmware:
$ make upload-mees-esp32-s3
This first time you flash a new board, this command will end with the error This device has not been provisioned yet, cannot set firmware hash
. That is because the EEPROM is not programmes yet You will need to provision the EEPROM before use:
$ rnodeconf /dev/ttyACM0 -r --platform ESP32 --model a4 --product f0 --hwrev 3
Migrating to another system
When the rnode is flashed, it is signed with a key. If you connect the RNode to another system, this key cannot be validated. It is possible to write a new key to the RNode by erasing the eeprom and writing the local key to it.
$ rnodeconf /dev/ttyACM0 --eeprom-wipe
$ rnodeconf /dev/ttyACM0 -r --platform ESP32 --model a4 --product f0 --hwrev 3
Adding support for SX1268 module
Boards.h
Change the following section:
const uint8_t interfaces[INTERFACE_COUNT] = {SX1278};
const bool interface_cfg[INTERFACE_COUNT][3] = {
// SX1278
{
false, // DEFAULT_SPI
false, // HAS_TCXO
false // DIO2_AS_RF_SWITCH
},
};
const int8_t interface_pins[INTERFACE_COUNT][10] = {
// SX1278
{
10, // pin_ss
12, // pin_sclk
11, // pin_mosi
13, // pin_miso
-1, // pin_busy
15, // pin_dio
14, // pin_reset
-1, // pin_txen
-1, // pin_rxen
-1 // pin_tcxo_enable
}
};
with this:
#if BOARD_VARIANT == MODEL_F1
const uint8_t interfaces[INTERFACE_COUNT] = {SX1278};
const bool interface_cfg[INTERFACE_COUNT][3] = {
// SX1278
{
false, // DEFAULT_SPI
false, // HAS_TCXO
false // DIO2_AS_RF_SWITCH
},
};
const int8_t interface_pins[INTERFACE_COUNT][10] = {
// SX1278
{
10, // pin_ss
12, // pin_sclk
11, // pin_mosi
13, // pin_miso
-1, // pin_busy
15, // pin_dio
14, // pin_reset
-1, // pin_txen
-1, // pin_rxen
-1 // pin_tcxo_enable
}
};
#elif BOARD_VARIANT == MODEL_F2
const uint8_t interfaces[INTERFACE_COUNT] = {SX1262};
const bool interface_cfg[INTERFACE_COUNT][3] = {
// SX1262
{
false, // DEFAULT_SPI
true, // HAS_TCXO
true // DIO2_AS_RF_SWITCH
},
};
const int8_t interface_pins[INTERFACE_COUNT][10] = {
// SX1262
{
10, // pin_ss
12, // pin_sclk
11, // pin_mosi
13, // pin_miso
16, // pin_busy
15, // pin_dio
14, // pin_reset
-1, // pin_txen
17, // pin_rxen
-1 // pin_tcxo_enable
}
};
#endif
Radio.cpp
Add the following statement to the function sx126x::enableTCXO().
// Board added by Mees Electronics
#elif BOARD_MODEL == BOARD_MEES_ESP32_S3
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
Makefile
Change the following section:
# Added board from Mees Electronics
firmware-mees-esp32-s3:
arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\""
with this:
# Added board from Mees Electronics
firmware-mees-esp32-s3:
arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\" \"-DBOARD_VARIANT=0xF2\""
If you want to compile the board for the SX1278 just change BOARD_VARIANT to 0xF1.
License
(C) 2025 M.T. Konstapel https://meezenest.nl/mees
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Many thanks to Tom for proofreading this document.