When your loop() stretches past 50 lines of tangled sensor reads, math, and display updates, your code becomes a nightmare to debug. Using custom functions in Arduino is the standard way to encapsulate logic, isolate variables, and keep your main loop clean. In this guide, we will build a multi-sensor environmental monitor to demonstrate proper function architecture, pass-by-reference data handling, and I2C error trapping.
This project targets the Arduino Nano Every (ABX00033) due to its robust megaAVR architecture, 5V logic with 3.3V I2C tolerance, and ample flash for library-heavy sensor code. We will pair it with an Adafruit BME280 and an SSD1306 OLED.
Hardware Spec Sheet & Pin Mapping
Estimated Build Time: 45 minutes
Estimated Cost (2026): ~$38 USD
Parts List
- Microcontroller: Arduino Nano Every (Part: ABX00033) - $12.50
- Sensor: Adafruit BME280 I2C/SPI Breakout (Part: 2652) - $9.95
- Display: Adafruit Monochrome 0.91" 128x32 OLED (Part: 938) - $11.50
- Wiring: 22 AWG solid core jumper wires, half-size solderless breadboard
Pin Mapping Table
| Component | Pin/Pad | Nano Every Pin | Notes |
|---|---|---|---|
| BME280 | VIN | 5V | Breakout has onboard 3.3V regulator |
| BME280 | GND | GND | Common ground |
| BME280 | SCK/SCL | A5 | I2C Clock |
| BME280 | SDI/SDA | A4 | I2C Data |
| OLED | VIN | 5V | 3.3V to 5V tolerant |
| OLED | GND | GND | Common ground |
| OLED | SCL | A5 | Shared I2C bus |
| OLED | SDA | A4 | Shared I2C bus |
The Code: Structuring Custom Functions in Arduino
Instead of dumping every Wire.read() and display.print() into the loop(), we break the logic into four distinct functions: hardware initialization, sensor reading, math calculation, and display rendering. Notice the use of pass-by-reference (the & symbol) in readSensorData() to return multiple values without relying on global variables.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// --- Pin & Hardware Definitions ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define BME_ADDRESS 0x76
#define SEALEVELPRESSURE_HPA (1013.25)
// --- Object Instantiation ---
Adafruit_BME280 bme;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// --- Function Prototypes ---
bool initHardware();
void readSensorData(float &temp, float &hum, float &pres);
float calculateDewPoint(float temp, float hum);
void updateDisplay(float temp, float hum, float dew);
void setup() {
Serial.begin(115200);
// Halt execution if hardware fails to initialize
if (!initHardware()) {
Serial.println(F("Hardware init failed. Check I2C wiring."));
while (1) { delay(100); }
}
}
void loop() {
float t = 0, h = 0, p = 0, d = 0;
readSensorData(t, h, p);
d = calculateDewPoint(t, h);
updateDisplay(t, h, d);
delay(2000); // Non-blocking alternative recommended for advanced builds
}
// --- Custom Functions Implementation ---
bool initHardware() {
Wire.begin();
Wire.setClock(400000); // 400kHz I2C Fast Mode
if (!bme.begin(BME_ADDRESS, &Wire)) {
return false; // BME280 not found
}
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
return false; // OLED not found
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
return true;
}
void readSensorData(float &temp, float &hum, float &pres) {
temp = bme.readTemperature();
hum = bme.readHumidity();
pres = bme.readPressure() / 100.0F;
}
float calculateDewPoint(float temp, float hum) {
// Magnus-Tetens approximation for dew point
float a = 17.271;
float b = 237.7;
float gamma = (a * temp / (b + temp)) + log(hum / 100.0);
return (b * gamma) / (a - gamma);
}
void updateDisplay(float temp, float hum, float dew) {
display.clearDisplay();
display.setCursor(0, 0);
display.print("T:"); display.print(temp, 1); display.print("C");
display.setCursor(0, 10);
display.print("H:"); display.print(hum, 1); display.print("%");
display.setCursor(0, 20);
display.print("D:"); display.print(dew, 1); display.print("C");
display.display();
}
Beginners often use global variables (e.g.,
float globalTemp;) to pass data out of functions. This causes namespace collisions and makes code un-portable. By passing variables by reference (float &temp), the function modifies the exact memory address of the local variable in loop(), keeping memory usage tight and scope isolated.
Debugging: "Not Declared in This Scope" & Runtime Hangs
When working with functions in Arduino, you will inevitably hit compiler errors or runtime watchdog resets. Here is how to diagnose the two most common failure modes.
Compiler Error: 'calculateDewPoint' was not declared in this scope
If you see this exact error string in the Arduino IDE output console, the compiler encountered a function call before it knew the function existed.
- Missing Prototype: You forgot to declare the function signature at the top of the sketch (above
setup()). The Arduino IDE attempts to auto-generate prototypes, but it frequently fails on functions using reference pointers (&) or custom structs. - Case Sensitivity Typo: C++ is strictly case-sensitive. Calling
calculatedewpoint()instead ofcalculateDewPoint()will trigger this scope error. - Nested Functions: Standard C++ does not support defining a function inside another function. Ensure your custom function isn't accidentally nested inside
loop()orsetup().
Runtime Failure: I2C Bus Hang (Soft WDT Reset)
If your code compiles but the serial monitor prints Soft WDT reset or the board simply freezes after 2 seconds, your I2C bus is hanging inside readSensorData().
The First Three Things to Check When It Fails:
- Verify Pull-Up Resistors: The Adafruit BME280 (2652) has onboard 10kΩ pull-ups, but if your jumper wires are loose or exceeding 30cm, signal reflection will cause the
Wirelibrary to wait indefinitely for an ACK bit. Keep I2C traces under 15cm. - Check I2C Addresses: Run an I2C scanner sketch. The BME280 defaults to
0x77or0x76depending on the jumper pad on the back of the breakout. If the code targets0x76but the board is set to0x77,bme.begin()will fail (caught by ourinitHardware()error trap), but rawWirecalls elsewhere will hang. - Inspect Power Brownouts: The OLED and BME280 combined can draw spikes of 20mA. If you are powering the Nano Every via a weak USB hub, the 3.3V regulator may brownout, corrupting the I2C state machine. Power via the
VINpin with a 7-9V wall adapter for stability.
How to Extend or Simplify the Build
To Simplify: If you only need serial output and want to save flash memory, strip out the Adafruit_SSD1306 and Adafruit_GFX libraries. Remove the updateDisplay() function entirely and replace it with a simple Serial.printf() call. This reduces compile size by roughly 18KB and frees up RAM.
To Extend: To add a third sensor (like a TSL2591 Light Sensor), do not just paste code into the loop. Create a new readLightSensor(float &lux) function. If you plan to add WiFi via an ESP32 coprocessor, abstract the I2C reads into a non-blocking state machine function, replacing the delay(2000) with a millis() timer check to prevent dropped WiFi packets.
FAQ: Advanced Questions on Functions in Arduino
How do you pass multiple values from functions in Arduino?
Standard C functions only return one value via the return keyword. To pass multiple values back to the caller, use pass-by-reference (e.g., void getCoords(int &x, int &y)) or pass pointers (e.g., void getCoords(int *x, int *y)). Alternatively, you can define a custom struct to group variables and return the struct object, though this consumes slightly more stack memory during the copy operation.
Why are my custom functions in Arduino running so slow?
If a custom function takes milliseconds to execute, you likely have hidden blocking calls inside it. The most common culprit is calling Serial.print() or display.display() inside a high-frequency sensor polling function. I2C and UART transmissions are relatively slow. Keep your data-gathering functions strictly focused on reading registers, and move all display/serial formatting to a separate, slower-running output function.
Can I use delay() inside functions in Arduino without blocking the main loop?
No. The delay() function is a blocking hardware timer loop. If you call delay(1000) inside a custom sensor function, the entire microcontroller halts, ignoring button presses, serial interrupts, and watchdog timers. For non-blocking timing inside functions, pass a previousMillis variable by reference and evaluate if (currentMillis - previousMillis >= interval).
What is the difference between void and return functions in Arduino?
A void function performs an action (like updating a display or toggling a pin) but yields no data back to the caller. A return function (e.g., float, int, bool) calculates or fetches a value and passes it back. Use bool return types for initialization functions (like our initHardware()) so the main sketch can gracefully handle hardware failures instead of crashing blindly.






