To successfully run multiple 3.3V SPI devices on a 5V Arduino Nano, you must use hardware SPI with a BSS138 bidirectional logic level converter and explicitly manage chip select (CS) pins and clock speeds via SPI.beginTransaction(). Directly wiring 3.3V SPI sensors to a 5V ATmega328P will fry the sensor's MISO pin protection diodes within minutes, leading to silent failures or a permanently locked bus.
This guide walks through the exact hardware decisions, wiring topology, and C++ implementation required to share a single Serial Peripheral Interface Arduino bus between a MicroSD card and a BME280 environmental sensor, terminating in a concrete debugging framework for when the bus inevitably locks up.
The Decision Tree: Hardware SPI vs. Software SPI & Level Shifting
Before wiring a single jumper, you must decide how to route the SPI bus. Software SPI (bit-banging) is tempting when you run out of hardware pins, but it introduces severe timing jitter and caps your throughput at a few hundred kilohertz. Here is the decision framework for selecting your SPI implementation and logic level strategy.
| Condition / Constraint | Decision Path | Concrete Pick / Action |
|---|---|---|
| Bus speed requirement > 1 MHz (e.g., SD Card, TFT Display) | Must use Hardware SPI | Use pins 11 (MOSI), 12 (MISO), 13 (SCK) |
| Board logic is 5V (Uno, Nano, Mega) AND sensor is 3.3V | Must level-shift bidirectionally | Default Pick: BSS138 Logic Level Converter (Adafruit 757) |
| Board logic is 3.3V (Nano 33 IoT, ESP32, Zero) | Direct wiring is safe | Skip level shifter; wire MISO/MOSI/SCK directly |
| Hardware SPI pins (11,12,13) are physically inaccessible or broken | Fallback to Software SPI | Use SoftwareSPI library; cap clock at 250 kHz |
Parts List & SPI Pin Mapping for 5V Arduinos
This build targets the Arduino Nano V3.0 (ATmega328P, 5V logic). The code and pinouts below assume this exact variant. If you are using an ESP32 or a 3.3V Arduino, omit the logic level converter and wire the LV and HV sides together.
Bill of Materials (BOM)
- Microcontroller: Arduino Nano V3.0 (ATmega328P) - ~$6.00
- Sensor: Adafruit BME280 SPI Breakout (Product ID: 2652) - ~$19.95
- Storage: MicroSD Card Adapter Module (with 3.3V LDO onboard) - ~$2.50
- Level Shifter: BSS138 Bidirectional Logic Level Converter (Adafruit 757 or generic equivalent) - ~$3.95
- Consumables: 22 AWG solid core wire, half-size breadboard, Class 10 MicroSD card (FAT32 formatted).
Pin Mapping & Level Shifter Topology
The BSS138 level shifter has a High Voltage (HV) side for the 5V Arduino and a Low Voltage (LV) side for the 3.3V sensors. Both sides must share a common ground.
| Arduino Nano (5V) | BSS138 Shifter (HV Side) | BSS138 Shifter (LV Side) | SPI Devices (3.3V) |
|---|---|---|---|
| 5V Pin | HV | LV (Connected to 3.3V out) | VIN / VCC |
| GND | GND (HV) | GND (LV) | GND |
| D13 (SCK) | Channel 1 HV | Channel 1 LV | SCK (Both SD & BME) |
| D12 (MISO) | Channel 2 HV | Channel 2 LV | MISO / DO (Both SD & BME) |
| D11 (MOSI) | Channel 3 HV | Channel 3 LV | MOSI / DI (Both SD & BME) |
| D10 (CS_SD) | Channel 4 HV | Channel 4 LV | CS (SD Card Module) |
| D9 (CS_BME) | Bypass Shifter* | Bypass Shifter* | CS (BME280) |
*Note: Arduino digital pins output 5V for HIGH, which is safe for the BME280 CS pin as it is a high-impedance input. However, for strict signal integrity, route D9 through a 5th channel on the shifter if your board has 8 channels.
Wiring the SPI Bus: Step-by-Step
- Establish Power Rails: Connect the Nano 5V to the breadboard red rail (HV) and the Nano 3.3V pin to the blue rail (LV). Connect Nano GND to the black rail. Warning: The Nano's onboard 3.3V regulator maxes out at ~50mA. If your SD card draws more during writes, power the LV rail from a dedicated 3.3V LDO (like an AMS1117-3.3).
- Wire the Level Shifter: Plug the BSS138 into the center trench. Connect HV to the 5V rail, LV to the 3.3V rail, and both GND pins to the black rail.
- Route the Shared Bus (SCK, MOSI, MISO): Run wires from Nano D13, D11, and D12 to the HV side of the shifter. Run wires from the LV side to the SCK, MOSI, and MISO pins on both the SD module and the BME280. SPI is a daisy-chain/bus topology; these three lines are shared in parallel.
- Route Chip Select (CS) Lines: Wire Nano D10 to the SD card CS pin (through the shifter). Wire Nano D9 to the BME280 CS pin. Critical: The BME280 defaults to I2C mode on boot. To force it into SPI mode, you must also wire the
CSBpin on the BME280 to 3.3V (LV rail). - Verify with a Multimeter: Before plugging in the Nano via USB, use your DMM in continuity mode to ensure the LV GND and HV GND are bonded. A missing common ground will cause the MOSFETs in the level shifter to never turn on, leaving MISO floating.
Complete Compilable Code: BME280 + SD Card on One SPI Bus
Sharing an SPI bus requires strict management of clock speeds and SPI transactions. The SD card initializes at 400 kHz and runs at up to 25 MHz. The BME280 runs comfortably at 10 MHz. If you don't use SPI.beginTransaction() and SPI.endTransaction(), the SD library will change the bus clock speed, and your subsequent BME280 read will pull garbage data.
#include <SPI.h>
#include <SD.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// --- PIN DEFINITIONS ---
#define SD_CS_PIN 10
#define BME_CS_PIN 9
// Hardware SPI pins (11, 12, 13) are managed by the SPI library automatically
// --- OBJECTS & SETTINGS ---
Adafruit_BME280 bme;
// BME280 SPI Max speed is 10MHz, MSB first, SPI Mode 0
SPISettings bmeSPISettings(10000000, MSBFIRST, SPI_MODE0);
File dataFile;
unsigned long lastLogTime = 0;
const unsigned long LOG_INTERVAL = 5000; // Log every 5 seconds
void setup() {
Serial.begin(115200);
while (!Serial) { delay(10); } // Wait for serial port (Nano 33 IoT/Leonardo)
Serial.println(F("Initializing Multi-Device SPI Bus..."));
// 1. Initialize BME280 via Hardware SPI
// Pass the CS pin to the Adafruit library
if (!bme.begin(BME_CS_PIN)) {
Serial.println(F("FATAL: Could not find a valid BME280 sensor, check wiring & CSB pin!"));
while (1) { delay(10); } // Halt execution
}
Serial.println(F("BME280 initialized on SPI."));
// 2. Initialize SD Card
// The SD library handles its own SPI transactions internally
if (!SD.begin(SD_CS_PIN)) {
Serial.println(F("FATAL: SD initialization failed! Check CS pin and FAT32 format."));
while (1) { delay(10); }
}
Serial.println(F("SD Card initialized."));
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - lastLogTime >= LOG_INTERVAL) {
lastLogTime = currentMillis;
// --- READ SENSOR WITH EXPLICIT SPI TRANSACTION ---
SPI.beginTransaction(bmeSPISettings);
float temp = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F;
SPI.endTransaction(); // Release the bus so SD can use it
// --- WRITE TO SD CARD ---
dataFile = SD.open("datalog.txt", FILE_WRITE);
if (dataFile) {
dataFile.print(currentMillis);
dataFile.print(",");
dataFile.print(temp);
dataFile.print(",");
dataFile.print(humidity);
dataFile.print(",");
dataFile.println(pressure);
dataFile.close();
Serial.println(F("Data logged successfully."));
} else {
Serial.println(F("ERROR: Failed to open datalog.txt for writing."));
}
}
}
Debugging SPI Failures: The First Three Things to Check
When an SPI bus fails, it rarely throws a helpful compilation error. It fails at runtime with silent data corruption or specific library error codes. If your build fails, follow this exact troubleshooting sequence.
1. The Exact Error: SD error: 0X20,0X0 or SD.begin() returned false
What it means: The Arduino sent the CMD0 (reset) command to the SD card over MOSI, but the MISO line stayed HIGH. The card is not responding.
- Cause A (Most Likely): MISO line is disconnected, or the level shifter GND is missing. Measure voltage between the LV GND pin on the shifter and the Nano GND pin. It must read
0.00V. If it reads >0.5V, your ground bond is broken. - Cause B: You are using a raw MicroSD breakout board without a 3.3V LDO, and you fed it 5V. You have permanently destroyed the card's internal SPI controller. Swap the card and the module.
- Cause C: The SD card is formatted as exFAT. The standard Arduino
SD.hlibrary only supports FAT16 and FAT32. Reformat the card using the official SD Memory Card Formatter.
2. The Exact Error: BME280 reads NaN or Sensor ID returns 0x00
What it means: The Arduino is clocking data, but the BME280 is either in I2C mode or the Chip Select is floating.
- Cause A: The
CSBpin on the BME280 is not tied to 3.3V. If CSB is left floating, the sensor defaults to I2C on boot and ignores all SPI clock pulses. Solder a jumper from CSB to VIN on the breakout. - Cause B: Another device on the SPI bus is holding MISO low. Ensure the SD card CS pin is driven
HIGHbefore callingbme.begin(). The Adafruit library handles this, but if you have a third device (like an SPI display) wired up, manually set its CS pin toOUTPUTandHIGHinsetup().
3. The Exact Error: Garbled Data / Random Spikes in Sensor Readings
What it means: SPI Mode mismatch or Clock Speed violation.
- Cause A: Missing
SPI.endTransaction(). If you omit this in the code above, the SD card leaves the bus configured for 25 MHz. When the BME280 tries to read at its own speed without a transaction wrapper, the clock edges misalign. Always wrap manual SPI reads inbeginTransaction(). - Cause B: SPI wires are too long. SPI is highly susceptible to capacitance. At 10 MHz, breadboard jumper wires longer than 15cm (6 inches) will cause signal ringing on the SCK line. Keep SPI traces under 10cm, or drop the
bmeSPISettingsclock to1000000(1 MHz).
Extending and Simplifying the Build
Once you have the BME280 and SD card logging reliably, you will likely want to scale the system. Here is how to modify the architecture based on your end goal.
How to Simplify: Move to a 3.3V Microcontroller
If you are tired of troubleshooting the BSS138 level shifter and dealing with breadboard capacitance, the ultimate simplification is to eliminate the 5V-to-3.3V translation entirely.
The Pick: Swap the Arduino Nano for an Adafruit Feather ESP32-S2 or an Arduino Nano 33 IoT. Because these boards operate natively at 3.3V logic, you can wire MISO, MOSI, and SCK directly to the sensors. You save 12 jumper wires, eliminate the level shifter failure point, and gain native WiFi (ESP32) for MQTT telemetry.
How to Extend: Adding an SPI TFT Display
Adding an ILI9341 or ST7789 TFT display to this bus is the logical next step for a local UI.
The Catch: TFT displays are SPI bus hogs. They require massive continuous data streams and often use DMA (Direct Memory Access) on advanced boards. On an ATmega328P, DMA isn't available, so the CPU must bit-bang the display updates, which will block your SD card writes.
The Solution: If you add a TFT display, you must use a microcontroller with dual SPI buses or DMA support, like the ESP32-WROOM-32. Wire the SD Card and BME280 to the default VSPI bus (Pins 18, 19, 23), and wire the TFT display to the HSPI bus (Pins 14, 12, 13). This physically separates the high-speed display traffic from the low-speed sensor logging, preventing bus contention and SD card write timeouts.
SPISettings object. If the system works at 1 MHz but fails at 10 MHz, you have a wiring capacitance or level-shifter bandwidth issue, not a code issue.






