View on GitHub

The AVR-Sandbox Project

Dig deeper into the avr world and discover every single bit in embedded engineering and IoT.

Download this project as a .zip file Download this project as a tar.gz file

Hello-SPI

Topics Covered:

1) SPI basics. 2) SPI Register in atmega32A/atmega328p. 3) SPI Implementation for atmega32A/atmega328p. 4) Cracking MCP3008 Datasheet. 5) Operating MCP3008 using the SPI data bus interface. —————————————————————————

1) SPI basics: –Jump to Topics–

The SPI (serial peripheral interface) is a bus interface connection incorpo- rated into many devices such as ADC, DAC, and EEPROM.

In this tutorial, we are going to show off the basics of the SPI protocol and interfacing with MicroChip MCP3008 10-bit ADC as an example.

image

Clock Polarity (CPOL) Clock Phase (CPHA)
Determines the base value of the Serial Clock, either HIGH or LOW Determines when to sample data, either on the leading edge or on the trailing edge
SPI Mode 0 (MODE_0_0) SPI Mode 1 (MODE_0_1) SPI Mode 2 (MODE_1_0) SPI Mode 3 (MODE_1_1)
CPOL = 0, base value of SCLK is LOW CPOL = 0 CPOL = 1, base value of SCLK is HIGH CPOL = 1
CPHA = 0, sample data on the leading edge of the SCLK, aka on the rising edge CPHA = 1, sample data on the trailing edge of the SCLK, aka on the falling edge CPHA = 0, sample data on the leading edge, aka falling edge CPHA = 1, sample data on the trailing edge, aka the rising edge

image


2) SPI Registers in atmega32A/atmega328p: –Jump to Topics–

SPCR SPSR SPDR
image image image
SPI Clock Speed Configurations
image

3) SPI Implementation for atmega32A/atmega328p: –Jump to Topics–

1) Setup pins directions using DDRX (data direction registers) for MOSI, SCLK, SS and MISO according to data transmission and the user manual:

/** Define SPI pins according to the device and the datasheet */
#if defined (__AVR_ATmega32__)
#   define MISO 6
#   define MOSI 5
#   define SCK 7
#elif defined (__AVR_ATmega328P__)
#   define MISO 4
#   define MOSI 3
#   define SCK 5
#endif 
if (transmissionType == MASTER) {
    /* define user-defined pins as output */
    DDRB |= (1 << MOSI) | (1 << SCK);
} else if (transmissionType == SLAVE) {
    DDRB |= (1 << MISO);
}
/* set slave select as output */
DDRB |= (1 << SS_PIN);

The SS_PIN is trivial and you can choose to add as many SS as you want, the SS initializes the SLAVE communication and ends the communication with the MASTER.

2) Setup SPI clock speed with respective to the Fosc:

/** Defines the SPI rate with respect to the Fosc */
#define Fosc_1_4  ((SPIFosc) 0.25)
#define Fosc_1_16 ((SPIFosc) 1/16)
#define Fosc_1_64 ((SPIFosc) 1/64)
#define Fosc_1_128 ((SPIFosc) 1/128)
#define Fosc_1_2 ((SPIFosc) 0.5)
#define Fosc_1_32 ((SPIFosc) 1/32)
...

if (spiFosc == Fosc_1_2) {
    /* flip SPR0/1 to zero */
    SPCR = SPCR & (~(1 << SPR0) & ~(1 << SPR1));
    /* flip SPI2X bit to one */
    SPSR |= (1 << SPI2X);
} else if (spiFosc == Fosc_1_4) {
    /* flip SPR0/1 to zero */
    SPCR = SPCR & (~(1 << SPR0) & ~(1 << SPR1));
    /* flip SPI2X bit to zero */
    SPSR &= ~(1 << SPI2X);
} else if (spiFosc == Fosc_1_16) {
    /* flip SPR0 to one and SPR1 to zero */
    SPCR = (SPCR & ~(1 << SPR1)) | (1 << SPR0);
    /* flip SPI2X bit to zero */
    SPSR &= ~(1 << SPI2X);
} else if (spiFosc == Fosc_1_32) {
    /* flip SPR0 to zero and SPR1 to one */
    SPCR = (SPCR & ~(1 << SPR0)) | (1 << SPR1);
    /* flip SPI2X bit to one */
    SPSR |= (1 << SPI2X);
} else if (spiFosc == Fosc_1_64) {
    /* flip SPR0/1 to one */
    SPCR = SPCR | (1 << SPR0) | (1 << SPR1);
    /* flip SPI2X bit to one */
    SPSR |= (1 << SPI2X);
} else if (spiFosc == Fosc_1_128) {
    /* flip SPR0/1 to one */
    SPCR = SPCR | (1 << SPR0) | (1 << SPR1);
    /* flip SPI2X bit to zero */
    SPSR &= ~(1 << SPI2X);
}

3) Setup SPI Clock MODE either Mode_0, Mode_1, Mode_2 or Mode_3:

/** Serial Data modes sampling */
#define MODE_0_0 (ModeOfTransmission (SPCR & (~(1 << CPOL) & ~(1 << CPHA))))
#define MODE_0_1 (ModeOfTransmission (SPCR & (~(1 << CPOL) | (1 << CPHA))))
#define MODE_1_0 (ModeOfTransmission (SPCR & ((1 << CPOL) & ~(1 << CPHA))))
#define MODE_1_1 (ModeOfTransmission (SPCR & ((1 << CPOL) | ((1 << CPHA))))

4) Setup the transmission type, either MASTER or SLAVE:

/** Defines the [TransmissionType] datatype */
#define TransmissionType int

/** Defines the [MASTER] and [SLAVE] transmission modes */
#define MASTER (TransmissionType (1 << MSTR))
#define SLAVE (TransmissionType (SPCR & ~(1 << MSTR)))

5) Now add the SPCR command to start the protocol with some configuration:

...
/* start the protocol */
SPCR |= (1 << SPE) | (transmissionType) | MODE;

The final function to start the SPI protocol would look like:

void Serial::SPI::startProtocol(const TransmissionType& transmissionType, const SPIFosc& spiFosc, const ModeOfTransmission& MODE) {

    if (transmissionType == MASTER) {
        /* define user-defined pins as output */
        DDRB |= (1 << MOSI) | (1 << SCK);
    } else if (transmissionType == SLAVE) {
        DDRB |= (1 << MISO);
    }

    SPCR = 0x00;

    if (spiFosc == Fosc_1_2) {
        /* flip SPR0/1 to zero */
        SPCR = SPCR & (~(1 << SPR0) & ~(1 << SPR1));
        /* flip SPI2X bit to one */
        SPSR |= (1 << SPI2X);
    } else if (spiFosc == Fosc_1_4) {
        /* flip SPR0/1 to zero */
        SPCR = SPCR & (~(1 << SPR0) & ~(1 << SPR1));
        /* flip SPI2X bit to zero */
        SPSR &= ~(1 << SPI2X);
    } else if (spiFosc == Fosc_1_16) {
        /* flip SPR0 to one and SPR1 to zero */
        SPCR = (SPCR & ~(1 << SPR1)) | (1 << SPR0);
        /* flip SPI2X bit to zero */
        SPSR &= ~(1 << SPI2X);
    } else if (spiFosc == Fosc_1_32) {
        /* flip SPR0 to zero and SPR1 to one */
        SPCR = (SPCR & ~(1 << SPR0)) | (1 << SPR1);
        /* flip SPI2X bit to one */
        SPSR |= (1 << SPI2X);
    } else if (spiFosc == Fosc_1_64) {
        /* flip SPR0/1 to one */
        SPCR = SPCR | (1 << SPR0) | (1 << SPR1);
        /* flip SPI2X bit to one */
        SPSR |= (1 << SPI2X);
    } else if (spiFosc == Fosc_1_128) {
        /* flip SPR0/1 to one */
        SPCR = SPCR | (1 << SPR0) | (1 << SPR1);
        /* flip SPI2X bit to zero */
        SPSR &= ~(1 << SPI2X);
    }
    /* start the protocol */
    SPCR |= (1 << SPE) | (transmissionType) | MODE;
}

6) Generating clocks with specific width (or delay):

void Serial::SPI::generateSCLK(const uint32_t& count, const uint8_t& width) {
    for (uint32_t i = 0; i < count; i++) {
        PORTB &= ~(1 << SCK);
        _delay_us(width / 1000);
        PORTB |= (1 << SCK);
        _delay_us(width / 1000);
    }
}

7) Writing data to the SPDR:

/** Defines boolean flags */
#define isTransmissionCompleted() ((boolean) (SPSR & (1 << SPIF)))
...
void Serial::SPI::write(const uint8_t& data) {
    SPDR = data;
    while (!isTransmissionCompleted());
}

4) Cracking MCP3008 Datasheet: –Jump to Topics–

General Charachteristics Packages overview
image image
Dark Red: refers to the resolution and the channels for each package Dark red: the AIN Channels and the Vref for resolution channel (comparator resolution)
Light Red: refers to the supported SPI modes of operation Green: Vin
Green: refers to a brief electrical charachteristics including operating voltage and maximum current draw (in case of low resistivity status circuit) Black: GNDs
Orange: refers to the packages available in the market Blue: the SPI lines
Absolute charachteristics
image
Clock Data General SPI Clock diagram of the operation
image image
Serial Data frames
image
Blue: the SPI data lines
Purple: the CS or SS delay timings
Green Square: represents the starter byte written to the SPDR of the atmega32 at the DIN (MOSI or COPI) before the 1st 8-clocks
Blue Square: represents the CHANNEL configuration byte written to the SPDR of the atmega32 at the DIN (MOSI or COPI) before the 2nd 8-clocks
Red Square: represents the data read from the SPDR at the DOUT (MISO or CIPO) line after the last 8-clocks
SPI Mode_0 SPI Mode_3
image image
In this mode the SPI CLK starts with LOW specified by [CPOL = 0] and the data is sampled at the leading edge specified by [CPHA = 0] In this mode the SPI CLK defaults to HIGH specified by [CPOL = 1] and the data is sampled at the trailing edge specified by [CPHA = 1]
Green dot and square: refers to the starter byte loaded to the SPDR before the first 8 clocks Green dot and square: refers to the starter byte loaded to the SPDR before the first 8 clocks
Blue dot and square: refers to the configuration byte loaded to the SPDR before the second 8 clocks Blue dot and square: refers to the configuration byte loaded to the SPDR before the second 8 clocks
Red dot and square: refers to the data read from the SPDR after the transmission of the last two 8 clocks Red dot and square: refers to the data read from the SPDR after the transmission of the last two 8 clocks
Orange: refers to the data write to the SPDR sampling state, the falling edge of the clk Orange: refers to the data write to the SPDR sampling state, the falling edge of the clk
Purpule/Pink: refers to the data read from SPDR sampling state, the rising edge of the clk Purpule/Pink: refers to the data read from SPDR sampling state, the rising edge of the clk
Green Arrow: shows the idle mode state, LOW The idle mode state is HIGH in this case
Configuration bits for MCP3008
image

5) Operating MCP3008 using the SPI data bus interface: –Jump to Topics–

MCP3008 - Arduino-NANO/AtMega328p Circuit diagram

1) Initialize the SPI as MASTER with SPI MODE_0. 2) Bring CS to LOW to start the A/D Communication. 3) Send the starter byte to the A/D register [0]-[0]-[0]-[0]-[0]-[0]-[0]-[1]. 4) Generate 8 clocks to clock out the starter byte at the MOSI to the ADC at the rising edge of the SCLK.

void AD::MCP3008::init(volatile uint8_t& PORT, const uint8_t& SS_PIN) {
     
    isADConversionFinished = 0;

     /* start protocol */
    Serial::SPI::getInstance()->startProtocol(MASTER, Fosc_1_4, MODE_0_0);
    
    /* bring CS or SS to low to start this slave communication */
    PORTB &= ~(SS_PIN);
    _delay_us(100 / 1000);

    /* send starter bits */
    Serial::SPI::getInstance()->write(0b00000001);

    /* clock out the data to the A/D IC */
    Serial::SPI::getInstance()->generateSCLK(8, 125);
}

5) Start the A/D Conversion by writing the input AIN Channel configuration. 6) Generate 8 SCLK signals to clock out the config data at the MOSI line to the ADC register and clock out the first data frame out of the A/D on the falling edge of the SCLK. 7) Read the SPDR register to get a [x]-[x]-[x]-[x]-[x]-[null-0]-[B10]-[B9] data, an 8-bit data consisting of 5 UNKNOWN BITS, NULL BIT representing the start of the data frame and last 2 bits in the data frame representing the MSBs of the A/D conversion. 8) Get rid of UNKOWN bits and NULL bit from the first data frame by ANDING the first data frame with 0b00000011 to preserve only the last 2 bits which represent the higheset 2 order bits of the conversion. 9) Shift the first data frame eight bits to the left to leave room for the lower order bits (lower order frame). 10) Generate another 8 SCLK signals to clock out the second data frame from the ADC register on the falling edge of the SCLK. 11) Read the second data frame byte and concatenate it to a uint16_t data variable.

#define Configuartion uint8_t

/** Define A/D Channels for config byte */
#define CHANNEL_0 ((uint8_t) 0b10000000) 
#define CHANNEL_1 (CHANNEL_0 + 1)
#define CHANNEL_2 (CHANNEL_1 + 1)
#define CHANNEL_3 (CHANNEL_2 + 1)
#define CHANNEL_4 (CHANNEL_3 + 1)
#define CHANNEL_5 (CHANNEL_4 + 1)
#define CHANNEL_6 (CHANNEL_5 + 1)
#define CHANNEL_7 (CHANNEL_6 + 1)
...
void AD::MCP3008::startADConversion(const Configuartion& config) {

    /* start A/D conversion on a CHANNEL */
    Serial::SPI::getInstance()->write(config);

    /* clock out config to the A/D IC */
    Serial::SPI::getInstance()->generateSCLK(8, 125);

    /* remove NULL bit (bit no.3) from the first data frame and shift first frame 8-bits to the left */
    analogData = (SPDR & 0b0000011) << 8;

    /* clock to read the 2nd data frame */
    Serial::SPI::getInstance()->generateSCLK(8, 125);

    /* add the data frame to the uint16_t bit data buffer */
    analogData |= SPDR;

    isADConversionFinished = 1;
}

12) Terminate the A/D conversion by bringing CS to HIGH.

void AD::MCP3008::endADConversion(volatile uint8_t& PORT, const uint8_t& SS_PIN) {
    /* finish A/D conversion by bringing the CS to high */
    PORT |= SS_PIN;
    _delay_us(270 / 1000);
}

13) Re-iterate for more A/D conversion or use an A/D monitoring pattern.

void AD::MCP3008::monitorADConversion(const Configuartion& config, volatile uint8_t& PORT, const uint8_t& SS_PIN, void(*action)(uint16_t&)) {
    while (1) {
        /* initialize the MCP */
        init(PORT, SS_PIN);
        /* start A/D conversion on a CHANNEL */
        startADConversion(config);
        /* fire an observer */
        if (action != NULL) {
            action(analogData);
        }
        /* end A/D conversion */
        endADConversion(PORT, SS_PIN);
    }
}

Example:

#define F_CPU 16000000UL

#include<Serial.h>
#include<util/delay.h>
#include<MCP3008/MCP3008.h>

#define SS_PIN 2

void Serial::SPI::onDataTransmitCompleted(volatile uint8_t& data) { }

void Serial::UART::onDataReceiveCompleted(const uint8_t& data) { }

void Serial::UART::onDataTransmitCompleted(const uint8_t& data) { }

static inline void invoke(uint16_t& data) {
    Serial::UART::getInstance()->println(data, 10);
}

int main (void) {
    Serial::UART::getInstance()->startProtocol(BAUD_RATE_57600_16MHZ);
    Serial::UART::getInstance()->sprintln((char*) "Communication starts...");
   
    /* set slave select as output */
    DDRB |= (1 << SS_PIN);
    /* start adc monitoring */
    AD::MCP3008::getInstance()->monitorADConversion(CHANNEL_0, PORTB, (1 << SS_PIN), &invoke);

    return 0;
}