Reading temperature sensor in Rust using Raspberry Pi GPIO
May 17, 2020
Intro
While learning Rust one of the exercises I always wanted to try is GPIO and low-level device communication.
Simple examples like configuring button handler or led blinking are not that exciting because they are not practical.
Reading temperature and humidity, on the contrary, allows building something like a weather station or just simple graphs.
While it will be much easier to use python with existing libraries. In my opinion, writing it in Rust assembles efficiency of C and readability and safety of Python.
Devices
I use the AM2302 sensor, which is a wired version of DHT22 sensor and Raspberry Pi 3 Model B+, which is on the picture above.
Connection
Utility pinout which is part of python-gpiozero package, allows to get an actual number of the pin for a particular Raspberry Pi model,
here is part of the output for the Raspberry Pi 3 model:
The red wire goes to pin 2 5V. Most other examples use 3.3V pin, but I decided to use 5 to avoid low voltage issues.
Black wire goes to pin 9, ground. Any other ground can be used.
Yellow wire goest to pin 7, which refered as GPIO4, so 4 will be used in the code
Code
There are several options for working with GPIO interface in Rust:
I decided to use gpio-cdev package, as sysfs interface is going to be deprecated.
Prepare
First, adding a dependency to Cargo.toml:
Gpio-cdev has pretty clear and simple documentation so we can have this function to get a line object.
As documentation describes line: “Lines are offset within gpiochip0”
gpio_number in my case is 4.
Requesting data
According to the sensor documentation to initialize data transfer from the device, it should be set the status to zero (pull down) for about 1 - 10 milliseconds.
Reading Data, Part 1
There are 2 options reading the data,
Pulling data manually using line request and .get_value() method
Subscribing to events from the line, using .events() method of the line
Let’s try first approach as it is straight forward.
In following example we will read state for changes during 10 seconds
But how to check if all these changes are valid data, or if initiliazing works properly?
I prefer to stop here, and start from other end: write decoding data part, and then get back to reading and check if data is correct.
Decoding data
Here is example from documentation on how data is repesented:
So let’s express it in Rust!
I define Reading struct that will hold actual temperature and humidity
Data we receive is not always correct, so errors will happen, this enum will represent it:
And to build reading we will need to define a constructor, that will take vector of bytes and converts it to Reading.
It checks:
That there are only 40 bits
That vector contains only 1s and 0s
That checksum is correct
That actual values are valid by specification
It also does all the necessary conversions.
Note, that convert function that converts vector of 1s and 0s to integer is described in my another article
Also, specificaion does not mention it explicitly, but check sum is counted with overflow.
And it is always a good idea to write tests for it. Here I will only show couple of them, the rest is available in the repository
Having all this allows to get back to reading actual data and check if it is correct.
Reading Data, Part 2
How do I convert all these state changes to a vector of bits?
According to documentation, actual values are represented by the amount of time signal was in 1 state, where
So considering error in measurements, I will take 35 milliseconds as the cutoff between 1 and 0.
Also, I want to separate reading data and parsing it, so no CPU cycles will be spent during receiving on other than reading data from device.
“Raw data” will be represented with Event structure, which will have timestamp and type:
And to convert vector of events to vector 1 or 0s pretty handy .windows iterator method will be used
Read events function should be just record timestamp of the changes and change type:
Events vector is created outside of this function, because request to device is done before, spending time creating new vector can lead to loss of events.
Combining it all together and testing
in main.rs I will declare 2 function, try_read, which will get bits we were able to pull from the device and check they can be converted to the data.:
And main function simply tries to read every 5 seconds:
Output will look like this:
As you might see, it is not always to possible to read the data, and often first attemp is ParityBitMisMatch
Note on a different approach
Pulling doesn’t look like the most performant way of reading data, and subscribing to events using .events() method suppose to be more efficient, but I was unable to get enough transitions in the callback. I haven’t investigated, why this is happening.
Conclusion
Making binary data more readable simplifies experimentation a lot!
Only tests can make “some rust code” to become “rust code you trust”.
Couple improvement can be done, such as:
Skipping 1st bit if there are 41 bits was received, as it is signal bit