ADC, SPI PROTOCOL

The source code of all examples can be downloaded from here.

 

Sensor calibration

 

The main purpose of most sensor is to convert a physical quantity to a voltage. If the output voltage is in a linear dependency to the physical quantity, the sensor is called a linear sensor. Sometime the linear relationship only holds for a certain range of the input, sometime the relationship is completely non-linear.

Non-linearity is not really a problem, because it may be compensated by the following data processing algorithm. More annoying is an unstable or drifting relationship due to influences from the environment (temperature, humidity, etc.)

To define the input-output relationship, often some kind of calibration process is needed where the sensor is exposed to known input values. In this process, one or several sensor parameters are determined that are used later by the application.

 

 

Analog-to-Digital converters

 

Since the output of most sensors is a voltage changing smoothly with the input quantity, the voltage must be converted to digital data that can be fed into a processing unit. An analog-to-digital converter (ADC) has the following main features:

  • Input voltage range, typically 0..10V (single ended: voltage between input and GND) or +-5V (differential: voltage between two inputs)
  • Resolution, typically 8, 10, 12 or 16 bits
  • Conversion speed, e.g. maximal number of conversions per second, typically 1000 /s up to 106 /s (and more)
  • Type of interface, typically parallel (3.3V, 5V), I2C, SPI
  • Number of input channels, typically 1, 4, 8

Some of the characteristics may be configurable by the wiring, other by software (values of control registers).

Because the requirement for the ADC may differ widely from one application to another, the designers of the Raspberry Pi decided not to include an ADC on the main board. This is somewhat annoying in an educational environment. To overcome this difficulty, external ADCs has to be connected to the GPIO bus . In view of the increasing difficulty to buy ICs in DIL packages, more and more small modules (called breakouts) are used. In this chapter we show both approaches with the focus on how to program ADCs with I2C and SPI interfaces.

For demonstrations and tests the input voltage is not taken from a sensor, but from a variable voltage source. A simple potentiometer of about 10 kOhm with one end at VCC (5V or 3.3V) and the other end to GND is perfect (even with less danger than an external power supply, because of the voltage limitation).

Caution: All GPIO input voltages must be in the range 0..3.3V. If you power an external I2C board with 5 V, make sure that the SDA and SCL lines are not pulled-up by resistors to the 5 V rail! Either remove the pull-up resistors or power the board with 3.3 V.

Most I2C chips (not mounted into a board) have open-collector SDA/SCL outputs. These devices can be used with no danger.

Another safe solution is to use a bidirectional voltage-level converter 3.3V-5V (such as the BSS138 or AN97055). There are specially designed small boards for I2C conversion.  

 

 

Experiment 1: Using the PCF8591

 

The PCF8591 is a single-chip, single-supply low-power 8-bit CMOS data acquisition device with four analog inputs, one analog output and a serial I2C-bus interface. Three address pins A0, A1 and A2 are used for programming the hardware address, allowing the use of up to eight devices connected to the I2C-bus without additional hardware. The operating voltage is 2.5 to 6 V, so it works with the Raspberry Pi 3.3 V or 5 V supply. The input may be configured by software as 4 channels single input or 3 channels differential inputs. For more information consult the data sheet.

The 7-bit I2C address is composed as follows:

A6
A5
A4
A3
A2
A1
A0
1
0
0
1
x
x
x
where A2, A1, A0 are selected by 3 input pins. So if the 3 pins are shorted to GND, the address is b1001000 = 0x48

Aim:
Digitize a voltage in the range 0..5V and display the result in the console and (if available) on an attached display.

Circuitry:
The input voltage range is 0..5 V, providing data values in the range 0...255. No harm to the Raspberry Pi, because SDA, SCL are connected without pull-up resistors.

Remember that multiple devices can be connected to SDA, SCL provided that their addresses are different. To check, if the device is present, call i2cdetect -y 1 (or i2cdetect -y 0 for Raspberry Pi version A). You should see the device at address 48 (hex)..

Program:[►]

# ADC1a.py
# Read values from analog input 0

import smbus
import time
from py7seg import Py7Seg # xxx

bus = smbus.SMBus(1)  # RPi revision 2 (0 for revision 1)
i2c_address = 0x48
control_byte = 0x00 # to read channel 0
ps = Py7Seg() # xxx
t = 0
while True:
    data = bus.read_byte_data(i2c_address, control_byte)
    print t, "s:", data
    ps.showText("%4d" %data) # xxx
    t += 0.1
    time.sleep(0.1)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
If you do not have a ELV display or you use another display, comment out or modify the lines marked with xxx. For more information about the control byte, consult the data sheet.

The PCF8591 is not fabricated any more. But you find many suppliers on the Internet (Ebay, aliexpress, and others). If you use a board version (e.g. CP12001) powered with 5 V, make sure that you remove the pull-up resistors on the SDA/SCL lines.  

 

 

Experiment 2: Using the ADS1015/ADS1115

 

The ADS1015 is a 12-bit ADC with 4 channels, whereas the ADS1115 is its 16-bit counterpart (consult data sheets ADS1015 or ADS1115). Both have the same pin layout and are only available with a SMT package. To avoid soldering SMT, module boards can be purchased. They are available from different sources, especially from vendors of Arduino components. You also find cheap offers of the ADS1115 module fabricated in China on Ebay (if you are patient enough to wait for the delivery).

The ADS1x15 series has an I2C-bus interface with 4 user-selectable addresses depending on how the ADDR pin is connected (see table). As you see, if ADDR is wired to GND, the address is 0x48.  

The ADS1x15 may be configured for 4 single-ended inputs or 2 differential inputs. In single-ended mode the maximum voltage range is 0 to VCC (never apply a negative voltage or a voltage higher than VCC). In differential mode, the maximum differential voltage range is -VCC to VCC. The actual range depends on the gain set in the programmable gain amplifier (PGA).

For the ADS1015 the ouput data range is 0..2047 (single ended) and -2048..2047 (differential). For the ADS1115 the output data range is 0..32767 (single ended) and -32768..32767 (differential).

Aim:
Digitize a voltage in the range 0..3.3V or 0..5V and display the result in the console and (if available) on an attached display.

Circuitry:

If you don't use pull-up resistors to the VDD rail, you can power the IC with 3.3V or 5V, but be aware that the pull-up resistors are normally part of the module.

Caution: If you want to power the board from the 5V supply, you must remove the two pull-up resistors R1 and R2 shown by the red arrows. If you power it with 3.3V (or you use a voltage-level converter), no modification is needed.

If you use the ALERT pin and a 5V supply, the pull-up resistor R4 must also be removed.

 

Board schematic (by most manufacturers, with no guarantee!):

Program:

Compared to other I2C-devices the ADS1x15 chip is a bit more complicated to be configured by software. Therefore we recommend to use a well-designed Python class library ADS1x15 written by Tony DiCola for Adafruit Industries and generously put to public domain. Download the module file ADS1x15.py (and some examples) from here and put it in the same folder where your program resides. Consult the Python documentation and the comments in the source code for more information.

By encapsulating the details of the code to communicate with the device, the program becomes extremely simple.

Program:[►]

# ADC2a.py

from ADS1x15 import ADS1015
from py7seg import Py7Seg # xxx
import time

adc = ADS1015()
channel = 0
gain = 1
ps = Py7Seg() # xxx
t = 0
while True:
    data = adc.read_adc(channel, gain)
    print t, "s:", data
    ps.showText("%4d" %data) # xxx
    t += 0.1
    time.sleep(0.1)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
If you do not have a ELV display or you use another display, comment out or modify the lines marked with xxx.

 

 

Experiment 3: Using the MCP3021

 

Module supplier: KAmodRPI (KAMAMI), Didel RaspEasy (www.didel.com)

The MCP3021 is 10-bit ADC with a I2C interface that is extremely simple to use. Unfortunatly only a SMT version (SOT23) is available. Because it only has 5 pins, you can easily solder it to a SMT-to-DIL adapter and use it in your breadboard.

The I2C device address is not user-configurable. It depends on the device type and is pre-programmed in the range 0b1001000 = 0x48 for the MCP3021A0 to 0b1001111 = 0x4F for the MCP3021A7. (For detailed information consult the MCP3021 data sheet.)

Aim:
Digitize a voltage in the range 0..3.3V and display the result in the console and (if available) on an attached display.

Circuitry:

Program:[►]

# ADC3a.py

import smbus
import time
from py7seg import Py7Seg # xxx

bus = smbus.SMBus(1) # RPi revision 2 (0 for revision 1)
i2c_address = 0x4D  # default address
ps = Py7Seg() # xxx
t = 0
while True:
    # Reads word (2 bytes) as int
    rd = bus.read_word_data(i2c_address, 0)
    # Exchanges high and low bytes
    data = ((rd & 0xFF) << 8) | ((rd & 0xFF00) >> 8)
    # Ignores two least significiant bits
    data = data >> 2
    print t, "s:", data
    ps.showText("%4d" %data) # xxx
    t += 0.1
    time.sleep(0.1)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
If you do not have a ELV display or you use another display, comment out or modify the lines marked with xxx.

You may power the device with the Raspberry Pi 5V supply (pin #2) and extend the input voltage range to 0..5V.

It is strongly recommended that you structure your program, for example by creating a class that encapsulates the code dealing with the ADC.

Program:[►]

# ADC3b.py

import smbus
import time
from py7seg import Py7Seg # xxx
 
class MCP3021:
    VINmax = 3.3
    bus = smbus.SMBus(1)
    
    def __init__(self, address = 0x4D):
        self.address = address
    
    def setVINmax(self, v):
        self.VINmax = v
    
    def readRaw(self):
        # Reads word (16 bits) as int
        rd = self.bus.read_word_data(self.address, 0)
        # Exchanges high and low bytes
        data = ((rd & 0xFF) << 8) | ((rd & 0xFF00) >> 8)
        # Ignores two least significiant bits
        return data >> 2
    
    def getValue(self):
        return float(self.VINmax) * self.readRaw() / 1023.0

adc = MCP3021()
ps = Py7Seg() # xxx
t = 0
while True:
    v = adc.getValue()
    w = "%4.3f" %v
    print t, "s:", w
    ps.showText(w[0] + w[2] + w[3] + w[4], dp = [0, 0, 1]) # xxx
    t += 0.1
    time.sleep(0.1)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

If you run the program in the graphics desktop of the Raspberry Pi (with an attached screen or via VNC), you can use the PyGPanel graphics module to easily draw sensor data into a coordinate grid in real time. You must download gpanel.py from here and copy it in the same directory as your program. (The distribution contains also the full documentation and many example programs.)

You may solder a 10 kOhm potentiometer directly on a SMD adapter containing the MCP3021 ADC. This "digital potentiometer" is a handy tool for many testing purposes.

Program:[►]

# ADC3c.py
# Read ADC and show graphics

import smbus
import time
from gpanel import *

dt = 0.1

def readData():
    adc_address = 0x4D
    rd = bus.read_word_data(adc_address, 0)
    data = ((rd & 0xFF) << 8) | ((rd & 0xFF00) >> 8)
    data = data >> 2
    return data

def init():
    clear()
    setPenColor("gray")
    drawGrid(0, 10, 0, 1.0)
    setPenSize(2)
    setPenColor("blue")
            
bus = smbus.SMBus(1)
makeGPanel(-1, 11, -0.1, 1.1)
t = -1

while True:
    v = readData() / 1023.0 
    if t == -1 or t > 10:
        init()
        t = 0
        pos(0, v)
    else:   
        draw(t, v)
    t += dt
    time.sleep(dt)
Highlight program code (Ctrl+C copy, Ctrl+V paste)


 

My "Digital Potentiometer"

 

ADC15

adc14

 

 

Experiment 4: Using the MCP3008 (SPI)

 

The SPI Serial Peripheral Interface) protocol uses a bus with 5 lines: GND, SCLK (Clock), MOSI (Master Out Slave In), MISO (Master In Slave Out) and CS (Chip Select). Data are transferred bitwise and the timing is extremely simple:

To send data from the master (normally the Raspberry Pi) to a client (here the MCP3008 chip), the master activates CS (by pulling it down), sets the bit (high or low) to MOSI and sends a clock pulse by setting SCLK to high and shortly after to low.

To request data from the client (with CS enabled), the master sends a clock pulse and reads the level on MISO.

Aim:
Digitize a voltage in the range 0..3.3V and display the result in the console and (if available) on an attached display.

Circuitry:

Program:[►]

# ADC4a.py

import RPi.GPIO as GPIO
import time
from py7seg import Py7Seg # xxx

SPI_CLK = 23
SPI_MISO = 21
SPI_MOSI = 19
SPI_CS = 24

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(SPI_MOSI, GPIO.OUT)
    GPIO.setup(SPI_MISO, GPIO.IN)
    GPIO.setup(SPI_CLK, GPIO.OUT)
    GPIO.setup(SPI_CS, GPIO.OUT, initial = GPIO.HIGH)

def readADC(channel):
    LOW = GPIO.LOW
    HIGH = GPIO.HIGH

    if channel > 7 or channel < 0: # illegal channel
        return -1

    GPIO.output(SPI_CLK, LOW) # Start with clock low
    GPIO.output(SPI_CS, LOW)  # Enable chip

    # Send command
    control = channel # control register
    control |= 0b00011000  # Start bit at b4,  
                           # Single-ended bit at b3
                           # Channel number (b2, b1, b0)
    for i in range(5):  # Send bit pattern starting from bit b4
        if control & 0x10:  # Check bit b4
            GPIO.output(SPI_MOSI, HIGH)
        else:
            GPIO.output(SPI_MOSI, LOW)
        control <<= 1 # Shift left
        GPIO.output(SPI_CLK, HIGH) # Clock pulse
        GPIO.output(SPI_CLK, LOW)

    # Get reply
    data = 0
    for i in range(11):  # Read 11 bits and insert at right
        GPIO.output(SPI_CLK, HIGH)  # Clock pulse
        GPIO.output(SPI_CLK, LOW)
        data <<= 1  # Shift left, LSB = 0
        if GPIO.input(SPI_MISO): # If high, LSB = 1,
            data |= 0x1

    GPIO.output(SPI_CS, HIGH) # Disable chip
    return data

setup()
channel = 0
ps = Py7Seg() # xxx
t = 0
while True:
    data = readADC(channel)
    print t, "s:", data
    ps.showText("%4d" %data) # xxx
    t += 0.1
    time.sleep(0.1)
Highlight program code (Ctrl+C copy, Ctrl+V paste)

Remarks:
If you do not have a ELV display or you use another display, comment out or modify the lines marked with xxx.

To request a 10-bit value from the device, a control (or command) byte is sent that is composed as follows:

Higher nibble: binary 0001 (1 is called start bit);
Lower nibble: binary S/D, D2, D1, D0 (as shown in the table below taken from the data sheet).

 

Examples:

  • To read with single ended input at CH0, the control byte is 0x18

  • To read with differential input at CH2/CH3, the control byte is 0x12

A total of 5 bits is sent as command to initiate the conversion. The device sends back 11 bits and the lower 10 bits are the digitized data.

A SPI driver library (called spidev) is available that simplifies the code. But problems are reported when used with some Raspberry Pi OS versions. Our code only uses the GPIO, so it is not necessary to enable the SPI support in the raspi-config setup.