Skip to the content
PCBKitNew board
4 layers76.2 × 104.1 mmCt4, the printed circuit board in 3D

python import pandas as pd from importlib import reload import matplotlib.pyplot as plt import numpy as np from IPython.display import display

Size
76.2 × 104.1 mm
Copper layers
4
Thickness
1.6 mm
Parts
34
Nets
48
Pads
118
Tracks, vias
144, 0
Routed
Yes
KiCad
KiCad 8.0 (file 20240108)
Created
25 Jan 2026
Updated
4 Feb 2026

Overview

Ct4 is an open-source printed circuit board by mckennatim, from the mckennatim/ct4 repository on GitHub. It is a 4-layer board of 76.2 × 104.1 mm with 34 parts on 48 nets, routed with 144 tracks and 0 vias.

It is built around the ADS1115. Its parts include 12 resistors, 8 capacitors, 6 ICs and 4 connectors.

Main components

1

README

README.md

setup

python
import pandas as pd
from importlib import reload
import matplotlib.pyplot as plt
import numpy as np
from IPython.display import display

ct4 ads1115

Design a 4 CT energy monitor for 4 circuits. On hand are a number of 15A/1v and 20A/1v current transformers. When the CT has that kind of designation, the burden resistor is already included. (If not calculate burden resistor below in ref)

ref

Best Resources for Explanation:

  1. Electronics Textbooks:

    • "The Art of Electronics" by Horowitz & Hill (Chapter 1)
    • "Practical Electronics for Inventors" by Paul Scherz
  2. Online Resources:

    • All About Circuits (allaboutcircuits.com) - Has excellent free tutorials on capacitive reactance and AC coupling
    • Electronics Tutorials (electronics-tutorials.ws) - Great explanations with diagrams
    • Khan Academy - Physics section on capacitors and RC circuits
    • MIT OpenCourseWare - Circuit theory courses
  3. YouTube Channels:

    • ElectroBOOM
    • EEVblog
    • GreatScott!

Circuit

parts

notes on pcb order

*lots of holes too big

design

test known currents, read CT output

Consider the current range of the heatpump we want to monitor.

python
# unit label data
total_A = 10.9
compressor_A = 9.2
fan_A = .3
ampacity = 13.2
indoor_Fan_A = .35
indoor_model = "ASUH09KTAS"
indoor_serial = "LDA000334"
outdoor_model = "AOUH09KTAP1"
outdoor_serial = "LDN000103"

Test (120V) hair drier, fan, immersion blender and jig saw to find mV produced by sensor, for each current produced as measured by clamp on current meter.

plot A vs mV

python
# fan: fh,fl immersion blender: im, hairdrier lo hi, cool, warm, hi: dlc, dlw, dlh, dlh, dhh
sources = ["fh","fl+im", "dlc", "dhc", "dhc+fh+im", "dlw", "dlw+fh+im", "dlh", "dlh+fh+im" "dhh"]
known_A1 = [.48, .75, 1.04, 1.75, 2.39, 3.90, 4.45, 6.65, 7.12, 8.25, 11.93]
known_A2 = [.54, .71, 1.04, 1.75, 2.39, 3.99, 4.52, 6.77, 7.25, 8.36, 12.15]
known_A3 = [.48, 1.0, 1.72, 2.06, 3.88, 6.72, 7.38, 9.88, 12.02, 12.4, 13.31]
mv_A1 = [36, 54,72, 121, 165, 268, 306, 448, 478, 554, 821]
mv_A2 = [78, 110, 141, 238, 328, 517, 600, 723, 810, 920, 1665]
mv_A3 = [53, 108, 183, 219, 406, 684, 778, 1037, 1264, 1305, 1391]
df1 = pd.DataFrame({"A": known_A1, "mv": mv_A1})
df2 = pd.DataFrame({"A": known_A2, "mv": mv_A2})
df3 = pd.DataFrame({"A": known_A3, "mv": mv_A3})
coeffs1 = np.polyfit(df1["A"], df1["mv"], 1)
coeffs2 = np.polyfit(df2["A"], df2["mv"], 1)
coeffs3 = np.polyfit(df3["A"], df3["mv"], 1)
poly1 = np.poly1d(coeffs1)
poly2 = np.poly1d(coeffs2)
poly3 = np.poly1d(coeffs3)
print("Calibration Polynomial 1 (A1): ")
print(poly1)
print("Calibration Polynomial 2 (A2): ")
print(poly2)
print("Calibration Polynomial 3 (A3): ")
print(poly3)
print(poly2)
text
  Cell In[11], line 50
    plt.plot(df2["A"], df2["mv"], "o", label="Known A2")
    ^
IndentationError: unexpected indent
python
sources = ["fh","fl+im", "dlc", "dhc", "dhc+fh+im", "dlw", "dlw+fh+im", "dlh", "dlh+fh+im", "dhh"]
known_A1 = [.48, .75, 1.04, 1.75, 2.39, 3.90, 4.45, 6.65, 7.12, 8.25, 11.93]
known_A2 = [.54, .71, 1.04, 1.75, 2.39, 3.99, 4.52, 6.77, 7.25, 8.36, 12.15]
known_A3 = [.48, 1.0, 1.72, 2.06, 3.88, 6.72, 7.38, 9.88, 12.02, 12.4, 13.31]
mv_A1 = [36, 54,72, 121, 165, 268, 306, 448, 478, 554, 821]
mv_A2 = [78, 110, 141, 238, 328, 517, 600, 723, 810, 920, 1665]
mv_A3 = [53, 108, 183, 219, 406, 684, 778, 1037, 1264, 1305, 1391]
df1 = pd.DataFrame({"A": known_A1, "mv": mv_A1})
df2 = pd.DataFrame({"A": known_A2, "mv": mv_A2})
df3 = pd.DataFrame({"A": known_A3, "mv": mv_A3})
coeffs1 = np.polyfit(df1["A"], df1["mv"], 1)
coeffs2 = np.polyfit(df2["A"], df2["mv"], 1)
coeffs3 = np.polyfit(df3["A"], df3["mv"], 1)
poly1 = np.poly1d(coeffs1)
poly2 = np.poly1d(coeffs2)
poly3 = np.poly1d(coeffs3)
print("Calibration Polynomial 1 (A1): ")
print(poly1)
print("Calibration Polynomial 2 (A2): ")
print(poly2)
print("Calibration Polynomial 3 (A3): ")
print(poly3)
def A_to_mv1(A):
    return poly1(A)
text
Calibration Polynomial 1 (A1): 
 
67.85 x + 1.626
Calibration Polynomial 2 (A2): 
 
123.6 x + 1.254
Calibration Polynomial 3 (A3): 
 
104.7 x + 0.9221

png

text
[67.85287623  1.62603625]
python
# y = mx + b
m = 104.7
b = .9221

x = 10.9
y = 800 

c = (y-b)/m
print(f"Calculated current for {y:0.2f} mV: {c:0.2f} A")
text
Calculated current for 800.00 mV: 7.63 A
understand concepts

The 1uF capacitor on S blocks the DC component of the bias voltage from dragging down and shorting out through the 53ohm internal R of the CT (as measured at the pins)

The load impedance isn't just the 53Ω CT - it's the parallel combination of:

  • CT: 53Ω
  • R1 || R2: 82kΩ || 153kΩ ~ 53K

Since 60kΩ >> 53Ω, the AC impedance is essentially 53Ω.

But actually, for the cutoff frequency, what matters is the impedance that C1 sees:

C1 sees: CT (53Ω) in series with [R1 || R2] = 53Ω + 53kΩ ≈ 53kΩ

To pass AC mV (60 Hz) and block the DC

for the high-pass filter cutoff, the relevant impedance is the bias network (R1||R2 = 53kΩ)

(The 53Ω only matters for loading the voltage divider with AC current, which is minimal with 60kΩ bias resistors. )

Rule of Thumb: Choose fc to be 1/10th of your lowest frequency of interest for minimal signal loss.

For fc = 6 Hz: C = 1 / (2π × 53,000 × 6) ≈ 0.5 µF

text
Round up to 1uF



#### calculating values


```python
R1 = 82000
R2 = 153000
Rct =53

R1llR2 = (R1*R2)/(R1+R2)
print(f"Parallel resistance of R1 and R2: {R1llR2:0.1f} ohms")  
Vbias = 2.5 * R2 /(R1+R2)
print(f"Bias voltage at divider point: {Vbias:0.3f} V")

Rctll_R1llR2 = (Rct*R1llR2)/(Rct+R1llR2)
print(f"Total resistance seen at bias point with load: {Rctll_R1llR2:0.1f} ohms")

fc = 6  # hz 1/10 of 60hz
C1 = 1 / (2*3.14159 * R1llR2 * fc)

print(f"Capacitance needed: {C1*1e6:0.2f} uF")
text
Parallel resistance of R1 and R2: 53387.2 ohms
Bias voltage at divider point: 1.628 V
Total resistance seen at bias point with load: 52.9 ohms
Capacitance needed: 0.50 uF

R controlling current through the lm4040

To account for the load, you need to ensure the resistor provides enough current for both the LM4040’s minimum operating current and the current drawn by your voltage divider (and any other load).

  1. Calculate the current your voltage divider (and any other load) will draw from the reference: $$ I_{load} = \frac{2.5,V}{R_{divider}} $$
  2. Add the LM4040’s minimum required current (from the datasheet, e.g., 60 µA): $$ I_{total} = I_{load} + I_{min} $$
  3. Calculate the resistor value: $$ R = \frac{V_{in} - V_{ref}}{I_{total}} $$

Choose the next lower standard value to ensure enough current (e.g., 10 kΩ).

python
Iload = 2.5/(100000+150000)
print(f"Load current through resistor: {Iload*1000} mA")
Imin = 60e-6
print(f"Minimum current through resistor with load: {Imin*1000:0.3f} mA")
Itotal = Imin + Iload
print(f"Total current through resistor with load: {Itotal*1000:0.3f} mA")
R=(3.3-2.5)/Itotal
print(f"Resistor value to set minimum current with load: {R:0.1f} ohms")
text
Load current through resistor: 0.01 mA
Minimum current through resistor with load: 0.060 mA
Total current through resistor with load: 0.070 mA
Resistor value to set minimum current with load: 11428.6 ohms

fixing the toolchain

Changed the default terminal in vscode to cmd. cmd-shift-p Terminal:Select default profile

cmd couldn't find where python

  • "C:\Users\mcken.platformio\penv\Scripts" needed to be added to system path
    • windows search env...Edit environmental Variables, find path in system variables and add to the path

Platformio uses its own penv python. It complained about missing a library. Since cmd uses conda, pip would have used the active conda env pip and put it in the wrong place. To use Platformio's python:

  • C:\Users\mcken.platformio\penv\Scripts\python.exe -m pip install intelhex

from cmd run platformio system info to see where python is and it is

remember to hold the boot button on esp32's

A fatal error occurred: Failed to connect to ESP32: Wrong boot mode detected (0x13)! The chip needs to be in download mode

  • Hold down the “BOOT” button on your ESP32 board.
  • While holding “BOOT”, click “Upload” in PlatformIO/VSCode.
  • Keep holding “BOOT” until you see the upload progress start (dots or percentage in the terminal).
  • Release “BOOT” after the upload starts.

text
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32doit-devkit-v1.html
PLATFORM: Espressif 32 (6.12.0) > DOIT ESP32 DEVKIT V1
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash

esp32 dev kit1 pinout

software setup 1

declare functions defined later before setup

calibrateZeroOffset()

Take 500 samples: zeroOffsetADC is the average of the digital counts of the ADC at 0V

zeroOffsetADC = sum / numSamples;

For the ADS1115, each count at GAIN_ONE (±4.096 V) is 0.125 mV

Calibrating zero offset... ensure CT is not measuring any current

Zero offset calibrated: 12895.45 ADC counts

This corresponds to: 1.612 V bias

python
counts0 = 12895.45
volts0 = counts0 * 0.000125
print(f"Zero offset: {counts0:0.2f} counts = {volts0:0.3f} V")
text
Zero offset: 12895.45 counts = 1.612 V
python
counts7_1A = 10704
volts7_1A = .734
x = volts7_1A / counts7_1A
print(f"x = {x:0.8f},{x*2:0.8f}")
text
x = 0.00006857,0.00013714
python
cz= 16
vz=.0000015
x = vz/cz
print(f"x = {x:0.8f},{x*2:0.8f}") 
text
x = 0.00000009,0.00000019

esp32 code for ct4

goto ct4.ipynb for specific info

Parts

34
QtyValueFootprintReferences
4.1uFCapacitor_THT:C_Disc_D7.5mm_W5.0mm_P5.00mmC2, C3, C5, C7
410KResistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P7.62mm_HorizontalR3, R4, R7, R10
4153KResistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P7.62mm_HorizontalR1, R6, R9, R12
41uFCapacitor_THT:C_Disc_D7.5mm_W5.0mm_P5.00mmC1, C4, C6, C8
42.5Package_TO_SOT_THT:TO-92_InlineU1, U3, U5, U6
482KResistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P7.62mm_HorizontalR2, R5, R8, R11
4AudioJack3footprints:audiojack_female_3pin_3.5mmJ1, J2, J3, J4
4MountingHoleMountingHole:MountingHole_3.5mmH1, H2, H3, H4
1ADS1115footprints:ADC1115_ADC_ModuleU2
1~footprints:ESP32_30pinU4

Questions

How big is the Ct4 PCB?

It measures 76.2 × 104.1 mm and has 4 copper layers, 1.6 mm thick.

What parts does the Ct4 use?

34 parts, built around the ADS1115. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Ct4 KiCad files?

From the mckennatim/ct4 repository on GitHub, at the commit this page was made from: the board is kicad8/ct4/ct4.kicad_pcb and its schematic kicad8/ct4/breadboard/breadboard.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Ct4 design in my own project?

Its repository states no licence, so its author keeps all rights: ask mckennatim before reusing the design.

Similar boards

  • 2 layers168.9 × 123.3 mmOEM ECU, the printed circuit board in 3D

    OEM ECU

    aeonSolutions/AeonLabs-AI-Volvo-MKII-Open-Hardware

    Volvo MKII Open Hardware

    296 parts161CC0-1.0
  • 2 layers94 × 79 mm5 - Lizard Car, the printed circuit board in 3D

    5 - Lizard Car

    TRD-B/Lizard

    3D printed RC race car with custom electronics

    54 parts129CC-BY-4.0
  • 2 layers54.6 × 33 mmJuntekOnSteroidsTTGO, the printed circuit board in 3D

    JuntekOnSteroidsTTGO

    rin67630/Drok-Juntek-on-steroids

    Extend the possibilities of cheap digital power supplies with a WiFi module

    35 parts62
  • 2 layers155 × 70 mmPayphone v1, the printed circuit board in 3D

    Payphone v1

    sdylewski/Analog-Redial

    A circuit board for old telephones to come back to life. Plug one or two phones in, and you can call the other phone, use bluetooth, connect to others via voip, and do a variety of other things with a plain old rotary…

    137 parts46
  • 2 layers32.4 × 33 mmOpenXsensor, the printed circuit board in 3D

    OpenXsensor

    Ingwie/OpenAVRc_Hw

    OpenAVRc Hardware and addons

    40 parts26
  • 2 layers33.9 × 90 mmBiasPro InputProtection v3, the printed circuit board in 3D

    BiasPro InputProtection v3

    ToneAlchemy/BiasPro

    A professional-grade, dual-probe Arduino Bias Meter for guitar tube amplifiers. Open source hardware & firmware

    18 parts16MIT
  • 4 layers52.5 × 59.7 mmESPHome Indoor Multi Sensor, the printed circuit board in 3D

    ESPHome Indoor Multi Sensor

    mikelawrence/esphome-indoor-multi-sensor-hardware

    ESPHome Indoor Multi-Sensor hardware. This design supports Radar/PIR Presence, Temperature, Humidity, Pressure, Light, CO2, CO, VOC, NOX and PM sensors.

    113 parts14CERN-OHL-P-2.0
  • 4 layers260 × 113 mmCellKeeper, the printed circuit board in 3D

    CellKeeper

    cellkeeper-bms/cellkeeper-hardware

    PCB for the CellKeeper BMS

    480 parts11GPL-3.0