ATtiny13 – software UART (debug logger)

Since ATtiny13 does not have hardware USART/UART in some cases we’re forced to use Software UART. In this example project a simple bit-banging UART has been presented. Compiled for both TX and RX it uses just 248 bytes of flash. Default serial configuration is 8/N/1 format at 19200 baud (lowest error rates during tests). It can be easily changed to other standard RS-232 baudrate. Note that it is a great solution for a simple debug/logging but to other purposes I would recommend using an AVRs with a built-in UART. The code is on Github, click here.

Edit: To make a simpler integration with external projects I have created a little library:

Parts Required

Circuit Diagram


Serial communications tools

1) TTL Serial Adapter (UART <-> USB converter)


2) Serial terminal

I usually use CuteCom a graphical serial port communications program, similar to Minicom.


Or python scripts, i.e.:

#!/usr/bin/env python

import serial
import time

class Client:

        def __init__(self, port="/dev/ttyUSB0"):
                self.port = serial.Serial(

        def loop(self):
                while True:
                        req = "%s\r\n" % raw_input("client$ ")
                        n = self.port.write(req)
                        print "Sent %d bytes" % n
                        rep = self.port.readline()
                        print "Received %d bytes, rep=\"%s\"" % (len(rep), rep.replace("\r\n", ""))

if __name__ == '__main__':


This code is written in C and can be compiled using the avr-gcc. More details on how compile this project is here.

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define    UART_RX_ENABLED        (1) // Enable UART RX
#define    UART_TX_ENABLED        (1) // Enable UART TX

#ifndef F_CPU
# define        F_CPU           (1200000UL) // 1.2 MHz
#endif  /* !F_CPU */

#if defined(UART_TX_ENABLED) && !defined(UART_TX)
# define        UART_TX         PB3 // Use PB3 as TX pin
#endif  /* !UART_TX */

#if defined(UART_RX_ENABLED) && !defined(UART_RX)
# define        UART_RX         PB4 // Use PB4 as RX pin
#endif  /* !UART_RX */

#if (defined(UART_TX_ENABLED) || defined(UART_RX_ENABLED)) && !defined(UART_BAUDRATE)
# define        UART_BAUDRATE   (19200)
#endif  /* !UART_BAUDRATE */

#define    TXDELAY             (int)(((F_CPU/UART_BAUDRATE)-7 +1.5)/3)
#define RXDELAY             (int)(((F_CPU/UART_BAUDRATE)-5 +1.5)/3)
#define RXDELAY2            (int)((RXDELAY*1.5)-2.5)
#define RXROUNDED           (((F_CPU/UART_BAUDRATE)-5 +2)/3)
#if RXROUNDED > 127
# error Low baud rates unsupported - use higher UART_BAUDRATE

static char uart_getc();
static void uart_putc(char c);
static void uart_puts(const char *s);

    char c, *p, buff[16];

    uart_puts("Hello World!\n");

    /* loop */
    while (1) {
        p = buff;
        while((c = uart_getc()) != '\n' && (p - buff) < 16) {
            *(p++) = c;
        *p = 0;

    char c;
    uint8_t sreg;

    sreg = SREG;
    PORTB &= ~(1 << UART_RX);
    DDRB &= ~(1 << UART_RX);
    __asm volatile(
        " ldi r18, %[rxdelay2] \n\t" // 1.5 bit delay
        " ldi %0, 0x80 \n\t" // bit shift counter
        "WaitStart: \n\t"
        " sbic %[uart_port]-2, %[uart_pin] \n\t" // wait for start edge
        " rjmp WaitStart \n\t"
        "RxBit: \n\t"
        // 6 cycle loop + delay - total = 5 + 3*r22
        // delay (3 cycle * r18) -1 and clear carry with subi
        " subi r18, 1 \n\t"
        " brne RxBit \n\t"
        " ldi r18, %[rxdelay] \n\t"
        " sbic %[uart_port]-2, %[uart_pin] \n\t" // check UART PIN
        " sec \n\t"
        " ror %0 \n\t"
        " brcc RxBit \n\t"
        "StopBit: \n\t"
        " dec r18 \n\t"
        " brne StopBit \n\t"
        : "=r" (c)
        : [uart_port] "I" (_SFR_IO_ADDR(PORTB)),
        [uart_pin] "I" (UART_RX),
        [rxdelay] "I" (RXDELAY),
        [rxdelay2] "I" (RXDELAY2)
        : "r0","r18","r19"
    SREG = sreg;
    return c;
    return (-1);
#endif /* !UART_RX_ENABLED */

uart_putc(char c)
    uint8_t sreg;

    sreg = SREG;
    PORTB |= 1 << UART_TX;
    DDRB |= 1 << UART_TX;
    __asm volatile(
        " cbi %[uart_port], %[uart_pin] \n\t" // start bit
        " in r0, %[uart_port] \n\t"
        " ldi r30, 3 \n\t" // stop bit + idle state
        " ldi r28, %[txdelay] \n\t"
        "TxLoop: \n\t"
        // 8 cycle loop + delay - total = 7 + 3*r22
        " mov r29, r28 \n\t"
        "TxDelay: \n\t"
        // delay (3 cycle * delayCount) - 1
        " dec r29 \n\t"
        " brne TxDelay \n\t"
        " bst %[ch], 0 \n\t"
        " bld r0, %[uart_pin] \n\t"
        " lsr r30 \n\t"
        " ror %[ch] \n\t"
        " out %[uart_port], r0 \n\t"
        " brne TxLoop \n\t"
        : [uart_port] "I" (_SFR_IO_ADDR(PORTB)),
        [uart_pin] "I" (UART_TX),
        [txdelay] "I" (TXDELAY),
        [ch] "r" (c)
        : "r0","r28","r29","r30"
    SREG = sreg;
#endif /* !UART_TX_ENABLED */

uart_puts(const char *s)
         while (*s) uart_putc(*(s++));

21 thoughts on “ATtiny13 – software UART (debug logger)

  1. Hi Łukasz , I am so thankful to you for this solution. Simple and works right away. I have been struggling with debugging my ATTiny13 but not anymore.
    Thank you so much for this! Keep up the good work!

  2. I experienced some problems while trying the IR receiver demo where I was not able to have a UART communication. So I tried this part and it works for me out of the box. When I change the demo here to FUSE_L=0x7A, FUSE_H=0xFF, F_CPU=9600000, -DUART_BAUDRATE=57600 (taken from the IR project), I get some compile errors:

    main.c:70:2: warning: asm operand 4 probably doesn’t match constraints
    __asm volatile(
    main.c:70:2: error: impossible constraint in ‘asm’

    I’m now confused as the code looks identical when comparing these projects. I would be glad if I could get a helping hand ….

  3. Hi, I like your blog and I share your addiction to such a tiny but powerful devices as attiny13. There no too much resources in internet and your blog is real gem for me. So, first of all – thank you for the great work you did.
    I have no too much experience in AVRs and may be I’m doing something wrong but I’ve played a little bit with your code and noticed couple of issues. When I’ve added just an additional if-else branch in main() inside while loop, the “Hello World!” stopped working, which is pretty weird. Seems like memory or registers corruption (it might be that assembly code somehow interfere with code generated by gcc?)
    Also, it seems me excessive to do DDRB |= 1 << UART_TX each time when you need to send character (and may be PORTB |= 1 << UART_TX as well). Would it be more efficient to take it out of the function and perform only once in kind of "setup()"?

    • Thanks, Alex. Comments like that give me a power for making next projects.

      Issue with program hangs was probably caused by incorrect condition inside a loop (Boma-fix). It has been updated on page after you reported concerns about that bug. Yes, you can achieve one-time initialization of I/O pins by refactoring the code. Note that these redundant instructions cost is two clock-cycles while additional setup function will increase the size of program space. This compromise between CPU speed and program space depend on project requirements. You can choose what fits you best.

  4. I’ve compiled the code from the repo and uploaded to an attiny13 using an arduino mini. The hex is successfully uploaded without error, but when I connect the uart adapter to the attiny and open the serial monitor there is no response. Why?

    • Hi, I must admit that I never tried upload hex files using an Arduino and I cant help with what could go wrong. I always recommend to check twice if there is a bug related to wiring or mistake in firmware code.

Leave a Comment