Skip to content

Instantly share code, notes, and snippets.

@zopieux
Last active February 8, 2026 16:27
Show Gist options
  • Select an option

  • Save zopieux/0b38fe1c3cd49039c98d5612ca84a045 to your computer and use it in GitHub Desktop.

Select an option

Save zopieux/0b38fe1c3cd49039c98d5612ca84a045 to your computer and use it in GitHub Desktop.
QNAP TS-453 Pro: LCD, LEDs, fan & buttons

QNAP TS-453 Pro: LCD, LEDs, fan & buttons

QNAP NAS model TS-453 Pro has limited documentation on how to control its various peripherals, namely:

  • the front LCD display, a 2 row, 16 column blue & white LCD display, and its two menu buttons ("ENTER", "SELECT")
  • the front LEDs "STATUS" (red & green) & "USB" (blue)
  • the 4 "disk error" LEDs (red)
  • the front "USB COPY" button
  • the main rear fan

I've spent some time toying with the stock firmware to figure that out.

Some background

Just FYI, to act of these peripherals (and a lot of other stuff such as flashing the firmware), the stock firmware uses a combination of a 1.3 MB shared library libuLinux_hal.so, a 364 KB hal_daemon service and a 176 KB hal_app command-line tool, plus some other libraries for parsing the specific INI-stored "HAL config" for each device model. For example, to produce a beep:

$ hal_app --se_buzzer enc_id=0,mode=1

sends a message to hal_daemon which eventually writes to the relevant low-level I/O port through libuLinux_hal.

Fan control

This can be controlled using standard hwmon tooling on Linux. Install lm-sensors/fancontrol. A sensible /etc/fancontrol is presented below:

INTERVAL=10
DEVPATH=hwmon1=devices/platform/coretemp.0 hwmon2=devices/platform/f71882fg.656
DEVNAME=hwmon1=coretemp hwmon2=f71869a
FCTEMPS=hwmon2/device/pwm1=hwmon1/temp2_input
FCFANS= hwmon2/device/pwm1=hwmon2/device/fan1_input
MINTEMP=hwmon2/device/pwm1=30
MAXTEMP=hwmon2/device/pwm1=65
MINSTART=hwmon2/device/pwm1=150
MINSTOP=hwmon2/device/pwm1=0

LCD display

This is a well-documented A125 board. There are multiple scripts and programs on the web to read the buttons and control the LCD display. See eg. the implementation for qcontrol: a125.c.

The tl;dr is:

  • serial port /dev/ttyS1, baudrate 1200 (yes; that's 2 updates per second at best)
  • write 0x4D, 0x5E, 0x00 to switch the backlight off, write 0x4D, 0x5E, 0x01 to switch the backlight on
  • write 0x4D, 0x0C, 0x00, 0x20, {16 chars} to write to the first line, write 0x4D, 0x0C, 0x01, 0x20, {16 chars} to write to the second line (pad with spaces to clear the previous characters)
  • for each button press and release, the read buffer will receive 4 characters of the form 0x53, 0x05, 0x00, {BM} where {BM} is a bitmask of 0x01 → ENTER (UP) and 0x02 → SELECT (DOWN). Possible values are thus 0x00, 0x01, 0x02, 0x03. Pressing ENTER then pressing SELECT then releasing ENTER then releasing SELECT will send four 4-byte messages ending with 0x01 → 0x03 → 0x02 → 0x00.

Front LEDs and USB "COPY" button

This is the funny, otherwise undocumented part.

The stock firmware contains model-specific config files, eg. model_QW370_QW550_16_10.conf, that describes on what low-level I/O ports should one read and write. Reproduced below are the interesting parts:

[System Enclosure]
VENDOR = QNAP
MODEL = TS-453 Pro
SIO_DEVICE = F71869A  # The motherboard model.
# …
[System IO]
RESET_BUTTON = SIO:I92:B1
STATUS_GREEN_LED = SIO:I91:B2
STATUS_RED_LED = SIO:I91:B3
USB_COPY_BUTTON = SIO:IE2:B2
FRONT_USB_LED = SIO:IE1:B7
# …
[System Disk 1]
DEV_BUS = B00:D28:F0
DEV_PORT = 1
ERR_LED = SIO:I81:B0
# …

What I couldn't find in this file is the base port number. strace-ing the stock binary, I discovered it's 0xA05 (2565).

Understanding the Super I/O spec format

Port Purpose
0xA05 Control "register" XX (:I{XX})
0xA06 Register value, using negative bitmask offset X (:B{X})

Switching a LED

To change a LED state, write its control register to 0xA05 and the relevant bitmask to 0xA06.

For instance, to change STATUS_GREEN_LED = SIO:I91:B2:

  • write to 0xA05: 0x91 ( :I91)
  • write to 0xA06: 0xFF ^ 0b100, as GREEN is 2nd bit (:B2)

Reading a button status

To read a button status, write the control register to 0xA05, read 0xA06 and mask it to get the relevant bit. For instance, to poll for COPY button status USB_COPY_BUTTON = SIO:IE2:B2:

  • write to 0xA05: 0xE2 ( :IE2)
  • read 0xA06 and get its 2nd bit (:B2)

There is a sample C program below to demo this feature.

[System Enclosure]
VENDOR = QNAP
MODEL = TS-453 Pro
CAP=0x06145bdc
MAX_DISK_NUM = 4
MAX_FAN_NUM = 1
MAX_TEMP_NUM = 2
MAX_NET_PORT_NUM = 4
INTERNAL_NET_PORT_NUM = 4
MAX_PCIE_SLOT = 1
CPU_TEMP_UNIT=DTS:4
SYSTEM_TEMP_UNIT=SIO:3
SIO_DEVICE = F71869A
PWR_RECOVERY_UNIT = SIO
PWR_RECOVERY_CMOS_STORE = 0x70,0x61
BOARD_SN_DEVICE = NET:1
ETH_MAC_DEVICE = NET
DISK_DRV_TYPE = ATA
DISK_DEFAULT_MAX_LINK_SPEED = PD_SATA_SAS_6G
SYSTEM_DISK_CACHEABLE_BITMAP = 0x1e
SS_MAX_CHANNELS = 40
SS_FREE_CHANNELS = 2
[System FAN]
FAN_UNIT = SIO
FAN_1=I1
FAN_2=I2
FAN_LEVEL_0 = 0
FAN_LEVEL_1 = 70
FAN_LEVEL_2 = 90
FAN_LEVEL_3 = 110
FAN_LEVEL_4 = 130
FAN_LEVEL_5 = 150
FAN_LEVEL_6 = 200
FAN_LEVEL_7 = 250
[System I2C]
DEV_BUS = B00:D31:F3
DEV_PORT = 0
[System EDID 1]
DEV_BUS = B00:D02:F0
DEV_PORT = 4
[System IO]
RESET_BUTTON = SIO:I92:B1
STATUS_GREEN_LED = SIO:I91:B2
STATUS_RED_LED = SIO:I91:B3
LED_BV_CTRL = GPIO
USB_COPY_BUTTON = SIO:IE2:B2
FRONT_USB_LED = SIO:IE1:B7
VPD_MB = I2C:0x54
VPD_BP = I2C:0x56
[System Disk 1]
DEV_BUS = B00:D28:F0
DEV_PORT = 1
ERR_LED = SIO:I81:B0
[System Disk 2]
DEV_BUS = B00:D28:F0
DEV_PORT = 0
ERR_LED = SIO:I81:B1
[System Disk 3]
DEV_BUS = B00:D28:F1
DEV_PORT = 0
ERR_LED = SIO:I81:B2
[System Disk 4]
DEV_BUS = B00:D28:F1
DEV_PORT = 1
ERR_LED = SIO:I81:B3
[System Network 1]
DEV_BUS = B00:D28:F2
PCI_SWITCH_PORT = 1
DEV_PORT = 0
[System Network 2]
DEV_BUS = B00:D28:F2
PCI_SWITCH_PORT = 2
DEV_PORT = 0
[System Network 3]
DEV_BUS = B00:D28:F3
PCI_SWITCH_PORT = 1
DEV_PORT = 0
[System Network 4]
DEV_BUS = B00:D28:F3
PCI_SWITCH_PORT = 2
DEV_PORT = 0
[System PCIE SLOT 1]
DEV_BUS = B00:D01:F0
MAX_PCIE_LINK_WIDTH = 8
[Usb Enclosure]
VENDOR = QNAP
MODEL = USB
MAX_PORT_NUM = 5
USB3_PORT_BITMAP = 0xE
[Usb Port 1]
DEV_BUS = B00:D20:F0
IN_HUB = 1
HUB_PORT = 1
DEV_PORT = 3
[Usb Port 2]
DEV_BUS = B00:D20:F0
IN_HUB = 1
HUB_PORT = 1
DEV_PORT = 1
[Usb Port 3]
DEV_BUS = B00:D20:F0
IN_HUB = 1
HUB_PORT = 1
DEV_PORT = 4
[Usb Port 4]
DEV_BUS = B00:D20:F0
IN_HUB = 0
DEV_PORT = 3
[Usb Port 5]
DEV_BUS = B00:D20:F0
IN_HUB = 0
DEV_PORT = 2
[Boot Enclosure]
VENDOR = QNAP
MODEL = BOOT
MAX_DISK_NUM = 1
DISK_DRV_TYPE = USB
[Boot Disk 1]
DEV_BUS = B00:D20:F0
IN_HUB = 0
DEV_PORT = 4
[System Memory]
MAX_CHANNEL_NUM = 2
MAX_SLOT_NUM = 2
SLOT1_ADDR = 1, 0x50
SLOT2_ADDR = 2, 0x51
// Copyright 2019 Google LLC.
// SPDX-License-Identifier: Apache-2.0
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/io.h>
#define BASEPORT 0xa05
#define NPORTS 2
#define GREEN_LED 0x91
#define GREEN_LED_B (1 << 2)
#define RED_LED 0x91
#define RED_LED_B (1 << 3)
#define USB_LED 0xe1
#define USB_LED_B (1 << 7)
#define COPY_BUTTON 0xe2
#define COPY_BUTTON_B (1 << 2)
#define RESET_BUTTON 0x92
#define RESET_BUTTON_B (1 << 1)
// You can also act of disk error LEDs, not included for brevity.
int main() {
// Get access to the ports
if (ioperm(BASEPORT, NPORTS, 1)) { perror("ioperm"); exit(1); }
// Switch some LEDs on
outb(GREEN_LED, BASEPORT);
outb(0xff ^ GREEN_LED_B, BASEPORT + 1);
outb(USB_LED, BASEPORT);
outb(0xff ^ USB_LED_B, BASEPORT + 1);
usleep(1000000);
// Switch them off
outb(GREEN_LED, BASEPORT);
outb(0xff, BASEPORT + 1);
outb(USB_LED, BASEPORT);
outb(0xff, BASEPORT + 1);
// Poll USB COPY button for a while
outb(COPY_BUTTON, BASEPORT);
for (int t = 0; t < 10; t++) {
int value = inb(BASEPORT + 1) & COPY_BUTTON_B;
printf("COPY button: %s\n", value ? "released" : "pressed");
usleep(100000);
}
// We don't need the ports anymore
if (ioperm(BASEPORT, NPORTS, 0)) { perror("ioperm"); exit(1); }
}
@stephenhouser
Copy link

Nice work @dynek! Thanks for sharing.

@vm-wylbur
Copy link

For anyone stuck with non-working LCD on IT8528-based QNAP models (eg TVS-h874X) running non-QTS OS:
The issue isn't the A78 protocol - it's that the IT8528 Super I/O needs initialization. On some models like TVS-h874X, the BIOS already does this. Just need to "wake" the LCD with multiple backlight commands:

for i in {1..3}; do
  printf "\x4d\x5e\x01" > /dev/ttyS1
  sleep 0.5
done

Then A78 protocol works perfectly in Ubuntu 24.04. More details here

Thanks for documenting the protocol!

@davidedg
Copy link

davidedg commented Feb 8, 2026

For anyone stuck with non-working LCD on IT8528-based QNAP models (eg TVS-h874X) running non-QTS OS: The issue isn't the A78 protocol - it's that the IT8528 Super I/O needs initialization. On some models like TVS-h874X, the BIOS already does this. Just need to "wake" the LCD with multiple backlight commands:

Nice one! Is the init code needed also for the physical buttons? Seems related to my workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment