Open-source project
PaulKlinger/cnn_card avatar
PaulKlinger/cnn_card

PaulKlinger/cnn_card: a convolutional network on a business card, activations and all

Business card running / showing the internals of a CNN to classify 5x5 digits

100 stars5 forksCMIT

At a glance

What is it?
A business card that classifies five-by-five pixel digits with a small convolutional network and lights up the internal activations of every layer on its own LEDs, driven by two shift registers and a coin cell. The interesting part for anyone building embedded machine learning is the path the model takes to get there, which runs from a training notebook to a generated C file inside a microcontroller project.
Who is it for?
Use this repository as a reference for what a working embedded vision project looks like end to end, since the model, the firmware, the board and the power budget are all documented with the numbers attached. Do not treat the network as something to benchmark, because the author says plainly that it is overfitting a small hand-made dataset and questions whether that word even applies here.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Probably not. The repository last received commits 71 months ago, on December 6, 2020.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • PaulKlinger/cnn_card100 ★ · +0
Star history
Repository2026-08-182026-10-21
PaulKlinger/cnn_card100100

0 Stars in the last 64 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

A convolutional network sized for a business card

The task is small on purpose: classify digits drawn as five-by-five pixel images. The model is three convolutional layers with 4, 8 and 16 filters respectively, followed by a dense layer at the output. What the card does with that model is unusual, because it does not just print a digit. It shows the internal activations for all layers, which means the network's intermediate feature maps are rendered on LEDs as the classification happens. The repository is organised around that pipeline in four directories: one holding the training data and notebook, one holding the microcontroller project, one holding the board design files, and one holding auxiliary notebooks. There is also a project video linked at the top of the README. The language is recorded as C, and the licence is MIT.

The author says the model is overfitting, then questions the word

The training data was generated by hand, described in the README as whatever the author could come up with, with augmentation on top consisting of shifting and random pixel flips. What follows is one of the more honest passages in embedded machine learning documentation. The number of parameters is quite large compared with the number of training samples, so the model is probably mostly overfitting, and then the author immediately adds a caveat: that might be the wrong word here, because they do not think there are many more possible samples that could have been included. That is a subtle point and it matters. With a five-by-five input the space of meaningful images is small enough that a hand-made set may already approach what is learnable, in which case the usual framing of memorising a large dataset does not fit the situation.

Weights travel from a notebook into a C file

The path from research to device is short and completely explicit. The training notebook lives with the training data, and it exports the trained weights to C code. That generated file is then copied into the microcontroller project, where it is named model_weights.c. So the deployment artifact is not a binary blob or a checkpoint but a source file sitting among the firmware sources, which is why the firmware repository and the model repository have to be kept in step. Retraining is therefore not just a matter of running the notebook again: it produces a new C file that has to replace the one the firmware compiles. That is a deliberate trade, giving up compact loading and any notion of updating the model in the field in exchange for a build that links like ordinary C.

Two shift registers multiplex the LEDs and the buttons

The hardware side is where the engineering effort went, and the README says so. The LEDs and the buttons are both driven and scanned by multiplexing, through two shift registers. That is what makes a display of this density possible on a small board: instead of a dedicated output pin per LED, a pair of shift registers is scanned and the persistence of vision does the rest. The consequence, spelled out in the README, is timing. The main challenge with the code was making the multiplexing fast and consistent enough to allow PWM control of brightness. Scanning that is slow enough to flicker, or inconsistent between frames, and the brightness control stops looking smooth. Any attempt to reproduce this should treat the multiplex timing as the primary constraint rather than an implementation detail.

Two more notebooks generate the animation and the gamma table

The auxiliary directory is not documentation, it is tooling. One notebook there generates the startup animation, which the README describes as a concentric pulse, and another calculates the gamma correction table for the LEDs. Both exist because the firmware needs data that is tedious or impossible to write by hand. A gamma correction table is the obvious case: brightness on an LED is not linear in the duty cycle you ask for, so a table mapping requested brightness to a duty value has to be produced once and embedded. The startup animation is the same kind of thing in a different register, a short sequence of frames that would be miserable to encode as literal numbers. Both are generated into the firmware the same way the weights are, keeping the pattern consistent across the project.

A 1 mm board with ENIG and a matte black soldermask

The board was made by a board house and the fabrication choices are given precisely: one millimetre thickness, ENIG surface finish, and a matte black soldermask. Design files live in the board folder, with gerbers alongside Eagle and Fusion sources, plus a schematic as a PDF and a bill of materials as a CSV. A stencil was ordered from the same supplier and used with hot air and paste to solder the components, with one exception the README is candid about: the battery and its holder were soldered by hand. For testing, the coin cell holder listed in the bill of materials was used first and then replaced with a CR2032 cell with tabs soldered on, specifically to reduce the thickness a little. That last detail is a mechanical one, and it is the kind of decision that only appears in a project that has actually been assembled.

Standby current is 0.14 microamps

The power budget is quoted closely enough to be checked. In standby the card draws 0.14 microamps, and on that figure the README says the battery should last basically forever, in the order of decades. In use it lasts more than ten hours: current stays under 10 milliamps at maximum, meaning every LED at full brightness, and the estimate for normal use is closer to two or three milliamps. After two minutes with no input the card goes into standby on its own. Those three numbers together are what make a coin cell viable here rather than aspirational, and they also set the constraint that the next section depends on. A design that had to hold tens of milliamps would need a different cell and a different board.

White LEDs won because the multiplexed current is tiny

The colour choice is the last puzzle, and the explanation is a good illustration of why current budget matters more than datasheet headline figures. White LEDs were used because they work best in terms of maximum brightness, even though the forward voltage looks as though it should be too high for the coin cell. At the currents involved it is not a problem: under 0.1 milliamps averaged per LED, and under 1 milliamp momentarily because of the multiplexing. Two consequences follow. The forward voltage stops being a constraint at those levels, and white emitters are simply much more efficient. The phosphor coating adds a second, non-electrical reason, since it makes the emitted light more diffuse, which the author notes looks nicer. Every one of those arguments depends on the scan keeping the average current where it is.

Editorial conclusion

Use this repository as a reference for what a working embedded vision project looks like end to end, since the model, the firmware, the board and the power budget are all documented with the numbers attached. Do not treat the network as something to benchmark, because the author says plainly that it is overfitting a small hand-made dataset and questions whether that word even applies here. Two things to check before you replicate any of it. The generated weights file is the deployment artifact, so retraining means regenerating a source file inside the firmware project. And the power figures depend on the multiplexing current staying below a tenth of a milliamp per LED, which is the assumption the white LED choice rests on.

Frequently asked questions

What does PaulKlinger/cnn_card do?

It is a business card that runs a convolutional neural network to classify five-by-five pixel digits and shows the internal activations for all layers on its LEDs. The network has three convolutional layers with 4, 8 and 16 filters and a dense layer at the output.

How does the model get onto the card?

The training notebook in the cnn directory exports the trained weights to C code, and that generated file is copied to model_weights.c inside the microcontroller project. Retraining therefore means regenerating a source file that the firmware compiles.

How are the LEDs and buttons driven?

Both are driven and scanned by multiplexing through two shift registers. The README says the main challenge was making that multiplexing fast and consistent enough to allow PWM control of the brightness.

How long does the cnn_card battery last?

Standby current is 0.14 microamps, which the README puts in the order of decades of years, and use lasts more than ten hours, drawing under 10 milliamps at maximum brightness and closer to two or three in normal use. It enters standby automatically after two minutes without input.

Why does cnn_card use white LEDs?

Because at the currents involved they give better maximum brightness despite a forward voltage that looks too high for the cell. Under 0.1 milliamps per LED on average and under 1 milliamp momentarily during multiplexing, the forward voltage stops mattering, white emitters are more efficient, and the phosphor makes the light more diffuse.

Official sources

  1. Official README
  2. Project repository
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/paulklinger-cnn-card.svg)](https://hysenlabs.com/projects/paulklinger-cnn-card)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/paulklinger-cnn-card.svg)](https://hysenlabs.com/projects/paulklinger-cnn-card)