Fab Academy 2019

Progress Documentation - Christian Schmidt

Networking & Communications

Making things talk to each other

Having dealt with input and output devices in the last few weeks, we are already able to build a plethora of things that can react to their environment and act on it accordingly. This week, however, we had to make things that interact with each other in a meaningful way. I learned that working with communication busses and -protocols can be hard, especially when implementing them from scratch. But in the end, networking greatly enhances the range of possibilities for electronic projects.

For this assignment (which took me way longer than just the week that's allotted for it), I implemented an I2C library for ATTiny microprocessors in C++. There's code for a synchronous master and an asynchronous slave, with user-definable callbacks. Hardware-wise, I used my board design from the output devices week. I had to fix the I2C bus on the board, however, because the previous design was missing pull-up resistors on the I2C lines.

Communication via I2C

I built two boards that can be connected to an I2C bus; one acts as a master device, the other as a slave. Both feature an RGB LED and a single button. Pressing the button on the slave device changes the color of its LED in a cycle. This color is repeatedly read by the master device, which adapts the color of the slave devices' LED for its own LED. Pressing the button on the master device changes the way that the LED on the slave device is illuminated: always on, fading on and off, or blinking. This configuration demonstrates reading and writing on both master and slave side.

Things needed for this project:

  • Two boards as presented in the output devices week
  • A bunch of cables to connect the two
  • The source code from the download section
  • An Arduino (or similar device with a known working I2C implementation) for debugging

Protocol

This is a very brief overview. For a more thorough explanation check out this tutorial (but beware, I found the code to be buggy!) or the specification.

I2C is a synchronous serial two-wire communication protocol, meaning

  • it uses two wires
  • it's synchronous, so there is always at most one device talking on the line while the others wait/listen
  • it is serial, so each byte is transferred bit-by-bit

There are master and slave devices, and each slave devices has a 7 bit address, thus allowing up to 127 slave devices on one bus (with extensions even more), but practically, this number is often limited by parasitic capacitance and various other effects. Multiple master configurations are allowed, but not that often used. Only master devices may initiate a conversation via the start signal, and they indicate the end of transmission via a stop signal. Start signals may be repeated to send multiple messages in one go. Standard I2C allows transmission speeds of up to 100kHz, in fast mode up to 400kHz. Note the up to - every slave device is allowed to stretch the clock signal in order to finish processing, thereby slowing down transmission speed. The two wires are called SDA (serial data) and SCK (serial clock). Both are connected by a pull-up resistor to VCC (commonly used values are 4.7kΩ), and only driven low to signify a logical 0 (never drive a line high when implementing I2C!).

Hardware

I needed two boards for this assignment, so I tried to reuse one of my older boards. I quickly noticed that I was missing the pull-ups on the original board, so I had to modify it slightly. In the same step, I got rid of the transistors that I used on the former version. I milled the board on our labs pcb mill and soldered the components by hand.


Implementation

Programming was done with avrdude and my FabISP, as outlined in the embedded programming week. My implementation utilizes the USI module that can be found in many ATTinies, and is inspired by the driver that Atmel provides in their application notes. However, it is written in C++11. The slave device manages a couple of "registers" that can be read & written by the master device. This configuration is very commonly used with I2C devices, so I wanted to create an implementation that's easy to use and reuse while being small and efficient. Have a look at the code in the download section. In this case, the slave device provides five registers: three to define the LED color, and two to determine the way it flashes the LED. The first three registers are repeatedly read by the master device, the other two are written when the button on the master device is pressed.

Using my I2C abstraction, I wrote code for a master board and a slave board. For the code of the abstraction layer, download the zip file in the download section. The abstraction layer is a reimplementation of the i2c-by-usi driver provided by Atmel, but in C++11 with templates, so it is more size efficient and reusable. Additionally, it defines a class that allows to easily read/write registers on a slave device. First, the code for the master board:

#include "i2c.h"
#include "register_device.h"
#include <avr/interrupt.h>

// Define a shorter name for the I2C master class
using I2C_Master = i2c::Synchronous_USI_Master<>;
I2C_Master master;
// Define an i2c slave device with its address
SlaveRegisterDevice<I2C_Master> slave_device(0x54);
uint8_t registers[3];

// Define the peripherals used in this program
using namespace peripherals;
using PIN_RED   = Pin<PORTA_t, DDRA_t, PINA_t, PA5>;
using PIN_GREEN = Pin<PORTA_t, DDRA_t, PINA_t, PA7>;
using PIN_BLUE  = Pin<PORTB_t, DDRB_t, PINB_t, PB2>;
using LED = RGBLED<PIN_RED, PIN_GREEN, PIN_BLUE>;

// Switch read/write mode by button press
bool triggered = false;
bool write_mode = false;
ISR(PCINT0_vect) {
  if(triggered){
    triggered = false;
  } else {
    triggered = true;
    write_mode = true;
  }
}

int main(void) {
  // Setup Clock
  CLKPR = (1 << CLKPCE);
  CLKPR = 0;

  // Stop timer during configuration
  GTCCR |= (1 << TSM);

  // Fast PWM, Clear OCOA/OCOB on BOTTOM, set at match
  TCCR0A |= (1 << COM0A1) | (0 << COM0A0) | (1 << COM0B1) | (0 << COM0B0) | (1 << WGM01) | (1 << WGM00);
  // Fast PWM, clock source is I/O clock/256
  TCCR0B |= (0 << WGM02) | (0 << CS02) | (1 << CS01) | (0 << CS00);
  
  // Always on
  OCR0A = 0xFF;
  OCR0B = 0xFF;

  // Same configuration as timer0
  TCCR1A |= (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0) | (0 << WGM11) | (1 << WGM10);
  TCCR1B |= (0 << WGM13) | (1 << WGM11) | (0 << CS12) | (1 << CS11) | (0 << CS10);

  OCR1AL = 0xFF;
  OCR1BL = 0xFF;

  // start timer
  GTCCR &= ~(1 << TSM);

  registers[0] = 255;
  registers[1] = 0;
  registers[2] = 0;

  PORTA |= (1 << PA0);
  DDRA &= ~(1 << PA0);

  // Enable interrupts for port A
  GIMSK |= (1 << PCIE0);
  // Enable pin change interrupts for pin A7
  PCMSK0 |= (1 << PCINT0);

  master.init();
  LED::init();

  sei();

  uint8_t slave_mode[2] = { 0, 255 };
  for(;;) {
    if(write_mode) {
      // If we're in write mode, we write the led color to the slave
      slave_device.write_registers(3, slave_mode, 2);
      slave_mode[0]++;
      // Controls the way the slave device blinks
      if(slave_mode[0] > 3){
        slave_mode[0] = 0;
        slave_mode[1] = 255 - slave_mode[1];
      }
      write_mode = false;
    } else {
      // Read color from slave device
      if(slave_device.read_registers(0, registers, 3)) write_mode = true;
    }

    // Set own Led to color read from slave
    LED::set_color(255-registers[0], 255-registers[1], 255-registers[2]);
    _delay_ms(500);
  }
  return 0;
}

An here's the code for the slave board:

#include "i2c.h"
#include "register_device.h"
#include <avr/interrupt.h>

// Define Registers that can be read via i2c, including callbacks
constexpr uint8_t nRegister = 5;
RegisterDevice<nRegister> registers;
using Callb = RegisterDevice<nRegister>::Callback<registers>;
i2c::Asynchronous_USI_Slave<Callb> slave(0x54);

// Define peripherals
using namespace peripherals;
using PIN_RED   = Pin<PORTA_t, DDRA_t, PINA_t, PA5>;
using PIN_GREEN = Pin<PORTA_t, DDRA_t, PINA_t, PA7>;
using PIN_BLUE  = Pin<PORTB_t, DDRB_t, PINB_t, PB2>;
using LED = RGBLED<PIN_RED, PIN_GREEN, PIN_BLUE>;

constexpr uint8_t n_colors = 0b111;
uint8_t color = 1;

// process i2c start signal
ISR(USI_START_vect) {
  slave.on_start();
}

// process single byte i2c transfer
ISR(USI_OVF_vect) {
  slave.on_transfer_complete();
}

// Change color on button press
bool triggered = false;
ISR(PCINT0_vect) {
  if(triggered) {
    triggered = false;
  } else {
    triggered = true;
    color++;
    if(color > n_colors) color = 1;
  }
}

int main(void) {
  // Setup Clock
  CLKPR = (1 << CLKPCE);
  CLKPR = 0;

  // Stop timer during configuration
  GTCCR |= (1 << TSM);

  // Fast PWM, Clear OCOA/OCOB on BOTTOM, set at match
  TCCR0A |= (1 << COM0A1) | (0 << COM0A0) | (1 << COM0B1) | (0 << COM0B0) | (1 << WGM01) | (1 << WGM00);
  // Fast PWM, clock source is I/O clock/256
  TCCR0B |= (0 << WGM02) | (0 << CS02) | (1 << CS01) | (0 << CS00);
  
  // Always on
  OCR0A = 0xFF;
  OCR0B = 0xFF;

  // Same configuration as timer0
  TCCR1A |= (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0) | (0 << WGM11) | (1 << WGM10);
  TCCR1B |= (0 << WGM13) | (1 << WGM11) | (0 << CS12) | (1 << CS11) | (0 << CS10);

  OCR1AL = 0xFF;
  OCR1BL = 0xFF;

  // start timer
  GTCCR &= ~(1 << TSM);

  registers[0] = 255;
  registers[1] = 0;
  registers[2] = 0;
  registers[3] = 3;
  registers[4] = 255;

  PORTA |= (1 << PA0);
  DDRA &= ~(1 << PA0);

  // Enable interrupts for port A
  GIMSK |= (1 << PCIE0);
  // Enable pin change interrupts for pin A7
  PCMSK0 |= (1 << PCINT0);

  slave.init();
  LED::init();

  sei();

  // Blink with LED in color defined by master
  uint8_t count = 1;
  uint8_t power = 255;
  for(;;) {
    count++;
    cli();
    switch(registers[3]) {
      case 0:
        power = count;
        break;
      case 1:
        if(count < 128) {
          power = 2 * count + 1;
        } else {
          power = 2 * (255 - count) + 1;
        }
        break;
      case 2:
        if(count < 128) {
          power = 255;
        } else {
          power = 0;
        }
        break;
      default:
        power = 255;
        break;
    }
    sei();
    registers[0] = power * (color & 0x01);
    registers[1] = power * ((color & 0x02) >> 1);
    registers[2] = power * ((color & 0x04) >> 2);
    LED::set_color(registers[0], registers[1], registers[2]);
    if(registers[4]) {
      _delay_ms(3000/255);
    } else {
      _delay_ms(1000/255);
    }
  }
  return 0;
}

Downloads