Unit F - Assessment & Reference

32. Build Your Own Library

Worked example: package thermistor maths as a reusable Arduino library. Best after Lesson 20.

Estimated time 2-3 hours

Project: Creating the SimpleThermistor Library

Prerequisites: Basic Arduino C  •  analogRead()  •  Serial.print()

This guide is designed to help students transition from writing standard Arduino code to creating their first C++ library. It follows a methodical, "empty-shell first" approach to ensure the folder structure and file relationships are understood before diving into the logic.

This tutorial will guide you through creating a library that calculates temperature from a thermistor using a voltage divider circuit.

You already write Arduino sketches in C - analogRead(), Serial.print(), delay(). None of that knowledge disappears. A C++ library is simply a way of wrapping your code into a reusable, self-contained tool that any sketch can use in one line.

Everything new in this exercise comes from one concept - a class. A class is a template that bundles related data and functions together. You have already used classes every time you wrote:

Serial.begin(9600);   // Serial is an object - an instance of HardwareSerial
Serial.println("hi"); // .println() is a member function of that object

Your SimpleThermistor class will work the same way. The sketch will create an object and call its functions with the dot operator:

SimpleThermistor mySensor(A0, 10000, 10000);
float t = mySensor.getCelsius();

Overall workflow: design each file as a skeleton first, then fill in the logic.

Phase File What you do The analogy
1 All three files Create empty files in the right folders Lay the foundations before building
2 SimpleThermistor.h Skeleton, then add the class block Draw the blueprint
3 SimpleThermistor.cpp Skeleton, then add each function Wire up the machinery
4 Test .ino sketch Write the sketch that uses the library Flip the switch
Colour key for code blocks: grey italic text = comments / explanations  •  coloured keywords and names = code you type  •  comments like // <-- goes here --> = placeholders to replace later

Phase 1: Creating the Folder Structure

Before writing any code, we must set up the environment so the Arduino IDE can "see" your new library.

  1. Locate your Arduino Libraries folder. This is usually found in Documents > Arduino > libraries.
  2. Create a new folder named exactly SimpleThermistor.
  3. Inside that folder, create two empty text files. Rename them exactly as follows:
    • SimpleThermistor.h (This is the Header file - the "Table of Contents").
    • SimpleThermistor.cpp (This is the Source file - the "Story").

Arduino IDE looks for libraries in a specific location. The folder name must match the class name exactly.

Operating systemLibraries folder
WindowsDocuments\Arduino\libraries\
macOS~/Documents/Arduino/libraries/
Linux~/Arduino/libraries/

After creating the library folder, you should have:

  • Arduino/libraries/SimpleThermistor/SimpleThermistor.h - declares the class shape
  • Arduino/libraries/SimpleThermistor/SimpleThermistor.cpp - contains all the logic

Sketches live separately from the library. In your Arduino sketchbook folder, also create:

  • Arduino/SimpleThermistorTest/SimpleThermistorTest.ino - the test sketch that will prove the library works
Windows tip: Make sure file extensions are visible (View → File name extensions). The file must be SimpleThermistor.h, not SimpleThermistor.h.txt.
Checkpoint: Three empty files exist in the correct locations. Nothing compiles yet - that is expected. You are building the scaffold before the walls.

Phase 2: Defining the Header (.h)

The header file tells the Arduino IDE what functions and variables exist in your library. We start here to define our "contract."

The header file (.h) is the contract. It tells the Arduino IDE - and any sketch that includes it - exactly what the library provides: what data it needs, and what functions it offers. There is no working code here yet, only declarations.

We build this file in two steps: skeleton first, class block second.

Step 1: Open SimpleThermistor.h and add the "include guards."

These prevent the library from being loaded twice.

#ifndef SimpleThermistor_h
#define SimpleThermistor_h

#include "Arduino.h"

// The class definition will go here

#endif

#ifndef / #define / #endif - These three lines form an include guard. They prevent the file from being processed twice if multiple files happen to include it. Always start and end every .h file this way. The name after #ifndef must be unique - by convention it mirrors the filename.

#include "Arduino.h" - This gives the library access to Arduino-specific types and functions (int, float, pinMode, analogRead, and the pin constant A0). Use double quotes for your own files; angle brackets < > are for system libraries.

Step 2: Define the Class.

We need to store the pin, the resistor value, and the thermistor's nominal resistance.

class SimpleThermistor {
  public:
    // Constructor: This runs when the user creates the object
    SimpleThermistor(int pin, long seriesResistor, long thermistorNominal);

    // Functions to get temperature
    float getCelsius();
    float getFahrenheit();

  private:
    int _pin;
    long _seriesResistor;
    long _thermistorNominal;
};

public: Everything listed here is accessible from the sketch. This is your library's visible interface - the buttons on the front panel.

private: Everything here is hidden from the sketch. The sketch cannot read or change these directly. The underscore prefix on _pin, _seriesResistor, and _thermistorNominal is a convention - it signals that these are private member variables.

There is no code inside getCelsius() or getFahrenheit() yet - only their signatures. A declaration tells the compiler the function exists and what types it accepts and returns. The actual logic comes in Phase 3.
Note the semicolon after the closing brace of the class - required in C++: };
Checkpoint: Save SimpleThermistor.h. If you restart Arduino IDE it will detect the library in its list - although it will not yet compile because the .cpp is still empty.

Phase 3: Writing the Logic (.cpp)

Now we fill in the "how" for the functions we defined in the header.

The .cpp file contains the actual working code for every function you declared in the header. The pattern for each function is:

ReturnType ClassName::functionName(parameters) {
  // code here
}

The double-colon :: is the scope resolution operator - it tells the compiler which class this function belongs to. Every function definition in this file must have it.

Again we build skeleton first, then fill in each function one at a time so you can focus on one idea at a time. A useful starting skeleton is:

#include "SimpleThermistor.h"
#include <math.h> // needed for log() in the Beta equation

// <-- Constructor goes here in Step 1 -->

// <-- getCelsius() goes here in Step 2 -->

// <-- getFahrenheit() goes here in Step 3 -->

Why #include "SimpleThermistor.h" with double quotes? Because it is your own file sitting in the same folder. Double quotes tell the compiler to search the local folder first. Angle brackets < > are for system libraries on the IDE's search path.

Step 1: Link the header and write the Constructor.

This maps the user's inputs to our internal private variables.

#include "SimpleThermistor.h"
#include <math.h> // Needed for logarithmic calculations

SimpleThermistor::SimpleThermistor(int pin, long seriesResistor, long thermistorNominal) {
  _pin = pin;
  _seriesResistor = seriesResistor;
  _thermistorNominal = thermistorNominal;
}

The constructor runs once, automatically, the moment the sketch creates the object. Its job is simple: store the three values the sketch passes in so the other functions can use them later.

Each line copies one argument into its matching private member variable. The argument names (pin, seriesResistor, thermistorNominal) live only inside this constructor. The _pin, _seriesResistor, _thermistorNominal versions survive for the lifetime of the object and are accessible by getCelsius() and getFahrenheit().

Step 2: Write the getCelsius logic.

We will use a simplified calculation in three clear steps: read the pin, find resistance, then convert to temperature.

float SimpleThermistor::getCelsius() {
  int analogVal = analogRead(_pin);

  // Step 1: resistance of the thermistor (voltage divider)
  float resistance = _seriesResistor / (1023.0 / analogVal - 1.0);

  // Step 2: beta equation → temperature in Kelvin
  // (25 C = 298.15 K, B = 3950)
  float tempK = 1.0 / (1.0 / 298.15 + log(resistance / _thermistorNominal) / 3950.0);

  // Step 3: Kelvin → Celsius
  return tempK - 273.15;
}

This is the heart of the library. It performs three calculations in sequence:

Step A - Read the pin. analogRead() returns a value from 0 to 1023 that represents the voltage at the pin (0 V = 0, 5 V = 1023). You have used this exact function before.

Step B - Voltage divider algebra. The circuit places the series resistor and the thermistor in series across 5 V. The pin reads the junction voltage. Rearranging the voltage divider formula gives:

R_therm = R_series / (1023 / adcVal − 1)

The 1023.0 (with a decimal) forces floating-point division so no precision is lost.

Step C - The Beta equation. This is a simplified form of the Steinhart-Hart equation that is accurate enough for classroom use. One line converts resistance to Kelvin (25 °C = 298.15 K, B = 3950); the next subtracts 273.15 to arrive at Celsius. The constant 3950.0 is the Beta value - a property of the thermistor that should match its datasheet.

About the Beta constant 3950: This value suits most cheap NTC thermistors sold for Arduino kits. If your readings are consistently several degrees wrong, look up the Beta (B25/85) value on your thermistor's datasheet and replace 3950.0 with the correct number.

Step 3: Write the getFahrenheit logic.

Instead of re-calculating everything, we simply call our existing Celsius function!

float SimpleThermistor::getFahrenheit() {
  return (getCelsius() * 9.0 / 5.0) + 32.0;
}

Here is where C++ starts to pay off. Instead of re-doing all the mathematics, you call the member function you just wrote and convert the result. One line - no repeated maths.

Checkpoint: All placeholder comments are gone. Save SimpleThermistor.cpp. The library is now complete and will compile. The last remaining file is the test sketch.

Checkpoint Summary - Full Library Files

Before moving on, pause and make sure your two library files match the complete versions below. These are what Phases 2 and 3 built, piece by piece.

Full file: SimpleThermistor.h

#ifndef SimpleThermistor_h
#define SimpleThermistor_h

#include "Arduino.h"

class SimpleThermistor {
  public:
    // Constructor: This runs when the user creates the object
    SimpleThermistor(int pin, long seriesResistor, long thermistorNominal);

    // Functions to get temperature
    float getCelsius();
    float getFahrenheit();

  private:
    int _pin;
    long _seriesResistor;
    long _thermistorNominal;
};

#endif

Full file: SimpleThermistor.cpp

#include "SimpleThermistor.h"
#include <math.h> // Needed for logarithmic calculations

SimpleThermistor::SimpleThermistor(int pin, long seriesResistor, long thermistorNominal) {
  _pin = pin;
  _seriesResistor = seriesResistor;
  _thermistorNominal = thermistorNominal;
}

float SimpleThermistor::getCelsius() {
  int analogVal = analogRead(_pin);

  // Step 1: resistance of the thermistor (voltage divider)
  float resistance = _seriesResistor / (1023.0 / analogVal - 1.0);

  // Step 2: beta equation → temperature in Kelvin
  // (25 C = 298.15 K, B = 3950)
  float tempK = 1.0 / (1.0 / 298.15 + log(resistance / _thermistorNominal) / 3950.0);

  // Step 3: Kelvin → Celsius
  return tempK - 273.15;
}

float SimpleThermistor::getFahrenheit() {
  return (getCelsius() * 9.0 / 5.0) + 32.0;
}

Phase 4: Using the Library

Now, create a new Arduino Sketch (.ino) to test your work.

  1. Restart the Arduino IDE so it scans the new library folder.
  2. Write the following sketch:
#include <SimpleThermistor.h>

// Setup: Pin A0, 10k ohm series resistor, 10k ohm thermistor
SimpleThermistor mySensor(A0, 10000, 10000);

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.print("Celsius: ");
  Serial.println(mySensor.getCelsius());

  Serial.print("Fahrenheit: ");
  Serial.println(mySensor.getFahrenheit());

  delay(1000);
}

The sketch is written last because it consumes the library - you cannot use a tool that does not exist yet. Apart from the #include line and the object creation line, this file contains nothing new.

Before uploading, restart Arduino IDE so it rescans the libraries folder and picks up SimpleThermistor.

The new lines explained:

  • #include <SimpleThermistor.h> - Loads your library. Angle brackets tell the IDE to search the libraries folder.
  • SimpleThermistor mySensor(A0, 10000, 10000); - Creates an object called mySensor. This calls the constructor with three arguments: the pin, the series resistor value (10 000 Ω), and the thermistor's nominal resistance (10 000 Ω). Adjust these numbers to match your actual components.
  • mySensor.getCelsius() - Calls the member function you wrote in Phase 3. The dot operator is the same one you already use on Serial.

Upload and test:

  1. Verify (✓) first - fix any compile errors before uploading
  2. Upload (→) to your board
  3. Open Serial Monitor - Tools → Serial Monitor - baud rate 9600
  4. You should see Celsius and Fahrenheit readings update every second

Troubleshooting

Compile error: no such file - SimpleThermistor.h
Restart the IDE. Confirm the folder is exactly Documents/Arduino/libraries/SimpleThermistor/ and the file is named SimpleThermistor.h (not .h.txt). Folder name spelling must match.
Temperature reads −273 or a huge number
Usually a wiring problem: open connection, thermistor/resistor swapped, or A0 not connected to the junction. Recheck: 5V → resistor → junction (to A0) → thermistor → GND. Also avoid dividing by zero if the ADC reading is stuck at 0.
Readings are consistently several degrees off
Check that constructor values match your real parts (10000, 10000 for two 10k components). If still off, update the Beta constant 3950.0 from your thermistor datasheet (B25/85).

Summary of Workflow for Students:

  • Structure: Create the folder and empty files so the IDE recognizes the library.
  • Header (.h): Define the "Shape" of your tool (What inputs does it need? What outputs does it give?).
  • Source (.cpp): Define the "Brain" (How does it math?).
  • Sketch (.ino): Use the tool in a real project.

Every Arduino library you ever write will follow the same four-phase pattern:

Phase File The question you answer The analogy
1 All files Where do the files live and what are they called? Lay the foundations
2 .h header What inputs does my tool need? What outputs does it give? Draw the blueprint
3 .cpp source How does the maths actually work? Wire up the machinery
4 .ino sketch Does the tool work in a real sketch? Flip the switch

And within the header/source phases, the micro-pattern is always:

  1. Write the skeleton with placeholder comments first
  2. Replace placeholders one at a time, reading the explanation before typing the code
  3. Save and check at each step rather than writing everything and debugging at the end

C++ Concepts Introduced

Concept Syntax Plain-English meaning
Include guard #ifndef / #define / #endif Prevent a header being loaded twice
Class declaration class SimpleThermistor { }; Blueprint for a new type of object
Constructor SimpleThermistor::SimpleThermistor() Runs once when the object is created
Member variables int _pin; Data stored privately inside the object
public / private public: ... private: ... Controls what the sketch can and cannot see
Scope resolution SimpleThermistor::getCelsius() Links a function definition to its class
Object creation SimpleThermistor mySensor(...); Makes a live instance of the class
Member function call mySensor.getCelsius() Runs a function that belongs to an object

Extension Challenges

Once your library is working, try these to go further:

  • Add a getKelvin() function that returns the raw Kelvin value - no conversion needed
  • Add a constructor parameter for a custom Beta value instead of hard-coding 3950.0
  • Create a second object on pin A1 - you get a second independent sensor for free
  • Add a function bool isAbove(float threshold) that returns true if the temperature exceeds the given value

End of guide - good luck with your first C++ library!

Hardware Setup: Connection Diagram

Wire a voltage divider so the Arduino can measure the thermistor. Use a 10kΩ series resistor and a 10kΩ NTC thermistor - matching the sketch arguments (A0, 10000, 10000).

Arduino Uno connected to a 10k thermistor and 10k resistor voltage divider on a breadboard

How to connect it

  1. 5V (red) - Arduino 5V to one end of the 10kΩ series resistor.
  2. Series resistor - Other end of the resistor becomes the junction (middle of the divider).
  3. Thermistor - One leg to that same junction; the other leg to GND (black wire / blue rail).
  4. Signal (pink) - From the junction to Arduino analog pin A0.
  5. GND (black) - Arduino GND to the breadboard ground rail.

Circuit path: 5V → 10k resistor → junction (to A0) → thermistor → GND

As temperature changes, the thermistor’s resistance changes, so the voltage at A0 changes. That ADC reading is what getCelsius() converts into temperature.

Circuit reminder: 5 V → [series resistor R] → (analog pin) → [thermistor NTC] → GND. The analog pin reads the voltage at the junction between the two components. This matches the resistance formula used in getCelsius():

resistance = _seriesResistor / (1023.0 / analogVal - 1.0)