Walkthrough: Nordic nRF9151-DK over LTE / Cellular
Who this is for: Developers running the cellular variant of the SDK, where a single Nordic nRF9151-DK board authenticates over LTE directly to a hosted TrustBridge stack, with no gateway, laptop, or local Docker in the middle.
What you'll accomplish: Wire the crypto chip, build and flash the board, and run mutual authentication (and firmware verification and installation) over LTE against the hosted cloud.
This variant lives on its own branch:
release/v3.6.2(the cellular / LTE line of the SandGrain-TrustBridgeSDK repo). Don't mix it with the USB-Token branch (release/v3.6.0) or the Bluetooth LE walkthrough.
How this topology works
One nRF9151-DK does the whole job. The gateway-mode SDK runs on the board with an SGT1001 crypto chip wired to its SPI header, and the board talks MQTT over LTE straight to the hosted TrustBridge stack. There is no Raspberry Pi, no laptop, no local Docker, and no relay in the middle.
nRF9151-DK Hosted TrustBridge (AWS)
┌───────────────────────────┐ ┌────────────────────────┐
│ gateway-mode SDK │ LTE-M/NB-IoT │ EMQX broker :1883 │
│ SDK_MODE=GATEWAY_MODE │◀────────────▶ │ TrustBridge BE :3000 │
│ GW_CRYPTO_ENABLED=1 │ MQTT (TCP) │ + frontend │
│ ┌─────────────────────┐ │ │ + sandgrain cloud app │
│ │ SGT1001 over SPI │ │ │ + CyberRock │
│ │ SCK/MISO/MOSI/CS │ │ └────────────────────────┘
│ └─────────────────────┘ │
│ UART menu @ 115200 ──────┼──▶ PuTTY / miniterm
└───────────────────────────┘
This is a deliberate development shortcut for the cellular phase. Instead of a device and a gateway on opposite ends of a link, one board plays both roles: it holds its own crypto chip and authenticates its own token against the cloud (the SDK's Token-to-Server / Server-to-Token path). Production keeps device and gateway separate.
Every connection the board makes is outbound, so carrier-grade NAT on the mobile network never gets in the way.
Authentication can be started two ways, both worth showing:
- from the board's UART menu, or
- from the TrustBridge frontend, with no keystroke on the board. Unlike a Bluetooth LE device, this board is a full MQTT client: it publishes its own retained presence and subscribes to its own trigger topic. Nothing is proxied. See How TrustBridge drives the board below.
What you need
- nRF9151-DK with the LTE antenna fitted.
- An activated SIM with data, in LTE-M or NB-IoT coverage. The board attaches through Zephyr's connection manager with the carrier defaults, so there's nothing to configure in the app, but the first attach on a cold SIM can genuinely take several minutes.
- An SGT1001 crypto chip and six jumper wires (see Step 1).
- nRF Connect SDK v3.3.0, the SEGGER J-Link drivers, and nRF Util (see Step 2). Only the machine that builds needs the SDK; a machine that just flashes a prebuilt hex needs nRF Util and the J-Link drivers only.
- A serial terminal that sends keystrokes (PuTTY, or the miniterm the flash script opens with
-Monitor). A passive log viewer shows the menu but can't drive it.
No Docker, no local broker, no firewall rules. The board reaches the hosted stack directly.
Step 1 - Wire the SGT1001
The chip sits on &spi3 (the Arduino SPI header), chip select CS[0]. The on-board flash is on CS[1] and stays disabled, so there's no conflict.
| SGT1001 | nRF9151-DK | Pin |
|---|---|---|
| SCK | D13 | P0.13 |
| MISO | D12 | P0.12 |
| MOSI | D11 | P0.11 |
| CSN | D10 | P0.10 |
| VCC | 3V3 | - |
| GND | GND | - |
The bus runs at 1 MHz with 1 microsecond CS setup and hold. If you lengthen the jumpers and the bus starts misbehaving, that clock is the first thing to lower, in boards/nrf9151dk_nrf9151_ns.overlay.
The chip is read over SPI before any network exists, so a wiring fault shows up immediately at boot rather than after the LTE attach:
[INIT] FAILED - crypto chip not responding.
Check the wiring:
CLK->D13
MISO->D12
MOSI->D11
CSN->D10
VCC->3V3
GND->GND
Step 2 - Install the Nordic toolchain (one-time per PC)
Only needed on the machine that will build. A machine that only flashes a prebuilt hex needs nRF Util and the J-Link drivers, nothing else, so skip to Step 3.
The build uses west (Zephyr's build tool) driving the nRF Connect SDK, compiled by the Zephyr SDK. Install all of it bundled through Nordic's Toolchain Manager rather than piece by piece. Either route works:
- nRF Connect for VS Code: install the nRF Connect for VS Code Extension Pack, then in the nRF Connect side panel choose Install Toolchain, then Manage SDKs, and install SDK v3.3.0.
- nRF Connect for Desktop: install nRF Connect for Desktop, open the Toolchain Manager app, and install SDK v3.3.0.
The bundle includes west, the Zephyr SDK, GCC, CMake, Ninja, Python, and nrfutil. Nothing needs to be installed separately or put on PATH, and flash_cellular.ps1 resolves all of it from toolchains.json at run time. Budget several GB of disk.
Use v3.3.0. That's what this app is built and verified against, and what the flash script defaults to. Another version can be forced with
-NcsVersion, but the/nsbuild target pulls in TF-M and the modem library, so a version mismatch tends to surface as a confusing build error rather than a clean "wrong version" message.
J-Link drivers. Install the SEGGER J-Link / USB drivers. They come with nRF Connect for Desktop, or separately from SEGGER. They're what lets you program the board and what exposes its serial (COM) port.
nRF Util. The flash script programs with nRF Util, specifically its device command (nrfutil install device). If nrfutil is on PATH but without that command, the script installs it for you on first run. If it's missing entirely, install nRF Util from Nordic.
Linux and macOS. Use flash_cellular.sh, the twin of the PowerShell script. Same discovery logic, same defaults, dashed flags:
chmod +x flash_cellular.sh
./flash_cellular.sh –monitor
One-time extras on Linux: the SEGGER J-Link package, the nrf-udev rules, and your user in the dialout group. Serial ports are /dev/ttyACM* on Linux and /dev/tty.usbmodem* on macOS instead of COMx.
Modem firmware. The nRF9151-DK ships with modem firmware already on it, and this app doesn't require a specific version. If LTE never attaches and the SIM, antenna, and coverage all check out, an outdated modem image is the next thing to rule out: update it with the Programmer app in nRF Connect for Desktop, or with nrfutil device program pointing at the modem firmware zip from the nRF9151-DK product page. Modem firmware is a separate image from the application; flashing this app never touches it.
Step 3 - Build and flash
flash_cellular.ps1 finds the NCS root itself and resolves the toolchain hash out of toolchains.json, so it works on any machine with NCS installed.
On a machine with the toolchain:
cd SandGrain-SDK\PoC_SDK_v3\poc_sdk_cellular_nrf9151
.\flash_cellular.ps1 -Monitor
That builds pristine for nrf9151dk/nrf9151/ns, flashes with chip_erase_mode=ERASE_ALL, resets, and opens a serial monitor on the auto-detected J-Link CDC port at 115200.
On a machine with only the board attached (no toolchain): build elsewhere with -NoFlash -Zip, copy out\poc_sdk_cellular_nrf9151.zip across, extract, then:
.\flash_cellular.ps1 -PrebuiltHex .\merged.hex
That skips the toolchain and the build entirely; it only needs nrfutil. This is the right mode when the DK lives on a remote test PC.
Flags worth knowing:
| Flag | Use |
|---|---|
-Monitor |
open miniterm on the detected COM port after flashing |
-Port COMx |
force the serial port if auto-detection picks wrong |
-Snr <serial> |
pick the J-Link when more than one board is attached |
-PrebuiltHex <hex> |
flash only, no toolchain needed |
-NoFlash -Zip |
build and package for another machine |
-NcsRoot / -NcsVersion |
override NCS discovery |
If flashing fails: power switch on, USB in the DEBUG port nearest the power switch, J-Link visible in Device Manager, and nRF Connect Programmer closed (it holds the J-Link open). The app path must not contain a space, or Zephyr's CMake fails with a confusing "File not found" during configure.
Step 4 - Serial terminal
115200 8N1 on the J-Link CDC port. A healthy boot looks like this:
*** Booting nRF Connect SDK v3.3.0-... ***
[AUTH_DB] Initialized (100 slots)
[MW] SDK ready
SandGrain DEVICE+GATEWAY - mutual auth over LTE
board: nrf9151dk/nrf9151/ns
broker: <BROKER_IP>:1883
[INIT] Initializing gateway SDK (reads TID over SPI)...
[INIT] SDK ready (chip variant: SGB)
[BIST] status byte: 0x50 (PASS)
TID: 800000000000000000000000000000248d2ebe2d4a13c446b016706a701c50a6
[LTE] Bringing the modem network interface up...
[LTE] Attaching to the network (can take a few minutes)...
[LTE] Connected - IP address assigned
[MQTT] Connecting to <BROKER_IP>:1883 ...
[MQTT] Connected to broker - credentials OK
[MQTT] Presence: online (retained)
[MQTT] cmd/run_auth subscription active
The boot order is deliberate. The chip is read first, before the modem comes up, because SPI needs no network and the TID must be known before the MQTT CONNECT packet is built. The retained Last Will (offline on this chip's status topic) rides inside that CONNECT, so a TID that arrives late can't be installed without dropping and reconnecting. If the chip is unreadable at boot, the board connects anyway with a fallback client ID and no will, and prints
[WARN] TID unknown. Fix the wiring and press0: that re-reads the chip and forces the drop-and-reconnect so the identity is installed properly.
Step 5 - Enroll the TID
Press 0 on the menu. It prints the 64-hex-digit TID starting 8000.... Enroll that TID in the TrustBridge frontend before running anything else, or every authentication is rejected at the cloud with a message that looks like a device fault.
Step 6 - The menu
Select an option:
0 BIST - read + print TID
1 Mutual Authentication with TrustBridge (over LTE)
2 FW Verification - correct RW (expect FW_OK, arms option 4)
3 FW Verification - wrong RW (expect FW_NOK)
4 Installation - request install auth (run option 2 first)
Menu keys are refused while a run is in flight ([!] Auth run in progress), so you can't stack two flows on top of each other.
0 - BIST. Reads the chip over SPI, prints the status byte and the TID, and refreshes the retained presence. Works with no network. Doubles as the recovery path for a chip that wasn't readable at boot.
1 - Mutual authentication. Both phases against the cloud. Phase 1 (Token to Server): the board asks CyberRock for a Challenge Word, computes the Response Word on the chip, sends it back, and the cloud returns a verdict. Phase 2 (Server to Token): the board derives a challenge from the previous RW, asks the cloud for its Response Word, recomputes it on the chip, and compares. Phase 2 is the half that proves the cloud is genuine, not just the chip. Success ends with >> MUTUAL AUTHENTICATION OK.
2 and 3 - FW verification. The board SHA-256s a 64-byte simulated firmware image, asks the chip for the host-auth RW over that hash, and compares against the RW a signer would have embedded. Option 2 passes the correct RW: expect FW_OK, a cloud ACK_OK, and option 4 armed. Option 3 passes an all-zero expected RW: expect a local mismatch and FW_NOK. The RW printed is the chip's real one in both cases, because what differs is the expected value, not what the chip computed, so seeing the same RW in options 2 and 3 is correct.
6 - Installation. Asks the chip for the install RW and requests installation authorization from the cloud. Expect CR verdict: INSTALL_OK. Run option immediately before option 2, with no 3 in between. The cloud keeps the FW verification record for 300 seconds, and option 3 deliberately records a failed verification. Get either wrong and installation is denied with missing_or_expired_fw_verification, which reads like a board fault and isn't.
How TrustBridge drives the board
Unlike a Bluetooth LE device, this board needs no proxy; it's its own MQTT client. At boot it connects (with a retained "offline" Last Will on its status topic), publishes a retained online presence, and subscribes to its own cmd/run_auth topic. The frontend can then trigger authentication by publishing {"mode":N}, and the board authenticates exactly as if its menu key had been pressed.
Points that matter in practice:
modepicks the same option as the menu: 1 normal. Anything unrecognised falls back to a normal mutual auth.- Presence is a real Last Will, not a heartbeat. The offline message is retained and installed at CONNECT time, so a backend that restarts while the board is powered down still sees offline rather than a ghost online.
- The client ID carries the TID (
sgr-plus the first 12 hex characters), so several boards can be connected at once without knocking each other off. - Keepalive is 30 s, so the broker declares a dead client at about 45 s.
- Reconnect is automatic after an LTE or TCP drop (backoff from 5 s doubling to a 60 s cap), re-subscribing and republishing presence on reconnect. A reconnect that lands mid-authentication fails that one run with a communication error; just trigger it again.
First-time chips claim themselves. A chip that has never been claimed authenticates correctly, but the cloud answers CLAIM_TOKEN instead of AUTH_OK. TrustBridge then claims the token, and a second authentication a moment later succeeds. The board drives that second pass itself (up to four passes, three seconds apart). Nothing to do but watch.
MQTT topics
All under sandgrain/v1/<tid>/, with <tid> the 64-hex-digit TID.
| Topic | Direction | Purpose |
|---|---|---|
req/requestCW |
board to cloud | ask CyberRock for a Challenge Word |
req/replyRW |
board to cloud | return the CW echo and Response Word |
req/requestRW |
board to cloud | Phase 2: ask the cloud for its RW |
req/checkAuthStatusCloud |
board to cloud | poll the cloud's auth state |
req/fwVerification |
board to cloud | bridge an FW_OK / FW_NOK verdict |
req/fwInstallation |
board to cloud | request installation authorization |
rsp/# |
cloud to board | every reply; one subscription covers all |
status |
board to broker | retained online / offline (Last Will) |
cmd/run_auth |
cloud to board | {"mode":N} authentication trigger |
gw_log |
board to cloud | per-step telemetry |
auth_result |
board to cloud | final verdict |
Frames are single-line JSON. The frame's type field selects the request topic, using the same type-to-topic table as the PC gateway port.
Broker
Editor note (confirm before publishing): the values below are the shared hosted development broker, including a username and password. Confirm these are approved for a customer-facing article, or replace them with placeholders / gate this article to internal readers, before publishing.
The demo broker is the hosted EMQX instance on plain TCP, user sandgrain. This is the Elastic IP of the EC2 origin, not the public hostname: trustbridge.sandgrain.dev resolves to Cloudflare, which only proxies web traffic, so MQTT has to reach the origin directly. A different environment's address will not work with these credentials. The value is defined in src/ports/app_mqtt.h.
Troubleshooting
| Symptom | Fix |
|---|---|
[INIT] FAILED - crypto chip not responding |
Wiring (Step 1). MOSI/MISO swapped and a missing GND are the usual two. Long jumpers can corrupt the bus; lower spi-max-frequency in the overlay. |
[BIST] prints but the status byte is not 0x50 |
Chip is answering but unhappy. Check 3V3 and GND before suspecting the chip. |
| LTE never gets past "Attaching to the network" | SIM not activated, no LTE-M/NB-IoT coverage, or the antenna isn't fitted. First attach on a cold SIM can take several minutes. |
[MQTT] Connecting... then nothing |
The broker IP is unreachable from the mobile network. Confirm you're on the correct broker address. |
| Broker connects but every auth is rejected | The TID isn't enrolled in TrustBridge. Press 0, copy the TID, enroll it. |
| Board shows Offline in TrustBridge although it's running | Its TID was unknown at CONNECT time, so no will and no presence were installed. Press 0 to force the reconnect that installs them. |
| Board shows Online after it was powered off | Only if the will was never installed (as above). With a TID the will is retained, so the broker publishes offline on its own. |
Installation (4) denied with missing_or_expired_fw_verification |
Run 2 immediately before 4, within 3 minutes, with no 3 in between. |
| Menu keys do nothing | The COM port is open in a passive log viewer. Use PuTTY or -Monitor. |
| Cannot flash, COM port busy | Close the serial monitor first. Also close nRF Connect Programmer, which holds the J-Link. |
| "Could not find the NCS root" from the flash script | NCS is installed somewhere non-standard. Pass -NcsRoot <path> (the folder containing toolchains and v3.3.0), or set $env:NCS_ROOT. |
| "No toolchain registered for v3.3.0" | The Toolchain Manager has a different SDK version installed. Install v3.3.0, or pass -NcsVersion and expect build errors from the /ns target. |
west build fails with "File not found" |
The app or build path contains a space. Zephyr's CMake can't handle it. Move the checkout to a space-free path. |
| west crashes at import with a ctypes error | A machine-wide PYTHONPATH / PYTHONHOME is leaking into the bundled interpreter. The flash script clears these; if calling west by hand, clear them yourself. |
| nrfutil sees no devices | Power switch on, USB in the DEBUG port nearest the switch, J-Link in Device Manager. With more than one board attached, pass -Snr. |
| Behaviour doesn't match this doc after a pull | Stale image. The build is pristine every time, so the flash didn't take: re-run and watch for the "Flashed + reset" line. |