A 12 V 1 A adapter browns out at start-up: the datasheet's inrush current is 5x the 550 mA maximum, about 2.75 A at 12 V. List the supply current per voltage and recommend 12 V 3 A, 12 V 2 A with a bulk capacitor, or 24 V. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
367 lines
15 KiB
Markdown
367 lines
15 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.
|
||
|
||
- 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 a clock doesn't flicker.
|
||
- Supports every command in the datasheet: brightness, blinking, screen
|
||
on/off, the triangle markers, cursor control, scroll modes, user-defined
|
||
characters, code tables and the self-test.
|
||
- Converts UTF-8 text to the display's built-in font, so `é`, `ß`, `°`, `½`,
|
||
`█`, Greek and Cyrillic come out as real glyphs.
|
||
|
||
<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 device config
|
||
examples/secrets.yaml.example
|
||
docs/ photos and the 20L203DA12 datasheet
|
||
case/ 3D-printable desk case (OpenSCAD + STL), see case/README.md
|
||
tools/gen_charmaps.py regenerates components/samsung_vfd/charmaps.h
|
||
```
|
||
|
||
## 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:
|
||
type: git
|
||
url: https://mangavalk.com/mangavalk/esphome-samsung-vfd
|
||
ref: main
|
||
components: [samsung_vfd]
|
||
refresh: 1d
|
||
|
||
uart:
|
||
id: uart_vfd
|
||
tx_pin: GPIO2 # D1 R1 pin D9 (UART1 TX on the ESP8266)
|
||
baud_rate: 9600 # match the display's jumpers (factory default: 9600, no parity)
|
||
|
||
time: # required by the component (used by samsung_vfd.printf)
|
||
- platform: sntp
|
||
id: sntp_time
|
||
|
||
samsung_vfd:
|
||
id: vfd
|
||
uart_id: uart_vfd
|
||
```
|
||
|
||
The `uart:` block above uses the pin from the reference build, a WEMOS D1 R1
|
||
(ESP8266); see [Hardware](#hardware). Any ESPHome board with a free TX pin
|
||
works.
|
||
|
||
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.
|
||
|
||
The reference build drives it from a WEMOS D1 R1, an UNO-sized ESP8266 board,
|
||
through a MAX3232. The [desk case](case/README.md) is made for that board.
|
||
|
||
### Connector (3-pin Molex 5046-03A)
|
||
|
||
| Pin | Signal | Connect to |
|
||
|-----|--------|------------|
|
||
| 1 | VCC | 5–24 V DC supply |
|
||
| 2 | RXD | MAX3232 T1OUT (T1IN ← D1 R1 GPIO2), see below |
|
||
| 3 | GND | supply GND, MAX3232 GND **and** D1 R1 GND |
|
||
|
||
### Power
|
||
|
||
The display takes 4.5–26 V. Its supply current with all dots lit (datasheet
|
||
section 4.6):
|
||
|
||
| Supply | Typical | Maximum | Inrush at power-on (5 × max) |
|
||
|--------|---------|---------|------------------------------|
|
||
| 5 V | 0.8 A | 1.2 A | up to 6 A |
|
||
| 12 V | 0.35 A | 0.55 A | up to 2.75 A |
|
||
| 24 V | 0.2 A | 0.3 A | up to 1.5 A |
|
||
|
||
The inrush current sets the size of the supply, not the running current. A
|
||
12 V 1 A adapter is **not enough**: it current-limits at power-on and the
|
||
display browns out during start-up. A dev-board regulator can't supply this
|
||
either, so give the display its own supply and connect the grounds together:
|
||
|
||
- **12 V, 3 A or more** (recommended). The D1 R1 can share it through its
|
||
barrel jack, which accepts up to 12 V.
|
||
- **12 V, 2 A plus a bulk capacitor** of 2200 µF / 25 V or more across VCC and
|
||
GND at the display connector, so the capacitor supplies the inrush peak.
|
||
- **24 V, 2 A.** This has the lowest current, but don't feed it to the D1 R1's
|
||
barrel jack: power the board separately, e.g. over USB.
|
||
|
||
### 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 ESP8266 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 D1's 3.3 V, then wire GPIO2 → T1IN and
|
||
T1OUT → display RXD. The MAX3232 produces real RS-232 levels and does the
|
||
inversion itself, so use the plain `tx_pin: GPIO2`.
|
||
- **Direct from the ESP8266.** A plain TTL signal arrives inverted, so wire
|
||
GPIO2 straight to RXD and invert the pin in the UART config:
|
||
|
||
```yaml
|
||
uart:
|
||
id: uart_vfd
|
||
tx_pin:
|
||
number: GPIO2
|
||
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.
|
||
|
||
### Why GPIO2
|
||
|
||
The ESP8266 has two hardware UARTs. UART0 (GPIO1/GPIO3) is the USB serial
|
||
port that ESPHome's logger uses. UART1 can only transmit, and only on GPIO2,
|
||
which is all this display needs. On the D1 R1, GPIO2 is the pin labelled D9.
|
||
GPIO2 must be high at boot; the MAX3232's input doesn't pull it low, so that
|
||
is fine.
|
||
|
||
### Jumpers
|
||
|
||
The 2×8 pin header on the board selects the following. Jumpers are read only
|
||
at power-on. All open is the factory default.
|
||
|
||
| Jumper 1, 2 | Baud rate |
|
||
|---|---|
|
||
| open, open | **9600** |
|
||
| open, short | 4800 |
|
||
| short, open | 2400 |
|
||
| short, short | 19200 |
|
||
|
||
| Jumper 3, 4 | Parity |
|
||
|---|---|
|
||
| open, any | **none** |
|
||
| short, open | odd |
|
||
| short, short | even |
|
||
|
||
| Jumper 5 | |
|
||
|---|---|
|
||
| open | **normal** |
|
||
| short | self-test at power-on (about 20 s) |
|
||
|
||
| Jumper 6, 7, 8 | Font for 0x80–0xFF |
|
||
|---|---|
|
||
| open, open, open | **PC437** |
|
||
| short, open, open | PC850 |
|
||
| open, short, open | PC866 |
|
||
| short, short, open | PC737 |
|
||
| open, open, short | PC775 |
|
||
| short, open, short | CP10 for DP86-20 |
|
||
| open, short, short | PC864 |
|
||
| short, short, short | PC852 |
|
||
|
||
Set `baud_rate` and `parity` (`NONE`, `ODD` or `EVEN`) under `uart:` to match
|
||
jumpers 1–4. The component sends the font from its `code_table` setting at
|
||
start-up, so jumpers 6–8 don't matter.
|
||
|
||
## Configuration
|
||
|
||
```yaml
|
||
samsung_vfd:
|
||
id: vfd
|
||
uart_id: uart_vfd
|
||
cols: 20 # default 20 (1–80)
|
||
rows: 2 # default 2 (1–8)
|
||
brightness: 100% # 40/60/80/100 %; other values are rounded
|
||
code_table: PC437 # font for 0x80–0xFF, see "Characters"
|
||
international_charset: USA # see "Characters"
|
||
send_newlines: false # leave off for this display
|
||
user_characters: # up to 4 custom glyphs
|
||
- code: "~" # the ASCII character it replaces
|
||
pattern: # 7 rows × 5 pixels, top row first
|
||
- ".#.#." # '#', 'X', '1' or '*' = on
|
||
- "#####" # '.', '-', '0', '_' or ' ' = off
|
||
- "#####"
|
||
- "#####"
|
||
- ".###."
|
||
- "..#.."
|
||
- "....."
|
||
```
|
||
|
||
The UART needs a `tx_pin`. The component requires a `time:` component in the
|
||
config.
|
||
|
||
The component remembers brightness, blink, on/off, triangle markers, code
|
||
table, character set and user characters. It sends them again after
|
||
`samsung_vfd.reinit` or a self-test.
|
||
|
||
## Actions
|
||
|
||
`id:` is optional in every action when there is only one display.
|
||
|
||
### Text
|
||
|
||
| 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. |
|
||
|
||
```yaml
|
||
- samsung_vfd.print:
|
||
line: 0
|
||
text: !lambda 'return "Temp " + to_string((int) id(temp).state) + "°C";'
|
||
- samsung_vfd.printf:
|
||
time_id: sntp_time
|
||
line: 1
|
||
centered: true
|
||
format: "%a %d %b %H:%M"
|
||
```
|
||
|
||
### Display settings
|
||
|
||
| Action | Options |
|
||
|--------|---------|
|
||
| `samsung_vfd.set_brightness` | `brightness:` (templatable percentage). Short form: `samsung_vfd.set_brightness: 60%` |
|
||
| `samsung_vfd.set_blink` | `interval:` (templatable time): on for that long, off for that long, in 13 ms steps up to 3.3 s. `0ms` stops blinking. |
|
||
| `samsung_vfd.turn_off` / `turn_on` | Blanks the screen and brings it back without losing its contents. |
|
||
| `samsung_vfd.set_annunciator` | `column:` 1–20, or 0 for all; `state:` true/false (both templatable). Switches the triangle under that column. |
|
||
| `samsung_vfd.reinit` | Resets the display, re-applies all settings and redraws the screen. Use it when the display was power-cycled without the ESP8266. |
|
||
| `samsung_vfd.self_test` | Runs the built-in self-test (about 20 s), then restores settings and the screen. |
|
||
|
||
### Raw control
|
||
|
||
The text actions above assume overwrite mode and always redraw from the
|
||
top-left. The commands below give direct access to the cursor. After raw
|
||
writes, the next `print` redraws the whole screen.
|
||
|
||
| Action | Options |
|
||
|--------|---------|
|
||
| `samsung_vfd.home` | Moves the cursor to the top-left. |
|
||
| `samsung_vfd.set_cursor` | `column:` and `line:` (0-based, templatable). |
|
||
| `samsung_vfd.write` | Writes at the cursor. Either `text:` (templatable, converted like `print`) or `data:`, which sends bytes unchanged, e.g. `[0x1F, 0x58, 0x02]` or `"abc"`. |
|
||
| `samsung_vfd.set_display_mode` | `OVERWRITE` (default), `VERTICAL_SCROLL` or `HORIZONTAL_SCROLL`. The scroll modes are for raw `write`s, e.g. a ticker; switch back to `OVERWRITE` before using `print` again. |
|
||
|
||
### Lambda API
|
||
|
||
Every datasheet command (section 5.4) has a method on `id(vfd)`:
|
||
|
||
| # | Command | Bytes | Method |
|
||
|---|---------|-------|--------|
|
||
| 1 | BS | `08` | `cursor_left()` |
|
||
| 2 | HT | `09` | `cursor_right()` |
|
||
| 3 | LF | `0A` | `cursor_down()` |
|
||
| 4 | US LF | `1F 0A` | `cursor_up()` |
|
||
| 5 | HOM | `0B` | `home()` |
|
||
| 6 | CR | `0D` | `cursor_line_start()` |
|
||
| 7 | US CR | `1F 0D` | `cursor_line_end()` |
|
||
| 8 | US B | `1F 42` | `cursor_bottom()` |
|
||
| 9 | US $ n m | `1F 24 n m` | `set_cursor(column, line)` (0-based) |
|
||
| 10 | CLR | `0C` | `clear()` |
|
||
| 11 | CAN | `18` | `clear_cursor_line()` |
|
||
| 12 | ESC @ | `1B 40` | `reinit()` |
|
||
| 13 | ESC % n | `1B 25 n` | sent automatically by `define_char()` |
|
||
| 14 | ESC & … | `1B 26 01 n n 05 p1…p5` | `define_char(code, {p1, p2, p3, p4, p5})` |
|
||
| 15 | ESC ? n | `1B 3F n` | `cancel_char(code)` |
|
||
| 16 | ESC = n | `1B 3D n` | `select_peripheral(n)` (1 = pass-through only, 2 = display, 3 = both) |
|
||
| 17 | ESC R n | `1B 52 n` | `set_international_charset(samsung_vfd::CHARSET_GERMANY)` |
|
||
| 18 | ESC t n | `1B 74 n` | `set_code_table(samsung_vfd::CODE_TABLE_PC850)` |
|
||
| 19–21 | US MD1/2/3 | `1F 01`/`02`/`03` | `set_display_mode(samsung_vfd::MODE_OVERWRITE)` |
|
||
| 22 | US E n | `1F 45 n` | `set_blink(ms)`, `turn_off()`, `turn_on()` |
|
||
| 23 | US X n | `1F 58 n` | `set_brightness(0.0–1.0)` |
|
||
| 24 | US # n m | `1F 23 n m` | `set_annunciator(column, state)` |
|
||
| 25 | US @ | `1F 40` | `self_test()` |
|
||
|
||
The buffered text API is `print(text, centered)`, `print_line(line, text, centered)`
|
||
and `line_text(line)`. For anything else, use `write_text(text)`,
|
||
`write_raw(bytes)` and `encode(text)`, which returns the display bytes.
|
||
|
||
## Characters
|
||
|
||
Text is UTF-8, and each character becomes one byte:
|
||
|
||
1. ASCII (0x20–0x7E) passes through unchanged. Control characters become
|
||
spaces.
|
||
2. Other characters are looked up in the selected `code_table`, the display's
|
||
font for bytes 0x80–0xFF. With PC437, for example, `é` → 0x82, `°` → 0xF8,
|
||
`½` → 0xAB and `█` → 0xDB. The pages at the end of the datasheet show
|
||
every table.
|
||
3. Accented letters missing from the table lose their accent (`ő` → `o`).
|
||
Anything else becomes `?`.
|
||
|
||
| `code_table` | Covers |
|
||
|---|---|
|
||
| `PC437` | US / Western European, box drawing, some Greek and maths |
|
||
| `PC850` | Western European (more accented capitals) |
|
||
| `PC852` | Central European (Polish, Czech, Hungarian …) |
|
||
| `PC866` | Cyrillic |
|
||
| `PC737` | Greek |
|
||
| `PC775` | Baltic |
|
||
| `PC864` | Arabic |
|
||
| `CP10` | DP86-20 compatible; no conversion table, accents are stripped |
|
||
|
||
The conversion tables in
|
||
[charmaps.h](components/samsung_vfd/charmaps.h) are generated from Python's
|
||
standard code page codecs by [tools/gen_charmaps.py](tools/gen_charmaps.py).
|
||
|
||
`international_charset` (`USA`, `FRANCE`, `GERMANY`, `UK`, `DENMARK1`, `SWEDEN`,
|
||
`ITALY`, `SPAIN1`, `JAPAN`, `NORWAY`, `DENMARK2`) replaces 12 ASCII characters
|
||
such as `#`, `[`, `\`, `]` and `{` with national letters. For example, with
|
||
`GERMANY`, `[` shows as `Ä`. The text conversion doesn't account for this, so
|
||
keep `USA` unless you want those swaps.
|
||
|
||
User characters replace an ASCII character: with the example config, every
|
||
`~` in your text shows the heart.
|
||
|
||
## 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).
|
||
- Home Assistant entities:
|
||
- **VFD line 1** (text, max 20 chars) is shown on the top line and kept
|
||
across reboots.
|
||
- **VFD status** (text, 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.
|
||
- **VFD brightness** (select: 40/60/80/100 %).
|
||
- **VFD display** (switch) blanks the screen and brings it back.
|
||
- Wi-Fi with a captive-portal fallback AP, OTA updates and an encrypted native
|
||
API.
|
||
|
||
## How updates are sent
|
||
|
||
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. Nothing is sent when the content didn't change.
|
||
|
||
## Troubleshooting
|
||
|
||
- **Garbage or nothing on the display:** toggle `inverted:` on the TX pin (see
|
||
[Signal levels](#signal-levels)). Check that `baud_rate` and `parity` match
|
||
jumpers 1–4.
|
||
- **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.
|
||
- **The display was power-cycled and lost its settings or text:** call
|
||
`samsung_vfd.reinit`, for example from a Home Assistant button.
|