Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Heartbeat Business Card

This is my business card. You put a finger on it and it shows you your own pulse.

I do biomedical instrumentation — near-infrared light through tissue, mostly fNIRS. Handing someone a piece of paper felt like a missed opportunity to just show them what that means. So the card has an optical pulse sensor on the front. It lights your fingertip up red at 660 nm, measures at 880 nm in the infrared where you can't see it, and beats a ten-LED heart in time with your own.

It also has an NFC tag, so if you'd rather not put your finger on a stranger's circuit board you can just tap your phone and get the vCard. That works with the card switched off — the tag runs on the phone's field.

Front of the assembled card

Back of the assembled card

Rev 1, assembled and working.

demo.mp4

Contents


At a glance

Form factor 85.6 × 54 mm — ISO ID-1, same as a credit card
Stackup 2-layer FR4, 1.6 mm, purple soldermask, ENIG
Assembly Single-sided, everything on top
MCU ATtiny1616, VQFN-20, UPDI, 5 MHz
Sensor MAX30102 optical PPG, I²C 0x57
NFC NT3H2111 (NTAG I²C plus 1k), I²C 0x55, PCB antenna
Power CR2032 → slide switch → boost to 3V3 → LDO to 1V8
Output 10 red LEDs in a heart, 1 green power LED
Measured 3.1 mA idle, 4.4 mA with a finger on it, 0.06 µA off

System overview

Power

A coin cell is a lousy supply for anything that pulses — high internal resistance, and the voltage sags the moment the sensor fires its LEDs. So rather than run off 3.0 V and watch it droop, everything sits behind a TPS61221 boost at a steady 3.3 V. The MAX30102 then gets its own XC6206 LDO at 1.8 V on top of that, because its analog supply wants to be quiet while its LED driver pin sinks the actual current and is fine on the noisier rail. The sensor's analog ground returns through a ferrite bead.

The slide switch cuts everything, so off means off. A U.FL connector sits across the cell terminals so I can run the board from a bench supply during bring-up without a cell in the holder, and a 100 µF bulk cap on the switched rail buffers the bursts the cell can't follow.

rev 1 errata — the switch legend is backwards. ON and OFF are swapped in the silkscreen: the position marked OFF is the one that powers the card. The board itself is correct, only the label is wrong. Fixed in rev 2.

Current budget

Everything on the card is sized from one number: how much a CR2032 can deliver before its own internal resistance pulls the rail down. Worst case is all ten heart LEDs lit at once.

at 3V3
D1–D10, 1 kΩ, Vf ≈ 1.75 V 1.55 mA each → 15.5 mA
D11, 4.7 kΩ 0.29 mA
ATtiny1616 @ 5 MHz + MAX30102, active ~5 mA
3V3 rail ~20.8 mA

Referred through the boost at a 3.0 V cell and ~87 % efficiency:

Iin = 20.8 mA × 3.3 V / (3.0 V × 0.87) ≈ 26 mA from the cell

Droop is then just that current across the cell's internal resistance:

cell Rint droop terminal
fresh ~15 Ω 0.39 V 2.6 V
aged ~50 Ω 1.3 V ~1.7 V

Both survive — the TPS61221 keeps regulating down to 0.7 V once started — and the fresh-cell figure is what the LED resistors were chosen against.

The MCU clock matters more than it looks: at 20 MHz the ATtiny alone draws ~11 mA, almost as much as all ten LEDs together, which pushes the total to 34 mA and the droop to 0.5 V. Soft PWM for ten LEDs plus I²C runs fine at 5 MHz, so it runs at 5 MHz.

That was the design budget. What it actually draws is about a fifth of it.

The sensor

A MAX30102 — red and infrared emitters, photodiode, 18-bit ADC and a 32-sample FIFO in one package. It hands out raw reflectance counts; baseline removal, gain tracking and beat detection all happen on the ATtiny.

Both wavelengths run, for different reasons. Infrared does the actual work, because hemoglobin absorption at 880 nm gives the cleanest pulsatile signal through a fingertip. The red one is mostly theatre: it makes your fingertip glow, which is how you know the thing is looking at you. That's what makes people put their finger down in the first place.

While it waits for a finger the sensor runs at 50 Hz with the IR emitter turned down and the red one off entirely. A finger brings it up to 100 Hz with both emitters at full drive. That difference is 3.1 mA against 4.4 mA, which is worth having on a card that might get left switched on in a pocket.

NFC

NT3H2111, an NTAG I²C plus, with two ways into the same 1 kB of memory. The RF side is fully passive: tap a phone, get the vCard, works with no cell in the holder at all. The I²C side sits on the same bus as the sensor, so the MCU can rewrite the NDEF record at runtime — and the tag's field-detect pin goes to the MCU too, so the card notices being tapped and runs a lap of the heart. For phones that won't do NFC there's also a 10 mm QR code on the front silkscreen, pointing at my GitHub.

One thing that isn't obvious going in: a blank tag shows a phone nothing at all, however healthy it looks over I²C. The capability container ships as zeros and has to be written before any NDEF message can be read. The bring-up sketch does that once per board, carefully — block 0 also holds the I²C address byte and the static lock bits, so writing back what you just read there would move the tag to a different address, permanently.

The antenna is a spiral on the top copper using the Class 5 geometry from NXP AN11276, redrawn as a KiCad footprint. Two things drove the placement: a copper keepout spanning both layers under the coil, because copper opposite the antenna kills the coupling through eddy currents, and the coin cell sitting diagonally opposite, because a CR2032 is a big steel disc and detunes it.

The heart

Ten red 0603 LEDs in a heart outline, each with its own resistor and its own GPIO. No FET, no charlieplexing, no driver IC. It eats ten pins, which on a 20-pin part is most of them, but every LED can be dimmed independently — and that's the point, because a heart that just blinks looks like a novelty toy, while a heart with brightness sweeping around the contour looks alive.

The ATtiny has nowhere near ten PWM channels, and the three pins that do have timers are taken by I²C and the UART, so all ten are bit-banged: a 6-bit software PWM, one frame of 64 steps taking about 1.6 ms at 5 MHz. Fast enough not to flicker, cheap enough to run in a blocking loop between sensor reads.

The series resistors are 1 kΩ, putting each LED at about 1.5 mA. Dim by datasheet standards, still clearly visible indoors, and low enough that all ten can be on at once with no current budgeting in firmware. The green power LED runs at 4.7 kΩ.

A beat is a bright front rising out of the point and spreading up both sides at once, over a whole-heart glow that fades between beats. Two other styles are in the source — a front sweeping once round the contour, and the whole outline flashing together — but flashing reads as a novelty blinker, and something travelling is the entire reason for ten separate pins. Both the contour order and the mirrored pairs come out of the footprint centres in the KiCad file, since the reference designators don't run round the outline.

The back

The back of a two-layer board is mostly empty space, and a block of silkscreen text felt like a waste of it. PCB art seemed like the better use.

I went with Joy Division's Unknown Pleasures — best album cover ever made, as far as I'm concerned, and the story behind it is as good as the design. The image is a stack of successive radio pulses from PSR J1921+2153, CP 1919 back then: the first pulsar ever found, picked up at Cambridge in 1967 by Jocelyn Bell Burnell during her own PhD. The plot came a few years later, out of Harold D. Craft Jr.'s 1970 doctoral thesis at Cornell, drawn from Arecibo data. Stacking is the whole point of it: one image shows that the individual pulse shapes vary considerably while the 1.337 s period does not. A thesis figure, drawn that way because it made a real result legible at a glance, and good enough to end up on a record sleeve.

Craft programmed that plot himself, on a CDC 3200 at the observatory — including the routine that fills in under each curve so the ones behind stay hidden, which is the whole reason the image reads at all. The plotter output was then traced over in India ink back at Cornell. It's one of the earliest scientific figures that was programmed rather than drawn, and occlude() in my script does the same job his routine did.

I rebuilt it with my own data: an fNIRS recording of my own head, taken with hardware I built for my master's thesis, which is what led into my current PhD. SiPM detector, 3.5 cm source–detector separation. The individual heartbeats get pulled out of the recording and stacked into ridgelines the same way.

The design tool lives in hardware/pcb/back-silkscreen/: the raw recording and the Python that segments the beats, shapes them and renders a preview. The geometry that actually ships was emitted straight into the KiCad file as silkscreen lines, so the preview is for judging the composition, not a fabrication source.

It comes out as about 7300 line segments, which was the one part of this board I expected a fab to struggle with — 0.25 mm silkscreen lines packed that tightly sit close to what most of them quote as a minimum. It came back crisp.

Back silkscreen layer in KiCad

The artwork on its own in the layout editor.

Pin map

Pin Function
PA0 UPDI
PA1, PA3–PA7 Heart LEDs 1–6
PA2 Sensor interrupt
PB0 / PB1 I²C SCL / SDA, 4.7 kΩ pull-ups
PB2 / PB3 USART0 TX / RX, on test points
PB4 / PB5 Heart LEDs 8, 9
PC0 NFC field detect
PC1 Power LED
PC2 / PC3 Heart LEDs 7, 10

Every pin is used. TX and RX come out on labelled test points rather than a header, because I'll use them a handful of times and then never again.

Programming is over UPDI through a keyed header — 1.27 mm through-hole raster, four positions with the second left empty, so the programmer physically can't go in backwards.

Firmware

Two sketches, both megaTinyCore, both at 5 MHz:

megaTinyCore:megaavr:atxy6:chip=1616,clock=5internal

firmware/heartbeat_card is the card itself: startup animation, a dim point chasing round the contour while it waits, the beat animation once a finger is down, and a lap of the heart when a phone taps the antenna.

firmware/pcb_bringup is the per-board test, and it's the one worth reading first — it has its own README. The heart doubles as the test display: ten checks, one per LED, filling from the point upwards, all ten lit is the pass criterion. The top LED of each half needs a human — one for a phone tap, one for a finger — so the last few can't be faked by a dead board. It also provisions the NFC tag on first run and prints a one-line RESULT you can paste into a build log.

That one is work in progress. The structure holds up and the report is genuinely useful, but the individual checks aren't robust yet — a pass currently means nothing is obviously dead, not that the board is good. Treat it as a smoke test rather than an acceptance criterion.

The signal chain is all integer maths: IIR baseline removal, an envelope tracker for automatic gain so it works across different fingers without recalibration, and a peak detector.

The detector's refractory period is the part that needed real measurement. A naive threshold reads the dicrotic notch as a second beat and reports roughly double your actual heart rate, and the PPK2 trace showed the obvious fix wasn't enough: the notch fired 320 and 360 ms after a real beat while the true interval was 600–760 ms, so a 300 ms guard let it straight through. It's now a 450 ms floor plus 60 % of the running interval — and the running average itself refuses anything under 450 ms, because otherwise an accepted double drags the average down until the adaptive guard shrinks to match and the doubling sustains itself.

What it actually draws

Measured on the assembled card with a Nordic PPK2 in source-meter mode, fed through the U.FL across the cell terminals — so these are cell-side figures, including the boost's own losses. Full write-up in docs/power-report.md.

state current
Switched off 0.06–0.13 µA
Idle, waiting for a finger 3.10 mA
Finger down, beating 4.0–4.8 mA, mean 4.4
Ten LEDs at full brightness +9.1 mA
Power-on inrush 1.52 A peak, ~25 ms

Off really is off — 0.06 µA is at the floor of what the instrument can resolve, so shelf life is the cell's own self-discharge and nothing else. On a 220 mAh cell, idle runs about 71 hours; a 30 second demo costs roughly 40 µAh, so one cell is good for something like 5500 demos. Handing the card around is effectively free. The only way to flatten one is to leave the switch on.

Against the budget above: measured worst case in normal operation is 4.8 mA rather than 26 mA, and even the full-heart flash at startup only reaches 11.6 mA. The soft PWM means the LEDs are never continuously lit, so the design budget turned out about five times pessimistic. There is a lot of headroom in those LED resistors if I ever want a brighter heart.

The report also documents one open bug: the sensor doesn't drop back to its 50 Hz idle rate after a finger has been removed, which costs 0.65 mA. The finger-detect threshold is the likely culprit.

Getting it made

Production files are in hardware/pcb/business-card-pcb/jlcpcb/, generated from KiCad, alongside a schematic PDF, a front-and-back plot, a STEP export and an interactive BOM for hand assembly.

Everything reflows, in one pass. There are pads for an NFC tuning cap built into the antenna footprint in case the coil needed pulling onto 13.56 MHz — it didn't. The AN11276 Class 5 geometry lands close enough as drawn, so C12 stays unpopulated and phones read the tag without it.

Five footprints aren't in the stock KiCad libraries and live in business-card-pcb.pretty/: the AN11276 antenna, the NT3H2111 in its NXP TSSOP variant, the CR2032 holder, the slide switch and the UPDI header.

Layout in KiCad

All layers, with the board outline dimensioned: 85.6 × 54 mm, 2.5 mm corner radius, and the notch that clears the switch actuator.

References

Third-party app notes, datasheets and vendor code stay out of the repo and are linked instead.

Datasheets: MAX30102 · NT3H2111 · ATtiny1616 · TPS61221 · XC6206

The antenna footprint is my own KiCad work, derived from the AN11276 Class 5 dimensions.

License

Hardware under CERN-OHL-S-2.0, firmware under MIT. Use it, modify it, build it, sell it — just release hardware modifications under the same licence.

About

A PCB business card that shows you your own pulse. Optical PPG sensor drives a ten-LED heart; NFC tag serves the vCard with the card switched off. ATtiny1616, MAX30102, CR2032. KiCad sources, firmware and power measurements.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages