Home About Me

Op-Amp Circuits for Three-Phase Current and Temperature Sensing

The three-phase monitoring board has eight analog channels to handle: three phase currents, three phase temperatures, ambient temperature, and residual current. The phase currents are far too large to connect to an ADC directly, so current transformers step them down to a safe level for sampling. The temperature channels are simpler: an NTC divider produces a voltage that the ADC can read.

To keep the MCU pin count under control, the front end uses a 74HC4052D analog multiplexer and an MCP6004 quad op-amp.

Analog switching and the op-amp

The 74HC4052D is a dual 4:1 analog switch. Each half connects one of four inputs to a common output, selected by S0 and S1; E is active low. The schematic and truth table are below.

Schematic

Truth table

In this design, four temperature channels and three current channels go through the multiplexer. The residual-current channel has its own op-amp path.

The MCP6004 is a 14-pin quad op-amp, which makes it convenient to build several small signal-conditioning stages in one package.

Op-amp pinout

Two op-amp assumptions are used throughout: the inputs are at the same voltage (virtual short), and no current flows into the inputs (virtual open). These are enough to derive the circuits below.

Voltage follower

Voltage follower

With virtual short, Vout = Vin. This is a gain-of-one buffer. It matters because a divider or sensor output can be loaded by the next stage; the op-amp has very high input impedance and very low output impedance, so it isolates the source from the load. The temperature channels use this buffer.

Inverting amplifier

Inverting amplifier

  1. Virtual open gives i- = 0, so iR1 = iR3.
  2. Virtual short gives V+ = V- = 0 V.
  3. Therefore Vin/R1 + Vout/R3 = 0, or Vout = -(R3/R1)Vin.
  4. R2 is the balance resistor.

Non-inverting amplifier

Non-inverting amplifier

  1. Virtual open gives i- = 0, so iR1 = iR3.
  2. Virtual short gives V+ = V- = Vin.
  3. Therefore (Vout - Vin)/R3 = Vin/R1, or Vout = (1 + R3/R1)Vin.
  4. R2 is the balance resistor.

Difference amplifier

Difference amplifier

Waveforms

The difference amplifier amplifies the voltage between two inputs, so the input signal is normally AC.

  1. The AC inputs are Vi+ and Vi-.
  2. Virtual open gives i- = 0, so iR1 = iR3.
  3. Virtual short gives V+ = V-.
  4. (Vi- - V-)/R1 = (V- - Vout)/R3.
  5. (Vi+ - V-)/R2 = V-/R4.
  6. In the usual case R4 = R3 and R1 = R2.
  7. Substituting gives Vout = (R3/R1)(Vi+ - Vi-).

Current-sense circuit with bias

The goal is to turn the AC current into a voltage the ADC can read. The ADC only accepts 0 to VDD, so the op-amp stage also adds a bias that lifts the negative half of the waveform into range.

Current-sense circuit

Waveforms

  1. A voltage follower generates a 1.65 V bias and feeds it to Vi-.
  2. The circuit is a non-inverting amplifier with an offset. A difference amplifier plus offset is another option; it is worth simulating to see why it is not used here.
  3. Virtual open gives i- = 0, so iR2 = iR4.
  4. Virtual short gives V+ = V- = Vi+.
  5. Therefore (Vout - Vi+)/R4 = (Vi+ - Vbias)/R2, or Vout = (1 + R4/R2)Vi+ - (R4/R2)Vbias.
  6. To get RMS current: Vrms = Vpp/(2√2), and Irms = Vrms/R3.
  7. The peak-to-peak value comes from the positive and negative extremes: Vpp = (1 + R4/R2)Vvpp.

Firmware: temperature lookup

Temperature calculation follows the NTC B value and R25, but a lookup table is usually the practical choice because real probes have tolerances. The NTC supplier table is converted into ADC values and stored in firmware.

static const u16 stemperatureTable[] = {
    382,402,424,446,469,493,518,544,570,598,            // -20~-11
    626,656,686,717,749,782,816,851,886,922,            // -10~-1
    959,997,1036,1076,1116,1156,1198,1240,1282,1325,    // 0~9
    1369,1413,1457,1502,1547,1592,1637,1683,1729,1774,    // 10~19
    1820,1866,1911,1957,2002,2047,2092,2136,2180,2224,    // 20~29
    2267,2310,2353,2395,2436,2477,2517,2557,2596,2634,    // 30~39
    2671,2708,2745,2780,2815,2849,2882,2916,2948,2979,    // 40~49
    3010,3040,3069,3098,3125,3153,3179,3205,3230,3255,    // 50~59
    3278,3302,3324,3346,3368,3388,3409,3428,3447,3466,    // 60~69
    3484,3501,3518,3535,3551,3566,3582,3596,3610,3624,    // 70~70
    3638,3651,3663,3675,3687,3698,3709,3721,3731,3741,    // 80~89
    3751,3760,3770,3779,3787,3796,3803,3812,3819,3827,    // 90~99
    3834,3840,3847,3853,3859,3866,3871,3877,3882,3888,    // 100~109
    3893,3898,3904,3908,3913,3918,3923,3928,3932,3936,    // 110~119
    3941};                                                // 120

TemperatureHandle averages eight samples, compares the result against the table, and uses the index minus 20 as the temperature.

/*----------------------------------------------------------------
  *Function:        TemperatureHandle
  *Description:        Temperature data processing
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
void TemperatureHandle(void)
{
    static u16 u16Buff[4];
    u16 temp[4];
    s8 temperature[4];
    u8 i, j;
    static u8 inCheck[4];

    if(++inCheck[0] < 9)
    {
        u16Buff[0] += gsAdcValue.temperatureA;
    }
    else
    {
        inCheck[0] = 0;
        temp[0] = (u16Buff[0] >> 3);
        u16Buff[0] = 0;
    }
    if(++inCheck[1] < 9)
    {
        u16Buff[1] += gsAdcValue.temperatureB;
    }
    else
    {
        inCheck[1] = 0;
        temp[1] = (u16Buff[1] >> 3);
        u16Buff[1] = 0;
    }
    if(++inCheck[2] < 9)
    {
        u16Buff[2] += gsAdcValue.temperatureC;
    }
    else
    {
        inCheck[2] = 0;
        temp[2] = (u16Buff[2] >> 3);
        u16Buff[2] = 0;
    }
    if(++inCheck[3] < 9)
    {
        u16Buff[3] += gsAdcValue.temperatureAmb;
    }
    else
    {
        inCheck[3] = 0;
        temp[3] = (u16Buff[3] >> 3);
        u16Buff[3] = 0;
    }

    for(i = 0; i < 4; i++)
    {
        if(u16Buff[i] == 0)
        {
            for(j = 0; j < (sizeof(stemperatureTable)/sizeof(u16)); j++)
            {
                if(temp[i] >= stemperatureTable[j])
                {
                    temperature[i] = j-20;
                }
            }
            gsModuleProperty.temperatureA.value = temperature[0];
            gsModuleProperty.temperatureA.dataType = TCP_NUM;
            gsModuleProperty.temperatureA.updata = SLAVE;
            gsModuleProperty.temperatureB.value = temperature[1];
            gsModuleProperty.temperatureB.dataType = TCP_NUM;
            gsModuleProperty.temperatureB.updata = SLAVE;
            gsModuleProperty.temperatureC.value = temperature[2];
            gsModuleProperty.temperatureC.dataType = TCP_NUM;
            gsModuleProperty.temperatureC.updata = SLAVE;
            gsModuleProperty.temperatureAmb.value = temperature[3];
            gsModuleProperty.temperatureAmb.dataType = TCP_NUM;
            gsModuleProperty.temperatureAmb.updata = SLAVE;
        }
    }
}

Firmware: current RMS and compensation

The current code follows the same equations. A few points matter in practice:

  1. With no current from the transformer, the voltage sits at the 1.65 V bias, so the ADC reading is about 2047.
  2. The AC input and output are 50 Hz, one cycle every 20 ms. With a 500 us tick, that gives 40 samples per cycle.
  3. The RMS current is calculated from the current-sense equations, then scaled by the transformer turns ratio to get the actual phase current.
  4. The final values need compensation because real circuits have error.
/*----------------------------------------------------------------
  *Function:        CurrentHandle
  *Description:        Current data processing
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
void CurrentHandle(void)
{
    u8 i;
    FP32 vout;
    static u16 maxTemp[4] = {2047, 2047, 2047, 2047}, minTemp[4] = {2047, 2047, 2047, 2047};
    static u16 maxTempBuff[4], minTempBuff[4];
    static u8 cnt;
    static u8 inCheck[4];


    if(inCheck[0]++ < 40)
    {
        maxTemp[0] = (gsAdcValue.currentA > maxTemp[0])?gsAdcValue.currentA:maxTemp[0];
        minTemp[0] = (gsAdcValue.currentA < minTemp[0])?gsAdcValue.currentA:minTemp[0];
    }
    else
    {
        inCheck[0] = 0;
        maxTempBuff[0] += maxTemp[0];
        minTempBuff[0] += minTemp[0];
        maxTemp[0] = 2047;
        minTemp[0] = 2047;
    }
    if(inCheck[1]++ < 40)
    {
        maxTemp[1] = (gsAdcValue.currentB > maxTemp[1])?gsAdcValue.currentB:maxTemp[1];
        minTemp[1] = (gsAdcValue.currentB < minTemp[1])?gsAdcValue.currentB:minTemp[1];
    }
    else
    {
        inCheck[1] = 0;
        maxTempBuff[1] += maxTemp[1];
        minTempBuff[1] += minTemp[1];
        maxTemp[1] = 2047;
        minTemp[1] = 2047;
    }
    if(inCheck[2]++ < 40)
    {
        maxTemp[2] = (gsAdcValue.currentC > maxTemp[2])?gsAdcValue.currentC:maxTemp[2];
        minTemp[2] = (gsAdcValue.currentC < minTemp[2])?gsAdcValue.currentC:minTemp[2];
    }
    else
    {
        inCheck[2] = 0;
        maxTempBuff[2] += maxTemp[2];
        minTempBuff[2] += minTemp[2];
        maxTemp[2] = 2047;
        minTemp[2] = 2047;
    }
    if(inCheck[3]++ < 40)
    {
        maxTemp[3] = (gsAdcValue.currentResidual > maxTemp[3])?gsAdcValue.currentResidual:maxTemp[3];
        minTemp[3] = (gsAdcValue.currentResidual < minTemp[3])?gsAdcValue.currentResidual:minTemp[3];
    }
    else
    {
        inCheck[3] = 0;
        maxTempBuff[3] += maxTemp[3];
        minTempBuff[3] += minTemp[3];
        maxTemp[3] = 2047;
        minTemp[3] = 2047;
    }
    if((inCheck[0] == 0) && (inCheck[1] == 0) && (inCheck[2] == 0) && (inCheck[3] == 0))
    {
        if(++cnt >= 8)
        {
            cnt = 0;
            for(i = 0; i < (sizeof(maxTempBuff) / sizeof(u16)); i++)
            {
                maxTempBuff[i] = maxTempBuff[i] >> 3;
                minTempBuff[i] = minTempBuff[i] >> 3;

                if(i < 3)
                {
                    vout = (maxTempBuff[i] - minTempBuff[i]) / 2 / CURRENT_OP_GAIN;
                    vout = vout / 4095 * CURRENT_VDD;
                    vout = vout / 1.4142 / CURRENT_SENSE_RES * CURRENT_TRANS_GAIN;
                    vout *= CURRENT_COMPENSATION;
                }
                else
                {
                     vout = (maxTempBuff[i] - minTempBuff[i]) / 2 / CURRENT_OP_GAIN2;
                     vout = vout / 4095 * CURRENT_VDD;
                     vout = vout / 1.4142 / CURRENT_SENSE_RES2 * CURRENT_TRANS_GAIN2;
                     vout = (vout >= 0.013)?(vout-0.013):0;            // compensation
                     vout = vout * 1000;
                    // vout *= CURRENT_COMPENSATION;
                }

                switch (i)
                {
                    case 0:
                        vout = (vout >= 0.1)?(vout-0.1):0;                // compensation
                        gsModuleProperty.currentA.value = vout;
                        gsModuleProperty.currentA.dataType = TCP_NUM;
                        gsModuleProperty.currentA.updata = SLAVE;        // update flag
                        break;
                    case 1:
                        vout = (vout >= 0.79)?(vout-0.79):0;            // compensation
                        gsModuleProperty.currentB.value = vout;
                        gsModuleProperty.currentB.dataType = TCP_NUM;
                        gsModuleProperty.currentB.updata = SLAVE;        // update flag
                        break;
                    case 2:
                        vout = (vout >= 0.28)?(vout-0.28):0;            // compensation
                        gsModuleProperty.currentC.value = vout;
                        gsModuleProperty.currentC.dataType = TCP_NUM;
                        gsModuleProperty.currentC.updata = SLAVE;        // update flag
                        break;
                    case 3:
                        gsModuleProperty.currentResidual.value = vout;
                        gsModuleProperty.currentResidual.dataType = TCP_NUM;
                        gsModuleProperty.currentResidual.updata = SLAVE;
                    default:
                        break;
                }
                maxTempBuff[i] = minTempBuff[i] = 0;    // clear cache
            }
        }
    }
}

Last note

Analog calculation can look more involved than digital design, but many classic circuits are now packaged as ICs. Knowing the basic math is still useful: it tells you what the IC is doing, what the bias point should be, and whether a reading is plausible. The ICs are convenient—but they are not free.