Reverse engineered from firmware v4.0.3 (squashfs-root/). For custom firmware that lets you push arbitrary images to the display, see custom_display_fw/.
- Hardware Architecture
- Kernel Driver Stack
- Software Architecture
- uictld — UI Control Daemon
- Host - MCU Serial Protocol
- libafixui.so — UI Rendering Library
- LCD MCU Firmware Upgrade Flow
- Integration Points
- Display on MCU Side
- Summary
The AFi-R has a physically separate LCD display board connected to the main router board via micro-USB. This is a two-processor architecture:
| Component | Detail |
|---|---|
| Main SoC | Qualcomm Atheros QCA956X (MIPS 74Kc, 775 MHz) — runs Linux/OpenWrt |
| Display MCU | STM32F205VGT6 (ARM Cortex-M3, LQFP100, 1MB flash, 128KB SRAM) |
| HSE Crystal | 8.00 MHz (TXC brand, confirmed from board inspection) |
| LCD Panel | 240x240 ILI9341V or ST7789V — driven via FSMC 8080 parallel from the STM32 |
| Touch Controller | Cypress TrueTouch Gen5 (cyttsp5) — connected via I2C to the STM32 |
| Communication | USB CDC ACM (virtual serial port) — /dev/ttyACM0 |
| Board P/N | 11-00428-04 (Ubiquiti), also labeled 444G0 |
| Connector | Purpose |
|---|---|
| Micro-USB | Communication with main router board (USB CDC serial) |
| SWD | 6-pin SWD debug/programming header (pin 1 marked with rectangle = VCC 3.3V) |
| J7 | 2-pin BOOT0 jumper — short both pads to enter STM32 ROM DFU bootloader |
| LCM | FFC connector to LCD panel (bottom right, FSMC 8080 parallel) |
| J2 | Touch panel / I2C connector (top right) — Parade S1222 via I2C1 (PB6/PB7) |
| J4 | Additional connector (right side) |
| J8 | Speaker connector (DAC CH2 via amplifier) |
| J28 (on router mainboard) | 6-pin header carrying USB signals + power to the display board. Can be hot-plugged while router is on. |
- Resolution: 240x240 pixels
- Color depth: RGB565 (16-bit, 65K colors)
- Interface: 8-bit 8080 parallel via STM32 FSMC (NOT SPI as the host-side driver name suggests)
- FSMC mapping: Command register at
0x60000000, Data register at0x60020000(A16 selects D/C) - Controllers: Dual support — ILI9341V (config 0) or ST7789V (config 1), selected at runtime via flag at RAM
0x20000EA8 - LCD driver vtable in RAM at
[0x20002484]with function pointers for read/write/dimensions
0483:5740— Normal operation (CDC ACM virtual serial port, STMicroelectronics VID)0483:df11— STM32 ROM DFU bootloader (activated by shorting J7 BOOT0 jumper)1f9b:0— Alternate LCD module detection (checked by uictld)
For PLL/clock configuration and DFU recovery instructions, see custom_display_fw/.
| File | Size | Purpose | Version String |
|---|---|---|---|
lib/firmware/AFi-R_LCD.bin |
896 KB | Application firmware | aFiDsp.AP.03.75 |
lib/firmware/AFi-R_LCD_BL.bin |
64 KB | Bootloader firmware | aFiDsp.LD.01.32 |
lib/firmware/cyttsp5_fw.bin |
58 KB | Touch controller firmware | — |
The MCU firmware is ARM Cortex-M with:
- Initial SP:
0x2001FFCF(128 KB SRAM) - Reset vector:
0x08021578(in Flash, offset past bootloader) - Bootloader at
0x0800xxxx, app at0x08020000+
Module loading order (from /etc/modules.d/):
02-afi-leds → afi_leds.ko (LED ring)
06-fb → fb.ko (framebuffer core)
07-fb-sys-* → sysfillrect/syscopyarea/sysimgblt/fb_sys_fops.ko
08-afi-sp8120fb → sp8120fb.ko (Visionox SPI framebuffer)
backlight → video.ko, backlight.ko
input-cyttsp5 → cyttsp5.ko, cyttsp5_i2c.ko, cyttsp5_loader.ko
60-input-evdev → evdev.ko
- SPI-connected framebuffer driver (device tree compatible:
visionox,sp8120fb) - Module is loaded but NO SPI device is bound — verified via sysfs:
/sys/bus/spi/drivers/visionox,sp8120fbexists (driver registered)- No
/sys/class/graphics/fb0exists (no device instance) - No
/sys/class/backlight/sp8120fb0/brightnessexists
- The main SoC has no direct SPI connection to the LCD panel — the only physical connection to the display board is USB
- Writing to
/dev/fb0has no effect on the display (confirmed via testing) - The
libafixui.soframebuffer code path (fbdev_init, fbdev_flush) is unused in this hardware configuration
- Standard USB ACM driver, creates
/dev/ttyACM0for host-MCU serial communication - This is the sole communication path between the main SoC and the display
┌─────────────────────────────────────────────────────────┐
│ Main SoC (MIPS) │
│ │
│ ┌──────────┐ ubus IPC ┌──────────────────────┐ │
│ │ ajconfd │◄──────────────►│ uictld │ │
│ │ dnsmasq │ │ (UI Control Daemon) │ │
│ │ bleproto │ │ │ │
│ │ firewall │ │ │ │
│ │ hotplug │ │ │ │
│ └──────────┘ └──────────┬───────────┘ │
│ │ │
│ /dev/ttyACM0 │
│ │ │
│ cdc-acm.ko │
│ (USB serial) │
└─────────────────────────────────┬───────────────────────┘
│ USB (sole connection)
▼
┌─────────────────────────────────────────────────────────┐
│ LCD Display Board (STM32F2xx) │
│ │
│ AFi-R_LCD_BL.bin (bootloader, 64KB @ 0x08000000) │
│ AFi-R_LCD.bin (application, 896KB @ 0x08020000) │
│ │
│ ┌──────────┐ ┌───────────────┐ ┌────────────────┐ │
│ │ USB CDC │ │ FSMC 8080 par │ │ I2C → cyttsp5 │ │
│ │ (comms) │ │ → ILI9341V / │ │ (touch input) │ │
│ │ │ │ ST7789V LCD │ │ │ │
│ └──────────┘ └───────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────┘
Note: sp8120fb.ko is loaded on the host but has NO bound SPI device.
The host has no direct path to the LCD. ALL display control goes
through the MCU via USB CDC serial.
Binary: /usr/bin/uictld (MIPS32, dynamically linked, musl libc)
Init: Starts at boot priority 13 via procd with auto-respawn (3600s window, 5s delay). Run with -f (foreground).
libafixui.so— UI rendering (LittlevGL + custom Xui C++ framework)libubus.so— OpenWrt IPC buslibuci.so— OpenWrt configurationlibjson-c.so.5— JSON parsinglibblobmsg_json.so— ubus message encodinglibiwinfo.so— wireless infolibsw.so— switch library (port states)libuhsys.so— Ubiquiti system libraryliba.so— Ubiquiti utility library
From uictld.@uictld[0]:
| Key | Purpose |
|---|---|
lcd_backlight |
Backlight brightness level |
lcd_backlight_timeout |
Screen timeout/sleep delay |
lcd_clock_type |
Clock display format |
lcd_test |
Enable test mode |
led |
LED brightness |
night_mode |
Night mode on/off |
night_start |
Night mode start hour |
night_end |
Night mode end hour |
snd_sys |
System sound enabled |
snd_tap |
Tap sound enabled |
snd_volume |
Sound volume |
adoption_filter |
Adoption RSSI filtering |
adoption_min_rssi |
Min RSSI for adoption |
Object: uictld
Methods:
| Method | Arguments | Purpose |
|---|---|---|
lcd |
See below | Send display commands |
force-inetcheck |
— | Force internet connectivity check |
pause-inetcheck |
{"timeout": N} |
Pause inet check N seconds |
upgrade |
— | Enter upgrade mode on display |
reboot |
— | Reboot display MCU |
get-params |
— | Get current display parameters |
get-adoption-cache |
— | Get cached adoption info |
set-params |
snd_sys, snd_tap, snd_volume, led, lcd_backlight, clock_type, lcd_backlight_timeout, night_mode, night_start, night_end (all Integer) |
Set display/sound parameters |
locate |
{"timeout": N} |
Enter locate mode (pulsing) |
cfg |
{"state": "string"} |
Set configuration state |
wifi |
{"state": N, "rssi": N, "uplink": "string"} |
WiFi state update |
button-pressed |
— | Simulate button press |
button-released |
— | Simulate button release |
wps-start |
— | Start WPS |
wps-cancel |
— | Cancel WPS |
wps-done |
— | End WPS |
lcd method arguments:
sta-count (Int), eth-count (Int), timezone (String), guest-timeout (Int), guest-remaining (Int), guest-count (Int), mp-pause (Int), inet-pause (Int), paused-devices (Int), test-cmd (String), wifi-proto-clients (Table), wifi-proto-client-removed (Table), update-avail (Table), port-up (Int), port-down (Int), wps-enabled (Int), teleport-pairing (Table), powerbox (Table), turbo-mode (Boolean), qos-enabled (Boolean), wan-bridge-mode (Boolean)
Events listened for:
ubus.object.add/ubus.object.remove- Switch port hotplug events
- Configuration state changes
- Firmware update notifications
Communication over /dev/ttyACM0 uses a text-based message protocol with \r\n line termination. Messages are prefixed with hostmsg= (host to MCU) or parsed as JSON (MCU to host).
The MCU firmware has two separate command parsing layers:
Layer 1 — CDC-level prefix check (checked first, in order):
debugmsg— debug output controlusbcdcreattach— USB CDC reconnectwatchdog=— watchdog controlmcutemp— temperature sensorhostmsg=— main display state protocol (JSON payload)
Layer 2 — Dispatch table (checked only if Layer 1 doesn't match):
16-entry table at flash 0x080A1A94 (string pointers) and 0x080A1AD4 (function pointers):
| # | Command | Purpose |
|---|---|---|
| 0 | blversion |
Bootloader version |
| 1 | listcmd |
List all commands |
| 2 | reboot |
Reboot MCU |
| 3 | blreboot |
Reboot into bootloader |
| 4 | echo= |
Echo input (debug) |
| 5 | mode |
Current mode (APP/LOADER) |
| 6 | status |
Status code |
| 7 | fwversion |
Firmware version |
| 8 | fwbuildtime |
Build timestamp |
| 9 | blhash= |
Set bootloader hash |
| 10 | fwhash= |
Set firmware hash (for updates) |
| 11 | mem=0x |
Read memory (Flash/RAM only) |
| 12 | storedfwhash |
Get stored firmware hash |
| 13 | psustatus |
Power supply status |
| 14 | lcdsettings |
LCD controller info (ILI9341V/ST7789V) |
| 15 | tpstatus |
Touch panel status |
Additional commands registered at runtime (not in flash table):
audiotest=, hwtestall
Note: Direct serial access to the dispatch table commands (via dd of=/dev/ttyACM0) can be unreliable due to Linux cdc_acm driver buffering. The ubus call uictld ... interface is the recommended way to control the stock display.
hostmsg={"host_state": "booting"}
hostmsg={"host_state": "ready"}
hostmsg={"host_state": "<state>"}
hostmsg={"inet_status": "<color>","ip": "<wan_ip>","ip_lan": "<lan_ip>","ipv6": "<y/n>","sta_count": N,"eth_count": N,"paused_count": "N"}
hostmsg={"port_1": "0/1","port_2": "0/1","port_3": "0/1","port_4": "0/1","port_w": "u/d"}
hostmsg={"setup_ssid": "<ssid>"}
hostmsg={"throughput_tx": N,"throughput_rx": N}
hostmsg={"max_bandwidth_tx": N,"max_bandwidth_rx": N}
hostmsg={"usage_date": [y,m,d],"usage_rx": N,"usage_tx": N,"usage_unit": N}
hostmsg={"signal": N,"signal_desc": "<desc>"}
hostmsg={"backlight": N}
hostmsg={"screen_timeout": "[0,N]"}
hostmsg={"wps_enabled": "0/1"}
hostmsg={"wps_remaining": "N"}
hostmsg={"qos": "0/1"}
hostmsg={"update": "<version>"}
hostmsg={"add_device": "<device_info>"}
hostmsg={"teleport_mac": "<mac>"}
hostmsg={"mp_pause": "0/1"}
hostmsg={"time":[h,m,s,d,mo,y,dow,dst]}
hostmsg={"turbo": "0/1"}
hostmsg={"gaming": "1"}
hostmsg={"g_wifi_remaining": N,"g_wifi_timeout": N}
hostmsg={"sndsys": N}
hostmsg={"sndtap": N}
hostmsg={"config":{"LedBrightness":N,"SystemSoundVolume":N}}
hostmsg={"powerbox": "0/1","pb_port_1": "0/1",...,"pb_port_7": "0/1","pb_charging": "0/1","pb_battery": "N","pb_remaining": "N"}
The MCU responds with status messages containing fields like:
status— "ok" or errormode— "application" or "bootloader"fwversion— firmware version stringblversion— bootloader versionfwhash=/blhash=— MD5 hashes for integrity verificationstoredfwhash— stored firmware hash on device- Touch panel events:
[info:] TouchPanel: Ready, Status: 0x%X, ID: %s, Ver: %02X%02X%02X%02X - Button events relayed back to uictld
INET_STATUS_GREEN— connectedINET_STATUS_YELLOW— partial (no DNS, no HTTP, etc.)INET_STATUS_RED— disconnected
INET_REASON_NO_LINK, INET_REASON_NO_GW_ARP, INET_REASON_NO_GW, INET_REASON_NO_ADDR, INET_REASON_NO_DNS, INET_REASON_NO_HTTP, INET_REASON_NONE
Written in C++ with LittlevGL (lvgl) v5.x embedded — a lightweight graphics library for embedded systems. Linked by uictld but its framebuffer code path (fbdev_init, fbdev_flush) is unused on this hardware since sp8120fb has no bound SPI device. The MCU firmware handles all display rendering directly.
The Xui class is the central coordinator. Its exported C API:
| Function | Purpose |
|---|---|
Xui_Init() |
Initialize graphics system, framebuffer, touch, lvgl |
Xui_Exec() |
Run the main event loop (calls lv_task_handler) |
Xui_IncreaseTick(ms) |
Advance lvgl tick counter |
Xui_SetState(state_t) |
Set router state, switch screens |
Xui_SetNotification(notification_t) |
Set notification icon (Internet/IP/Link/Pause/None) |
Xui_SetBrightness(level) |
Set backlight |
Xui_SetNightMode(enabled) |
Enable/disable night mode |
Xui_SetSetupSsid(ssid) |
Show setup SSID on screen |
Xui_Set24hClock(bool) |
Toggle 12h/24h clock |
Xui_SetClockAvailable(bool) |
Show/hide clock |
Xui_UpdateClock() |
Refresh clock display |
Xui_SetPortState(wan, lan) |
Update port status display |
Xui_SetIp(wan_ip, lan_ip) |
Update IP addresses |
Xui_SetClients(wireless, ethernet, guest) |
Update client counts |
Xui_SetThroughput(tx, rx, max_tx, max_rx) |
Update throughput arcs |
Xui_SetUsage(tx, rx, unit, date) |
Update usage stats |
Xui_SetSignal(level, desc) |
Update mesh signal display |
Xui_SetQos(qos_t) |
Update QoS status |
Xui_SetAppConnected(bool) |
Show app connection status |
Xui_SetMeshPointPaused(bool) |
Show mesh point paused state |
Xui_SetUpdateCallback(cb) |
Register firmware update callback |
Xui_Update(version) |
Show update notification |
Xui_UpdateConfirm() |
Handle update confirmation |
Xui_SetAddDeviceCallback(cb) |
Register device adoption callback |
Xui_AddDevice(info) |
Show add device dialog |
Xui_AddDeviceConfirm() |
Handle adoption confirmation |
Xui_AddDeviceNotification(info) |
Show device notification |
Xui_RunLcdTest() |
Run LCD color test |
Xui_RunTpTest() |
Run touch panel test |
Xui_RunBurnInTest() |
Run burn-in test |
Xui_HideUpdate() |
Dismiss update dialog |
Xui_HideAddDevice() |
Dismiss add device dialog |
The SetState function drives the main screen transitions:
| State | Screen Shown |
|---|---|
SPLASH |
Boot splash screen |
SETUP |
Initial setup (show SSID, "Get the App") |
CONFIGURING |
Configuration in progress |
CONNECTING |
Connecting to internet |
READY / DONE |
Normal operation (cycle through info screens) |
UPDATING |
Firmware update in progress |
REBOOTING |
Reboot in progress |
LOCATE |
Locate mode (pulsing animation) |
Each screen is a C++ class on a named layer:
| Class | Layer Name | Description |
|---|---|---|
ScreenSplash |
— | Boot splash with AmpliFi logo |
ScreenSsid |
ssid |
Shows setup SSID |
ScreenApp |
— | "Get the App" / app store icons |
ScreenClock |
clock |
Time display (12h/24h) |
ScreenIp |
ip.wan, ip.device |
WAN IP / Device IP display |
ScreenPorts |
ports |
Ethernet port status (4 ports + WiFi) |
ScreenThroughput |
throughput |
TX/RX arc animations |
ScreenUsage |
usage |
Data usage with arc visualization |
ScreenSignal |
signal |
Mesh point signal strength |
ScreenQos |
— | QoS on/off status |
ScreenInfo |
— | Generic info screen (icon + text) |
ScreenDialog |
— | Confirmation dialogs (Update/AddDevice) |
ScreenUpdate |
update |
Firmware update prompt (UPDATE/SKIP buttons) |
ScreenAddDevice |
add.device |
Device adoption prompt (CONFIRM/DISMISS) |
ScreenLocate |
— | Locate pulsing animation |
ScreenBlank |
blank |
Screen off / sleep |
ScreenSaverBase |
— | Screensaver base class |
ScreenSaverClock |
saver.clock |
Clock screensaver (moves position) |
ScreenSaverImage |
— | Image screensaver (moves position) |
TestLcd |
test.lcd |
LCD color cycling test (SwitchBackgroundColor) |
TestTp |
test.tp |
Touch position test (ShowTouchPosition) |
TestBurnIn |
test.burnin |
Burn-in test |
Shown as overlay icons:
NONE— no notificationINTERNET— internet status indicatorIP— IP address notificationLINK— link state notificationPAUSE— paused state
Fonts (Rajdhani family, Google Font):
rajdhandi_light_40,rajdhandi_medium_16,rajdhandi_medium_40rajdhandi_semibold_13,rajdhandi_semibold_15,rajdhandi_semibold_16,rajdhandi_semibold_22,rajdhandi_semibold_mono_22lv_font_dejavu_20,lv_font_symbol_20(lvgl built-ins)
PNG images (compiled into the .so as C arrays):
amplifi_png, app_png, app_store_png, bell_png, cable_png, connect_png, done_png, dots_png, ethernet_icon_png, gear_png, globe_png, google_play_png, pause_png, pause_icon_png, port_connected_png, port_disabled_png, port_disconnected_png, power_icon_png, power_png, rx_png, tx_png, tx_gray_png, type_c_png, update_png, wan_icon_png, wireless_icon_png
Burn-in test images: burn_bw_png, burn_bw_small_png, burn_gray_png, burn_gray_i_png, burn_logo_png, burn_logo_i_png, burn_ubnt_png, burn_ubnt_i_png, burn_wb_png, burn_wb_small_png
Global style objects control theming:
g_styleBase— base backgroundg_styleWhite,g_styleGray— color variantsg_styleArc,g_styleArcCurve,g_styleArcMask— throughput/usage arc renderingg_styleLocate— locate mode pulsingg_styleConfirmButton,g_styleConfirmButtonPressed— dialog buttonsg_styleTestLcd— test mode- Font-specific styles:
g_styleRajdhandiSemiBold13throughg_styleRajdhandiMedium40with Gray/Black/Mono variants
The Layers namespace manages screen stacking with Layers::Index enum values. Methods:
Layers::Initialize()/Layers::Deinitialize()Layers::Show(Index)/Layers::Hide(Index)Layers::GetHandle(Index)— returns lvgl parent object
The Xui class manages a std::map<Layers::Index, std::unique_ptr<ScreenBase>> and a std::map<std::string, std::function<ScreenBase*()>> factory for screen creation.
The MCU firmware can be upgraded in-field from the host:
- Hash check: Host reads stored hash from MCU (
storedfwhash), compares with MD5 ofAFi-R_LCD.bin - If different: Initiates firmware upgrade via
forcefwupgrade=<size>command - Transfer: Raw binary data sent over ttyACM to MCU
- Verify: MCU verifies hash and stores firmware
- Reboot:
startappcommand or automatic reboot; bootloader verifies and boots new firmware
The bootloader handles crash recovery: if app crashes >= 5 times, it stays in bootloader mode. The stored firmware hash is kept at flash 0x080FFC00 (last 1 KB of sector 11).
port down → ubus call uictld lcd '{"port-down":N}'
if uplink port → force-inetcheck
port up → ubus call uictld lcd '{"port-up":N}'
if uplink port → pause-inetcheck 5s
1. ubus call uictld upgrade → LCD shows "Updating" screen
2. echo upgrade > /proc/afi_leds/mode → LED ring shows upgrade pattern
3. uh-fw-tool -f <image> → flash firmware
4. reboot -f
uictld processes physical button events:
- < 1 sec: Click (counted for multi-click patterns)
- 1-7 sec hold: Standard action
- 7 sec hold: Medium reset
- 15 sec hold: Factory reset
ajconfd— device configuration, WPS, adoption, firmware updatesbleproto— Bluetooth mesh device discoveryinetcheck— internet connectivity monitoring, drivesinet_statuson LCD
The STM32F2xx MCU firmware (aFiDsp.AP.03.75) contains:
- Embedded GIF images (GIF87a/GIF89a headers found) — boot animations, status indicators
- USB CDC communication stack ("STM32 Virtual ComPort", "CDC Config")
- STM32 HAL drivers: ADC, Cortex, DAC, DMA, Flash, GPIO, I2C, IWDG (watchdog), PCD (USB), RCC, RTC, TIM, FSMC
- RTC for timekeeping (receives time sync from host via
hostmsg={"time":[...]}) - IWDG (independent watchdog) for crash recovery
- Temperature sensor reporting
- CPU usage reporting
- LCD settings with two configuration modes (0 and 1)
The MCU renders UI strings directly:
"ROUTER IP", "DEVICE IP", "PORT STATUS", "MESH SIGNAL", "POWERBOX", "Hold to enable WPS", "Press WPS button", "Internet is paused", "Mesh Point is paused", "AmpliFi will be unavailable during update", "Teleport: Accept/Dismiss", "Charging"
STM32F2xx flash sectors used by the application firmware:
| Sector | Address Range | Size | Content |
|---|---|---|---|
| 5 | 0x08020000-0x0803FFFF |
128 KB | App code + vector table + version string |
| 6 | 0x08040000-0x0805FFFF |
128 KB | App code + string tables |
| 7 | 0x08060000-0x0807FFFF |
128 KB | Embedded GIF images (149 GIFs) |
| 8 | 0x08080000-0x0809FFFF |
128 KB | App code + command dispatch table |
| 9 | 0x080A0000-0x080BFFFF |
128 KB | App data |
| 10 | 0x080C0000-0x080DFFFF |
128 KB | App data |
| 11 | 0x080E0000-0x080FFFFF |
128 KB | App data + free space + tail tag |
Firmware file is exactly 916,480 bytes (7 x 128KB - 1024). The last 1 KB of Sector 11 (0x080FFC00-0x080FFFFF) is reserved for bootloader metadata (firmware hash storage).
Key offsets within the firmware file:
| File Offset | Flash Address | Content |
|---|---|---|
0x000000 |
0x08020000 |
Vector table (initial SP, reset vector, exceptions) |
0x000200 |
0x08020200 |
Version string: aFiDsp.AP.03.75 |
0x0435DC |
0x080635DC |
Start of embedded GIF images |
0x081A94 |
0x080A1A94 |
Command dispatch string pointer table (16 entries) |
0x081AD4 |
0x080A1AD4 |
Command dispatch function pointer table (16 entries) |
0x0D59B8 |
0x080F59B8 |
End of initialized data / .data init image (zeros) |
0x0D5AA8 |
0x080F5AA8 |
Free space (erased flash, 0xFF, ~41 KB) |
0x0DFBF0 |
0x080FFBF0 |
Tail tag: 16 bytes of 0x41 ('A') — integrity marker |
Integrity checks performed by the bootloader:
- Tail tag check — verifies 16 bytes of
0x41at0x080FFBF0 - MD5 hash check — compares stored hash (from
fwhash=command during update) against computed hash of firmware in flash - Vector table validation — checks initial SP and reset vector are in valid Flash/RAM ranges
- Crash counter — tracks consecutive boot failures; stays in bootloader after 5 crashes
Critical note for firmware modification: The region at file offset 0x0D59B8-0x0D5AA7 (240 bytes of zeros) is NOT free space — it is the .data section initialization image. Writing non-zero bytes here will corrupt RAM variables on boot and cause crashes. True free space begins at 0x0D5AA8 (the 0xFF region).
149 GIF images are embedded in the firmware, including:
- Full screen: 240x240 (1 image — "Plug in Cable" setup screen)
- Banners: 240x151 (2 images — "Latency Optimized", etc.)
- UI elements: 207x92, 200x61, 190x61, 100x167, 98x174, 96x96, etc.
- Digit/character sprites: 32x47 (20 images), 26x42 (many), 13x42, 14x18
- Icons: Various small sizes for ports, power, wireless, etc.
GIF images can be replaced by modifying the firmware binary at their offsets. Single-byte changes to GIF data are accepted by the bootloader integrity check (confirmed via testing — the MD5 hash is recomputed by uictld during the firmware update).
LCD_WriteCmd(0x0802FF00) — sends command byte
→ lcd_send_cmd(0x080317F0)
→ fsmc_write_cmd(0x080316C8) — writes to FSMC CMD @ 0x60000000
LCD_WriteData(0x0802FF1A) — sends data byte
→ lcd_send_data(0x080317D6)
→ fsmc_write_data(0x080316A8) — writes to FSMC DATA @ 0x60020000
Note: There is a vtable-based indirection for LCD drivers supporting both ILI9341V and ST7789V. The vtable pointer is at RAM [0x20002484].
The display module includes a speaker driven by the STM32's DAC peripheral.
Hardware:
| Pin | Function | Notes |
|---|---|---|
| PA4 | DAC CH1 | Backlight brightness control |
| PA5 | DAC CH2 | Audio output to amplifier |
| PA6 | Amplifier enable | GPIO push-pull, active HIGH. No sound without this. |
| J8 | Speaker connector | On the display board |
- DAC: 12-bit, dual-channel (
0x40007400). CH1 = backlight (PA4), CH2 = audio (PA5). - Sample rate: 16 kHz (stock firmware uses TIM7 at
0x40001400; TIM6 at0x40001000also works with TSEL2=000) - DMA: DMA1 Stream 6, Channel 7 transfers samples to DAC CH2
- Amplifier: Enabled by driving PA6 HIGH. Stock firmware sets PA6 HIGH before playback and LOW after to avoid idle noise.
- PC6/TIM3: Stock firmware configures PC6 as TIM3 CH1 PWM (AF2). Testing showed this does not control speaker volume and causes audible 10 kHz whine when driven with PWM. Its actual purpose is unknown; leaving it as GPIO HIGH (as part of backlight init) works fine.
Built-in tones (stored as 12-bit sample arrays in flash):
| Tone | Purpose |
|---|---|
bell |
Notification chime |
fantasy / fantasy fix |
Startup jingle variants |
ring |
Ring tone |
sweep |
Frequency sweep (test) |
tap |
Touch feedback click |
tone |
Simple tone |
Stock firmware protocol:
hostmsg={"sndsys":1}— enable/disable system soundshostmsg={"sndtap":1}— enable/disable tap soundsaudiotest=bell— play a tone directly (MCU serial command){"command":"bell","volume":50,"volume-tmp":1}— play tone with volume (sent by uictld withouthostmsg=prefix){"volume":50}— set volume level
UCI config: uictld.@uictld[0].snd_sys, snd_tap, snd_volume
The display module includes a Parade/Cypress TrueTouch Gen4 capacitive touchscreen (not Gen5 as the kernel module name suggests).
Hardware:
| Pin | Function | Configuration |
|---|---|---|
| PB6 | I2C1_SCL | AF4, open-drain, no pull-up (external pull-ups on board) |
| PB7 | I2C1_SDA | AF4, open-drain, no pull-up |
| PB8 | Touch reset (XRES) | Push-pull output, active LOW: assert 20ms, release, wait 500ms |
| PB9 | Touch interrupt (INT) | Input with pull-up, active LOW = data ready |
- Controller: Parade TrueTouch Gen4, silicon ID S1222, protocol DS4 R4.0.0
- I2C address: 0x20 (7-bit, app mode), 0x08 (bootloader mode)
- I2C clock: 100 kHz standard mode (APB1=30MHz, CCR=150, TRISE=31)
- Firmware:
lib/firmware/cyttsp5_fw.bin(59,204 bytes) — also embedded pre-processed in the MCU flash at offset 0xC56E4. The firmware is configured by Ubiquiti to map the touch sensor grid to 240x240 display pixel coordinates. - Host-side kernel modules:
cyttsp5.ko,cyttsp5_i2c.ko,cyttsp5_loader.ko - Dual-driver architecture: stock firmware probes for FocalTech FT3308 (0x38) first, then Parade S1222 (0x20/0x08)
I2C protocol (app mode):
-
Stock firmware uses
HAL_I2C_Mem_Write/HAL_I2C_Mem_Read(polling, not interrupt/DMA) -
Touch data register is dynamically discovered at boot via descriptor scan (pages 0-4, registers 0xE9 down by 6 to 0x05, type byte 0x11)
-
Read 6 bytes from the discovered page+register using page-select protocol
-
Touch record format (6 bytes):
Byte Content 0 Touch flags (bit 0 = tip switch: 1=touching, 0=lifted) 1 Touch ID 2 X high byte 3 Y high byte 4 X low nibble [3:0], Y low nibble[7:4](packed)5 Pressure -
Coordinate extraction:
X = (buf[2] << 4) | (buf[4] & 0x0F),Y = (buf[3] << 4) | (buf[4] >> 4)— packed as 12-bit fields, values map to display pixels (0-239 x 0-239, origin at top-left) -
I2C bus recovery (GPIO SCL toggle 9x + STOP) required at init — peripheral SWRST alone cannot clear a stuck bus
I2C protocol (bootloader mode):
- Page-select: write page (0x20) to register 0xFF before each access
- Firmware upload in 16-byte blocks via register 0x02, with notifications via register 0x12
- Bootloader reports block counts via register 0x10
- See stock firmware functions at offsets 0x0FF68 (hid_i2c_read), 0x0FFD0 (hid_i2c_write), 0x10BC4 (upload)
Embedded touch firmware (in MCU flash):
- Location: offset 0xC56E4, 256-byte header + Section 1 (0xB000 bytes, 2816 blocks) + Section 2 (80 bytes, 5 blocks)
- Header contains: silicon ID "S1222", protocol "DS4 R4.0.0", section offsets, I2C descriptor
- Section 1: application firmware code (encrypted/obfuscated)
- Section 2: configuration/calibration data, first bytes
11 52 00 02(expected version response)
Stock firmware behavior:
- Touch events are processed by the MCU firmware directly
- The MCU reports touch events to the host:
[info:] TouchPanel: Ready, Status: 0x%X, ID: %s, Ver: %02X%02X%02X%02X - Touch is used for screen navigation (swipe between info screens) and button presses (WPS, update confirm/dismiss)
The STM32 MCU has sole control over the display, speaker, and touchscreen. The MIPS host communicates via hostmsg= JSON over USB CDC serial (/dev/ttyACM0). The host cannot send arbitrary pixel data through the stock firmware — it can only influence which predefined screen is shown and what data it displays. The sp8120fb framebuffer driver on the host is loaded but has no bound SPI device and is unused.
For arbitrary display control, see the custom display firmware.
| Path | Description |
|---|---|
/usr/bin/uictld |
UI control daemon (MIPS32) |
/usr/lib/libafixui.so |
UI rendering library (LittlevGL + Xui) |
/lib/firmware/AFi-R_LCD.bin |
LCD MCU application firmware |
/lib/firmware/AFi-R_LCD_BL.bin |
LCD MCU bootloader firmware |
/lib/firmware/cyttsp5_fw.bin |
Touch controller firmware |
| Path | Description |
|---|---|
/lib/modules/4.1.16/sp8120fb.ko |
Visionox SP8120 SPI framebuffer |
/lib/modules/4.1.16/fb.ko |
Framebuffer core |
/lib/modules/4.1.16/fb_sys_fops.ko |
Framebuffer file operations |
/lib/modules/4.1.16/backlight.ko |
Backlight subsystem |
/lib/modules/4.1.16/cyttsp5.ko |
Cypress TrueTouch driver |
/lib/modules/4.1.16/cyttsp5_i2c.ko |
Touch I2C transport |
/lib/modules/4.1.16/cyttsp5_loader.ko |
Touch firmware loader |
/lib/modules/4.1.16/evdev.ko |
Input event device |
/lib/modules/4.1.16/cdc-acm.ko |
USB CDC ACM serial |
/lib/modules/4.1.16/afi_leds.ko |
LED ring driver |
/lib/modules/4.1.16/gpio-button-hotplug.ko |
Physical button handler |
/lib/modules/4.1.16/spi-gpio.ko |
SPI over GPIO |
/lib/modules/4.1.16/spi-gpio-custom.ko |
Custom SPI GPIO config |
| Path | Description |
|---|---|
/etc/init.d/uictld |
Service init script (START=13, procd) |
/etc/hotplug.d/swport/01-update-uictld |
Port state change handler |
/etc/modules.d/08-afi-sp8120fb |
SP8120 module autoload |
/etc/modules.d/backlight |
Backlight module autoload |
/etc/modules.d/input-cyttsp5 |
Touch module autoload |
/lib/functions/afi.sh |
Board detection helpers (has_lcd, etc.) |
/sbin/afiupgrade |
Firmware upgrade script |
