Introduction

Clive Burke posted an excellent cold weather suggestion to modify the ICE coolant thermister circuit with a 1k resistor. This 'raises' the apparent ICE temperature so the engine can auto-stop at temperatures below 70C, saving fuel in colder climates. Since then, several of us have been investigating this approach for NHW11 Prius.

Background

The following is a discussion of how ICE coolant temperature controls engine auto-stop. From Ken@Japan's posting in Prius Technical Stuff on Japanese experiments:

NHW20 Warm-Up Cycle

Ken@Japan provided the excellent NHW20 data in this web page:

NHW11 Warm-Up Cycle

An NHW20 test shows that the engine runs ~80 seconds to bring the catalytic converter to operating temperature and put an initial charge on the ICE:


A US NHW11 requires the engine to run until the coolant reaches 70C. Like Ken@Japan's NHW20, the battery was charged in ~80 seconds but the ICE continues to run:


This warm-up fuel burn can only be mitigated by moving down the road but with no electric vehicle mode until 70C is reached.

An automated thermistor circuit was used to modify the NHW11 warmp-cycle:

The thermistor hack saves about 3.5 minutes in the warm-up, allowing the NHW11 to autostop the ICE. This circuit also avoid the ICE cooling down and startup up just to keep the coolant warm:

Principals of Operation

The engine coolant probe is connected to a circuit in the engine ECU:

All we have to do is modify the resistance of the thermister detected by the engine ECU.

Using a voltage-temperature plot from Hobbit, Ken@Japan and the maintenance manual, the voltage at each temperature is known:


All we have to do is lower the thermister resistance by putting additional resistance in parallel and the engine ECU will see a 'warmer' engine. This lead to the following approaches.

Fixed Resistor

The first experiments used fixed resistors in the 1-1.5 kohm range but there were problems. Too high of a resistor and the engine controller doesn't see 70C in time to do much good. Too low of a resistor and when the engine warms up, the engine controller thinks it is overheated and complains.

Diode-Resistor Network

Noticing the voltage range was within the cutoff voltage of a diode, a diode in series with a resistor was tried. As the engine warms up, the decreasing voltage soon shutsdown the diode so at higher temperatures, only the engine thermostat controls the temperature. This solved the over temperature alarm but cold temperature startup was a problem.

At ICE temperatures below 30C, too low of a resister results in the engine stalling and throwing an error code. Higher value resistors delay getting the engine controller to start auto-shutdown. The following chart shows what happens when the circuit is manually switched in at 30C compared to an ordinary warm-up cycle:

Transistor Controlled

Cor van de Water designed a circuit that uses transistors to provide: Thereafter the diode-resistor network causes the engine controller to read at least 70C and allow auto-shutdown and EV modes.

Microprocessor

Under development, this approach uses software to set the 'threshold' cold temperature and then move the thermostat temperature indication to 70C. Data recording is used to measure performance and tune the system.

The power supply consists of a 3A, 1N4003, protection diode into a 47 uF capacitor, slightly less than 12VDC rail voltage. The 12VDC supply capacitor feeds a 5V linear voltage regulator for the test resistor network and a 3.3V linear regulator for the microprocessor.

The interface circuit is:

The interface has these major componets:

The software, analog-to-digital converter loop also handles generating the output PWM signal. Be sure to handle a power-on race condition where the microprocessor comes up before the engine ECU and thermistor circuit. For now, the circuit keeps the transistor off until a temperature of 40C is reached. From 40C to 80C, the transistor is set steady-on and adjusts the circuit to 70C. Once the temperature reaches 80C, the PWM begins to turn down the transistor.

The following is the wire map from the 14 conductor flat cable to the MSP430-F2013 pins:

  1. P1 - Vcc (internal), 3.3 V. (upper right)
  2. P14 - Gnd (external), uses same as signal (upper left)
  3. P2 - P1.0 (external), port and pin
  4. P13 - DX in (internal), watch crystal
  5. P3 - P1.1 (external), port and pin
  6. p12 - DX out (internal), watch crystal
  7. P4 - P1.2 (external), port and pin
  8. P11 - SBWTCK (internal), JTAG clock
  9. P5 - P1.3 (external), port and pin
  10. P10 - SBWTDIO (internal), JTAG data
  11. P6 - P1.4 (external), port and pin
  12. P9 - P1.7 (external), port and pin
  13. P7 - P1.5 (external), port and pin (lower right)
  14. P8 - P1.6 (external), port and pin (lower left)

The following images show the circuit and key componets from various angles:



When building a circuit, we start with a design and fix things during integration and test. The following is the 'as-built' values:

Source Code

/******************************************************************************/
// 
//  temperature_hack.c
//
//  This program manages the thermistor voltage in an NHW11 Prius. When the
//  ICE coolant temperature is below 40C, the LED remains off. Once the 
//  coolant temperature reaches 40C (the set point,) a bias transistor is 
//  pulsed to raise the apparent temperature to 70C. Thereafter, the LED
//  flashes using the pulse width modulation (PWM) pattern.
//
//  The target temperature, approximately 73C, has a dead-band below and
//  above of about 2-4C. If the temperature rises above the dead-band,
//  the PWM pattern decrements by one. Too low and the PWM pattern 
//  increments. Each step occurs on a one second interval.
//
//  Internally, all measurements are taken from a continious stream and 
//  averaged over the last eight samples. Only the high-order, 16-bits 
//  are used since this provides sufficient accuracy for the 3 - 0.20 VDC
//  thermistor range and matches the expected accuracy of the SD16.
//
//  The watchdog timer is used as a 1 second, interval timer. This drives
//  the temperature and LED flashing logic. After initialization, the LED
//  flashes the current, PWM modulation pattern. A solid LED is maximum
//  and off means it is the minimum modulation.
//
//  PWM pulse generation is integrated with the measurements because of 
//  the high sampling rate, ~30kHz. The actual modulation pattern is 
//  embedded in a 9 element, byte array and the watchdog timer selects the 
//  PWM pattern to use based upon the current, measured temperature.
//
//  The modulation array varies from eight, 0, to eight, 1. All zeros is
//  the minimum, PWM modulation. However, tri-state OFF, by turning off
//  P1.4, is the 'most off' and used during the initial warm-up phase.
//
//  The conditional debug symbols for debug code are still in the source
//  file. 
//
//  Bob Wilson, January 31, 2007, bwilson4use@hotmail.com
//
/******************************************************************************/

#include "msp430x20x3.h"
#include "intrinsics.h"
#include "stdio.h"
#include "string.h"

//
//  The LED timing pattern starts with low bit and the last
//  high bit ends the pattern. Each bit status is used, 
//  except the highest, last one, which resets the pattern.
//  Thus '101' would flash ON-OFF and the load the next pattern.
//
#define LED_on 0x3
#define LED_off 0x2
#define LED_flash 0x5
#define LED_full 0x2d
#define LED_slow 0x10f
#define LED_start 0x115
//
//  SD16-Divisions per 0.01 volt
//    Voltage-Thermister_low-temp
//    Voltage-Thermister_high-temp
#define SD16_volt 69
#define VT_low 150
#define VT_target 60
#define VT_range 8
#define VT_high 20

#define O2_delay 8

//
//  Tri-state, a PWD_max disables the digital output
//
#define PWM_max 9

//
//  Enable IDLE and conversion counters, for debug
//

//#define __IDLE__
//#define __SD16__
//#define __CAL__
//
//  Global data
//
static unsigned char WDCLK_cnt=0;        // Counter of watchdog-interval timer, seconds
static unsigned char O2_warmup=0;        // Seconds for O2 warm-up and power up
#ifdef __IDLE__
static unsigned char IDLE_cnt=0;          // The idle loop seconds flag
#endif
#ifdef __SD16__
static unsigned char SD16_cnt=0;         // The SD16 converter seconds flag
#endif

static unsigned int LED_state=0x1;      // LED display bits, lowest bit is state
static unsigned int LED_next=LED_off;   // Start quiet

static unsigned int SD16_average=0;         // Average of eight SD16 values, 0 is null
static unsigned int SD16_value[8]={0,0,0,0,0,0,0,0};       // Raw values, samples

#ifdef __IDLE__
static unsigned long IDLE_last[2]={0,0};  // Last idle counters
#endif
#ifdef __SD16__
static unsigned long SD16_last[2]={0,0};      // Last count of conversions
#endif
//
//  PWM variables and lookup table
//
static unsigned int PWM_state=0x1;        // Signal load the next
static unsigned char PWM_next=PWM_max;          // The next value index, requires array
static unsigned char PWM_value[PWM_max] =       // The array of PWM values
{0x00,0x01,0x11,0x49,0x55,0xb6,0xee,0xfe,0xff}; //   the 1s increasing values

//
//  This is entered via a Power On Reset and the 'glue' logic includes
//  an invisible routine to clear the RAM. By default, the watchdog
//  timer re-enters this way, thus wiping out all flags and counters.
//
void main(void)
{
  WDTCTL = WDTPW + WDTHOLD;       // Turn off the watchdog timer, for now
  //
  //  Setup clocks, max MCLCK, fastest CPU and stuff
  //
  DCOCTL = CALDCO_16MHZ;                // Set DCO to 16 MHZ
  BCSCTL1 = CALBC1_16MHZ;               // MCLK 16 MHZ modulator
  BCSCTL3 = 0;                          // Make sure ACLK is Xtal
  WDTCTL = WDTPW+WDTTMSEL+WDTSSEL;     // Set the watchdog timer interval, 1 sec by Xtal 
  //
  //  Ready the LED to flash and let watchdog run when ready
  //
  P1DIR = 0x01;                       // Set P1.0 to output, others left alone
  IE1 |= WDTIE;                       // Enable timer interrupts when GIE enabled
  //
  //  Setup the SD16 to start running with the zero value.
  //
  SD16AE = SD16AE2;                     // Enable the external sensor on P1.2
  SD16CTL = SD16INTDLY_2 + SD16SSEL_1 +   // Set the SD16 to use SMCLK/16
    SD16REFON +                     //  use internal reference voltage
      0 ;                          //   end of register options
  SD16INCTL0 = 0 +                // Interrupt on 4th conversion
    SD16INCH_1 +                  //   select P1.2 as source signal
      SD16GAIN_2 +                //   use 2x gain, based on earlier vehicle tests
        0 ;                              //   end of register options
  SD16CCTL0 = SD16XOSR + SD16OSR_1024 + // Extended conversion, 1024 oversampling
    SD16DF +                           //   make it 2s complement
      SD16UNI +                   // unipolar to cut high order bit
        SD16IE +                     //   enable interrupt   
          SD16SC +                    // start conversions
            0 ;                        // End of register options
  //
  //  Enable interrupts and put initialization code 'to sleep'
  //
  _BIS_SR(GIE);               // Enable the interrupts
#ifdef __IDLE__
  //
  //  Typical idle loop, no extra, is 566,000 loop ticks
  //
  do {                                // Idle CPU counting loop
    if (IDLE_cnt == WDCLK_cnt)
    {
      IDLE_last[1]++;                   // Count idle loop
    } else
    {
      IDLE_last[0] = IDLE_last[1];  // Save the oldest idle counter
      IDLE_last[1] = 0;             // Reset the idle counter
      IDLE_cnt = WDCLK_cnt;        // Reset the counter until next second
    }
  } while ( 1 );                    // Idle loop never ends
#else
  _BIS_SR(LPM1_bits);         // Turn off CPU in idle loop
#endif
  //   MCLK disabled
  //   SMCLK and ACLK remain active
}

// 0xFFF4 Watchdog Timer - currently flashes LED
#pragma vector=WDT_VECTOR
__interrupt void wdt_vector(void)
{
  WDCLK_cnt++;                    // Count interrupt for performance tests
  if ( LED_state == 0x0001 )      // End of the current LED state?
  {
    LED_state = LED_next;         // Load the next pattern
  }
  if ( LED_state & 0x1 )        // Check or next LED status
  {
    P1OUT |= 0x01;               // Keep LED on solily
  } else
  {
    P1OUT &= 0xFE;               // Turn off the LED
  }
  LED_state >>= 1;              // Remove last LED bit
  
#ifndef __CAL__
  //
  //  Wait for O2 sensors and power-up to stabalize
  //
  if ( O2_warmup < O2_delay)
  {
    O2_warmup += 1;             // Count this time delay
  }
  //
  //  The following routine manages the PWM parameters.
  //
  if (                          // Check if within management range
      ( SD16_average > (VT_low * SD16_volt) )
        )
  {
    PWM_next = PWM_max;         // Too cold or too hot, tri-state off
  }
  else
  {
    if ( PWM_next == PWM_max )   // Are we off?
    {
      if ( SD16_average < ( VT_target * SD16_volt) ) // high or low?
      {
        PWM_next = 7;         // Start with maximum adjustment
      } else
      {
        PWM_next = 0;         // Start with minimum adjustment
      }
    } else                    // No, see if we need to adjust
    {
      //
      //    
      //
      if ( SD16_average < ( (VT_target-VT_range) * SD16_volt ) ) // Too hot?
      {
        if ( PWM_next != 0 )
        {
          PWM_next--;           // YES - cut down the adjustment
        }
      }
      if ( SD16_average > ( (VT_target+VT_range) * SD16_volt ) ) // Too cool?
      {
        if ( PWM_next != (PWM_max-1) )
        {
          PWM_next++;           // YES - increase the adjustment
        }
      }
    }
  }
#else
  {
    int cur_state=0;          // Current state
    if ( (O2_warmup & 0x07) == 0 )
    {
      cur_state = O2_warmup >> 3; // Convert 1sec to 8 sec internval
      switch ( cur_state )
      {
      case 0:
        PWM_next=PWM_max;
        LED_next=LED_on;
        break;
      case 1:
        PWM_next=0;
        LED_next=LED_off;
        break;
      case 2:
        PWM_next=1;
        LED_next=LED_on;
        break;
      case 3:
        PWM_next=2;
        LED_next=LED_off;
        break;
      case 4:
        PWM_next=3;
        LED_next=LED_on;
        break;
      case 5:
        PWM_next=4;
        LED_next=LED_off;
        break;
      case 6:
        PWM_next=5;
        LED_next=LED_on;
        break;
      case 7:
        PWM_next=6;
        LED_next=LED_off;
        break;
      case 8:
        PWM_next=7;
        LED_next=LED_on;
        break;
      default:
        O2_warmup = 0xff;
      }
    }
    cur_state = SD16_average + 1; // Add and forget
    O2_warmup+=1;                // Count this one second interval
  }
#endif
}

// 0xFFEA Sigma Delta ADC
#pragma vector=SD16_VECTOR
__interrupt void sd16_vector(void)
{
  int i;                        // Counter
  unsigned long average=0;       // Set the average to zero
  //
  //  Looks like it is making 30,780 samples/sec
  //
#ifdef __SD16__
  if ( SD16_cnt == WDCLK_cnt )  // Same seconds as last?
  {
    SD16_last[1]++;             // Count this conversion
  } else
  {
    SD16_last[0] = SD16_last[1];   // Remember the last count
    SD16_last[1] = 0;           // Reset the conversion counter
    SD16_cnt = WDCLK_cnt;       // Reset for next second
  }
#endif
  //
  //  Based upon the PWM state, clock out the next bit
  //
  if ( PWM_next == PWM_max )      // Turn off the PWM output?
  {
    P1DIR &= (0xff - 0x08);      // Turn off the PWM 
  } else
  {
    P1DIR |= 0x08;              // Turn on the PWM
    //
    //  Check for reloading the shift register
    //
    if ( PWM_state == 0x1 )     // Time for a reload?
    {
      PWM_state = PWM_value[PWM_next]; // Grab the next value
      PWM_state |= 0x100;       // Set the EOF bit
      LED_next = PWM_state;     // Set the LED flash to current PWM pattern
      LED_state = 0x2;          // Trigger early LED reload and avoid shift race condition
    }
    if ( PWM_state & 0x1 )
    {
      P1OUT |= 0x08;            // Turn on the output
    } else
    {
      P1OUT &= (0xff - 0x08);   // Turn off the output
    }
    PWM_state >>= 1;             // Shift out the last bits
  }
  //
  //  Last, figure the average, so the next timer tick and set the mode
  //
  for (i=0;i<7;i++)
  {
    SD16_value[i] = SD16_value[i+1];  // Shift everyone down
    average += SD16_value[i];         // Add to accumulator
  }
  SD16_value[7] = SD16MEM0;           // Save the most recent
  average += SD16_value[7];           // Add to accumulator
  if (SD16_value[0] != 0)             // Do we have enough for useful average?
  {
    average >>= 3;                    // Now we average everyone
    SD16_average = average;           // Do an atomic update to average
  }
}