Skip to content

Arduino - String Object

In Arduino programming, you can work with text in two primary ways: the C-style character array (a.k.a. C-string) and the String object. This tutorial focuses on the String object, a more user-friendly but resource-intensive way to handle text.

In Object-Oriented Programming (OOP), an object is a structure that bundles data (properties) and functions that operate on that data (methods). The Arduino String object holds a sequence of characters as its data and provides a rich set of methods to manipulate that text, such as converting to uppercase, replacing substrings, and more.

While powerful, the String object’s convenience comes at a cost, especially on memory-constrained microcontrollers like the Arduino Uno. It uses dynamic memory allocation, which can lead to a problem called heap fragmentation. We will explore this and best practices below.

Practical Example: A Simple Serial Command Parser

Section titled “Practical Example: A Simple Serial Command Parser”

Let’s create a sketch that listens for simple commands like LED ON or LED OFF from the Serial Monitor to demonstrate the String object’s capabilities.

// Pin for the built-in LED
const int ledPin = LED_BUILTIN;
// A String object to store incoming data from the serial port
String command;
void setup() {
// Initialize serial communication at 9600 bits per second:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// Configure the LED pin as an output
pinMode(ledPin, OUTPUT);
// Pre-allocate memory for the command String to reduce heap fragmentation.
// This is a crucial best practice!
command.reserve(32);
Serial.println("Arduino Command Parser Ready");
Serial.println("Enter 'LED ON' or 'LED OFF'");
}
void loop() {
// Check if there is data available to read from the serial port
if (Serial.available() > 0) {
// Read the incoming bytes until a newline character is received
command = Serial.readStringUntil('\n');
// Remove any leading/trailing whitespace
command.trim();
// Convert the command to uppercase to make matching case-insensitive
command.toUpperCase();
Serial.print("Received command: ");
Serial.println(command);
// Process the command
if (command == "LED ON") {
Serial.println("Executing: Turning LED ON");
digitalWrite(ledPin, HIGH);
} else if (command == "LED OFF") {
Serial.println("Executing: Turning LED OFF");
digitalWrite(ledPin, LOW);
} else {
Serial.println("Error: Unknown command.");
printHelp();
}
// (Optional) Get and print the length of the last command
Serial.print("Command length was: ");
Serial.println(command.length());
// Clear the string for the next command
command = "";
}
}
// A helper function to print available commands
void printHelp() {
Serial.println("Available commands: 'LED ON', 'LED OFF'");
}

After uploading the sketch, open the Serial Monitor (set to 9600 baud and ‘Newline’ line ending). If you type led on and press Enter, you will see:

Received command: LED ON
Executing: Turning LED ON
Command length was: 6
  • reserve(size): A best-practice method to pre-allocate a buffer of a certain size in memory. This helps prevent heap fragmentation by finding one block of memory upfront rather than constantly asking for new, differently sized blocks.
  • readStringUntil(char): Reads characters from the serial buffer into a String until a specific character (in our case, the newline \n) is found.
  • trim(): Removes any whitespace characters (like spaces, tabs) from the beginning and end of the String.
  • toUpperCase(): Converts all alphabetic characters in the String to their uppercase form. This is great for making command parsing case-insensitive.
  • length(): Returns the number of characters in the String, excluding the null-terminator.
  • Comparison (==): String objects can be directly compared using operators like ==, which is much simpler than the strcmp() function used for C-style strings.

The String Dilemma: When and When Not to Use It

Section titled “The String Dilemma: When and When Not to Use It”
  • Ease of Use: The rich set of built-in methods makes text manipulation intuitive and requires less code.
  • Simplicity: Operations like concatenation (+) and comparison (==) are straightforward.
  • Automatic Memory Management: You don’t need to manually manage buffer sizes (though this is also its biggest weakness).

Disadvantages and Dangers (Especially on AVR boards like Uno/Nano)

Section titled “Disadvantages and Dangers (Especially on AVR boards like Uno/Nano)”
  • Heap Fragmentation: Repeatedly creating and destroying String objects can leave small, unusable gaps in the Arduino’s limited RAM (SRAM). Eventually, the system may fail to find a contiguous block of memory large enough for a new allocation, causing your program to crash or behave erratically, even if there is technically enough total memory free.
  • High Memory Overhead: String objects use more memory than their C-string counterparts.
  • Hidden Complexity: The simplicity of the String object can hide performance-intensive operations, making it hard to debug performance issues.

To write robust and stable Arduino code, follow these guidelines:

  1. Avoid String in loops: Never create or modify String objects repeatedly inside the main loop() if you can help it. This is a primary cause of heap fragmentation.
  2. Use reserve(): If you must use a String object, use reserve() in setup() to pre-allocate a fixed, generous amount of memory for it.
  3. Prefer C-style strings for reliability: For mission-critical or long-running applications, learn to use null-terminated character arrays (char myStr[32];). They give you full control over memory and are not susceptible to fragmentation. You can manipulate them with standard C library functions like strcpy(), strcat(), and strcmp() from the <string.h> library.
  4. Consider board choice: On more powerful 32-bit boards (like an ESP32 or Arduino Due) with significantly more RAM, the risks associated with String objects are lower, but good memory management is still a valuable skill.