2026-10-31 17:29:09 +01:00
# Samsung serial VFD — ESPHome component
2026-10-31 17:44:07 +01:00
An ESPHome external component for the Samsung 20L203 2-line × 20-character VFD
(vacuum fluorescent display) pole display, driven over a single serial wire at
9600 baud.
2026-10-31 17:29:09 +01:00
It keeps a copy of the screen in RAM and only sends data when something
changes. Updates overwrite the screen in place instead of clearing it first,
so the clock doesn't flicker.
2026-10-31 17:44:07 +01:00
<p>
<img src="docs/front.jpg" alt="Front of the display" height="300">
<img src="docs/back.jpg" alt="Back of the controller board" height="300">
</p>
2026-10-31 17:29:09 +01:00
```
components/samsung_vfd/ the component (Python schema + C++)
examples/samsung-vfd.yaml full example: clock + two Home Assistant text entities
examples/secrets.yaml.example
2026-10-31 17:44:07 +01:00
docs/ photos and the 20L203DA12 datasheet
2026-10-31 17:29:09 +01:00
```
## Using it from your ESPHome server
Add this to your device YAML. ESPHome fetches the component from git when it
compiles.
```yaml
external_components :
- source : github://REPO_OWNER/REPO_NAME@main
components : [ samsung_vfd]
refresh : 1d
uart :
id : uart_vfd
tx_pin : GPIO17
baud_rate : 9600
time : # needed by the component (used by samsung_vfd.printf)
- platform : sntp
id : sntp_time
samsung_vfd :
id : vfd
uart_id : uart_vfd
```
For a complete device, copy [examples/samsung-vfd.yaml ](examples/samsung-vfd.yaml )
into your ESPHome config folder. Add the keys from
[examples/secrets.yaml.example ](examples/secrets.yaml.example ) to your
`secrets.yaml` .
2026-10-31 17:44:07 +01:00
## Hardware
2026-10-31 17:29:09 +01:00
2026-10-31 17:44:07 +01:00
The board in the photos is a 20L203DA5 (rev A). The included
[datasheet ](docs/20L203DA12-datasheet.pdf ) is for the 20L203DA12, a variant
with the same command set.
2026-10-31 17:29:09 +01:00
2026-10-31 17:44:07 +01:00
### Connector (3-pin Molex 5046-03A)
2026-10-31 17:29:09 +01:00
2026-10-31 17:44:07 +01:00
| Pin | Signal | Connect to |
|-----|--------|------------|
| 1 | VCC | 5– 24 V DC supply |
| 2 | RXD | MAX3232 T1OUT (T1IN ← ESP32 GPIO17), see below |
| 3 | GND | supply GND, MAX3232 GND **and** ESP32 GND |
2026-10-31 17:29:09 +01:00
2026-10-31 17:44:07 +01:00
### Power
The display takes 5– 24 V. At 5 V it draws about 0.8 A typical and 1.2 A
maximum, and the inrush current at power-on can be up to five times that. A
dev-board regulator can't supply this, so give the display its own supply,
for example 12 V at 1 A, and connect the grounds together.
### Signal levels
The RXD input goes through an RS-232 receiver: it expects ±3 to ±15 V and
idles at the negative level (mark). It tolerates ±30 V, so a 3.3 V ESP32 pin
can't damage it. There are two ways to connect it:
- **Through a MAX3232 (recommended, and how the original build is wired).**
Power the MAX3232 from the ESP32's 3.3 V, then wire GPIO17 → T1IN and
T1OUT → display RXD. The MAX3232 produces real RS-232 levels and does the
inversion itself, so use the plain `tx_pin: GPIO17` .
- **Direct from the ESP32.** A plain TTL signal arrives inverted, so wire
GPIO17 straight to RXD and invert the pin in the UART config:
```yaml
uart:
id: uart_vfd
tx_pin:
number: GPIO17
inverted: true
baud_rate: 9600
` ``
This relies on the receiver's switching threshold, which on most RS-232
receivers is around 1.3 V. It isn't guaranteed by the spec, which asks for
at least 3 V.
If the display shows garbage or nothing at all, the inversion setting is the
first thing to check: off with a MAX3232, on without one.
### Jumpers
The 2× 8 pin header on the board selects the following. All jumpers open is the
factory default.
| Jumpers | Setting | Default (open) |
|---------|---------|----------------|
| 1, 2 | baud rate: 9600 / 4800 / 2400 / 19200 | 9600 |
| 3, 4 | parity: none / odd / even | none |
| 5 | self-test at power-on | off |
| 6, 7, 8 | font for codes 0x80– 0xFF | PC437 |
This component expects 9600 baud and no parity, which ESPHome's UART uses by
default.
2026-10-31 17:29:09 +01:00
## Configuration
` ``yaml
samsung_vfd:
id: vfd
uart_id: uart_vfd
cols: 20 # default 20 (1– 80)
rows: 2 # default 2 (1– 8)
2026-10-31 17:44:07 +01:00
send_newlines: false # default false; leave off for this display
2026-10-31 17:29:09 +01:00
` ``
The UART must be 9600 baud with a ` tx_pin`. The component requires a ` time:`
component in the config.
## Actions
| Action | Options |
|--------|---------|
| ` samsung_vfd.print` | ` text` (templatable). Optional ` line:` (0-based) replaces one line. Without ` line:`, the text is word-wrapped over the whole screen; ` \n` forces a new line. Optional ` centered: true`. |
| ` samsung_vfd.printf` | Shows the current time formatted with strftime, e.g. ` format: "%H:%M:%S"`. Requires ` time_id:`. Takes the same ` line:` and ` centered:` options. Does nothing until the clock has synced. |
| ` samsung_vfd.clear` | Clears the screen. |
| ` samsung_vfd.home` | Moves the cursor home. |
` ``yaml
- samsung_vfd.print:
id: vfd
line: 0
text: !lambda 'return "Temp " + to_string((int) id(temp).state) + " C";'
- samsung_vfd.printf:
id: vfd
time_id: sntp_time
line: 1
centered: true
format: "%a %d %b %H:%M"
` ``
From a lambda you can call ` id(vfd).print(...)`, ` print_line(line, text, centered)`,
` clear()`, ` home()` and ` line_text(line)`.
Text is converted to one byte per character. Accented Latin-1 letters lose
their accent (é → e), other non-ASCII characters become ` ?`, and control
characters become spaces.
## What the example config does
- On boot: clears the screen, shows ` Hello World!` for 3 s, then restores line 1.
- Bottom line: a live clock, ` %a %d %b %H:%M:%S`, updated every second (SNTP,
Europe/Amsterdam).
- Two Home Assistant ` text` entities:
- **VFD line 1** (max 20 chars) is shown on the top line and kept across
reboots.
- **VFD status** (max 40 chars) shows a full-screen, word-wrapped, centred
banner for 10 s. The clock pauses meanwhile, then line 1 and the clock come
back.
- Wi-Fi with a captive-portal fallback AP, OTA updates and an encrypted native
API.
## Protocol
2026-10-31 17:44:07 +01:00
These are the commands the component uses. The datasheet, section 5.4, lists
the rest, including cursor positioning, brightness, blinking and the
annunciator triangles.
2026-10-31 17:29:09 +01:00
| Bytes | Effect |
|-------|--------|
2026-10-31 17:44:07 +01:00
| ` 1B 40` (` ESC @`) | initialise: reset settings, clear, cursor home |
| ` 0C` | clear screen, cursor home |
| ` 0B` | cursor home (top-left) |
| ` 20`– ` FF` | print a character and move the cursor right |
2026-10-31 17:29:09 +01:00
2026-10-31 17:44:07 +01:00
At power-on the display is in overwrite mode. After the last column of the top
line the cursor continues on the bottom line, and after the last column of the
bottom line it goes back to the top-left. Nothing scrolls. Each update
therefore sends ` 0B` (home) followed by both lines, each padded to exactly 20
characters.
2026-10-31 17:29:09 +01:00
## Troubleshooting
2026-10-31 17:44:07 +01:00
- **Garbage or nothing on the display:** toggle ` inverted:` on the TX pin (see
[Signal levels](#signal-levels)). Check that the baud jumpers 1 and 2 are
open (9600) and that parity jumpers 3 and 4 are open (none).
- **The display runs its self-test for about 20 s after power-on:** jumper 5 is
shorted.
- **Line 2 text lands on line 1:** make sure ` send_newlines` is off. With
20-column lines the cursor has already wrapped to line 2, and an extra line
feed sends it back to line 1.