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.

Front of the display Back of the controller board

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.

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. Any ESPHome board with a free TX pin works.

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.

The reference build drives it from a WEMOS D1 R1, an UNO-sized ESP8266 board, through a MAX3232. The desk case 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:

    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

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.
- 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 writes, 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 are generated from Python's standard code page codecs by 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). 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.
S
Description
No description provided
Readme
16 MiB
0 Stars 1 Watchers 0 Forks
Languages
C++ 52.3%
OpenSCAD 24.4%
Python 23.3%