Skip to content

Instantly share code, notes, and snippets.

@Sv443
Last active February 27, 2025 23:18
Show Gist options
  • Save Sv443/4cbbd86ff672e8efae912afb58fb0e0b to your computer and use it in GitHub Desktop.
Save Sv443/4cbbd86ff672e8efae912afb58fb0e0b to your computer and use it in GitHub Desktop.
Arduino ESP32 C++ Cheat Sheet

Arduino ESP32 C++ Cheat Sheet



IDE

  • I highly recommend you use PlatformIO with VSCode. It's much better than the Arduino IDE.
    Here's a guide that walks you through it.
  • When creating a new project with PlatformIO, select your board and framework (Arduino) and it will automatically include the necessary libraries and files for you.
    Keep the monitor_speed in platformio.ini in mind. This is the baud rate (amount of bits per second) for the serial monitor. A good value is 115200 (115.2kHz bandwidth).
  • Set up the serial monitor in the bottom bar of VSCode by clicking on the "No device" text and selecting the correct port. Also ensure the BAUD rate matches the monitor_speed above and the Serial.begin() call in your sketch.
  • You can also use the Arduino IDE, but it is less feature-rich and more cumbersome to use.
    You can download it here.



Introduction

There are two entrypoint functions, setup() and loop().

  • setup() is called once when the program starts.
    This is where you initialize your hardware libraries, set up the mode of each pin (input or output), load NVS (see concepts section) values, etc.
  • loop() is called repeatedly after setup() is done.
    This is where you put your main program logic.
    You should avoid using blocking functions (delay, etc.) as much as possible, since the program runs in a single, synchronous thread.
    Instead, use non-blocking methods (millis, etc.) or hardware interrupts. For example:
    #ifndef String
      #include <Arduino.h>
    #endif
    
    void setup() {}
    
    void loop() {
      static unsigned long lastMillis = 0;
      if(millis() - lastMillis >= 1000) {
        lastMillis = millis();
        // Do something every second
      }
    }
  • Variable types are the same as in C++, but I recommend using specific types for the integers, since the memory footprint is so important.
    For example, if you want to store a simple percentage, use uint8_t instead of int. For comparing timestamps, you probably want to use either uint32_t or uint64_t.
  • When you create extra files, I recommend you stick to the extensions .cpp and .hpp and include a #pragma once at the top of the .hpp files, so it is only included once.
  • For interfacing with modules, you should look for a good recently updated library and download it via the IDE.
    For example, if you want to use a GPS module, search for "GPS" in the library manager and install a library that has a lot of downloads.
    Usually, libraries will have example sketches bundled with them that you can either directly upload to your ESP or use as a reference for your own code.
  • If you want to use a library that is not in the library manager, you can download it from GitHub and include it in your project folder.
    The default include path also contains Documents/Arduino/libraries/, so that is a good place to put your libraries.



Concepts

  • Specifying pins can be a bit trickier on ESP boards, since the pin numbers that are printed onto the module are the GPIO pins numbers, not the physical pin numbers.
    For example, the ESP32 DevKit has a pin labeled "IO2", which is GPIO2, but is physically pin 25.
    You can find a pinout diagram for your board by searching for "ESP32 pinout diagram". Most sellers provide one somewhere on their product page.
  • The USB serial monitor is like a developer console. It can be opened from the IDE and from your script, you can print values to it to debug the board while it's plugged in via USB and even read input from it.
    For most intents and purposes, it behaves like a regular TTY.
  • The ESP32 has a dual-core processor, which means it can run two threads at the same time.
    Example:
    #ifndef String
      #include <Arduino.h>
    #endif
    
    void core0Task(void *parameter) {
      while(true) {
        Serial.println("Core 0");
        delay(1000);
      }
    }
    
    void core1Task(void *parameter) {
      while(true) {
        Serial.println("Core 1");
        delay(1000);
      }
    }
    
    void setup() {
      Serial.begin(115200);
      xTaskCreatePinnedToCore(core0Task, "Core 0 Task", 10000, NULL, 1, NULL, 0);
      xTaskCreatePinnedToCore(core1Task, "Core 1 Task", 10000, NULL, 1, NULL, 1);
    }
    
    void loop() {}
  • Since the sketch runs on very limited hardware, you should avoid using dynamic memory allocation (new, malloc, etc.) as much as possible.
    Instead, use static memory allocation (global variables, etc.) or stack memory allocation (local variables, etc.).
    Also always remember to free memory (like pointers) when you're done with them, because a memory leak is much more significant on a microcontroller than on a PC.
  • Try to use #define as much as possible instead of declaring variables, since program flash is much more abundant than RAM.
    The ESP32 has 4MB of flash memory, but only a few hundred KB of RAM.
  • The ESP32 has a builtin encrypted non-volatile storage (NVS), which you can use to persistently store data.
    This is useful for storing settings, calibration data, etc. that you want to keep even after a power cycle.
    Do note that writing and reading from the NVS is relatively slow.
    Example:
    #ifndef String
      #include <Arduino.h>
    #endif
    #include <Preferences.h>
    
    Preferences storage;
    
    void setup() {
      Serial.begin(115200);
    
      storage.begin("my-app", false); // create the NVS namespace "my-app" with unencrypted keys (data itself is still encrypted)
      storage.putUInt("my-key", 123); // write a value
    
      uint32_t value = storage.getUInt("my-key", 0); // read a value
      
      Serial.println(value);
    }
    
    void loop() {}
  • The ESP firmware doesn't come with Arduino support built in, so you have to include the Arduino core library yourself.
    This is done by including the Arduino.h header at the top of your sketch, like this:
    #ifndef String
      #include <Arduino.h>
    #endif
    The core libraries include many useful functions and classes.
    You can find some examples outlined below, and the full reference here.



Reference

  • Use Serial for serial communication over the builtin USB port:
    Serial.begin(115200);
    Serial.println("Hello, world!");
    Serial.printf("Value: %.2f\n", 3.14159);
  • Math functions, like abs(), min(), max(), pow(), sqrt(), clamp(), map(), etc.
  • Use millis() or micros() for getting the number of milliseconds or microseconds since the program started.
  • Use delay() or delayMicroseconds() for pausing the program for a certain amount of time.
  • Use String() as an actual string class comparable to that of JavaScript or C#.
    Try to use String objects sparingly, because they are generally slower than C-style strings and can cause memory fragmentation.
    Here are some examples:
    // String only exists in the Arduino core libs, so they have to be included if
    // they're missing (which they usually are on ESPs):
    #ifndef String
      #include <Arduino.h>
    #endif
    
    String myString = "Hello, world!";
    myString += " How are you?";
    
    // cut off the first 16 characters
    myString = myString.substring(16);
    
    // find the index of the first space
    int spaceIndex = myString.indexOf(' ');
    
    // get the substring from the start to the space
    String firstWord = myString.substring(0, spaceIndex);
    
    // print the first word
    Serial.println(firstWord);
    
    // convert the string to a C-style string
    const char* cString = firstWord.c_str();
    
    // convert the c-style string back to a String
    String newString = String(cString);
    
    // convert numbers and other types to a string
    String numberString = String(123);
    String floatString = String(3.14159, 2);
    String hexString = String(0x1234, HEX);
    String binaryString = String(0b1010, BIN);
    String charString = String('A');
    String boolString = String(true);
  • random() and randomSeed() are for generating random numbers, although the ESP32 has a hardware random number generator.
    Use them like so:
    // generic Arduino random:
    void setup() {
      // read from a floating pin, so the fluctuating voltage can be used as a seed
      randomSeed(analogRead(0));
    }
    
    uint8_t randomValFoo = random(0, 100);
    
    
    // ESP32 hardware random:
    #include <esp_random.h>
    
    uint8_t randomValBar = map(esp_random(), 0, UINT32_MAX, 0, 100);
  • pinMode() is used to set a pin to input or output mode.
  • Use analogRead() and analogWrite() for reading and writing analog electrical values to a certain pin.
    The writing is done via a (square wave) PWM (pulse width modulation) signal and is not a true (sinusoidal) analog output.
  • Use digitalRead() and digitalWrite() for reading and writing digital electrical values to a certain pin.
    This basically reads a 0 or 1 from a pin or sets it to 0 (0V) or 1 (supply voltage, usually 3.3V or 5V).
    For example:
    #ifndef String
      #include <Arduino.h>
    #endif
    
    void setup() {
      pinMode(2, INPUT_PULLUP); // set pin 2 to input mode with a builtin pull-up resistor that prevents the voltage from floating and pins it to 3.3V or 5V by default
      pinMode(3, OUTPUT);       // set pin 3 to output mode
    }
    
    void loop() {
      if(digitalRead(2) == LOW) // read the state of pin 2
        digitalWrite(3, HIGH);  // set pin 3 to HIGH
      else
        analogWrite(3, 128);    // set pin 3 to 50% duty cycle (128/255)
    }
  • With attachInterrupt() and detachInterrupt() you can set up and remove hardware interrupts, basically define a function that gets called whenever the electrical state on a given pin changes.
    This is useful for reading sensors, buttons, etc. without having to constantly poll them in the loop().
    In interrupt handlers however, you can not use functions that are not interrupt-safe (like Serial.print), so it is best to set a flag in the interrupt handler and handle the actual work in the loop().
    Example:
    #ifndef String
      #include <Arduino.h>
    #endif
    
    #define BTN_PIN 2;
    
    volatile bool buttonWasPressed = false;
    void buttonPressed() {
      // only set the flag in the interrupt handler
      buttonWasPressed = true;
    }
    
    void setup() {
      Serial.begin(115200);
      pinMode(BTN_PIN, INPUT_PULLUP);
      // call buttonPressed() whenever BTN_PIN goes from HIGH to LOW
      // to achieve this electrically, the GPIO pin needs to be connected to a push button that will connect it to the GND pin when pressed
      attachInterrupt(digitalPinToInterrupt(BTN_PIN), buttonPressed, FALLING);
    }
    
    void loop() {
      // handle in loop() because Serial.print is not interrupt-safe
      if(buttonWasPressed) {
        buttonWasPressed = false;
        Serial.println("Button was pressed");
      }
    }



Communication protocols

Most modules connect to the ESP in one of the following ways:

  • I2C:
    Bi-directional bus topology communication.
    Uses two pins (SDA and SCL) to communicate with daisy-chained devices. SDA stands for serial data, SCL is the serial clock.
    Pros: Supports up to 127 daisy-chained devices, only requires 2 pins.
    Cons: Requires pull-up resistors, can get congested and slow with more devices.
    See also this article on the basics of I2C.
  • Serial/UART:
    Bi-directional star topology communication.
    Uses two pins (TX and RX) to communicate. TX of the host goes to RX of the module and vice versa.
    Example: GPS modules, Bluetooth modules, etc.
    Pros: Simple, easy to implement.
    Cons: Requires 2 pins per module, can only communicate with one module at a time.
    See also this article on the basics of UART.
  • SPI:
    Bi-directional bus topology communication.
    Uses 4 pins (POCI/MISO, PICO/MOSI, SCK, CS/SS) to communicate with daisy-chained devices. MISO stands for master in slave out, MOSI is master out slave in, SCK is the Serial ClocK, which dictates the transmission speed. SS is the Slave Select, which allows the microcontroller to toggle communication with this specific device on and off.
    Newer terms are POCI (Peripheral Output Chip Input) instead of MISO, PICO (Peripheral Input Chip Output) instead of MOSI and CS (Chip Select) instead of SS.
    Pros: Fast, supports multiple devices, only requires 4 pins.
    Cons: Requires an extra pin per device, can get congested with more devices.
    See also this article on the basics of SPI.
  • WiFi/BT LE:
    Also, the ESP can communicate via WiFi and Bluetooth LE out of the box.
    There are some example sketches in the IDE that show how to use these protocols.

Comparing the different protocols:

Protocol Efficiency Latency Bandwidth Notes
I2C Moderate High Up to 5 MHz Good for multi-device communication, but speed is limited by pull-ups.
Serial/UART Low High Up to several Mbps Simple, but suffers from high overhead and potential synchronization issues.
SPI High Low Up to 50+ MHz Best for high-speed communication but requires more pins.



Electronics Basics

As a general companion app for calculating some useful stuff and acting as a simple reference, I recommend you download the ElectroDoc app.

  • Terms:
    • Anode:
      The positive terminal (plus pole) of a component, like on a battery or LED.
      When connecting a voltage source to a component, both anodes are connected together.
    • Cathode:
      The negative terminal (minus pole) of a component, like on a battery or LED.
      When connecting a voltage source to a component, both cathodes are connected together.
    • Ground/GND:
      The reference point in a circuit, usually equivalent to 0V.
      All components are connected to ground, so the voltage across them is measured relative to ground.
    • V/VCC/VDD/3V3/5V:
      The supply voltage in a circuit, usually 3.3V or 5V.
      All components are connected to VCC, so the voltage across them is measured relative to VCC.
    • DC:
      Direct current, like the current from a battery or wall adapter.
      The current flows in one direction only, for an electrician, it flows from the positive terminal to the negative terminal.
      In physics, electrons flow from the negative terminal to the positive terminal.
    • AC:
      Alternating current, like the current inside a wall outlet.
      The current changes direction periodically, usually 50 or 60 times per second.
      The voltage is usually sinusoidal, but can theoretically be square, triangular, etc.
      The frequency is usually 50Hz or 60Hz, but can also vary.
      Calculating power in an AC circuit is more complicated than in a DC circuit, because the voltage and current are not constant.
      The ElectroDoc app has a calculator for this.
    • PWM:
      Pulse Width Modulation, usually used to control the brightness of an LED, the speed of a brushless motor, etc.
      The voltage is pulsed between 0V and the supply voltage, with the duty cycle determining the average voltage.
      A duty cycle of 25% means the voltage is HIGH 25% of the time and LOW 75% of the time, switched at a high frequency.
    • Logic levels:
      The voltage levels that represent a digital signal, usually 0V for LOW and the supply voltage for HIGH.
      The ESP32 uses 0V for LOW and 3.3V for HIGH, but some boards can use 5V for HIGH.
      The logic levels of most external modules support 3.3V, but some require a very specific voltage, so you might need to use a level shifter to communicate with them.
  • Units:
    • Voltage:
      The difference in electrical potential between two points, like the pressure in a water pipe.
      Measured in volts (V).
      The ESP32 usually runs on 3.3V, but some boards can run on 5V.
      The voltage of pins can only be set to 0V (LOW) or the supply voltage (HIGH).
      When using PWM, the voltage is pulsed between 0V and the supply voltage.
    • Current:
      The flow of electrical charge, like how much water passes through a pipe per second.
      Measured in amperes (A).
      The ESP32 can only source or sink a limited amount of current per pin, usually around 10-20mA.
      This is enough for an LED and to communicate with most modules, but not enough to drive a motor or a relay.
      If you need more current, you need to use a transistor to switch the current directly from the supply voltage.
    • Resistance:
      The opposition to the flow of electrical current, like the diameter of a water pipe, or the friction and turbulence.
      Measured in ohms (Ω).
      Generally speaking, the higher the resistance, the less current flows. It's not quite as simple as the electricity taking the path of least resistance, but it's still a good analogy.
    • Power:
      The rate at which electrical energy is transferred by an electric circuit, like how much water flows through a pipe per second.
      Measured in watts (W).
      For DC (direct current) circuits, power is calculated by multiplying the voltage by the current, so 5V and 1A equals 5W.
    • Capacity:
      The amount of electrical charge stored in a battery or capacitor, like the amount of water a tank can hold.
      Measured in ampere-hours (Ah), milliwatt-hours (mWh), farads (F), etc.´ Knowing the capacity in mWh and the power draw of your circuit in mW allows you to calculate how long a li-ion battery will last, for example.
  • Components:
    • Resistor:
      Limits the current flow in a circuit, like a narrow segment in a thicker water pipe.
      Usually they are used to limit the current to an LED or transistor, so it doesn't burn out.
      Look up led/transistor series resistor calculator to find calculators that tell you which resistor to use with a certain LED or transistor and voltage.
    • Transistor:
      Amplifies or switches electrical signals of different characteristics (like a low-current switching circuit and higher current load).
      There are two types: NPN and PNP.
      NPN transistors are used to switch a load to ground, while PNP transistors are used to switch a load to the supply voltage.
      Their implementation differs slightly, but the basic principle is the same.
      Send power to the base / gate pin to allow current to flow from the collector / source pin to the emitter / drain pin.
    • Diode:
      Allows current to flow in one direction only, like a one-way valve.
      They are used to protect circuits from reverse polarity, to rectify AC to DC, etc. (i.e. a FULL-BRIDGE RECTIFIER!!!)
      Diodes have a forward voltage drop, which is usually around 0.7V for silicon diodes, meaning that the voltage across the diode is 0.7V lower than the supply voltage.
      This also means they need at least 0.7V to conduct, so they can't be used with voltages lower than that.
    • Capacitor:
      Stores electrical energy in an electric field, like a tank that fills up with water.
      They are often used to smooth out voltage spikes, filter out noise, etc.
      To do this, a capacitor is connected in parallel (plus to plus, minus to minus) to the power supply, so it can absorb and release energy quickly on demand.
      Capacitors have a capacitance value, measured in farads (F), but usually in microfarads (µF) or picofarads (pF).
      The higher the capacitance, the more energy it can store, and the longer it takes to charge and discharge.
      Capacitors also have a voltage rating, which is the maximum voltage they can handle before breaking down.
    • Lithium battery:
      Single-cell lithium batteries have a nominal voltage of 3.7V and a maximum voltage of 4.2V.
      They need to be charged via a special constant-current and constant-voltage charger, since they can catch fire or explode if overcharged.
      They also need protection circuits to prevent over-discharge, over-charge, short-circuits, etc.
      I recommend using a module like the TC-4056A, if your dev board doesn't have an onboard charging circuit.
      If you need more voltage, either use a boost converter module to step up the voltage, or use multiple cells in series, which will however require a dedicated balance charger.
    • Voltage regulator:
      Converts a higher voltage to a lower voltage, like a transformer in a power supply.
      They are used to power the ESP32 from a higher voltage, like a 9V battery or a 12V power supply.
      They come in linear and switching varieties, with linear regulators being simpler and cheaper, but less efficient.
      The most common linear regulator is the LM7805, which converts 5V to 3.3V.
      The most common switching regulator is the LM2596, which can convert voltages up and down.
    • Level shifter:
      Sometimes it is necessary to use 5V logic levels with the ESP32, but some modules will only ever accept 3.3V logic levels and will burn out by communicating with them with 5V.
      For this purpose, you can use a level-shifter module, which converts 5V to 3.3V and vice versa.
    • Operational amplifier:
      Amplifies the difference in voltage between two inputs, like a magnifying glass for electrical signals.
      Also works as a comparator, comparing two voltages and outputting a high or low signal depending on which is higher.
      They are used to amplify weak signals, filter out noise, boost signal gain, etc.
      They have two inputs, an inverting and a non-inverting input, and one output.
      The output is the difference between the two inputs, multiplied by the gain of the amplifier.
    • Voltage divider:
      By connecting two resistors in series, you can create a voltage divider, which outputs a fraction of the input voltage.
      This is useful for creating a reference voltage, for example, or for measuring a voltage that is higher than the ESP32 can handle.
      Note that this isn't useful for supplying much current, as the resistors will heat up and the voltage will drop under load.
      To calculate which values of resistors you need, search for voltage divider calculator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment