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>
204 lines
6.9 KiB
Markdown
204 lines
6.9 KiB
Markdown
# 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.
|
||
|
||
<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>
|
||
|
||
```
|
||
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.
|
||
|
||
```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`.
|
||
|
||
## Hardware
|
||
|
||
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.
|
||
|
||
### 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:
|
||
|
||
```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.
|
||
|
||
## Configuration
|
||
|
||
```yaml
|
||
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. |
|
||
|
||
```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
|
||
|
||
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](#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.
|