Investigating creeping ground fault

Was having some problems with something in my lab tripping the ground fault relay. It seemed weather related but otherwise random.

I decided to make a device that could monitor the residual current of my mains installation over time to see if the if it would uncover anything.

Getting the signal

The way residual current is usually measured is by passing the live and neutral wires of the mains feed through a current transformer and measuring the net current. In a normal situation all current going in and out through these wires should neatly cancel out to zero. In the case some connection between one or more of the live wires and earth occurs (e.g. person touching live and earth) this will show as a non-zero net current in the transformer and usually trigger a ground fault interrupter relay. In my country this relay is set to trip at 30mA.

Started out with a 1:1000 current transformer. By loading this transformer with a 1kOhm resistor a residual current of 100mA(rms @ 50Hz)  going through the transformer would result in 100mV(rms) over the resistor:


I tested this by running a wire from my signal generator through the transformer via. a 10 Ohm resistor.  By adjusting the voltage over the resistor to 1Vrms I knew the current to be 100mA:

I wanted to use a micro controller to measure this signal and send it to some data logging cloud service. To get any useful resolution from a typical imbedded A/D converter @ 100mV would require some sort of magnification.

Attempt 1

I initially rigged up a AD8221 instrumentation amplifier board to amplify the signal by 10x before feeding it to the A/D of a ESP32 microcontroller:

This worked quite well but the amplifier required split power supply and I had to account for the non linearity of the ESP32’s A/D converter:

Not quite pleased with the results I decided in stead to use a Audio ADC to capture the signal from the current transformer.

A better way

I used a board with a PCM1808 24bit I2S audio ADC:

The PCM1808 has built in voltage reference and plenty of sensitivity to handle the faint signal from the transformer. It also have two channels so the project may be adapted to measure true power by addition of a voltage signal on the other channel. It also have a digital high pass filter that removes DC from the received signal. This makes it perfect for RMS measurements of AC signals since any DC component would corrupt the calculation.

Non intrusive measurement

It was not really practical to remove the wires from my switch box to feed them through the current transformer so I opted for a clip-on version of the current transformer that could be applied to the wires without breaking with the circuit:

This kind transformer has a less defined turns ratio and the result had to be calibrated in software.

Data processing

Wrote a basic Arduino sketch that will read the signal from the transformer via. the PCM1808 board. The signal arrives as buffers of 256 samples sampled at 20KHz. Each sample is rectified by simply negating negative samples. The rectified samples are then filtered by a third order software filter:

Every 10 buffers the signal are simply printed to Serial. (Code below)


Even though I was pretty confident that the circuit was working I still wanted to test it in a real life situation in case e.g. interference was a problem. I borrowed a commercial ground fault tester that was able to generate different kinds of residual current:

Since the test device was mains powered I had to hook it up to actual mains while doing the testes. Since I did not want to trip the ground fault relay during the test I simply connected the ground wire to neutral outside the transformer. This way the test current could “escape” from the line and neutral wire without tripping anything.

The graph below shows a 100mA pulse generated from my signal generator followed by a 30mA pulse from the tester:

The two signals was superimposed by running an extra wire from the signal generator along with the test leads.

I then tried two 100mA pulses one from the generator and one from the test device:

It seems that the test device deliver a slightly higher current than specified in both tests.


I designed this system to keep an eye on creeping ground fault problems in my mains installation. I am not completely convinced about the absolute accuracy of this simple setup but I am convinced that it can detect changes in residual current and be a great tool for fault finding.

It seems that using a fixed core that does not split offers much higher accuracy bit requires the measured circuit to be interrupted to feed the wires through the core.

I think having the ability to selectively test for residual current in a non intrusive way offers a lot of potential. I have often been in situations at festivals, worksites and other places with temporary power installations where residual current issues has been difficult to troubleshoot.

Future work

Making some sort of net connected or handheld device based on this prototype is certainly seething I will pursue in the future.

If I could find a way to measure the voltage of the mains wire(s) in some non intrusive way I think it could be interesting to make a clip-on power monitor to accurately measure power consumption using this principle.


Here is the code I used in case you want to have a go at it yourself.

//  RMS current measurement using a PCM1808 audio ADC
//  License: Enjoy, happy to help.
//  (F) Dzl 2024
#include <driver/i2s.h>
#include <driver/ledc.h>
// Settings:
#define FILTER_CUTOFF 5.0 //[Hz]
#define CAL_100_MA 15883.0    //Calibrate transformer/resistor combination.
//  ADC interface:
#define CODEC_RATE 20000      //Sample rate
#define CODEC_SIZE 256        //Buffer size
#define DAC_SCALE 2147483647  //2^24
#define I2S_BCK_IO (GPIO_NUM_35)
#define I2S_WS_IO (GPIO_NUM_34)
#define I2S_DO_IO (GPIO_NUM_33)
#define I2S_DI_IO (GPIO_NUM_32)
#define I2S_XCK (GPIO_NUM_18)
static const i2s_port_t i2s_num = (i2s_port_t)I2S_NUM_0;  // i2s port number
size_t RXcount = 0;
int32_t codecIN[CODEC_SIZE * 2];
bool sampleFlag = false;  //Buffer ready
// Data:
volatile float RMS = 0;
// Filter:
float a0, a1, a2, b1, b2;
float z1, z2;
#define Qfilter 0.55
void setFilter(float Fc) {
  double K = tan(M_PI * Fc);
  double norm = 1 / (1 + K / Qfilter + K * K);
  a0 = K * K * norm;
  a1 = 2 * a0;
  a2 = a0;
  b1 = 2 * (K * K - 1) * norm;
  b2 = (1 - K / Qfilter + K * K) * norm;
inline float applyFilter(float in) {
  float out = in * a0 + z1;
  z1 = in * a1 + z2 - b1 * out;
  z2 = in * a2 - b2 * out;
  return out;
//Thread to read data from ADC:
void DSP_Thread(void* inp) {
  while (1) {
    i2s_read(i2s_num, (char*)codecIN, sizeof(codecIN), &RXcount, portMAX_DELAY);
    for (int i = 0; i < CODEC_SIZE; i++) {
      float f = (float)codecIN[i * 2] / (DAC_SCALE / CAL_100_MA);
      if (f < 0.0)
        f = -f;
    sampleFlag = true;
// Set up ADC interface:
bool initDSP() {
  i2s_config_t i2s_config;
  i2s_config.sample_rate = CODEC_RATE;
  i2s_config.fixed_mclk = CODEC_RATE;
  i2s_config.bits_per_sample = (i2s_bits_per_sample_t)I2S_BITS_PER_SAMPLE_32BIT;
  i2s_config.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S);
  i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
  i2s_config.intr_alloc_flags = ESP_INTR_FLAG_INTRDISABLED;
  i2s_config.dma_buf_count = 4;
  i2s_config.use_apll = false;
  i2s_config.dma_buf_len = CODEC_SIZE;
  i2s_config.mode = (i2s_mode_t)(I2S_MODE_SLAVE | I2S_MODE_RX);
  i2s_config.tx_desc_auto_clear = true;
  i2s_config.mclk_multiple = (i2s_mclk_multiple_t)256;
  i2s_config.bits_per_chan = (i2s_bits_per_chan_t)0;
  i2s_driver_install(i2s_num, &i2s_config, 0, NULL);  //start i2s driver
  //Pins init
  i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_BCK_IO,
    .ws_io_num = I2S_WS_IO,
    .data_out_num = I2S_DO_IO,
    .data_in_num = I2S_DI_IO  //Not used
  i2s_set_pin(i2s_num, &pin_config);
  //-Generate clock for ADC @ 256*sample rate
  ledc_timer_config_t ledc_timer;
  ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE;
  ledc_timer.duty_resolution = LEDC_TIMER_2_BIT;
  ledc_timer.timer_num = LEDC_TIMER_0;
  ledc_timer.freq_hz = CODEC_RATE * 256;
  ledc_timer.clk_cfg = LEDC_AUTO_CLK;
  ledc_channel_config_t ledc_channel;
  ledc_channel.intr_type = LEDC_INTR_DISABLE; = LEDC_CHANNEL_0;
  ledc_channel.gpio_num = I2S_XCK;
  ledc_channel.speed_mode = LEDC_HIGH_SPEED_MODE;
  ledc_channel.timer_sel = LEDC_TIMER_0;
  ledc_channel.duty = 2;
  ledc_channel.hpoint = 2;
  Serial.println("DSP Start...");
  xTaskCreate(DSP_Thread, "DSP_Thread", 2048, NULL, 2, NULL);
  return true;
//  Setup:
void setup() {
//  Loop:
int divider = 0;
void loop() {
  if (sampleFlag) {
    if (divider >= 10) {
      Serial.println(RMS, 10);
      divider = 0;
    } else
    sampleFlag = 0;

Leave a Reply