17 Jan 2021

Sensing Temperature with a RaspberryPi

Devices for sensing temperature in electronics projects come in 3 basic varieties:

  1. resistive sensors
  2. thermometer ICs
  3. digital thermometers
In this blog post I'll discuss the first variety. Future posts will discuss the others.

Resistive Sensors

Thermistor

The thermistor is the most basic, and cheapest, way to detect temperature. A thermistor is a 2-lead device whose resistance at any given point in time is directly related to its temperature at that time.

Thermistors come in 2 varieties:

  • PTC (positive temperature coefficient)
    • resistance increases with increasing temperature
  • NTC (negative temperature coefficient)
    • resistance decreases with increasing temperature

NTC-type thermistors are the most common.

In reality all electronic components (especially resistors) are affected by temperature, but ideally the effects due to temperature are minimal. In most cases the effects are so small that trying to use a regular resistor to measure temperature would require sensitive measuring devices and amplification in order to detect the variances. It could only be used as a very crude temperature sensing device with little resolution. A thermistor is specifically designed to highlight the effects of temperature and to make these effects noticeable in response to relatively small temperature changes.

One major challenge with using a thermistor is that the relationship between resistance and temperature is not linear across a temperature range. Pretend you have a thermistor that at 0°C was 1kΩ and at 10°C was 2kΩ, meaning there's a 1kΩ change for every 10°C. That doesn't mean that if the resistance at 40°C is XkΩ that the resistance at 50°C will be (X+1)kΩ. The change might be (X+7) or (X+0.4).

Figuring out how to translate between measured resistance and temperature can require a tricky bit of math! A commonly accepted relationship between resistance and temperature is given by the Steinhart-Hart Equation:

For this equation the temperature is specified in Kelvins, and you need to know the a, b, and c coefficients for your specific thermistor in order to use this equation. The datasheet for a thermistor will go into a lot of detail to help explain how to perform the translation and will often provide these coefficients for you (or coefficients for other equations it provides). This third-order equation wouldn't be too hard for a RaspberryPi to perform, but a lookup table with linear interpolation would probably work better in practice (especially for math-challenged SoCs such as 8-bitters).

Another issue faced with thermistors is their susceptibility to self-heating. Any electronic circuit will tend to heat up when it is powered; thermistors are no exception. However, given that the job of this device is to measure temperature, heating up as a result of being powered on will cause a thermistor to report a slightly higher temperature than its surroundings. Again, a datasheet provides the details for how to account for this effect mathematically.

Although we're now aware of various issues that can cause our measurement to require correcting, we still haven't figured out how to use a RaspberryPi to measure the resistance of a thermistor.

Analog-to-Digital Converters

If we take a step back, we'll realize that a varying resistance is an analog signal. One of the best ways to measure an analog signal is to use an analog-to-digital converter (ADC): a circuit whose job it is to measure analog signals and convert them into 1s and 0s for digital processing.

The problem is that an ADC measures voltage, not resistance. Therefore we need a way to convert a varying resistance into a varying voltage. Luckily this problem isn't too hard to solve: some sort of voltage divider circuit can be easily constructed for this purpose.

A second issue is that the RaspberryPi doesn't have (or doesn't expose) any ADCs for our use. Most SoCs have at least one ADC which the SBC exposes through its interface header. This is especially true of boards that are targeted at this market (i.e. the "maker" space or educational electronics). The standard Arduino, for example, has 6 analog pins. It's possible, however, that not having an available ADC might be a good thing. There are tens of thousands of discrete ADC ICs from which to choose (hundreds that are through-hole alone). One can choose the resolution (i.e. the number of bits) from 6 to 32, the communication protocol (e.g. I2C, SPI, QSPI, parallel, 1-wire, serial, etc and combinations thereof), the sampling rate (from as low as a couple samples a second up to over 10G S/s), the voltage ranges you want to sample, the signal-to-noise ratio, and the type/architecture of the conversion circuitry. When an ADC is integrated into the SoC, the ADC that is chosen will be a general-purpose one with middle-of-the-road (or below) specs. Being able to choose the right ADC for your application might be a good thing.

Now we have all the pieces we need in order to use a thermistor with a RaspberryPi to read temperature:

Note that the components in the drawing are not drawn to scale, and that the voltage divider drawing is meant to be a generic representation of a voltage divider circuit, although perhaps not the exact circuit that is needed. I chose the MCP3001 as my ADC simply because when I looked through the drawer of ADC components I have on my workbench, that's one of the ones I found. It happens to be a 1-channel ADC that uses SPI for communication, has 10 bits of resolution, and has a sampling rate of 75kS/s at 2.7V (or 200kS/s at 5V).

The thermistor forms one of the loads of the voltage divider. The output of the divider is fed to the IN+ pin of the ADC (pin 2). Pins 5, 6, and 7 of the ADC are the SPI pins that are hooked up to the RaspberryPi. +3.3V and grounds are hooked up as appropriate.

Our circuit is ready to read temperature. Now on to the software.

Communication Buses

Before we can get on with the job of reading the converted voltage values from the ADC over SPI, we need to take a quick look at communication buses in general.

If you want to connect two or more computers together in the same building, chances are you'd use Ethernet, WiFi, or maybe TokenRing: these are the common communication buses used between personal computing devices. Expansion cards on a motherboard are often connected via ISA, PCI, and/or PCIe buses. External devices are often connected to a personal computer with some version of USB.

In the embedded world, two of the most common inter-chip communication buses are I2C and SPI; these are two (mostly) standardized ways of connecting individual components together on a PCB. Like all good specifications, the design of the I2C and SPI buses define aspects such as the signaling, what to do in case of a collision, the algorithm of which devices get to speak when, the data rate(s), if/how multiple devices can be connected at the same time, and how to physically connect devices together (how many pins and wires are needed).

As we've just seen, we found ourselves in the position where we needed to add a discrete IC to our design (i.e. the ADC). In theory every chip could define its own communication method(s), or every company could define its own communication protocols for the devices it manufactures. There is much benefit to the end user if manufacturers standardize on a small set of common protocols.

Some buses are discoverable. This means there is a mechanism devices can use to identify themselves uniquely and/or specify which generic service they provide. The PCIe and USB buses are discoverable. Without any intervention from the user, the OS is able to identify each connected device, and get them ready to be used. For example, if the user plugs an external hard-drive into a computer's USB bus, the OS (if instructed to do so) can automatically mount one or more of the hard-drive's partitions into the filesystem. This requires anyone wanting to create and sell PCIe or USB devices to buy, and pay a yearly fee, in order to own a unique vendor ID.

I2C and SPI buses are not discoverable. There is no way for an OS (or RTOS) to probe these buses and be sure they've uniquely found all connected devices. Therefore it is not possible for Linux (for example) to generate sysfs entries for devices connected on these buses and perform various conversions automatically. Which means that if you want to read an ADC over SPI (or I2C) you'll need to write your own software (or find ready-to-use libraries from someone who has already written them).

SPI

Before you can write software to interact with an SPI-connected device, you have to understand a few things about how SPI works. SPI defines a clock line, and 2 data lines (one in each direction). It uses a physical chip select line to select which slave device can respond to the master.

When a slave device writes to the bus, it will make its data valid on either the rising or falling edge of the clock. The clock line (which is controlled by the master) can either idle high or idle low when not in use. The master and slave have to agree on these parameters of the communication in order for them to communicate successfully. Defining which way the clock line idles is generally referred to as the CPOL, or clock polarity. Defining whether the data is valid on the rising or falling clock edge is generally referred to as the CPHA, or clock phase.

The datasheet for your specific SPI device will tell you which combinations of (CPOL, CPHA) the device supports. The MCP3001, for example, only supports configurations (0, 0) and (1, 1).

The SPI bus supports a range of data rates, but not all devices support all possible data rates. The data rate is related to the sampling rate; there wouldn't be much use in a high sampling rate if there was no way to send that data to the master in time. Again, consult the datasheet for your device to know which data rates can be safely used; the MCP3001 supports data rates up to 2.8MHz.

The last piece of information you need to know is the data format. The MCP3001 is a single-channel, 10-bit ADC. In theory 10 bits of data could be transferred using 10 bits. However Linux seems to prefer to communicate using 8-bit bytes. My attempts to change the bit size from 8 to 10 weren't successful; possibly a driver issue. In any case I was forced to communicate with the MCP3001 using 8-bit packets. For reasons that aren't entirely clear, when the MCP3001 detects that 8-bit bytes are being used for communication, it encodes its 10 bits of data as follows:


The act of initiating a read from the device is what causes the conversion to commence. Therefore the first three bits from the device are two unknowns (X) and a 0 due to the time required to perform a conversion. Next the device starts writing the 10 bits starting with the MSb. Once it reaches the LSb it then writes these same bits out again going from the LSb to the MSb. Any bits that are read after these 22 bits, are read as zeros.

Knowing these parameters for your specific SPI device, you can now write the code to communicate with your device over SPI.

OpenEmbedded/Yocto

Before you can use SPI on your RaspberryPi, you have to enable it. Enabling SPI on the RaspberryPi requires the user to modify the contents of the config.txt file (correctly) and reboot. If you're using any images or distros that are not based on OpenEmbedded/Yocto then there are a bunch of different tools and ways to modify this file (including using a simple text editor). If you don't make your edits correctly you could end up with a device that refuses to boot until you correct any problems.

If you are using OpenEmbedded/Yocto to generate your images, enabling SPI is as simple as adding the following line to your configuration (most likely your conf/local.conf file):

I can't imagine anything simpler. Rebuild your image, flash it to your µSD card, and boot.

SPI in Linux on RPi

Writing code for Linux to read an SPI device in user-space is very straight-forward, provided you've read the datasheet of your device and you know how it needs to be configured (i.e. which SPI mode(s) it uses, the bit size, and speed) and the format of the data it will give you.

If your image is setup correctly with SPI enabled, after your device boots you'll find the following device nodes in your filesystem:

# ls -l /dev/spidev*
crw-------    1 root     root      153,   0 Jan  1  1970 /dev/spidev0.0
crw-------    1 root     root      153,   1 Jan  1  1970 /dev/spidev0.1

Your device will be connected to one of these nodes. Simply write a program that opens the correct one, uses ioctl(2)s to configure it, and read(2)s from it to get data from your SPI device.

The RaspberryPi's 40-pin GPIO Header exposes one SPI bus with two chip selects:

Connect the MCP3001's Pin-6 (DOUT) to the RPi's Pin-21 (SPI0_MISO), connect MCP3001's Pin-7 (CLK) to RPi's Pin-23 (SPI0_SCLK). If you connect MCP3001's Pin-5 (/CS) to RPi's Pin-24 (SPI0_CE0_N) then your device will be at /dev/spidev0.0. If, instead, you connect the MCP3001's chip-select to RPi's Pin-26 (SPI0_CE1_N) then you'll find your device at /dev/spidev0.1. This is how you know which device to use in your code. In the string "spidevX.Y" the X refers to the SPI bus, and the Y refers to the chip-select for SPI bus X.

You'll notice that since the MCP3001 is a 1-channel ADC, it doesn't receive any commands from the master. In fact, it doesn't even have a MOSI data line (master out, slave in), it only has a DOUT data line (aka MISO). Therefore there's nothing to connect to the RPi's Pin-19 (SPI0-MOSI) pin. Most other SPI devices would have something connected to this pin.

Now that you've open(2)ed the correct device, you can proceed to configure it with various SPI ioctl(2)s. You can set the mode with SPI_IOC_WR_MODE32, you set the number of bits with SPI_IOC_WR_BITS_PER_WORD, and you set the speed with SPI_IOC_WR_MAX_SPEED_HZ. The Linux kernel's SPI documentation provides lots of information, or you can just read the source code itself.

I've written a small MCP3001 program (which you can find here) to demonstrate. The defaults are all setup to work "out-of-the-box" (assuming you're using /CS0). Otherwise the program takes a range of optional cmdline arguments that can be used to tweak the configuration.

# mcp3001 -h
mcp3001 1.0.0
usage: mcp3001 [options]

  where:
    options:
      -D|--device <d> SPI device to use (default: /dev/spidev0.0)
      -s|--speed  <s> max speed (Hz)
      -b|--bpw <b>    bits per word
      -O|--cpol       clock polarity, idle high (default: idle low)
      -H|--cpha       clock phase, sample on trailing edge (default: leading)
      -L|--lsb        least significant bit first
      -C|--cs-high    chip select active high
      -3|--3wire      SI/SO signals shared
      -N|--no-cs      no chip select
      -R|--ready      slave pulls low to pause
      -v|--verbose    add verbosity
      -V|--version    print version


A Thermistor and a RaspberryPi

At this point we have everything we need to read a temperature with a RaspberryPi using a thermistor: in addition to the RPi and a thermistor we have an ADC which can read the analog signal and convert it to binary, and we have a voltage divider circuit which converts a varying resistance to a varying voltage which the ADC can measure. We have SPI enabled in our image, we have our ADC device hooked up to our RPi, and we even have some code to read the values being generated by the ADC over SPI.

As you can see, reading a temperature of a thermistor with a RaspberryPi is not very straight-forward; despite the information in this post, more work remains. I glossed over the math parts because, frankly, it's not worth it. Also, I didn't dig very deeply into the details of the voltage divider circuit needed to convert the varying resistance to a varying voltage. As we'll see in subsequent posts, there are much easier ways (and, ironically, much more accurate ways) to read a temperature.

Nothing Wasted

The good news is: time spent learning something new is never wasted.

Although the details needed to use a thermistor aren't really worth the effort, the things we've learned are applicable to a great many other situations. There are a number of sensors that measure something in the physical world, and describe it using a varying resistance.

For example: a user rotating a dial on a potentiometer changes the amount of resistance. A photocell changes its resistance based on how much light falls on it. Force sensitive resistors change their resistance based on how hard they are being pressed. Also, 2-axis analog joysticks don't just report that their toggle has been moved (for example) to the left, they provide a set of resistors that are read to indicate how far along each of the X and Y planes the toggle has been moved.

Everything we've learned hooking up a thermistor to a RaspberryPi can be used to read a whole host of other sensors!


No comments: