0

I've put together a simple software serial (send-only) implementation for use on my attiny8[45] microcontrollers. I realize that there are more efficient USI-based implementations out there; this was mostly an educational exercise. The code uses TIMER0 in CTC mode to time the sending of bits on an available output pin.

It works fine from 300bps up to 4800bps, but anything faster than that just results in garbage.

The complete code is at the bottom of this answer. The following settings all work correctly:

  1. 4800bps

    #define BPS 4800
    #define SCALE_FLAG 1
    #define SCALE_VAL 1
    
  2. 2400bps

    #define BPS 2400
    #define SCALE_FLAG 2
    #define SCALE_VAL 8
    
  3. 1200bps

    #define BPS 1200
    #define SCALE_FLAG 2
    #define SCALE_VAL 8
    
  4. 300bps

    #define BPS 300
    #define SCALE_FLAG 3
    #define SCALE_VAL 64
    

While this fails:

  1. 9600bps

    #define BPS 9600
    #define SCALE_FLAG 1
    #define SCALE_VAL 1
    

Because everything other than 9600bps works I'm tempted to think that my code is correct, and yet I expect that I should be able to achieve 9600bps with these microcontrollers.

Are there any obvious bugs in this code? I think that's the most likely explanation, but after poking at this for a couple of days I'm not seeing it.


#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <string.h>

#define BPS 9600
#define SCALE_FLAG 1  // This is the value that goes in the TCCR0B register
#define SCALE_VAL 1   // This is the divisor selected by SCALE_FLAG

#define TXPORT PORTB
#define TXDDR DDRB
#define TXPIN PORTB0

#define TICKS_PER_BIT (F_CPU/BPS/SCALE_VAL)
#define mS_PER_BIT (1000000/BPS/1000)
#define uS_PER_BIT ((1000000 / BPS) - (1000 * mS_PER_BIT))

typedef struct SERIAL_PORT {
    uint8_t data;
    uint8_t index;
    uint8_t busy;
} SERIAL_PORT;

void millis_init();
uint32_t millis();

void serial_init();
void serial_putchar(char c);
void serial_print(char *s);
void serial_println(char *s);
void delay(uint32_t m);

volatile SERIAL_PORT port;
volatile uint32_t _millis,
         _micros = 1000;

int main() {
    millis_init();
    serial_init();
    while (1) {
        serial_println("hello world");
        delay(500);
    }
}

void delay(uint32_t m) {
    uint32_t t_start = millis();
    while (millis() - t_start < m);
}

void millis_init() {
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        _millis = 0;
    }
}

uint32_t millis() {
    uint32_t x;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      x = _millis;
    }
    return x;
}

void serial_init() {
    DDRB |= 1<<TXPIN;       // Set TXPIN as an output
    TXPORT |= 1<<TXPIN;     // Set TXPIN high (serial idle)
    TCCR0A = 1<<WGM01;      // Select CTC mode
    TCCR0B = SCALE_FLAG;    // Set clock scaler
    OCR0A = TICKS_PER_BIT;  // Set CTC target value
    TIMSK0 |= 1<<OCIE0A;    // Enable compare match interrupt
    sei();
}

void serial_putchar(char c) {
    while (port.busy);
    port.data = c;
    port.index = 0;
    port.busy = 1;
}

void serial_print(char *s) {
    while (*s) serial_putchar(*s++);
}

void serial_println(char *s) {
    serial_print(s);
    serial_putchar('\r');
    serial_putchar('\n');
}

ISR(TIM0_COMPA_vect) {
    if (port.busy) {
        switch(port.index) {
            case 0:
                // send start bit
                TXPORT &= ~(1<<TXPIN);
                break;
            case 9:
                // send stop bit
                TXPORT |= (1<<TXPIN);
                port.busy = 0;
                break;
            default:
                // send data bit
                if (port.data & 1) {
                    TXPORT |= 1<<TXPIN;
                } else {
                    TXPORT &= ~(1<<TXPIN);
                }
                port.data >>= 1;
                break;
        }
        port.index++;
    }

    _millis += mS_PER_BIT;

    if (uS_PER_BIT > _micros) {
        _millis++;
        _micros = 1000;
    } else {
        _micros -= uS_PER_BIT;
    }
}
  • What is your clock frequency? – Majenko Jan 29 at 17:19
  • Sorry, I should have mentioned. The attiny84 is currently running at 1Mhz. – larsks Jan 29 at 18:00
  • Have you considered that you just don't have enough time to run your ISR at that speed? Simplify it (remove the millis stuff) and see if it improves. – Majenko Jan 29 at 18:02
  • ...and if I set the clock frequency to 8Mhz instead, it starts working. I suspect you are correct. – larsks Jan 29 at 18:02
  • Well for 9600 at 1MHz you have 104 ticks per bit. That's the number of clock cycles you have to git your ISR into, and that ISR compiles to around 126 clock cycles total (I didn't follow the branches, just counted the instructions). – Majenko Jan 29 at 18:23
1

At 1MHz and 9600 baud, you have a total of 104 clock cycles available to run your interrupt routine in. A quick count up of the assembly instructions generated comes to more than that (around 126 at first glance), so you can't fit it into the time.

As a result, your baud rate will actually be lower than 9600, and the chip will be completely starved of clock cycles for doing anything in your main loop.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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