« Back to home

A Software UART for the ATtiny85

I've recently started work on a small project to provide visual notifications from a PC over a Bluetooth connection. I mentioned it on G+ a little while ago, it's essentially a minimalist version of this product.

I started working on a post describing the work I was doing but it quickly expanded into something much larger than I expected as I started to describe aspects of my ATtiny85 template library which is being used as the basis of the project.

I decided to split the post into a number of smaller ones so I can cover the various topics in detail without swamping you with information. This post, the first in the series, provides an overview of the software UART implementation in the library - how it's implemented and how it's used. The upcoming posts will cover:

  1. Using a HC-05 breakout module with the library. This will include using AT commands to configure the module and basic connections to the ATtiny.
  2. PWM output on the ATtiny and the template libraries equivalent to the analogOut() function in the Arduino library.
  3. Finally the full project including schematics and code. This uses RGB LEDs to provide colour coded notifications under control of a remote PC through Bluetooth. Use it to provide information in a glance - you can control the colour based on whatever data you choose, stock information, website analytics or Bitcoin exchange rate.

    For now, a brief introduction to the library and how to use the software UART.

The ATtiny85 Template Library

I've mentioned my ATtiny Breakout Board and the associated template library in previous posts but only on a very superficial level. Although this post will concentrate on the software UART implementation it's worth running over it again.

Breakout Board

The library is available on GitHub at the link above and the documentation for it can be found here. I've done my best to provide enough information in the project documentation to make it easy to get started. I'd appreciate any feedback you might have on how to improve it.

To use the library as a basis for your own projects simply fork the GitHub project (or clone your own local copy) and start modifying the code in the firmware directory. To get started you only need to start modifying the file hardware.h (which contains the pin assignments and is used to enable or disable various parts of the library) and main.c.

To keep up to date with newer version of the library simply merge the master branch of the original GitHub library into your project every now and then. The majority of updates will be in the firmware/shared and firmware/include directories which contain the implementations and definitions of the library components respectively.

Serial IO Basics

Before I get into the implementation details of the UART functions it's probably best to run through how serial communication appears on the wire. Because we are generating these signals in software it's a good idea to know what is expected and what sort of issues to look out for.

The diagram above shows the voltage levels on the Tx pin when the ASCII character sequence "AVR" is being sent. Obviously this is what will be seen on the Rx pin as well on the receiver end of the connection. In this case the connection is running at 3.3V levels (not RS232 levels) and the connection is configured for 57.6 KBaud, 8 data bits, 1 stop bit and no parity (57600 8N1). I've marked each character on the diagram along with the values for the individual bits.

The diagram above shows the detail for a single character (in this case the ASCII character 'A'). Even though we are only sending 8 bits of data per character additional bits are required for framing (to allow the receiver to detect the start of a character and to determine it has reached the end). In every case there is a single start bit - this causes the communications line to transition from it's idle state (high) to low for at least the duration of a single bit. The data bits come next starting from the least significant bit (bit 0) to the most (bit 7) and finally, the stop bit. The stop bit ensures the line comes back up to it's idle state for at least one bit.

The width, or duration, of each bit is a function of the baud rate - for the speed we are running at that is 1/57600 seconds per bit (about 17 microseconds) - and each bit must be exactly the same width. For our configuration it takes 10 bits to transmit a single byte (1 start bit, 8 data bits and a stop bit) so it takes 170 microseconds per byte of data giving a peak transfer rate of 5.6Kb per second assuming there are no gaps between the bytes. In reality it will be lower).

From the first diagram you can see that there are no restrictions on the gaps between the characters (the inter-character delay). The line must remain at an idle state for this period (logic high) but the duration doesn't have to be a multiple of whole bits and can be as long as is needed.

Writing Serial Data

Writing data out the serial port is fairly straight forward - we know the width of each bit so we simply toggle an output pin as needed and ensure it stays in the right state for the full duration of a bit. All we need to do is pull the output pin low for one bit width for the start bit, send out the individual bits from LSB to MSB and then bring the output high for one bit width as the stop bit. The output is then left high to represent the idle state.

The current implementation does just that in a small loop of assembly code. Because the timing is critical interrupts are disabled while this is happening - you will need to keep this in mind if you are depending on interrupts in your code.

NOTE: The serial send and receive code in the library makes heavy use of the assembly routines developed by Ralph Doncaster and described here. I said that generating the output is straight forward and, as a concept, it is. The actual implementation to get the timing right based purely on the number of instruction cycles needed to execute each op-code is another matter. The code developed by Ralph is a really nice piece of work - I highly recommend visiting his site for more examples.

Reading Serial Data

Reading the serial data is the reverse operation. We simply watch the input pin and wait for it to transition from high (idle state) to low which will be our start bit. We then wait for 1.5 bit periods and then start sampling for the input pin once every bit period to get the value for the appropriate bit. We need 9 samples in all - 8 for the data and 1 for the stop bit.

The reason for the 1.5 bit delay at the start is to ensure that we are sampling the bit value in the middle of it's transmission period. If we are slightly off in our timing we will still get the right value for the bit.

The full implementation of the receiver can be found here. Initially this only provided a blocking implementation - if you called the uartRecv() function it would wait indefinitely until a start bit was detected before it would read and return a character. In cases where nothing happens until it is requested over the serial port (like a bootloader for example) it doesn't matter that the processor is locked in an endless loop with interrupts turned off.

If you want to be able to do work and only respond to serial input when it was available it was not the perfect solution. I've now added an option to have the start bit trigger a pin change interrupt which will cause the incoming byte to be read and buffered for later processing. There is also a uartAvail() function that tells you how many characters are currently buffered.

Pin Change Interrupts

The AVR allows for an interrupt to be triggered when the value of an input pin changes state (in either direction). On the ATtiny85 all of the inputs can be used to trigger this interrupt (which uses the vector PCINT0) but, if you enable multiple pins as triggers, you can't tell which one actually caused the interrupt. You can figure out what the transition was though - if the current reading on the input is 0 you can assume a high to low transition for example.

When the interrupt handler is invoked interrupts will be disabled until they are explicitly enabled by the handler code or the interrupt handler returns. The interrupts don't queue so if another triggering event occurs while the current interrupt is being handled it won't be immediately triggered again when it is done.

Handling the Interrupt

Supporting the interrupt is actually straight forward - what was the original code for the uartRecv() function has been moved to an ISR handler. The code to wait for the start bit is no longer necessary as it has already been detected to trigger the interrupt.

 For our purposes we only want to respond to the start bit which is a transition from 1 to 0 on the input. We sample the Rx pin and if it is not 0 we simply ignore the interrupt and move on. If it is zero we take it to be the *start* bit and start reading the character.

 Because the compiler generates some start up code for the ISR we take that into account in the delay time before we start reading bits.


## Input Buffering

 Because the receive code is not called directly and can't return a value we need to store the input character somewhere so the main application code can access it later. For this purpose we maintain a small FIFO buffer - newly read characters are added to the end of the buffer and calling the *uartRecv()* function will take any pending characters from the front of the buffer.

 The size of the buffer is set in *hardware.h* for the project, it generally doesn't need to be too large - just enough to store characters until the main program gets a chance to read the cached characters and do something about them.


# Using the Serial Port

 The serial port, like other modules in the library, is enabled and configured in the [firmware/hardware.h](http://thegaragelab.github.io/tinytemplate/hardware_8h.html) file. The relevant sections look like this:
 /* Software UART      *      * This feature provides a software implementation of a UART allowing you to      * perform serial communications. It supports speeds of up to 250Kbps, can be      * configured to use a single IO pin and has an option to be interrupt driven.      */
#define UART_ENABLED

 /* Baud rate to use      *      * The implementation is optimised for higher baudrates - please don't use      * anything below 57600 on an 8MHz clock. It does work at up to 250000 baud      * but you may experience a small amount of dropped packets at that speed.      */
#define BAUD_RATE 57600

 /* Define the pin to use for transmission      */
#define UART_TX   PINB1

 /* Define the pin to use for receiving      *      * If this pin is the same as the TX pin the code for the single pin UART      * implementation is compiled. This means no buffering and no interrupts.      */
#define UART_RX   PINB0

 /* Enable interrupt driven mode      *      * Only valid in two pin configuration. If this is defined and you are using      * two pins for the UART then received data will be read and buffered as it      * arrives.      */
#define UART_INTERRUPT

 /* Size of input buffer      *      * Only available in interrupt driven mode. This sets the size of the receive      * buffer (max 256 bytes).      */
#define UART_BUFFER 4

The UART_ENABLED macro must be defined to enable any UART functions, UART_TX and UART_RX define the pins to use for Tx and Rx respectively (because the ATtiny only has a single IO port you can use the definitions from - PINB2, PINB3, etc). If UART_INTERRUPT is defined then interrupt driven IO with an input buffer is enabled. Finally UART_BUFFER defines the size of the input buffer to use - this must be at least 1, I've found that 4 is a reasonable value for most cases.

NOTE: Because this is implemented in software rather than hardware it is not full duplex. The code uses instruction cycles to determine the timing of each bit, to ensure that this is accurate interrupts are disabled during both send and receive. What this means is that if a character is incoming while you are sending when the send is complete the receiving code could detect a low data bit as the start bit. This will result in garbage data being detected as a valid character. The solution is to implement a command/response type protocol so both sides of the connection have an idea of when they should be expecting data and when they should be sending it.

With the new changes there are now 3 modes of operation for the UART that you can choose from. These are ...

Single Pin Mode

To use this mode simply define UART_TX and UART_RX to be the same pin, the code will recognise this and change it to be an input or output as needed for communications.

One Pin Serial

This mode is best to use when you want to minimise the number of pins in use on your project but it has a few drawbacks. First, it requires some additional discrete components to work (a transistor, a resistor and a diode - shown in the schematic to the right) and secondly it will echo any data it receives back to the sender. Any code you write to communicate with a device using this configuration will have to take that into account.

This is the configuration used in the sample schematics included in the template and used by the Microboot bootloader. In that situation we use PINB5 (and disable the RESET functionality normally attached to the pin) for code transfer. It's a good place to start with your project and enables you to easily add debugging output using the same connection you load code through.

This mode does not support interrupts so the UART_INTERRUPT macro will be ignored and calling uartRecv() will block until data is available. The uartAvail() function is still present but it will always return 0.

Two Pin Mode (Blocking)

In this mode Tx and Rx are on different pins so no external hardware is needed. It also eliminates the transmission echo that is present on the previous mode.

In the blocking version of the mode (UART_INTERRUPT is not defined) the functions behave as they do for the single pin mode - calling uartRecv() will block until data is available. The uartAvail() function is still present but it will always return 0.

Two Pin Mode with Buffering

If UART_INTERRUPT is set and UART_TX and UART_RX are defined as different pins you will get the interrupt driven implementation with a receive buffer. The value of UART_BUFFER will be used for the buffer size and the uartAvail() function will return the number of characters currently in the buffer - if this value is greater than zero than a call to uartRecv() will not block. If uartAvail() returns 0 then a call to uartRecv() will block until a character is received.

When you are using the interrupt driven implementation you will need to enable interrupts on the chip in your application initialisation as follows:

``` void main() { uartInit(); sei(); while(true) { // Do stuff here } }

Helper Functions

I've added a number of helper functions to the UART library that work with any of the operation modes. You can see a full description of them in the documentation.

Essentially they provide a simple way to print 16 bit values to the serial port (in decimal or hexadecimal) as well as zero terminated strings from either RAM or program memory. There is also a minimalist 'printf()' style function called uartFormat() that lets you use a format string to display a combination of strings and integers.

Next Steps

The next post will describe how to configure and use a HC-05 Bluetooth breakout board with the library so we can control a device remotely. Once that is done we can start adding visual notifications with an RGB LED and finally put it all together with a battery pack in a nice 3D printed case.