Skip to content

Instantly share code, notes, and snippets.

@vm-wylbur
Created February 7, 2026 23:38
Show Gist options
  • Select an option

  • Save vm-wylbur/603a338acd226348146e15338800cec4 to your computer and use it in GitHub Desktop.

Select an option

Save vm-wylbur/603a338acd226348146e15338800cec4 to your computer and use it in GitHub Desktop.
QNAP TVS-h874X LCD Control on Ubuntu

QNAP TVS-h874X LCD Control on Ubuntu

Successfully enabled the front-panel LCD on QNAP TVS-h874X running Ubuntu 24.04 LTS.

Hardware

  • Model: QNAP TVS-h874X
  • Chip: IT8528 Embedded Controller (Chip ID: 0x5571)
  • LCD: 16x2 character display via A78 protocol
  • Serial Port: /dev/ttyS1 at 0x02F8, IRQ 3, 1200 baud

The Problem

On non-QTS operating systems (Ubuntu, TrueNAS, etc.), the LCD exists at /dev/ttyS1 but doesn't respond to commands. The IT8528 Super I/O configuration registers need to be initialized.

The Solution

Initialize IT8528 LDN 0x02 (UART B) registers to enable COM2/LCD communication.

Required Register Values

LDN 0x02 (UART B / COM2 / LCD):
  CR30: 0x01  (Activate)
  CR60: 0x02  (Base address high)
  CR61: 0xF8  (Base address low) → 0x02F8
  CR70: 0x03  (IRQ 3)
  CR71: 0x02  (IRQ mode)
  CR74: 0x04  (Configuration)
  CR75: 0x04  (Configuration)

Files

  • init_it8528_lcd.c - IT8528 initialization program
  • lcd_write.sh - Simple script to write to LCD (includes init)
  • dump_ldn02_full.c - Dump IT8528 LDN 0x02 registers (diagnostic)

Usage

Compile

gcc -O2 -o init_it8528_lcd init_it8528_lcd.c
gcc -O2 -o dump_ldn02_full dump_ldn02_full.c

Test LCD

sudo ./init_it8528_lcd

# Initialize LCD (send backlight commands multiple times)
for i in {1..3}; do
  sudo bash -c 'printf "\x4d\x5e\x01" > /dev/ttyS1'
  sleep 0.5
done

# Write to LCD
sudo bash -c 'printf "\x4d\x0c\x00\x10Hello Ubuntu   " > /dev/ttyS1'
sudo bash -c 'printf "\x4d\x0c\x01\x10Line 2 Text    " > /dev/ttyS1'

Read Buttons

sudo hexdump -C /dev/ttyS1

Press Enter/Select buttons on front panel:

  • 53 05 00 01 = Enter pressed
  • 53 05 00 00 = Enter released
  • 53 05 00 02 = Select pressed
  • 53 05 00 00 = Select released

A78 LCD Protocol

Serial: /dev/ttyS1 at 1200 baud, 8N1

Commands:

  • Backlight on: 0x4D 0x5E 0x01
  • Backlight off: 0x4D 0x5E 0x00
  • Write line 0: 0x4D 0x0C 0x00 0x10 + 16 chars (pad with spaces)
  • Write line 1: 0x4D 0x0C 0x01 0x10 + 16 chars (pad with spaces)

Button input (from LCD):

  • Format: 0x53 0x05 [button_id] [state]
  • Enter: button_id=0x00, Select: button_id=0x02
  • State: 0x01=pressed, 0x00=released

Notes

  • No qnap8528 driver needed for LCD functionality
  • BIOS initializes IT8528 on Ubuntu - registers already set correctly
  • LCD needs multiple backlight commands on first use to "wake up"
  • 16x2 display = very limited space for info

Tested On

  • QNAP TVS-h874X
  • Ubuntu 24.04 LTS (live USB)
  • Kernel: 6.8.0-51-generic

Credits

  • @dynek - IT8528 initialization discovery
  • @zopieux - A78 protocol documentation
  • @tobetter - qnap8528 kernel driver

Other QNAP Models

This should work on other QNAP models with IT8528 chip. Check with:

sudo ./dump_ldn02_full

If you see Chip ID 0x5571 and similar register layout, it should work.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/io.h>
#define SIO_INDEX_PORT 0x2E
#define SIO_DATA_PORT 0x2F
#define SIO_ENTER_KEY 0x87
#define SIO_EXIT_KEY 0xAA
void sio_enter(void) {
outb(SIO_ENTER_KEY, SIO_INDEX_PORT);
outb(SIO_ENTER_KEY, SIO_INDEX_PORT);
}
void sio_exit(void) {
outb(SIO_EXIT_KEY, SIO_INDEX_PORT);
}
unsigned char sio_read(unsigned char reg) {
outb(reg, SIO_INDEX_PORT);
return inb(SIO_DATA_PORT);
}
int main() {
if (ioperm(SIO_INDEX_PORT, 2, 1)) {
perror("ioperm");
exit(1);
}
sio_enter();
// Select LDN 0x02 (UART B / COM2 / LCD)
outb(0x07, SIO_INDEX_PORT);
outb(0x02, SIO_DATA_PORT);
printf("IT8528 LDN 0x02 (UART B/COM2/LCD) - Full Register Dump\n");
printf("======================================================\n\n");
// Dump all important registers
for (int reg = 0x30; reg <= 0x7F; reg++) {
unsigned char val = sio_read(reg);
if (val != 0x00 || reg == 0x30 || reg == 0x60 || reg == 0x61 || reg == 0x70) {
printf("CR%02X: 0x%02X\n", reg, val);
}
}
sio_exit();
ioperm(SIO_INDEX_PORT, 2, 0);
return 0;
}
// IT8528 Super I/O Initialization for LCD (ttyS1/COM2)
// Configures LDN 0x02 (UART B) to enable COM2/LCD port
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/io.h>
#define SIO_INDEX_PORT 0x2E
#define SIO_DATA_PORT 0x2F
#define SIO_ENTER_KEY 0x87
#define SIO_EXIT_KEY 0xAA
void sio_enter(void) {
outb(SIO_ENTER_KEY, SIO_INDEX_PORT);
outb(SIO_ENTER_KEY, SIO_INDEX_PORT);
}
void sio_exit(void) {
outb(SIO_EXIT_KEY, SIO_INDEX_PORT);
}
void sio_write(unsigned char reg, unsigned char value) {
outb(reg, SIO_INDEX_PORT);
outb(value, SIO_DATA_PORT);
}
unsigned char sio_read(unsigned char reg) {
outb(reg, SIO_INDEX_PORT);
return inb(SIO_DATA_PORT);
}
int main() {
if (ioperm(SIO_INDEX_PORT, 2, 1)) {
perror("ioperm - run as root");
exit(1);
}
printf("IT8528 COM2/LCD Initialization\n");
printf("===============================\n\n");
sio_enter();
// Select LDN 0x02 (UART B / COM2 / LCD)
sio_write(0x07, 0x02);
printf("Before:\n");
printf(" CR30: 0x%02X\n", sio_read(0x30));
printf(" CR60: 0x%02X\n", sio_read(0x60));
printf(" CR61: 0x%02X\n", sio_read(0x61));
printf(" CR70: 0x%02X\n", sio_read(0x70));
printf(" CR71: 0x%02X\n", sio_read(0x71));
printf(" CR74: 0x%02X\n", sio_read(0x74));
printf(" CR75: 0x%02X\n\n", sio_read(0x75));
// Configure LDN 0x02 with QTS values
sio_write(0x30, 0x01); // Activate
sio_write(0x60, 0x02); // Base high = 0x02
sio_write(0x61, 0xF8); // Base low = 0xF8 → 0x02F8
sio_write(0x70, 0x03); // IRQ 3
sio_write(0x71, 0x02); // IRQ mode
sio_write(0x74, 0x04); // Config
sio_write(0x75, 0x04); // Config
printf("After:\n");
printf(" CR30: 0x%02X\n", sio_read(0x30));
printf(" CR60: 0x%02X\n", sio_read(0x60));
printf(" CR61: 0x%02X\n", sio_read(0x61));
printf(" CR70: 0x%02X\n", sio_read(0x70));
printf(" CR71: 0x%02X\n", sio_read(0x71));
printf(" CR74: 0x%02X\n", sio_read(0x74));
printf(" CR75: 0x%02X\n\n", sio_read(0x75));
sio_exit();
ioperm(SIO_INDEX_PORT, 2, 0);
printf("✓ IT8528 LDN 0x02 configured\n");
printf("✓ COM2/ttyS1 at 0x02F8, IRQ 3\n\n");
printf("Test LCD:\n");
printf(" stty -F /dev/ttyS1 1200\n");
printf(" echo -ne 'M\\x5e\\x01' > /dev/ttyS1\n");
printf(" echo -ne 'M\\x0c\\x00\\x10Hello Ubuntu ' > /dev/ttyS1\n");
return 0;
}
#!/bin/bash
# Initialize IT8528 LCD
/path/to/init_it8528_lcd > /dev/null 2>&1
# Wake up LCD with multiple backlight commands
for i in {1..3}; do
printf "\x4d\x5e\x01" > /dev/ttyS1
sleep 0.2
done
# Function to write to LCD
lcd_write() {
local line=$1
local text=$(printf "%-16s" "$2")
printf "\x4d\x0c\x0${line}\x10${text}" > /dev/ttyS1
}
# Write whatever you want
lcd_write 0 "$1"
lcd_write 1 "$2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment