Skip to content

Arduino - Functions

Arduino - Writing Modular Code with Functions

Section titled “Arduino - Writing Modular Code with Functions”

As your programs grow, keeping all your code inside setup() and loop() becomes messy and hard to manage. Functions are the primary tool for structuring programs into logical, reusable blocks of code, each designed to perform a specific task.

  • Organization: Functions break down complex problems into smaller, manageable pieces (e.g., readSensors(), updateDisplay(), sendData()).
  • Reusability (DRY Principle): Stands for “Don’t Repeat Yourself”. If you need to perform the same action multiple times, you write the code once in a function and call it whenever needed.
  • Readability: Well-named functions make your code self-documenting. blinkLed(3, 500); is much clearer than a block of for, digitalWrite, and delay calls.
  • Debugging: Isolating code into functions makes it easier to test and debug individual parts of your program without affecting the rest.
  • Modularity: Functions allow you to create libraries of code that can be easily shared and used in other projects.

Every Arduino sketch already uses two special, required functions: setup(), which runs once at the beginning, and loop(), which runs continuously.

A function definition in C++ has four main parts:

returnType functionName(parameterType parameterName1, ...)
{
// The function's body: code to be executed
return returnValue; // (only if returnType is not 'void')
}
  1. Return Type: The data type of the value the function sends back after it’s done (e.g., int, float, bool). If the function doesn’t return anything, you use void.
  2. Function Name: A descriptive name that follows the same rules as variable names.
  3. Parameters (or Arguments): A list of variables that act as inputs to the function. This is how you pass data into the function. If a function doesn’t take any inputs, the parentheses are empty: functionName().
  4. Function Body: The block of code inside the curly braces {} that gets executed when the function is called.

The C++ compiler reads your code from top to bottom. It must know about a function before you try to call it. You have two ways to achieve this:

  1. Definition Before Use: Simply define the entire function above any code that calls it (e.g., above setup() and loop()). This is simple but can make the top of your file cluttered.
  2. Prototype then Definition (Recommended): Declare a prototype at the top of your sketch. A prototype is like a function’s signature without the body. This tells the compiler, “A function with this name, return type, and parameters exists somewhere.” You can then place the full function definition anywhere else, typically below loop() for better organization.

This complete sketch demonstrates a void function and a function that returns a value, using the recommended prototype method.

// ============ Function Prototypes ============
// A function that takes two parameters and performs an action (blinking).
// It does not return a value, so its return type is 'void'.
void blinkLed(int pin, int blinkCount);
// A function that performs a calculation and returns a result.
// It returns an integer, so its return type is 'int'.
int celsiusToFahrenheit(int celsius);
// ============ Global Variables ============
const int BUILTIN_LED_PIN = LED_BUILTIN;
// ================== Setup ===================
void setup() {
Serial.begin(9600);
pinMode(BUILTIN_LED_PIN, OUTPUT);
Serial.println("--- Function Demo ---");
// Calling the 'blinkLed' function to signal setup is complete.
Serial.println("Blinking LED 3 times...");
blinkLed(BUILTIN_LED_PIN, 3);
// Calling the 'celsiusToFahrenheit' function and storing the result.
int tempC = 25;
int tempF = celsiusToFahrenheit(tempC);
Serial.print(tempC);
Serial.print(" degrees Celsius is ");
Serial.print(tempF);
Serial.println(" degrees Fahrenheit.");
}
// ================== Loop ====================
void loop() {
// The main loop is kept clean. All logic is in functions.
// This makes the program's intent clear.
}
// ========== Function Definitions ===========
/**
* @brief Blinks a specified LED a certain number of times.
* @param pin The pin number of the LED to blink.
* @param blinkCount The number of times to blink the LED.
*/
void blinkLed(int pin, int blinkCount) {
for (int i = 0; i < blinkCount; i++) {
digitalWrite(pin, HIGH);
delay(250);
digitalWrite(pin, LOW);
delay(250);
}
}
/**
* @brief Converts a temperature from Celsius to Fahrenheit.
* @param celsius The temperature in degrees Celsius.
* @return The temperature in degrees Fahrenheit as an integer.
*/
int celsiusToFahrenheit(int celsius) {
// Using floating point for the calculation for accuracy before converting back to int
return (celsius * 9.0 / 5.0) + 32;
}
  • Single Responsibility Principle: A function should do one thing and do it well. If your function is named readSensorAndPrintValue, consider splitting it into int readSensor() and void printValue(int value).
  • Descriptive Naming: Name functions based on the action they perform (calculateAverage, checkForButtonPress) or the value they return (isButtonPressed).
  • Keep Functions Short: Shorter functions are easier to understand, test, and debug.
  • Pass by Value vs. Pass by Reference: By default, arguments are passed by value, meaning the function gets a copy of the variable. Modifying it inside the function does not affect the original. To modify the original variable, you can pass by reference by adding an ampersand (&) to the parameter type (e.g., void myFunction(int& myVar)). Use this with caution.