My most recent posts have been about making temperature measurements with the Maxim/Dallas DS18B20 temperature sensors. I have an enclosure with a 2 line by 16 character display that can connect to many of these sensors.
If I want to go beyond merely displaying the temperature, and wish to record and graph the temperature, I wirelessly transmit the data from the enclosure to my Raspberry Pi. Consequently, I need some sort of transmitter and receiver. The transmitter and receiver I chose operate at 434MHz and are small and simple. Both are available from Sparkfun Electronics.
I don't transmit directly to the Pi because I want the receiving device to be dedicated to processing the received data, A microcontroller is ideal for that type of task. A multitasking microprocessor, like the Pi, cannot do this very well - it doesn't focus on one task. The Pi, however, is really good at logging and storing data. My Pi has a marvelous capability to graph temperatures (See my posts about RRDTool).
My receiver connects to my Gertboard, which, in turn, connects to my Pi. The Gertboard has the same microcontroller device as an Arduino Uno: an Atmel ATmega328P. My temperature measuring enclosure also has the same microcontroller.
My next post will tell how my data gets from the Gertboard to the Raspberry Pi. It's an interesting story and deserves a post of its own.
Asynchronous Data Transfer
I am sending data asynchronously, it can start at any time. The receiver must be able to find the start of a burst of data. For that to happen, I precede the data with a synchronization pattern. This pattern is chosen so that it can never be confused with the actual data. I accomplish this by encoding the actual data in a way that there are never more than two logic 1's or two logic 0's together. My synchronization pattern, then, must have more than two 1's and/or two 0's, together.
I use Manchester encoding with my data, which is a standard protocol. Every logic 1 in the data is replaced with a 01, while a logic 0 becomes a 10. You can see how this avoids more than two 0's or 1's together. For example: 0000 becomes 10101010. Only where there is a change in logic level will you find two 0's or 1's together. For example: 001100 becomes 101001011010.
There is a good reason for avoiding long strings of 1's or 0's: the receiver will see this an an interruption of data. The receiver has an automatic gain control, and if it sees no data, it will increase its gain to the maximum. The receiver will then output rail to rail thermal noise (See the beginning of the yellow scope trace below).
The disadvantage of this scheme is that I must transmit two bits for every single bit of data.
Here is what the start of my data looks like on the scope:
The upper trace (red) is the data to the transmitter, while the lower trace (yellow) is the received data at the Gertboard.
After every burst of transmitted data, there is a lapse of several hundred milliseconds (transmitter outputs 0V). During this time one if the temperature sensors makes its measurement and converts the measurement to two bytes of digital data. At some point during this dead time, the receiver's automatic gain control goes to its maximum. You can see the resulting noise in the beginning of the trace of the receiver's output.
You can clearly see the two synchronization pulses in both the transmitted and received data. The synchronization pulses consist of four high bits followed by four low bits. The synchronization pulses are preceded by a 101010 pattern. The receiver will recognize this pattern as actual data and will adjust its automatic gain control. There is 10 pattern following the synchronization pulses. The start of the Manchester encoded data follows that.
My Data
I am sending 20 bytes of data with each measurement:
- byte 0: Number of sensors being accessed
- byte 1 and 2: From the DS18B20 Scrathpad - the measured temperature
- byte 3: From the DS18B20 Scrathpad - Upper temperature alarm. Previously set by the user
- byte 4: From the DS18B20 Scrathpad - Lower temperature alarm. Previously set by the user
- byte 5: From the DS18B20 Scrathpad - Resolution (9, 10, 11, or 12 bits). Previously set by the user
- byte 6 through 17: From the ATmeta328P EEPROM - Sensor Description. Previously set by the user
- byte 18: The number of the sensor as determined by its position in the ATmega328P's EEPROM.
- byte 19: Cyclic Redundancy Check (CRC) byte calculated from the previous 19 bytes.
Except for byte 0, each burst of data contains the data from one temperature sensor. Each sensor is accessed, in turn, before starting with the first sensor, again.
Transmit Code
The following is a fragment of the code I usually run on my Temperature Sensor Enclosure. I say usually because I can upload other code to the box via its attached USB/FTDI port. The entire code can be seen here:
The enclosure has one permanently mounted DS18B20 temperature sensor and three receptacles for plugging in more sensors. It is possible to connect hundreds of these sensors to the enclosure (normal and parasitic power is supported). However, I only have room in the ATmega's EEPROM for the ROM codes and descriptions of 50 sensors. In the photo above, you can see I have two DS18B20 cables attached to the box. The code auto-detects the sensors when the code starts. As long as the ROM code and description of the sensor is in the EEPROM, the code will include the sensor in the measurements and in the transmitted data. If I have a new sensor, I have code to upload to the enclosure that will detect new sensors and will prompt for description, and alarm and resolution settings.
Whenever we are ready to transmit data, the function "synchronize()" is called, followed by 20 calls to function "send_data()" - one call for each data byte in the burst. The timing is established by the global variable "bit_time". It is calculated to send data at 600 baud (1200 bps - remember it takes 2 Manchester encoded bits to send one bit of data).
Note my use of ATmega register programming rather than "bitMode" and "digitalWrite". I have a whole tutorial about register programming at by blog here:
Receive Code
The following is a fragment of code that runs on my Gertboard that receives Manchester encoded data from my Temperature Sensor Enclosure. The entire code can be found here.
While the transmit code is pretty easy to figure out, the receive code is interesting and bears some analysis. First, it illustrates the use of ATmega interrupts. Not shown in the code fragment are two lines in setup():
EICRA = B00000001; // Any change will trigger interrupt
EIMSK = B00000001; // Enable INT0 interrupt
This means that any change in logic level of the ATmeta328P pin connected to the receiver circuitry will trigger an interrupt. Put another way: if the logic level changes from 0 (0V) to 1 (3.3V) or from 1 (3.3V) to 0 (0V), an interrupt is issued.
Lines 18 - 20 constitutes the Interrupt Service Routine. Whenever an interrupt is issued, the variable "found_transistion" becomes true. We set "found_transistion" false, in lines 34, 41, 66, and 75. When the "while" statements in line 35, 43, 67, and 77 are reached, processing is halted until the interrupt occurs. Using the "time" and "duration" variables and the "micro()" function, the time between interrupts (corresponds to the time between transitions) can be determined.
Analyzing The Receive Data
When sychronize() is called from the main program, it looks for a positive pulse of 4 bits in duration (plus or minus 10%). It keeps looking until it find it. If it next finds a negative pulse of 4 bits in duration (plus or minus 10%), it exits the routine. It will stay in this function until it is satisfied it has found the synchronization pattern.
Once we drop out of the sychronize() function, the manchester_data() function is called. We stay in that function until 20 bytes are received. Looking at the waveform above, we start in the function at point "c". Next we look for one more transistion (lines 66 and 67) to bring us to point "d".
We look for the actual received data by entering the outer "do" loop at line 72, performing the inner "do" loop (line 74) 20 times. The logic level of each bit of data we find is tracked by the variable "present_bit". This starts as 0 because the 10 pattern following the synchronization pulses is equivalent to a Manchester code of 0.
We are at point "d" looking for the next transition which occurs at point "e". Using the threshold of 1.5 times the time of one bit (in global variable bit_time), we determine if we have encountered a change in logic level from the original 0 to a 1. Since the time from point "d" to "e" is obviously less than the threshold, we have not encountered a change in logic level. Therefore, the first bit received is a 0. The "if/else" statements in lines 80 to 86 determine the actions taken if we do exceed, or do not exceed the threshold. Since we have not, "present_bit" does not change and the variable "no_clocks" is incremented by 1.
The variable "no_clocks" has two functions. First, it indicates when we have received a bit of data. Since we must receive two bits of Manchester encoded data for each bit of sensor data, we take no action if "no_clocks" is an odd number ("if" statements lines 87 to 90). If "no_clocks" is even, we capture the last bit by shifting the contents of variable "hold_byte" to the left, and inserting the value of "present_bit" at the least significant position of "hold_byte".
The second function of "no_clocks" is to tell us when we have received an entire byte of sensor data. This will be the case if "no_clocks" reaches 16. If that is the case, the value of "hold_byte" is transferred to the next byte of the array "frame[]". When we have received all 20 bytes of "frame[]" we exit the function.
When last we were looking at the waveform, we were at point "e". We return to the beginning of the inner "do" loop to look for the next transition (point "f"). The time from "e" to "f" is also less than the threshold. We return back to the beginning of the inner do loop and find the next transition (point "f" to "g"). This time we find the threshold is exceeded so the value of "present_bit" flips to a 1 and "no_clocks" increments by 2. I think the action is easy to follow from here.