ATtiny13A: Serial Communications

Where I describe the process of developing serial communications for the Microchip ATtiny13A.

Introduction#

Even with a microcontroller with limited capabilities as the ATtiny13A, it is desirable to have the ability to communicate via the serial port. The ATmega328P and others have a UART built-in which makes it much easier to integrate serial communications. With the ATtiny13A, serial communications must be performed completely in software and using assembly language makes it possible and reliable.

The code for this article and other articles on the ATtiny13A can be found in the ATtiny13A repository. This particular example is examples/asm_softserial.

Serial Background#

Serial communications can either be synchronous (using a common clock) or asynchronous (using an agreed protocol). The latter is more prevelant and the mode we’ll use. The hardware required, other than two pins on the ATtiny13A, will be a USB to serial interface, such as USB to TTL Serial Cable or Olimex Black USB Type A to 3 Wire.

The concept is simple, send a start bit, 7-8 data bits, then a stop bit, all at a constant rate. On both ends, have the software wait for a start bit, compile the next 7-8 bits into a data byte, confirm the stop bit and repeat. To be successful, it requires a fairly accurate clock and the ability to respond quickly. The ATtiny13A has an RC oscillator which can be used, however, it is not highly accurate (+/- 10%). Using assembly language solves the second problem as the clock cycles for an instruction will be in the sub-microsecond range, while the bit rates will be in the millisecond range.

For the host system, you need to run a serial program. There are quite a few which work, the two I like the best are CoolTerm (macOS and Windows) and for Linux, a command line program called tio. For both of them you will want to setup the terminal to accept 9600 baud, 8 data bits, 1 stop bit and no parity bit.

Architecture#

The critical code in assembly language are the character read, char_read() and character write, char_write() functions. Any other functionality will use these two functions as their interface to serial communications.

The functionality is limited by intent. With 1KB of program memory, there is little room for integer/ASCII functionality such as itoa() and atoi(). When a number is written to the serial port, it is best to read it in hex, while a character could be read as ASCII or hex.

Due to the limited SRAM (64 bytes), there is little value in writing ASCII characters or strings from RAM. The function flash_write() can write strings from program memory, albeit those need to be short as well.

Finally, two additional routines either enable the serial communications, init_serial() or expand on it word_write(). The former must be called to enable the communications, while the latter can be used to write a 16-bit value to the serial port.

Usage#

The constant definitions are in assembly language, registers.S:

; ---------- Serial Communications ----------
; 1. Define the TX/RX pins
; 2. Leave period/half-period alone, unless changing baud rate
; 3. If communications are flaky, use examples/osccal to determine TRIM
; 4. Default: 9600 baud, 8 data bits, 1 stop bit and 0 parity bits
#define TX          PB3   ; transmit pin, output
#define RX          PB4   ; receive pin, input pullup
#define period       37   ; # of ticks for 1 bit period (9600 baud @ 1.2MHz)
#define half_period  20   ; # of ticks for  a .5 bit period
#define TRIM         0x60 ; OSCCAL trim value, use examples/osccal to determine
#define no_bits     8     ; no of bits, typically 8

To use either the assembly language or C language interface, you must #include "serial_asm.h".

The assembly language API:

; --------------------------------------------------------------------
; main – application logic starts here
; --------------------------------------------------------------------
main_setup:

	; initialize the serial port, this must be called to use serial communications
    rcall   init_serial

    ; prompt to show terminal is live, defined as a hex digit
    ldi     r24, prompt
    rcall   char_write

; simple echo loop to test read/write char, read/write both use register 24
main_loop:
    rcall   char_read
    rcall   char_write
    rjmp    main_loop

The C language API:

// Initialise TX/RX pin and idle state.
// Must be called before char_write or char_read.
void init_serial(void);

// Transmit one byte at 9600-8-N-1.
// The character is passed in r24 per the AVR-GCC ABI.
void char_write(uint8_t c);

// Transmit one word at 9600-8-N-1.
// The character is passed in r25/r24 per the AVR-GCC ABI.
void word_write(uint16_t c);

// Block until one byte is received; return it.
// The result is returned in r24 per the AVR-GCC ABI.
uint8_t char_read(void);

// Write program memory text to console
// The address is passed in r31/r30.
void flash_write(uint16_t addr);

Note on Adjusting OSCCAL#

Prior to the assembly language serial interface, I used one written in C. Due to two issues, C’s inability for precise timing and the ATtiny13A’s RC oscillator lack of precision, the interface was not reliable. Asynchronous communications require no more than 10% difference in timing between the two devices and the interface wasn’t able to consistently maintain it.

In order to resolve the RC oscillator lack of precision, one can tune it by adjusting the parameter, OSCCAL. A brilliant way to do this is by using the program contained in examples/osccal. This program will start with a low value of OSCCAL, slowly incrementing and printing a line of text on the serial port. Once the text is legible, the value at which it is legible can be set in registers.S, which will provide a more reliable speed of communications.

An example of running the communications looks like this:

❯ tio ascii
[10:44:50.849] tio c43d2f6
[10:44:50.849] Press ctrl-t q to quit
[10:44:50.850] Waiting for tty device..
[10:44:55.856] Connected to /dev/ttyUSB0
�hip OSCCAL=60
������}imr@���abc������}mnr@���abc������}mgz@���abc������}mpr@���abc������}eyz@���abc������}e�z@���abc
������}u�z@���qrs
ϓ����=5�:`���1r3
OSCCAL=5D: ABC123
OSCCAL=5E: ABC123
OSCCAL=5F: ABC123
OSCCAL=60: ABC123
OSCCAL=61: ABC123
OSCCAL=62: ABC123
OSCCAL=63: ABC123
OSCCAL=64: ABC123
���CAL��M'
�23
�������
       ��L�j��ڍ�ʲ�
                   ��,�*��ꍡʲ�
                               ��,�*��ڍ���
                                           ��,�:��҅��ҡ
                                                      ��$�2��օ��Ң
                                                                  ��$�2��օ��ڣ��$΅�'V!�ڠ TJTEa�Zc�`JY��GtGA�ZB�@4HY��
[10:45:15.247] Disconnected

In this example, OSCCAL has already been set to correct value, you can see its initial value on the first line �hip OSCCAL=60. That said, OSCCAL is set to a new, much lower value then gradually incremented. The first good value for OSCCAL is 5D, however, the best value sits in the center, 60 or 61. This value can change for each specific ATtiny13A chip.