Skip to main content
3 of 3
Some grammar changes to help readability.
SamGibson
  • 18.3k
  • 5
  • 41
  • 63

Simplex UART echo implementation on ATtiny

I'm currently trying to implement a simple UART echo application on the ATtiny85, with no additional pins other than RX/TX (no autoflow control).

I'm using the Universal Serial Interface of the ATtiny and basing my work on this article. I've got so far that I can send and receive individual bytes and send them back. However I run into problems when I send more than 1 byte, as the program immediately tries to echo it back before realizing that it's receiving more:

oscilloscope traces

I am very new with stuff this low level and so I am wondering what the correct way to go about this is. I have an internal state that determines whether the device is receiving and currently it switches into sending once a full byte is received.

I am guessing that I have to wait until after the stop bit and see if a start bit follows immediately and only after determining that no other byte is being received, then switch into sending mode. Is this correct and if so, how would I go about implementing this?

here is the code:

#ifdef __INTELLISENSE__
#define ECHO
#endif

#ifdef ECHO

/*

ATTiny85 Hookup

RESET -|1 v 8|- Vcc
    PB3 -|2   7|- PB2/SCK
    PB4 -|3   6|- PB1/MISO
    GND -|4 _ 5|- PB0/MOSI/SDA

ATTiny85 PB0/MOSI/SDA -> Serial UART Rx, connect to Tx of serial input device
ATTiny85 PB1/MISO/DO = Serial UART Tx -> connect to Rx of serial output device
*/

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include "Arduino.h"

#define byte uint8_t

/* Supported combinations:
*   F_CPU 1000000   BAUDRATE 1200, 2400 
*   F_CPU 8000000   BAUDRATE 9600, 19200
*   F_CPU 16000000  BAUDRATE 9600, 19200, 28800, 38400
*/

// Set your baud rate and here
#define BAUDRATE 9600
#define STOPBITS 1
// F_CPU defined by Arduino, e.g. 1000000, 8000000, 16000000

// If bit width in cpu cycles is greater than 255 then  divide by 8 to fit in timer
// Calculate prescaler setting
#define CYCLES_PER_BIT (F_CPU / BAUDRATE)
#if (CYCLES_PER_BIT > 255)
#define DIVISOR 8
#define CLOCKSELECT 2
#else
#define DIVISOR 1
#define CLOCKSELECT 1
#endif
#define FULL_BIT_TICKS (CYCLES_PER_BIT / DIVISOR)
#define HALF_BIT_TICKS (FULL_BIT_TICKS / 2)

// Number of code CPU cycles from from pin change to starting USI timer
#define START_DELAY (99)

// Number of CPU cycles delay after setting COMPA timer until global interrupt is enabled
#define COMPA_DELAY 42
#define TIMER_MIN (COMPA_DELAY / DIVISOR)

#define TIMER_START_DELAY (START_DELAY / DIVISOR)
#if (HALF_BIT_TICKS - TIMER_START_DELAY) > 0
#define TIMER_TICKS (HALF_BIT_TICKS - TIMER_START_DELAY)
#if (TIMER_TICKS < TIMER_MIN)
#warning TIMER_TICKS too low, USI bit sample will after center of bit
#endif
#else
#error "TIMER_TICKS invalid, choose different values for F_CPU, BAUDRATE and START_DELAY"
#define TIMER_TICKS 1
#endif

// Old timer values
#ifdef ARDUINO
volatile static uint8_t oldTCCR0A;
volatile static uint8_t oldTCCR0B;
volatile static uint8_t oldTCNT0;
#endif

// Serial
volatile bool serialDataReady = false;
volatile uint8_t serialInput;

volatile bool receiving = false;

//---- Send -----
// USISerial send state variable and accessors
enum USISERIAL_SEND_STATE
{
    AVAILABLE,
    FIRST,
    SECOND
};
static volatile enum USISERIAL_SEND_STATE usiserial_send_state = AVAILABLE;
static inline enum USISERIAL_SEND_STATE usiserial_send_get_state(void)
{
    return usiserial_send_state;
}
static inline void usiserial_send_set_state(enum USISERIAL_SEND_STATE state)
{
    usiserial_send_state = state;
}
bool usiserial_send_available()
{
    return usiserial_send_get_state() == AVAILABLE;
}

// Transmit data persistent between USI OVF interrupts
static volatile uint8_t usiserial_tx_data;
static inline uint8_t usiserial_get_tx_data(void)
{
    return usiserial_tx_data;
}
static inline void usiserial_set_tx_data(uint8_t tx_data)
{
    usiserial_tx_data = tx_data;
}

static uint8_t reverse_byte(uint8_t x)
{
    x = ((x >> 1) & 0x55) | ((x << 1) & 0xaa);
    x = ((x >> 2) & 0x33) | ((x << 2) & 0xcc);
    x = ((x >> 4) & 0x0f) | ((x << 4) & 0xf0);
    return x;
}

char message[] = "USI Serial\r\n";

void usiserial_send_byte(uint8_t data)
{
    while (usiserial_send_get_state() != AVAILABLE)
    {
        // Spin until we finish sending previous packet
    };
    usiserial_send_set_state(FIRST);
    usiserial_set_tx_data(reverse_byte(data));

    // Save current Arduino timer state
#ifdef ARDUINO
    oldTCCR0B = TCCR0B;
    oldTCCR0A = TCCR0A;
    oldTCNT0 = TCNT0;
#endif

    // Configure Timer0
    TCCR0A = 2 << WGM00;    // CTC mode
    TCCR0B = CLOCKSELECT;   // Set prescaler to clk or clk /8
    GTCCR |= 1 << PSR0;     // Reset prescaler
    OCR0A = FULL_BIT_TICKS; // Trigger every full bit width
    TCNT0 = 0;              // Count up from 0

    // Configure USI to send high start bit and 7 bits of data
    USIDR = 0x00 |                                         // Start bit (low)
            usiserial_get_tx_data() >> 1;                  // followed by first 7 bits of serial data
    USICR = (1 << USIOIE) |                                // Enable USI Counter OVF interrupt.
            (0 << USIWM1) | (1 << USIWM0) |                // Select three wire mode to ensure USI written to PB1
            (0 << USICS1) | (1 << USICS0) | (0 << USICLK); // Select Timer0 Compare match as USI Clock source.
    DDRB |= (1 << PB1);                                    // Configure USI_DO as output.
    USISR = 1 << USIOIF |                                  // Clear USI overflow interrupt flag
            (16 - 8);                                      // and set USI counter to count 8 bits
}

// Call from main loop to read from serial
// returns true if data was read
// data placed in variable at pData address
bool readSerialData(uint8_t *pData)
{
    if (serialDataReady)
    {
        *pData = serialInput;
        serialDataReady = false;
        return true;
    }
    return false;
}

// Initialize USI for UART reception.
void initializeUSI()
{
    oldTCCR0B = TCCR0B;
    oldTCCR0A = TCCR0A;
    DDRB &= ~(1 << DDB0); // Set pin 0 to input
    PORTB |= 1 << PB0;    // Enable internal pull-up on pin PB0
    USICR = 0;            // Disable USI. GIFR = 1 << PCIF;     // Clear pin change interrupt flag.
    GIMSK |= 1 << PCIE;   // Enable pin change interrupts
    PCMSK |= 1 << PCINT0; // Enable pin change on pin PB0
}

void onSerialPinChange()
{
    oldTCNT0 = TCNT0;      // Save old timer counter
    GIMSK &= ~(1 << PCIE); // Disable pin change interrupts
    TCCR0A = 2 << WGM00;   // CTC mode
    TCCR0B = CLOCKSELECT;  // Set prescaler to clk or clk /8
    GTCCR |= 1 << PSR0;    // Reset prescaler
    OCR0A = TIMER_TICKS;   // Delay to the middle of start bit accounting for interrupt startup and code execution delay before timer start
    TCNT0 = 0;             // Count up from 0
    TIFR = 1 << OCF0A;     // Clear output compare interrupt flag
    TIMSK |= 1 << OCIE0A;  // Enable output compare interrupt
}

// Will fire for all enabled pin change interrupt pins
ISR(PCINT0_vect)
{
    uint8_t pinbVal = PINB;      // Read directly as Arduino digitalRead is too slow
    if (!(pinbVal & 1 << PINB0)) // Trigger only if DI is Low
    {
        receiving = true;
        digitalWrite(PB2, receiving);
        onSerialPinChange();
    }
}

ISR(TIMER0_COMPA_vect)
{
    if(!receiving) {
        return;
    }
    // COMPA interrupt indicates middle of bit 0
    TIMSK &= ~(1 << OCIE0A); // Disable COMPA interrupt
    TCNT0 = 0;               // Count up from 0
    OCR0A = FULL_BIT_TICKS;  // Shift every bit width
    // Enable USI OVF interrupt, and select Timer0 compare match as USI Clock source:
    USICR = 1 << USIOIE | 0 << USIWM0 | 1 << USICS0;
    // Clear Start condition interrupt flag, USI OVF flag, and set counter
    USISR = 1 << USIOIF | /*1<<USISIF |*/ 8;
}

void serialReceived(uint8_t data)
{
    serialDataReady = true;
    //clearReceive = micros() + (float)(FULL_BIT_TICKS/(F_CPU/1000000);
    receiving = false;
    digitalWrite(PB2, receiving);
    serialInput = data;
}

// Reverse USI byte
uint8_t ReverseByte(uint8_t x)
{
    x = ((x >> 1) & 0x55) | ((x << 1) & 0xaa);
    x = ((x >> 2) & 0x33) | ((x << 2) & 0xcc);
    x = ((x >> 4) & 0x0f) | ((x << 4) & 0xf0);
    return x;
}

// USI overflow interrupt indicates we've received a byte
ISR(USI_OVF_vect)
{
    if (receiving)
    {
        digitalWrite(PB2, receiving);
        uint8_t temp = USIBR;
        USICR = 0; // Disable USI

        //Restore old timer values
        TCCR0A = oldTCCR0A;
        TCCR0B = oldTCCR0B;
        // Note Arduino millis() and micros() will loose the time it took us to receive a byte
        // Approximately 1ms at 9600 baud
        TCNT0 = oldTCNT0;

        serialReceived(ReverseByte(temp));

        GIFR = 1 << PCIF;   // Clear pin change interrupt flag.
        GIMSK |= 1 << PCIE; // Enable pin change interrupts again
        // We are still in the middle of bit 7 and if it is low we will get a pin change event
        // for the stop bit, but we will ignore it because it is high
    }
    if (!receiving)
        digitalWrite(PB2, receiving);
    {
        if (usiserial_send_get_state() == FIRST)
        {
            usiserial_send_set_state(SECOND);
            USIDR = usiserial_get_tx_data() << 7 // Send last 1 bit of data
                    | 0x7F;                      // and stop bits (high)
            USISR = 1 << USIOIF |                // Clear USI overflow interrupt flag
                    (16 - (1 + (STOPBITS)));     // Set USI counter to send last data bit and stop bits
        }
        else
        {
            PORTB |= 1 << PB1;    // Ensure output is high
            DDRB |= (1 << PB1);   // Configure USI_DO as output.
            USICR = 0;            // Disable USI.
            USISR |= 1 << USIOIF; // clear interrupt flag

            //Restore old timer values for Arduino
            #ifdef ARDUINO
                TCCR0A = oldTCCR0A;
                TCCR0B = oldTCCR0B;
                // Note Arduino millis() and micros() will lose the time it took us to send a byte
                // Approximately 1ms at 9600 baud
                TCNT0 = oldTCNT0;
            #endif

            usiserial_send_set_state(AVAILABLE);
        }
    }
}

const uint8_t ledPin = 4;

void setup()
{
    // Tweak clock speed for 5V, comment out if running ATtiny at 3V
    OSCCAL += 3;

    initializeUSI();

    pinMode(ledPin, OUTPUT);
    digitalWrite(ledPin, 0);

    pinMode(PB2, OUTPUT);

    // Send
    pinMode(1, HIGH);      // Configure USI_DO as output.
    digitalWrite(1, HIGH); // Ensure serial output is high when idle
}

void loop()
{
    uint8_t serialInput;
    if (readSerialData(&serialInput))
    {
        //analogWrite(ledPin, serialInput);
        while (!usiserial_send_available())
        {
            // Wait for last send to complete
        }
        usiserial_send_byte(serialInput);
    }

}

#endif
WhiteSpin
  • 179
  • 1
  • 1
  • 7