Arduino Uno to ESP32 radio link
Give an old Arduino Uno a radio: two nRF24L01+ modules carry a light sensor and a button from the Uno to an ESP32, which plots them on the bench and serves them on a web page.
Wiring
| From | To board | Note |
|---|---|---|
| ESP32 nRF24 VCC | 3V3 | 3.3 V only: 5 V destroys it. 10 µF capacitor across VCC and GND, right at the module |
| ESP32 nRF24 GND | GND | |
| ESP32 nRF24 SCK | GPIO 18 | |
| ESP32 nRF24 MISO | GPIO 19 | |
| ESP32 nRF24 MOSI | GPIO 23 | |
| ESP32 nRF24 CSN | GPIO 21 | |
| ESP32 nRF24 CE | GPIO 22 | IRQ stays unconnected |
| Uno nRF24 VCC | Uno 3.3V | Again 3.3 V only, with its own 10 µF capacitor |
| Uno nRF24 GND | Uno GND | |
| Uno nRF24 SCK / MISO / MOSI | Uno 13 / 12 / 11 | The Uno's fixed SPI pins |
| Uno nRF24 CSN / CE | Uno 10 / 9 | |
| Photoresistor | Uno 5V to A0 | 10 kΩ from A0 to GND makes the divider: brighter light, higher number |
| Push button | Uno 2 to GND | Uses the Uno's internal pull-up |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32 Dev Module
// nRF24L01+ receiver on the ESP32: hears an Arduino Uno across the room or garage,
// prints what it sends for the bench plotter, and shows it on a small web page.
// Libraries: RF24 (by TMRh20). The Uno's sketch is further down this page.
#include <SPI.h>
#include <RF24.h>
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_SSID = "your-wifi";
const char* WIFI_PASS = "your-password";
// ---- Pins ----
const int PIN_SCK = 18, PIN_MISO = 19, PIN_MOSI = 23;
const int PIN_CSN = 21, PIN_CE = 22;
// Must match the Uno's sketch exactly.
const uint8_t ADDRESS[6] = "BNCH1";
const uint8_t CHANNEL = 108; // 2.508 GHz, above the Wi-Fi channels
// The packet. Packed and fixed-size so the Uno (8-bit) and the ESP32 (32-bit) agree on its layout.
struct __attribute__((packed)) Packet {
uint32_t seq; // counts up with every packet
uint16_t light; // photoresistor, 0 (dark) to 1023 (bright)
uint8_t button; // 1 while the button is held
};
RF24 radio(PIN_CE, PIN_CSN);
WebServer server(80);
Packet last = {};
uint32_t received = 0, lost = 0;
unsigned long lastHeard = 0;
void handleRoot() {
const unsigned long age = lastHeard ? (millis() - lastHeard) / 1000 : 0;
char page[900];
snprintf(page, sizeof(page),
"<!doctype html><meta name=viewport content='width=device-width'>"
"<meta http-equiv=refresh content=2><title>Radio link</title>"
"<body style='font:18px system-ui;margin:2em'><h1>Uno over nRF24</h1>"
"<p>Light: <b>%u</b> of 1023<p>Button: <b>%s</b>"
"<p>Packets: %lu received, %lu missed<p>%s</body>",
last.light, last.button ? "pressed" : "up", (unsigned long)received, (unsigned long)lost,
lastHeard ? (age < 5 ? "The Uno is talking." : "No packets for a while: is the Uno on?") : "Waiting for the first packet.");
server.send(200, "text/html", page);
}
void handleJson() {
char json[160];
snprintf(json, sizeof(json), "{\"light\":%u,\"button\":%u,\"seq\":%lu,\"received\":%lu,\"lost\":%lu}",
last.light, last.button, (unsigned long)last.seq, (unsigned long)received, (unsigned long)lost);
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CSN);
if (!radio.begin(&SPI)) {
Serial.println("nRF24 not found: check 3.3 V, the 10 uF capacitor and the SPI wires");
while (true) delay(1000);
}
radio.setChannel(CHANNEL);
radio.setDataRate(RF24_250KBPS); // slower, but reaches further and tolerates noise
radio.setPALevel(RF24_PA_LOW); // raise to RF24_PA_HIGH for longer range
radio.setPayloadSize(sizeof(Packet));
radio.openReadingPipe(1, ADDRESS);
radio.startListening();
Serial.println("radio listening");
WiFi.begin(WIFI_SSID, WIFI_PASS);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
if (WiFi.status() == WL_CONNECTED) {
server.on("/", handleRoot);
server.on("/data.json", handleJson);
server.begin();
Serial.printf("web page at http://%s/\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("Wi-Fi not joined; the radio still works on the serial port");
}
}
void loop() {
if (WiFi.status() == WL_CONNECTED) server.handleClient();
while (radio.available()) {
Packet p;
radio.read(&p, sizeof(p));
if (received > 0 && p.seq > last.seq + 1) lost += p.seq - last.seq - 1;
last = p;
received++;
lastHeard = millis();
Serial.printf("light = %u button = %u\n", p.light, p.button);
}
static unsigned long lastReport = 0;
if (millis() - lastReport >= 10000) {
lastReport = millis();
Serial.printf("missed = %lu\n", (unsigned long)lost);
}
}
Wiring
| From | To board | Note |
|---|---|---|
| ESP32 nRF24 VCC | 3V3 | 3.3 V only: 5 V destroys it. 10 µF capacitor across VCC and GND, right at the module |
| ESP32 nRF24 GND | GND | |
| ESP32 nRF24 SCK | GPIO 12 | |
| ESP32 nRF24 MISO | GPIO 13 | |
| ESP32 nRF24 MOSI | GPIO 11 | |
| ESP32 nRF24 CSN | GPIO 10 | |
| ESP32 nRF24 CE | GPIO 9 | IRQ stays unconnected |
| Uno nRF24 VCC | Uno 3.3V | Again 3.3 V only, with its own 10 µF capacitor |
| Uno nRF24 GND | Uno GND | |
| Uno nRF24 SCK / MISO / MOSI | Uno 13 / 12 / 11 | The Uno's fixed SPI pins |
| Uno nRF24 CSN / CE | Uno 10 / 9 | |
| Photoresistor | Uno 5V to A0 | 10 kΩ from A0 to GND makes the divider: brighter light, higher number |
| Push button | Uno 2 to GND | Uses the Uno's internal pull-up |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32S3 Dev Module
// nRF24L01+ receiver on the ESP32: hears an Arduino Uno across the room or garage,
// prints what it sends for the bench plotter, and shows it on a small web page.
// Libraries: RF24 (by TMRh20). The Uno's sketch is further down this page.
#include <SPI.h>
#include <RF24.h>
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_SSID = "your-wifi";
const char* WIFI_PASS = "your-password";
// ---- Pins ----
const int PIN_SCK = 12, PIN_MISO = 13, PIN_MOSI = 11;
const int PIN_CSN = 10, PIN_CE = 9;
// Must match the Uno's sketch exactly.
const uint8_t ADDRESS[6] = "BNCH1";
const uint8_t CHANNEL = 108; // 2.508 GHz, above the Wi-Fi channels
// The packet. Packed and fixed-size so the Uno (8-bit) and the ESP32 (32-bit) agree on its layout.
struct __attribute__((packed)) Packet {
uint32_t seq; // counts up with every packet
uint16_t light; // photoresistor, 0 (dark) to 1023 (bright)
uint8_t button; // 1 while the button is held
};
RF24 radio(PIN_CE, PIN_CSN);
WebServer server(80);
Packet last = {};
uint32_t received = 0, lost = 0;
unsigned long lastHeard = 0;
void handleRoot() {
const unsigned long age = lastHeard ? (millis() - lastHeard) / 1000 : 0;
char page[900];
snprintf(page, sizeof(page),
"<!doctype html><meta name=viewport content='width=device-width'>"
"<meta http-equiv=refresh content=2><title>Radio link</title>"
"<body style='font:18px system-ui;margin:2em'><h1>Uno over nRF24</h1>"
"<p>Light: <b>%u</b> of 1023<p>Button: <b>%s</b>"
"<p>Packets: %lu received, %lu missed<p>%s</body>",
last.light, last.button ? "pressed" : "up", (unsigned long)received, (unsigned long)lost,
lastHeard ? (age < 5 ? "The Uno is talking." : "No packets for a while: is the Uno on?") : "Waiting for the first packet.");
server.send(200, "text/html", page);
}
void handleJson() {
char json[160];
snprintf(json, sizeof(json), "{\"light\":%u,\"button\":%u,\"seq\":%lu,\"received\":%lu,\"lost\":%lu}",
last.light, last.button, (unsigned long)last.seq, (unsigned long)received, (unsigned long)lost);
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CSN);
if (!radio.begin(&SPI)) {
Serial.println("nRF24 not found: check 3.3 V, the 10 uF capacitor and the SPI wires");
while (true) delay(1000);
}
radio.setChannel(CHANNEL);
radio.setDataRate(RF24_250KBPS); // slower, but reaches further and tolerates noise
radio.setPALevel(RF24_PA_LOW); // raise to RF24_PA_HIGH for longer range
radio.setPayloadSize(sizeof(Packet));
radio.openReadingPipe(1, ADDRESS);
radio.startListening();
Serial.println("radio listening");
WiFi.begin(WIFI_SSID, WIFI_PASS);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
if (WiFi.status() == WL_CONNECTED) {
server.on("/", handleRoot);
server.on("/data.json", handleJson);
server.begin();
Serial.printf("web page at http://%s/\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("Wi-Fi not joined; the radio still works on the serial port");
}
}
void loop() {
if (WiFi.status() == WL_CONNECTED) server.handleClient();
while (radio.available()) {
Packet p;
radio.read(&p, sizeof(p));
if (received > 0 && p.seq > last.seq + 1) lost += p.seq - last.seq - 1;
last = p;
received++;
lastHeard = millis();
Serial.printf("light = %u button = %u\n", p.light, p.button);
}
static unsigned long lastReport = 0;
if (millis() - lastReport >= 10000) {
lastReport = millis();
Serial.printf("missed = %lu\n", (unsigned long)lost);
}
}
Wiring
| From | To board | Note |
|---|---|---|
| ESP32 nRF24 VCC | 3V3 | 3.3 V only: 5 V destroys it. 10 µF capacitor across VCC and GND, right at the module |
| ESP32 nRF24 GND | GND | |
| ESP32 nRF24 SCK | GPIO 4 | |
| ESP32 nRF24 MISO | GPIO 5 | |
| ESP32 nRF24 MOSI | GPIO 6 | |
| ESP32 nRF24 CSN | GPIO 7 | |
| ESP32 nRF24 CE | GPIO 10 | IRQ stays unconnected |
| Uno nRF24 VCC | Uno 3.3V | Again 3.3 V only, with its own 10 µF capacitor |
| Uno nRF24 GND | Uno GND | |
| Uno nRF24 SCK / MISO / MOSI | Uno 13 / 12 / 11 | The Uno's fixed SPI pins |
| Uno nRF24 CSN / CE | Uno 10 / 9 | |
| Photoresistor | Uno 5V to A0 | 10 kΩ from A0 to GND makes the divider: brighter light, higher number |
| Push button | Uno 2 to GND | Uses the Uno's internal pull-up |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32C3 Dev Module
// nRF24L01+ receiver on the ESP32: hears an Arduino Uno across the room or garage,
// prints what it sends for the bench plotter, and shows it on a small web page.
// Libraries: RF24 (by TMRh20). The Uno's sketch is further down this page.
#include <SPI.h>
#include <RF24.h>
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_SSID = "your-wifi";
const char* WIFI_PASS = "your-password";
// ---- Pins ----
const int PIN_SCK = 4, PIN_MISO = 5, PIN_MOSI = 6;
const int PIN_CSN = 7, PIN_CE = 10;
// Must match the Uno's sketch exactly.
const uint8_t ADDRESS[6] = "BNCH1";
const uint8_t CHANNEL = 108; // 2.508 GHz, above the Wi-Fi channels
// The packet. Packed and fixed-size so the Uno (8-bit) and the ESP32 (32-bit) agree on its layout.
struct __attribute__((packed)) Packet {
uint32_t seq; // counts up with every packet
uint16_t light; // photoresistor, 0 (dark) to 1023 (bright)
uint8_t button; // 1 while the button is held
};
RF24 radio(PIN_CE, PIN_CSN);
WebServer server(80);
Packet last = {};
uint32_t received = 0, lost = 0;
unsigned long lastHeard = 0;
void handleRoot() {
const unsigned long age = lastHeard ? (millis() - lastHeard) / 1000 : 0;
char page[900];
snprintf(page, sizeof(page),
"<!doctype html><meta name=viewport content='width=device-width'>"
"<meta http-equiv=refresh content=2><title>Radio link</title>"
"<body style='font:18px system-ui;margin:2em'><h1>Uno over nRF24</h1>"
"<p>Light: <b>%u</b> of 1023<p>Button: <b>%s</b>"
"<p>Packets: %lu received, %lu missed<p>%s</body>",
last.light, last.button ? "pressed" : "up", (unsigned long)received, (unsigned long)lost,
lastHeard ? (age < 5 ? "The Uno is talking." : "No packets for a while: is the Uno on?") : "Waiting for the first packet.");
server.send(200, "text/html", page);
}
void handleJson() {
char json[160];
snprintf(json, sizeof(json), "{\"light\":%u,\"button\":%u,\"seq\":%lu,\"received\":%lu,\"lost\":%lu}",
last.light, last.button, (unsigned long)last.seq, (unsigned long)received, (unsigned long)lost);
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CSN);
if (!radio.begin(&SPI)) {
Serial.println("nRF24 not found: check 3.3 V, the 10 uF capacitor and the SPI wires");
while (true) delay(1000);
}
radio.setChannel(CHANNEL);
radio.setDataRate(RF24_250KBPS); // slower, but reaches further and tolerates noise
radio.setPALevel(RF24_PA_LOW); // raise to RF24_PA_HIGH for longer range
radio.setPayloadSize(sizeof(Packet));
radio.openReadingPipe(1, ADDRESS);
radio.startListening();
Serial.println("radio listening");
WiFi.begin(WIFI_SSID, WIFI_PASS);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
if (WiFi.status() == WL_CONNECTED) {
server.on("/", handleRoot);
server.on("/data.json", handleJson);
server.begin();
Serial.printf("web page at http://%s/\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("Wi-Fi not joined; the radio still works on the serial port");
}
}
void loop() {
if (WiFi.status() == WL_CONNECTED) server.handleClient();
while (radio.available()) {
Packet p;
radio.read(&p, sizeof(p));
if (received > 0 && p.seq > last.seq + 1) lost += p.seq - last.seq - 1;
last = p;
received++;
lastHeard = millis();
Serial.printf("light = %u button = %u\n", p.light, p.button);
}
static unsigned long lastReport = 0;
if (millis() - lastReport >= 10000) {
lastReport = millis();
Serial.printf("missed = %lu\n", (unsigned long)lost);
}
}
Transmitter, Arduino sketch for the Uno
// nRF24L01+ transmitter on an Arduino Uno: sends the photoresistor and a button
// to the ESP32 twice a second, and straight away when the button changes.
// Libraries: RF24 (by TMRh20). Board: Arduino Uno.
#include <SPI.h>
#include <RF24.h>
const int PIN_CE = 9, PIN_CSN = 10; // SPI itself is fixed on the Uno: 11 MOSI, 12 MISO, 13 SCK
const int LIGHT_PIN = A0; // photoresistor divider
const int BUTTON_PIN = 2; // button to GND
const uint8_t ADDRESS[6] = "BNCH1";
const uint8_t CHANNEL = 108;
struct __attribute__((packed)) Packet {
uint32_t seq;
uint16_t light;
uint8_t button;
};
RF24 radio(PIN_CE, PIN_CSN);
Packet p = {};
unsigned long lastSend = 0;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
if (!radio.begin()) {
Serial.println(F("nRF24 not found: check 3.3 V, the 10 uF capacitor and the SPI wires"));
while (true) {}
}
radio.setChannel(CHANNEL);
radio.setDataRate(RF24_250KBPS);
radio.setPALevel(RF24_PA_LOW);
radio.setPayloadSize(sizeof(Packet));
radio.openWritingPipe(ADDRESS);
radio.stopListening();
Serial.println(F("radio sending"));
}
void loop() {
const uint8_t button = digitalRead(BUTTON_PIN) == LOW;
if (button != p.button || millis() - lastSend >= 500) {
lastSend = millis();
p.seq++;
p.light = analogRead(LIGHT_PIN);
p.button = button;
const bool ok = radio.write(&p, sizeof(p)); // true when the ESP32 acknowledged it
Serial.print(F("sent "));
Serial.print(p.seq);
Serial.println(ok ? F(" ok") : F(" no ack"));
}
}
Parts
| Quantity | Part | Where to buy |
|---|---|---|
| 1× | ESP32 dev board | Buy at Amazon |
| 1× | Arduino Uno | |
| 2× | nRF24L01+ modules | |
| 2× | 10 µF capacitors, one across each module's VCC and GND | |
| 1× | Photoresistor (light sensor) and a 10 kΩ resistor | |
| 1× | Push button | |
| 16× | Jumper wires |
Amazon buy links may include an Associates tag, and this site may earn a commission from qualifying Amazon purchases at no extra cost to you. Adafruit links are direct.
Libraries
RF24, WiFi (built in), WebServer (built in)
Load it
- In the Arduino IDE, install the esp32 board package and pick the board named above the sketch.
- Install the libraries listed above from the Library Manager.
- Paste the sketch, choose the board's port and upload. Hold BOOT if the upload stalls at Connecting.
- Close the IDE's serial monitor, then open the bench here to watch the output.
Field notes
- Upload the ESP32 sketch to the ESP32 and the Uno sketch (further down) to the Uno. Both use the RF24 library by TMRh20 from the Library Manager. The ESP32 prints its web address on the bench; open it on a phone to watch the light level.
- nRF24L01+ modules run on 3.3 V only. The Uno's 3.3V pin can supply a plain module like these (around 12 mA while sending), but not the long-range versions with an antenna and amplifier, which need their own 3.3 V regulator.
- The Uno's signals are 5 V. Nordic's datasheet lists the nRF24L01+ inputs as 5 V tolerant, and wiring them straight to an Uno is how most guides do it, but it is the module's supply, not its inputs, that must stay at 3.3 V. If a module behaves oddly, a level shifter on SCK, MOSI, CSN and CE is the careful fix.
- Most nRF24 trouble is power: the module draws short bursts that a long jumper cannot supply. The 10 µF capacitor right across VCC and GND fixes most 'not found' and dropped-packet problems.
- Both sketches use channel 108 (2.508 GHz), above the Wi-Fi channels, and 250 kbit/s, which reaches further than the default. Change CHANNEL on both if you run two links side by side.
- The packet is a packed struct with fixed-size fields, so the 8-bit Uno and the 32-bit ESP32 agree on its layout. Keep the two structs identical if you add fields.
- On the bench, light and button are charted as they arrive, and missed counts packets that never made it.
Not tested yet
- Not tested on hardware yet. The ESP32 sketch compiles for every board listed with Arduino-ESP32 core 3.3.12, and the Uno sketch compiles for the Arduino Uno (PlatformIO Atmel AVR 5.3.0).
Community builds
Loading build reports from people who made this.
Built it? Send a build report with your board and what the serial port printed. Working reports become the "Tested on this board" entries above.