Reading the 10bit-ADC with I2C from a Pi!

Note: All the files here

Contents

I2C protocol

For this area, I followed the following tutorials on SparkFun and NXP-Philips. I detail some of my learnings from the SparkFun tutorial below (what I grasped).

Very simply, the Inter-integrated Circuit (I2C) Protocol is a protocol intended to allow multiple slave digital integrated circuits (chips) to communicate with one or more master chips. Like the Serial Peripheral Interface (SPI), it is only intended for short distance communications within a single device. Compared to UART (asynchronous RX/TX) where devices need to talk with agreed data clock speed, SPI (the one of MOSI/MISO/SCK.. ), where the number of wires rises with the number of devices, I2C is more convenient since it allows for several masters to be in the same system and for only the masters to be able to drive the data line high. This means that no slave will be able to lock the line in case other is talking:

Image Source: NXP semiconductors

So, I2C bus consists of two signals: SCL and SDA. SCL is the clock signal, and SDA is the data signal. The clock signal is always generated by the current bus master and both lines are pulled up, and therefore the lines are driven high when not used by any slave. Normal values for the pull up resistors could be around 5kΩ. The actual protocol works with messages broken up into two types of frame: an address frame, where the master indicates the slave to which the message is being sent, and one or more data frames, which are 8-bit data messages passed from master to slave or vice versa. Data is placed on the SDA line after SCL goes low, and is sampled after the SCL line goes high.

Image Source: Sparkfun

For the I2C protocol on a Attiny device, we need to look into some short of solucion as in here (more on this below).

Notes on Clock-Stretching

Clock stretching is a technique used when the slave is not able to provide the data, either because it’s not ready or because it’s busy with other things. In these situation, it is possible to do clock stretching. Put simply: normally, the clock is driven by the master devices and slaves simply put data on the bus, or take data off the bus in response to the master’s clock pulses. At any point in the data transfer process, an addressed slave can hold the SCL line low after the master releases it. The master is required to refrain from additional clock pulses or data transfer until such time as the slave releases the SCL line.

However, there are some implementations in the Raspberry Pi that don’t allow clock-stretching with the slaves. Sometimes if the AttinyX4 is run at 8MHz, it can provoke this problem, but it’s not guaranteed that one can get away with higher clock speeds. Nevertheless and for this reason, an external resonator with 20MHz will be used.

Board Tests

In this section I will detail the process I followed to obtain readings from the board using a Raspberry Pi. The board was already tested in the Input Devices week and here I will focus on reading it via I2C.

Communication via I2C

The communication between the AttinyX4 and the Raspberry Pi will be done over I2C. The Raspberry Pi I will be using is a model 3, and the pinout can be found in this link.


I will be connecting the 5V power supply to the I2C grove connector, and the SDA, SCL and GND lines to the grove connector ones. This connection right now is done via jumper cable, but it will be substituted by a Raspberry Pi Hat as part of my final project.

For reference, these connectors are in the board:


Setting up the Raspberry Pi

First thing, I will detail my workflow to set up the Raspberry Pi with a MAC. First thing is to download and mount a Raspbian version with Etcher onto the Pi. This can be done easily under the instructions of the official rasperry pi documentation.

Next, would be to connect to the Pi via ssh or VNC (thanks again Victor for the guidance). These are network communication ways to interact with the Pi without the use of a keyboard and a screen attached to the Pi.

  • SSH: stands for Secure Shell and it’s a secure way to connect to the Pi’s command line (and really to any known IP address on our same network). It can also redirect programs through the screen via output redirection.
  • VNC: stands for Virtual Network Computing and with it we will have access to the Pi’s screen and use the origin’s keyboard and mouse. This will be the procedure I will be using, with VNC Viewer for MAC.

Now, for both these procedures, we need the Pi to be connected to the our same network in order to use it’s IP. In order to discover the Pi’s address, we can use a command like this in MAC:

MY_RANGE=$(ip addr | grep "UP" -A3 | grep '192' -A0 | awk '{print $2}') && nmap -sn $MY_RANGE && arp -na | grep b8:27:eb

Where we are using grep and awk in order to retrieve the host network (normally something like 192.168.0.1/24). Then, this is used by a network scanner such as nmap to find a device with a MAC address that contains the b8:27:eb part of the Pi.

This command would give us something like 192.168.0.133 and it can be used for us to connect to the Pi.

Next, we need to turn on the Pi’s I2C with:

sudo raspi-config

And then going to Interface > I2C > Enable I2C. Next, we need to install a couple of libraries (depending on the Pi’s version) for the Pi to detect and interact with the I2C interface. Normally, they are present in the newest versions, but in case not, we can activate it through this command.

Finally, as a last check, we need to install i2c-tools in the Pi:

sudo apt-get install -y i2c-tools

And with it we can perform a first check on the Pi’s network, if anything connected (for this test I connected a SHT31 temperature sensor, which address is normally 0x44):

pi@raspberrypi:~/$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- 44 -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

Now we are all set up with the Pi’s network and we can move on to the Attiny84.

Attiny I2C library

Now, the Attiny has no TWI (two wire interface) in hardware for the I2C communication, however, they come with a USI (Universal Serial Interface) that can be used to implement via software a TWI interface compatible with I2C (page 121 of the datasheet) and also explained in a very complex way in this Atmel ANN.

For the Attiny Slave side, I will be using this implementation for the USI as a TWI in the Attiny84. There is a good example in this repository that reads a photoresistor with an Attiny and sends it over via I2C, onto which I will be basing my code.

A picture of the setup is shown below:


Through the FabISP we will be programming the board as specified above, and reading the sensor using a Python code in the Raspberry Pi. The values we will be reading are 10-bits (the resolution of the ADC in the tiny) and therefore we should be splitting them in at least 4 bytes: 2 bytes is the case used by the example, but if we want to send full resolution, we need to go from 16 bits to 32 (not possible to go to 24 since it’s not power of 2). With this operation we can split the values into 4 bytes and store each of the in the variable i2c_regs:

  i2c_regs[0] = pressureSmooth >> 24 & 0xFF;
  i2c_regs[1] = pressureSmooth >> 16 & 0xFF;
  i2c_regs[2] = pressureSmooth >> 8 & 0xFF;
  i2c_regs[3] = pressureSmooth & 0xFF;

We also need to specify the address in the code and set it up (I chose the address 13). Also, I include below the definition of the i2c registers and the library initialisation as a SLAVE:

/*
 * Set I2C Slave address
 */
#define I2C_SLAVE_ADDRESS 0x13

#ifndef TWI_RX_BUFFER_SIZE
#define TWI_RX_BUFFER_SIZE ( 16 )
#endif

// I2C Stuff
volatile uint8_t i2c_regs[] =
{
    0, //older 8
    0,
    0,
    0 //younger 8
};

void setup() {

  /*
   * Setup I2C
   */
  TinyWireS.begin(I2C_SLAVE_ADDRESS);
  TinyWireS.onRequest(requestEvent);

}

Finally, the library comes with an interrupt callback under I2C request to send the data over I2C. This function will be triggered everytime the master requests a value and will send the value needed:

void requestEvent()
{  

  TinyWireS.send(i2c_regs[reg_position]);

  reg_position++;
  if (reg_position >= reg_size)
  {
      reg_position = 0;
  }

}

We need to time this properly between both of them, so that the Pi receives the data in order. For that, I will be taking averages of the measurements in the attiny with a smoothing function, using a timer to take these measurements:

int smooth(int data, float filterVal, long smoothedVal){

  if (filterVal > 1){      // check to make sure params are within range
    filterVal = .99;
  }
  else if (filterVal <= 0){
    filterVal = 0;
  }

  smoothedVal = (data * (1 - filterVal)) + (smoothedVal  *  filterVal);

  return (int)smoothedVal;
}

void loop() {

  // What time is it?
  unsigned long currentMillis = millis();

  // Check if we have passed the minimum time between measurements:
  if (abs(currentMillis - lastReadout) > MAX_TICK) {

    int sensorReading = analogRead(SENSOR);
    /* 
    **
    ** Convert the values
    **
    */

    // Smooth them
    pressureSmooth = smooth(pressure, LPF_FACTOR, pressureSmooth); // in Pa

    // Send it over I2C
    i2c_regs[0] = pressureSmooth >> 24 & 0xFF;
    i2c_regs[1] = pressureSmooth >> 16 & 0xFF;
    i2c_regs[2] = pressureSmooth >> 8 & 0xFF;
    i2c_regs[3] = pressureSmooth & 0xFF;

    // Update the time
    lastReadout = currentMillis;
  }

WiringPi and SM.bus libraries

I tested two libraries for the I2C communication: WiringPi and SM.bus. Both of them connect properly to the I2C and read the data, but I finally used SM.bus for the final example (I found it more robust, but very likely I am not doing it that well with the WiringPi). Nevertheless, I detail below both workflows for reference:

WiringPi

The code for it is below, assuming 4 packs of data MSB (most significant first):

// Compile this using g++ SHT31.cpp -lwiring -o SHT31
// And then run it with ./SHT31

#include <wiringPiI2C.h>
#include <iostream>
using namespace std;

int fd, reading;
int transmission;
int i = 0;
int packet_size = 2;

int main(){

  fd = wiringPiI2CSetup(0x13);

    while (1) {
      
      transmission = wiringPiI2CRead (fd);
      
      //Print out the result
      cout << "Transmission" << endl;
      cout << transmission << endl;
      
      reading += (transmission  << 8*(i+1));
      
      i++;
      
      if (i == packet_size) {
        cout << "Reading" << endl;
        std::cout << reading << std::endl;
        reading = 0;
        i = 0;
      }
      
      //~ //Print out the result
      //~ cout << "Transmission" << endl;
      //~ cout << transmission << endl;
      //~ cout << "Reading" << endl;
      //~ std::cout << reading << std::endl;
  }
}

In order to compile and execute the program, we need to use g++ (gnu C compiler) and link it with WiringPi (important!) in the terminal

g++ TestWiringPi.cpp -lwiringPi -o TestWiringPi

Next, when we run it in the terminal:

./TestWiringPi
SM.Bus

The code is below, with the same 4 packs assumption MSB:

import smbus
import time

bus = smbus.SMBus(1) # Indicates /dev/i2c-1
address = 0x13
packet_size = 4

def ReadSensor(_address):
  
  i = 0
  _value = 0
  
  while (i < packet_size):

    _measure = bus.read_i2c_block_data(_address, 0, 1)
    #~ print "Measure"
    #~ print _measure
    
    _value |= _measure[0] << (8*(packet_size-(1+i)))
    i+=1
    
  return _value

while True:
    result = ReadSensor(address)
    #~ print "Result"
    print result
    time.sleep(1)

And then run it (no need to compile it since python is an interpreted language):

python TestSMBus.py

With SM.Bus, the results in DPa (I know, weird units) are:


Which are very representative of a normal sea level atmospheric pressure (~100kPa)!

As a final note, below, we find how the result is built:

Where 40 corresponds to 40 « 8 and 10240 (to be summed to the last value).

Final Project task

Note: All the designs below are available here

Here, I will detail the process followed to mill and design a Raspberry Pi Hat with 4 I2C Grove connectors that will connect to the different elements on my Final Project.

Design in KiCad

The schematic is pretty simple. I will be using the already created I2C connectors from previous assignments, as well as the generic 2x20 header:


The PCB layout looks like:


Then, the different milling strategies are exported to png. For the traces:


For the inner cuts (remember these ones first!):


For the outter cut:


Final Result

These are cut in the Modela MDX-20, with the following result after soldering: