Building an LED Letter Sign
Overview
In this tutorial you will build a small PCB sign that draws the letter T out of nine red LEDs, one current-limiting resistor per LED, and a 2-pin header for 5V power — all laid out programmatically from a bitmap of the letter.
We chose the letter T because its strokes are the simplest possible: one straight horizontal bar and one straight vertical stem, with no diagonals and no corners mid-stroke. That keeps the dot pattern compact (a 5x5 grid, 9 LEDs) while still being unmistakably readable. The technique works for any capital letter A–Z — you will see at the end that changing the letter means changing one array of strings.
How a Letter Becomes a Circuit
A letter drawn with LEDs is just a dot-matrix bitmap: each cell of the
grid either holds an LED (1) or stays empty (0). The letter "T" on a 5x5
grid looks like this:
# # # # #
#
#
#
#
As strings (one string per row, read left-to-right, top-to-bottom):
11111
00100
00100
00100
00100
Every 1 becomes an <led /> on the PCB. Because the grid coordinates are
just math, tscircuit can compute the PCB position of each LED for us — this
is what the original Draw any letter with
LEDs bounty asked for: use
math to automatically lay out the PCB and schematic.
Bill of Materials
| Ref | Part | Value / Package | Qty | Notes |
|---|---|---|---|---|
| LED1–LED9 | Red LED | 0603 | 9 | ~2V forward voltage at 10–20mA |
| R1–R9 | Resistor | 220Ω, 0402 | 9 | One per LED, limits its current |
| J1 | Pin header | 2-pin, 2.54mm, male | 1 | 5V in + GND |
Power comes from any 5V source (a USB breakout or a bench supply) through J1. There is no microcontroller on this board — the letter is always on while 5V is present. (If you want to blink or dim the whole letter later, switch the 5V rail with a GPIO pin or a small MOSFET.)
Step 1: Place LEDs from the Bitmap
Instead of typing nine <led /> elements by hand, we flatten the bitmap into
a list of "lit dots" and map over it. The PCB position of each LED falls out
of the grid math:
pcbX = (col - 2) * DOT_PITCHcenters the 5 columns on x = 0pcbY = (2 - row) * DOT_PITCH + 6flips the bitmap vertically (row 0 is the top of the letter, but PCB y grows upwards) and lifts the letter above the connector
const DOT_PITCH = 4
const LETTER_T = ["11111", "00100", "00100", "00100", "00100"]
const dots = LETTER_T.flatMap((rowBits, row) =>
rowBits.split("").map((bit, col) => ({ bit, col, row })),
).filter((dot) => dot.bit === "1")
export default () => (
<board width="42mm" height="32mm">
{dots.map(({ col, row }, i) => (
<led
key={"LED" + (i + 1)}
name={"LED" + (i + 1)}
color="red"
footprint="0603"
pcbX={(col - 2) * DOT_PITCH}
pcbY={(2 - row) * DOT_PITCH + 6}
schX={4}
schY={-i * 2}
/>
))}
</board>
)
The dot pitch is 4mm: with 0603 LEDs (1.6mm x 0.8mm body) that leaves enough courtyard clearance between neighbors to pass DRC. Four LEDs fill the stem, five fill the top bar — nine LEDs total, and the preview already reads as the letter "T".
Step 2: Size the Current-Limiting Resistors
Each LED needs its own series resistor. With a 5V supply and a typical red
LED (forward voltage Vf ≈ 2V, forward current If ≈ 10–20mA):
R = (V_supply - Vf) / If
R = (5V - 2V) / 15mA = 200 ohm
200Ω is not a standard E12/E24 value, so we pick the next standard value up: 220Ω. The actual current is then:
If = (5V - 2V) / 220 = 13.6mA (comfortably inside 10-20mA)
Sanity checks:
- Resistor power:
P = If^2 * R = 13.6mA^2 * 220 = 41mW— well under the 62.5mW (1/16W) rating of a 0402 resistor. - Total current:
9 * 13.6mA = 123mA— well within a USB port's 500mA budget.
One resistor per LED (rather than one shared resistor) matters: LEDs with slightly different forward voltages would otherwise share current unevenly, and a single resistor would have to be rated for the full 123mA.
Step 3: Wire One String per Dot and Add Power
Each lit dot gets its own series string:
5V (net.V5) -> R.pin1 -> R.pin2 -> LED anode -> LED cathode -> GND
The <pinheader /> J1 feeds both rails, and <autoroutingphase /> routes
the PCB with the built-in autorouter. Here is the complete circuit:
const DOT_PITCH = 4
// The letter "T" as a bitmap. Each string is one row of the 5x5 dot grid,
// read left-to-right, top-to-bottom. "1" = place an LED at that dot.
const LETTER_T = ["11111", "00100", "00100", "00100", "00100"]
// Flatten the bitmap into a list of lit dots with grid coordinates.
const dots = LETTER_T.flatMap((rowBits, row) =>
rowBits.split("").map((bit, col) => ({ bit, col, row })),
).filter((dot) => dot.bit === "1")
export default () => (
<board width="42mm" height="32mm">
{/* Route the PCB with the built-in autorouter */}
<autoroutingphase />
{/* Power input: 5V from USB or a bench supply */}
<pinheader
name="J1"
pinCount={2}
pinLabels={["5V", "GND"]}
gender="male"
pcbX={0}
pcbY={-13}
schX={-9}
schY={-8}
/>
{/* One resistor + LED string per lit dot, laid out from the bitmap */}
{dots.map(({ col, row }, i) => {
const ledName = "LED" + (i + 1)
const resName = "R" + (i + 1)
return (
<>
<led
name={ledName}
color="red"
footprint="0603"
forwardVoltage="2V"
pcbX={(col - 2) * DOT_PITCH}
pcbY={(2 - row) * DOT_PITCH + 6}
schX={4}
schY={-i * 2}
/>
<resistor
name={resName}
resistance="220"
footprint="0402"
pcbX={(i - 4) * DOT_PITCH}
pcbY={-7}
schX={-2}
schY={-i * 2}
/>
{/* 5V -> resistor -> LED anode -> LED cathode -> GND */}
<trace from={"." + resName + " .pin1"} to="net.V5" />
<trace
from={"." + resName + " .pin2"}
to={"." + ledName + " .anode"}
/>
<trace from={"." + ledName + " .cathode"} to="net.GND" />
</>
)
})}
<trace from=".J1 .pin1" to="net.V5" />
<trace from=".J1 .pin2" to="net.GND" />
</board>
)
On the schematic, the nine strings read as nine identical resistor-LED rows
stacked under the header — the schematic placement (schX, schY) is also
computed from the map index, so adding a dot adds a row without any manual
cleanup:
const DOT_PITCH = 4
const LETTER_T = ["11111", "00100", "00100", "00100", "00100"]
const dots = LETTER_T.flatMap((rowBits, row) =>
rowBits.split("").map((bit, col) => ({ bit, col, row })),
).filter((dot) => dot.bit === "1")
export default () => (
<board width="42mm" height="32mm">
<autoroutingphase />
<pinheader
name="J1"
pinCount={2}
pinLabels={["5V", "GND"]}
gender="male"
pcbX={0}
pcbY={-13}
schX={-9}
schY={-8}
/>
{dots.map(({ col, row }, i) => {
const ledName = "LED" + (i + 1)
const resName = "R" + (i + 1)
return (
<>
<led
name={ledName}
color="red"
footprint="0603"
forwardVoltage="2V"
pcbX={(col - 2) * DOT_PITCH}
pcbY={(2 - row) * DOT_PITCH + 6}
schX={4}
schY={-i * 2}
/>
<resistor
name={resName}
resistance="220"
footprint="0402"
pcbX={(i - 4) * DOT_PITCH}
pcbY={-7}
schX={-2}
schY={-i * 2}
/>
<trace from={"." + resName + " .pin1"} to="net.V5" />
<trace
from={"." + resName + " .pin2"}
to={"." + ledName + " .anode"}
/>
<trace from={"." + ledName + " .cathode"} to="net.GND" />
</>
)
})}
<trace from=".J1 .pin1" to="net.V5" />
<trace from=".J1 .pin2" to="net.GND" />
</board>
)
Step 4: Assembly
- Solder the resistors first (R1–R9). They sit lowest on the board and won't be in the way later. 0402 parts are small — tack one pad, slide the part in with tweezers, then solder the second pad.
- Solder the LEDs (LED1–LED9). 0603 LEDs are polarized: the marked (green/gray) side or the notch is the cathode, which must face the GND side of the pad. On this board every anode faces its resistor and every cathode faces away from the letter's center column — the silkscreen shows the pad outline.
- Solder the header J1 on the bottom edge of the board.
- First power-up: connect 5V and GND to J1 (a current-limited bench supply set to ~150mA, or a USB breakout). All nine LEDs should light and the board should draw roughly 120mA. Nothing should get more than warm — each resistor only dissipates ~41mW.
- If one dot stays dark, re-check that LED's orientation first (a reversed 0603 LED is the most common cause), then its solder joints.
Extending to Any Letter (A–Z) with <LedLetter />
The letter is data, not layout. We can wrap the bitmap layout logic into a
reusable <LedLetter letter="A" power="net.power" gnd="net.gnd" /> component
backed by a 5x5 font map covering all capital letters A–Z:
const DOT_PITCH = 4
const LETTERS: Record<string, string[]> = {
A: ["01110", "10001", "11111", "10001", "10001"],
B: ["11110", "10001", "11110", "10001", "11110"],
C: ["01111", "10000", "10000", "10000", "01111"],
D: ["11110", "10001", "10001", "10001", "11110"],
E: ["11111", "10000", "11110", "10000", "11111"],
F: ["11111", "10000", "11110", "10000", "10000"],
G: ["01111", "10000", "10111", "10001", "01111"],
H: ["10001", "10001", "11111", "10001", "10001"],
I: ["11111", "00100", "00100", "00100", "11111"],
J: ["00111", "00010", "00010", "10010", "01100"],
K: ["10001", "10010", "11100", "10010", "10001"],
L: ["10000", "10000", "10000", "10000", "11111"],
M: ["10001", "11011", "10101", "10001", "10001"],
N: ["10001", "11001", "10101", "10011", "10001"],
O: ["01110", "10001", "10001", "10001", "01110"],
P: ["11110", "10001", "11110", "10000", "10000"],
Q: ["01110", "10001", "10101", "10010", "01101"],
R: ["11110", "10001", "11110", "10010", "10001"],
S: ["01111", "10000", "01110", "00001", "11110"],
T: ["11111", "00100", "00100", "00100", "00100"],
U: ["10001", "10001", "10001", "10001", "01110"],
V: ["10001", "10001", "10001", "01010", "00100"],
W: ["10001", "10001", "10101", "11011", "10001"],
X: ["10001", "01010", "00100", "01010", "10001"],
Y: ["10001", "01010", "00100", "00100", "00100"],
Z: ["11111", "00010", "00100", "01000", "11111"],
}
export const LedLetter = ({
letter,
power = "net.power",
gnd = "net.gnd",
prefix = letter,
}: {
letter: keyof typeof LETTERS
power?: string
gnd?: string
prefix?: string
}) => {
const bitmap = LETTERS[letter.toUpperCase()] ?? LETTERS.T
const dots = bitmap
.flatMap((rowBits, row) =>
rowBits.split("").map((bit, col) => ({ bit, col, row })),
)
.filter((dot) => dot.bit === "1")
return (
<group>
{dots.map(({ col, row }, i) => {
const ledName = `${prefix}_LED${i + 1}`
const resName = `${prefix}_R${i + 1}`
return (
<>
<led
name={ledName}
color="red"
footprint="0603"
pcbX={(col - 2) * DOT_PITCH}
pcbY={(2 - row) * DOT_PITCH + 6}
schX={4}
schY={-i * 2}
/>
<resistor
name={resName}
resistance="220"
footprint="0402"
pcbX={(i - Math.floor(dots.length / 2)) * 2.5}
pcbY={-7}
schX={-2}
schY={-i * 2}
/>
<trace from={`.${resName} .pin1`} to={power} />
<trace from={`.${resName} .pin2`} to={`.${ledName} .anode`} />
<trace from={`.${ledName} .cathode`} to={gnd} />
</>
)
})}
</group>
)
}
You can then render any letter A–Z with <LedLetter letter="A" power="net.power" gnd="net.gnd" />, or sample points along stroke polylines from the
tscircuit/alphabet project.
Ordering the PCB
You can order this PCB by downloading the fabrication files and uploading them to JLCPCB. Follow the instructions from here.