Skip to content

Instantly share code, notes, and snippets.

@zopieux
Last active November 18, 2024 11:35
Show Gist options
  • 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); }
}
@stephenhouser
Copy link

@davidedg
Copy link

Hi. I'm trying to make the front usb copy button work on Debian12 (later on TrueNAS). Mine is a TS-470Pro, here's the relevant bits from model.conf:

`[System Enclosure]
VENDOR = QNAP
MODEL = TS-470 Pro
CAP = 0x614D15C
SIO_DEVICE = F71869A

[System IO]
RESET_BUTTON = SIO:I92:B1
USB_COPY_BUTTON = SIO:IE2:B2
STATUS_GREEN_LED = SIO:I91:B2
STATUS_RED_LED = SIO:I91:B3
FRONT_USB_LED = SIO:IE1:B7
POWER_LED = SIO:IE1:B6
VOICE_ALERT_SUPPORT = 0
`

I first tried your sample C program (sio_read_write.c) but nothing was working.
I thought smth was different with the hw so I started trying to reverse it myself.
So far I managed to run "hal_daemon -f" inside a chroot that is composed of my original QTS 4.3.x system. The hal_daemon works and I can:

  • enable/disable the buzzer with hal_app --se_buzzer enc_id=0,mode=1
  • ltrace the hal_daemon process and see that it periodically calls this function from libuLinux_hal: GPIO_Does_Copy_Button_Press:

.... GPIO_Does_Copy_Button_Press(0, 0, 0, 0) = 1 if button is pressed and 0 if button is not pressed.
This is the exact same ltrace I get inside the original QTS (compiling ltrace for QTS has proven... challenging!)

Now... I discovered a very interesting behaviour:
If I run the chrooted hal_daemon for some seconds ... and then I kill it.... your sio_read_write.c works perfectly!
So ... I guess there must be some sort of "initialization" code that "unlocks" access to the Super I/O chip?

I tried disassembling the libuLinux_hal library: there's a function GPIO_Does_Copy_Button_Press that eventually goes to the lower levels of the SuperIO ... but ... from what I could see, it is "poking" more ports than just the baseport.

Is there some magic I am missing?
thx!

@zopieux
Copy link
Author

zopieux commented Sep 29, 2024

@davidedg Honestly that is more investigation than I ever did for this project. I'm not sure why I don't seem to require such initialization at all with my upstream kernel.

@davidedg
Copy link

davidedg commented Oct 1, 2024

@davidedg Honestly that is more investigation than I ever did for this project. I'm not sure why I don't seem to require such initialization at all with my upstream kernel.

Possibly I have an updated bios? Or the specific model (470Pro) has some nuances?
I am happy with this workaround I created.

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