Firmware (Soldier + Queen)

SkillDev tools

Use when working on the silken_net STM32 firmware, Soldier (sense→TinyML→Lorenz→encrypt→TX, STOP2 loop) and Queen (LoRa RX→CIFO dedup→CoAP flush, OTA) in firmware/{soldier,queen}/main.c, the mruby bio_contract.rb, and the header-only One-Home libs in firmware/common/ (silken_sha256, lorenz_seed, lora_ccm, silken_crc, queen_attest). Knows the non-obvious gotchas, ECB-restore after CBC, Load_AES_Key before MX_CRYP_Init, RTC DR0..DR19 budget (DR7 freed by FW.54), post-FW.29 StatusByte bit-layout, Lorenz continuation vs cold-start, HAL_GetTick frozen in STOP2 (wall-seconds instead, HAL-free arithmetic in wall_time.h, the RTC read in soldier/main.c), vcap = VDDA-mV not EDLC-Vcap, gated CCM vs live ECB, and the host-test parity discipline (make -C firmware/test). Routes to CLAUDE.md §3 + the 03_01..03_06 canon, does not restate. Examples: \"add a sensor field\", \"change Lorenz params\", \"modify AES / CRYP init\", \"touch RTC-persisted state\", \"why do LoRa decrypts fail after a flush\", \"edit the seed / cold-start crypto\".

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Firmware (Soldier + Queen) skill

What this skill tells your AI

The instructions your AI receives, as published by alexey-lukin/silken_net in .claude/skills/firmware/SKILL.md and read by ahel’s review.

Navigation aid + non-obvious gotchas. The SSOT is the docs + code below — this skill points, it does not restate (so it can't drift). Verify a fact at its home before trusting a summary.

SSOT Documents — Read These First

DocumentWhat it covers
CLAUDE.md §3High-level architecture, AES table, OTA, Lorenz (summary — for exact packet/Status-byte bit-layout trust the canon docs below, not the summary)
docs/03_01_Firmware_Lifecycle_and_DMA.mdSoldier/Queen lifecycle, STOP2 phases, ISR map, RTC Backup Register Map §2 (canonical DR0..DR19 + magic markers), binary packet format §8, mesh-relay + rendezvous ladder §1.9 (CCM-era star-only; single-Queen scale ceiling §1.9.1)
docs/03_02_Queen_Gateway_Firmware.mdQueen: LoRa RX → CIFO dedup → CoAP flush, OTA broadcast, AES mode transitions
docs/03_03_TinyML_Acoustic_Inference.mdAudio DMA, TinyML INT8 forward-pass (NOT CMSIS-NN), decision logic, confidence thresholds (DR13/DR14)
docs/03_04_mruby_Lorenz_Attractor.mdLorenz constants (§1.2; Z-thresholds §4.1), Float-not-BigDecimal parity, RTC continuation vs cold-start (§2.1), Status-byte packing (§4.4)
docs/03_05_Hardware_Symmetric_Crypto_and_Security.mdAES modes, key rotation (FW.17), SE05x secure element (baseline SE051C2; SEC.6 + SEC.14 provisioning-only — KEYL stays in Protected Flash; ATECC608B = banner-legacy pattern), IV, QATT wire (§2.2), PQC roadmap
docs/03_06_Factory_Flashing_and_Key_Provisioning.mdFactory home (split from 03_05 §3.4): HKDF per-device derivation, K_seed, K_ota (FW.23 §4), flashing pipeline + ops-security (SEC.3 §5)
docs/00_07_Action_Plan_Tracker.mdFW./SEC. task status, open items (canonical home of blockers)

Source Files

FileRole
firmware/soldier/main.cSensor node: sense → TinyML → Lorenz → encrypt → TX (multi-phase STOP2 loop)
firmware/queen/main.cGateway: RX → decrypt → CIFO cache → batch flush via CoAP (CCM-ера gated: blind courier — air-кадр (30B rev2.1) НЕ розшифровується, запис air+1 via rx_route.h)
firmware/queen/*.hQueen-only pure headers (host-tested): at_engine/coap_pdu/sim7070_coap (FW.3/FW.56 модем-труба + Sim7070_Read_Csq), sim7070_udp (FW.60 сира CA*-розмова poll-тракту: Sim7070_Udp_Fetch, CARECV-заголовок посимвольно; механіка poll'а = Queen_Poll_Downlink main.c — канон 03_02 §4а), uart_rx_ring, ota_window (FW.52б), ota_sha_guard (FW.52 SHA-256 cross-check на Magic Re-Request по закритому OTA-вікну — Queen's ВЛАСНА сторінка 125 [номер збігається з Soldier-івською 125 (K_ota/KEYB, 03_01 §2.3) — інший чіп, не колізія, але завжди читай «сторінка 125» з іменем чіпа поруч], magic-last; свідомо сиблінг firmware/common/flash_ota.h, не reuse), rx_route (FW.2 роутер 16/air + запис air+1), soldier_cmd_queue (FW.20-Q2), coap_iv (SEC.12), helium_sos (ARCH.34: 12B SOS-wire byte-parity з HeliumSosWorker + тригер + бюджет сліпоти; MAC-шов Helium_Mac_SendSos = adapter у firmware/queen/lorawan_glue/helium_mac.c; ⚠️ поріг тиші SOS = 2× каденс флашу — заголовок несе лише #ifndef-дефолт для host-тестів, справжній каденс підставляє main.c з FLUSH_INTERVAL_MS+jitter і _Static_assertить нерівність: фіксоване 30 хв було МЕНШЕ за 60-хв каденс і кричало на здоровій Королеві при N ≈ 45–160 (⚖️ 2026-09-10) — той самий клас, що #13(a): pure-заголовок, який дзеркалить константу main.c, мусить мати #ifndef і static-assert, не число)
firmware/bio_contracts/bio_contract.rbmruby Lorenz attractor (runs on MCU); calculate_state is the sole entry-point
firmware/common/*.hShared header-only One-Home libs (compile into BOTH firmware + host tests — kill the mirror-drift pattern): silken_sha256.h (SHA-256/HMAC, FIPS/RFC KAT), lorenz_seed.h (FW.30 cold-start deriv), silken_crc.h (CRC16-CCITT, OTA), lora_ccm.h (CCM packet), lora_phy.h (FW.61 базлайн модуляції raw-LoRa P2P — pure, БЕЗ Radio, тож його включає й cad_sniff.h у host-тестах; дім номіналів 03_05 §2.1, модель airtime tools/firmware/lora_airtime.rb) + lora_phy_apply.h (той самий профіль → драйвер: Lora_Phy_Apply_Tx/Rx, єдиний шов, ЄДИНИЙ файл у common/, що тягне вендорський radio.h; ⛔ не зливати з pure-половиною), tx_defer.h (FW.10 Cold-TX: Should_Defer_TX — freeze-contract на < −15°C/4000mV, host-gated One-Home; раніше тест-локальний дубль = false-green; ⚠️ [ARCH.99] Vcap-половина у полі НЕ розрізняє — див. гочу 9), adc_convert.h (FW.50 VREFINT-cal ADC→mV; wired: vcap_voltage = Adc_Vdda_Mv(...) — VDDA-mV, not EDLC-Vcap), queen_attest.h (L1 QATT v2 signed-batch envelope + 8B health-блок пульсу — ARCH.54; wire home 03_05 §2.2; residue 1, ct=0 heartbeat легальний), flash_kv.h+fc_hiwater.h+ota_antirollback.h (journal Flash-KV база + FC/OTA-version high-water — SEC.20 anti-rollback ключ 0x15, строго >; база вимкнена з фіч-фліп-гейтів через SEC20_OTA_ANTIROLLBACK_ENABLED — вкл. декл/mount/compact (Put32 НЕ auto-компактить)), fw_report.h (SEC.20 wire-звіт contract-стану `[semantic:1
firmware/test/x86 host-based tests — make -C firmware/test (host gate; not gcov-instrumented — per-line coverage gaps → best-TU трюк [[reference_firmware_coverage_gcov_gotcha]]). make -C firmware/test asan = ASan+UBSan dynamic memory-safety lane (TEST.5, CI-gating in firmware_test — keep it green; canon 04_06 §B.1.1). New crypto in common/ ⇒ add a parity test vs OpenSSL, don't re-copy logic into the test file.
firmware/extern/subghz-phyVendored stm32-mw-subghz-phy @v1.5.0 (FW.46 Шлях A): radio_driver/radio.h = THE Semtech API both main.c compile against (owned stub deleted; both register a static RadioEvents_t — driver keeps the pointer). radio.c NOT compiled until radio_conf.h exists (.ioc board-freeze); ⚠️ its lorawan/ dir is the LBM radio layer (SWL2001 support: smtc_ral/ralf, radio_planner, SPI chip drivers) — NOT a MAC stack; the ARCH.34 LoRaMac-node = separate submodule firmware/extern/stm32-mw-lorawan @v2.6.2; its Conf/*_template.h are CubeMX-fill templates → owned copies live in queen/lorawan_glue/ (EU868-only profile, soft_timer/systime pure-cores, helium_mac.c adapter — episode = fresh OTAA join, DevNonce = the ONLY cross-join persist via MIB_NVM_CTXS→flash_kv 0x30). Host-smoke of the REAL MAC on a stub radio: make -C firmware/test helium_mac_smoke — full OTAA join+uplink cycle vs an in-test mock-LNS (crypto-valid JoinAccept on the zero-key se-identity; server-side MIC + FRM parity with LNS-derived session keys; deadline = the real 20s blind budget at SF12-TOA) + a crypto-valid 23B JoinRequest + DevNonce monotonicity/KV-reboot. The mock needs -DAES_DEC_PREKEYED (smoke-target-only — the server ENCRYPTS JoinAccept with AES-decrypt; mirrored in firmware/.clangd). ⚠️ Helium_Mac_Bind_Nvm keeps the pointer forever — bind only a static/file-scope FlashKv (ASan stack-use-after-scope class). ⚠️ The recipe links -lm: RegionCommon.c calls floor(), and the coverage lane's -O0 (make coverage) does NOT inline it as a builtin the way -O1 (asan) / -O2 (normal) do → a green local helium_mac_smoke + asan still red-CIs on coverage without it (the class: a vendored-math TU needs explicit -lm; make coverage is the only lane that catches it). ARM lanes: lorawan_mac_check (glue+MAC) + hal_check_ccm compiles the gated Queen KV-mount block (ARCH34_HELIUM_ENABLED=1). The submodule is OUR FORK Alexey-Lukin/stm32-mw-lorawan @v2.6.2-silken.1 — v2.6.2 + one fix: RegionCommonComputeSymbolTimeLoRa SF11/SF12 signed-overflow (upstream Semtech master has the same hole; UBSan found it, upstream-PR = 00_07 ARCH.34 👤). Bumping the submodule → re-check that fix survives. Include-path lives in CMake hal-lane + firmware/.clangd (change one → sync the other)

Line counts drift every commit — don't hardcode them (see [[feedback_no_volatile_counts]]); wc -l if you need a number.

Gotchas Not Obvious From Docs

  1. Shared peripheral, per-operation reconfig: restore the baseline — and make sure a baseline EXISTS. Named instance: Queen MUST call Restore_ECB_Mode() after every CBC operation, else subsequent LoRa decrypts silently fail (wrong KeySize + wrong key in CRYP). The class is wider than CRYP — it has bitten in three different shapes, and the breakage always lands on the NEXT user of the peripheral, never on the caller who reconfigured. Check all three whenever a peripheral has more than one caller (CRYP · Radio · SPI1 · ADC · DMA):
    • (a) configured, never returned — the original SEC.8/FW.16 ECB↔CBC, plus CCM width-units (MX_CRYP_Restore_From_CCM puts DATATYPE back to WORD, gotcha 7.1). CRYP-specific home: 03_05 §7.
    • (b) nothing to restore TO — ✅ closed 2026-09-11 [FW.61], and the shape is worth keeping because it is the one a restore-reflex cannot reach. Baseline LoRa modulation used to be set by no call at all, so the peripheral carried whatever the last writer left. RadioInit is not a baseline: it programs timers/IRQ and SUBGRF_SetTxParams(RFO_LP, 0, …) — the low-power PA at 0 dBm — while SF/BW/CR/preamble/CRC stay unwritten, and SubgRf.RxContinuous stays false, which silently turns the Queen's always-on listener into RX-single. A driver default is not a baseline; only an explicit call is. The doc home of the NUMBERS is 03_05 §2.1 (00_06 §2.3 registry); the compiler's declared MIRROR is firmware/common/lora_phy.h (pure profile) + lora_phy_apply.h (Lora_Phy_Apply_Tx/Rx — the single seam both firmwares and make -C firmware/test lora_phy call). ⚠️ The two setters share ONE pair of driver structs, so order matters: TX then RX, and RadioSend rewrites payload length per frame.
    • (c) a partial restore reads as a complete one — ✅ closed by construction in the same pass, and that is the point: the panic-TX restore used to re-set ONE of six SetTxConfig arguments (preamble) and pass a private LORA_PANIC_* quartet for the rest, correct only because that quartet happened to equal canon. Those defines are gone; PANIC now differs from baseline in exactly one argument, because there is only one place the other twelve are written. Whenever you see a "restore" that names fewer arguments than the call it undoes, the fix is not to widen the restore but to remove the second copy of the defaults. 1b. Queen CDNSGIP re-resolve on flush-fail — FW.58 host-half SHIPPED (vilize 07-11)coap_server_ip is cached for the boot, but Coap_Reresolve_Due(coap_consec_fail) (sim7070_coap.h) now invalidates it after N≥3 consecutive flush-fails → next flush re-resolves (an A-record flip = the zero-infra failover is picked up без IWDG-reboot). coap_consec_fail streak resets on any send_success; g_coap_fail_count stays a lifetime health-odometer (never reset). DNS-fail path already clears the IP (return before the streak logic — no double-count). Silicon-confirm = RUNBOOK §5.5 (👤 bench). CMD_SET_BACKEND never existed — the doc-phantom is purged. 1c. Retransmit is UPLINK-only — poll has none, and the canon claimed otherwise for months (FW.63). Flush_Cache_To_Rails increments coap_mid ONCE and retries the same PDU inside for (retry < COAP_MAX_RETRIES) — a genuine same-MID CON retransmit. Queen_Poll_Downlink does the opposite: coap_mid++ on every attempt and returns on an empty reply, and Sim7070_Udp_Fetch is a one-shot CAOPEN→CASEND→CADATAIND→CARECV→CACLOSE with no internal retry. So a lost 2.05 on the downlink poll is never re-requested, and because Rails advances the command state when it BUILDS the reply, the order is lost forever while the trail says confirmed. Rails' CoapGate::REPLY_CACHE is real but only dedups a duplicated datagram. ⚠️ And "it cannot fire without a retransmit" — which stood here — was wrong in the direction that costs: coap_mid lives in the Queen's RAM and the cache slot has no TTL, so after a reboot the same number arrives carrying a DIFFERENT question. Serving the cache there answers someone else's poll and silently swallows the live pending command. Closed 2026-09-08 by keying the slot on a request FINGERPRINT (route + query): match → poll_duplicate, mismatch → poll_mid_collision + fresh derivation. The transferable half: a cache keyed on a counter that its writer resets is keyed on nothing — ask what zeroes the key, not just what increments it. Two reflexes: (a) never generalise one CoAP tract's reliability onto the other — that is exactly how the false claim entered three docs and two code comments; (b) the OTA leg shows the correct shape — g_ota_fetch_next_ch does not advance until the chunk is handled, so a lost chunk is simply re-requested; CMD has no such cursor. ⚠️ CMD now has an equivalent cursor too — closed 2026-09-09, ratified 2026-09-10. Rails no longer advances past dispatch! at build time; acknowledge! waits for observe_delivered_command! to see a cmd=<token> echo from Queen (g_last_acked_cmd_token, mirroring fw=) on a later poll, so a lost 2.05 now re-serves the same envelope instead of losing the order — Cmd_Dedup_Check makes the redelivery safe. Design-justification (why echo over blind re-delivery or a documented open-loop) → 03_02 §4а. ⚠️ And the echoed token is the LAST colon-field, never «the 3rd colon» (FW.60, 2026-09-10): an ACTION:value payload shifted the old window onto ACTUATOR_ID, so the echo never matched idempotency_token and an EXECUTED command died by TTL as failed — the dedup hash stayed self-consistent, which is exactly why nothing noticed. Locator = pure header firmware/queen/cmd_token.h (host-pinned, mutation-verified); Rails forbids a colon in ACTION (ActuatorCommand::ALLOWED_PAYLOAD_FORMAT), so both ends agree by construction — do not reintroduce a colon-counting parser.
  2. RTC registers are nearly all spoken-for — but allocated ≠ optimally packed — DR0..DR19 allocated except DR7, freed by FW.54 (DID is now deterministic f(UID) recomputed on boot via did_derive.h03_01 §7; spend DR7 only via the 03_01 §2.2 procedure). After the FW.2 freeze-contract even DR15 is reserved (CCM Frame Counter, dormant until FW2_CCM_ENABLED flips at bench). For new persisted state: first check 03_01 §2.3.2 reclamation (under-utilized DRs → freeable without Flash: DR2 holds a 1-bit flag, DR14/DR19 pack) before paying Flash-KV (03_01 §2.3) wear. RAM-state inventory + key→field map = 03_01 §2.3.1. Canonical register map (bit-fields + magic markers) = 03_01 §2 — never restate the allocation in code comments, reference it.
  3. Load_AES_Key() BEFORE MX_CRYP_Init() — reversing the order means CRYP uses a zeroed key. Both files follow this order but it's easy to break during refactoring.
  4. Cold-TX deferral — at low temp + low Vcap (FW.10 winter ESR guard), Soldier skips TX entirely to avoid brownout. Don't assume every wake cycle emits a packet.
  5. StatusByte layout is post-FW.29 — packet byte 10 = [PanicFlag:1 (bit7) | Status:2 (bits6..5) | GrowthPoints:5 (bits4..0)]; firmware packs (status << 5) | growth_points, backend unpacks (byte & 0x1F) * 2. Status now has 4 values (0 homeostasis / 1 stress / 2 anomaly / 3 vm_error — [SLASH-1 P0, 2026-07-05] status=3 is written ONLY by BIO_STATUS_VM_ERROR; mruby pack_status_byte returns 0..2; physical tamper rides PANIC_FLAG, NOT the status bits — backend decodes 3 as vm_errorfirmware_fault alert, never vandalism_breach). Normal frames force bit7=0 (lora_payload[10] &= ~PANIC_FLAG_BIT); panic frames set it. Canon: 03_04 §4.4 Status-byte + 03_01 §8.
  6. Lorenz state: continuation vs cold-start — on boot, DR19 == LORENZ_STATE_MAGIC (0x4C5A5354 "LZST") && isfinite(x,y,z) → restore (x,y,z) from DR16/17/18 (warm continuation, >99% of cycles); else cold-start derives (x₀,y₀,z₀) from K_seed (Flash) via firmware/common/lorenz_seed.h (pure-C HMAC-SHA256 — byte-parity with backend SeedDerivation, FW.30 closed; mbedTLS no longer needed). epoch_day prefers beacon UTC (soldier_unix_ts, FW.20) over the RTC calendar; RTC-default after VBAT loss = 10957 (2000-01-01) = backend's FIRMWARE_RTC_DEFAULT_EPOCH_DAY. After each cycle the C side MUST write DR16-18 + set DR19. Canon: 03_04 §2.1; layout 03_01 §2.
  7. CCM is gated, ECB is live — WL CCM API is TWO-PHASE, and the CCM era is TWO-KEY — the FW.2 AES-128-CCM path (30B wire-rev2.1, 8B MIC, DR15 FC) is fully integrated behind FW2_CCM_ENABLED 0 (Soldier Phase-4 + panic TX; Queen = blind courier — cleartext-DID demux, NO decrypt, air+1 records via firmware/queen/rx_route.h). The shipping build is AES-128-ECB transitional. Traps: (1) HAL_CRYPEx_AESCCM_Encrypt/Decrypt DO NOT EXIST in WL-HAL (F4/F7/L4 API) — the real flow is Init.B0 (Build_CCM_B0) + HAL_CRYP_Encrypt/Decrypt (Size in BYTES) + HAL_CRYPEx_AESCCM_GenerateAuthTAG + caller-side Fw2_Ccm_Tag_Equal; after CCM restore width-units to WORD (MX_CRYP_Restore_From_CCM). (2) In the 14B CCM plaintext (rev2.1) status_byte is offset 6, offset 10 is diag, offsets 12..13 are ema_delta_t_s («wire = GP input» contract, E.63 (г)) — never read CCM payload through legacy 16B offsets. (3) Key-scoping (FW.2 (в), 2026-07-03): Soldier's ambient hcryp = KEYB cluster control-plane (bcast_key, page 125, Load_Broadcast_Key fail-open fallback→KEYL); session aes_key (KEYL) lives ONLY inside the CCM scope — MX_CRYP_Init_CCM sets pKey EXPLICITLY, Restore returns KEYB (a sticky session key would MIC-fail every frame on Rails). Queen's single LoRa key = the KEYB value (her code unchanged). FW.17 ratchet rotates session only. Mesh Scenario Б is gated OUT of the CCM build (star-only; mesh return = wire-rev3 addressing → 00_07 ARCH.43, scale ceiling 03_01 §1.9.1). (4) Gated-branch code compiles NOWHERE locally — host tests mirror, clangd can't see past the gate; a call before its static definition needs an explicit prototype or only hal_check_ccm (CI) catches it. All flip-gates DECIDED (founder 2026-07-03) — flip day = bench attestation only; 03_05 §2.1 flip-checklist + §3.1 two-key model + 00_07 FW.2.
  8. HAL_GetTick() is frozen in STOP2 — SysTick stops during EnterSTOP2Mode, so a tick delta measures only active-time, NOT wall-time. Host mocks have a monotonic tick → they hide this. FW.49 S1 host-half is shipped: delta_t_seconds (the primary bio-signal) runs on wall-seconds. ⚠️ The timebase is SPLIT across two files on purpose, and the header is the half that must stay pure: firmware/common/wall_time.h carries the HAL-free arithmetic (Silken_Wall_Delta_Seconds · Silken_Wall_Elapsed_Seconds · Silken_Wall_Is_Utc · Silken_Civil_From_Unix) so it compiles into the firmware AND the host tests, while the HAL half — Wall_Seconds_Now, which reads the RTC calendar and is ≡0 on silicon until the clock is set — lives in firmware/soldier/main.c. ⛔ Do not add a HAL call to the header: that seam is exactly what makes «host-half is shipped» mean anything. (Wall_Seconds_Now, RTC-calendar timebase, cold-start/backwards/epoch-jump guards → сентинел DELTA_T_UNKNOWN_S; wire dT:2 saturates @0xFFFF). ⚠️ On silicon Wall_Seconds_Now returns 0 (honest refusal → сентинел → GP=0) until the LSE/RTC clock-tree bench bring-up — 00_07 FW.49. ⚠️ Absent is the CLOCK-TREE, not the symbol: MX_RTC_Init is declared, called and stubbed in three files (firmware/soldier/main.c, firmware/test/hal_mock.h, firmware/hal_glue/soldier_hal_check.c) — its body is empty, so a grep for the symbol finds it and proves nothing; FW.27-B re-request counts silent wakeups instead of ticks (STOP2-immune by design). Still: never trust a raw tick delta as wall-seconds. Canon 03_01 §1.4/§1.10. 🔴 АЛЕ «honest refusal → baseline» вище описує лише ПІВ-контракту, і друга половина коштує ГРОШЕЙ (переміряно 2026-08-16): BASELINE_DELTA_T_S = 60 подано як нейтральний, а metabolic_health(60) = (7200−60)/6600 = 1.08 → clamp 1.0expected_homeostasis_gp = 31 = GP_HOMEO_MAX. Тобто відмова чесна, а її посадкова точка — максимум балів росту, які йдуть у Wallet#credit!, у мінт і в leaf0 L1-якоря. Іронія несуча: коментар над самим викликом називає рівно цей режим тим, що FW.49 і лікував («m(delta_t) ≈ максимум у всіх → over-mint») — полагодили ДЖЕРЕЛО часу, посадку лишили. І оскільки на кремнії Wall_Seconds_Now() ≡ 0, це не крайній випадок, а стан за замовчуванням: гілка cold-start спрацьовує щоцикла, а EMA прогрівається в те саме число. Рефлекс, ширший за цей сайт: обираючи fallback-константу, підстав її у ВСЮ ланцюжок до кінця і подивись, у що вона мапиться — «нейтральне» значення входу регулярно виявляється екстремумом виходу.ПОЛАГОДЖЕНО 2026-08-16 (ARCH.102): посадка тепер DELTA_T_UNKNOWN_S = 0 — окремий СЕНТИНЕЛ, а не значення, і гілка на нього стоїть ПЕРЕД формулою обабіч дзеркала (bio_contract.rb#pack_status_byteAttractor.expected_homeostasis_gp) → status = 0, GP = 0. 🔴 Чому саме окрема гілка, а не «краща константа»: формула на нулі вироджується у свою протилежністьm = 7200/6600 = 1.09 → clamp 1.0 → GP = 31, тобто будь-яке значення-заглушка тут дає максимум. Отже клас лікується не підбором числа, а виведенням «не виміряно» з простору значень. ⚠️ Дзеркальний ризик, що лишається: vcap поруч і далі падає на nominal 3300, і це СВІДОМО — шину стабілізує BQ25570, тож там 3300 не здогад, а специфікація; дві різні відсутності мусять мати дві різні відповіді. 🔴 І коли ти нарешті ПИШЕШ тіло MX_RTC_Init на бенчі — знай, що жоден гейт помилки в ньому не впіймає [FW.49, виміряно 2026-09-05]. make -C firmware/testasan) glue-TU не компілює взагалі; ARM-джоба збирає firmware/hal_glue/soldier_hal_check.c як OBJECT-бібліотеку (firmware/CMakeLists.txt, SILKEN_WITH_HAL=OFF за замовчуванням) — тобто компілює, але НЕ лінкує й НЕ виконує. Обидві впадають в агрегат ci-ok, тож він зеленітиме, бо КЛИЧЕ НЕ ТУ ЛАНКУ. ⚠️ Ціна не косметична: типова STM32-пастка (забутий HAL_PWR_EnableBkUpAccess, не той RTCCLKSource) дає HAL_OK зі СМІТТЄВИМ, але ненульовим часом — це вже не «чесна відмова», а бракований delta_t, що годує metabolic_health() і мінт GP, тобто рівно той over-mint, задля якого FW.49 і заводився. Верифікація тіла — приладом на бенчі, ніколи зеленим CI.
  9. vcap_voltage is honest mV of VDDA, NOT the EDLC Vcap — post-FW.50 the raw-counts-as-mV bug is fixed: vcap_voltage = Adc_Vdda_Mv(...) (factory VREFINT-cal, firmware/common/adc_convert.h, host-tested) → ≈3300 mV while the buck lives. But VREFINT sees VDDA behind the buck, and BQ25570 regulates that rail to 3.3 V from VSTOR ≥ 3.4 V up to the cap's 5.5 (02_03 §7) — so the rail carries no information about stored energy at all, by design of the power tree. Until a real Vcap divider + separate ADC channel lands (hardware residual, 00_07 FW.50), **every threshold on this rail is degenerate, and there are FOUR of t

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
22
Forks
1
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
firmware
Source
github.com/alexey-lukin/silken_net