Every time you call dht.readTemperature() or servo.write(90), you are using someone’s custom Arduino library. Libraries are the backbone of the Arduino ecosystem — they encapsulate complex hardware interaction into clean, reusable interfaces that let any developer use sophisticated functionality with just a few lines of code.
Writing your own Arduino library is a skill that transforms you from a library consumer into a library author. Whether you want to package your sensor driver for reuse across projects, share your work with the community, or simply write better-organized code, understanding Arduino library structure and C++ class design is an essential step forward.
This guide takes you from zero to a fully functional, publishable Arduino library — with a real-world example of a temperature sensor abstraction class.
Arduino Library Structure and File Organization
An Arduino library is a directory with a specific structure that the Arduino IDE and PlatformIO recognize. The minimum required structure is:
MyLibrary/
├── src/
│ ├── MyLibrary.h # Header file (class declaration)
│ └── MyLibrary.cpp # Implementation file (method definitions)
├── examples/
│ └── BasicExample/
│ └── BasicExample.ino
├── keywords.txt # Syntax highlighting definitions
└── library.properties # Library metadata
The src/ directory is the modern convention (Arduino IDE 1.5+). Older libraries placed .h and .cpp files directly in the root. Both work, but the src/ layout is preferred for new libraries as it separates source from metadata.
The library directory name must exactly match the library name in library.properties and the header file name. Consistency here prevents confusing IDE errors.
To create a library manually, create this directory structure inside your Arduino libraries folder (typically ~/Arduino/libraries/ on Linux/Mac or Documents/Arduino/libraries/ on Windows). Both Arduino IDE and PlatformIO will discover it automatically.
Writing the Header File (.h)
The header file is the public interface of your library — it declares what your class can do without revealing how it does it. A well-designed header file should be readable and self-explanatory even without the .cpp implementation.
We’ll build a SimpleSensor library that abstracts a generic analog temperature sensor like the LM35:
// src/SimpleSensor.h
#ifndef SIMPLESENSOR_H
#define SIMPLESENSOR_H
// Required Arduino includes
#include <Arduino.h>
// Library version (can be read by user code)
#define SIMPLESENSOR_VERSION "1.0.0"
// Temperature units enum for clean API
enum TempUnit {
CELSIUS,
FAHRENHEIT,
KELVIN
};
class SimpleSensor {
public:
// Constructor: accepts the analog pin number
SimpleSensor(uint8_t pin);
// Initialize the sensor (call in setup())
bool begin();
// Read temperature in the specified unit
float readTemperature(TempUnit unit = CELSIUS);
// Get the raw ADC reading (0-1023)
int readRaw();
// Set number of samples to average (default: 1)
void setSamples(uint8_t samples);
// Set reference voltage for ADC calculations (default: 5.0V)
void setReferenceVoltage(float vref);
// Check if sensor is responding (basic sanity check)
bool isConnected();
private:
uint8_t _pin; // The analog pin connected to sensor
uint8_t _samples; // Number of readings to average
float _vref; // ADC reference voltage
bool _initialized; // Has begin() been called?
// Private helper: convert ADC value to Celsius
float _adcToCelsius(int adcValue);
// Private helper: take multiple readings and average
float _averagedReading();
};
#endif // SIMPLESENSOR_H
Several important points about this header:
- Include guards:
#ifndef SIMPLESENSOR_H / #define SIMPLESENSOR_H / #endifprevent the header from being included multiple times in the same compilation unit, which would cause “redefinition” errors. - Arduino.h: Must be included because Arduino types (uint8_t, byte, etc.) and functions (analogRead, pinMode) are defined here. Without it, your library won’t compile.
- Private vs Public: Public methods are the user-facing API. Private methods and variables are implementation details the user doesn’t need to see or interact with.
- Default parameters:
readTemperature(TempUnit unit = CELSIUS)gives users a clean default while preserving flexibility. - Naming conventions: Private members prefixed with underscore (_pin, _vref) is a common Arduino library convention that makes them visually distinct.
Writing the Implementation File (.cpp)
The .cpp file contains the actual implementations of all the methods declared in the header. Every method definition uses the ClassName:: scope resolution operator:
// src/SimpleSensor.cpp
#include "SimpleSensor.h"
// Constructor: initialize member variables
SimpleSensor::SimpleSensor(uint8_t pin) {
_pin = pin;
_samples = 1; // Default: single reading
_vref = 5.0; // Default: 5V reference (Uno, Mega)
_initialized = false;
}
// begin(): set up the pin and validate
bool SimpleSensor::begin() {
// Analog pins are inputs by default, but set explicitly
pinMode(_pin, INPUT);
_initialized = true;
// Quick sanity check: ADC value shouldn't be 0 or max if sensor connected
int rawVal = analogRead(_pin);
return (rawVal > 0 && rawVal < 1023);
}
// readTemperature(): the main public method
float SimpleSensor::readTemperature(TempUnit unit) {
if (!_initialized) return -999.0; // Error: begin() not called
float celsius = _averagedReading();
switch (unit) {
case FAHRENHEIT:
return (celsius * 9.0 / 5.0) + 32.0;
case KELVIN:
return celsius + 273.15;
case CELSIUS:
default:
return celsius;
}
}
// readRaw(): return raw ADC value
int SimpleSensor::readRaw() {
return analogRead(_pin);
}
// setSamples(): configure averaging
void SimpleSensor::setSamples(uint8_t samples) {
// Clamp between 1 and 64 to avoid blocking too long
_samples = constrain(samples, 1, 64);
}
// setReferenceVoltage(): for 3.3V systems or external AREF
void SimpleSensor::setReferenceVoltage(float vref) {
_vref = vref;
}
// isConnected(): basic check if sensor is responding
bool SimpleSensor::isConnected() {
int raw = analogRead(_pin);
// A floating pin reads erratically; a connected LM35 reads in range
return (raw > 10 && raw < 1010);
}
// PRIVATE: convert ADC reading to Celsius for LM35
// LM35: 10mV per degree Celsius
// Celsius = (ADC * Vref / 1024) / 0.01
float SimpleSensor::_adcToCelsius(int adcValue) {
float voltage = (adcValue * _vref) / 1024.0;
return voltage / 0.01; // LM35: 10mV per degree
}
// PRIVATE: take multiple readings and return average
float SimpleSensor::_averagedReading() {
long sum = 0;
for (uint8_t i = 0; i < _samples; i++) {
sum += analogRead(_pin);
if (_samples > 1) delay(2); // Small delay between readings
}
return _adcToCelsius(sum / _samples);
}
Notice how the private helper methods _adcToCelsius() and _averagedReading() are not in the user-facing API but do the mathematical heavy lifting. This keeps the public interface clean while hiding complexity.
keywords.txt and library.properties
library.properties — this file tells the Arduino IDE (and PlatformIO) everything it needs to know about your library:
name=SimpleSensor
version=1.0.0
author=Your Name <[email protected]>
maintainer=Your Name <[email protected]>
sentence=A simple analog temperature sensor library for Arduino.
paragraph=Supports LM35 and similar analog sensors with averaging, unit conversion, and configurable reference voltage.
category=Sensors
url=https://github.com/yourusername/SimpleSensor
architectures=*
depends=
The architectures=* means the library works on all Arduino platforms. If your library is AVR-specific, use architectures=avr. The depends field lists other libraries yours requires.
keywords.txt — provides syntax highlighting in Arduino IDE. The format is: KEYWORD TAB TYPE
#######################################
# Syntax Coloring Map for SimpleSensor
#######################################
# Datatypes (orange)
SimpleSensorttKEYWORD1
TempUnitttKEYWORD1
# Methods and functions (brown/orange)
begintttKEYWORD2
readTemperaturettKEYWORD2
readRawtttKEYWORD2
setSamplestttKEYWORD2
setReferenceVoltagetKEYWORD2
isConnectedttKEYWORD2
# Constants (teal)
CELSIUStttLITERAL1
FAHRENHEITttLITERAL1
KELVINtttLITERAL1
Writing Example Sketches
Example sketches are how users learn to use your library. Put them in the examples/ directory — they appear in Arduino IDE under File → Examples → SimpleSensor:
// examples/BasicReading/BasicReading.ino
#include <SimpleSensor.h>
// Create sensor on analog pin A0
SimpleSensor tempSensor(A0);
void setup() {
Serial.begin(9600);
// Average 8 readings for smoother output
tempSensor.setSamples(8);
if (tempSensor.begin()) {
Serial.println("SimpleSensor initialized successfully!");
} else {
Serial.println("Warning: Sensor may not be connected.");
}
}
void loop() {
float tempC = tempSensor.readTemperature(CELSIUS);
float tempF = tempSensor.readTemperature(FAHRENHEIT);
Serial.print("Temperature: ");
Serial.print(tempC, 1);
Serial.print(" C / ");
Serial.print(tempF, 1);
Serial.println(" F");
delay(2000);
}
Good examples show the most common use case first (basic reading), then provide more complex examples in separate sketch directories (averaging, unit switching, error handling). Each example should be complete and self-contained.
Advanced Techniques: Callbacks, Templates, and Inheritance
Callbacks for Event-Driven Libraries
For libraries that need to notify user code when something happens (threshold crossed, data received, etc.), function pointer callbacks are the standard approach:
// In header
typedef void (*AlertCallback)(float temperature);
class SimpleSensor {
public:
// Register a callback for temperature alerts
void setAlertCallback(AlertCallback callback, float threshold);
// Call in loop() to check threshold and trigger callback
void update();
private:
AlertCallback _callback;
float _threshold;
};
// User code:
void onHotAlert(float temp) {
Serial.print("ALERT! Temperature: "); Serial.println(temp);
}
sensor.setAlertCallback(onHotAlert, 40.0); // Alert above 40°C
Template Classes for Generic Sensors
If your library needs to support different data types or sensor configurations, C++ templates allow generics without performance overhead:
// Generic ring buffer useful in many sensor libraries
template <typename T, uint8_t SIZE>
class RingBuffer {
public:
void push(T value) {
_buf[_head] = value;
_head = (_head + 1) % SIZE;
if (_count < SIZE) _count++;
}
T average() {
T sum = 0;
for (uint8_t i = 0; i < _count; i++) sum += _buf[i];
return _count ? sum / _count : 0;
}
private:
T _buf[SIZE];
uint8_t _head = 0;
uint8_t _count = 0;
};
// Use:
RingBuffer<float, 16> tempHistory; // 16-element float ring buffer
Inheritance for Sensor Families
If you’re building a family of related libraries (e.g., multiple temperature sensor types), use inheritance to share common code:
// Base class with common interface
class TemperatureSensor {
public:
virtual float readTemperature() = 0; // Pure virtual - must override
virtual bool begin() = 0;
// Common utility - same for all subclasses
float readFahrenheit() {
return (readTemperature() * 9.0 / 5.0) + 32.0;
}
};
// Concrete implementations
class LM35Sensor : public TemperatureSensor { /* ... */ };
class DHT11Sensor : public TemperatureSensor { /* ... */ };
class DS18B20Sensor : public TemperatureSensor { /* ... */ };
Testing and Publishing Your Library
Testing Your Library
Before sharing your library, test it thoroughly:
- Compile test: Open each example sketch and verify it compiles without warnings on all target boards
- Hardware test: Upload and test with real hardware — the sensor, the Arduino, and any other components your library supports
- Edge cases: Test with incorrect pin numbers, unconnected sensors, extreme values. Make sure your library fails gracefully (returns error values, not garbage or crashes)
- PlatformIO: If you want PlatformIO users, test your library with a simple platformio.ini project
Publishing to Arduino Library Manager
- Create a public GitHub repository for your library
- Create a release tag (e.g., v1.0.0) on GitHub
- Submit a pull request to the arduino/library-registry GitHub repository, adding your library’s GitHub URL to the list
- The Arduino team reviews submissions (usually within a few days)
- Once approved, your library appears in Arduino IDE’s Library Manager for the entire global community
Publishing to PlatformIO Registry
- Install PlatformIO Core:
pip install platformio - Login:
pio account login - Publish from your library directory:
pio pkg publish
Frequently Asked Questions
Do I need to know C++ to write Arduino libraries?
A basic understanding of C++ classes, constructors, and member functions is required. However, the Arduino library API keeps C++ usage relatively simple — you don’t need templates, inheritance, or advanced metaprogramming for most libraries. If you know how to write Arduino sketches confidently, you are 70% of the way to writing a basic library. The main new concepts are: class declaration in a .h file, method definition in a .cpp file using the ClassName:: prefix, and the difference between public and private members.
How do I access hardware (SPI, I2C, Serial) from inside a library?
Include the relevant header in your library’s .h or .cpp file and use the global hardware objects. For I2C use #include <Wire.h> and call Wire.begin(), Wire.requestFrom(), etc. For SPI use #include <SPI.h>. For Serial, just use Serial.print() directly. It is good practice to call Wire.begin() or SPI.begin() in your library’s begin() method, since multiple libraries calling these in setup() is harmless — they are designed to handle multiple calls.
Can my library use global variables?
Avoid global variables in libraries wherever possible. Use class member variables instead. Global variables in libraries can conflict with user code or other libraries that use the same variable names. If you absolutely need a global (e.g., for an ISR that must be accessed outside the class), prefix it with your library name to minimize conflicts and document it clearly: volatile bool SimpleSensor_irqFired = false;
How do I debug library code? I can’t easily use Serial.print inside a library.
You can use Serial.print inside library code — it works fine. However, it’s best practice to make debug output conditional on a compile-time flag, so users can disable it to save flash space. Define a debug macro at the top of your header: #define SIMPLESENSOR_DEBUG 0. Then in .cpp: if (SIMPLESENSOR_DEBUG) { Serial.println("Debug info"); }. Advanced libraries use #ifdef SIMPLESENSOR_DEBUG preprocessor guards so the debug code compiles to nothing when disabled.
What is the maximum amount of code a library can have?
There is no practical limit to library size — the Arduino IDE compiles everything together. However, all the code in all your included libraries (plus your sketch) must fit in the target microcontroller’s flash memory. If your library is too large for small boards like the Uno (32KB flash), document this requirement and suggest larger boards like the Mega (256KB) or Nano 33 family. Conditional compilation (#ifdef __AVR_ATmega2560__) can enable/disable features based on the target board.
Take your Arduino skills to the professional level with quality hardware from Zbotic.in’s Arduino & Microcontrollers store — browse our complete selection of Arduino boards, sensors, and accessories, all with fast delivery across India.
Add comment