PROJECT_04

ZKTeco

Control de Acceso Físico

Access control for a school, with a board made at home: the backend decides who gets in, not the reader.

TYPE
Hardware · Full Stack
STATUS
IN PRODUCTION
DIAGRAMS
4
SCREENSHOTS
13
  • React
  • Vite
  • TailwindCSS
  • Java
  • Spring Boot
  • MySQL
  • Arduino / ESP32
  • C++
  • KiCad
  • Wiegand 26/34

01 What it is

Access control for a school, end to end: a custom PCB with an Arduino Nano driving two Wiegand readers and two locks, a ZKTeco PROID10BM MIFARE card reader on each door, a Spring Boot backend that decides who gets in and a web panel to add, remove and block students, hosted on the school own server.

  • pcb_control board Rev A: 170 × 115 mm, 61 through-hole components, 42 nets, ERC and DRC at zero.
  • Two independent Wiegand 26/34 readers, two door relays as dry contacts and two exit buttons.
  • The board decides nothing: it reads the card, sends the ID over serial and opens only when the backend gives the signal.
  • Spring Boot backend that checks whether the student is active or blocked, and a web panel with students, cards, access history and status.

02 The board · Rev A

Designed in KiCad. Everything is through-hole on purpose: no SMD, so the whole board solders with one technique and one iron. Four M3 screws in 3.2 mm holes.

BlockPartsWhat for
BrainArduino Nano v3 (A1)Reads both readers on interrupt and drives relays, buttons, LED and buzzer
PowerPTC F1 1.1 A · SB340 Schottky D7 · MP1584 buck U1 · C1 100 µF · C2 10 µFProtected 12 V; 5 V for the Nano and the pull-ups
Wiegand input ×2P6KE6.8CA TVS D1–D4 · C4–C7 1 nF · R3, R4, R7, R8 220 Ω · pull-ups R1, R2, R5, R6 2.2 kΩProtects the AVR and buys cable length
Relays ×2SRD-12VDC-SL-C K1, K2 · BC337 Q1, Q2 · 1N4148 D5, D6 · snubber R17, R18 100 Ω + C10, C11 100 nF/250 VCOM/NC/NO dry contact to the lock
Reader feedbackULN2003A U2 · unpopulated R22–R25 4.7 kΩ pull-upsLED and buzzer of both readers with a single IC
IndicatorsGreen LED1 (PWR) · red LED2, LED3 (relay 1 and 2)None of them costs a Nano pin
Terminal blocksMX126/DG126 5.00 mm pitch: J2, J3 7-way · J4, J5 3-way · J1, J6, J7 2-wayReaders, doors, 12 V input and exit buttons

03 How it works

Power

12 V comes in through J1, through the PTC F1 (resettable: it recovers on its own once the fault is gone) and the Schottky D7, which blocks reverse polarity. That rail directly feeds the relay coils, both readers and the input of the U1 buck, which steps down to 5 V for the Nano and the pull-ups: about 40 mA in total.

Reading Wiegand

The reader D0 and D1 lines are open collector: at rest both sit at 5 V through the 2.2 kΩ pull-ups. To send a bit, the reader pulls one to ground for ~50 µs; a pulse on D0 = bit 0, a pulse on D1 = bit 1. A 26-bit card is 26 pulses spread across the two lines.

Every line runs TVS → 1 nF filter → 220 Ω series resistor → Nano pin. That chain is what buys distance: Wiegand is not differential and that is why it is sensitive to noise.

Opening and output

When the backend authorizes, the Nano drives D6 high, R9 pushes ~4.3 mA into the base of Q1, the transistor saturates and connects the coil negative to ground. R10 (10 kΩ) keeps the base grounded while the Nano boots: without it the pin floats and the relay could fire on its own at power-up. D5 absorbs the coil reverse spike.

J4 and J5 deliver COM/NC/NO as a dry contact: the relay is just a switch and the lock current never goes through the board. The RC snubber (R17 + C10) bridges COM–NO so the arc does not weld the contacts; RC was chosen over a diode because it has no polarity and works the same with DC or AC loads.

Board LEDs, reader LED and buzzer

Three LEDs and none costs a Nano pin. LED1 (green, PWR) hangs off the 5 V rail: if it lights, the buck works. LED2 and LED3 (red) go from +12 V to the relay coil through R20 and R21: they light exactly when the transistor saturates, so they report the real actuation rather than what the firmware believes. If the relay LED lights and the door does not open, the problem is from the relay outwards; if it does not light, from the transistor inwards. It splits the diagnosis in two.

The reader LED and buzzer are open collector: they must be pulled to ground. Instead of four transistors with their resistors there is a ULN2003A (U2), which brings seven drivers with base resistors and clamp diodes built in; four channels are used. The Nano drives them through A0–A3 and not D10–D13 for a concrete reason: D13 carries the Nano on-board LED and the bootloader blinks it on every boot, so the reader would beep on every reset.

04 Terminal blocks

MX126 / DG126 family, 5.00 mm pitch, 1.3 mm drill and Ø2.8 mm pad. J2 and J3 are rotated 180° so the cable mouth faces the edge, which puts pin 1 on the right: the silkscreen is labelled pad by pad and reads SHIELD · BUZZ · LED · D1 · D0 · GND · 12V left to right. Trust the silkscreen, not the pin number.

BlockUsePins
J2 / J3Reader 1 and 2 · 7-way12V · GND · D0 · D1 · LED · BUZZ · SHIELD
J4 / J5Door 1 and 2 · dry contact · 3-wayCOM · NC · NO
J112 V input · 2-way+12V · GND
J6 / J7Exit button 1 and 2 · 2-wayBTN · GND

05 Nano pinout

The Nano has only two hardware interrupts and four Wiegand lines are needed here. Reader 1 takes INT0/INT1; reader 2 goes through a pin-change interrupt. That is why the firmware cannot be the basic two-line example.

PinFunctionNetNote
D2 · D3Reader 1 · D0 / D1W1_D0 · W1_D1INT0 / INT1, hardware
D4 · D5Reader 2 · D0 / D1W2_D0 · W2_D1PCINT20 / PCINT21
D6 · D7Relay 1 / 2REL1_CTRL · REL2_CTRLhigh = open
D8 · D9Exit button 1 / 2BTN1 · BTN2low = pressed
A0 · A1Reader 1 LED / buzzerRDR1_LED_C · RDR1_BEEP_C→ ULN2003A I1 / I2
A2 · A3Reader 2 LED / buzzerRDR2_LED_C · RDR2_BEEP_C→ ULN2003A I3 / I4
+5V5 V input+5Vfrom the buck; the Nano USB goes to the server and its Schottky keeps it from being back-fed
VINNot connected—on purpose: fed through the 5 V pin

D10–D13 and A4–A7 remain free: room for door sensors, an RTC or storing cards over I²C.

06 Firmware

control_acceso.ino compiles for arduino:avr:nano with no warnings: 6.5 KB of flash (21 %) and 324 B of RAM. It reads both readers on interrupt and assembles the frame bit by bit; closes the read after 25 ms of silence; accepts 26 and 34 bits and validates both parity bits before sending anything. It stores no cards: the ID goes out over the Nano USB, as a 115200 serial port, to the school server, and the backend decides.

  • Sends CARD with the reader number and the ID; if the backend answers OPEN, it opens the relay for 3 s without blocking the loop, with a long beep and green LED; on DENY or a bad frame, three short beeps.
  • Exit buttons with 30 ms debounce.
  • The ID is (facility << 16) | number, the same for Wiegand 26 and 34: both formats collapse to the same 32-bit ID, which is what the server stores when the student is registered.
  • The backend records every read, authorized or not, in the access history.
  • The PCINT2 vector covers the whole port D, so the routine compares against the previous state to keep only the falling edges of PD4 and PD5.
> CARD 1 00A1B2C3    # Nano → backend: lector 1 leyó este ID
> OPEN 1             # backend → Nano: alumno activo, abre el relé 1 tres segundos
> DENY 1             # backend → Nano: bloqueado o desconocido, tres pitidos
> BTN 2              # Nano → backend: botón de salida 2, solo para el historial

07 Resilience

An access controller lives inside a box, with nobody watching it, and has to keep working. These are the decisions made with that in mind, not for elegance.

If…Then
the firmware hangsA 2 s watchdog resets the Nano, and the first thing setup() does is drop both relays: if the reset happened with a door open, it does not stay open
the reader is not 26-bitWiegand 26 and 34 are accepted with their parities; the bit register is 64 bits because a 34-bit frame does not fit in 32
there is noise on the cableA frame that is not exactly 26 or 34 bits discards itself; the counter keeps climbing past the limit so a burst ends up with an impossible n instead of looking like a card
the backend does not answer or the USB is unpluggedThe board stays powered by the buck, not by USB. The door does not open: no OPEN, no opening, and the attempt ends in three beeps. Exit buttons keep working because they are local to the board
a student must be registered or blockedDone in the panel, without touching the board: the card ID is captured by presenting it at the reader and the change applies on the next read
the board has been on for 49 daysmillis() overflows; every wait uses unsigned subtraction, which survives the overflow, and there is not a single delay(): relays, LEDs and beeps are state machines

08 Verification and risks

What was checked and what was not, kept apart on purpose. A passing ERC does not mean the circuit works: during the design, a serious mistake (BC337 base and collector swapped) survived a perfectly clean ERC. It only showed up when reading the netlist net by net and asking KiCad for the real pin order of the transistor.

VerifiedHow
ERC: 0 errors, 0 warningsSchematic electrical rules with --severity-all
Netlist: 42 netsExported and read net by net: base on Q1.2, collector on Q1.1, coil on K1.2, common on K1.1
Footprints: 61 of 61Every footprint checked against its real .kicad_mod, pin to pad
DRC: 0 violationsBoard design rules
Reader LED and buzzer polarityAgainst HID documentation, the industry reference: active low, 0–12 V / 100 mA, within ULN2003A spec

DIAGRAMS

−0.4 V 12 V 5 V J1 · 12 V external supply F1 · PTC 1.1 A resettable D7 · SB340 blocks reverse polarity ~11.6 V rail C1 100 µF K1 · K2 coils and both readers U1 · MP1584 12 V → 5 V buck Arduino Nano + pull-ups · ~40 mA LED1 · PWR if it lights, the buck works
01 Power: protected 12 V, 5 V for the brain
W1_D0 ZKTeco reader D0 = bit 0 · D1 = bit 1 J2 · 7-way shield grounded only here D1 · P6KE6.8CA bidirectional TVS C4 · 1 nF line filter R3 · 220 Ω series to the AVR R1 · 2.2 kΩ to 5 V pull-up: idle high D2 · INT0 falling edge reader 2: same path to D4 · PCINT20
02 A Wiegand bit: from reader to pin
COIL CLOSES NO Nano · D6 high = open · 3 s R9 · 1 kΩ ~4.3 mA into the base Q1 · BC337 saturates: coil to ground K1 · SRD-12VDC D5 absorbs the reverse spike J4 · COM / NC / NO dry contact R10 · 10 kΩ base grounded during boot LED2 · red across the coil 12 V lock own supply · RC snubber LED on and door closed → from the relay outwardsLED off → from the transistor inwards
03 Opening the door without the current crossing the board
ID USB OPEN 1 DENY 1 Interrupt assembles the frame bit by bit 26 or 34 bits + parities after 25 ms of silence Serial 115200 CARD 1 00A1B2C3 Spring Boot backend active or blocked? Relay 3 s long beep · green LED Denied three short beeps Noise impossible n, not card-like History every read, pass or not
04 The board reads, the server decides

01 04

01 Power: protected 12 V, 5 V for the brain
−0.4 V 12 V 5 V J1 · 12 V external supply F1 · PTC 1.1 A resettable D7 · SB340 blocks reverse polarity ~11.6 V rail C1 100 µF K1 · K2 coils and both readers U1 · MP1584 12 V → 5 V buck Arduino Nano + pull-ups · ~40 mA LED1 · PWR if it lights, the buck works

the Schottky costs ~0.4 V and the rail sits at ~11.6 V · the relay pulls in at ~9 V · the Nano is fed through its 5 V pin and VIN stays floating

SCREENSHOTS

Schematic of the pcb_control Rev A board in KiCad: Nano, two Wiegand inputs with TVS and filters, two relays with BC337 and snubber, ULN2003A and terminal blocks
Schematic · Rev A
ZKTeco PROID10BM MIFARE card reader used in the access control system
ZKTeco PROID10BM reader
ZKTeco reader installed next to the entrance door
On-site installation
Design of the custom PCB with Arduino and relay for the electric lock
PCB design
Trace routing view of the access control board
Trace routing
3D view of the board with connectors and step-down supplies
3D view of the board
3D view of the board from the back, showing the reader terminal silkscreen
Terminal silkscreen
Login screen of the access control web panel
Panel login
List of registered students in the admin panel
Registered students
Student registration form and card assignment
Student registration
Access history log with date and time
Access history
Admin panel showing student status
Student status
Settings view of the access control system
Settings

01 01