Monday, June 18, 2012

Two's Complement Conversion in a Microcontroller

Recently I've been working on a project where I use a micro-controller to take temperature measurements and then log the data to an SD card.  So essentially it's a data logger using an ATMEGA128, a little bit of overkill, but I already have a dev board running that chip at 16MHz, so it's ideal to hit the ground running.

Getting the SD card up and running is a bit problematic but I'm making headway.  I have however managed to get the temperature sensor connected to the micro-controller via a bit banged I2C interface using a library from Peter Fleury's Site.  The temperature sensor in question is the ADT75 from Analog Devices.  I don't plan to use this sensor in the final design because it isn't accurate enough, but I already had it mounted on a breakout board for a project I did at Uni.  So for now I can get things up and going and at a later point change the sensor.

ADT75 temperature sensor in operation

The ADT75 measures from -55 °C to 125 °C with a temperature resolution of 0.0625 °C.  The return data from the sensor is sent as a two's complement formatted two byte sequence.  As each measurement is only 12 bits, the last 4 bits of the last byte are always zero and are ignored.  It would be easy to just log the hex code from the sensor to the file, but to make things more user friendly I plan to record the hex code and the human readable format.  During testing I can also output this data to the 16x2 character LCD I have connected, once again via a library from Peter Fleury's Site.

To convert from the two's complement format to a human readable decimal format is a relatively simple process. The first step is to test if the number is negative.  The most significant bit of the 16 bit temperature reading will be 1 if the reading is negative.  This is tested by masking the reading with 0x8000.

positive reading
  0xxxxxxx xxxx0000
& 10000000 00000000
= 00000000 00000000

negative reading
  1xxxxxxx xxxx0000
& 10000000 00000000
= 10000000 00000000

If a negative measurement is detected a negative symbol is added to a character buffer, and the measurement is then negated.  This is done by toggling the bits in the reading by XORing them with 0xFFF0 and then adding 1.  It is important to remember that the 4 least significant bits are to be ignored, therefore we actually add 0x0010.

  1xxxxxxx xxxx0000
^ 11111111 11110000
= 0yyyyyyy yyyy0000
+ 00000000 00010000
= 0zzzzzzz zzzz0000

Any negative measurements will be now be positive.  The next step is to convert the positive readings to a decimal representation.  The 8 most significant bits will now contain the integer part of the temperature measurement and can be converted to a string and added to the character buffer followed by a decimal point. The fractional part of the reading is now contained in the the upper 4 bits of the lower byte.  There are a couple of ways to convert this to decimal, you could use maths functions, or use a look up table as I am going to do.  The first step is to mask off the required bits with 0x00FF and shift them to the lower 4 bits.

  0zzzzzzz zzzz0000
& 00000000 11111111
= 00000000 zzzz0000
= 00000000 0000zzzz

We are now left with a number between 0 and 15.  As this is the fractional part of the reading, it represents how many 16th make up the numbers after the decimal point.  The decimal representation can be calculated by multiplying this number by one sixteenth, which is 0.0625.  However, as there are only 16 different possibilities it's more efficient to pre-calculate them and store them in memory.  It will soon become apparent why, but we are lucky the decimal values are all able to be represented with 4 digits.  The string below contains the 16 different 4 digit decimal sequences.  Each sequence is marked by the ^ symbol.

^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^

For example if the value is 7, the fraction to calculate is 7/16.  To retrieve the answer go the the eighth sequence and read the next 4 characters.  We go to the eighth sequence because zero is included.  For a zero indexed array this sequence will start at the position 7*4 = 16. The string returned will be 4375 giving an answer of 0.4375.  These digits can then be added to the character buffer, followed by a null terminator.

We can take a short-cut to calculate the index position.  The position in the array is found by multiplying the fraction required by 4, but in the previous step we did a right shift by 4 bits which is equivalent to division by 16, by combining these two steps we can replace the right shift by 4 with a right shift by 2.

index position
  0zzzzzzz zzzz0000
& 00000000 11111111
= 00000000 zzzz0000
= 00000000 00zzzz00

My function to complete this operation is listed below.  It takes a pointer to a uint16_t temperature measurement and a pointer to a character buffer where the result can be stored.  It is important to make sure that buffer is large enough to hold the result otherwise a buffer overflow can occur.  A buffer 9 characters long should be enough to hold the conversion.

Negative Value

Positive Value

void binaryToDecimal(uint16_t * Temp, unsigned char * tempString){
    static const char mantissaLookup[64]  PROGMEM = {
        '9','3','7','5'};    //lookup string for mantissa of multiples of (1/16)

    uint16_t  temperature = *Temp;                    //pointer to temperature variable
    unsigned char buffer[16];                        //temporary buffer for string conversion
    uint8_t stringIndex = 0x00;                        //string position

    if(temperature & 0x8000){                        //test if the temperature reading (2's complement) is negative 
        temperature ^= 0xFFF0;                        //invert bits in reading.  The 4 LSBits are to be ignored
        temperature += 0x10;                        //add 1 - negative readings now turned positive. The 4 LSB are to be ignored
        tempString[stringIndex] = '-';                //add negative sign to the output string 
        stringIndex++;                                //increment the position in the string

    utoa((temperature >> 8),buffer,10);              //take the top 8 bits of the reading (integer part) and convert to a string
    uint8_t bufpos = 0;                              //buffer position
    while(buffer[bufpos] != '\0'){                   //while the null character is not reached continue
        tempString[stringIndex] = buffer[bufpos];    //add the character from the buffer to the temperature string
        bufpos++;                                    //increment the buffer position
        stringIndex++;                               //increment the position in the string

    uint16_t mantissa;                               //variable to hold the mantissa of the reading
    mantissa = ((temperature & 0x00FF) >> 2);        //mantissa is equal to (x/16), where x is held in the bits 4 highest bits of the LSByte
                                                     //to get this value, mask off the LSByte and divide by 16.
                                                     //This value is then multiplied by 4 to get the position of the digits in the lookup table
                                                     // (a>>4)<<2 = a>>2

    tempString[stringIndex] = '.';                                                    //add the decimal point to the output string
    stringIndex++;                                                                    //increment the position in the string

    tempString[stringIndex] = pgm_read_byte_near(mantissaLookup + mantissa + 0);    //put digit 1 of the mantissa in the output string
    stringIndex++;                                                                    //increment the position in the string
    tempString[stringIndex] = pgm_read_byte_near(mantissaLookup + mantissa + 1);    //put digit 2 of the mantissa in the output string
    stringIndex++;                                                                    //increment the position in the string
    tempString[stringIndex] = pgm_read_byte_near(mantissaLookup + mantissa + 2);    //put digit 3 of the mantissa in the output string
    stringIndex++;                                                                    //increment the position in the string
    tempString[stringIndex] = pgm_read_byte_near(mantissaLookup + mantissa + 3);    //put digit 4 of the mantissa in the output string
    stringIndex++;                                                                    //increment the position in the string
    tempString[stringIndex] = '\0';                                                    //add the null terminator character to the output string

When testing the temperature sensor I wanted to make sure that it was measuring and converting negative temperature readings.  To do this I used common electronics trick.  If you take a standard can of pressurised air that is used to clean keyboards and use it upside down, the propellant comes out.  There is a warning not to do this on the can because the propellant is very cold and could cause injuries.  We can use this to our advantage and give the temperature sensor a quick blast of the propellant to take its temperature below zero.  First make sure that there are no sources of ignition as the propellant is flammable.

The ADT75 measuring a negative temperature

No comments:

Post a Comment