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.


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.

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

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

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

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


The difference amplifier amplifies the voltage between two inputs, so the input signal is normally AC.
- The AC inputs are Vi+ and Vi-.
- Virtual open gives i- = 0, so iR1 = iR3.
- Virtual short gives V+ = V-.
- (Vi- - V-)/R1 = (V- - Vout)/R3.
- (Vi+ - V-)/R2 = V-/R4.
- In the usual case R4 = R3 and R1 = R2.
- 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.


- A voltage follower generates a 1.65 V bias and feeds it to Vi-.
- 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.
- Virtual open gives i- = 0, so iR2 = iR4.
- Virtual short gives V+ = V- = Vi+.
- Therefore (Vout - Vi+)/R4 = (Vi+ - Vbias)/R2, or Vout = (1 + R4/R2)Vi+ - (R4/R2)Vbias.
- To get RMS current: Vrms = Vpp/(2√2), and Irms = Vrms/R3.
- 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:
- With no current from the transformer, the voltage sits at the 1.65 V bias, so the ADC reading is about 2047.
- The AC input and output are 50 Hz, one cycle every 20 ms. With a 500 us tick, that gives 40 samples per cycle.
- The RMS current is calculated from the current-sense equations, then scaled by the transformer turns ratio to get the actual phase current.
- 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.