The Core Dilemma: Microcontroller vs Microprocessor
When makers ask 'Arduino vs RPi: which is better?', they are usually asking the wrong question. The Arduino is a microcontroller designed for deterministic, low-power, bare-metal hardware I/O. The Raspberry Pi is a microprocessor running a full Linux OS, built for heavy compute, networking, and multitasking. If your project requires reading analog sensors with microsecond precision while simultaneously pushing data to an MQTT broker, the answer isn't one or the other—it's both.
Linux is not a real-time operating system. If you try to poll an analog-to-digital converter (ADC) directly from a Raspberry Pi using an SPI or I2C ADC chip, background OS tasks will introduce jitter, causing you to miss samples. By offloading the hard real-time sensor polling to an Arduino and passing the aggregated data to the Pi via I2C, you get the best of both worlds.
Hardware Specification Comparison (2026 Benchmarks)
| Feature | Arduino Nano 33 IoT | Raspberry Pi 5 (4GB) |
|---|---|---|
| Core Architecture | SAMD21 Cortex-M0+ (32-bit ARM) | BCM2712 Quad-core Cortex-A76 (64-bit ARM) |
| Clock Speed | 48 MHz | 2.4 GHz |
| RAM | 32 KB SRAM | 4 GB LPDDR4X |
| Native ADC | 12-bit (up to 350 kSPS) | None (Requires external I2C/SPI ADC) |
| I2C Hardware Blocks | 1 (Shared with external pins) | 3 (1 dedicated to HAT, 2 on GPIO) |
| Operating System | None (Bare-metal / RTOS) | Debian-based Linux (Bookworm) |
| Typical Active Power | ~45 mW | ~2.5 W to 8 W |
| Approx. Board Price | $22.00 | $60.00 |
Sources: Arduino Nano 33 IoT Official Docs, Raspberry Pi 5 Hardware Specs.
Parts List and Pin Mapping
For this build, we are creating a hybrid data logger. The Arduino reads an analog thermistor and acts as an I2C slave. The Raspberry Pi acts as the I2C master, requesting data every second and logging it.
Bill of Materials (BOM)
- Microcontroller: Arduino Nano 33 IoT (with headers soldered)
- Microprocessor: Raspberry Pi 5 (4GB model) running Raspberry Pi OS Bookworm
- Sensor: 10k NTC Thermistor + 10k precision pull-down resistor
- Passives: Two 4.7kΩ pull-up resistors (critical for I2C stability)
- Wiring: 22 AWG solid core jumper wires
I2C Pin Mapping Table
| Signal | Arduino Nano 33 IoT Pin | Raspberry Pi 5 GPIO (Physical Pin) | Notes |
|---|---|---|---|
| SDA (Data) | A4 | GPIO 2 (Pin 3) | Requires 4.7kΩ pull-up to 3.3V |
| SCL (Clock) | A5 | GPIO 3 (Pin 5) | Requires 4.7kΩ pull-up to 3.3V |
| GND | GND | GND (Pin 6) | Common ground is mandatory |
| VCC (3.3V) | 3V3 | 3V3 Power (Pin 1) | Powers the pull-up resistors |
The Arduino I2C Slave Code
This code targets the Arduino Nano 33 IoT. It reads the analog voltage from a thermistor voltage divider on pin A0, converts it to a temperature in Celsius, and formats it into a byte array to send to the Pi when requested.
Gotcha Warning: The Nano 33 IoT only has one hardware I2C bus exposed on A4/A5. If you try to attach an I2C sensor (like a BME280) to the same bus while the Arduino is acting as an I2C slave, bus collisions will occur. This is why we use an analog sensor for the primary reading in this architecture.
// Target Board: Arduino Nano 33 IoT
// I2C Slave Sensor Hub
#include <Wire.h>
#include <math.h>
#define I2C_SLAVE_ADDRESS 0x08
#define THERMISTOR_PIN A0
#define SERIES_RESISTOR 10000.0
#define THERMISTOR_NOMINAL 10000.0
#define TEMPERATURE_NOMINAL 25.0
#define B_COEFFICIENT 3950.0
#define ADC_RESOLUTION 1023.0
volatile float currentTempC = 0.0;
volatile bool dataReady = false;
void setup() {
Serial.begin(115200);
pinMode(THERMISTOR_PIN, INPUT);
// Initialize I2C as slave
Wire.begin(I2C_SLAVE_ADDRESS);
Wire.onRequest(requestEvent);
// Force 100kHz standard mode to prevent Pi clock-stretching bugs
Wire.setClock(100000);
Serial.println('Arduino Nano 33 IoT I2C Slave initialized.');
}
void loop() {
// Read analog sensor and calculate temperature
float reading = analogRead(THERMISTOR_PIN);
// Prevent divide-by-zero error if sensor is disconnected
if (reading < 1 || reading >= ADC_RESOLUTION) {
currentTempC = -999.0; // Error flag value
} else {
float resistance = SERIES_RESISTOR * ((ADC_RESOLUTION / reading) - 1.0);
float steinhart;
steinhart = resistance / THERMISTOR_NOMINAL; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= B_COEFFICIENT; // 1/B * ln(R/Ro)
steinhart += 1.0 / (TEMPERATURE_NOMINAL + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert to C
currentTempC = steinhart;
}
dataReady = true;
delay(500); // Poll twice a second
}
// Interrupt Service Routine for I2C Master Request
void requestEvent() {
// Convert float to 4-byte array for I2C transmission
byte payload[4];
memcpy(payload, ¤tTempC, sizeof(float));
// Error handling: check if Wire buffer can accept the payload
if (Wire.write(payload, 4) != 4) {
Serial.println('I2C TX Buffer Error: Failed to write 4 bytes.');
}
dataReady = false;
}
Debugging the I2C Bus: Errors and Fixes
When you write the Python master script on the Raspberry Pi using the smbus2 library, the most common failure you will encounter is the dreaded I2C NACK or timeout. If your Python script crashes with this exact error string:
OSError: [Errno 121] Remote I/O error
Or if your Arduino serial monitor prints Wire: NACK received, do not panic. This is almost always a physical layer or OS configuration issue, not a code logic bug. Here are the first three things to check, ranked by probability:
- Missing or Inadequate Pull-Up Resistors: The Raspberry Pi's internal I2C pull-ups are roughly 50kΩ. This is far too weak for reliable communication at 100kHz, especially with the capacitance added by jumper wires. You must solder or breadboard external 4.7kΩ resistors between SDA and 3.3V, and SCL and 3.3V. Measure the bus with a multimeter; both lines should read exactly 3.3V when idle.
- Missing Common Ground: I2C is a single-ended protocol referenced to ground. If you are powering the Arduino from your laptop's USB and the Pi from its official 27W USB-C supply, their grounds might be floating relative to each other. Always run a dedicated GND wire between the Pi and the Arduino.
- Raspberry Pi I2C Baudrate Mismatch: The Pi 5 running Bookworm OS sometimes defaults to an I2C baudrate that causes clock-stretching timeouts with slower microcontrollers. You must cap the Pi's I2C speed. Open your Pi's boot config file via terminal:
sudo nano /boot/firmware/config.txt(note: on older Pi OS versions this was/boot/config.txt). Add this line to the bottom:
dtparam=i2c_baudrate=100000
Reboot the Pi. This forces the Pi to respect the 100kHz standard mode we defined in the Arduino code.
i2cdetect -y 1. You should see 08 in the grid. If the grid is empty, you have a wiring or pull-up resistor issue. If the grid shows UU, the Pi's OS already has a driver claiming that address.
Extending and Simplifying the Build
Once your baseline hybrid hub is passing temperature data reliably, you have two distinct paths for project evolution depending on your end goal.
How to Extend (Scale Up)
If you need to monitor a server rack or a greenhouse with multiple analog sensors, do not try to wire multiple sensors directly to the Pi. Instead, add an analog multiplexer like the CD74HC4067 (16-channel) to the Arduino. The Arduino cycles through the 16 channels, reads the ADC, and stores the array in its SRAM. When the Pi requests data via I2C, the Arduino sends a 64-byte payload containing all 16 float values. The Pi then uses its heavy compute power to run a local SQLite database and a Grafana dashboard to visualize the thermal gradients over time.
How to Simplify (Scale Down)
If your only reason for including the Raspberry Pi was to push the sensor data to Wi-Fi or an MQTT broker, you are over-engineering the build. Drop the Pi entirely and swap the Arduino Nano 33 IoT for an ESP32-WROOM-32 development board (approx. $6). The ESP32 has a 12-bit ADC, native Wi-Fi/BLE, and runs bare-metal C++ just like the Arduino. You eliminate the I2C bus complexity, the Linux OS overhead, and the $60 Pi cost, reducing the BOM to under $10 while maintaining sub-millisecond sensor polling.
Choosing between an Arduino and a Raspberry Pi isn't about finding a universal winner; it's about matching the silicon to the specific timing and compute constraints of your subsystem. Use the microcontroller for the physical world, and the microprocessor for the data world.






