Scaling analog values

Most products reading analog values return a 16-bit integer in a limited range defined by the hardware. For example, most Digi products return a value from 0 to 1023 which represents a reading within 0 to 10.25v or 0 to 23.5mA. Yet knowing that your room temperature is 675 binary or 4.56 volts or 17.2mA is not very useful.

You will want to SCALE this value to something more useful like 76.3 degrees Fahrenheit or 297.6 degrees Kelvin.

While this conversion is not difficult, it is complex to envision since you need to fit two distinct line equations to obtain a result. Below is a simple example which does this in two steps. First it converts the raw binary input into a percentage range 0-100% represented as 0.0 to 1.0 appropriate for the sensor (this might go below 0% or above 100%). Second, it converts the range percentage into the actual published sensor value.

An example

The sensor

A Minco model TT321PD1M precision RTD temperature probe with 4-20mA interface defines 4mA as -58.0 DegF and 20mA as 122.0 DegF. So the Digi product is placed into Current Loop mode and returns a value like 17.3mA - how is this converted to DegF?

The first step is to stop thinking of the input as 17.3mA, but to instead think of it as a percentage of the vendor-defined range. In this example the range is from a 0% signal representing -58.0 DegF, while a 100% signal represents 122.0 DegF. The table below shows the theoretical relationship of 4-20mA on a Digi product which converts 0-23.5mA to binary 0-1023.

mA percent of 0-23.5mA percent of 4-20mA binary in 0 to 1023 Example DegF
0 mA 0% -25% 0 -103 DegF
4 mA 17% 0% 174 -58 DegF
8 mA 34% 25% 348 -13 DegF
12 mA 51% 50% 522 +32 DegF
16 mA 68% 75% 697 +77 DegF
20 mA 85% 100% 871 +122 DegF
24 mA 102% 125% 1045 +167 DegF

Note While this allows defining temperatures 'out-of-range' such as -103 DegF at 0mA, users should never make active use of sensor data when outside of their published range, which in this example is 4-20mA and -58.0 to +122 DegF.

Example 'Terms' for the scaling

It is convenient to think of the conversion as using a five-element tuple of the form (raw_zero, raw_span, user_zero, user_span, units_str), where:

Some example term tuples used in the code sample below:

(0, (1023-0), 0, (10.25-0), 'V')

Binary 0 is called 0% while binary 1023 is called 100%. 0-100% is returned as 0.0V to 10.25V

(0, (1023-0), 0, (23.5-0), 'mA')

Binary 0 is called 0% while binary 1023 is called 100%. 0-100% is returned as 0.0mA to 23.5mA

(174, (871-174), 0, (100-0), 'prc')

Binary 174 is called 0% while binary 871 is called 100%. 0-100% is returned as 0prc to 100prc

(174, (871-174), -58.0, (122.0-(-58.0)), 'DegF')

Binary 174 is called 0% while binary 871 is called 100%. 0-100% is returned as -58.0DegF to 122.0DegF

Implementation Note: It may be easier to allow customers to configure the 'zero' and 'full' values, then calculate the 'span' during start-up. However, recalculating the 'span' as '(full-zero)' every processing cycle is a waste of CPU resources, so retain the calculated span for reuse. The Python code sample included here assumes the 2nd and 4th terms are the span, not the full.

The Python scale code

def sample():

    # Note: the 'get_raw_analog(x)' call is imaginary
    #       Substitute in the call required to obtain a raw binary number

    # Report Analog One as 0-10.25v
    trm_1 = (0, 1023, 0, 10.25, 'V')
    ain_1 = scale_value(get_raw_analog(0), trm_1)
    print 'Analog 1 was %0.2f %s' % (ain_1, trm1[4])

    # Report Analog Two as 0-23.5mA
    trm_2 = (0, 1023, 0, 23.5, 'mA')
    ain_2 = scale_value(get_raw_analog(1), trm_2)
    print 'Analog 2 was %0.2f %s' % (ain_2, trm2[4])

    # Report Analog Three as 0-100% for 4-20mA (might be < 0% or > 100%)
    # Note that we shift the expected raw range away from 0-1023 to 174-871
    trm_3 = (174, (871-174), 0, 100, 'prc')
    ain_3 = scale_value(get_raw_analog(2), trm_3)
    print 'Analog 3 was %0.2f %s' % (ain_3, trm3[4])

    # Put it all togather
    # Report Analog Four as DegF for the example RTD 4-20mA sensor
    trm_4 = (174, (871-174), -58.0, (122.0-(-58.0)), 'DegF')
    ain_4 = scale_value(get_raw_analog(3), trm_4)
    print 'Analog 4 was %0.2f %s' % (ain_4, trm4[4])

    return


def scale_value(raw_in, terms):
    """Scale the raw_in value (assumed raw int) to user-units float
    terms = (raw_zero, raw_span, user_zero, user_span, units_str)"""

    # msg = ' >> terms = %s' % terms
    scaled = None

    if len(terms) > 2 and terms[1] != 0:
        # first we calculate 0-100% as 0.0 to 1.0
        # so y = (raw_in - raw_zero) / raw_span
        scaled = float(raw_in - terms[0]) / terms[1]
        # msg += ' prc=%0.4f' % scaled
        
        # second we optionally change to user units
        if len(terms) > 4 and terms[2] is not None:
            scaled =  terms[2] + (scaled * terms[3])
            # msg += ' usr=%0.4f%s' % (scaled,terms[4])

    # print msg
    return scaled