

Sensor pcb
Open-source capacitive linear encoder / DIY DRO scale: KiCad PCBs, STM32 firmware, and test data. Same principle as digital calipers.
- Size
- 57 × 33 mm
- Copper layers
- 4
- Thickness
- 1.6 mm
- Parts
- 69
- Nets
- 58
- Pads
- 240
- Tracks, vias
- 509, 226
- Pours
- 70
- Routed
- Yes
- KiCad
- KiCad 9.0 (file 20241229)
- Created
- 23 Sep 2026
- Updated
- 22 Oct 2026
Overview
Sensor pcb is an open-source printed circuit board by nichlock, from the nichlock/open-linear-scale repository on GitHub. It is a 4-layer board of 57 × 33 mm with 69 parts on 58 nets, routed with 509 tracks and 226 vias.
It is built around the SKRKAHE020, APT3216LSECK/J3-PRV and APT3216LZGCK, with the BSS138BKSH, JR200 and S4B-PH-SM4-TB among its other parts. Its parts include 20 capacitors, 16 resistors, 3 ICs, 3 connectors and 2 diodes. It belongs with the sensors and measurement boards and the STM32 boards of this library.
Main components
7SKRKAHE020
SMD,3.9x2.9mm-40℃~+85℃ 12V 1N 2.9mm 200,000 Cycles 2mm 3.9mm 50mA Round Button SMD(SMT) Tab SPST-NO Surface Mount,Vertical SMD,3.9x2.9mm Tactile Switches ROHSALPSALPINETactile SwitchesAPT3216LSECK/J3-PRV
1206-40℃~+85℃ 1.8V 140° 2mA 40mcd 625nm 640nm Red Water Clear 1206 LED Indication - Discrete ROHSKingbrightLED Indication - DiscreteAPT3216LZGCK
SMD,3.2x1.6mm-40℃~+85℃ 102.5mW 150° 2.65V 2mA 50mcd 515nm 525nm Discrete Diode Emerald Green Water Clear SMD,3.2x1.6mm LED Indication - Discrete ROHSKingbrightLED Indication - DiscreteBSS138BKSH
TSSOP-6-1.3mm-55℃~+150℃ 1.6V 1.6Ω@10V 2 N-Channel 280mW 320mA 56pF 60V 700pC@4.5V TSSOP-6-1.3mm MOSFETs ROHSNexperiaMOSFETsJR200
SMD-40℃~+85℃ 1.15mm 125V 1500@1MHz 4.5pF~20pF SMD Trimmers, Variable Capacitors ROHSKnowlesTrimmers, Variable CapacitorsS4B-PH-SM4-TB
UncategorisedJSTS5B-PH-SM4-TB
SMD,P=2mm,5PUncategorisedJST
README
README.mdOpen-Source Capacitive Linear Encoder
This is an open-source capacitive encoder design with hardware, firmware, test data, and design details. This design is still in development. It's targeted towards open-source DRO sensors and digital calipers. The build costs roughly $32 before shipping.

Overview
| Parameter | Value |
|---|---|
| Encoder Type | Incremental |
| Method | Capacitive |
| Accuracy | 40µm (1.6 thou)* |
| Output Type | UART |
*Measured against a Mitutoyo 543-793B-12 at room temperature. This value is based on the electrical precision and resolution. See Practical Accuracy Measurements for a summary of the measured error.
Quickstart
- Order the 4-layer sensor and the reflector (hardware).
- Assemble.
- Flash the firmware.
- Apply Kapton for the 0.1mm gap.
- Set the trimmer.
- Run
LiveView.pywith your serial port, for examplepython LiveView.py COM3(tests).
Why Make This
This sensor came out of a long-running personal project. I started by reverse-engineering digital capacitive calipers. I wanted to publish this working capacitive encoder because I had trouble finding open information about them.
I hope this project is useful to someone, but my real goal was to learn, not to replace existing pro-grade encoders.
What's Included
This is a capacitive encoder, the same technology found in older pro-grade capacitive linear scales. Today, capacitive encoders are mostly found in hobby-grade digital calipers.
It is also incremental. It measures position precisely within each 5.08mm period, and counts how many periods it has passed through. At power-up it assumes it is at position zero, so it needs zeroing or homing every time it starts.
The design is split into folders by discipline:
hardware: The KiCad PCB designs.firmware: Firmware for the sensor PCB.mechanical: Mechanical test fixtures.tests: Test and analysis scripts.
Each folder has its own README with deeper dives into its topics.
How it Works
The encoder has two parts: a sensor PCB that moves along the axis, and a passive reflector PCB fixed along its length. They do not touch. The copper faces sit about 0.1mm apart.
| Sensor PCB Bottom | Sensor PCB Top |
|---|---|
![]() | ![]() |
The sensor PCB has a sensing grid of 8 repeating transmit antennas, which I've called emitters. It also has an RX antenna, which feeds a charge integrator and an anti-alias filter. The analog hardware is fully integrated within the STM32L4. See Analog Frontend for more design information.
| Reflector PCB Top |
|---|
![]() |
The reflector has a row of T-shaped copper pads. They "reflect" the capacitance of four of the eight repeating emitters into the RX antenna.
The position of the reflector relative to the sensor decides which emitters couple, and how strongly.
Measurement cycle. The STM32 steps its 8 emitter pins through a fixed 16-step pattern. At each step, some emitters switch low-to-high or high-to-low. Charge couples through the reflector into the RX antenna. A charge integrator captures it, a low-pass filter smooths it, and the ADC oversamples it. The steps run at 16kHz, so a full sequence completes at 1kHz.
Position estimate. Each sequence gives a position within one period. The Functional Description explains how. Unwrapping successive estimates gives a continuous position. For this to work, the sensor can't be moving too fast for the algorithm to track. Conservatively, the maximum speed might be 0.6m/s, although I have not tested this.
Output. The sensor outputs a hex-equivalent floating point value over UART at 460800 baud, every millisecond. The script tests/LiveView.py plots a position on the host. The sensor can also output other formats and debugging information.
Status and Limitations
A capacitive encoder is sensitive to liquids between the boards. At these accuracies, thermal expansion over long distances also matters. Plan for both if you would like to integrate with this.
When I started, I hoped for 10µm electrical accuracy. My first revision was about 0.5mm. I'm still working towards 10µm, along with a few other features:
- Automatic electrical calibration.
- Temperature compensation.
- Less reliance on the STM32L4, which is getting harder to source. The plan is a more standard STM32 with a discrete analog frontend.
- Improvements to the layout and mechanical design.
- Absolute position sensing.
I'd welcome help with these! Especially the mechanical design, which is far outside my expertise. See CONTRIBUTING.md for how to get involved.
Practical Accuracy Measurements
I give an accuracy 'rating' of 40µm. I measured the error against a Mitutoyo 543-793B-12 digital indicator. The mechanical README has more detail on the setup. I read the accuracy figure visually from the plots and rounded it up conservatively. My overall design isn't mature enough for a true tolerance stackup yet. The tests README has an in-depth analysis. Here is a summary of the major error sources:
- Systematic error is dominated by a periodic error that correlates with position. It's the large triangular deviations around 1–3mm and 7–9mm. There is also a smaller error that repeats every emitter (0.635mm) and drift that most likely comes from my test assembly.
- Noise is smaller than the systematic error. It is mostly white noise, plus a low-frequency resonance of my 3D printer.
Functional Description
Measurement Cycle
The diagram below gives an overview of each step of a position measurement:
The emitters switch at 16kHz, stepping through 16 states. After all 16, the measurements go through the position algorithm.
The timing diagram below shows an example of the 16 states:
The integrator output always has roughly the shape shown in the plot. As the position changes, it appears phase-shifted relative to the zero position.
The Position Algorithm
This section builds up the reasoning behind each step of the position algorithm. The final steps are best read in the code.
The sensor measures how the reflector lines up with the emitters, which are coupled to the RX antenna through capacitance.
Consider a single emitter. If the reflector overlaps it, there is a strong capacitance from the emitter to the RX antenna. If not, there is no extra capacitance. When an overlapped emitter switches from low to high, the RX antenna sees a pulse.
When the emitter is only partly overlapped, the coupling falls roughly linearly with movement.
With more emitters, the effect adds up linearly. By driving the first four emitters low-to-high and the next four high-to-low, the size of the pulse tells us where the reflector is.
In the plot, "coupling" goes negative. That means the reflector is aligned with the emitters making the negative transition, not that the capacitance is negative.
That is technically enough to get a position, but there are two problems:
- We don't know the pulse size of a fully overlapped emitter, and it changes over time.
- Because the pattern repeats, each pulse size matches two possible positions. In the plot below, a measurement could mean 3.1 or 4.9, and we can't tell which.
Taking more measurements solves both problems. Instead of sampling one location, we "slide" the pattern across all eight emitters, giving eight pulse sizes. The second four are the negatives of the first four, so they combine into four pulses.
Each of the four pulses gives two candidate positions, and we could pick the one they all agree on. Near the edges, though, noise makes that choice unreliable. The resulting error is larger than the noise itself, and it is nonlinear.
Finding the zero crossing of the line through the pulses avoids the nonlinearity and gives lower noise. Depending on the convention, there may be a constant offset (-2 in the plot below).
A few practical problems remain:
- The samples don't always cross zero.
- A digital line can't repeatedly step in the same direction.
- The emitters and reflector pads need clearance between them.
Pulse Sequences That Don't Cross Zero
The signal is periodic, so we can wrap the first pulse around after the last one and interpolate there. A virtual sample at location 4, equal to the negative of location 0, always creates a zero crossing if there wasn't one already.
Repeated Positive/Negative Pulses
True sliding pulses would need the emitter voltages to keep stepping up. A digital line can only go low-to-high once; it has to go high-to-low before it can rise again.
My solution is a partial transient between the full ones, which flips three emitters in each half. Why not flip just one? Because this extra transient can also give a position. Three emitters make a larger signal, and one that works with the same interpolation method.
The three-emitter transients give a pulse sequence much like the four-emitter one. They only reach 3/4 of the magnitude, but within that range the signal looks the same, so the same zero-crossing method gives a position.
That estimate is shifted ahead by 0.5 emitters, so adding 0.5 brings it into line.
Non-Zero Spacing
The emitters and reflector pads need electrical clearance between them.
A naive parallel-plate model would say the signal doesn't change while an edge passes over this gap, so positions inside it can't be recovered.
In reality, as long as the gap is small compared to the copper width, fringing at the copper edges adds significant capacitance. The transition is smooth, but unfortunately not a straight line.
Separating the sensor and reflector reduces the problem. I usually use about 0.1mm. The plot below shows the error that remains, despite fringing, when the PCBs touch.
Combined Symmetric Measurements
The design takes advantage of a few symmetries in the emitter pattern.
Of the 8 emitters, the second half is always driven as the inverse of the first half. The emitter states are states in time, but what we measure is the difference from one state to the next. The firmware README shows how this maps to the code.
The second half of the sequence is also an inverted repeat of the first half. Each transient in the second half is the negative of its partner in the first half, so subtracting the two doubles the signal.
The transients also interleave. They alternate between flipping three emitters in each half and flipping four. I call these two interleaved sets lanes, which leaves two lanes of four values each. Most of my analysis only considers four emitters and four transients, because that describes most of the signal.
Each lane gives one zero-crossing estimate with its own offset. After applying the offsets, the estimates are averaged.
The offsets are relative. It doesn't matter which lane is treated as the reference, as long as the difference between them is consistent.
Analog Frontend
The RX antenna receives a voltage pulse. Its height depends on the capacitive coupling, and how long it lasts depends on resistive leakage.
You can sample that pulse directly with the ADC, but only for low accuracy. It needs very short sample windows and can't be anti-alias filtered well, so it picks up a lot of noise.
Instead, the RX antenna feeds a charge integrator. The ratio of its two capacitors sets its gain. The integrator turns the short pulse into a held step. The ADC can then oversample over a long window, which improves SNR.
An anti-alias filter follows the integrator. It removes the above-Nyquist electrical noise that the integrator passes through.
The firmware measures the change from one integrator state to the next, so the integrator never needs a reset.
The hardware README covers the design in more detail with rationale.
Mechanical Integration
The sensor and reflector PCBs are both aligned with 3mm pins and mounted with M3 fasteners.
The sensor and reflector need a gap between them. I aim for 0.1mm. Without it, the signal is larger, but physical nonlinearities wipe out any SNR benefit.
I haven't measured how sensitive the sensor is to a gap, sideways offset or tilt. The effect doesn't seem large, and in theory the design should tolerate all three reasonably well.
I tried a 3D printed shim to set the gap, but two layers of Kapton tape turned out to be much easier and more accurate.
The mechanical README covers a typical mechanical stackup and the test fixtures.
License
This project uses two licenses:
| Folders | License |
|---|---|
hardware/, mechanical/ | CERN-OHL-P-2.0 |
firmware/, tests/, docs/ | MIT |
Both are permissive. You can use, modify, make and sell the designs and code, including commercially, as long as you keep the copyright and license notices.
Some files are covered by their own licenses instead:
firmware/src/cube-mx-autogen/contains files generated by STM32CubeMX, © STMicroelectronics. They are under BSD-3-Clause or Apache-2.0, depending on the file. See the license file in that folder.
See LICENSE for the summary.
Parts
69| Qty | Value | Footprint | References |
|---|---|---|---|
| 9 | 1uF | Capacitor_SMD:C_0603_1608Metric | C1, C2, C3, C4, C11, C13, C14, C19, C20 |
| 9 | TestPoint | TestPoint:TestPoint_Pad_D1.0mm | TP3, TP4, TP5, TP7, TP9, TP11, TP13, TP15, TP16 |
| 7 | 10k | Resistor_SMD:R_0603_1608Metric | R3, R5, R11, R12, R13, R14, R15 |
| 6 | 0.1uF | Capacitor_SMD:C_0402_1005Metric | C5, C6, C7, C8, C9, C10 |
| 6 | TP_Keystone_5015 | TestPoint:TestPoint_Keystone_5015_Micro_Mini | TP6, TP8, TP10, TP12, TP14, TP17 |
| 4 | M3 | MountingHole:MountingHole_3.2mm_M3_Pad_Via | MH1, MH2, MH3, MH4 |
| 3 | 0R | Resistor_SMD:R_0603_1608Metric | R2, R9, R16 |
| 3 | Fiducial | Fiducial:Fiducial_0.5mm_Mask1mm | FID1, FID2, FID3 |
| 2 | 5.1k | Resistor_SMD:R_0603_1608Metric | R4, R6 |
| 2 | 510 | Resistor_SMD:R_0603_1608Metric | R1, R8 |
| 2 | AP7354-33W5-7 | Package_TO_SOT_SMD:SOT-23-5_HandSoldering | U2, U4 |
| 2 | SKRKAHE020 | Button_Switch_SMD:SW_Push_SPST_NO_Alps_SKRK | SW1, SW2 |
| 1 | 1.1nF | Capacitor_SMD:C_0603_1608Metric | C17 |
| 1 | 100M | Resistor_SMD:R_0603_1608Metric | R10 |
| 1 | 1pF | Capacitor_SMD:C_0402_1005Metric_Pad0.74x0.62mm_HandSolder | C16 |
| 1 | 2.2nF | Capacitor_SMD:C_0603_1608Metric | C18 |
| 1 | 4.7uF | Capacitor_SMD:C_0805_2012Metric | C12 |
| 1 | 680k | Resistor_SMD:R_0603_1608Metric | R7 |
| 1 | APT3216LSECK/J3-PRV | LED_SMD:LED_1206_3216Metric | D2 |
| 1 | APT3216LZGCK | LED_SMD:LED_1206_3216Metric | D1 |
| 1 | BSS138BKSH | Package_TO_SOT_SMD:SOT-363_SC-70-6 | Q1 |
| 1 | Conn_ARM_SWD_TagConnect_TC2030 | Connector:Tag-Connect_TC2030-IDC-FP_2x03_P1.27mm_Vertical | J1 |
| 1 | JR200 | JRx-trimmer:JRx-trimmer | C15 |
| 1 | S4B-PH-SM4-TB | Connector_JST:JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal | J4 |
| 1 | S5B-PH-SM4-TB | Connector_JST:JST_PH_S5B-PH-SM4-TB_1x05-1MP_P2.00mm_Horizontal | J2 |
| 1 | STM32L476RCTx | Package_QFP:LQFP-64_10x10mm_P0.5mm | U1 |
Questions
How big is the Sensor pcb PCB?
It measures 57 × 33 mm and has 4 copper layers, 1.6 mm thick.
What parts does the Sensor pcb use?
69 parts, built around the SKRKAHE020, APT3216LSECK/J3-PRV, APT3216LZGCK and BSS138BKSH. 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 Sensor pcb KiCad files?
From the nichlock/open-linear-scale repository on GitHub, at the commit this page was made from: the board is hardware/sensor-pcb/sensor-pcb.kicad_pcb and its schematic hardware/sensor-pcb/sensor-pcb.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Can I use the Sensor pcb design in my own project?
Its repository states no licence, so its author keeps all rights: ask nichlock before reusing the design.
Similar boards
- 2 layers114.3 × 114.3 mm

- 4 layers60 × 33 mm

- 6 layers567.7 × 328.4 mm

- 2 layers30 × 21.1 mm

Rp2040 stamp sculpt interface
techberet/msft_sculpt_rp2040_stamp_hw
Lots of details available in my (rather long) blog post.
8 parts9MIT - 4 layers

- 4 layers84.5 × 92 mm

- 4 layers65 × 100 mm

PGR
Projeto-Guarda-Rios/PCB
O repositório para a PCB do Projeto Guarda Rios! Inclui todos os ficheiros relacionados com a PCB!
188 parts2MIT - 4 layers80 × 49 mm



