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:
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.
|
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:
Aim: Circuitry: 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) Remarks:
|
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 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: 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.
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) Remarks: |
Experiment 3: Using the MCP3021 |
Module supplier: KAmodRPI (KAMAMI), Didel RaspEasy (www.didel.com)
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: 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) Remarks: 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) 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)
|
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: 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) Remarks: To request a 10-bit value from the device, a control (or command) byte is sent that is composed as follows:
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. |