ET-SoC-1 · per-sensor temperatures ·

Your patch reads all 35 sensors. To see heat move between tiles, it needs the raw codes.

← AI Foundry lab dashboard

Roman, thanks for DM_CMD_GET_ASIC_PVT_SAMPLES. My agents read it against the source, built it, and measured the mailbox on aifoundry3's card. The command, the layouts and the handler are sound. Two things stand between it and what I want to do on Friday: it returns whole degrees, and it lets any reader reset everyone's peaks. A small patch on top of yours fixes both, and it builds for all five BL2 variants.

35 / 35sensors enabled and reachable; one call covers the minion shires
1 °Cstep of the readings the patch returns
0.061 °Cthe sensors' own step; the raw code fits the same 16-bit word
0.1–1 °Cexpected difference between nearby tiles

Yes, with two firmware changes; the host needs no library change

Most important first. The first three are in 0004, a patch on top of yours. The host side needs nothing from libDM: as you said, a Python tool can read the command straight from the management node.

  1. Firmware · the one that decides itReturn the raw 12-bit codes. New group PVT_SAMPLE_GROUP_TS_RAW: all 35 sensors (34 minion shires, then the I/O shire) as current, high and low codes in one reply, 105 of the 234 words. It needs a short reader, about 30 lines, in pvt_controller.c, because pvt_get_ts_data converts to whole degrees in integer arithmetic and pReg_Pvtc is static to that file. I should have said this up front: whole degrees can't show a 0.2 °C front crossing a tile.
  2. Firmware · shared cardsDon't serve HILO_RESET from a read. pvt_hilo_reset() clears the TS, PD and VM latches on all five controllers: every other reader's peaks, the SP's own minshire_high/low, and the per-shire voltage min/max. 0004 stops serving it; a TS-only reset of the requested channels could come back later as a SET command.
  3. Firmware · robustnessCheck the request size. The dispatcher's buffer is static, so a short request would read group, first and count left over from an earlier command. 0004 serves it as an invalid group. It also makes pvt_samples_t aligned(8) instead of packed: the same layout (512 B, data at byte 40), without the one new warning, -Waddress-of-packed-member at thermal_power_monitor.c:1018.
  4. Host · no change neededRead it from the control plane with Python, as you suggested. The driver checks only a message's size, never its command code, so pvt_ctl.py pushes the message itself and pops the reply, with no libDM. Checked on aifoundry3's card: the same temperature reply as dev_mngt_service (45 µs on the card), and a GET's request body reaches the firmware. libDM itself would refuse code 74 and drop the request body of a GET, so 0005 matters only for libDM tools like ettelem.

Four questions for you

  1. Your file is 0003-…. Do 0001 and 0002 exist, and do they carry a host side?
  2. The cards run release 1.3.1 (BL2 0.20.0 on aifoundry3) and 1.2.0 (aifoundry1 card 1), and the patch is against the July tree. Will you backport the command, or flash a build of the July tree?
  3. Is sensor n in minion shire n, and where does each shire sit on the die? The TS path indexes channels linearly, while the voltage map for the same shires is scrambled (pvt_controller.c:154). With the patch I can also measure it: heat one block and see which sensors move.
  4. Do these parts carry fused per-sensor calibration? RUN_1 applies one nominal formula to all 35 sensors, and at idle the hottest sensor already reads 1–2 °C above the mean, which may be calibration rather than heat.

Almost all of the spatial signal is under one degree

The lab's conduction model with the heatsink on, at 0.68 W per shire. Against the two cases measured so far it reads low, not high.

Whole degrees can still see the large contrasts if the experiment is run as a lock-in: switch the load between two places at a fixed rate and average each sensor synchronously, and the readings dither down to about 22 mK in 60 s. Raw codes do the same 7× quieter, which is the difference between a one-minute average and a heat-map movie.

Pattern, switched at 1 Hz, 60 sSNR, whole °CSNR, raw codes
One shire4.332
2×2 block5.842
Checkerboard6.648
Half die18133

Rate is not the limit. Heat crosses a 3.7 mm tile in about 0.17 s, so 20–50 readings a second is the target. The command is served by the dispatcher, not the SP's thermal pass, and on aifoundry3's card, running your new firmware, one call for all 34 minion-shire sensors takes 42 µs on the card and about 1.4 ms from Python. Use a compute-only heater for the load: mesh and DRAM traffic can stretch a 22 ms management reading past a second.

What we checked, on aifoundry2 and aifoundry3

CheckResult
Applies to et-platform 836a4ab60, the public headClean
Full BL2 build, your patchAll 5 variants build; one new warning (packed member)
Full BL2 build, your patch + 0004All 5 variants build; the touched files compile with -Werror
Command 74 on aifoundry3 card 0, your firmware (release 1.4.0, BL2 0.22.0)Works: all 34 minion-shire sensors, no faults, 42 µs on the card; 55–58 °C at idle, in whole degrees
Python, no libDM, aifoundry3 card 0Works: the same reply as dev_mngt_service; a GET's request body arrives (state 4 answers -9004)
libDM with 0005 (optional)Compiles (syntax check against the patched headers)
Strides against the structsAll six match: TS 3, PD 3, minion VM 9, mem VM 6, I/O 15, PCIe VM 6
Response layout512 B, data at byte 40, no field wider than 16 bits
Clipping, fault_mask, concurrencyCorrect; the getters are plain register reads
Minion VM groupTwo calls: 34 × 9 words exceeds 234
I/O-shire groupWorks on aifoundry3's card: the I/O-shire temperature arrives (57 °C), no fault
Temperature readings at idle, aifoundry3 card 0, read-only0 changes in 32 s at 45 readings/s, while board power changed 48 times
Mailbox cost, same cardTemperature query 48 µs on the card; 250 two-query readings/s from the host

Nothing was flashed, no new command was sent to a card, and no peaks were reset. aifoundry3's card was held for 56 s under its lock.

Friday: one card, three steps

aifoundry2's card is out of service, so aifoundry3, or aifoundry1 card 1.

  1. Map the sensors. Heat each 2×2 block in turn with a compute-only kernel and record which indices rise. That fixes the 6×6 placement whatever the documentation says.
  2. Lock in. Switch the load between opposite corners at 0.5–2 Hz, read TS_RAW at 20–50 Hz, and difference each sensor against its own baseline (pvt_ctl.py pvt --group ts_raw). The phase map, about 80–165 ms of lag per tile at these rates, is what shows heat spreading.
  3. Camera, slowly. From the back of the board, expect a half-die contrast at slow switching (0.01–0.03 Hz), not single tiles. That is an estimate, because the board's layer stack isn't known.

The patches

Both patches apply on top of your 0003 with git apply or patch -p1. pvt_ctl.py is the control-plane reader, like your et-pvt-top; it has read command 74 (groups 0 and 4) on aifoundry3's card.

0004-pvt-samples-raw-codes-and-fixes.patch
--- a/device-api/include/management-api/device_mgmt_api_rpc_types.h
+++ b/device-api/include/management-api/device_mgmt_api_rpc_types.h
@@ -1572,8 +1572,10 @@
     uint8_t total;             /**< total entries available in this group */
     uint8_t pad[3];            /**< Padding for alignment */
     uint64_t fault_mask;       /**< bit (first+i) set if entry i could not be sampled (data zeroed) */
-    uint16_t data[PVT_SAMPLES_MAX_WORDS]; /**< count*stride words, entry-major */
-} __attribute__((packed));
+    uint16_t data[PVT_SAMPLES_MAX_WORDS]; /**< count*stride words, entry-major; temperatures in C
+                                                are int16_t, TS_RAW words are 12-bit sensor codes */
+} __attribute__((aligned(8))); /* not packed: the firmware writes entries through pointers into
+                                  data[]; the field offsets are the same */
 
 /*! \struct device_mgmt_pvt_samples_cmd_t
     \brief Command to request disaggregated PVT samples
--- a/device-api/include/management-api/device_mgmt_api_spec.h
+++ b/device-api/include/management-api/device_mgmt_api_spec.h
@@ -310,8 +310,11 @@
     PVT_SAMPLE_GROUP_MEMSHIRE_VM = 3, /**< Mem Shire voltage monitors, 6 words/entry: ms, noc x (cur,hi,lo) (mV) */
     PVT_SAMPLE_GROUP_IOSHIRE = 4,     /**< IO Shire, 1 entry of 15 words: ts(3), pd(3), vm noc/0p75/mxn (9) */
     PVT_SAMPLE_GROUP_PSHIRE_VM = 5,   /**< PCIe Shire voltage monitors, 1 entry of 6 words: pshr, noc x (cur,hi,lo) */
-    PVT_SAMPLE_GROUP_HILO_RESET = 6,  /**< Reset all high/low latches; no data returned */
-    PVT_SAMPLE_GROUP_MAX = 7,         /**<  */
+    PVT_SAMPLE_GROUP_HILO_RESET = 6,  /**< Reserved, not served: resetting the latches is global state */
+    PVT_SAMPLE_GROUP_TS_RAW = 7,      /**< All 35 temperature sensors (0-33 Minion Shires, 34 IO Shire), 3 words/entry:
+                                           raw 12-bit codes current, high, low. RUN_1 converts them as
+                                           T(C) = (57400 + 249400 * code / 4096 - 124700) / 1000, 0.061 C per code */
+    PVT_SAMPLE_GROUP_MAX = 8,         /**<  */
 };
 
 typedef uint8_t mdi_hart_selection_flag_e;
--- a/device-bootloaders/src/ServiceProcessorBL2/services/thermal_power_monitor.c
+++ b/device-bootloaders/src/ServiceProcessorBL2/services/thermal_power_monitor.c
@@ -13,6 +13,7 @@
 
 #include "bl2_thermal_power_monitor.h"
 #include "bl2_pvt_controller.h"
+#include <stddef.h>
 #include <string.h>
 #include "minion_configuration.h"
 #include <hwinc/hpdpll_modes_config.h>
@@ -962,6 +963,9 @@
         case PVT_SAMPLE_GROUP_PSHIRE_VM:
             status = pvt_get_pshire_vm_sample((PShire_VM_sample *)out);
             break;
+        case PVT_SAMPLE_GROUP_TS_RAW:
+            status = pvt_get_ts_raw_sample(idx, (TS_Sample *)out);
+            break;
         default:
             status = -1;
             break;
@@ -969,6 +973,13 @@
     return status;
 }
 
+/* The response stays within the 512 B that PVT_SAMPLES_MAX_WORDS is sized for, and entries are
+   written through uint16_t pointers into data[]. */
+_Static_assert(sizeof(struct device_mgmt_pvt_samples_rsp_t) <= 512, "PVT samples response over 512 B");
+_Static_assert((offsetof(struct device_mgmt_pvt_samples_rsp_t, samples) +
+                offsetof(struct pvt_samples_t, data)) % sizeof(uint16_t) == 0,
+               "PVT samples data[] not 2-byte aligned");
+
 static void pwr_svc_get_asic_pvt_samples(uint16_t tag, uint64_t req_start_time, void *buffer)
 {
     static struct device_mgmt_pvt_samples_rsp_t dm_rsp; /* ~0.5 KB, keep off the task stack */
@@ -980,6 +991,13 @@
     uint8_t first = cmd->req.first;
     uint8_t count = cmd->req.count;
 
+    /* The SQ buffer is static: a request shorter than this command would leave group, first and
+       count holding bytes of an earlier command. Serve it as an invalid group. */
+    if (cmd->command_info.cmd_hdr.size < sizeof(struct device_mgmt_pvt_samples_cmd_t))
+    {
+        group = PVT_SAMPLE_GROUP_MAX;
+    }
+
     memset(&dm_rsp.samples, 0, sizeof(dm_rsp.samples));
 
     switch (group)
@@ -990,10 +1008,10 @@
         case PVT_SAMPLE_GROUP_MEMSHIRE_VM: stride = 6; total = PVTC_MEM_SHIRE_NUM; break;
         case PVT_SAMPLE_GROUP_IOSHIRE: stride = 15; total = 1; break;
         case PVT_SAMPLE_GROUP_PSHIRE_VM: stride = 6; total = 1; break;
-        case PVT_SAMPLE_GROUP_HILO_RESET:
-            status = pvt_hilo_reset();
-            stride = 0; total = 0; count = 0;
-            break;
+        case PVT_SAMPLE_GROUP_TS_RAW: stride = 3; total = PVTC_MINION_SHIRE_NUM + 1; break;
+        /* PVT_SAMPLE_GROUP_HILO_RESET is not served by this GET: pvt_hilo_reset() clears the TS, PD
+           and VM latches on all five controllers, which the SP's own reports (minshire_high/low, the
+           voltage min/max) read too, so one reader's request would wipe every other reader's peaks. */
         default:
             status = THERMAL_PWR_MGMT_PVT_ACCESS_FAILED;
             count = 0;
--- a/device-bootloaders/src/ServiceProcessorBL2/driver/pvt_controller.c
+++ b/device-bootloaders/src/ServiceProcessorBL2/driver/pvt_controller.c
@@ -664,6 +664,44 @@
     return pvt_get_ts_data(pvt_id, ts_id, ts_sample);
 }
 
+/* Same registers as pvt_get_ts_data, without pvt_ts_conversion: the integer conversion keeps
+   whole degrees, while the sensor resolves 0.061 C. No log on a fault: a host may poll this. */
+static int pvt_get_ts_raw_data(uint8_t pvt_id, uint8_t ts_id, TS_Sample *ts_sample)
+{
+    uint32_t sample_data;
+    uint32_t hilo;
+
+    ts_sample->current = 0;
+    ts_sample->high = 0;
+    ts_sample->low = 0;
+
+    sample_data = pReg_Pvtc[pvt_id]->ts.ts_individual[ts_id].sdif_data;
+    if (TS_PD_INDIVIDUAL_IP_SDIF_DATA_SAMPLE_FAULT_GET(sample_data))
+    {
+        return ERROR_PVT_SAMPLE_FAULT;
+    }
+
+    ts_sample->current = (int16_t)TS_PD_INDIVIDUAL_IP_SDIF_DATA_SAMPLE_DATA_GET(sample_data);
+
+    hilo = pReg_Pvtc[pvt_id]->ts.ts_individual[ts_id].smpl_hilo;
+
+    ts_sample->high = (int16_t)TS_PD_INDIVIDUAL_IP_SMPL_HILO_SMPL_HI_GET(hilo);
+    ts_sample->low = (int16_t)TS_PD_INDIVIDUAL_IP_SMPL_HILO_SMPL_LO_GET(hilo);
+
+    return 0;
+}
+
+int pvt_get_ts_raw_sample(uint8_t ts_index, TS_Sample *ts_sample)
+{
+    if (ts_index > PVTC_IOSHIRE_TS_PD_ID)
+    {
+        return ERROR_PVT_MINION_ID_OUT_OF_RANGE;
+    }
+
+    return pvt_get_ts_raw_data((uint8_t)(ts_index / PVTC_TS_NUM), (uint8_t)(ts_index % PVTC_TS_NUM),
+                               ts_sample);
+}
+
 static int pvt_get_pd_data(uint8_t pvt_id, uint8_t pd_id, PD_Sample *pd_sample)
 {
     uint32_t sample_data;
--- a/device-bootloaders/src/ServiceProcessorBL2/include/bl2_pvt_controller.h
+++ b/device-bootloaders/src/ServiceProcessorBL2/include/bl2_pvt_controller.h
@@ -542,6 +542,14 @@
 */
 int pvt_get_ioshire_ts_sample(TS_Sample *ts_sample);
 
+/*! \fn int pvt_get_ts_raw_sample(uint8_t ts_index, TS_Sample *ts_sample)
+    \brief Reads one temperature sensor's raw 12-bit codes (current, high, low), unconverted
+    \param ts_index 0-33 Minion Shire, PVTC_IOSHIRE_TS_PD_ID (34) IO Shire
+    \param ts_sample Raw codes (0-4095); RUN_1: T(C) = (57400 + 249400 * code / 4096 - 124700) / 1000
+    \return Status indicating success or negative error
+*/
+int pvt_get_ts_raw_sample(uint8_t ts_index, TS_Sample *ts_sample);
+
 /*! \fn int pvt_get_min_shire_pd_sample(PVTC_MINSHIRE_e min_id, PD_Sample *pd_sample)
     \brief This function gets PD sample for Minion Shire
     \param min_id Minion Shire ID
0005-libdm-pvt-samples.patch
--- a/devicemanagement/include/deviceManagement/DeviceManagement.h
+++ b/devicemanagement/include/deviceManagement/DeviceManagement.h
@@ -98,6 +98,7 @@
   {"DM_CMD_GET_SHIRE_CACHE_CONFIG", device_mgmt_api::DM_CMD::DM_CMD_GET_SHIRE_CACHE_CONFIG},
   {"DM_CMD_SET_VMIN_LUT", device_mgmt_api::DM_CMD::DM_CMD_SET_VMIN_LUT},
   {"DM_CMD_GET_VMIN_LUT", device_mgmt_api::DM_CMD::DM_CMD_GET_VMIN_LUT},
+  {"DM_CMD_GET_ASIC_PVT_SAMPLES", device_mgmt_api::DM_CMD::DM_CMD_GET_ASIC_PVT_SAMPLES},
   {"DM_CMD_MDI_SELECT_HART", device_mgmt_api::DM_CMD::DM_CMD_MDI_SELECT_HART},
   {"DM_CMD_MDI_UNSELECT_HART", device_mgmt_api::DM_CMD::DM_CMD_MDI_UNSELECT_HART},
   {"DM_CMD_MDI_RESET_HART", device_mgmt_api::DM_CMD::DM_CMD_MDI_RESET_HART},
--- a/devicemanagement/src/DeviceManagement.cc
+++ b/devicemanagement/src/DeviceManagement.cc
@@ -516,6 +516,7 @@
     } break;
     case device_mgmt_api::DM_CMD::DM_CMD_GET_MODULE_RESIDENCY_THROTTLE_STATES:
     case device_mgmt_api::DM_CMD::DM_CMD_GET_MODULE_RESIDENCY_POWER_STATES:
+    case device_mgmt_api::DM_CMD::DM_CMD_GET_ASIC_PVT_SAMPLES:
     case device_mgmt_api::DM_CMD::DM_CMD_SET_DM_TRACE_RUN_CONTROL:
     case device_mgmt_api::DM_CMD::DM_CMD_SET_DM_TRACE_CONFIG:
     case device_mgmt_api::DM_CMD::DM_CMD_MDI_SELECT_HART:
Sources
  • et-platform 836a4ab60: pvt_controller.c 567–585 (the integer conversion), 618–664 (TS reads, linear index), 1029–1082 (the latch resets), 154 (the scrambled voltage map), 143–149 (all 35 TS channels enabled); command_dispatcher.c 105 (the static request buffer); libDM DeviceManagement.h 27–117 and DeviceManagement.cc 441–444, 482, 517–547.
  • Expected signal and lock-in numbers: the lab's model in docs/reports/data/2026-09-30-without-heatsink/imaging_calc.out; measured gaps: docs/findings/05-claims.md E52, E53 (et-soc1-prototyping).
  • Mailbox measurements: aifoundry3 card 0, release 1.3.1, BL1/BL2 0.20.0, read-only, .
  • What I asked for originally: the spatial temperature brief and CF14 in Requests for Roman.