Skip to content

Firmware Internals

Second round of reverse engineering targeting binaries from the Waveshare LC29H wiki downloads. The first round analyzed QGNSS.exe and QNMEA.dll; this round covers the QXWZ RTK SDK, firmware update tool, bootloader, and log analyzer.

BinaryTypeSizeFunctionsPurpose
libqxwz-pssdk-1.5.0ARM ELF 32-bit233 KB826QXWZ RTK positioning SDK
FlashUpdater4775_CL389797_r.exeWin32 PE2.3 MB14,425LC29H firmware update tool
bootloader.pkgRaw firmware (f3f2f1f0)20 KBN/ABroadcom bootloader package
QGNSSLog.exeWin64 PE2.0 MB4,143NMEA log analyzer/plotter
  • Architecture: ARM:LE:32:v8 (ARMv8-A 32-bit, little-endian)
  • Stripped: No (full symbols — 826 named functions)
  • Compiler: GCC 4.8.3 (Linaro GCC 2014.03)
  • Vendor: QXWZ (Qianxun Spatial Intelligence)

QXWZ operates China’s national high-precision positioning network, providing RTK corrections via their “FindCM” service. This SDK is the client-side library for accessing that service on embedded Linux (Raspberry Pi).

The SDK has a clean layered architecture with six major subsystems:

graph TD
    SDK["qxwz_sdk_init / qxwz_sdk_start / qxwz_sdk_tick"]
    SDK --> Account & Auth & Capabilities & Config & PubSub["Data Pub/Sub"] & Manager

    Account & Auth & Capabilities --> OpenAPI["OpenAPI · HTTP REST\nqxwz_http_*"]
    Config & PubSub & Manager --> OSS["OSS · MQTT-based\nqxwz_oss_*"]

    OpenAPI & OSS --> Socket["Socket Layer\nqxwz_sock_* / qxwz_dns_*"]

From decompilation of qxwz_sdk_init():

  1. qxwz_log_init() — Initialize logging
  2. sdk_mutex_init() — Create thread-safety mutex
  3. sdk_check_config(config) — Validate configuration struct
  4. sdk_check_serv_conf() — Validate server configuration
  5. sdk_init_modules() — Initialize all subsystems
  6. Set account credentials: key type, key, secret, device ID, device type
  7. Four additional callback function pointers stored from config

Error codes: -1 (mutex fail), -3 (bad config), -9 (already initialized), -15 (bad server config).

qxwz_sdk_config() accepts a type code and config pointer:

TypeFunctionDescription
0qxwz_set_serv_conf()Server host/port configuration
1qxwz_set_oss_conf()OSS (MQTT) connection settings
2qxwz_set_bds3_fmt()BDS-3 signal format preference
3qxwz_set_net_timeout()Network timeout values
4qxwz_dns_set_server()Custom DNS server (16 entries max)

Default API endpoint: openapi.qxwz.com

The auth system uses a Device Service Key (DSK) model with two key types:

Key Type 1 — Standard DSK flow:

  1. auth_get_timestamp() — Sync time with QXWZ servers
  2. auth_reload_dsk() — Reload existing DSK from storage
  3. auth_alloc_dsk2() — Allocate a new DSK2 (v2 format)

Key Type 2 — Alternative auth path (possibly enterprise/OEM) with a different serial work sequence during auth_start().

Account credentials from the config struct:

OffsetFieldMax Size
0x00key_type4 bytes (int)
0x04key128 bytes
0x84secret128 bytes
0x104device_id128 bytes
0x184device_type128 bytes

The qxwz_signature() function implements HMAC-SHA384 request signing with deterministic canonicalization:

Algorithm:

  1. Format the secret into a key string (max 256 chars)
  2. Format the timestamp into a nonce string (max 64 chars)
  3. Sort key-value pairs alphabetically by key (bubble sort with strcmp) — ensures deterministic signatures regardless of parameter order
  4. Initialize HMAC-SHA384 with the formatted secret as the HMAC key
  5. Feed data in strict order: path string → sorted k-v pairs (key then value) → timestamp nonce
  6. Sign → 48 bytes (384 bits)
  7. Hex-encode to 96-character lowercase hex string

The signature is appended as _sign parameter to API requests.

Client ID (oss_sess_generate_clientid): Random UUID-like identifier with 5 groups of hex characters separated by dashes, generated via qxwz_gen_random_num() % 16.

Password (oss_sess_generate_password): A JSON object containing a signed device credential:

{
"deviceInfo": "<device_id>/<device_type>",
"version": "<sdk_version>/<protocol_version>",
"timestamp": 1234567890,
"sign": "abcdef0123456789...96chars..."
}

The MQTT broker verifies the HMAC signature on every connection, providing per-session authentication tied to the device’s secret key.

FunctionAlgorithmUsage
qxwz_signature()HMAC-SHA384API request signing
qxwz_aes_cbc_encrypt/decrypt()AES-128-CBCDSK file encryption/decryption
qxwz_crc24q()CRC-24QRTCM3 frame validation
qxwz_md5()MD5Legacy hashing
qxwz_base64_encode/decode()Base64HTTP auth encoding

The presence of qxwz_crc24q() confirms RTCM3 data handling — CRC-24Q is the standard RTCM 3.x frame check sequence.

OSS Protocol — MQTT-Based Correction Streaming

Section titled “OSS Protocol — MQTT-Based Correction Streaming”

The “OSS” (Online Service System) is a custom MQTT v3.1 client. The protocol string MQIsdp at address 0x3d058 identifies MQTT v3.1 (v3.1.1 uses "MQTT" instead).

Connection state machine:

stateDiagram-v2
    [*] --> UNCONNECTED
    UNCONNECTED --> TCP_CONNECTING
    TCP_CONNECTING --> TCP_CONNECTED
    TCP_CONNECTED --> CONNECTING: MQTT CONNECT sent
    CONNECTING --> CONNECTED: CONNACK received
    CONNECTED --> DISCONNECTING
    DISCONNECTING --> [*]

Data flow: GGA position is published upstream via qxwz_sdk_upload_gga() (max 256 bytes, requires state 0x03 “running”), and RTCM corrections are received via MQTT PUBLISH on subscribed topics.

SIDS is QXWZ’s proprietary compact correction format using RTCM3 framing with proprietary payload encoding.

Frame structure:

packet-beta
  0-7: "0xD2 Preamble"
  8-13: "Reserved"
  14-23: "Length (10-bit)"
  24-55: "Payload (variable)"
  56-79: "CRC-24Q (3 bytes)"
  • Preamble: 0xD2 (same as standard RTCM3)
  • Length: 10-bit field at bit offset 14
  • CRC: 24-bit CRC-24Q, identical to RTCM3

Payload encoding uses bit-level extraction (qxwz_ubit_get()):

Offset (bits)WidthField
2410constellation_id
3416signal_id
5030correction_data
801status flag
817type qualifier
88+variablesatellite/signal masks + data

Constellation ID mapping: 1=GPS, 4=GLONASS, 8=Galileo, 32=BeiDou.

PathDescription
/rest/qxwz.core.reloadDSKReload device service key
/rest/qxwz.core.allocateDSK.2Allocate new DSK v2
/rest/qxwz.core.queryDSKQuery DSK status
/rest/qxwz.core.activateDSK.2Activate DSK v2
/rest/qxwz.core.resumeDSKResume expired DSK
/rest/nosr.dsk.getCoordinateFrameGet coordinate system
/rest/nosr.dsk.setCoordinateFrameSet coordinate system
/rest/nosr.dsk.getConfigGet NOSR configuration

Supports WGS-84 and CGCS2000 coordinate systems.


  • Architecture: x86 32-bit Windows PE (MSVC statically linked)
  • Functions: 14,425
  • Chip target: BCM4775 (Broadcom GNSS SoC in LC29H)
  • Source path: d:\gps\v10_4775bettab1\proprietary\deliverables\lhe2_dev\

The binary self-identifies as “BCM4775 Flash Updater” — revealing that the LC29H contains a Broadcom BCM4775 GNSS SoC (not MediaTek as initially assumed from the bootloader magic bytes).

graph TD
    FU["FlashUpdater\n(application layer)"]
    LH["LhEngine\n(Link Handler engine)"]
    RPC["RPC Engine\n(Remote Procedure Call layer)"]
    TL["Transport Layer\n(packet framing, retransmit)"]
    HAL["HAL\n(UART / SPI / I2C)"]

    FU --> LH --> RPC --> TL --> HAL

Decompilation of firmware_downloader.cpp reveals an 18-state FSM:

StateNameAction
0FD_IDLEIdle / Done
1FD_REQUEST_DOWNLOADSet sync target to ROM, connect to ASIC
2FD_WAIT4AUTOBAUDSend auto-baud byte sequence
3FD_SENT_AUTOBAUDWait for auto-baud response
5FD_GOT_SYNCRoute to ROM (7) or RAM (14) sync target
6FD_TIMEOUT_SYNCRetry or fail
7FD_GOT_SYNC_INITROM SYNC received, send version request
8FD_WAIT4VERSION_INITWaiting for ROM version response
9FD_GOT_VERSION_INITValidate chip ID, decide next phase
10FD_ROMRESETROM version mismatch, reset ASIC
11FD_WAIT4DOWNLOAD_STARTInitialize patch, begin transfer
12FD_DOWNLOADINGTransfer patch data (500ms timeout)
13FD_WAIT4APPWait for app to start
14FD_GOT_SYNC_CONFIRMRAM SYNC received, send version request
15FD_WAIT4VERSION_CONFIRMWaiting for RAM version
16FD_GOT_VERSION_CONFIRMPatch verified, notify completion
17FD_DONEClean up, signal complete

State 9 validates the chip version register against supported BCM GNSS SoC revisions:

Chip IDSoC
0x47730A30BCM4773 rev A30
0x47730A20BCM4773 rev A20
0x004774A0BCM4774 rev A0
0x004775A0BCM4775 rev A0
0x004775A1BCM4775 rev A1
0x004775B0BCM4775 rev B0
0x034775B0BCM4775 rev B0 (variant 3)
0x074775B0BCM4775 rev B0 (variant 7)
0x054775B0BCM4775 rev B0 (variant 5)
0x044775B0BCM4775 rev B0 (variant 4)
0x0D4775B1BCM4775 rev B1 (variant D)
0x044775B1BCM4775 rev B1 (variant 4)
0x004775B1BCM4775 rev B1

This confirms the LC29H module family uses the Broadcom BCM4773/4774/4775 GNSS SoC family. The variant prefix byte likely indicates package or OEM configuration.

stateDiagram-v2
    [*] --> REQUEST_DOWNLOAD: set sync target = ROM

    REQUEST_DOWNLOAD --> AUTOBAUD
    AUTOBAUD --> SENT_AUTOBAUD
    SENT_AUTOBAUD --> GOT_SYNC: sync received
    SENT_AUTOBAUD --> TIMEOUT: timeout
    TIMEOUT --> AUTOBAUD: retry

    GOT_SYNC --> GOT_SYNC_INIT: route to ROM

    GOT_SYNC_INIT --> WAIT4VERSION: send version request
    WAIT4VERSION --> GOT_VERSION: validate chip ID

    GOT_VERSION --> WAIT4DOWNLOAD_START
    WAIT4DOWNLOAD_START --> DOWNLOADING
    DOWNLOADING --> WAIT4APP: patch transfer complete

    WAIT4APP --> GOT_SYNC_CONFIRM: set sync target = RAM
    GOT_SYNC_CONFIRM --> WAIT4VERSION_CONFIRM
    WAIT4VERSION_CONFIRM --> GOT_VERSION_CONFIRM: patch verified

    GOT_VERSION_CONFIRM --> DONE
    DONE --> [*]

After the bootloader patch loads, 6 RPC commands become available:

RPC IDFunction
6Flash Read
9Flash Write
10Flash Erase
35Flash Config Query
36Flash CRC Verify
39Flash Status

Flash operation sequence:

  1. Query flash config (RPC 35) — Get flash size and sector erase time
  2. Backup (optional, RPC 6) — Read entire flash to FlashBackup_YYYYMMDD-HHMMSS.bin
  3. Erase (RPC 10) — Erase sectors for new image (4KB sector-aligned)
  4. Write (RPC 9) — Write new image to flash
  5. Verify (RPC 36) — Read back and CRC compare
FlashUpdater4775 [options] flash_bin_file
Options:
eraseTime=<ms> # 4KB sector erase time
flashOffset=<byte_offset> # Write offset (hex with 0x prefix)
skipBackup=true|false # Skip flash backup
patchFile=<patch_file> # Download patch first
retrieveFile=<local_file> # Read flash to local file
retrieveLength=<num_bytes> # Flash read length
HAL Configuration:
HALserial=uart # or i2c or spi
HALcom=COM20 # Serial port
HALbaud=115200 # Baud rate
HALgpio=COM3 # GPIO port
ChipInterfaceHAL Classes
FTDIUART + GPIO + SPIFtdiGpio, FtSpiHal, FtSpiDev
CP210x (Silicon Labs)USB-UARTCP21x library
CP213x (Silicon Labs)USB-SPIUsbSpiCp2130

  • Magic bytes: f3 f2 f1 f0 (Broadcom firmware container header)
  • Size: 20,092 bytes
  • Target: BCM4775 GNSS SoC
Offset Bytes Interpretation
0x00 f3 f2 f1 f0 Broadcom package magic (descending sequence)
0x04 ac dd c1 11 Package identifier / version
0x08 21 11 17 20 Build timestamp or version
0x0c 5c 4e 00 00 Payload size (0x4E5C = 20,060 bytes ≈ file - header)
0x20 48 00 00 18 Code entry / branch table
0x24 48 00 00 8e Code entry / branch table

The value at offset 0x0C (0x4E5C = 20,060) equals the file size minus the header (20,092 - 32 = 20,060), confirming it encodes the payload length.

The bootloader is downloaded to BCM4775 RAM via the FlashUpdater’s patch mechanism (FSM states 11-12). It serves as a “Scratch APP” bootstrap that:

  1. Gets loaded to RAM at the specified PatchAddress
  2. Starts executing (jump from ROM to RAM code)
  3. Registers the 6 RPC flash command handlers
  4. Enables flash read/write/erase operations via the RPC protocol

Without this bootloader, the host cannot access the SPI flash through the GNSS chip — the ROM bootloader only supports download, not flash operations.


  • Architecture: x86-64, Windows PE
  • Version: QGNSSLog_V1.2.4
  • Framework: Qt5 (QCustomPlot for visualization)

QGNSSLog is a standalone NMEA log file viewer and signal analyzer. It opens recorded .log, .txt, or .nma files and plots satellite signal quality metrics (SNR/C/N0, elevation, azimuth) over time.

Unlike QGNSS.exe (which commands live hardware), QGNSSLog performs no serial I/O — it is purely a post-processing visualization tool.

QGNSSLog imports 7 functions from QNMEA.dll, revealing a log analysis API not discovered in round 1:

ImportPurpose
QNMEA_NEWCreate parser instance
QNMEA_DELETEDestroy parser instance
Read_logParse an entire log file
set_qlog_data_cbRegister per-sentence data callback
set_qlog_status_cbRegister progress/status callback
Set_Read_PKGSet read buffer size (1 MB)
Set_Qlog_cfgPass filter configuration

Data pipeline:

sequenceDiagram
    participant App as QGNSSLog.exe
    participant Lib as QNMEA.dll

    App->>Lib: QNMEA_NEW()
    Note right of Lib: Create parser
    App->>Lib: set_qlog_data_cb(fn, ctx)
    App->>Lib: set_qlog_status_cb(fn)
    App->>Lib: Set_Read_PKG(0x100000)
    Note right of Lib: 1 MB buffer
    App->>Lib: Set_Qlog_cfg(&cfg)

    App->>Lib: Read_log(filepath)
    Lib-->>App: data_cb(sentence, ctx)
    Note left of App: Per sentence
    Lib-->>App: status_cb(progress)
    Note left of App: Periodic

    App->>Lib: QNMEA_DELETE()
    Note right of Lib: Cleanup

The QLOG_CFG struct (loaded from FilterOptions.ini):

struct QLOG_CFG {
uint8_t elevation; // min satellite elevation (degrees)
uint8_t snr_cn0; // min SNR/C/N0 threshold (dB-Hz)
uint8_t reserved[2]; // padding
uint16_t azimuth; // azimuth filter (degrees)
};

QGNSSLog contains the most comprehensive signal reference found in any Quectel binary: 52 signals across 7 constellations plus SBAS augmentation.

SignalBandDescription
L1 C/A1575.42 MHzCivilian standard
L1 P(Y)1575.42 MHzMilitary precision (encrypted)
L1M1575.42 MHzMilitary M-code
L2 P(Y)1227.60 MHzPrecision on L2
L2C-M1227.60 MHzCivilian medium-length code
L2C-L1227.60 MHzCivilian long code
L5-I1176.45 MHzIn-phase component
L5-Q1176.45 MHzQuadrature component
SignalBandDescription
G1 C/A~1602 MHzStandard accuracy
G1P~1602 MHzHigh accuracy
G2 C/A~1246 MHzStandard accuracy
G2P~1246 MHzHigh accuracy
SignalBandDescription
E5a1176.45 MHzOpen service (= L5)
E5b1207.14 MHzOpen service
E5a+b1191.795 MHzAltBOC wideband
E6A1278.75 MHzPublic Regulated Service
E6BC1278.75 MHzCommercial Service
L1A1575.42 MHzPublic Regulated Service
L1BC1575.42 MHzOpen service (= L1)
SignalBandDescription
B1I1561.098 MHzBDS-2 open service
B1Q1561.098 MHzBDS-2 authorized
B1C1575.42 MHzBDS-3 open (= L1/E1)
B1A1575.42 MHzBDS-3 authorized
B2a1176.45 MHzBDS-3 open (= L5/E5a)
B2b1207.14 MHzBDS-3 open (= E5b)
B2a+b1191.795 MHzBDS-3 wideband (= E5a+b)
B3I1268.52 MHzBDS-2/3 open
B3Q1268.52 MHzBDS-2 authorized
B3A1268.52 MHzBDS-3 authorized
B2I1207.14 MHzBDS-2 open
B2Q1207.14 MHzBDS-2 authorized
SignalBandDescription
L1 C/A1575.42 MHzGPS-compatible
L1CD / L1CP1575.42 MHzL1C data/pilot channels
L1S1575.42 MHzSub-meter augmentation
L2C-M / L2C-L1227.60 MHzGPS-compatible L2C
L5-I / L5-Q1176.45 MHzL5 in-phase/quadrature
L6D1278.75 MHzCLAS data
L6E1278.75 MHzMADOCA
SignalBandDescription
L5 SPS1176.45 MHzStandard positioning
S-SPS2492.028 MHzS-band standard
L5 RS1176.45 MHzRestricted service
S-RS2492.028 MHzS-band restricted
L1 SPS1575.42 MHzL1 standard positioning
ServiceCoverage
WAASNorth America
SDCMRussia
EGNOSEurope
BDSBASChina / Asia-Pacific
MSASJapan
GAGANIndia

sequenceDiagram
    participant Host as Host (Linux/Windows)
    participant SoC as BCM4775 GNSS SoC

    rect rgba(59, 130, 246, 0.1)
    Note over Host,SoC: ROM Bootloader Phase
    Host->>SoC: Auto-baud detection
    SoC-->>Host: Sync response
    end

    rect rgba(34, 197, 94, 0.1)
    Note over Host,SoC: Patch Download Phase
    Host->>SoC: Download bootloader.pkg (to RAM)
    SoC-->>Host: Patch verified
    Host->>SoC: Start bootloader (jump to RAM)
    SoC-->>Host: RPC available
    end

    rect rgba(234, 179, 8, 0.1)
    Note over Host,SoC: Flash Operations (RPC)
    Host->>SoC: Query flash config
    SoC-->>Host: Flash size, erase time
    Host->>SoC: Read flash (backup)
    SoC-->>Host: Flash data
    Host->>SoC: Erase sectors
    Host->>SoC: Write flash (new image)
    Host->>SoC: CRC verify
    SoC-->>Host: Match
    end
FeatureStandard NTRIPQXWZ Service
TransportHTTP/1.0 TCPMQTT v3.1
AuthBasic HTTP authHMAC-SHA384 + DSK
CorrectionsRTCM3 streamRTCM3 + proprietary SIDS
Position uploadGGA via HTTP bodyGGA via MQTT PUBLISH
APISource table onlyFull REST API
EncryptionNoneAES-128-CBC for credentials
Coordinate framesWGS-84 onlyWGS-84 + CGCS2000

The FlashUpdater protocol is now documented in sufficient detail to implement a Linux-native firmware update tool, eliminating the Windows dependency for firmware updates on the Waveshare LC29H HAT. The key components are:

  1. Transport: UART auto-baud → ROM SYNC handshake → version request/validate against BCM477x chip ID table
  2. Patch download: Stream bootloader.pkg to RAM, verify, start Scratch APP (0x41)
  3. RAM SYNC: Second handshake after bootloader starts
  4. RPC flash operations: Query (35), Read (6), Erase (10), Write (9), CRC verify (36), Status (39)
  5. Verification: Read-back + CRC comparison

The transport layer uses sequence numbers, ACKs, and automatic retry (tracked via RxPacketLost, TxPacketRetry, MaxRetry counters).