AVR programming introduction/Serial Peripheral Interface

From Wikiversity
Jump to navigation Jump to search

Serial Peripheral Interface, or SPI, is used to interface a multitude of common peripherals, such as:

In this task, we will consider the use of the ATmega8 internal SPI port[1] to interface perhaps the simplest possible SPI device based on either of the following shift register chips:

  • 74HC595[2] (КР1564ИР52) – having an internal latch;
  • or 74HC164[3] (К1564ИР8) – lacking one.

Before you begin[edit | edit source]

For this task, we connect the following circuits and components to our simplistic device.

  • 2–8 LED circuits, each of which is comprised of the following two components connected in series:
    • the LED itself, which could be of any color and shape (as long as it’s possible to connect it to the circuit), for the voltage of around 1.5 V;
    • a current-limiting resistor for the resistance in the 470–1100 Ω range (assuming a 3–5 V power source), or – provided that no more than 4 or 5 LEDs are connected to the register – 360–820 Ω.
  • The 74HC595 (74HCT595, КР1564ИР52) chip connected as follows:
    • directly to the ground (with its pin 8) and Vcc (16);
    • via 100 kΩ or so series resistors to Vcc – with its MR and OE (10, 13) pins;
    • to the LED circuits described above (15, 1–7);
    • to the SS, MOSI, SCk (16, 17, 19; or D 10, D 11, D 13 on Arduino Uno) ATmega8 pins – with StCP, DS, ShCP (12, 14, 11.)
  • Or the 74HC164 (74HCT164, К1564ИР8) chip, connected as follows:
    • directly to the ground (with its pin 7) and Vcc (14);
    • via 100 kΩ or so series resistors to Vcc – with its A and Clr (1, 9) pins;
    • to the LED circuits described above (3–6, 10–13);
    • to the MOSI, SCk (17, 19; or D 11, D 13 on Arduino Uno) ATmega8 pins – with B, Clk (2, 8);
    • Please note that the ATmega8 SS (16) pin in this case remains unconnected.
  • The LED circuits, in turn, are connected to either ground or Vcc, according to the chosen polarity (anode-to-register or cathode-to-register) of the LEDs.

In some cases, it may be convenient to use a resistor pack (preferrably with a common lead) of the resistance given above. Similarly, instead of the individual LEDs, it’s possible to use a LED row, or a single-digit seven-segment LED display.

Counting time in binary[edit | edit source]

/*** ledspi.c – Control a row of LEDs via SPI (74HC595, 74HC164)  -*- C -*- */
#include <avr/interrupt.h>      /* for sei (), ISR () */
#include <avr/io.h>             /* for DDRB, PORTB, etc. */
#include <avr/sleep.h>          /* for sleep_enable (), etc. */
#include <avr/wdt.h>            /* for wdt_enable (), etc. */

#if (defined (TIMSK) && ! defined (TIMSK1))
/* ATmega8 compatibility */
#define TIMSK1  TIMSK
#endif

#ifndef USE_PB2_SS
/* use PB2 as SPI Slave Select (/SS) */
#define USE_PB2_SS      1
#endif

/* NB: main loop gets its first event for free */
static volatile uint8_t seen_interrupt_p  = 1;
ISR (TIMER1_OVF_vect) {
  seen_interrupt_p   = 1;
}

#if USE_PB2_SS
ISR (SPI_STC_vect) {
  /* do nothing */
}
#endif

int
main ()
{
  /* expecting a reset about every 0.6 s */
  wdt_enable (WDTO_1S);

  /* Set up Port B:
     - PB2 (D 10) is /SS
     – PB3 (D 11) is MOSI
     – PB5 (D 13) is SCk */
#if USE_PB2_SS
  /* raise /SS */
  PORTB |=    (1 << PB2);
#endif
  DDRB    |= ((1    << DDB5)
              | (1  << DDB3)
              | ((USE_PB2_SS ? 1 : 0)
                 <<    DDB2));

  /* set up Timer/Counter 1 */
  TCCR1A   =  0;
  TCCR1B   = (0
              /* CS1    =  011 _2: Divide clock by 64 */
              | (1  << CS11)
              | (1  << CS10));
  /* Timer overflow frequency is thus F_CPU / 64 / 65536,
     or 1.76 Hz to 4.77 Hz for F_CPU of 7.3728 MHz to 20 MHz */

  /* Set up the SPI master */
  SPCR     = (((USE_PB2_SS ? 1 : 0)
               <<      SPIE)
              | (1  << SPE)
              | (1  << MSTR)
              | (1  << SPR1)
              | (1  << SPR0));

  /* NB: ignoring the datasheet recommendation! */
  sleep_enable ();

  /* enable interrupts */
  TIMSK1  |= (1     << TOIE1);  /* timer 1 overflow */
  sei ();

  /* main loop */
  uint8_t i;
  for (i = 0; ; ) {
    if (seen_interrupt_p) {
#if USE_PB2_SS
      /* lower /SS */
      PORTB &= (~ (1 << PB2));
#endif
      SPDR  = i;
      i++;
      seen_interrupt_p  = 0;
    } else {
#if USE_PB2_SS
      /* raise /SS back */
      PORTB |=    (1 << PB2);
#endif
    }

    /* reset the watchdog timer */
    wdt_reset ();

    /* sleep until the next event */
    sleep_cpu ();
  }

  /* not reached */
  /* . */
  return 0;
}
/*** ledspi.c ends here */

Reading the code[edit | edit source]

We will read the code above starting with the payload parts of its main loop and referring to the initialization part and the preamble as necessary. We will not, however, consider the fragments already covered in the simplistic program and PWM example tasks.

  1. The SPDR = i; line copies the value to be sent to the device from the i variable to the Serial Peripheral Interface buffer (data) register. This also initiates the SPI data transmission.

    Immediately after that, i++; increments the value of i by one. Since this variable is declared as uint8_t, the modulo 256 (2⁸) arithmetic is used, and the value “wraps around,” becoming 0 after 255.

  2. The PORTB &= (~ (1 << PB2)); line before SPDR is loaded sets the low level on the PB2 pin (SS; or D 11 on Arduino Uno.)

    The PORTB |= (1 << PB2); code is evaluated both before the main loop (as part of the initialization part of the code) and upon the completion of every SPI data transmission. It sets the PB2 pin to high.

    When the 74HCT595 chip is used, this signal serves as a strobe – on its raising edge, the data from this chip’s shift register is copied to its storage register (or latch), whose outputs drive (assuming that the OE input is low) the Q₀, …, Q₇ pins.

  3. The DDRB |= ((1 << DDB5) | (1 << DDB3) | ((USE_PB2_SS ? 1 : 0) << DDB2)); fragment configures the PB5, PB3, PB2 ATmega8 pins as outputs. The first two of them are used by the internal SPI port for transmitting the SCk and MOSI interface signals, respectively.

    The PB2 pin, however, deserves a special attention: if left configured as an input, and driven high during the device’s operation, the interface will stop its current transmission, raise an interrupt (if enabled), and will switch to the slave mode, – until after the MSTR flag in the SPCR register is explicitly set again.

    In order to avoid that, and also since 74HC595 requires an additional signal anyway, we configure this pin in our example as an output. This way, the MCU does not interpret its state in any special way.

  4. The flags and bit fields of the SPCR register are set as follows:

    SPIE
    1 – the events related to the SPI operation will result in interrupts;
    SPE
    1 – the interface itself is enabled;
    MSTR
    1 — the interface will operate in the master mode;
    SPR
    11₂ – the interface will be clocked using the MCU clock divided by 256.
  5. The SPI_STC_vect interrupt handler does absolutely nothing. The only purpose of the interrupt itself is to end the wait for an event, as initiated with the sleep_cpu (); line.

  6. The CS1 bit field of the TCCR1B register is set to 011₂ – the counter will be fed with the MCU clock divided by 64.[4]

    Given this counter width (16 bit), the expected overflow condition (and thus the overflow interrupt) rate is 1.76–4.77 Hz for the MCU clock frequences of 7.3728–20 MHz.

    All the other bit fields of this register, as well as the TCCR1A register as a whole, are set to zero, which, in particular, disables the pulse-width modulation function of this timer/counter unit.

Building[edit | edit source]

  1. Add the following dependency specifications to the Makefile created earlier:

    default: ledspi.hex ledspi
    
    ledspi: ledspi.c
    
  2. Create the ledspi.c file as shown above.

  3. Build the example with make:

    $ make ledspi.hex 
    avr-gcc -O2 -Wall -std=gnu11 -mmcu=atmega8 -DF_CPU=7372800   ledspi.c   -o ledspi
    avr-objcopy -O ihex ledspi ledspi.hex
    $ 
    
    NB

    When using an MCU other than ATmega8, or the clock frequency other than 7.3728 MHz, the MCU and F_CPU build parameters are to be set accordingly.

    See the command line given in the simplistic program task for an example on how to build this code for an Arduino Uno board.

  4. Check for no errors in the output, and that the ledspi.hex file is created, containing the image to upload in the Intel hex format.

Uploading and observing operation[edit | edit source]

  1. Upload the image we got to the MCU flash memory, as described in the simplistic program task.

  2. Check the device and code operation as follows:

    1. connect the power;
    2. reset the MCU;
    3. observe the counting in binary from 0 to 11111111₂ (or in the reverse direction, should the LED circuits be connected cathode-to-register);
    4. estimate the time the binary number displayed gets increased by 2ⁿ (which is the half-period of the signal at the “nth” shift register output) and compare it to the one computed from the MCU clock frequency per t = 2²⁴⁺ⁿ ∕ ƒMCU.

Research[edit | edit source]

  1. In this example, the update (increment or decrement) frequency for the number displayed is given by dividing the MCU clock by 64 ((1 << CS11) | (1 < CS10)).[4] Explore how the system works using the following other divisors:
    • 8 – (1 << CS11);
    • 256 – (1 << CS12);
    • 1024 – (1 << CS12) | (1 << CS10).

    Should the device cease to operate as expected when using one of these, – try to explain what causes the issue. (Tip: calculate the expected time span between the successive main loop iterations.)

  2. Try to change the code so that the time is counted in seconds, half-seconds or minutes:

    • for one specific MCU clock frequency;
    • for any MCU clock frequency in the 7.3728–20 MHz range, as passed to the code via the F_CPU macro.
  3. Try to experimentally determine the maximum SPI clock frequency for which the data reliably reaches the shift register. Propose one or more limiting factors for this frequency. If possible, verify your hypothesis by:
    1. assembling a variant of the device which is free (to a greater extent) of one or more of these factors;
    2. verifying that the reliable MCU-to-register data transmission rate indeed gets increased.
  4. Change the code so that the system counts in the Gray code.

References[edit | edit source]

  1. "Serial Peripheral Interface – SPI" (PDF). ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Retrieved 2013-04-29.
  2. "74HC595; 74HCT595 – 8-bit serial-in, serial or parallel-out shift register with output latches; 3-state" (PDF). 2011-12-12. Retrieved 2012-11-22.
  3. "8-Bit Parallel-Out Serial Shift Registers" (PDF). Retrieved 2013-11-09.
  4. 4.0 4.1 "Timer/Counter0 and Timer/Counter1 Prescalers" (PDF). ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Retrieved 2013-04-29.