In my previous post, USI Serial UART Receive on ATtiny, we discussed how to implement a Serial UART receiver on an Atmel ATtiny using the USI module. This post describes how to implement a simple UART transmitter using the USI module.
UART Signal
The transmitter will be simpler than the receiver as we just need to generate the signal with the correct timing.
Our transmitter will send 8 data bits with one or two stop bits.
#define STOPBITS 1
Calculating the width of a bit
We define the clock speed, F_CPU, and the baud rate. Note F_CPU is already defined in Arduino and may already be defined in your development environment, it’s normally defined as a build symbol.
#define F_CPU 8000000 #define BAUDRATE 9600
We can use these to calculate the number of CPU clock cycles per bit width.
#define CYCLES_PER_BIT ( F_CPU / BAUDRATE )
If this number is 255 or less then we set the clock source to be the CPU clock, otherwise we will use the prescaler to divide the clock input to Timer/Counter0 by 8.
We use the bottom three Clock Select bits of Timer/Counter0 Control Register B, TCCR0B, to configure the prescaler. A Clock Select value of 1 is for the CPU clock and a value of 2 is for CPU clock divided by 8.
#if (CYCLES_PER_BIT > 255) #define DIVISOR 8 #define PRESCALE 2 #else #define DIVISOR 1 #define PRESCALE 1 #endif #define FULL_BIT_TICKS ( CYCLES_PER_BIT / DIVISOR )
Choosing the Tx pin
We can configure the USI module to output data on PB0 (SDA), used in Two Wire I2C mode or PB1 (DO), used in Three Wire SPI mode. But it can only read data from PB0 (DI/SDA).
A serial UART has separate transmit, Tx, and receive, Rx, lines. So if we want to enable both transmit and receive on the same device then we will need to use DO/PB1 for output.
Sending the first buffer
Finally we need to fill the USI Data Register with some data and start the clock countdown. Our packet is going to be longer than the 8 bit USI data register once we have added start and stop bits. So will need to split our packet across two buffer sends. The first buffer from our send function and will contain the start bit and 7 bits of data. The second buffer will be sent from the USI overflow interrupt and contain 1 bit of data and one or two stop bits.
Because the USI overflow interrupt will be called twice, once after sending the first buffer and again after sending the second buffer, we need a way to tell it which buffer it has just sent, so it can decide what to do next. We will store this state in a volatile variable as it will be accessed from an interrupt.
enum USISERIAL_SEND_STATE { AVAILABLE, FIRST, SECOND }; static volatile enum USISERIAL_SEND_STATE usiserial_send_state = AVAILABLE;
The USI sends the most significant bit (bit 7) and shifts left on each clock cycle, but the UART standard requires that the least significant bit (bit 0) be sent first. So the data needs to be loaded into the USI register in reverse bit order.
We can use the following function to efficiently reverse the bits in a byte:
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; }
We will use a volatile variable to store the reversed data as it will be accessed from an interrupt.
static volatile uint8_t usiserial_tx_data;
The send routine
The first thing our serial send routine does is set the usiserial_send_state variable to indicate that we are sending the FIRST buffer.
void usiserial_send_byte(uint8_t data) { usiserial_send_state = FIRST;
Then we save the reversed data in our volatile usiserial_tx_data variable.
usiserial_tx_data = reverse_byte(data);
Configuring the timer
We want to configure Timer/Counter0 to Clear Timer on Compare Match (CTC) mode. We do this by setting the three bits of the Waveform Generation Mode, WGM0, flag to 2 (binary 010). Just for fun bit 0 and bit 1 are in the TCCR0A register and bit 2 is in the TCCR0B register. The other bits of TCCR0A are set to zero to indicate normal port operation. The least significant three bits of TCCR0B are used for the Clock Select mode and the rest of the bits (including WGM0 bit 2) are set to zero.
TCCR0A = 2<<WGM00; // CTC mode TCCR0B = CLOCKSELECT; // Set prescaler to clk or clk /8
Then we reset the prescaler to indicate that we changed its configuration and start Timer/Counter0 at zero.
GTCCR |= 1 << PSR0; // Reset prescaler TCNT0 = 0; // Count up from 0
We store the number of bits to count in Output Compare Register A, OCR0A.
OCR0A = FULL_BIT_TICKS; // Trigger every full bit width
Configuring the USI module
To use PB1 (DO) for USI output we select Three Wire SPI mode by setting Wire Mode, USIWM[1:0], to 01 in the USI Control Register USICR and setting the PB1 (DO) data direction to output. We also use the USI Control Register to enable the USI Counter overflow interrupt, USIOIE=1 and select Timer0 Compare Match as the input source, USICS[1:0]=01 and USICLK=0.
USICR = (0<<USIWM1)|(1<<USIWM0)| // Select three wire mode so USI output on PB1 (1<<USIOIE)| // Enable USI Counter OVF interrupt. (0<<USICS1)|(1<<USICS0)|(0<<USICLK); // Timer0 Compare Match as USI clock source DDRB |= (1<<PB1); // Configure USI DO, PB1 as output
Now we can load up the USI data register with the start bit (low) and the first 7 bits of reversed data.
USIDR = 0x00 | // Start bit (low) usiserial_tx_data >> 1; // followed by first 7 bits of serial data
Finally we ensure that the USI Overflow Interrupt Flag is cleared and set the counter to increment eight times before overflowing. The 4 bit USI counter can hold a value from 0 to 15 and overflows as it attempts to increment to 16, so we set the starting value of the counter to 16 – 8 = 8.
USISR = 1<<USIOIF | // Clear USI overflow interrupt flag (16 - 8); // and set USI counter to count 8 bits }
The USI interrupt vector
After the first buffer has been sent by the USI module, the USI overflow interrupt vector will be called with our state variable, usiserial_send_state, set to FIRST. Now we set usiserial_send_state to SECOND to indicate we are sending the second buffer.
ISR (USI_OVF_vect) { if (usiserial_send_state == FIRST) { usiserial_send_state = SECOND;
We load up the USI Data Register with our last bit of data and the stop bits (high). There should only be one or two stop bits, but we need to set the rest of the bits to something, so we set them high. Interestingly, while one and two are the UART supported number of stop bits, values higher than two would work as the serial UART standard supports periods of line idle (high) between packets.
USIDR = usiserial_tx_data << 7 // Send last 1 bit of data | 0x7F; // and stop bits (high)
The number of bits to send is the number of stop bits + 1, so we set the counter to 16 minus this value. The USI Overflow Interrupt Flag is cleared to indicate the interrupt has been processed. If we did not do this the microcontroller would immediately re-invoke the interrupt vector rather than when the counter overflowed.
USISR = 1<<USIOIF | // Clear USI overflow interrupt flag (16 - (1 + (STOPBITS))); // Set USI counter for 1 data bit and stop bits }
After the second buffer
After the second buffer has been sent by the USI module, the USI overflow interrupt vector will be called with our state variable, usiserial_send_state, set to SECOND.
Our packet has been sent, so we just need to turn off the USI module returning the Tx pin to an idle high state. Just to be safe we make sure PB1 is still configured as a high output.
else // usiserial_send_state == SECOND { PORTB |= 1 << PB1; // Ensure output is high DDRB |= (1<<PB1); // Configure USI_DO as output. USICR = 0; // Disable USI.
Finally we clear the USI Overflow Interrupt Flag and set our state variable to AVAILABLE, to indicate that our routine is available to send another packet.
USISR |= 1<<USIOIF; // clear interrupt flag usiserial_send_state = AVAILABLE; } }
Sending multiple bytes
To keep this tutorial simple our routine only sends one byte at a time. To send multiple bytes we need to wait for our state variable to become AVAILABLE before calling our send routine with the next byte.
char message[] = "USI Serial\r\n"; uint8_t len = sizeof(message)-1; for (uint8_t i = 0; i<len; i++) { while (!(usiserial_send_state==AVAILABLE)) { // Wait for last send to complete } usiserial_send_byte(message[i]); }
For safety and convenience we could also wait for usiserial_send_state to become AVAILABLE at the very beginning of our send routine, usiserial_send_byte.
Enhancing for multiple bytes
When sending multiple bytes we stop the USI module after sending the second buffer and reinitialize the timer when we start sending the next packet. This introduces a small gap of idle time between packets, this isn’t a problem as the UART standard supports idle (line high) time between packets.
However if we knew that we were going to send another packet we could just load the USI Data Register with the first buffer of the next packet and set the state variable back to FIRST.
Arduino Timer
When running this code on Arduino we need to borrow the timer used for millis. To avoid disrupting the Arduino timer we should save the state of the timer registers in volatile variables.
#ifdef ARDUINO volatile static uint8_t oldTCCR0A; volatile static uint8_t oldTCCR0B; volatile static uint8_t oldTCNT0; #endif
We can save these registers in our send routine after setting the usiserial_send_state to FIRST.
#ifdef ARDUINO oldTCCR0B = TCCR0B; oldTCCR0A = TCCR0A; oldTCNT0 = TCNT0; #endif
Then we can restore the registers it in the USI Overflow Interrupt vector before setting usiserial_send_state to AVAILABLE.
#ifdef ARDUINO TCCR0A = oldTCCR0A; TCCR0B = oldTCCR0B; TCNT0 = oldTCNT0; #endif
Arduino will lose the time it takes us to send each byte, approximately 1ms at 9600 baud, which will not be an issue for most programs. However, if you need to maintain Arduino time with more accuracy, see my post on Borrowing an Arduino Timer.
Get the example code
I have uploaded an Arduino sketch to GitHub at:
https://github.com/MarkOsborne/becomingmaker/tree/master/USISerialSend
This example sends the message “USI Serial” every second. Connect your favorite serial device’s Rx pin to PB1/DO.