MicroPython I²C on ESP32: Scan for Devices and Fix “No Device Found”
I²C is a two-wire bus commonly used to connect displays, sensors, real-time clocks, ADCs, and other peripherals to a microcontroller. With MicroPython on an ESP32, you can create an I²C bus and scan for device addresses that acknowledge communication. This is a fast way to check an OLED or sensor before debugging a larger project.
A scan is a diagnostic, not a complete hardware test. A correctly wired device may remain silent because it is unpowered, held in reset, using a different bus, or not ready to respond. This guide explains safe wiring, the MicroPython API, address interpretation, common failures, and what to test after detection.
1. Understand I²C
I²C normally uses two signal lines: SDA carries data and SCL carries the clock. Devices share the lines and are selected by an address. The bus uses open-drain signalling, so pull-up resistors bring the lines high when no device pulls them low. Many breakout boards include pull-ups, but not all do; multiple boards may also place their pull-ups in parallel.
Common peripherals use 7-bit addresses, often shown in hexadecimal, such as 0x3C or 0x68. An address identifies a device on the bus, not necessarily a unique model. Some devices allow address selection using a jumper or pin. A scan will not find devices that communicate over SPI, UART, or another interface.
ESP32 boards generally use 3.3 V logic. Do not connect 5 V pull-ups directly to ESP32 SDA or SCL. Check the breakout schematic and datasheet, especially if its power input accepts 5 V: that does not guarantee its I²C lines are level-shifted.
2. Wire one known device
For the first test, use one known I²C breakout. Connect GND to GND, the module's supported supply to the correct ESP32 supply, SDA to the selected SDA GPIO, and SCL to the selected SCL GPIO. Verify pin order on the module itself; header layouts vary.
The examples use GPIO 21 for SDA and GPIO 22 for SCL because these are common choices on classic ESP32 development boards. They are not universal defaults. Check your exact ESP32 variant and board schematic. GPIO availability differs among ESP32, ESP32-C3, ESP32-S3, and other variants, and some pins are reserved for flash or onboard functions.
Disconnect USB before changing wires. Check for shorts and confirm a common ground. Keep leads short during diagnosis. If external pull-ups are needed, select them according to bus voltage, capacitance, speed, and device specifications; do not add them blindly when breakouts already contain pull-ups.
3. Confirm MicroPython
Connect the board using a data-capable USB cable and open a MicroPython REPL in Thonny or a similar tool. Test the hardware module:
import machine
print("I2C available:", hasattr(machine, "I2C"))
MicroPython commonly supports hardware I²C through machine.I2C on ESP32 ports. Constructor options and available bus IDs depend on firmware release and chip family. If you get an import or constructor error, check the documentation for the installed firmware rather than assuming every ESP32 port has identical defaults.
A missing address does not automatically mean firmware must be updated. Start with power, ground, SDA/SCL mapping, pull-ups, and the peripheral configuration. If you do change firmware, back up scripts first because flashing can erase the filesystem.
4. Run a scan
Run this diagnostic in the REPL. It creates a hardware I²C instance, scans for responding 7-bit addresses, and prints each result in hexadecimal.
from machine import Pin, I2C
# Verify these GPIOs for your board.
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
print("Addresses:", [hex(address) for address in i2c.scan()])
If a device responds at address 0x3C, the list may contain 0x3c. An empty list means no address responded on that bus at that moment; it does not prove the ESP32 lacks I²C capability or that the module is broken.
Start at 100 kHz. Higher speeds may work, but all devices and the wiring must support the chosen speed. A conservative standard-mode speed helps eliminate timing and signal-integrity variables. If your firmware has a different constructor signature, adapt the setup using its API documentation.
5. Diagnose an empty scan
Check systematically. First verify the module's supply and the shared ground. Then confirm SDA connects to SDA and SCL to SCL, and that the GPIO numbers in code match the physical pins used. Breadboard rows and module header order can cause mistakes that look like software problems.
Inspect the signal levels. I²C lines should idle high through suitable pull-ups. If a line remains low, disconnect the module and see whether the ESP32 line returns high. A low line can indicate a short, incorrect wiring, an unpowered device clamping the bus, or a peripheral stuck during a transaction. Do not force a line high with a jumper wire; diagnose the cause.
Check that the module really uses I²C. Some displays and sensors have SPI versions that look similar. Verify interface-select jumpers, address pins, reset pins, and enable pins. Some peripherals need a startup delay. Test one device at a time and remove other devices until the scan is repeatable.
6. Try SoftI2C if supported
If hardware I²C setup is confusing, SoftI2C can be a useful diagnostic on MicroPython ports that support it. It generates the bus protocol in software using GPIO rather than a hardware I²C peripheral. Availability and timing behaviour depend on the port, and software I²C is not necessarily the best final choice.
from machine import Pin, SoftI2C
i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=100000)
print([hex(address) for address in i2c.scan()])
Keep the same wiring and verify the selected pins. If hardware I²C and SoftI2C both find no devices, focus on power, wiring, address configuration, and electrical conditions. If one works and the other does not, compare pin assignments and check your firmware documentation. Software I²C cannot compensate for incorrect voltage, missing pull-ups, or a peripheral that is not powered.
7. Interpret common addresses
A common I²C OLED based on SSD1306 may appear at 0x3C or 0x3D. Some RTC modules appear at 0x68; other sensors use addresses such as 0x76 or 0x77. These are examples, not guarantees for every model. Check the exact part and board configuration. A scan result is more useful than assuming an address from a product listing, but it must still be matched against documentation.
Two devices with the same fixed address can conflict on one bus. If one supports address selection, change its address as documented. Otherwise consider a second I²C bus on separate supported GPIOs, an I²C multiplexer, or a different device. A scan cannot distinguish two physical devices sharing the same address; it reports responding addresses, not device count.
A device found by scanning may still be unusable if a driver expects a different address or interface. An address can acknowledge while the device's registers do not behave as expected. Follow the scan with a small driver-specific test.
8. Test a display or sensor after detection
For an OLED, confirm the controller, resolution, interface, and driver library. SSD1306 and SH1106 displays can look alike but are not interchangeable in every driver. After finding the address, use a driver intended for the controller and pass the address where the library expects it. If the display remains blank, check reset wiring, initialization, resolution settings, and whether the module is actually I²C rather than SPI.
For a sensor, identify its exact part number and use the correct MicroPython driver. A generic scan cannot tell you whether the temperature or pressure register is being read correctly. Start with the library's smallest example, then compare returned values with the datasheet. Check startup delay, measurement mode, and any data-ready status.
Avoid writing arbitrary register values just to test a device. Some registers control configuration, calibration, or operating modes. Prefer documented read-only identification registers where available, and record the original configuration before changing settings.
9. Improve bus reliability
Keep SDA and SCL wires short and route them away from noisy switching circuits. A breadboard is useful for learning, but loose contacts and long jumpers can cause intermittent faults. For longer cables, review I²C electrical limits and consider a bus extender or an interface designed for longer-distance communication.
Too many breakout-board pull-ups can make the effective resistance too low because parallel resistors reduce total resistance. Missing or overly weak pull-ups can instead create slow rising edges. Select the pull-up network based on supply voltage, bus capacitance, device sink-current limits, and speed. For a beginner test, use one known-good breakout and avoid changing multiple electrical variables at once.
If a peripheral disappears after reset, check power sequencing, its reset line, and whether the ESP32 begins communicating before the device is ready. A robust application can catch I/O exceptions and retry initialization after a bounded delay. It should report missing data clearly instead of treating it as valid.
10. Troubleshooting table
| Symptom | What to check |
|---|---|
scan() returns [] | Supply, common ground, SDA/SCL mapping, pull-ups, interface mode, reset/enable pins |
| Address appears inconsistently | Loose jumper, long wires, pull-up strength, power stability, bus speed |
| Address found but display blank | Wrong controller or driver, resolution, reset, initialization, SPI variant |
| Two devices conflict | Duplicate address; change an address or use a separate bus or multiplexer |
| I²C constructor error | Firmware API and supported bus IDs; consult installed MicroPython docs |
| Bus line stays low | Short, wiring fault, unpowered peripheral, or device stuck mid-transaction |
Change one thing at a time and rerun the scan. Record the GPIO mapping, bus speed, address list, firmware version, and module model. This makes a fault reproducible and prevents repeatedly moving wires without knowing what changed.
Final checklist and next steps
Confirm the exact ESP32 board and suitable SDA/SCL GPIOs. Check module supply and common ground, and ensure the I²C lines are not pulled up to 5 V. Use the supported MicroPython constructor at 100 kHz initially, scan with one known device attached, and match the detected address against the device documentation. After detection, test the correct driver and save a copy of the working script.
A successful scan is only the beginning of communication. Next, read a documented identification register or draw text on a supported OLED. Add other peripherals one at a time and verify that addresses are unique.
SolderHub takeaway: when an I²C scan finds nothing, debug the physical bus before rewriting application code. Check power, ground, GPIO mapping, pull-ups, interface mode, and address configuration in that order. Let the datasheet decide device-specific details.
Extra diagnostic: isolate the bus in a repeatable sequence
When the scan remains empty, return to a minimal setup rather than changing code and wiring at the same time. Disconnect USB, remove every peripheral except one known I²C module, then reconnect power. Run only the short scan script. Write down the exact board, firmware build, GPIO mapping, module part number, supply voltage, and scan output. This creates a baseline you can compare after each change.
If you have a multimeter, check the module's supply at its header while powered, using care to avoid shorting adjacent pins. With the circuit powered off, continuity checks can help identify an open ground or a wire placed in the wrong breadboard row. Do not use resistance or continuity mode on a powered circuit. A logic analyser can help inspect SDA and SCL transitions if you already have one, but it is not required for the basic workflow.
If a known-good device works on the ESP32 but the target module does not, investigate the target's interface selection, address pins, reset state, and voltage requirements. If no known device works, re-check the selected GPIOs and bus constructor against the specific MicroPython port. Once one device responds consistently, add the remaining peripherals individually and rescan after each addition. This approach separates an electrical fault from an address conflict or driver problem.
Keep a known-good scan script
Save the scan as a separate file such as i2c_scan.py on your computer and keep the pin mapping at the top. This makes it easy to rerun after changing a sensor library or application. Avoid leaving a repeated scan loop in main.py in a finished project because it can clutter the console and hide useful errors. Once wiring is confirmed, use the peripheral's documented driver and keep the scan script as a maintenance tool.