Saturday, December 27, 2014

1 Wire Interface - DS18B20

Well well, here we go, I opened the DS18B20 datasheet, and we start rolling. Well there should be a reset / presence pulse, then we have several other to go thru.
5 years later ...
Ok, sorry got carried away, but managed to get that reset thingy. It was just making the pin output, so it can get logic low, waiting for 480 microseconds, making it input again (high-z), then waiting for another 40 microseconds or so, then reading, if it was kept high by PULL-UP resistor, then we connected something wrong, or in other words - trouble. The best case scenario is - you get the wire pulled low when you do the reading at that time.
  // send reset / presence pulses
  DDRB |= _BV(PB0); // make it output, keep it low
  PORTB &= ~_BV(PB0);
  _delay_us (480); // wait for some time
  DDRB &= ~_BV(PB0); // make it into input again
  _delay_us (60);
  if (! (PINB & _BV(PB0)) ) {
    PORTA ^= _BV(PA0);
    send_rw ("Got response ...\n\r");
  } else
    send_rw ("No response, check connections \n\r");
In code, it would look something like this. So I managed to get response message in my debug terminal when the IC was in breadboard, while it was gone, I got error message. First step done! Considering that there is still 4 main function-y things yet to go, I think making it all functions will come handy, you know, less code, less effort.
Update: Seems I forgot to mention the basic principle of 1 wire interface. You basically have a master device (e.g. MCU) and one or more slave devices. You have 1 data bus as you might have guessed, which is pulled up by a resistor (5-10k usually). So all the devices are 3 state - high, low, high impedance. You work by pulling the bus low or going hi-z. Here is the schematic for DS18B20
Spoiler: we power it normally - not the parasite power mode

Ok, so now lets see the write and read commands for 1 wire interface. I'm basically telling you what I understood from the DS18B20 datasheet.

Write command

You send data using write slots, which has a duration of 60 us. It all starts by pulling the data line low (making the pin output). And then if you transmit 1, you should get back to hi-z within 15 us, to let pull-up resistor pull the level to high. Of course to finish the slot, you should wait another ~45 us.
In case of writing 0, you should keep the data bus low for hole 60 us. To finish transmitting one bit, you should pull the bus back to high (go hi-z). And after each write slot, you should give it a recovery time at least 1 us. The code I wrote looks like..
void write_1 (void) {
  // pull the bus low
  DDRB |= _BV(PB0);
  // wait within 15 us
  _delay_us (10);
  // go hi-z
  DDRB &= ~_BV(PB0);
  // wait to finish waiting 60 us
  _delay_us (50);
}

void write_0 (void) {
  // pull the bus low
  DDRB |= _BV(PB0);
  // wait 60 us
  _delay_us (60);
  // go hi-z
  DDRB &= ~_BV(PB0);
}
This functions will transmit either 1 or 0. Just one bit, so in order to transmit 8 bits, or 1 byte, you've got to call it 8 times. So, the function for writing a whole byte would look like
void write (uint8_t data) {
  // lsb first
  register uint8_t i = 0;
  for (i=0; i<8; ++i) {
    if (data & _BV(i)) write_1();
    else write_0();
    _delay_us (1);
  }
}
Then I asked myself, will this work ? One way to check - read the datasheet. I have only one DS18B20 connected, so I didn't need to do ROM search, I could just use the Read ROM(0x33) command to get 8 bytes of info back. The 8 bytes are - family code, 6 serial number bytes, and CRC byte. But in order to receive them I need to read from the bus. So here comes the second part..

Read command

By far how I could understand - you read and write bits, separately. So read command consists of bit-bit reading. In order to start read slot, you pull the bus low, wait for at least 1 us, then go hi-z. After that you wait ~ 15 us more, and sample data. If the pin is high, you read it as 1, otherwise it's 0. And you finish the read slot waiting enough, to make the slot ~60 us. You also need at least 1 us time between reads. Well yeah, this interface is pretty slow.
uint8_t read (void) {
  uint8_t rx = 0;
  register uint8_t i=0;
  for (i=0; i<8; i++) {
    // pull the bus low
    DDRB |= _BV(PB0);
    // wait 1 us
    _delay_us (1);
    // go hi-z
    DDRB &= ~_BV(PB0);

    _delay_us (10);

    if (PINB & _BV(PB0)) {
      // rx 1
      rx |= _BV(i);
    } else {
      // rx 0
      rx &= ~_BV(i);
    }

    _delay_us (50);
  }
  
  _delay_us (2);
  return rx;
}
Yeah, in case I forgot to mention, all transmissions are LSB first. So we read bit by bit to read a whole byte by that function. After that, I just tried sending Read ROM(0x33) and looking at output.
  // ROM commands
  
  write (0x33); // read rom (64 bit)

  family_code = read ();
  debug_out ("FC: 0x%02X", family_code);
  for (i=0; i<6; ++i) {
    serial_code[i] = read();
    debug_out ("Serial code [%u]: 0x%02X", i, serial_code[i]);
  }
  CRC_byte = read();
  debug_out ("CRC byte: 0x%02x", CRC_byte);
The output was this.

Meaning, we managed to establish connection! Now by following the protocol we should be able to get temperature readings.. But.. There is yet one 'but' I want to see. If I indeed got the address correctly, I should be able to do the checking that the datasheet says (page 6).

CRC checking

So the first byte is indeed 28h - the family code is correct. To check the next 64 bytes, we'll need to use the cyclic redundancy check (CRC byte) that we got in the last place. Now looking at the datasheet, there is this shift register made generator.. You must realize that this step is important, as with it you can somewhat see if that data was corrupted, and if it was, ask for new data.

Now let's see how we can do the checking with minimum effort..
Some time later
[added later] I wrote another post dedicated to CRC checking, turn's out there is a pretty handy library in avr-libc for that. Link can be found here.

Once again - RESET / PRESENCE

After looking at the datasheet once again, I realize that this step is the first step to every command you send. So to write less code, we will turn this into a function.
void reset_presence (void) {
  // send reset / presence pulses
  DDRB |= _BV(PB0); // make it output, keep it low
  PORTB &= ~_BV(PB0);
  _delay_us (480); // wait for some time
  DDRB &= ~_BV(PB0); // make it into input again
  _delay_us (60);

  if (! (PINB & _BV(PB0)) ) {
    PORTA ^= _BV(PA0);
    send_rw (">>> Got presence pulse ...\n\r");
  } else
    send_rw (">>> No response, check connections \n\r");
}
So the steps for sending a command are:
  1. Reset / Presence pulses - if we don't get presence pulse, there is a connection problem
  2. ROM command - like select certain device, or skip ROM selection
  3. The function command - e.g. Convert temperature.
The only exceptions are Read ROM and Alarm Search commands. After those, you can't send any functioning commands.

Temperature Reading!

Ok let's finish this up by finally getting the temperature on our terminal screen.
As it was described above, we should do the reset thingy, then a ROM command, then some function command. Assuming that we don't want to change any alarms or configuration settings (too much effort), we will go all lazy and just get temperature in 12 bit mode. Our steps will be:
  1. Reset/Presence & Read ROM
  2. Reset/Presence & Match ROM & Convert T
  3. Reset/Presence & Match ROM & Read Scratchpad
Where with first step we just get the address of our slave device, then we select it, telling it to start converting temperature. We wait until it finishes, then we select him again and get the data out of it's scratchpad - the place where it holds results (and much more). I know, it is weird that we use Read ROM and Match ROM, because in this example we have single slave IC, and it is such a drag, but just to make sure everything we are doing is right (kindof). So less talk more code.

Step 1

Yess, you guessed it, the part from above, where we store family_code, serial_code, and CRC_byte. No need to rewrite it here.

Step 2

  // send reset / presence pulses .. again
  reset_presence ();

  // send match ROM (0x55) command
  debug_out ("Sending MATCH ROM (0x55) command..");
  write (0x55);
  // send ROM 64 bit data
  write (family_code);
  for (i=0; i<6; ++i)
    write (serial_code[i]);
  write (CRC_byte);

  // send convert t (0x44) command
  debug_out ("Sending Convert T (0x44) command..");
  write (0x44);
Important - after convert-t command, we should either wait for some time, hoping it will finish, or keep asking if it did or not. It will read 0 until it's doing something, and 1 when it will finish. So..
  while (read() == 0) _delay_us (1);
  debug_out ("Finished converting temperature..");

Step 3

And yet again, yes, you guessed it.. or not. The Reset/Presence & Match ROM part is same from above. The Read Scratchpad will return back 9 bytes, first two of which will be the data we are after.
  // send READ SCRATCHPAD (0xBE)
  debug_out ("Sending READ SCRATCHPAD (0xBE) command..");
  write (0xBE);

  uint8_t scratchpad[8];
  for (i=0; i<9; ++i) {
    scratchpad[i] = read();
    debug_out ("Scratchpad[%u] = 0x%02x", i, scratchpad[i]);
  }
  debug_out ("Finished READ SCRATCHPAD (0xBE).");
After all this changes, my terminal looked something like this

NOW! To wrap it all out, we have those two bytes, which describe the temperature around. I am not even sure if this part works correctly, but I sure hope so.
  double temperature = 0;
  uint8_t temp = 0;
  temp |= (scratchpad[1]&0x03);
  temp <<= 4;
  temp |= ((scratchpad[0]&0xF0)>>4);

  temperature = temp;
  for (i=0; i<4; i++)
    if (scratchpad[0] & _BV(i))
      temperature += 1.0/(1<<(4-i));
  
  if (scratchpad[1] & 0xF0) temperature *= -1;

  send_rw (">>> Calculated temperature: %.4f (c)", temperature);
I basically take the bytes, and convert them into a floating number, and send it back to the terminal. And I get:
>>> Calculated temperature: 16.8125 (c)
All the code together with the makefile can be found here.
However, sorry, this didn't turn out as well as I expected it to be, it's too messy, but hope you found it interesting and entertaining .. If there was anybody out there reading this. I'll see when I write a library for this one, and it goes into To post list, together with Encoder library. And that's all for this time. Later!

No comments:

Post a Comment