The 20L203 input is RS-232 (reference build uses a MAX3232), it runs on 5-24 V, and it boots in non-scrolling overwrite mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Samsung serial VFD — ESPHome component
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.
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.
components/samsung_vfd/ the component (Python schema + C++)
examples/samsung-vfd.yaml full example: clock + two Home Assistant text entities
examples/secrets.yaml.example
docs/ photos and the 20L203DA12 datasheet
Using it from your ESPHome server
Add this to your device YAML. ESPHome fetches the component from git when it compiles.
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
into your ESPHome config folder. Add the keys from
examples/secrets.yaml.example to your
secrets.yaml.
Hardware
The board in the photos is a 20L203DA5 (rev A). The included datasheet is for the 20L203DA12, a variant with the same command set.
Connector (3-pin Molex 5046-03A)
| 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 |
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:
uart: id: uart_vfd tx_pin: number: GPIO17 inverted: true baud_rate: 9600This 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.
Configuration
samsung_vfd:
id: vfd
uart_id: uart_vfd
cols: 20 # default 20 (1–80)
rows: 2 # default 2 (1–8)
send_newlines: false # default false; leave off for this display
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. |
- 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
textentities:- 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
These are the commands the component uses. The datasheet, section 5.4, lists the rest, including cursor positioning, brightness, blinking and the annunciator triangles.
| Bytes | Effect |
|---|---|
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 |
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.
Troubleshooting
- Garbage or nothing on the display: toggle
inverted:on the TX pin (see 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_newlinesis off. With 20-column lines the cursor has already wrapped to line 2, and an extra line feed sends it back to line 1.

