An Ultra-Minimalist, High-Performance UART TX Library for AVR ATtiny Microcontrollers (Arduino IDE compatible)
The ATtiny microcontroller series (such as the ATtiny13, ATtiny25, ATtiny45, and ATtiny85) offers immense flexibility in a highly compact form factor. However, these devices suffer from severe Flash memory constraints (as low as 1KB) and a limited number of hardware timers.
Standard communication libraries like SoftwareSerial are often too bloated for these controllers, consuming valuable timers and significant portions of Flash and RAM. ATTinySerial was developed to solve this problem by providing a highly optimized, TX-only (Transmit) bit-banging implementation that requires zero hardware timers and reduces function-call overhead to an absolute minimum.
- Zero Hardware Timer Dependency:
The library performs precise software-based timing using the highly predictable
_delay_loop_2intrinsic function. This frees up the internal hardware timers (Timer0/Timer1) entirely for user applications like PWM generation, millis() tracking, or hardware interrupts. - Aggressive Flash Optimization via Inline Wrappers:
To prevent the compiler from generating costly jump instructions and stack operations for formatting variants (e.g., appending newlines or casting integer types),
ATTinySerialimplements these wrappers asinlinemethods directly within the header file. This leads to near-zero abstraction overhead in the compiled binary. - Interrupt-Safe Bit-Banging:
UART timing is highly sensitive to external interrupts. The library temporarily saves the Status Register (
SREG), clears the global interrupt flag during the transmission of a single byte, and restores the state immediately after the stop bit. - Robust Numeric and Floating-Point Handling:
Unlike many minimalist libraries that strip out floating-point math,
ATTinySerialincludes a robust implementation. It correctly handlesNaNandInfinitystates, provides 32-bit integer overflow protection, and natively implements rounding (e.g., rounding0.999correctly based on the specified decimal precision) without relying on bloated standard library implementations likesprintf. - Instruction Cycle Compensation:
The internal delay calculation automatically subtracts the required clock cycles needed for bit-shifting and loop operations (
cycles -= 3), ensuring accurate baud rates even at low CPU frequencies.
Inside the core write(char c) function, pointer referencing is utilized to cache the PORTB register. Bitmasks for the specific TX pin and its inverse are pre-calculated before the critical timing section begins. This ensures that the state changes inside the bit-banging for-loop are executed in the minimum possible number of clock cycles (typically using LD and ST or OUT assembly instructions).
RAM is the most scarce resource on an ATtiny (the ATtiny13 possesses only 64 Bytes). To prevent string literals from being copied to SRAM during initialization, the library natively supports the __FlashStringHelper class. Wrapping strings in the F() macro forces the compiler to keep the data in Flash memory, reading it byte-by-byte via pgm_read_byte().
- Download this repository as a
.zipfile. - Open the Arduino IDE.
- Navigate to Sketch > Include Library > Add .ZIP Library...
- Select the downloaded
.ziparchive.
You can easily install this library using the built-in Library Manager of the Arduino IDE.
- Open the Arduino IDE.
- Navigate to Tools → Manage Libraries... (or click the Library Manager icon on the left sidebar in IDE 2.x).
- Search for
ATTinySerial. - Find the library by
DampflokHDand click Install.
If you are using PlatformIO, you can clone the repository directly into your lib/ directory or include it via platformio.ini:
This library supports the classic ATtiny series (ATtiny25, ATtiny45, and ATtiny85). To program these chips in the Arduino IDE, you need to install the ATTinyCore hardware package.
- Open the Arduino IDE.
- Go to File → Preferences (on macOS: Arduino → Preferences).
- Find the field Additional Boards Manager URLs.
- Copy and paste the following URL into the field:
(Note: If there are already other URLs there, separate them with a comma or place them on a new line).
https://descartes.net/package_drazzy.com_index.json - Click OK.
- Navigate to Tools → Board → Boards Manager...
- Type ATTinyCore into the search bar, locate the entry by Spence Konde, and click Install.
Open the Tools menu in your Arduino IDE and adjust the configuration to match your hardware setup. Here is how to configure it correctly:
- Board: Select
ATTinyCore→ATtiny25/45/85 (No bootloader). Choosing the "No bootloader" version is ideal since you are uploading your code directly via an ISP programmer. - Port: Select the serial port that your ISP programmer is connected to.
- B.O.D. Level: Choose according to your project's power and hardware preferences.
- Chip: Select the exact chip you are using (
ATtiny25,ATtiny45, orATtiny85). - Clock Source: Choose your desired clock speed (e.g.,
1 MHz (internal)for maximum power savings or8 MHz (internal)for standard performance). You can also use an external crystal (quartz) if your hardware requires it. - Save EEPROM: Select
EEPROM retainedif you want to keep your data stored in the EEPROM when uploading new sketches. (Note: Burning the bootloader will always erase the EEPROM, but enabling this option ensures your data persists during normal code uploads).. - LTO: Select
Enabled. Link Time Optimization (LTO) significantly reduces the flash memory occupied by your code. It is highly recommended for space-constrained chips like the ATtiny and works flawlessly with this library. - millis()/micros(): You can leave this
Enabledor change it toDisabledto save a massive amount of flash memory. Note: This library is highly optimized and works perfectly even with millis/micros disabled. - Timer 1 Clock: Keep this on the default setting:
CPU (CPU frequency). - Programmer: Select the ISP programmer you are using to connect to the chip (e.g.,
Arduino as ISPorUSBtinyISP).
Before uploading your actual sketch for the first time—or whenever you change core hardware options like the Clock Source or B.O.D. Level—you must write these settings onto the physical chip.
- Connect your ISP Programmer to the pins of your ATtiny chip.
- Ensure your correct programmer is selected under Tools → Programmer.
- Click Burn Bootloader (at the very bottom of the Tools menu). (Don't worry: this does not actually load a heavy bootloader onto the chip; it simply configures the internal hardware registers and fuses to match your selected settings).
Since your chip does not use a bootloader, you must upload your sketch using your ISP programmer.
- Open your sketch in the Arduino IDE.
- Go to the Sketch menu.
- Click Upload Using Programmer (or press
Ctrl + Shift + U/Cmd + Shift + Uon macOS).
Your code is now running and ready to go on your ATtiny!
The simplest implementation initializing the transmission on the default pin (PB0).
#include <ATTinySerial.h>
// Instantiate the object using default TX pin PB0
ATTinySerial serial;
void setup() {
// Initialize UART at 9600 baud
serial.begin(9600);
}
void loop() {
serial.println("ATtiny is running.");
delay(1000);
}On an ATtiny13 with only 64 Bytes of RAM, strings can crash your program. Use the F() macro to keep text in Flash memory!
#include <ATTinySerial.h>
ATTinySerial serial(2); // Set TX pin to PB2
void setup() {
serial.begin(9600);
// The F() macro prevents the string from eating up your RAM!
serial.println(F("This string lives in Flash Memory!"));
}
void loop() {
// ...
}ATTinySerial comes with a robust float-to-string implementation. It automatically handles rounding and negative numbers.
#include <ATTinySerial.h>
ATTinySerial serial(0);
void setup() {
serial.begin(9600);
}
void loop() {
float temperature = 24.5678f;
int32_t uptime = 100000;
serial.print(F("Temp: "));
serial.print(temperature, 2); // Print with 2 decimal places -> "24.57"
serial.println(F(" C"));
serial.print(F("Uptime: "));
serial.println(uptime); // Handles large 32-bit integers perfectly
delay(2000);
}A small program to test all features and output results to the serial monitor, including an error notification on fail if a watchdog reset is triggered.
#include <ATTinySerial.h>
#include <avr/wdt.h>
ATTinySerial debugSerial;
void setup() {
uint8_t resetCause = MCUSR;
MCUSR = 0;
wdt_disable();
debugSerial.begin(9600);
if (resetCause & _BV(WDRF)) {
debugSerial.println(F("ERROR: watchdog reset"));
} else {
debugSerial.println(F("OK: startup"));
}
wdt_enable(WDTO_8S);
}
void loop() {
wdt_reset();
debugSerial.println(F("ATTinySerial test"));
debugSerial.write('>');
debugSerial.println();
debugSerial.print(F("char: "));
debugSerial.println('A');
debugSerial.print(F("string: "));
debugSerial.println("RAM string");
debugSerial.print(F("flash: "));
debugSerial.println(F("Flash string"));
debugSerial.print(F("bool: "));
debugSerial.print(true);
debugSerial.print(' ');
debugSerial.println(false);
debugSerial.println((int8_t)-128);
debugSerial.println((uint8_t)255);
debugSerial.println((int16_t)-32768);
debugSerial.println((uint16_t)65535);
debugSerial.println((int32_t)(-2147483647L - 1L));
debugSerial.println((uint32_t)4294967295UL);
debugSerial.print(F("float: "));
debugSerial.println(23.4567f, 4);
debugSerial.println(-0.0049f, 3);
debugSerial.println(12.5f, 0);
debugSerial.println(1.0f / 3.0f, 9);
debugSerial.println(NAN, 2);
debugSerial.println(INFINITY, 2);
debugSerial.println(-INFINITY, 2);
debugSerial.println(4294967296.0f, 2);
wdt_reset();
debugSerial.println(F("OK: test complete"));
debugSerial.println();
delay(5000);
}-
ATTinySerial(uint8_t pin = 0)
Instantiates the class object. It assigns the target transmission pin onPORTB, defaulting to0(which corresponds toPB0). -
void begin(uint32_t baudrate)
Configures the designated TX pin as an output, pulls it high to establish the proper UART idle state, and calculates precise delay cycles based on the core CPU frequency (F_CPU) and the targetbaudrate. -
void begin(uint32_t baudrate, uint8_t pin)
Allows you to reassign the active TX pin dynamically and initialize the transmission parameters in a single combined step.
void write(char c)
The low-level transmission engine. It handles the start bit, shifts out 8 data bits (least-significant-bit first), and concludes with the stop bit. To guarantee uncorrupted bit-banging timing, global interrupts are temporarily suspended usingcli()for the duration of the byte transmission.
-
void print(char c)
Transmits a single character directly, acting as a direct wrapper aroundwrite(). -
void print(const char* str)
Iterates through and transmits a null-terminated character string stored in SRAM. It includes built-innullptrprotection to prevent runtime crashes. -
void print(const __FlashStringHelper* str)
Transmits string literals stored directly in Flash memory via PROGMEM (pgm_read_byte), preserving scarce SRAM. This method is utilized automatically alongside theF()macro. -
void print(bool b)
Evaluates a boolean condition, outputting ASCII'1'for true and'0'for false. -
void print(int8_t/int16_t/int32_t num)
Converts signed integers of various widths into human-readable decimal strings. It cleanly handles edge cases and negative boundaries. -
void print(uint8_t/uint16_t/uint32_t num)
Converts unsigned integers into their decimal string representations. -
void print(float num, uint8_t decimals = 2)
Formats and transmits floating-point values with a user-defined precision (capped at a maximum of 9 decimal places). It features built-in handling forNaN,Inf, and-Inf, precise rounding adjustments (+0.5f), and 32-bit scale overflow protection (ovf).
void println()
Transmits a standard serial line ending sequence consisting of a Carriage Return (\r) followed by a Line Feed (\n).
The print() and println() functions support almost all native types:
- Characters:
print(char c) - Strings:
print(const char* str)&print(const __FlashStringHelper* str) - Booleans:
print(bool b)(Outputs'1'or'0') - Integers:
int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t - Floats:
print(float num, uint8_t decimals = 2)(Note:printlnvariants simply append\r\nto the output.)
To maintain its ultra-compact footprint, this library operates under specific constraints:
- Half-Duplex / TX-Only: This library cannot receive (RX) serial data. It is intended strictly for data transmission, logging, and debugging.
- Blocking Execution: During the transmission of a byte, _delay_loop_2 occupies the CPU completely, and global interrupts are temporarily turned off. High-frequency time-sensitive tasks running in the background may experience slight jitter.
- Clock Speed Dependency: The baud rate accuracy depends directly on an accurate F_CPU definition. Lower frequencies (like 1 MHz) limit reliable maximum baud rates compared to an 8 MHz configuration.
