Recently I am working on a small hardware project with Raspberry Pi. I want to use Raspberry Pi 3 B+ as an indoor monitor. It should be able to detect the brightness and turn on the light when it is getting dark; it should also be able to remind me to close the window when the room temperature gets too low. Therefore, a light sensor and a temperature sensor are required.
Choose the Sensors
Generally, there are two ways to add sensors: purchase pre-built sensor modules or build your own sensors from parts.
Pre-built sensors are very easy to use and are preferable if you simply want to add the sensors without considering too much. But in my case, I would rather build my own sensors from parts, since I could learn something about electronics and get some experience to benefit my future projects. So I decided to build my own sensors.
Sensors are actually very simple. First, we need a component to output a voltage based on the environment. This voltage is analog, so we need an ADC to convert the analog signal to digital. That’s it.
For the light sensor, I chose Adafruit 161 Photocell. This is a pretty cheap ($0.95) photoresistor and Adafruit has provided a great tutorial. Also, there is a great article about how to use it with Raspberry Pi.
The brightness (illuminance) is measured in lux. Wikipedia has a great table explaining illuminance and lux. Based on the table, my use case falls in the range between 20–100 lux, so the 161 photocell is perfectly fine. Also since I don’t care about the accurate illuminance value, and I have no way to measure it, I don’t need the sensor to provide the accurate value.
The photocell needs a pull-down resistor. A typical connection is like this:
The datasheet also demonstrated how to calculate the pull-down resistor. In my use case, the ambient light would be 10~100 lux, where the photocell resistance is between 10kΩ~1.5kΩ, so a 10kΩ pull-down resistor is perfectly fine.
For the temperature sensor, I chose LMT86 from Texas Instrument. This sensor is cheap too (less than $1).
I didn’t choose the famous LM35 because I didn’t know when I bought the parts. But from the datasheet, LMT86 seems totally fit my needs:
- Fast Thermal Time Constant, 10-s Typical
- Very Accurate: ±0.4℃ Typical
- Wide Temperature Range: -50℃ to 150℃
From Figure 9 in the datasheet, we can see that the output voltage is almost independent of the supply voltage as long as it is greater than 2.7V. Since Raspberry Pi provides 3.3V, this means we don’t need to consider the voltage difference, and we can get the temperature with a constant formula.
The LMT86 response is “almost” linear, it does have a slight umbrella parabolic shape. The datasheet provides an accurate lookup table in Table 3. It also provides a less accurate parabolic equation
However, this equation requires a square root operation. Although it is okay for my application since Raspberry Pi is USB-powered, this might be problematic on low-power devices. An alternative formula fits with a linear regression that consumes less power, which could be useful in some cases.
Since I am using Raspberry Pi, using a lookup table is not a problem, so I chose the most accurate table method. I created a package
lmt8x (GitHub) to make the lookup easier. You can install this package on Raspberry Pi using:
pip install lmt8x
There are something important to know in the datasheet.
First, the pin configurations are different for different packages. In LP/LPM packages (LMT86LP, LMT86LPM), the center pin is OUT, while in the LPG package (LMT86LPG) the left pin is OUT. Be careful when connecting — a wrong connection may damage your sensors!
Another thing is the “Thermal Time Constant”. This is a metric of how fast temperature sensors can respond with the true temperature. Although LMT86 has a fast thermal time constant, it still requires about 40s before it could respond with the correct temperature. Keep this in mind when experimenting — don’t use the data right after system boot!
Analog to Digital Conversion
Since both 161 and LMT86 have analog output, and Raspberry PI GPIO can only process digital input, we need an ADC (Analog-to-Digital Converter) to convert the signals to digital data. A commonly used ADC is MCP3008 (datasheet), which features a 10-bit resolution (output range from 0~1024) and 8 input channels.
MCP3008 supports two modes: single-ended mode, and differential mode (see datasheet Table 5–2). In this application, I only need to read data from sensors, so the single-ended mode is good enough.
The datasheet also explained how to read data from MCP3008 in the “5.0 Serial Communication” section. But fortunately, Adafruit provided a driver
adafruit-circuitpython-mcp3xxx so we don’t have to follow the clock sequence.
Adafruit also has a great tutorial along with Python code about how to use MCP3008, so I will omit the details here, but only showing some diagrams for references.
So the connection would be (from MCP3008 to Raspberry Pi):
- VDD → 3V3 (pin 1)
- VREF → 3V3 (pin 1)
- AGND → GND (pin 9, 25, 39)
- CLK → SCLK (pin 23)
- DOUT → MISO (pin 21)
- DIN → MOSI (pin 19)
- CS/SHDN → any GPIO pin
- DGND → GND (pin 9, 25, 39)
Assemble the Prototype
Now we only need to connect the photocell and the temperature sensor to the CH0, CH1 pins of MCP3008, then connect MCP3008 to Raspberry Pi. Here is a simple schematic:
Here I connected the CS pin to the
GPIO22 pin on Raspberry Pi. Therefore, I must use
board.D22 in the code. Also, Raspberry Pi has many redundant pins (e.g. pin 6, 9, 14, 20, 25… are all GND), you can use any pin that is convenient.
In my actual experiment, I added another temperature sensor LMT87LPG, to compare the results from two different models. So the photocell is connected to CH0, while LMT87 to CH1, and LMT86 to CH2.
After carefully examining the connections, I created the following code to test the sensors (I was using Python 3):
import adafruit_mcp3xxx.mcp3008 as MCP
from adafruit_mcp3xxx.analog_in import AnalogIn
from lmt8x import lmt87_v2t, lmt86_v2t# create the spi bus
spi = busio.SPI(clock=board.SCK, MISO=board.MISO, MOSI=board.MOSI)# create the cs (chip select)
cs = digitalio.DigitalInOut(board.D22)# create the mcp object
mcp = MCP.MCP3008(spi, cs)# create an analog input channel on pin 0
chan0 = AnalogIn(mcp, MCP.P0)# create an analog input channel on pin 0
chan1 = AnalogIn(mcp, MCP.P1)
chan1 = AnalogIn(mcp, MCP.P1)chan2 = AnalogIn(mcp, MCP.P2)
chan2 = AnalogIn(mcp, MCP.P2)while True:
print('light: %d, %.3fV\t\tlmt87: %d, %.3fC\t\tlmt86: %d, %.3fC' % (
chan1.value, lmt87_v2t(chan1.voltage * 1000),
chan2.value, lmt86_v2t(chan2.voltage * 1000),
Install the dependencies:
pip install adafruit-blinka adafruit-circuitpython-mcp3xxx lmt8x
Run this script:
We can see that the results from LMT87 and LMT86 do have a small discrepancy (approximately 0.3℃), but this is acceptable, and I’m planning to use the average of the two. I also tried to cover the light sensor and touch the temperature sensors with my fingers. Obvious voltage/temperature changes
can be observed from the result (the red boxes in the following screenshot), indicating that the sensors are working properly.
Soldering the PCB
I purchased some perf board in order to create a usable product beyond the breadboard. I struggled a lot trying to place the PCB into my Raspberry Pi case. The perf board is not the perfect size so I could not simply use the screw holes on the Raspberry Pi to fix the PCB. The easiest way is to solder a 40pin header to the PCB, and simply plug the header into the GPIO pins. Not very stable but is good enough. But if I do this, the PCB would be too far away from the edge of the case, and it would be impossible to place the sensors outside the case to pick up correct numbers!
I ended up with the solution of making a separated ADC board and a sensor board — the ADC board has a 40pin header and will be plugged into the GPIO pins and stay in the case, while the sensor board has all three sensors and will be kept outside the case, and connected to the ADC board with jumper wires. Not a perfect solution, but at least it worked for me.
So I created a new design for the ADC board. The PCB looks like this:
- The 40pin header (at the top) is soldered to the bottom layer, so all traces have to connect from the top layer. Similarly, JP1~JP5 and the MCP3008 chip are soldered to the top layer, so traces have to connect from the bottom layer.
- I used GPIO0 (pin 27) instead of GPIO22 in the schematic, only to make the routing easier. The code needs to be changed accordingly:
cs = digitalio.DigitalInOUt(board.D0)
- MCP3008 supports 8 channels, but only 5 channels are used. There was not enough space for 8 3pin headers.
- I put one VCC and one GND for each connector, which made the soldering much more difficult. Some product uses common VCC and GND pin. Later on, I felt it might be unnecessary to have multiple VCC and GND, and common VCC would be much easier.
The sensor board is pretty simple and I didn’t create a PCB design.
The final product looks like this (Reddit thread):
In fact, my experiments have some flaws that could be further improved. Since I don’t have the necessary parts in my hand and I’m pretty happy with my current result, I will stay with my current setup and will come back when I get all the parts.
Bypass Capacitor for the MCP3008
In the MCP3008 datasheet, section 6.4 “Layout Considerations”:
A bypass capacitor should always be used with this device and should be placed as close as possible to the device pin. A bypass capacitor value of 1μF is recommended.
So looks like I need two 1μF capacitors for the VCC and VREF pins of the MCP3008.
Filter Capacitor for the temperature sensors
The LMT86 datasheet 220.127.116.11 “Design Requirements” mentioned that
When the ADC charges the sampling cap, it requires instantaneous charge from the output of the analog source such as the LMT86 temperature sensor and many op amps. This requirement is easily accommodated by the addition of a capacitor, C_FILTER.
And here is the figure from the datasheet:
So I looked up the MCP3008 datasheet and found the following data.
- Switch Resistance: 1000Ω (page4)
- Sample Capacitor: 20pF (page4)
- Resolution: 10bit
- Sample Time: 1 / 100000Hz (
Based on the formula found on this page, and take
R_out = 1kΩ , we have
(R_on + R_out) * C_sample = (1000 + 1000) * 20e-12 = 4e-8 , and
1 / resolution * T_sample = 0.1 * (1 / 100000) = 1e-6 , the formula holds. So probably I don’t need to use C_FILTER in my design. But anyway I added a 100nF capacitor to each temperature sensor.
Unfortunately, my design could be wrong: based on the Reddit thread, the capacitors should be as close as possible to the ADC pins. In my case, I should put one capacitor for each input pin, even for the light sensor. This capacitor could also filter out the noises picked by the traces and the wires.
Use a HAT extension kit
From one of the Reddit threads, I learned that Adafruit has a “Perma-Proto HAT” kit which is perfect for such projects. Would definitely try this in my next project. Shout out to Antal_z for providing this!
Basically, this is how to use analog sensors with Raspberry Pi. Compared to pre-built sensors, I like this way better because I have full control of how the sensors work. It is very difficult to find analog sensors on Amazon.
It’s a very fundamental project from an electronics amateur who has little knowledge about capacitors. I hope this post could provide some information for the people who want to make similar projects, and please comment for any suggestions.
Thanks for reading!