Skip to content

Instantly share code, notes, and snippets.

@zopieux
Last active April 11, 2024 00:51
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zopieux/0b38fe1c3cd49039c98d5612ca84a045 to your computer and use it in GitHub Desktop.
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); }
}
@westernmagic
Copy link

Great work! Any hints as to how to control the buzzer?

@zopieux
Copy link
Author

zopieux commented Aug 29, 2020

Hey @westernmagic, thanks for the kind word. Sorry I didn't have interest in controlling the piezzo buzzer so I didn't look. Feel free to ping me if you find something!

@markb139
Copy link

This is great. My TS-453 Pro died the other day and after a bit of diagnosis I've come to the conclusion that the motherboard has failed. I didn't want to through it all away so was looking to re-use some parts. Was thinking of housing a Raspberry Pi. Being able to use the LCD will be useful

@artvel
Copy link

artvel commented May 28, 2021

Great work! Any idea how to make the power button working when running the NAS with debian?

@zopieux
Copy link
Author

zopieux commented May 29, 2021

@artvel I did not look but to change the behavior of power buttons you'll usually have to toy with ACPI, which is a standard interface. No reason it wouldn't work with QNAP NASes.

@artvel
Copy link

artvel commented May 29, 2021

Hi @zopieux, thanks for your response.
Just for the record it is a QNAP TVS-472XT-PT-4G.

Only the second Power Button event seems to respond but with a delay of approx. 4sec. So I had to hold the power button for 4 seconds until I saw the first event.

/dev/input/event3:	Sleep Button
/dev/input/event4:	Power Button
/dev/input/event5:	Power Button //only this one worked
/dev/input/event6:	ITE8708 CIR transceiver

I receive events on both on my other NAS. Seems a little bit awkward to me but anyways.

Hope it helps someone.

@system1357
Copy link

system1357 commented Aug 4, 2021

Hi,
Thanks for the research, may I ask that if you're interested in finding ways to configure the ITE (it87) variants?
I have a fairly old TS-469L, and after digging around in the stock firmware I found similar configs, with the only difference of the SuperIO chip (ITE IT8721F)
Here's the area of interest:

[System Enclosure]
VENDOR = QNAP
MODEL = TS-469L
# ...
CPU_TEMP_UNIT=SIO:1
SYSTEM_TEMP_UNIT=SIO:2
SIO_DEVICE = IT87

[System IO]
RESET_BUTTON = SIO:I00:B7
USB_COPY_BUTTON = SIO:I02:B4
STATUS_GREEN_LED = SIO:I01:B2
STATUS_RED_LED = SIO:I01:B3
FRONT_USB_LED = SIO:I04:B0
POWER_LED=SIO:I03:B0
# ...
[System Disk 1]
DEV_BUS = B00:D31:F2
DEV_PORT = 0
ERR_LED = SIO:I07:B4

I've tried to change the base address according to it87_wdt and gpio_it87 kernel drivers but failed
0x2e and 0x25 seems incorrect, and I have limited experience on reversing binaries
Any help is appreciated

@stephenhouser
Copy link

Curious how you got strace up and running on the QNAP. I have a TVS-671 and I'm not getting positive results. Thinking the base port might be different.

@zopieux
Copy link
Author

zopieux commented Feb 17, 2022

My QNAP original install had root access unlocked. I don't remember if I had to install some ipkg to get strace, but that was pretty much it.

@stephenhouser
Copy link

Thanks. I managed to get hal_app running under TrueNAS by copying over the shared libraries and some LD_LIBRARY_PATH trickery. Still have not quite found the right i/o port for the LEDs. Seems hal_app just sets bits in a file /var/../ledvalue that hal_daemon reads and outputs. Your work here has given me some good leads, so thank you for that!

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