The Short Answer: C++ vs. Python on Arduino Hardware

Standard AVR-based Arduino boards (like the classic Uno R3) do not use Python; they run C/C++ compiled via the Arduino IDE. The ATmega328P chip has only 2KB of SRAM, which is physically insufficient to run a Python interpreter. However, modern 32-bit Arduino boards built on ARM or ESP architectures—specifically the Arduino Nano ESP32 and Nano RP2040 Connect—natively support Python via MicroPython and CircuitPython.

If you want to write Python code that executes directly on the microcontroller, you must use one of these 32-bit boards. If you are stuck with a classic 8-bit Uno, Python can only run on your host PC (using libraries like pySerial or Firmata) to send commands to the board over a USB serial connection.

Bench Tip: When migrating from C++ to MicroPython, remember that Python relies on garbage collection. On memory-constrained 32-bit boards, you must manually trigger gc.collect() in your main loop to prevent heap fragmentation crashes during long-term sensor logging.

Hardware Spec Sheet: Which Arduino Boards Run Python?

Not all boards wearing the Arduino logo can run Python. The interpreter requires a minimum of 64KB of SRAM to initialize the heap and manage garbage collection, plus at least 1MB of Flash to store the firmware. Here is how the current lineup stacks up for Python development.

Board Variant Microcontroller Flash / SRAM Python Compatibility Clock Speed
Arduino Uno R3 ATmega328P (8-bit AVR) 32KB / 2KB None (C++ only) 16 MHz
Arduino Uno R4 Minima Renesas RA4M1 (ARM Cortex-M4) 256KB / 32KB Experimental (C++ primary) 48 MHz
Arduino Nano RP2040 Connect Raspberry Pi RP2040 (ARM Cortex-M0+) 16MB / 264KB Native (MicroPython / CircuitPython) 133 MHz
Arduino Nano ESP32 (ABX00092) ESP32-S3 (Xtensa LX7 Dual-Core) 8MB / 512KB (+2MB PSRAM) Native (MicroPython / CircuitPython) 240 MHz

Sources: Arduino MicroPython Documentation, MicroPython ESP32 Quick Reference.

Project Build: MicroPython Environmental Node on Nano ESP32

For this build, we are targeting the Arduino Nano ESP32 (ABX00092). This board is ideal because the ESP32-S3 includes native USB, making it trivial to mount the board as a drive and drag-and-drop MicroPython scripts without needing a dedicated hardware programmer.

Parts List

  • Microcontroller: Arduino Nano ESP32 (with headers)
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652)
  • Wiring: 4x M/M jumper wires, half-size breadboard
  • Power: USB-C cable (data + power)

Pin Mapping Table

The Nano ESP32 silkscreen labels the I2C pins as A4 (SDA) and A5 (SCL). Under the hood in MicroPython, these map to specific ESP32-S3 GPIO numbers. Always use the GPIO numbers in your machine.Pin definitions.

Nano ESP32 Silkscreen ESP32-S3 GPIO Number BME280 Breakout Pin Function
3V3 N/A (Power Rail) VIN 3.3V Logic Power
GND N/A (Ground) GND Common Ground
A4 GPIO 11 SDI (SDA) I2C Data
A5 GPIO 12 SCK (SCL) I2C Clock
Voltage Warning: The Arduino Nano ESP32 is strictly a 3.3V logic device. Do not connect the BME280 VIN pin to the 5V rail. While the BME280 can tolerate 5V power, feeding 5V into the SDA/SCL pins will fry the ESP32-S3 GPIO pads. Always use the 3V3 pin.

Complete MicroPython Code

This script initializes the I2C bus, scans for the BME280, reads the raw chip ID to verify communication, and toggles the onboard RGB LED if the temperature exceeds a threshold. It includes robust try/except error handling for I2C failures.


import machine
import time
import gc

# --- Pin Definitions for Arduino Nano ESP32 ---
I2C_SDA = 11  # Silkscreen A4
I2C_SCL = 12  # Silkscreen A5
BME_ADDR = 0x76  # Default Adafruit BME280 I2C address
LED_PIN = 48    # Nano ESP32 onboard RGB LED (Green channel)

# Initialize I2C and LED
i2c = machine.I2C(0, scl=machine.Pin(I2C_SCL), sda=machine.Pin(I2C_SDA), freq=400000)
led = machine.Pin(LED_PIN, machine.Pin.OUT)

def scan_i2c():
    devices = i2c.scan()
    if not devices:
        raise RuntimeError("No I2C devices found. Check wiring.")
    return devices

def read_chip_id():
    # BME280 Register 0xD0 contains the Chip ID (should be 0x60)
    try:
        chip_id = i2c.readfrom_mem(BME_ADDR, 0xD0, 1)[0]
        return chip_id
    except OSError as e:
        raise RuntimeError(f"Failed to read Chip ID: {e}")

def main():
    print("Starting MicroPython Environmental Node...")
    gc.collect()
    
    try:
        devices = scan_i2c()
        print(f"I2C Devices found at: {[hex(d) for d in devices]}")
        
        if BME_ADDR not in devices:
            raise RuntimeError(f"BME280 not found at {hex(BME_ADDR)}. Check address jumper.")
            
        chip_id = read_chip_id()
        print(f"BME280 Chip ID: {hex(chip_id)} (Expected: 0x60)")
        
        if chip_id != 0x60:
            print("Warning: Unexpected Chip ID. Sensor may be faulty.")
            
    except Exception as e:
        print(f"FATAL INIT ERROR: {e}")
        # Blink LED rapidly to indicate hardware fault
        while True:
            led.value(not led.value())
            time.sleep(0.1)

    # Main Loop
    print("Entering main loop. Press Ctrl+C to stop.")
    while True:
        try:
            # Mock temperature read for demonstration (Replace with full BME280 driver in production)
            # In a real build, import the bme280 library and call bme.values
            mock_temp = 26.5 
            print(f"Simulated Temp: {mock_temp}C")
            
            if mock_temp > 25.0:
                led.value(1)  # Turn on LED if hot
            else:
                led.value(0)  # Turn off LED if cool
                
            gc.collect()  # Prevent heap fragmentation
            time.sleep(2.0)
            
        except KeyboardInterrupt:
            print("Script interrupted by user.")
            led.value(0)
            break
        except Exception as e:
            print(f"Loop Error: {e}")
            time.sleep(1.0)

if __name__ == "__main__":
    main()

Debugging: Fixing the OSError: [Errno 19] ENODEV I2C Failure

When working with I2C on the bench, the most common error you will encounter in MicroPython is the OSError: [Errno 19] ENODEV. This exact string means the microcontroller sent an I2C address over the bus, but no peripheral pulled the SDA line low to acknowledge (ACK) it.

Ranked Causes and Fixes

  1. Missing or Weak Pull-up Resistors: I2C is an open-drain protocol. It requires pull-up resistors on SDA and SCL. The Adafruit BME280 breakout includes 10k pull-ups, which are sufficient for short runs. If you are using a raw BME280 chip on a custom PCB without pull-ups, the bus will float, causing Errno 19. Fix: Add 4.7k resistors from SDA/SCL to 3.3V.
  2. I2C Address Mismatch: The BME280 can sit at 0x76 or 0x77 depending on the state of the SDO pin. Adafruit defaults to 0x77, while many generic Amazon/eBay clones default to 0x76. Fix: Check the silkscreen on your breakout board and update the BME_ADDR variable in the code.
  3. Swapped SDA/SCL Lines: Unlike UART, I2C will not auto-correct if you swap data and clock. Fix: Verify GPIO 11 is strictly wired to SDI (SDA) and GPIO 12 to SCK (SCL).

The First Three Things to Check When It Fails

Before rewriting your code, run this physical and logical checklist:

  1. Run an I2C Scan: Open the MicroPython REPL and type i2c.scan(). If it returns an empty list [], your issue is 100% physical wiring or power.
  2. Verify VCC Logic Level: Put your multimeter in DC voltage mode. Probe the VIN pin on the BME280. It must read between 3.0V and 3.6V. If it reads 0V, you forgot the ground wire or the 3V3 rail.
  3. Check the Solder Joints: The Nano ESP32 header pins are notorious for cold solder joints if you hand-soldered them. Wiggle the board gently while watching the REPL output. If the error intermittently clears, reflow your header pins with fresh flux and solder.

Extending and Simplifying the Build

Once you have the basic I2C handshake working, you can scale this project up or down depending on your deployment needs.

How to Simplify (No External Sensors)

If you just want to test MicroPython on the Nano ESP32 without buying a BME280, use the ESP32-S3's internal temperature sensor. Replace the I2C initialization block with the internal esp32 module:


import esp32
temp_f = esp32.raw_temperature()
temp_c = (temp_f - 32) * 5.0 / 9.0
print(f"Internal Die Temp: {temp_c:.2f} C")

Note: The internal sensor measures the silicon die temperature, which runs 5-10°C hotter than ambient room temperature. It is useful for monitoring CPU load, not room climate.

How to Extend (WiFi and MQTT)

The primary advantage of the Nano ESP32 over the RP2040 Connect is its robust WiFi stack. To push your sensor data to Home Assistant or a cloud dashboard, import the network and umqtt.simple modules.

  1. Connect to your local 2.4GHz WiFi network using network.WLAN(network.STA_IF).
  2. Instantiate an MQTT client: client = MQTTClient('nano_env_node', '192.168.1.50').
  3. In your main loop, publish the payload: client.publish('home/livingroom/temp', str(mock_temp)).

When extending to WiFi, power consumption jumps from ~20mA to ~180mA during transmission. If you are running this off a 2000mAh LiPo battery, you must implement deep sleep (machine.deepsleep(60000)) between reads to achieve more than a few hours of battery life. For continuous mains-powered logging, keep the WiFi radio active and disable deep sleep to avoid connection dropout delays.

For more advanced sensor wiring diagrams and I2C troubleshooting, refer to the Adafruit BME280 Learning Guide and the official Arduino MicroPython documentation.