Menu
Home Videos BlogTech ProjectsAiHardwareSoftware Shop Digital Designs Physical Art Layered Maps Chinese Paper Cuttings About Contact Cart (0)

With the stock Bluetooth application confirmed to be scan/advertise/connect-locked (see the previous chapter), the next step was building a custom Bluetooth LE Network Co-Processor image and proving it could both run reliably on this specific board and be genuinely useful — in this case, as a passive reader for Govee H5075 BLE thermo-hygrometers.

Prior startup failures and the HFXO erratum

Earlier generated radio projects (Zigbee and Thread images built before this BLE work) had appeared to hang during clock initialization. SWD tracing showed execution stuck around Silicon Labs’ HFXO initialization path, and exposed the EFR32MG21 HFXO_E301 erratum: changing HFXO.DISONDEMAND from 0 to 1 while HFXO is enabled can hang the EMU/CMU handshake, requiring a physical pin reset to recover. This affects silicon revisions A through D with no documented silicon-level fix.

The stock SDK’s CMU_HFXOInit() crystal-mode sequence does exactly this forbidden transition. The durable workaround, tools/patch-sdk-hfxo-errata.py, removes the DISONDEMAND toggle and everything depending on the resulting FSM-override state. Earlier experiments that instead tried to relax HFXO wait conditions more broadly were reverted after skipping a FSMLOCK wait produced a real fault on a subsequent register write — proving that FSM state was meaningful and that bypassing synchronization more broadly was unsafe. The narrow HFXO_E301 patch was kept; the broad wait-loop bypass was not.

Earlier Zigbee/Thread projects also carried trustzone_unaware components and application constructors that rewrote SMU->PPUSATD* access masks or disabled SMU_SECURE_IRQn, because peripheral access to CMU, HFXO0, USART0, and radio-adjacent blocks could trap under some generated configurations. The final BLE build deliberately omits all of that: its app.c is the ordinary NCP application skeleton with no custom SMU constructor, and this is the configuration that produced clean, verified BGAPI responses.

UART pin mapping settled by the working build

Earlier phases of this broader investigation had recorded conflicting PA5/PA6 interpretations, partly because host TX/RX and MG21 TX/RX labels were being discussed from opposite viewpoints, and partly because some intermediate firmware genuinely used the wrong mapping. For the final BLE NCP, the generated configuration and live, bidirectional BGAPI traffic settle it unambiguously:

MG21 USART0 TX = PA5
MG21 USART0 RX = PA6

tools/build-ble-ncp.sh writes these exact pins into sl_iostream_usart_vcom_config.h and disables hardware flow control (the W1700K connection exposes only TX/RX, no CTS/RTS).

Baud-rate diagnosis

The MG21 firmware runs at a nominal 115200 baud. The W1700K host, however, needed to request 230400 to get clean framing, due to this snapshot kernel’s AN7581 UART driver behaving as if every requested rate needs to be doubled. Requesting 460800 (double the intended 230400, i.e. quadruple the firmware’s real 115200) produced framing errors and garbage. The final, verified working combination:

MG21 firmware setting: 115200
W1700K stty/request:   230400
format:                8N1, raw, no CTS/RTS, no XON/XOFF

A BOOT-pin test that had not fully released the pin also produced misleading bytes during diagnosis and was ruled out as a red herring before the baud-rate explanation was confirmed.

Flashing without WSTK

The final BLE application/S37 was first flashed over SWD at application origin 0x4000, with no mass erase and the Gecko bootloader preserved. The identical GBL was then separately tested through the W1700K’s own UART DFU path (the tooling built in the reverse-engineering chapter): with the H5075 reader stopped, GPIO537/536 entered the factory Gecko bootloader (a0 04 00 00 02 00 01 02), and with MG21_BAUD=230400, the router transferred all 254,868 bytes as 1,004 acknowledged chunks. DFU_FLASH_UPLOAD_FINISH returned 20 02 00 03 00 00, and reset produced the full BLE application boot event:

a0 12 01 00 0b 00 01 00 00 00 00 00 02 00 01 02 01 01 35 44 cd 01

This proves the BLE image can be installed and used entirely through the W1700K, with no SWD required for routine reflashing once the case is closed — the flashing uploader’s own comparison logic printed a spurious MISMATCH only because it compared this current boot payload against a hard-coded older stock-firmware payload; the actual upload and boot succeeded.

BGAPI command/response verification

Three commands proved the NCP was parsing requests correctly, not just emitting stray startup bytes:

system_hello:
  request:  20 00 01 00
  response: 20 02 01 00 00 00
 
system_get_version:
  request:  20 00 01 1b
  response: 20 12 01 1b 00 00 0b 00 01 00 00 00 00 00 02 00 01 02 35 44 cd 01
 
system_get_identity_address:
  response: 20 09 01 15 00 00 f1 88 0c 89 c0 1c 00

After starting active observation scanning, the NCP returned tens of kilobytes of valid legacy advertising-report events from multiple nearby devices (including a Govee_H6168_3D5E), confirming the scanner was not narrowly tailored to any one sensor.

The Govee H5075 and a generic reader

Among the scanned devices was a powered-on Govee H5075 temperature/humidity sensor:

FieldObserved value
Local nameGVH5075_52B3
BLE addressA4:C1:38:37:52:B3
Manufacturer/company ID0xEC88

The H5075 broadcasts its reading in manufacturer-specific advertising data, requiring no BLE connection. After the little-endian company ID, the body has the form 00 TT TT TT BB 00, where TT TT TT is a 24-bit big-endian packed value and BB is battery percentage:

packed      = big_endian(TT TT TT)
temperature = floor(packed / 1000) / 10
humidity    = (packed % 1000) / 10
battery     = BB

A captured test vector, 04 28 43, decodes to 27.2 °C and 45.1% RH, with 64 giving 100% battery.

A generic host utility, host/h5075-reader/main.go, was built to consume this: it configures /dev/ttyS1 for 230400/8N1/raw with no flow control, sets active scanning (100 ms interval/window) and 1M-PHY observation mode, resynchronizes and parses self-delimiting BGAPI frames, and decodes any advertiser whose name begins with GVH5075 and whose company ID decodes to 0xEC88 — dynamically, without hard-coding a MAC address, suffix, or serial number. It supports -json for machine-readable output and -once for a single-reading smoke test, and attempts a clean scan-stop on exit or SIGINT/SIGTERM. It was built reproducibly with CGO_ENABLED=0 GOOS=linux GOARCH=arm64.

Deployed as /usr/local/bin/h5075-reader on the router and run under start-stop-daemon (the OpenWrt image lacks nohup), a live log entry read:

2026-09-12T23:13:01Z  GVH5075_52B3  A4:C1:38:37:52:B3  27.3 C  44.3 %RH  100 % battery  RSSI -26 dBm

Because only one process may own /dev/ttyS1 at a time (BLE, Zigbee, and Thread all share the same MG21 UART), the reader must be stopped before running any other BGAPI client, serial bridge, or Zigbee/Thread process:

start-stop-daemon -K -p /tmp/h5075-reader.pid

Verification and open items

Confirmed: SWD and UART DFU flashing both work without mass erase; the TX=PA5/RX=PA6, no-flow-control USART0 mapping is bidirectional on this board; system_hello/get_version/get_identity_address all return complete responses; active scanning returns valid events from multiple devices; the H5075 decoder produces stable, plausible readings; and the reader’s unit tests pass.

Not yet verified: behavior with two or more H5075 sensors present simultaneously (the decoder is identity-independent by design but this was never tested with multiple units); reboot persistence (the current process is a test invocation under start-stop-daemon, not an OpenWrt init/procd service); long-duration UART/BGAPI stability across MG21 or host resets; and any BLE-to-MQTT/Home Assistant integration, which was deliberately deferred.