A low-RAM alternative to Arduino's Serial for AVR-based Arduinos. Down to 9 bytes of RAM, vs. Serial's 175+ bytes RAM. (Arduino uses 9 bytes for the millis() and micros() functions in addition to these numbers.)
Arduino is really great at keeping things easy for you. DietSerial is a little more "low-level" than Serial, and it only works for the ATmega168A/PA/328/P/PB: the Uno R3, Nano, Pro, Pro Mini, and for breadboard Arduinos based on the ATmega328P chip.
Also, DietSerial uses the Watchdog Timer for receive timeout. If your sketch uses the Watchdog Timer for other purposes then DietSerial is not suitable. If you don't know, you're probably OK.
If your sketch only prints data to the PC, and does not receive data input from the serial interface, consider using SendOnlySerial instead. It is smaller and uses less RAM.
Serial permanently allocates two 64-byte buffers and 17 or so bytes of bookkeeping variables that permanently take up space in your sketch's RAM. The buffers are used to accumulate incoming bytes and to hold bytes to send out over the hardware serial interface. The bookkeeping variables are use to male Serial cross-platform, to keep track of the baud rate and data format, to keep track of how full the buffers are, and for timeout functionality.
For receiving data, you allocate a temporary buffer of the size you need, in your sketch as you do for Serial.readBytes(). DietSerial uses this same method for readBytes(), readString(), and readStringExcept(). DietSerial directly sets the baud rate in DietSerial.begin(), with a default value of 9600. It only has one data format, 8N1, so needs no storage for that, and it has no hidden buffers, instead reading directly from the hardware. DietSerial uses one byte for the timeout which is in seconds rather than milliseconds, so the maximum timeout is 255, with the default at 90 seconds.
Printing is similar to Serial.print() and Serial.println. Ther is also a DietSerial.write() function for sending arrays and floating-point and integer numers in binary format. and DietSerial.readBytes(), DietSerial.readFloat(), DietSerial.readInt(), DietSerial.readDouble(), and DietSerial.readLong() for receiving them. Finally there are also DietSerial.readByte() and DietSerial.read() for single bytes or text characters respectively.
As an alternative to formatting numbers, dates, and so on in text with snprintf(), DietSerial includes several "convenience" functions for printing character by character: DietSerial.colon(), DietSerial.slash(), DietSerial.dash(), Dietserial.dot(), DietSerial.percent(), and so on. In this way the RAM requirement for printing formatted text is minimised.
Compiling the MemoryComparison example gives the following results with Arduino IDE 2.3.6 on an Uno:
| bytes: | FLASH | SRAM |
|---|---|---|
| Serial (built in) | 5116 | 198 |
| DietSerial library | 3650 | 18 |
These numbers are for SRAM that is permanently allocated at the start of the sketch, "static" SRAM.
- Arduino uses 9 bytes at the bare minimum in both cases, for the
millis()anddelay()functions. Serial.readString()only works with ArduinoStringobjects, which further inflates its flash and RAM demand.- The
DietSerial.readString()code includes error-checking while theSerial.readString()does not.
The DietSerial library is compatible with the Arduino Uno, the Nano, the Duemilanove, and the Pro Mini (both 5 volt and 3 volt) boards. It will also work with "breadboard Arduinos" using the AVR ATmega328P microcontroller and with a system clock at 16 MHz, 8 MHz, or 1 MHz.
DietSerial uses the RX0 and TX1 "hardware serial" pins, which are also connected to the USB interface on Unos and Nanos, just like Serial.
There are several difference from Serial, both obvious and not so obvious.
Arduino's String objects are not supported. If you're considering using this library, because you are running out of RAM, Arduino String objects use too much RAM anyway.
Some less common functions are not supported - Serial.find(), Serial.findUntil(), and others.
Arduino's Serial works in the background filling and emptying its buffers, accumulating data received or sending data out while your code can do other things.
In practice, most sketches tend to wait for data to be sent or received anyway, using the Serial.flush() function to ensure that the transmission has completed, and Serial.available() to check whether data has started to arrive.
DietSerial is blocking, meaning that multi-byte prints or reads wait for the serial interface to transmit or receive the data over the cables. The serial hardware holds one byte in its internal buffer.
Arduino Serial expects timeout to be in milliseconds. DietSerial.setTimeout() uses seconds, and has a default of 90 seconds and a maximum of 255 seconds.
DietSerial.setTimeout(60) is the same time-out as Serial.setTimeout(60000).
Under the covers, the time-out set with DietSerial.setTimeout(60) is timed by the ATmega328P's watchdog timer. The watchdog timer is not exactly precise.
DietSerial is limited to 8N1 formatted data frames. (8 data bits, no parity, 1 stop bit. 8N1 is by far the most common data frame format.) DietSerial uses "CR and LF" line endings when sending data. It will receive lines ending with just LF as well, though.
DietSerial.available() returns true or false, indicating whether the hardware has received a byte ready for use by your sketch. (Serial.available() tells you the number of bytes waiting in its buffer.)
DietSerial.read() with empty parentheses returns a char - a number between 0 and 127, intended to represent an ASCII character.
char c = 0;
c = DietSerial.read();
if (DietSerial.error() > 0) {
; // handle the error - see below for details on error()
}
else {
// do something with the character;
}
Here is the basic loop for reading data coming in over the serial hardware interface:-
char myBuffer[20]; // buffer to hold incoming data
for (auto& b : myBuffer) {b = 0;} // clear the buffer to all zeros
uint8_t bufPos = 0; // position in buffer to place received byte
char c = 0; // temporary holder for received character
while (bufPos < 20) { // while buffer is not full:
c = DietSerial.read(); // get the received byte, waiting if necessary
if (DietSerial.error()) // if hardware error has occurred,
break; // jump out of the while loop.
if ( (c == 0) || (c == '\n') || (c == '\r')) // Ascii NUL or LF or CR
break; // End of the text string: stop reading.
myBuffer[bufPos] = newByte; // Place received character into the buffer
++bufPos; // move ready for next character
}; // end bufPos loop.
// Handle errors and/or process myBuffer's contents....
Note that DietSerial.error() will return a non-zero code (8, in fact) if the buffer is filled up before we receive a a NUL or CR or LF.
This works as for read-with-empty-parentheses above, but returns a byte, with a value between 0 and 255 decimal. It treats NUL and CR and LF as ordinary data - the numbers 0, 14, and 10 respectively.
The "basic loop" above is what is done inside DietSerial.readString(myBuffer, 20). Here is a small example using that, and showing error handling:-
char myBuffer[20]; // as above
for (auto& c : myBuffer) c = 0;
size_t charsReceived = 0; // size_t is usual for counting array elements.
const size_t MinimumChars = 5; // We expect to receive at least 5 characters
charsReceived = DietSerial.readString(myBuffer, 20);
// charsReceived does not count the ending NUL character, as is normal with
// C's strlen() function.
// Handle errors and/or process myBuffer's contents.
if (DietSerial.error()) {
DietSerial.printError(DietSerial.error()); // prints text explaining the error
}
else
if ( charsReceived < MinimumChars) {
// handle unexpectedly short text.
}
else {
processBuffer(myBuffer, charsReceived);
}
This reads and accumulates incoming data until an end-of-line character or a NUL character is received. It replaces the end-of-line CR and LF with a null byte, so that the receiving buffer holds a C-style string, an array of characters ending with a null byte.
DietSerial.readString(myBuffer, buflen) returns the number of bytes received and updates DietSerial.error() in the same way as DietSerial.read(buffer, buflen) above. The difference is that bytesReceived may be less than the full length of ther supplied buffer. bytesReceived does not include the terminating null byte.
DietSerial.parseInt() returns a long. DietSerial.parseFloat() returns a double. Yes, the names are misleading.
As well as versions that work directly with incoming data, both of these functions have versions that work on already received data:-
// We are expecting the other end to send us 3 or more characters representing a
// floating-point number, followed by CRLF and/or LF, like 1.9<CR><LF>
// Possibly with other characters in there as well, like "distance: 16.5 cm<CR><LF>"
// ----------------------------------------------------
// 1. Directly receive a floating-point number as text.
// (Don't send data this way, if you can just use write(floatingpt);
// i.e., send four binary bytes instead of potentially 18 text bytes.
// But some third-party modules send text.)
double d1 = DietSerial.parseFloat(); // Receives a line of text and extracts a
// floating-point number from it. Takes time.
// process the number if text was received and represented a valid number.
if ((! DietSerial.error()) && (! isNan(d1))) // isNan comes from <math.h>: is not a number
{
processInput((float)d1); // processInput() expects a float argument.
}
// ----------------------------------------------------
// 2. Getting a floating-point number from a pre-existing string.
// Assume we have already received a line of text into myBuffer, like so:
uint8_t myBuffer[20]; for (auto& b : myBuffer) b = 0;
size_t bytesGot = DietSerial.readString(myBuffer, 20);
// Then we can parse out a floating-point number this way:-
double d2 = NAN; // not-a-number for doubles
if (!DietSerial.error() && bytesGot >= 3) {
d2 = DietSerial.parseFloat(myBuffer);
}
// if received a valid double, process it.
if (! isNAN(d2)) {
doMyThing((float)d2); // doMyThing() expects a float, not a double.
}
Two functions and three macros that may be useful for debugging your code.
DietSerial.printBinary(byte b): print a single byte in fixed length binary format, in two groups of four bits:-
DietSerial.printBinary(0xc6); // prints "0b1100 0110" via serial.
DietSerial.printDigit(byte b): prints the low four bits of b in ASCII:-
DietSerial.printDigit(0xf5); // prints "5".
DietSerial.printDigit((0xf5 >>4); // prints "f": the byte is right-shifted four bits.
DietSerial has several "convenience" functions for printing common characters: DietSerial.comma(), DietSerial.dot(), DietSerial.colon(), DietSerial.dash(), DietSerial.percent(), DietSerial.tab(), DietSerial.CRLF() for line endings, and so on.
These are usable unless NDEBUG is defined. (NDEBUG is a C standard macro for disabling assert() macros when compiling your code for deployment.)
Use these macros "bare", i.e. without putting DietSerial. in front:-
printVar(integer-variable):
int count = 73;
printVar(count); // prints "count 73 0x49"
printFloatVar(float-variable): prints a line with the name of the variable and its contents, in decimal with four decimal places.
printReg(REGMACRO): prints a line with the name and contents of the given ATmega register (or other byte variable), in binary, hexadecimal, and decimal.
printReg(UBRR0L); // prints: "UBRR0L 0b1100 1111 0xcf 207"
With NDEBUG #defined, these macros do nothing. You may need to #undef NDEBUG to use them where required.
All functions are members of the DietSerial object. That is, write DietSerial.begin();, not just begin(); in your code.
| Function | Remarks |
|---|---|
begin(BAUDRATE) |
Sets the baud rate for sending and receiving, and the default timeout duration (90 seconds) for receiving. The default baud rate, with an "empty" begin(), is 9600. Recommended baud rates, if the default is too slow, are "round" numbers, e.g. 100000, 125000, but not 115200. |
end() |
Disables the ATmega's internal serial hardware module and powers it off. |
setTimeOut(_seconds) |
Sets the number of seconds that read() functions should wait for input before giving up and setting the "receive timed out" error code, inspectable with DietSerial.error(). Allowed values: 0 to 255. The default is 90 (90 seconds). The timeout is per each character: successfully receiving a character resets the timer to zero, and it starts counting up to the timeout value again. |
available() |
Returns true or false, whether a byte has been received by the hardware ready to be read by your code. If available() is true, byte b = read(); returns immediately. Otherwise, read() will block, waiting for a byte to appear over the wire. All multi-byte readXxx() and parseXxx() functions block after the first character. |
hasByte() |
A synonym for available(). |
error() |
Returns the status of the last character receive attempt. 0 means no error, non-zero means an error occurred. See printError() for descriptions. |
read() |
Returns a single byte : uint8_t ch = DietSerial.read();. Returns a NAK 0x15, "receive unsuccessful", if the timeout expires, or a CAN, 0x18, "discard character", if a transmission error was detected. Sets the error code which can be inspected with error() and described using printError(DietSerial.error()). |
read(buffer, buflen) |
size_t stringSize = DietSerial.read(buffer, buflen);. Reads a line of text terminated with CR and LF, or just LF, into the supplied char array buffer. Replaces the CR-LF or LF at the end with a NUL (decimal 0) character. Returns the length of the string read, not including the terminating NUL. If no CR or LF is received after buflen - 1 characters are received, read(buffer, buflen) sets an error code, "buffer too small" - check it with DietSerial.error() - and replaces the last character with a NUL. read(buffer, buflen) sets error codes for other errors also. |
readString(buffer, buflen) |
Synonym for read(buffer, buflen). Makes it explicit that you are expecting a line of text from the serial input. Error codes as described under read(buffer, buflen) above. |
readBytes(buffer, nbrBytes) |
Read exactly nbrBytes bytes of data from serial input and store them in the supplied array buffer. |
readChar(), readInt(), readLong(), readFloat(), readDouble() |
Receives 1, 2, 4, 4, or 4 binary bytes respectively, pastes them together as required, and returns the value as the specified data type. char c = readChar();, int i = readInt();, etc. |
parseInt() |
For numbers sent as text. Expects a sequence of digit characters, possibly with a '-' in front. Reads the incoming characters until a non-digit occurs and returns a long int (int32_t). Returns 0 if an error occurred: use error() to check for errors. |
parseInt(buffer) |
Returns an integer from a sequence of digit characters in the NUL-terminated string in the char array buffer, which may have been read in with readString(). Returns 0 if there was an error; use error() to check for successful reading of the text if 0 is a possibly correct value. |
parseFloat() |
Expects to read in a sequence of characters representing a floating-point number in "natural" format, e.g. -0.0012345. Returns a double with the floating-point value if successful. Returns NAN and sets a non-zero error code if there was an error. |
parseFloat(buffer) |
As for parseInt(buffer). If successful, returns a double being the number specified in the NULL-terminated string of characters in buffer. |
printError(DietSerial.error()) |
Prints text describing the error code returned from read(), readBytes(), readLine(), parseInt(), or parseFloat() to the serial output. Error codes are 0: no error, 1: read timed out, 2: data is garbled, discard the byte or bytes, 4: other error, for example zero bytes before <CR> or <LF> in readstring(), 8: supplied buffer is too small. |
| Function | Remarks |
|---|---|
begin(BAUDRATE) |
The default is 9600. |
end() |
Disables the hardware and turns it off, saving a few microamps |
flush() |
Flush waits for the last byte to be transmitted by the USART hardware. |
print(), println() |
Print most types of data in readable format. |
printBinary() |
Print a byte as a fixed length string of form "0b0011 1010". |
printDigit() |
Print the lower 4 bits of the given byte as a single hexadecimal character 0-9,a-f. |
printP(), printlnP() |
Print named strings stored in program memory (flash). printP(promptText); works with promptText defined as static char promptText[] PROGMEM = "Type something please: ";. Useful if you want to print the same string in several places in your code. |
write() |
send individual characters(write(c)), or blocks of bytes (write(array, sizeOfArray)) without making them readable. There are also versions for int, long, float, and double variables, and the unsigned variants unsigned int and unsigned long: write(integerVar), write(floatVar), etc. These send the variables as fixed-length binary: write(floatVar) will send 4 bytes, ready to read at the other end with float f2 = readFloat();. |
All the above functions are members of the DietSerial object. Use DietSerial.begin();, and so on.
You might like to define shorthand macros and/or reference variables to save on typing and clutter in your code:
#define DS DietSerial // Now you can use DS.begin();, DS.println(); etc.
auto& DSref = DietSerial; // DSref is a reference to DietSerial; now you can use DSref.begin(); etc.
#define flashString(stringName, stringValue) static const char stringName[] PROGMEM = stringValue
// Now you can use:
// flashString(infoString1, "InfoInfoInfo");
// DietSerial.printlnP(infoString1);
If you print floating-point numbers in readable format, DietSerial uses an extra 2kb-ish of flash memory and 20-ish bytes of RAM, in the AVR-libC standard library function dtostrf() for formatting floating-point numbers.
Besides those listed above? :-) You have to wait.
Because Arduino claims the "USART Data Register Empty" interrupt for its Serial object, all of DietSerial's functions are blocking, meaning that your program waits while they do their thing.
print() and printP(), println and printlnP() mostly wait for the hardware to trundle the bits and bytes out over the wire, and they will return to your code only when the last byte has been handed off to the ATmega's internal hardware serial module for transmission. flush() waits for the hardware to tell us that that last byte has been sent.
Click the green "Code" button, and choose "Download zip". Unzip the downloaded zip file into your Arduino "libraries" folder inside your sketchbook folder. If using the Arduino IDE, search for DietSerial in the library manager.
Include the file at the top of your sketch:
#include "DietSerial.h"
Then use the functions and macros described above, seasoning to taste.
as at 2025-09-24 GvP.
Document functions more thoroughly.
Measure flash and RAM consumption more rigorously.
Adapt to support the ATmega2560 and/or ATmega1284P microcontrollers.
Adapt to support the ATtiny44/84 or ATtiny45/85 microcontrollers.
Support other parities, stop bits, and error checking.
Non-blocking transmit functions, controlled with a "WANT" define, for use outside the Arduino environment.