My Pi Description

My Experiences With the Raspberry Pi -- Tracking My Learning -- My Pi Projects

Saturday, July 5, 2014

My 1-Wire Arduino Library for DS18B20 Temperature Sensor

This is a continuation of my last blog post.

The Include File For the 1-Wire DS18B20 Library

This 1-wire library will work equally well with an Arduino and a Gertboard.
/*
Please see Maxim's datasheet for this device to understand the
functionality of the 1-wire interface and the device itself.
http://datasheets.maximintegrated.com/en/ds/DS18B20.pdf
MJL - thepiandi.blogspot.com 07/02/14
*/
#ifndef DS18B20_INTERFACE_H
#define DS18B20_INTERFACE_H
#include "Arduino.h"
#include <avr/io.h>
/* Clock factor is to correct delayMicrosecond. For Gertboard use .67.
For Arduion use 1 */
#define CLOCK_FACTOR 1.0
/* We need to define which ATmega328 pins to use to control the
DS18B20s and the hard pull-up for parasitic operation. See Atmega328 datasheet
pages 91 and 92 register names. This controls the following 10 selections: */
/*PORT_PIN and PORT_PIN_1 are the numbers of the ATmega328 pin within the port you have
selected. The choices are 0 - 7. PB5 would be 5 */
#define PORT_PIN 0 //For DS18B20 data pins
#define PORT_PIN_1 1 //For Hard Pull-up transistor
/* DDR is the data direction register for ATmega328 pins you select.
Choices are DDRB, DDRC, and DDRD */
#define DDR DDRB //For DS18B20 data pins
#define DDR_1 DDRB //For Hard Pull-up transistor
/* PORT is the port data register controlling the ATmega328
pinS you have connected to the DS18B20 and the hard pull-up transistor.
Choices are PORTB, PORTC, and PORTD */
#define PORT PORTB //For DS18B20 data pins
#define PORT_1 PORTB //For Hard Pull-up transistor
/* PIN is the input register for the ATmega328 pin you select for the DS18B20 devices. Not necessary
for hard pull-up transistor.
Choices are PINB, PINC, and PIND */
#define PIN PINB
/*DATAMASK selects the ATmega328 pin you have selected to connect to the
DS18B20. Not necessary for hard pull-up transistor.
For example, if you are using pin PB3, use 0b00000100 */
#define DATAMASK 0b00000001
class DS18B20_INTERFACE{
public:
DS18B20_INTERFACE();
boolean calculateCRC_byte(byte inByte[], int num_bytes);
boolean initialize();
void read_rom(byte rom_value[]);
void skip_rom();
void match_rom(byte rom_value[]);
void read_scratchpad(byte scratchpad_value[]);
int convert_t(boolean parasitic, int resolution);
void write_scratchpad(byte alarm_and_configuration[]);
int search_rom(byte Rom_no[], int LastDiscrepancy);
int alarm_search(byte Rom_no[], int LastDiscrepancy);
void copy_scratchpad(boolean parasitic);
int recall_eeprom();
boolean read_power_supply();
private:
void master_write0();
void master_write1();
void write_byte(byte dataword);
byte master_read();
byte read_byte();
int search_device(byte Rom_no[], int LastDiscrepancy, byte code);
};
#endif
At the top of the file are #define statements to account for your hardware set-up. Unless you use the same hardware choices I made, you make the appropriate changes to the file and save the file.
The first #define is for CLOCK_FACTOR. I reported in an earlier post that the delayMicroseconds(), Arduino IDE, function is not accurate when using the Gertboard. I tested this thoroughly, at different values, using an oscilloscope. My results show that you must divide the desired delay by 0.67 to get an accurate delay. Consequently, if you use the Gertboard, make the clock_factor 0.67. Select 1.0 for the Arduino. The difference is caused by the 12MHz clock frequency of the Gertboard compared to 16MHz of the Arduino. The delay() function requires no correction.
The rest of the #defines relate to the selection of the two Arduino data pins you select. One pin is the data pin for the 1-wire bus, and the other is the pin that controls the transistor for parasitic power. I will have a post devoted to the parasitic power transistor circuit. Hopefully, my comments, in the file, are enough to guide you through the selections.
If you use the Gertboard you can take the pin names as they appear by the J28 connector (PB1 for example). These are the same designations as used in the ATmega's data sheet, and, more importantly, the same designation as used in the avr/io.h include file. If you use the Arduino, where pin numbers are 0 to 13, you will need to convert them to the ATmega numbers.
Arduino ATmega Arduino Atmega Arduino Atmega
0 PD0 5 PD5 10 PB2
1 PD1 6 PD6 11 PB3
2 PD2 7 PD7 12 PB4
3 PD3 8 PB0 13 PB5
4 PD4 9 PB1
The rest of the file declares the functions in the library source file. These are the public functions, those that can be called from a sketch, and the private functions, those that are not available from sketches.

The Soruce File For the 1-Wire DS18B20 Library

/*
Please see Maxim's datasheet for this device to understand the functionality of
the 1-wire interface and the device itself.
http://datasheets.maximintegrated.com/en/ds/DS18B20.pdf
To understand the functions in this file, it important that you know how
ATmega328 register programming is done.
Please visit my blog entry of January 19, 2014, "Gertboard - Programming With
ATmega Registers", subtitled "Arduino IDE Built-in Functions Vrs. Direct Register
Programming"
http://thepiandi.blogspot.com/2014/01/gertboard-programming-with-atmega.html
MJL - thepiandi.blogspot.com 07/02/14
*/
#include "One_wire_DS18B20.h"
#include "Arduino.h"
DS18B20_INTERFACE::DS18B20_INTERFACE(){
}
/* THESE FUNCTIONS ARE PRIVATE AND ARE CALLED BY THE PUBLIC FUNCTIONS BELOW*/
//-----------------------master_write0()-----------------------------------
/* A "0" is written to the DS18B20 by the pulling the data pin low for 60usec.
and then releasing it (make data pin an input). An additional 7usec. makes
this operation one time slot long. */
void DS18B20_INTERFACE::master_write0(){
PORT &= ~(1 << PORT_PIN); //Data pin = 0
DDR |= (1 << PORT_PIN); //Data pin is OUTPUT
delayMicroseconds(60 / CLOCK_FACTOR);
DDR &= ~(1 << PORT_PIN); //Data pin is INPUT
delayMicroseconds(7 / CLOCK_FACTOR);
}
//-------------------------master_write1()------------------------------------
/* A "1" is written to the DS18B20 by the pulling the data pin low for 13usec.
and then releasing it (make data pin an input). An additional 54usec. makes
this operation one time slot long. */
void DS18B20_INTERFACE::master_write1(){
PORT &= ~(1 << PORT_PIN); //Data pin = 0
DDR |= (1 << PORT_PIN); //Data pin is OURPUT
delayMicroseconds(13 / CLOCK_FACTOR);
DDR &= ~(1 << PORT_PIN); //Data pin is INPUT
delayMicroseconds(54 / CLOCK_FACTOR);
}
//-------------------------write_byte()-----------------------------------------
/* Writing a byte is done least significant bit first. The least significant bit
is tested by seeing if the byte is even or odd using modulo 2. If odd,
master_write1 is called, if not, master_write0 is called. A right shift is done
to move all the bits down one position. This is repeated until all 8 bits are handled. */
void DS18B20_INTERFACE::write_byte(byte dataword){
for (int i=0; i<8; i++){
if (dataword % 2){
master_write1();
}
else{
master_write0();
}
dataword >>= 1;
}
}
//--------------------------master_read()---------------------------------------
/* This constitutes a read timeslot and reads one bit sent by the DS18B20. The
data pin is pulled low for 5 usec. then released. Data pin is read 6 usec. later
to determine if the DS18B20 has pulled it low or left it high. Time slot continues
for 50usec. more. */
byte DS18B20_INTERFACE::master_read(){
byte ret_val;
PORT &= ~(1 << PORT_PIN); //Datapin = 0
DDR |= (1 << PORT_PIN); //Datapin is OUTPUT
delayMicroseconds(5 / CLOCK_FACTOR);
DDR &= ~(1 << PORT_PIN); //Datapin is INPUT
delayMicroseconds(6 / CLOCK_FACTOR);
ret_val = PIN & DATAMASK; //Read input register and mask for your pin
delayMicroseconds(50 / CLOCK_FACTOR);
return ret_val;
}
//-------------------------read_byte()------------------------------------------
/* Since the DS18B20 transmits least significant bit first, each bit is entered
as a "0" or "1" in the most significant bit position and all the bits are shifted
right one position. Repeated until all 8 bits are read.*/
byte DS18B20_INTERFACE::read_byte(){
byte dataword = 0;
for (int i=0; i<8; i++){
dataword >>= 1;
if (master_read()){
dataword += 0b10000000;
}
}
return dataword;
}
//-------------------------------search_device()-----------------------------
/*
Function called by search_rom() and alarm_search(). This does the actual work
required of these functions. The only difference between search_rom() and
alarm_search() is they pass different command codes, or what Maxim calls protocol.
This function runs the basic algorithm as outlined in Maxim's Application
Note 187, "1-Wire Search Algorithm". The variable names, as much as possible
are the same as found in the application note. Function finds one device and stores
it's code in the array Rom_no. It returns the variable LastDescrepanvy.
If this function is called from search_rom(), the application script will call
this function once for each device on the bus. If called from alarm_search(),
the application script will call this function once for each device whose
temperature is out of tolerance. However, it will be called at least once.
The eight byte array, Rom_no[] will contain the ROM code of the device that was
found by the scan. The same Rom_no[] is passed into the function when it is called
by the application code. The other variable, LastDiscrepancy is passed recursively
into the function and is returned by the function when it exits. When LastDiscrepancy
returns a zero to the application script, it signals the script that no other devices
will be found.
The first time the function is run, Rom_no must be set to eight zeros and LastDiscrepancy
to 0.
*/
int DS18B20_INTERFACE::search_device(byte Rom_no[], int LastDiscrepancy, byte code){
int Rom_byte_mask;
int Rom_bit_mask;
boolean LastDeviceFound = false;
byte search_direction;
int bit_number;
int last_zero;
byte id_bit, cmp_id_bit;
Rom_byte_mask = 0; //To access the correct byte in Rom_no
Rom_bit_mask = 0; //To access correct bit in Rom_no
last_zero = 0;
bit_number = 1;
write_byte(code); //send code for search command
do { //Do Until All 64 Bits Are Accessed
id_bit = master_read();
cmp_id_bit = master_read();
if (id_bit == cmp_id_bit){ //There is a discrepancy
if (bit_number == LastDiscrepancy){
search_direction = 1;
}
else if (bit_number > LastDiscrepancy){
search_direction = 0;
last_zero = bit_number;
}
else{ // bit_number must be less that LastDiscrepancy
//Take on the value of same bit in Rom_no
if (Rom_no[Rom_byte_mask] & (1 << Rom_bit_mask)){
search_direction = 1;
}
else{
search_direction = 0;
last_zero = bit_number;
}
}
}
else{ //There is no discrepancy
search_direction = id_bit;
}
//We are at the compare stage, write to the device, one bit. Some devices may drop out
if (search_direction){
master_write1();
}
else{
master_write0();
}
//Now write that same bit into ROM ID at the present byte and bit position
if (search_direction){
Rom_no[Rom_byte_mask] |= (1 << Rom_bit_mask); //Set bit to 1
}
else{
Rom_no[Rom_byte_mask] &= ~(1 << Rom_bit_mask)); //Set bit to 0
}
Rom_bit_mask++; //Shift to the next bit position in the ROM ID
//If we have gone beyond the byte boundary, move to the next byte
if (Rom_bit_mask > 7){
Rom_byte_mask++;
Rom_bit_mask = 0;
}
bit_number++;
}while (bit_number < 65);
LastDiscrepancy = last_zero;
return LastDiscrepancy;
}
/* THE FOLLOWING FUNCTIONS ARE PUBLIC MEANING THEY CAN BE CALLED BY THE USER'S CODE*/
//-----------------------calculateCRC_byte()------------------------------------
/* This function can be called by the user but is not a ROM Command or a
Function Command as outlined in the DS18B20 datasheet.
The CRC is checked after reading the ROM code and reading the scrathpad.
The DS18B20 device appends its internal calculation of the CRC as the last byte
of the ROM code and the scratchpad. If the result of the CRC is "0", after
passing in all the bits the CRC passes. */
boolean DS18B20_INTERFACE::calculateCRC_byte(byte inByte[], int num_bytes){
byte workingByte[num_bytes];
boolean failure = false;
int crc = 0;
for (int i = 0; i < num_bytes; i++){
workingByte[i] = inByte[i];
for (int j = 0; j < 8; j++){
if ((crc % 2) ^ (workingByte[i] %2)){
crc ^= 0b100011000;
}
crc >>= 1;
workingByte[i] >>= 1;
}
}
if (crc) failure = true;
return failure;
}
/*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ROM Commands >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/
//-----------------------------initialize()-----------------------------------------
/* ROM Command called by user.
Initialization precedes all command sequences. The data pin is pulled low for
500us. then the pin switched to input. After 75usec. the data pin is read.
If "0" is read, initialization passes. The entire initialization process is
programmed to take 1ms. */
boolean DS18B20_INTERFACE::initialize(){
boolean failure = false;
int ret_val;
PORT &= ~(1 << PORT_PIN); //Data pin = 0
DDR |= (1 << PORT_PIN); //Data pin is OUTPUT
delayMicroseconds(500 / CLOCK_FACTOR);
DDR &= ~(1 << PORT_PIN); //Data pin is INPUT
delayMicroseconds(75 / CLOCK_FACTOR); //Making this 600 will cause failure.GOOD
ret_val = PIN & DATAMASK; //Read input register and mask for your pin
delayMicroseconds(425 / CLOCK_FACTOR);
if (ret_val == 1) failure = true; //if true device did not pull data pin down
return failure;
}
//--------------------------read_rom()-----------------------------------------
/* ROM Command called by user. Preceded by initialize().
WARNING: Do not use this function if more than one device is connected to the
1-wire bus.
The ROM code is 8 bytes long including the CRC byte. Eight bytes are read from
the DS18B20. Each byte becomes an element in an array. The CRC should be checked
after this function.
*/
void DS18B20_INTERFACE::read_rom(byte rom_value[]){
write_byte(0x33);
for (int i=0; i<8; i++){
rom_value[i] = read_byte();
}
}
//--------------------------skip_rom()-----------------------------------------
/* ROM command called by user. Preceded by initialize().
Skip ROM allows commands to be sent without sending the ROM contents. Useful
for commanding all devices perform temperature conversion if followed by
convert_t(). */
void DS18B20_INTERFACE::skip_rom(){
write_byte(0xCC);
}
//--------------------------match_rom()-----------------------------------------
/* ROM command called by user. Preceded by initialize().
Match rom precedes most Function Commands. It is the method that selects the
device the user wishes to address. The only alternative ROM commands that can
precede a Function Command are skip_rom(), if you want to address multiple
devices or read_rom(), used if there is no more than one device on the bus. */
void DS18B20_INTERFACE::match_rom(byte rom_value[]){
write_byte(0x55);
for (int i=0; i<8; i++){
write_byte(rom_value[i]);
}
}
//--------------------------search_rom()-----------------------------------------
/* ROM command called by user. Preceded by initialize().
Real work of this function is done by search_device.
*/
int DS18B20_INTERFACE::search_rom(byte Rom_no[], int LastDiscrepancy){
LastDiscrepancy = search_device(Rom_no, LastDiscrepancy, 0xF0);
return LastDiscrepancy;
}
//--------------------------alarm_search()---------------------------------------
/* ROM command called by user. Preceded by initialize().
Real work of this function is done by search_device.
*/
int DS18B20_INTERFACE::alarm_search(byte Rom_no[], int LastDiscrepancy){
LastDiscrepancy = search_device(Rom_no, LastDiscrepancy, 0xEC);
return LastDiscrepancy;
}
/*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Function Commands >>>>>>>>>>>>>>>>>>>>>>>>*/
//-------------------------convert_t()------------------------------------------
/* Function Command called by user. Must be preceded by match_rom(), skip_rom(),
or if there is only one device on the bus, read_rom(). Generally, a read_scratchpad()
is called after this function.
This commands the DS18B20 to make a voltage reading, convert that reading to
temperature, and store that value in the two most significant bytes of the scratchpad.
For non-parasitic operation, after writing the code to command the convert temperature
process, a read timeslot is initiated every ms. Once a "1" is received, the function
exits with the number of ms. returned to the calling program. If the number of ms.
is zero (data pin never went to "0"), the conversion never took place. If it stays "0"
for more than 2 seconds, the function does not wait any longer and exits.
The conditions of zero time or time greater than 2000 should be tested
for by the calling program and declared a failed situation.
For parasitic operation, the DS18B20'S data pin must be held high for the duration of the
maximum time specified for each resolution. The resistor pull-up does not provide
sufficient current for that operation so a transistor is turned on that pulls the DS18B20's
data pin up to Vcc. This is called a hard pull-up. In parasitic mode, the function
does not return anything.
*/
int DS18B20_INTERFACE::convert_t(boolean parasitic, int resolution){
if (parasitic){
//Data pin for hard pull-up pulled low
PORT_1 &= ~(1 << PORT_PIN_1);
write_byte(0x44);
//Pin controlling hard pull-up is output turning transistor on
DDR_1 |= (1 << PORT_PIN_1);
delay(94 * pow(2, (resolution - 9)));
//Pin controlling hard pull-up is input turning transistor off
DDR_1 &= ~(1 << PORT_PIN_1);
}
else{
byte value;
int count = -1;
write_byte(0x44);
do{
count++;
if (count > 2000){
break;
}
value = master_read();
delay(1);
} while (value == 0);
return (count);
}
}
//---------------------------read_scratchpad()---------------------------------
/*Function Command called by user. Must be preceded by match_rom(), or if there is
only one device on the bus, skip_rom(), or read_rom().
read_scratchpad reads 9 bytes from the DS18B20. The last byte is the CRC byte.
The 9 bytes become elements in an array. The CRC should be checked after this
function. */
void DS18B20_INTERFACE::read_scratchpad(byte scratchpad_value[]){
write_byte(0xBE);
for (int i=0; i<9; i++){
scratchpad_value[i] = read_byte();
}
}
//-------------------------------write_scratchpad()-----------------------------
/* Function Command called by user. Must be preceded by match_rom(), skip_rom(),
or if there is only one device on the bus, read_rom().
This command writes three bytes to the scratchpad. The first byte is the upper
temperature threshold, the second is the lower temperature threshold, and the third
is the configuration byte. The configuration byte has only two useful bits and
they are used to set the resolution as 9, 10, 11, or 12 bits.
*/
void DS18B20_INTERFACE::write_scratchpad(byte alarm_and_configuration[]){
write_byte(0x4E);
for (int i=0; i<3; i++){
write_byte(alarm_and_configuration[i]);
}
}
//-------------------------------copy_scratchpad()-------------------------------
/* Function Command called by user. Must be preceded by match_rom(), skip_rom(),
or if there is only one device on the bus, read_rom().
Copies the contents of bytes 2, 3, and 4 of the scratchpad to the device's eeprom.
Those three bytes represent, respectively, upper alarm temperature, lower alarm
temperature, and the configuration register that contains the resolution.
For non-parasitic mode it is enough to just send the command. However, for parasitic
mode, it is necessary to use the hard pull-up to pull the data pin to the positive
rail for 10ms. The hard pull-up consists of the pnp transistor controlled by the
ATmega device. This will be done in the same way as is done in the convert_t() function.
*/
void DS18B20_INTERFACE::copy_scratchpad(boolean parasitic){
if (parasitic){
//Data pin for hard pull-up pulled low
PORT_1 &= ~(1 << PORT_PIN_1);
write_byte(0x48);
//Pin controlling hard pull-up is output turning transistor on
DDR_1 |= (1 << PORT_PIN_1);
delay(10); //delay 10ms.
//Pin controlling hard pull-up is input turning transistor off
DDR_1 &= ~(1 << PORT_PIN_1);
}
else{
write_byte(0x48);
}
}
//-------------------------------recall_eeprom()--------------------------------
/* Function Command called by user. Must be preceded by match_rom(), skip_rom(),
or if there is only one device on the bus, read_rom().
Copies the alarm high, alarm low, and configuration register from the DS18B20's
eeprom to the device's scratchpad.
After writing the command, a read timeslot is initiated every ms. Once a "1" is
received, the function exits with the number of ms. returned to the calling
program. If the number of ms. is zero (data pin never went to "0"), the process never
took place. If it stays "0" for more than 2 seconds, the function does not wait
any longer and exits.
The conditions of zero time or time greater than 2000ms. should be tested
for by the calling program and declared a failed situation.
It may be rare that it is necessary to call this function as the device preforms this
operation upon power-up.
*/
int DS18B20_INTERFACE::recall_eeprom(){
byte value;
int count = -1;
write_byte(0xB8);
do{
count++;
if (count > 2000){
break;
}
value = master_read();
delay(1);
} while (value == 0);
return (count);
}
//-------------------------------read_power_supply()-----------------------------
/* Function Command called by user. Must be preceded by match_rom(), skip_rom(),
or if there is only one device on the bus, read_rom().
Purpose is to determine if a device is connected to the bus using parasitic power.
User may determine if ANY device is connected using parasitic power by preceding call
to this function by calling skip_rom(). User may also determine if a particular
device is connected using parasitic power by preceding call to this function with
match_rom(). If only one device is connected to the bus, read_rom() could preceed the
call to this function. That has not been tested because it is kind of silly. User
should know how his one device is connected without this function.
After issuing command function calls master_read() which is the equivalent of
read timeslot (datasheet terminology). Any device that is connected for parasitic
power will pull the bus low making master_read() return "0" or false. If all devices
are conventionally powered, master_read() will return a non "0", or true.
Returns boolean true or false according to the master_read() results.
*/
boolean DS18B20_INTERFACE::read_power_supply(){
write_byte(0xB4);
return(master_read());
}
Most of the functions are short. Their comments are longer than the code. You will have to be familiar with ATmega register programming and bit manipulation. I have tutorial on useing direct register programming here.
In the next several posts, I'll go into some detail on the search_device() and calculateCRC_byte() functions. But, first, I need to address parasitic power and what to do about it. That's next.

No comments:

Post a Comment