Networking Microcontrollers

Read time: 70 minutes (17532 words)

Before we leave the world of microcontrollers, let’s look at how we can set up a Microcontroller so it can communicate with other devices, including another Microcontroller!

Most microcontroller have a limited number of I/O lines available. So, it is common to use some form of serial communications to send data from one device to another. As it turns out, there are a large number of devices available to the microcontroller designer that can communicate using this approach. Therefore, we can expand the capability of our little system, by adding extra components! Cool!

Serial communications

In our study of computer architecture, we saw that data and addresses normally move over something called a bus, which is a bunch of parallel lines carrying data elements from place to place. These lines can deliver an entire word of data in one step, all the bits move together over the parallel lines. In the case of the AVR, we do not have a large number of data lines to use. In fact, we want to minimize the number of lines we dedicate to the task of moving data from place to place. The answer to this problem is to send the data bits from place to place one bit at a time over a single data line - serially! So, how do we send out serial data?

Sending data from one device to another is not a simple process. We call the data we want to move a message. A message can be of any length, and we usually measure its length in terms of bytes to be sent. Now, we know that a byte is made up of 8 bits, each with a value of zero or one. If we want to send the byte from place to place serially, we will need to send the bits one at a time over some form of communications channel, which could be as simple as a piece of wire (actually, two are needed - one for the ground line), or as complex as a wireless radio channel capable of reaching millions of miles through space!

Basic data transmission

We will be sending data over a communications channel of some form. If the channel involves wires, we will place an electrical voltage on the wire and a current will move through the wire at a rate of about 11.5 inches per nanosecond - pretty quick, actually! If the channel involves radio, the information might be encoded as one of two different tones, much like the voices you hear over your car radio. (In the old days, when everyone used dial-up connections to the Internet, you probably remember hearing these tones being sent over the phone lines). Actually, there are many schemes for moving bits from place to place one bit at a time, we will confine our discussion to moving them over wires.

To actually move the bits, we need electrical components capable of putting these signals on the communications channel, and detecting them on the other end. These electrical devices usually are smart enough to convert pure binary data into the required signal type depending on the channel to be used. The AVR is certainly smart enough to do this job.

So, once we have a way to place our binary data on the channel, things are easy from there, right? Wrong! There are still some pretty basic problems to overcome. A single bit needs to be placed on the channel for a finite amount of time, the shorter the better, since we need to send lots of bits over the channel. If we just start sending bits from the message, the receiving end is going to get confused in a hurry. To understand why, think of a byte made up of all ones, or all zeros. Each bit will be placed on the channel and the voltage will stay fixed for the time allocated to a single bit. However, if the next bit is also the same as the last, we end up with the channel staying at that voltage for two bit times, and so on. The receiving end will look at this and worry about figuring out where each bit time starts and stops.

Synchronous Data Transmission

The first idea that engineers came up with in thinking this problem through involves using a clock to control sending the data over the communications line. If the receiver is able to generate a clock that runs at exactly the same speed as the transmitter, it will be able to determine where the bits start and stop, and read the channel properly. The problem with this is that it is hard to get two devices, especially if they are far apart, to agree on the timing of even a simple clock. A simple solution to this problem is to build another communications channel on which we place a clock signal, which looks like one of those square waves we have been seeing in our AVR experiments.

With the addition of the clock signal, the receiver will be able to determine where the bits start and stop and the bits will be recovered properly - at the expense of that second wire or radio frequency. Because both transmitter and receiver are locked together by a common clock, we say that the communications is synchronous.

Asynchronous Data Transmission

In a second attempt to solve the bit detection problem, we eliminate the clock and rely on extra bits that are wrapped around the byte to be sent. These extra bits allow the receiver to detect when a byte frame starts, and where it ends. The extra bits may even include error checking data to detect noise on the channel that may be garbling the bits as they move along.

../../_images/asynchronous.png

The extra bits make the transmission time a bit longer, but it reduces the amount of wire we need to string, so this scheme has become the standard for sending data serially in the high level computer world. Unfortunately, the microcontroller world typically does not provide this kind of support except in advanced chips. Our little Arduino board is pretty advanced in that it supports full USB (Universal Serial Bus) communications. This is pretty new in the microcontroller world!

Communications Speed

How fast can we send our data? Usually we send data at some measurable number of bits per second. This rate is just the bit-rate. Sometimes we talk about sending symbols over a line and we measure the number of symbols per second (baud) we are sending. We will not worry about that term in this discussion, since it just adds a bit (!) of confusion.

Remember that we are sending both the bits that represent the message, and bits that are used as management overhead, so the effective transmission rate of somewhat lower than the published bit-rate.

Another term that crops up in a discussion of communications technologies is bandwidth. Simply put, bandwidth measures the capacity of a transmission media to move bits in the face of a number of complications, like noise on a line, and power limitations of the electronics involved. As usual, bigger is better in bandwidth, since it means we can send more bits over the channel!

Serial Protocols

When we send messages from one point to another, we usually add another layer to the puzzle. We have discussed how we might move a series of bytes over a communications channel, but now we face another problem. Where does the message start, where does it stop, and did it move from place to place correctly. To cope with these issues, we design a data container that will hold a chunk of a message. (Why not the whole message? Because the message may be huge, and we can break it up into small parts and number them so they can be reassembled at the remote end.)

Exactly what is in a container depends on the kind of communications we are trying to accomplish. The exact specification for the container is called a protocol and is usually defined officially and managed by a standards body of some sort. For example, in networking, the protocol is called TCP/IP (Terminal Control Protocol/Internet Protocol) which is a world-wide standard allowing computers to communicate with each other all over the globe. In our simple microcontroller world, we use a simpler protocol to move data. That protocol could be RS-232, which has been around a long time, or USB, which is relatively new, or one of several other protocols designed for microcontroller applications.

AVR Support for Serial Communications

The basic circuitry needed to move data serially over simple wire channels is included in many microcontroller chips, including the AVR. These features work just like those we have been using to perform other functions with the AVR, we need to read the data sheet, figure out how to set things up, then use the new features to move data over input and output pins on the chip.

The protocols commonly used with microcontrollers are well defined, allowing many manufacturers to build devices that can be connected to just about any microcontroller. The most common protocols we will see are these:

  • SPI - Serial Peripheral Interface (uses three wires)
    • limited to about 1.6Mbps
  • I2C - Inter IC (uses 2 wires)
    • limited to about 100kbps

Let’s look at the faster of these two techniques in a bit more detail.

SPI Communications

Many of the chips in the AVR family know all about SPI communications. Specifics for each chip are available in the associated data sheet. Our Arduino has full support for this mode of communications, so we will set up some experiments to try it out later.

Here is the basic idea:

../../_images/dataStream.jpg

As you can see, we are back to using a clock to control the communications process. The clock is used to signal when data is available. The sender sends a bit when the clock changes from high to low, and the receiver reads that bit when the clock changes from low to high. If you look at the diagram closely, you can see that this should work fairly well. You should also note that the clock is running twice as fast as the bits are moving! The best part of this scheme is that we do not really need much in the way of additional bits to make sure the bits get send correctly, but it is common to do something simple, like generate a checksum or a hash of the data stream that we can use to see if a block of bits got to the receiver correctly.

Master, I am a Slave!

Most SPI setups have on controller acting as a Master and one or more controllers (or peripherals) acting as Slaves. The Master controls the communications. The Master generates the communications clock signal used in the synchronous communications scheme. The Master also sends a select signal to the slave to tell it that it is to receive the communications. If there are several slaves on a single channel, we will need more lines to indicate which slave is to participate in the communications.

The Slave simply obeys the commands send by the Master. (Duh!)

AVR SPI Pin assignments

Our AVR chip uses pins on PORTB for SPI communications. Here are the pins we will use:

PortB Pin Function
PB3 (MISO) SPI Master input/Slave output
PB2 (MOSI) SPI Master output/Slave input
PB1 (SCK) SPI Bus clock (from Master)
PB0 (SS) SPI Slave select (from Master)

Here is a block diagram from the data sheet showing all the parts needed to make SPI work:

../../_images/SPIBlock.png

Complicated looking, isn’t it. Well, programming it is not all that bad, as we shall see in a bit.

There are several internal registers used to set up and control SPI communications. Again, the data sheet gives all the details.

Here is a typical setup, between a microcontroller and an SPI capable analog to digital converter:

../../_images/MasterSlave.jpg

So, how does it work?

As you can see from the diagram above, the data flows from an internal shift register (see below) out the output pin of one side of the communications, into the input pin of the second component, under control of the clock being generated by the master. This diagram does not show the select line that would be needed if more than one slave was attached to the communication channel.

In a situation where you have many slave devices, you will need to signal which device is to participate in the communications. Obviously, only one at a time can participate since the slave will be sending at the same time the master is sending. Having more than one at a time would result in junk moving down the wire.

Here is a setup showing several slaves.

../../_images/MasterSlave2.jpg

Notice the individual lines between the master and each slave. In this setup, we would use I/O pins on the master to generate signal to the particular chip who is to respond. A little extra circuitry on the master side would allow us to generate a binary address that turns on one of n lines, one for each of the n slaves. (5 pins on the master could select one of 32 slaves).

We could even use other schemes to build a system where as many devices as we like can be used. Engineering such systems can be a lot of fun - not to mention teach you a lot about computer circuits. This is basically how I learned all this stuff back in the late 1970’s!

Example applications

Here are a few examples of simple hardware configurations that let our AVR do interesting things beyond what is possible with the chip alone.

Serial to Parallel Conversion

Suppose you have several input signals that you want to send into an AVR, or several output signals you need to send to a peripheral device. There are simple components called shift registers that lets you stream bits into or out of the circuit, to or from a latch that holds typically eight independent bits. Here is how they would be used:

Serial out
../../_images/SPIoutput.jpg
Serial in

In this circuit, we can have a number of input lines attached to a shift register. We can send a signal to this chip to lock the inputs in place, then read them into the AVR serially.

../../_images/SPIinput.jpg

As you can see, there is not much needed to hook one of these up to an AVR. We can use a single output pin on the AVR to enable the shift register so the input is moved into the AVR, or the output is latched into place. You can hook the individual lines to anything you want, from lights to motor control, to bit detectors. Your choice!

Expanding Memory

The AVR has a limited number of memory locations available to store data. Suppose your application needs more room. There are a number of serial memory devices available ready to hook up to microcontrollers using serial communications. Obviously reading and writing data will be slower than if the memory was available in the chip, but that is nothing new to us. These systems are not typically used for intensely high speed calculations, anyway - so this is not usually a problem.

Number Crunching with the uM-FPU

A neat little 8 pin integrated circuit floating point unit, the uM-FPU V2, has been developed by MicroMega Corporation for use in a number of applications where 32 bit floating point math is required. Such applications can range from processing data obtained from a variety of sensors, to doing position calculations for a wandering robot. (Next term?)

The FPU has a fairly complete set of internal storage and instructions to perform complex math operations. There are 16 general purpose 32 bit registers available, 5 32 bit temporary registers for intermediate results, floating point and long integer math routines

So, how do we shoehorn floating point into the AVR. Especially if the AVR only has a limited amount of memory to hold data.

Well, we store the basic data in the AVR memory, then transmit it using one of the serial communications schemes to the FPU where the math is performed. Once we have the results we need, we fetch them back over the same serial communications lines!

Here is a sample configuration:

../../_images/FPU.jpg
Doing the Math

The 16 internal 32 bit registers allow the AVR to send a number of data values to the FPU and perform math without moving extra data back and forth.

Instructions and data are sent to the FPU using routines that are available as part of a support package from Micromega. Here are a few of the routines provided:

  • fpu_reset - synchronize the PIC and FPU
  • fpu_wait - wait for FPU to complete last operation
  • fpu_sendByte - send 8 bits from W register
  • fpu_readByte - read 8 bits into W register
  • fpu_readDelay - delay after sending a read before actually reading

The instructions defined in the FPU control loading values and performing operations. Here is a sample of the instruction set:

  • SELECTA - select A register (from the 16)
  • SELECTB - select B register
  • LOADBYTE - write signed byte to register 0, convert to float
  • LOADWORD - write signed word to register 0, convert to float
  • FADD - A = A + B
  • FSUB - A = A - B
  • SQRT - A = sqrt(A)
  • LOG - A = ln(A)
  • SIN - A = sin(A) in radians
  • FTOA - Convert float to ASCII, store in string buffer
  • ATOF - convert ASCII to float, store in A
  • FUNCTION - user defined function
Networking two Teensy2 chips

As an experiment in using networking with AVR chips. I took two Teensy2 boards, smaller boards with the same AVR chip used on Arduinos, and set up a simple network experiment.

Using our basic understanding of SPI communications, let’s see if we can get one master Teensy2 to send a command to another slave Teensy2.

Hooking the two devices up is fairly simple. Here is a crude diagram showing the basic setup:

../../_images/TeensyNetwork.png

You need to check out the pin definition card that comes with the Teensy2 to figure this circuit out. Basically, here is what is connected:

Master Slave
GND GND
PB0 (SS) PB6 (pullup)
PB6 (pullup) PB0 (SS)
PD6 (button)  
  PD6 (button)
PB1 (SCLK) PB1 (SCLK)
PB2 (MOSI) PB2 (MOSI)
PB3 (MISO) PB3 (MISO)

In this setup, a push-button has been set up on both Teensy2 chips. When you press the button, a signal is sent to pin PD0. A routine checks to make sure this signal is real (buttons bounce in real life and we need to read the signal at least twice to make sure it was real), and signals the designated chip to do two things:

  • Flash its own LED 5 times
  • Send a command to the other chip to flash its LED 20 times

The same software runs on both chips. The chip that detects a button press becomes the master and sends commands to the other chip which thinks it is a slave unless told to by its own button.

The pin definitions were found in the atmega32u4 data sheet.

Test code in C

Here is the basic C code I used to build this experiment, (modified from example code obtained from http://www.rocketnumbernine.com):

//masterslave.c

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <spi.h>

#define LED_PORT PORTD
#define LED_DDR DDRD
#define LED_BIT 6

#define BUTTON_PORT PORTD
#define BUTTON_PIN PIND
#define BUTTON_BIT PD0
#define DEBOUNCE_TIME 25

#define FLASH_LED_COMMAND 0x01
#define OTHER_SELECT_PIN PB6
#define SELECT_OTHER PORTB &= ~(1<<OTHER_SELECT_PIN)
#define DESELECT_OTHER PORTB |= (1<<OTHER_SELECT_PIN)

#define BUFSIZE 20
volatile unsigned char incoming[BUFSIZE];
volatile short int received=0;
    
void init_io() {
    LED_DDR = _BV(LED_BIT);
    LED_PORT &= _BV(LED_BIT);
    BUTTON_PORT |= _BV(BUTTON_BIT);
}
    
void flash_led(int count) {
    DDRD |= (1<<PD6);
    for (int i=0; i<count*2; i++) {
        PORTD ^= (1<<PD6);
        _delay_ms(75);
    }
}
    
// send a SPI message to the other device - 3 bytes then go back into 
// slave mode
void send_message() {
    setup_spi(SPI_MODE_1, SPI_MSB, SPI_NO_INTERRUPT, SPI_MSTR_CLK8);
    if (SPCR & (1<<MSTR)) { // if we are still in master mode
        SELECT_OTHER; // tell other device to flash LED twice
        send_spi(FLASH_LED_COMMAND); send_spi(0x02); send_spi(0x00);
        DESELECT_OTHER;
    }
    setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);
    flash_led(5);
}
    
ISR(SPI_STC_vect) {
    incoming[received++] = received_from_spi(0x00);
    if (received >= BUFSIZE || incoming[received-1] == 0x00) {
        parse_message();
        received = 0;
    }
}
    
void parse_message() {
    switch(incoming[0]) {
    case FLASH_LED_COMMAND:
        flash_led(incoming[1]);
        break;
    default:
        flash_led(20);
    }
}

int button_is_pressed() {
    if(bit_is_set(BUTTON_PIN, BUTTON_BIT)) {
        _delay_ms(DEBOUNCE_TIME);
        if(bit_is_set(BUTTON_PIN,BUTTON_BIT)) return 1;
    }
    return 0;
}
    
int main(void) {
    init_io();
    DESELECT_OTHER;
    DDRB |= (1<<OTHER_SELECT_PIN);
    setup_spi(SPI_MODE_1, SPI_MSB, SPI_INTERRUPT, SPI_SLAVE);
    sei();  
    flash_led(5);
    while (1)
        if(button_is_pressed()) {
            send_message();
        }
}


The supporting library files are these:

// spi.h

#ifndef _spi_h__
#define _spi_h__

#include <avr/io.h>

#define SPI_SS_PIN PORTB0
#define SPI_SCK_PIN PORTB1
#define SPI_MOSI_PIN PORTB2
#define SPI_MISO_PIN PORTB3

// SPI clock modes
#define SPI_MODE_0 0x00 // Sample (Rising) Setup (Falling) CPOL=0, CPHA=0
#define SPI_MODE_1 0x01 //Setup (Rising) Sample (Falling) CPOL=0, CPHA=1
#define SPI_MODE_2 0x02 // Sample (Falling) Setup (Rising) CPOL=1, CPHA=0
#define SPI_MODE_3 0x03 // Setup (Falling) Sample (Rising) CPOL=1, CPHA=1

// data direction
#define SPI_LSB 1 // send least significant bit (bit 0) first
#define SPI_MSB 0 // send most significant bit (bit 7) first

// whether to raise interrupt when data received (SPIF bit received)
#define SPI_NO_INTERRUPT 0
#define SPI_INTERRUPT 1

// slave or master with clock diviser
#define SPI_SLAVE 0xF0
#define SPI_MSTR_CLK4 0x00     // chip clock/4
#define SPI_MSTR_CLK16 0x01    // chip clock/16
#define SPI_MSTR_CLK64 0x02    // chip clock/64
#define SPI_MSTR_CLK128 0x03   // chip clock/128
#define SPI_MSTR_CLK2 0x04     // chip clock/2
#define SPI_MSTR_CLK8 0x05     // chip clock/8
#define SPI_MSTR_CLK32 0x06    // chip clock/32

// setup spi
void setup_spi(uint8_t mode,   // timing mode SPI_MODE[0-4]
    int dord,           // data direction SPI_LSB|SPI_MSB
    int interrupt,      // whether to raise interrupt on recieve
    uint8_t clock);     // clock diviser
    
// disable spi
void disable_spi(void);
    
// send and receive a byte of data (master mode)
uint8_t send_spi(uint8_t out);
    
// receive the byte of data waiting on the SPI buffer and
// set the next byte to transfer - for use in slave mode
// when interrupts are enabled.
uint8_t received_from_spi(uint8_t out);
    
#endif

And the actual SPI library:

// spi.c

#include <spi.h>
    
void setup_spi(uint8_t mode, int dord, int interrupt, uint8_t clock) {
    // specify pin directions for SPI pins on port B
    if (clock == SPI_SLAVE) {     // if slave SS and SCK is input
        DDRB &= ~(1<<SPI_MOSI_PIN); // input
        DDRB |= (1<<SPI_MISO_PIN);  // output
        DDRB &= ~(1<<SPI_SS_PIN);   // input
        DDRB &= ~(1<<SPI_SCK_PIN);  // input
    } else {
        DDRB |= (1<<SPI_MOSI_PIN);  // output
        DDRB &= ~(1<<SPI_MISO_PIN); // input
        DDRB |= (1<<SPI_SCK_PIN);   // output
        DDRB |= (1<<SPI_SS_PIN);    // output
    }
    SPCR = ((interrupt ? 1 : 0)<<SPIE)          // interrupt enabled
        | (1<<SPE)                                // enable SPI
        | (dord<<DORD) // LSB or MSB
        | (((clock != SPI_SLAVE) ? 1 : 0) <<MSTR) // Slave or Master
        | (((mode & 0x02) == 2) << CPOL)          // clock timing mode CPOL
        | (((mode & 0x01)) << CPHA)               // clock timing mode CPHA
        | (((clock & 0x02) == 2) << SPR1)         // cpu clock divisor SPR1
        | ((clock & 0x01) << SPR0);               // cpu clock divisor SPR0
    SPSR = (((clock & 0x04) == 4) << SPI2X);    // clock divisor SPI2X
}

void disable_spi() {
    SPCR = 0;
}

uint8_t send_spi(uint8_t out) {
    SPDR = out;
    while (!(SPSR & (1<<SPIF)));
    return SPDR;
}

uint8_t received_from_spi(uint8_t data) {
    SPDR = data;
    return SPDR;
}


Running this code generates the following behavior:

  • As each chip power up, it blinks 5 times
  • Pressing either button causes:
    • the LED on the selected chip blinks 5 times
    • the LED on the opposite chip blinks 20 times

As a last project (HA!), try converting this to assembly language!