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.

Parts Required

Circuit Diagram

uart-rxtx

Serial communications tools

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

uart2usb

2) Serial terminal

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

sw_uart_cutecom

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(
                        port=port,
                        baudrate=19200,
                        stopbits=serial.STOPBITS_ONE,
                        bytesize=serial.EIGHTBITS)

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


if __name__ == '__main__':
        Client().loop()

Firmware

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
#endif

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

int
main(void)
{
    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;
        _delay_ms(10);
        uart_puts(buff);
    }
}

char
uart_getc(void)
{
#ifdef    UART_RX_ENABLED
    char c;
    uint8_t sreg;

    sreg = SREG;
    cli();
    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;
#else
    return (-1);
#endif /* !UART_RX_ENABLED */
}

void
uart_putc(char c)
{
#ifdef    UART_TX_ENABLED
    uint8_t sreg;

    sreg = SREG;
    cli();
    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 */
}

void
uart_puts(const char *s)
{
         while (*s) uart_putc(*(s++));
}

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

  1. 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.

  2. 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.

Leave a Comment