Bootloader-equipped instruments store runtime firmware in flash memory slots managed by an on-device service bootloader. The SDK provides a field-update workflow for loading images, managing slots and booting runtime firmware. Original Zynq without a bootloader uses direct runtime attachment and does not acquire these service features simply because it uses the same API.
UTT810 units without a bootloader. Early UTT810 units were delivered without the bootloader. They keep working with this SDK and connect directly to their runtime. To receive firmware updates, such a unit needs a one-time bootloader installation by Nexatom service; contact Nexatom to arrange it. From then on, updates are loaded through the SDK or the app like on any other unit.
Ordinary measurement does not require this workflow: native connect_runtime can boot an already-valid image when needed. Use field update when you intend to change firmware. The SDK supplies the service tools and bundles the current firmware catalogue; catalogues are also published on their own (see Firmware catalogues).
Nexatom publishes firmware as numbered catalogues. Each catalogue is a GitHub release named firmware-catalog-N in the nexatom-downloads repository. A release holds the images, their firmware_manifest.json, a SHA256SUMS.txt file and a zip bundle of all of them. The pointer https://downloads.nexatom.in/firmware/latest.json names the current catalogue; the Nexatom app follows that pointer, reads the manifest and offers the images that match the connected instrument.
The SDK package carries the current catalogue in its firmware/ folder (firmware_manifest.json plus the images it lists; catalogue 3 in this SDK: Z080_004.bin and K168_004.bin). A catalogue published after the SDK can carry newer images; the app finds them through latest.json, and an SDK user can download the firmware-catalog-N release directly.
The current catalogue is catalogue 3:
| Image | Instrument | Notes |
|---|---|---|
Z080_004.bin |
UTT810 (Zynq, 8 inputs) | Current UTT810 runtime, device protocol ABI 1.1 |
K168_004.bin |
UTT160810 (Kintex, 16 inputs, 8 outputs) | Current UTT160810 runtime, device protocol ABI 1.1 |
Image names follow the product: Z080_nnn for the UTT810 and K168_nnn for the UTT160810, where nnn is the build count. A higher number is a later build. A file name always means the same bytes in every catalogue; new bytes get a new number. The earlier BOOT_001.bin name is retired.
Firmware images use a strict filename convention:
<NAME>_<VERSION>.bin
| Component | Format | Constraints | Example |
|---|---|---|---|
<NAME> |
ASCII alphanumeric | Exactly 4 characters, isalnum() |
Z080 |
<VERSION> |
Decimal unsigned integer | 0–4294967295 (uint32) |
004 |
| Extension | .bin |
Required | .bin |
Valid example: Z080_004.bin — name Z080, version 4.
The image loader validates this convention before loading a file. The example’s image_metadata_from_filename() function extracts and validates the name and version from the filename stem by splitting on the last underscore:
name, version = path.stem.rsplit("_", 1)
# name must be exactly 4 ASCII alphanumeric characters
# version must be a decimal unsigned integer
Firmware images that do not match this convention are rejected with ValueError before any flash write is attempted.
Each firmware catalogue carries a firmware_manifest.json with schemaVersion (currently 2), catalogVersion and an images array. The fields below explain that format. The SDK archives carry one catalogue in firmware/firmware_manifest.json beside its images; take an image, its size and its checksum from the same catalogue, whether that is the bundled one or a published firmware-catalog-N release.
| Field | Type | Description |
|---|---|---|
fileName |
string | Image filename in <NAME>_<VERSION>.bin format |
displayName |
string | Human-readable name |
description |
string | Purpose of the image |
model |
string | Target hardware model (UTT810 or UTT160810) |
channel |
string | Firmware channel (runtime) |
status |
string | current for the image to install; superseded for an earlier build kept for reference |
requires |
object | abi (device protocol ABI the image reports) and minSdk (oldest SDK that supports it) |
sizeBytes |
integer | Image size in bytes |
sha256 |
string | SHA-256 hex digest for integrity verification |
Use the supplier’s sha256 field to verify file integrity before loading. Filename validation does not establish hardware compatibility or verify the supplier’s checksum. A catalogue entry identifies the intended target; the filename’s four-character application name is not a replacement for that target information.
For a delivery that includes this catalogue, the following Python check compares actual file bytes with its selected entry. It performs no device operation:
import hashlib
import json
from pathlib import Path
directory = Path("firmware")
catalogue = json.loads((directory / "firmware_manifest.json").read_text())
entry = next(item for item in catalogue["images"] if item["fileName"] == "Z080_004.bin")
image = directory / entry["fileName"] # Replace the illustrative name with your supplied image.
if image.stat().st_size != entry["sizeBytes"]:
raise ValueError("Image size differs from its manifest")
with image.open("rb") as stream:
actual = hashlib.file_digest(stream, "sha256").hexdigest()
if actual.lower() != entry["sha256"].lower():
raise ValueError("Image checksum differs from its manifest")
print("Image bytes match the selected catalogue entry")
hashlib.file_digest requires Python 3.11 or later. On Python 3.10 use hashlib.sha256(image.read_bytes()).hexdigest() for an image small enough to read into memory. Select the supplier-approved image before performing either check.
A field update follows a multi-phase state machine managed by the native library. The nexatom_tt_load_field_update_image() function drives all phases synchronously and reports progress via an optional callback.
flowchart TD
A["IDLE (0)"] --> B["REQUESTING_SERVICE_ENTRY (1)"]
B --> C["WAITING_FOR_BOOTLOADER (2)"]
C --> D["WAITING_FOR_STARTUP_STATUS (3)"]
D --> E["SENDING_LOAD_REQUEST (4)"]
E --> F["SENDING_IMAGE (5)"]
F --> G["PROGRAMMING (6)"]
G --> H["VERIFYING (7)"]
H --> I["SETTING_DEFAULT (8)"]
H --> J["COMPLETED (10)"]
H --> K["BOOTING_SLOT (9)"]
I --> K["BOOTING_SLOT (9)"]
I --> J
K --> J
B --> L["FAILED (11)"]
C --> L
D --> L
E --> L
F --> L
G --> L
H --> L
I --> L
K --> L
nexatom_field_update_phase_t)| Value | Constant | Description |
|---|---|---|
| 0 | NEXATOM_FIELD_UPDATE_PHASE_IDLE |
No operation in progress |
| 1 | NEXATOM_FIELD_UPDATE_PHASE_REQUESTING_SERVICE_ENTRY |
Sending runtime-to-bootloader handoff request |
| 2 | NEXATOM_FIELD_UPDATE_PHASE_WAITING_FOR_BOOTLOADER |
Waiting for device to enter bootloader mode |
| 3 | NEXATOM_FIELD_UPDATE_PHASE_WAITING_FOR_STARTUP_STATUS |
Native service discovery and initial status exchange |
| 4 | NEXATOM_FIELD_UPDATE_PHASE_SENDING_LOAD_REQUEST |
Sending slot load request to bootloader |
| 5 | NEXATOM_FIELD_UPDATE_PHASE_SENDING_IMAGE |
Streaming image bytes to device |
| 6 | NEXATOM_FIELD_UPDATE_PHASE_PROGRAMMING |
Bootloader writing image to flash |
| 7 | NEXATOM_FIELD_UPDATE_PHASE_VERIFYING |
Bootloader verifying written image |
| 8 | NEXATOM_FIELD_UPDATE_PHASE_SETTING_DEFAULT |
Setting the loaded slot as default |
| 9 | NEXATOM_FIELD_UPDATE_PHASE_BOOTING_SLOT |
Issuing boot command for the loaded slot |
| 10 | NEXATOM_FIELD_UPDATE_PHASE_COMPLETED |
Operation completed successfully |
| 11 | NEXATOM_FIELD_UPDATE_PHASE_FAILED |
Operation failed |
The progress callback is invoked synchronously during nexatom_tt_load_field_update_image(). The payload is passed by value:
typedef void (*nexatom_field_update_progress_callback_t)(
nexatom_field_update_progress_t progress,
void* user_data
);
nexatom_field_update_progress_t layout:
| Field | Type | Description |
|---|---|---|
struct_size |
uint32 |
Size of this struct (version guard) |
phase |
uint32 |
Current nexatom_field_update_phase_t value |
percent |
uint32 |
Overall progress percentage (0–100) |
current_operation |
uint32 |
Bootloader operation code |
last_error |
uint32 |
Last error code from bootloader |
boot_reason |
uint32 |
Boot reason reported by bootloader |
bytes_sent |
uint64 |
Image bytes transmitted so far |
total_bytes |
uint64 |
Total image size in bytes |
slot_count |
uint32 |
Number of firmware slots reported |
default_slot |
uint32 |
Index of default slot |
slot_index |
uint32 |
Current target slot index |
slot_image_name_ascii32 |
uint32 |
4-byte ASCII image name (packed) |
slot_image_version |
uint32 |
Image version in target slot |
slot_state |
uint32 |
Current slot state |
slot_is_default |
uint32 |
Whether target slot is default |
field_update_e2e.py)python python/examples/field_update_e2e.py `
--home . `
--image firmware/Z080_004.bin `
--slot 1 `
--i-understand-this-writes-firmware `
--boot-after-load
This command illustrates the syntax; substitute the catalogue image for your instrument (Z080_nnn.bin for a UTT810, K168_nnn.bin for a UTT160810) and a slot reported by your device. The current images are in the SDK’s firmware/ folder, for example firmware/Z080_004.bin. Do not select slot 1 merely because it appears in the example.
| Flag | Default | Description |
|---|---|---|
--image |
(required) | Path to firmware image (.bin) |
--slot |
(required) | Target slot index |
--i-understand-this-writes-firmware |
Off | Required safety gate — script refuses to write without this |
--allow-valid-slot-overwrite |
Off | Allow overwriting a VALID or PENDING slot |
--allow-default-slot-overwrite |
Off | Allow overwriting the default slot |
--set-default-after-load |
Off | Mark loaded slot as default boot slot |
--boot-after-load |
Off | Boot the loaded slot after programming |
--runtime-proof-sec |
10.0 |
Post-boot CPS/telemetry validation duration (s) |
--skip-runtime-proof |
Off | Skip post-boot data validation |
--timeout-ms |
5000 |
Connect timeout (ms) |
--mode-timeout-sec |
20.0 |
Protocol mode transition timeout (s) |
1. Validate image filename convention (NAME_VERSION.bin)
2. Discover device and connect
3. Check protocol mode:
• RUNTIME → request_global_stop_all_modes()
request_field_upgrade_service_entry()
refresh_field_update_status() to complete native service discovery
• BOOTLOADER → proceed directly
• UNKNOWN → wait with timeout, abort if unresolved
4. Refresh slot table: refresh_field_update_status()
5. Validate target slot:
• Reject VALID/PENDING without --allow-valid-slot-overwrite
• Reject default slot without --allow-default-slot-overwrite
6. Load image: load_field_update_image()
• Progress callback prints phase/percent/bytes
7. [Optional] Set as default: set_field_update_default_slot_with_status()
8. [Optional] Boot slot: boot_field_update_slot()
9. [Optional] Additional close/reopen check and CPS+telemetry proof in the service example
nexatom_field_update_request_t)typedef struct nexatom_field_update_request_t {
uint32_t struct_size; /* sizeof(this struct) */
uint32_t slot_index; /* Target firmware slot */
const char* image_path; /* Null-terminated image file path */
uint32_t startup_ack_response_code; /* Deprecated, ignored */
uint8_t set_default_after_load; /* 1 = mark as default after write */
uint8_t boot_after_load; /* 1 = boot slot after write */
uint8_t _padding0[2]; /* Alignment padding */
uint32_t request_flags; /* Bitmask (see below) */
uint8_t reserved[12]; /* Reserved for future use */
} nexatom_field_update_request_t;
def progress(p):
print(f"Phase={p.phase} {p.percent}% {p.bytes_sent}/{p.total_bytes} bytes")
device.load_field_update_image(
image_path="firmware/Z080_004.bin",
slot_index=1,
set_default_after_load=False,
boot_after_load=False,
request_flags=0,
progress_callback=progress,
)
void on_progress(nexatom_field_update_progress_t p, void* ctx) {
(void)ctx;
printf("Phase=%u %u%% %llu/%llu bytes\n",
p.phase, p.percent,
(unsigned long long)p.bytes_sent, (unsigned long long)p.total_bytes);
}
nexatom_field_update_request_t req = {0};
req.struct_size = sizeof(req);
req.slot_index = 1;
req.image_path = "firmware\\Z080_004.bin";
req.set_default_after_load = 0;
req.boot_after_load = 0;
nexatom_error_code_t rc = nexatom_tt_load_field_update_image(
device, &req, on_progress, NULL);
Note.
nexatom_tt_load_field_update_image()is synchronous. It blocks until the entire load/program/verify sequence completes or fails. The progress callback executes on the calling thread. Ensure the calling thread is not a UI or event loop thread.
Check the returned error code (or Python exception) after loading. A progress percentage of 100 is not a substitute for successful completion. The code fragments above assume you have already refreshed service status, selected the correct image and applied the overwrite checks shown in the full script.
The UTT810 bootloader manages a table of firmware flash slots. Each slot can hold one firmware image.
nexatom_field_update_status_t status = { .struct_size = sizeof(status) };
nexatom_field_update_slot_info_t slots[8];
size_t slot_count = 0;
nexatom_error_code_t rc = nexatom_tt_refresh_field_update_status(
device, &status, slots, 8, &slot_count);
status, slots = device.refresh_field_update_status()
for slot in slots:
print(f"Slot {slot.slot_index}: state={slot.slot_state} "
f"version={slot.image_version} default={slot.is_default}")
Note. If the device is in runtime mode, stop acquisition and call
request_field_upgrade_service_entry()first. Then callrefresh_field_update_status()so native code can perform the service exchange and return the slot table. Polling only a cached protocol-mode getter does not initiate that exchange. A status refresh reports slot state; it does not prove every slot has just undergone a complete image-integrity scan.
| Value | Constant | Description |
|---|---|---|
| 0 | NEXATOM_FIELD_UPDATE_SLOT_STATE_EMPTY |
No image programmed |
| 1 | NEXATOM_FIELD_UPDATE_SLOT_STATE_VALID |
Contains a verified, bootable image |
| 2 | NEXATOM_FIELD_UPDATE_SLOT_STATE_PENDING |
Image write in progress or incomplete |
| 3 | NEXATOM_FIELD_UPDATE_SLOT_STATE_CORRUPT |
Image failed verification |
Only a VALID slot is eligible for runtime boot selection. PENDING and CORRUPT slots indicate incomplete or invalid programming state. Native boot still has to establish a usable runtime after the device accepts the boot request.
nexatom_field_update_slot_info_t)| Field | Type | Description |
|---|---|---|
slot_index |
uint32 |
Slot position in the flash table |
image_name_ascii32 |
uint32 |
4-byte ASCII image name (packed, e.g. 0x424F4F54 = BOOT) |
image_version |
uint32 |
Image version number |
slot_state |
uint32 |
One of EMPTY, VALID, PENDING, CORRUPT |
is_default |
uint32 |
Non-zero if this is the default boot slot |
nexatom_field_update_status_t)| Field | Type | Description |
|---|---|---|
struct_size |
uint32 |
Size guard for versioned struct layout |
boot_reason |
uint32 |
Bootloader boot reason code |
slot_count |
uint32 |
Total number of firmware slots |
default_slot |
uint32 |
Index of the current default boot slot |
current_operation |
uint32 |
Active bootloader operation |
last_error |
uint32 |
Last bootloader error code |
nexatom_tt_set_field_update_default_slot(device, slot_index);
Or atomically set default and refresh the slot table:
nexatom_tt_set_field_update_default_slot_with_status(
device, slot_index, &status, slots, 8, &slot_count);
The valid default slot has priority during automatic native runtime entry. Setting it changes persistent boot preference; a one-time boot_field_update_slot() call does not change that preference. Power-on service timing is handled by native discovery and the installed bootloader, so applications must not depend on a fixed boot-decision delay.
nexatom_tt_boot_field_update_slot(device, slot_index);
device.boot_field_update_slot(slot_index)
The native boot operation retains the same handle and waits for runtime readiness. Applications do not need to wait for a USB disappearance, rediscover the board or create a replacement handle. A successful boot acknowledgement alone is insufficient; check the boot call’s result and the resulting native profile before using measurement controls.
The advanced field_update_e2e.py and runtime_bootloader_handoff.py scripts additionally close and reopen a device after boot to exercise that lifecycle and, when requested, verify data. This is an extra diagnostic check in those examples, not the startup sequence required of every SDK client.
The select_runtime_slot() function selects the best slot to boot using this precedence:
| Priority | Rule |
|---|---|
| 1 | preferred_slot if specified and VALID |
| 2 | Default slot if VALID |
| 3 | Lowest-index VALID slot |
| — | RuntimeBootError if no VALID slot exists |
from nexatomtt import select_runtime_slot
slot_index = select_runtime_slot(status, slots, preferred_slot=None)
CAUTION. Firmware programming writes directly to on-device flash memory. Improper operation can render the device unbootable.
During firmware programming:
Do not interrupt the programming sequence (phases 5–7: SENDING_IMAGE → PROGRAMMING → VERIFYING). An interruption can leave the target slot unusable; refresh status after recovery rather than assuming a particular resulting slot state.
Slot protection safeguards:
The field_update_e2e.py script enforces three levels of protection:
| Safeguard | Flag required | Default |
|---|---|---|
| Any firmware write | --i-understand-this-writes-firmware |
Refused |
Overwrite a VALID or PENDING slot |
--allow-valid-slot-overwrite |
Refused |
| Overwrite the default boot slot | --allow-default-slot-overwrite |
Refused |
Recovery. If service remains reachable, refresh the slot table and use the guarded load workflow to repair the intended slot. PENDING requires the valid-slot overwrite acknowledgement; overwriting a default slot requires its separate acknowledgement. Preserve a known-good image when possible. A remaining VALID entry is a recovery candidate, not a guarantee against every flash, service or image failure.
Image source. Load only firmware images supplied by Nexatom or explicitly approved for your hardware model. The firmware_manifest.json provides SHA-256 checksums for verification. Verify the checksum before loading:
certutil -hashfile firmware\Z080_004.bin SHA256
Compare the output with the sha256 field in firmware_manifest.json.