|
SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
|
The OPTIGA Trust M is shared by every task that wants a credential, a signature, a certificate — and it shares its I2C block with the CM55 touch controller. Three mechanisms keep that from turning into bus collisions, and each has an exact pairing rule:
| Mechanism | API | What it answers | Pairing |
|---|---|---|---|
| Gate | optiga_chip_enter() / optiga_chip_exit() | "does another task hold the chip right now?" | one exit per successful enter |
| Lock | optiga_manager_lock() / optiga_manager_unlock() | "is the manager up, and may I have it?" | one unlock per successful lock; aliases onto the gate |
| Touch hold | optiga_manager_touch_hold[_reason]() / optiga_manager_touch_release() | "keep CM55 off the shared I2C block" | counted; strict 1:1 on every exit path |
Plus the acquire/release pair — optiga_manager_acquire() returns the optiga_util_t * you drive the chip with, and optiga_manager_release() is an alias for optiga_chip_exit().
The failure signature you are learning to recognise is OPTIGA_COMMS_ERROR (0x0102): the touch controller and the secure element were driven on the same SCB at the same time.
optiga_chip_enter() is re-entrant per FreeRTOS task (nesting is free) and returns false for exactly one reason: another task holds the chip. That is fatal — return an error, never proceed. What it is not is an initialisation check. When the manager was never initialised it deliberately returns true (tesaiot_optiga_manager.c:157-169): "Reporting failure here is what made every call site fail open, because a caller could not tell 'no lock exists' from 'somebody else has it'." Succeeding when nothing exists and failing only on contention lets denial mean one thing.
optiga_manager_lock() is the opposite: it returns false when never initialised. It is the correct "is the manager up?" test. Skipping optiga_manager_init() before TLS made every CertificateVerify fail because trustm_ecdsa_sign() begins with optiga_manager_lock() (chapter C4, mqtt_mtls_setup.c:184-201). Getting enter-vs-lock backwards is the documented cause of several ungated-access defects.
Never call optiga_chip_exit() after a failed enter — the deferred-close early return exists precisely for that branch. The gate is a depth counter per owning task; an unmatched exit corrupts the count for everyone.
This is trustm_ecdsa_sign(), the function every TLS handshake ends in. Lock first, then hold touch for the whole transaction — "the secure element and the touch controller share the bus, and the TLS handshake signs after the mTLS setup has already resumed touch polling."
Release the hold before unlocking. The same order is used in the staged model verify:
The clearest textbook shape of lock/unlock with every early exit balanced is in tesaiot_crypto.c — shipped reference, not compiled by proj_cm33_ns (it is in no SOURCES list). Read it as idiom, not as a call site:
One more ordering rule from the same file family: never open the OPTIGA application while holding the lock — open first, lock second (optiga_trust_helpers.c:4604-4622).
optiga_manager_acquire() returns NULL until optiga_manager_init() has run. On NULL, do not call release() — the implementation already exited the gate. On non-NULL, exactly one release() per exit path, errors included — the shipped function has three exits and releases on all three. The async pattern sets optiga_lib_status = OPTIGA_LIB_BUSY before the call and polls after.
The hold is counted (s_touch_holds) — nesting inside a callee that also holds is correct and required. The first hold sleeps 50 ms so an in-flight touch transfer can finish. Take it for the whole chip conversation, not per operation: this ingest learned that the hard way when a 580-byte certificate write lost the SCB5 bus mid-transfer and failed with 0x0102. Every exit funnels through one pu_done: label that releases.
prov_open_held, prov_close_held, prov_make_csr_held, prov_manifest_anchor_held, prov_publish_pu_held: each takes a hold with a reason string, does one thing, releases. The comment above them records why they exist: "of the eleven chip operations on this path only four" held touch themselves; key generation and the CSR signature — the two longest transactions — ran with CM55 polling the FT5406 on the same SCB, "which is what returns <tt>OPTIGA_COMMS_ERROR (0x0102)</tt> and then leaves the next call meeting <tt>OPTIGA_UTIL_ERROR_INSTANCE_IN_USE (0x0305)</tt>." And: "Nothing holds across the 60 second wait for the platform: no chip traffic happens there, and freezing the screen for a minute would be its own bug."
The reason strings are not decoration. They travel to CM55 and are shown on the panel while touch is disabled — "Opening the secure element", "Generating a key and signing the request", "Reading the secure element", "Closing the secure element".
Hold with reason, then optiga_chip_enter(); on a failed enter, release the hold and give the mutex back before returning. mqtt_mtls_setup.c is the extended version of the same discipline — seven releases for one hold.
CM55 treats IPC_CMD_TOUCH_RESUME as an unconditional touch_disabled = false — it is not a counter (ipc_hsm_handler.c:1393-1405). A raw resume sent from any task cancels a hold that another task is relying on; the next touchpad_read() on CM55 disables and re-enables the shared SCB block under a transfer CM33_NS still has in flight. That is exactly the 0x0102 this page's own provisioning poll used to cause "about 150 times per enrolment, at 400 ms intervals". Always go through optiga_manager_touch_hold_reason() / optiga_manager_touch_release(); never send the raw command. (Appendix X, item 2.)
These steps use the HSM page because it exercises all three mechanisms; the observables are the panel and the UART.
Home → HSM Security → Enrol Certificate (or run any credential read).
What you should observe. The screen's touch goes dead and a reason string appears — one of the strings above, e.g. Opening the secure element then Generating a key and signing the request. When the wrapper releases, touch returns. There is no UART line for hold/release.
Start an enrolment and, while its reason string is on screen, request a credential read from the same board (a second HSM page action, or optiga-module call from the REPL on mtb-mpy).
What you should observe. The second operation reports failure without touching the chip — the gate returned false because another task holds it. On the UART, ipc_hsm_cred_read_sync prints nothing on that path; a mutex timeout would print [HSM] cred_read_sync: mutex timeout (slot u) (ipc_hsm_handler.c:2408). The first operation completes normally. This is "denial means one thing": contention, never "not initialised".
You are not asked to cause this — the shipped code is designed so you cannot from the UI. Recognise it: any chip operation returning OPTIGA_COMMS_ERROR (0x0102), frequently followed by OPTIGA_UTIL_ERROR_INSTANCE_IN_USE (0x0305) on the next call, means a chip transaction ran while CM55 was polling touch on the same SCB. Look for a missing hold, a raw IPC_CMD_TOUCH_RESUME, or an unbalanced release that dropped the count to zero early. On the mTLS path the symptom is [PSA-Sign] ERROR: trustm_ecdsa_sign status=0x0102 (chapter C4).
What you should observe. The panel stays dead after an operation finishes — "an unbalanced release leaves the panel dead". Count holds and releases on every exit path of the function that ran last.
All ten optiga_manager_* / optiga_chip_* functions are in libbento_hsm.a, which needs zero MicroPython symbols. optiga_trust_helpers.c, tesaiot_pu_ingest.c and ipc_hsm_handler.c ship as source and compile on both variants. The touch IPC underneath — ipc_hsm_touch_pause, ipc_hsm_touch_pause_reason, ipc_hsm_touch_resume — is on the archive's consumer_must_provide.txt list (chapter D3): the template provides it, and so must any consumer that replaces the template's HSM handler.