Connecting Analog Sensors to the Raspberry Pi

The Raspberry Pi is a marvel of connectivity. It’s 40 pin header and associated peripheral ports provide a spectacular range of options to interface with the world outside the Raspberry Pi. However, one feature that the Pi doesn’t have built in is the facility to accept an analog input.

Analog and Digital

Signals (or even information in general) can be broken down into two different types; Analog and digital.

Analog

An analog signal is one that has an infinitely variable range of values that can change over time.

Analog
Analog

If we consider the question of how much light is shining outside we could imagine that the level of brightnesses varies between the blackness of a moonless night and a overcast sky to a cloudless day with the sun high in the sky.

These are rough approximations of dark and light, but between the two extremes is a range of brightness levels which are always changing. If we wanted to measure how bright it was at any particular time we could set ourselves a numeric range of 0 representing the middle of the night and 100 representing the middle of the day and the number that represented the brightness at any particular time would be somewhere between those two numbers. Typically in electronics an analog signal is a voltage that will be anywhere on that variable range between two limits. So straight up we can see that our analog sensor is going to be a device that provides an output value that varies between two extremes.

Digital

A digital signal represents information as discrete values.

Digital
Digital

For example at it’s most fundamental the light level outside could be described as dark or light. Represented numerically this could be dark = 0 and bright = 1. While this is perfectly valid, we would often prefer to have a little more granularity in our measurement and so we can increase the number of discrete steps that represent light levels to match our expectations of the type of information we’re interested in. if we add another couple of levels, we could have light that was dark = 0, dim = 0.33, glowing = 0.66 and bright = 1. We can continue to improve the resolution of our numerical perception of the level of light in a process that is called Analog to Digital Conversion or ADC. Essentially creating steps that represent different levels of what would otherwise be a smooth transition.

The Boards

The Analog Sensor

While this project is more about the conversion of analog signals into digital ones, this project will use a Keyes KY-018 sensor based on a Light Dependent Resistor (LDR) to produce a variable resistance in the presence of different light levels. An applied voltage (from the Pi) returns a variable voltage from the LDR. It is this variable voltage that is then digitised with the ADC.

Keyes KY-018 Analog Light Sensor
Keyes KY-018 Analog Light Sensor

In essence there are a range of different sensors that could be used to produce an analog signal. I have successfully also connected the Keyes analog hall effect sensor (KY-035, which senses magnetic fields) and are will be others in that range that will work in the same way.

The Light Dependant Resistor (LDR or Photoresistor)

Our sensor will use an LDR to produce a variable resistance in the presence of different light levels.

In the dark, their resistance is very high, sometimes up to 1MΩ, but when the LDR sensor is exposed to light, the resistance drops dramatically, even down to a few ohms, depending on the light intensity. LDRs have a sensitivity that varies with the wavelength of the light applied and are non-linear devices.

They are widely used in cameras, solar garden lights, clocks, mini night-lights, and a variety of light control devices.

Specifications from a typical LDR show that as illumination increases, the resistance of an LDR decreases.

Light Level            Resistance
Moonlight              1,000,000 Ohms
60W bulb at 1m         6,000 Ohms
Fluorescent Lighting   1,000 Ohms
Bright sunlight        1 Ohm

The Keyes KY-018 sensor board comprises an LDR and a fixed resistor with header pins for connecting the ground, the reference voltage (we will use the 3.3V from the Pi) and the sensors analog voltage output.

Keyes KY-018 Photoresister Sensor Board
Keyes KY-018 Photoresister Sensor Board

If we consider a simplified circuit of our sensor, the LDR in series with a fixed resistor allows the variation in resistance to develop a variation in output voltage.

LDR Sensor Output Voltage
LDR Sensor Output Voltage

Analog to Digital Conversion (ADC)

Luckily there are a wide range of options available to convert analog signals into digital ones. Therefore, people wanting to receive an analog signal with a Raspberry Pi can simply include a separate ADC into their project and it will work wonderfully. That’s what we’re going to do here using the ADS1015 from Adafruit. The ADS1015 has a 12bit resolution giving it the ability to convert an analog signal into one of 4096 discrete levels.

The ADS1015 Analog to Digital Converter

The ADS1015 is actually a component on our ADC board. This component is manufactured by integrated circuits manufacturer Texas Instruments. The circuit board that we’re using in the project is from Adafruit. It incorporates some interconnection circuity to make the signals as stable as practical and to provide a convenient physical interface (via header pins). The ADS1015 provides 12-bit (4096 levels) precision at up to 3300 readings per second (the rate is programmable). The board can be configured to accept four sensors of the type we will be using (single-ended), or two differential channels (which use two varying signals instead of a single signal and a ground). There is also a programmable gain amplifier built in with up to x16 gain, to help amplify smaller signals to the full range. The ADC can operate on a voltage range from 2V to 5V Which is applied to the VDD pin).

ADS1015 Sensor Board
ADS1015 Sensor Board

If all this sounds a bit ‘electrickery’, don’t worry. The aim here is to provide ourselves with enough information to get us started and if we feel like pressing on and learning more we will :-).

The ADS1015 will send the digital levels to the Pi via the I2C communications protocol. The address that this connection is made on can be changed to one of four options so you can have up to 4 ADS1015’s connected for up 16 sensor inputs!

Measure

Hardware required

  • A Raspberry Pi (huge range of sources)
  • ADS1015 Analog to Digital Converter from Adafruit.
  • Female to Female Dupont connector cables (Deal Extreme or build your own!)
  • Photoresistor module KY-018 from Keyes.

Connect

The LDR sensor board should be connected with ground pin (labelled ‘-‘) to a ground connector, the reference voltage pin (in the case of the board shown below the centre pin) to a 3.3V connector and the signal output pin (labelled ‘S’) to the A0 pin on the ADS1015 ADC.

The ADS1015 board should have the VDD pin connected to a 3.3V pin, the GND to a ground pin, the SCL pin to the SCL I2C connector on pin 5 and the SDA pin to the SDA I2C connector on pin 3 and lastly the ADDR pin should be connected to ground.

Both boards will support a connection to the ‘VDD’ and reference voltage connector of 5V, but this is not advisable for the Raspberry Pi as the resulting signal levels on the SDA connector may be higher than desired for the Pi’s input. This connection can be safely used with an Arduino board.

LDR Sensor Board Connection
LDR Sensor Board Connection

Connecting the sensor practically can be achieved in a number of ways. You could use a Pi Cobbler break out connector mounted on a bread board connected to the appropriate pins. But because the connection is relatively simple we could build a minimal configuration that will plug directly onto the pins using Dupont header connectors and jumper wire. The image below shows how simple this can be.

Physical Connection of ADS1015 and LDR Sensor
Physical Connection of ADS1015 and LDR Sensor

Test

Since the ADS1015 uses the I2C protocol to communicate, we need to load the appropriate kernel support modules onto the Raspberry Pi to allow this to happen.

Firstly make sure that our software is up to date

Since we are using the Raspbian distribution there is a simple method to start the process of configuring the Pi to use the I2C protocol.

We can start by running the command;

This will start the Raspberry Pi Software Configuration Tool.

On the first page select the Interfacing Options with the arrow keys and then tab to select

Interfacing Options
Interfacing Options

Then we select the I2C option for automatic loading of the kernel module;

Automatic Loading
Automatic Loading

Would we like the ARM I2C interface to be enabled? Yes we would;

ARM I2C Interface Enabled
ARM I2C Interface Enabled

Press ‘OK’ to acknowledge that the interface is enabled.

Enabled!
Enabled!

Press tab to select ‘Finish’.

We're Finished
We’re Finished

There’s still some work to do to get things sorted. We need to check the /etc/modules file using:

Where we need to ensure that the following line is at the end of the file:

i2c-dev

Under some circumstances (depending on the kernel version we are using) we would also need to update the /boot/config.txt file. We can do this using;

Make sure that the following line is uncommented (the ‘#’ is removed from in front of the line) in the file;

dtparam=i2c_arm=on

The we should load tools for working with I2C devices using the following command;

… and now we should reboot to load the config.txt if we changed it earlier

We can now check to see if our sensor is working using;

The output should look something like;

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

This shows us that we have detected our ADS1015 on address ‘48’. The ADS1015 can support four different addresses as shown on page 17 of the data sheet. The address is selected by what the ADDR (short for address!) pin on the board is connected to;

ADDR PIN   ADDRESS
Ground     48
VDD        49
SDA        4A
SCL        4B

As we noted earlier, this means we can connect up to four ADS1015’s on the same I2C bus.

Now we want to install Python libraries designed to read the values from the ADS1015. The library we are going to use was designed specifically to work with the Adafruit ADS1015/ADS1115 ADCs. In carrying out this library development, Adafruit have invested a not inconsiderable amount of time and resources. In return please consider supporting Adafruit and open-source hardware by purchasing products from Adafruit!

Now we create a directory that we will use to download the Adafruit Python library (assuming that we’re starting from our home directory);

Now we will download the library into the adc directory (the following command retrieves the library from the github site);

Now that we’ve downloaded it, we can set it up. Run the following commands;

This will download and extract the tools. Then we can change into the examples directory

Now we can run the example program simpletest.py as follows;

This will start a program that will present a continuous reading of the four analog channels connected to the ADC.

Reading ADS1x15 values, press Ctrl-C to quit...
|      0 |      1 |      2 |      3 |
-------------------------------------
|   5920 |   4704 |   4608 |   4608 |
|   5920 |   4576 |   4608 |   4656 |
|   7568 |   4640 |   4592 |   4656 |
|  14368 |   4656 |   4608 |   4672 |
|  15152 |   4656 |   4608 |   4656 |
|  14160 |   4608 |   4592 |   4672 |
|  13696 |   4608 |   4624 |   4640 |
|  14016 |   4608 |   4608 |   4704 |
|   8384 |   4608 |   4624 |   4672 |
|   4560 |   4592 |   4624 |   4656 |

If we move the LDR about we should see that channel 0 varies up and down as the amount of light it receives varies. Success!

We should see that when the sensor is exposed to a stronger light the value decreases and when it gets darker the value increases.

If we remember back to our earlier diagram showing the type of connection this helps put the changes into context

LDR Sensor Output Voltage
LDR Sensor Output Voltage

We can press Ctrl-C to stop the program.

At this point we have successfully read and displayed an analog signal from a sensor using the Raspberry Pi.

Record

To record this data we will use a Python program that connects to our sensor via the ADC, reads the returned value and writes that value into our database. At the same time a time stamp will be added automatically.

Our Python program will use cron to execute the program at a regular intervals (We used this earlier to automatically reconnect to the network if required.).

Record the readings

The following Python code simply reads channel 0 of our ADC and writes the value for time and light into our database.

The full code can be found in the code samples bundled with this book (light-record.py).

#!/usr/bin/python
#encoding:utf-8

import time               #Import time library
import sqlite3            #Import SQLite library
import Adafruit_ADS1x15   #Import ADS1x15 library

print "Light sensor measurement in progress"

# Create an ADS1015 ADC (12-bit) instance.
adc = Adafruit_ADS1x15.ADS1015()

# Choose a gain of 1 for reading voltages from 0 to 4.09V.
GAIN = 1

# Read ADC channel 0 using the gain value.
ldr = adc.read_adc(0, gain=GAIN)

# Get the time in the right format
dtg = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())

# Print the time and LDR values
print "Local current time :", dtg
print "LDR Value via ADC :", ldr

# Write the values to the database
try:
    # Opens a file called measurements
    db = sqlite3.connect('/home/pi/measurements')
    # Get a cursor object
    cursor = db.cursor()
    # Insers the values into the table
    cursor.execute('''INSERT INTO light(dtg, ldr)
                  VALUES(?,?)''', (dtg,ldr))
    # Commit the change
    db.commit()
# Catch any exception
except Exception as e:
    # Roll back any change if something goes horribly wrong
    db.rollback()
    raise e
finally:
    # Close the db connection
    db.close()

This script can be saved in our home directory (/home/pi) and can be run by typing;

The output should look something like this;

Light sensor measurement in progress
Local current time : 2018-04-15 09:31:23
LDR Value via ADC : 176

Run this script a few times and then we can check the results in our database by starting up SQLite as follows;

From the SQLite prompt we can query the database to return all the records using SELECT * FROM light; as follows;

sqlite> SELECT * FROM light;
2018-04-15 08:39:23|221
2018-04-15 08:41:34|56
2018-04-15 08:53:18|158
sqlite>

There are three records, including the times they were taken and the light levels recorded.

Recording data on a regular basis with cron

As mentioned earlier, while our code is a thing of beauty, it only records a single entry for the light every time it is run.

What we need to implement is a schedule so that at a regular time, the program is run. This is achieved using cron via the crontab.

To set up our schedule we need to edit the crontab file. This is is done using the following command;

Once run it will open the crontab in the nano editor. We want to add in an entry at the end of the file that looks like the following;

* * * * * /usr/bin/python /home/pi/light-record.py

This instructs the computer that every minute, every hour of every day of every month we run the command /usr/bin/python /home/pi/light-record.py (which, if we were at the command line in the pi home directory, we would run as python light-record.py, but since we can’t guarantee where we will be when running the script, we are supplying the full path to the python command and the light-record.py script.

Save the file and when the next hour rolls over our program will run on its designated schedule and we will have light values written to our database every minute. After a while we can check it out by running a SELECT * FROM light; on the measurement database.

sqlite> SELECT * FROM distance;
2018-04-15 08:39:23|221
2018-04-15 08:41:34|56
2018-04-15 08:53:18|158
2018-04-15 10:00:06|159
2018-04-15 10:01:06|160
2018-04-15 11:02:06|159
sqlite>

Managing database size

While it’s a great idea to save our local data into a database, we stand the risk of gradually letting that database fill up until it exceeds the capacity of our storage.

In the case of the measurements that we are carrying out, the readings are happening pretty regularly, so it’s worth thinking about. Capturing some simple measurements every minute means in the scheme of things that’s about 10,000 recordings per week.

What we’re looking for is a script that will run on a repeating schedule and remove old records. Sound familiar? That’s a very similar process to what we are doing when we record our data. A python script that is executed regularly by cron.

Here’s how we can do it.

The following python script (which we can name db-manage.py) opens our database, deletes any records older than a year, cleans up and exits.

#!/usr/bin/python
#encoding:utf-8

#Import SQLite library
import sqlite3

# Opens a database file called measurements
conn = sqlite3.connect('/home/pi/measurements', isolation_level=None)
db = conn.cursor()

# Delete any records that are older than 1 year
db.execute('DELETE FROM light WHERE dtg<DATETIME("now","localtime", "-1 years")')
# VACUUM the database to remove any unnecessary data
db.execute('VACUUM')

# Commit the changes to the database and close the connection
conn.commit()
conn.close

The file is available as db-manage.py and can be found in the code sample extras that can be downloaded with this book.

It’s a pretty simple script and we can schedule its operation by editing the crontab file like so;

We want to add in an entry at the end of the file that looks like the following;

1 0 */1 * * /usr/bin/python /home/pi/db-manage.py

This instructs the computer that at 1 minute past the hour at midnight (hence the 0) on the 1st day of every month we run the command /usr/bin/python /home/pi/db-manage.py (which, if we were at the command line in the pi home directory, we would run as python db-manage.py, but since we can’t guarantee where we will be when running the script, we are supplying the full path to the python command and the db-manage.py script.

Save the file and every month our program will run on its designated schedule and will make sure to delete any records older than a year.

Explore

Simple data point API

The main mechanism for exploring and using our data is going to be via a simple data block returned from a http request.

What does all that actually mean?

That’s a good question. Ultimately we’re measuring something and we want to be able to communicate that measurement to an external service. That service could be another database somewhere or to a web page or to a system that will alert based on the value of the light levels being within certain boundaries.

The very simplest way that we can do this is to present the data as the measured values when we ask for them in a web request. This could be thought of as a simplified form of an API (and I plan to make something more complicated in the future).

The data will be presented as JSON as that is one of the most ubiquitous data forms around.

Enough esoterics, what does the magic code look like that will do this?

<?php

$db = new PDO('sqlite://home/pi/measurements');

$result = $db->query('SELECT * FROM light ORDER BY dtg DESC LIMIT 1');

$datapie = array();

$result->setFetchMode(PDO::FETCH_ASSOC);

while ($row = $result->fetch()) {
    extract($row);
    echo json_encode($row);
}

?>

We can save this file as light.php and have it in the /var/www/html directory on our Pi (light.php can be found in the code sample extras that can be downloaded with this book).

How we can put in the IP address of our Pi to our browser along with our distance php file (http://10.1.1.120/light.php) and we should get something like the following appear in the browser;

{"dtg":"2018-03-24 11:01:06","ldr":"159"}

What good will getting this data be? Well……. I’m a bit of a believer that the information that gets captured by the Pi shouldn’t ultimately reside on the device in the long term. In the perfect world I would see it being requested by an external service that was checking a range of data points that would exist around the home (pressure, temperature inside / outside, CO2 levels, is the car parked in the garage, that sort of thing) so this is more of an enabling device than a ‘let’s display stuff’ deal. But I hear what you’re saying. “That’s lame. How can I impress people with that?”. Fair point. To deal with that problem let’s make a simple graph.

Extracting a Range of Data

Righto… If we’re going to make a graph of our light levels we’ll need a variation of our API that will gather and present a range of data that our graph can then display.

This will form a piece of code that our graph will use as a JSON formatted data source.

It will look as follows;

<?php

$data= array();

// connect to the database
$db = new PDO("sqlite://home/pi/measurements");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// prepare SQL command and execute
$query = "SELECT * FROM light WHERE 
          dtg>DATETIME('now','localtime', '-24 hours')";        
$result = $db->prepare( $query );
$result->execute();

// compile the returned data
$values = $result->fetchAll(PDO::FETCH_ASSOC);
array_push($data, $values);

// print the data
echo json_encode($values);

// close the database connection
$db = NULL;

?>

This block of PHP code will connect to our database and instead of returning a single piece of data it will return (‘echo’) a range of values from the past 24 hours. We’ll call the file light-range.php and it will be in the /var/www/html directory. A copy of the file can be found in the code sample extras that can be downloaded with this book.

The following file is our graph which will use our light-range.php file and display it. It uses the d3.js visualisation library and for a full description of the workings of the code please feel free to consult a copy of ‘D3 Tips and Tricks v4.x’. It’s free and can be downloaded from here.

<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 2px;
}

</style>
<body>

<!-- load the d3.js library -->    	
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%d %H:%M:%S");

// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);

// define the line
var valueline = d3.line()
    .curve(d3.curveBasis)
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });

// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

// Get the data
d3.json("light-range.php", function(error, data) {
  if (error) throw error;

  // format the data
  data.forEach(function(d) {
      d.date = parseTime(d.dtg);
      d.close = +d.ldr;
  });

  // Scale the range of the data
  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.close; })]);

  // Add the valueline path.
  svg.append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", valueline);

  // Add the X Axis
  svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

  // Add the Y Axis
  svg.append("g")
      .call(d3.axisLeft(y));

});

</script>
</body>

We will want to place a copy of this file which we will call light-graph.html in the /var/www/html directory. A copy of it can be found in the code sample extras that can be downloaded with this book.

We can see the end result by putting the web address into our browser. It should look something like http://10.1.1.120/light-graph.html. The end result should look a bit like the following;

Light Levels Graph
Light Levels Graph

Wrap Up

There we have it.

We’ve assembled our Raspberry Pi with an analog light sensor and an analog to digital converter. We installed an operating system and configured it for use. We’ve set up networking and installed a database and a web server. We’ve written code to record data into our database and an API to pull data out of it. We’ve even installed a graph to display it in a visual form. Nice work.

There is a strong possibility that the information I have laid out here could be littered with evil practices and gross inaccuracies.

But look on the bright side. Irrespective of the nastiness of the way that any of it was accomplished or the inelegance of the code, if the picture drawn on the screen is relatively pretty, you can walk away with a smile. :-)

Those with a smattering of knowledge of any of the topics I have butchered above (or below) are fully justified in feeling a large degree of righteous indignation. To those I say, please feel free to amend where practical and possible, but please bear in mind this was written from the point of view of someone with only a little experience in the topic and therefore try to keep any instructions at a level where a new entrant can step in.

Bibliography

The following texts were incredibly useful in drafting up this project.

RPi and I2C Analog-Digital Converter (OpenLabTools)

ADS1015 12-Bit ADC - 4 Channel with Programmable Gain Amplifier(Adafruit)

ADS1015 datasheet (Adafruit)

Adafruit 4-Channel ADC Breakouts (Adafruit Learning System)

Analog Sensors On The Raspberry Pi Using An MCP3008 (Matt, raspberrypi-spy.co.uk)

Analog Input Board for the Espiresso Pressure Sensor (int03.co.uk)

Arduino KY-018 Photo resistor module (TkkrLab)

Shenzhen KEYES DIY Robot co., Ltd

Light dependant resister datasheet (Sunroom Technologies)