0
\$\begingroup\$

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
\$\endgroup\$
0

2 Answers 2

2
\$\begingroup\$

My two Cents:

On USI-Receive Interrupt copy byte in RAM-buffer. Make a simple ring buffer which will overwrite if capacity is exceeded. With every byte receive you reset a counter delay. If counter times out no more bytes are received - rather the buffer is flushed out within a simple while () loop. After this go back to read mode.

In conclusion: Implement a state machine with these four states.

  1. Boot/Init
  2. Read Mode
  3. Send Mode
  4. Error Mode

Switch from Read to Send mode on basis of a frame time out logic. Leave send mode after complete flush. Enter Error mode upon custom logic.

If you want byte-wise relay implementation just do the following: Upon USI-Receive Interrupt prepare the peripheral to send and copy received byte into output buffer. Then trigger a flush. Also implement USI-Send interrupt and setup peripheral to receive again after transmit is finished. If there are no dedicated interrupts you must implement this behaviour in a state machine. Make sure to only use volatile variables.

Have fun :)

My approach: All the data handling is done in ISR. Main loop does only poll a few flags which are set within ISR's to indicate errors.

\$\endgroup\$
0
\$\begingroup\$

I'm currently trying to implement a simple UART echo application on the ATtiny85

If all you want to do is echo, then you can simplify a lot!

Set up a timer running at least 2X times the bitrate. On each timer interrupt, sample the RX pin and copy the value to the TX pin. If you want the RX to be delayed 1 byte behind the TX, then add a software shift register long enough to achieve the desired delay.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ thanks for your input, i should have clarified im building the echo application as a proof of concept and interested in how other simplex UART applications tackle this problem. \$\endgroup\$
    – WhiteSpin
    Commented Jan 10, 2020 at 9:28
  • \$\begingroup\$ @WhiteSpin Maybe ask a new question like "How to read and then retransmit multibyte serial strings with the ATTINYx5 USI?" that more specifically address what you are looking to do? \$\endgroup\$
    – bigjosh
    Commented Jan 11, 2020 at 21:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.