Fan, power & telemetry

This is the actuation-and-reporting core of Platform Control & Telemetry: a thin PWM driver that applies fan duty, a task scaffold for GPU power compliance, and the telemetry cache that serves sensor readings to external management hosts.

  1. Fan control driver
    1. Initialization and PWM backend
    2. Setting duty and reading it back
  2. GPU power controller
    1. Task lifecycle
    2. Compliance loop (placeholder)
  3. Telemetry cache
    1. Layout and item IDs
    2. Reading and writing entries
    3. Derived values on refresh
  4. SMBus Direct telemetry
  5. Control & telemetry data flow
  6. See also

Fan control driver

nv::fancontrol::Driver (src/nv/fancontrol/driver.h:38) is a stateless, all-static class — the doc comment calls it “Direct PWM hardware control … without task overhead” (src/nv/fancontrol/driver.h:33). It is the actuator layer: it converts a commanded duty-cycle percentage into a hardware PWM write. (Interpretation: the closed-loop thermal decision — what duty a given temperature should map to — lives in higher-level NSM/thermal logic outside this module; this driver just applies the number it is handed.)

Initialization and PWM backend

Driver::init() (src/nv/fancontrol/driver.cpp:41) does two things:

  1. Brings up the PWM peripheral selected at compile time. The backend is chosen by the project config.h constant FanPwm, compared against the PwmBackend enum (src/nv/fancontrol/common.h:38). A Ctimer backend routes to sys::pwm_ctimer, otherwise to sys::pwm0 (src/nv/fancontrol/driver.cpp:44).
  2. Writes each channel’s power-on default from the config.h array FanDefaultDuty (src/nv/fancontrol/driver.cpp:52).

Because the branch is an if constexpr, only the selected backend’s code is compiled in — there is no runtime dispatch cost.

Setting duty and reading it back

The public entry points are set_fan_pwm() (src/nv/fancontrol/driver.cpp:57) and stop_fan_pwm(), which is just a duty of 0 (src/nv/fancontrol/driver.cpp:62). Both funnel into the private set_pwm_duty_cycle() (src/nv/fancontrol/driver.cpp:67), which:

  • Clamps the duty to 0–100 using the local MaxPwmDuty = 100 (src/nv/fancontrol/driver.cpp:70), and rejects out-of-range fan indices (src/nv/fancontrol/driver.cpp:75).
  • Caches the commanded value into the shared-BSS array g_fan_duty (src/nv/fancontrol/driver.cpp:38, written at src/nv/fancontrol/driver.cpp:80).
  • Dispatches to the CTIMER or FlexPWM0 backend (src/nv/fancontrol/driver.cpp:83). For the FlexPWM0 path, fan index 0 maps to channel B and index 1 to channel A (src/nv/fancontrol/driver.cpp:91).

There is no duty sensor in hardware, so get_fan_pwm() simply returns the last-commanded value from the cache (src/nv/fancontrol/driver.cpp:102; the contract is spelled out at src/nv/fancontrol/driver.h:64). fan_count() returns the config-defined FanNum (src/nv/fancontrol/driver.cpp:107).

Fact vs. interpretation: everything above is direct from the driver source. The claim that this module is “only the actuator” is interpretation, based on the absence of any temperature input, timer, or feedback term in the file.


GPU power controller

The GPU power controller is an RTOS task scaffold, currently a stub. The header names it the “P4476 Coridelia Power Compliance Module” with functionality “To be defined in next fw release” (src/nv/gpu_pwr_controller/gpu_pwr_controller.h:27).

Task lifecycle

nv::gpu_pwr_controller::Task (src/nv/gpu_pwr_controller/task.h:26) derives from nv::ipc::Task and registers under TaskId::GpuPwrController (src/nv/gpu_pwr_controller/task.cpp:24). The supervisor creates it via the static Task::make() (src/nv/gpu_pwr_controller/task.cpp:30), which pins a stack of at least 2048 bytes and installs Task::entrypoint (src/nv/gpu_pwr_controller/task.cpp:33). entrypoint() casts the params back to the task, calls start(), then suspend() (src/nv/gpu_pwr_controller/task.cpp:41). start() hands control to the owned PowerCompliance manager’s main() (src/nv/gpu_pwr_controller/task.cpp:52).

Compliance loop (placeholder)

PowerCompliance::main() (src/nv/gpu_pwr_controller/gpu_pwr_controller.cpp:34) signals boot completion via set_task_booted(BootedEventBits::GpuPwrCtrl) (src/nv/gpu_pwr_controller/gpu_pwr_controller.cpp:37), then enters a while (true) loop that waits on EventId::GpuPwrCtrlEvent with a 5-second timeout (src/nv/gpu_pwr_controller/gpu_pwr_controller.cpp:42). The body is explicitly a placeholder — the source carries the comment “power compliance code will come here later” (src/nv/gpu_pwr_controller/gpu_pwr_controller.cpp:43).

Documenting the current baseline: the task is wired into the supervisor and boot-event machinery, but performs no power regulation yet. See SoC power smoothing for the power path that is implemented.


Telemetry cache

nv::telemetry::Cache (src/nv/telemetry/cache.h:27) is a singleton (Cache::inst(), src/nv/telemetry/cache.cpp:29) holding two structures: a byte-array telemetry _table and a boolean _alert_table (src/nv/telemetry/cache.h:56). init() fills the table with InvalidData (0xFF) and clears all alerts (src/nv/telemetry/cache.cpp:34).

Layout and item IDs

Telemetry items are enumerated by TelemId (src/nv/telemetry/utils.h:30) — GPU temps/powers, module temp/power, internal temp, GPIO word, plus CX8/QM4 temps. Each Value is a uint32_t, and the _table is sized CacheSize * sizeof(Value) bytes, where CacheSize is the number of TelemId entries (src/nv/telemetry/cache.h:36). Producers convert NSM Type-3 sensor IDs to TelemId via the compile-time maps TempSensorIdToTelemIdMapping (src/nv/telemetry/utils.h:50) and PowerSensorIdToTelemIdPowerMapping (src/nv/telemetry/utils.h:71).

Reading and writing entries

  • set_cache(item, value) looks up the item’s slot index in TelemIndexMapList (project-supplied), skips items mapped to -1, and memcpys the 4-byte value into _table (src/nv/telemetry/cache.cpp:46).
  • get_cache(item) is the mirror image, returning InvalidItem (0xFFFFFFFF) for unmapped or MaxItem requests (src/nv/telemetry/cache.cpp:63).
  • set_error() / clear_error() flip the per-item alert bit; the only alert today is I2cSensorAlert (src/nv/telemetry/cache.h:30, handlers at src/nv/telemetry/cache.cpp:83).

Derived values on refresh

get_table() calls refresh() before handing back a copy (src/nv/telemetry/cache.cpp:40), so consumers always see freshly derived aggregates. refresh() (src/nv/telemetry/cache.cpp:99) computes three things, each guarded by whether its slot is mapped on this project:

Derived item Rule Source
ModulePower sum of GPU1 + GPU2 power (invalid treated as 0; both invalid → invalid) src/nv/telemetry/cache.cpp:102
MaxModuleTemp max of the two TMP451 module temps src/nv/telemetry/cache.cpp:117
Gpio packs live GPIO pin reads plus two synthesized status bits src/nv/telemetry/cache.cpp:133

The GPIO word is assembled by reading each entry of the project GpioTelemetryTable through nv::gpio::Driver::read() (src/nv/telemetry/cache.cpp:136), then overlaying an internal-temp warning bit — asserted when InternalTemp exceeds TempThreshold (105 * 0.8, src/nv/telemetry/cache.h:40; logic at src/nv/telemetry/cache.cpp:148) — and an I2C sensor-alert bit taken from the alert table (src/nv/telemetry/cache.cpp:151).


SMBus Direct telemetry

Hosts that speak SMBus rather than MCTP/NSM read a separate, byte-addressable register window served by nv::smb_telemetry::SmbDirect (src/nv/telemetry/smb_telemetry.h:41). The window begins at register StartAddress = 0x50 (src/nv/telemetry/smb_telemetry.h:51) and the backing buffer cache_ is 160 bytes, pre-filled with 0xFF (src/nv/telemetry/smb_telemetry.h:55).

SmbDirect::refresh_cache() (src/nv/telemetry/smb_telemetry.cpp:58) rebuilds that buffer:

  1. Reads the project’s get_end_address() — a weak default that a product overrides in telemetry.cpp (src/nv/telemetry/smb_telemetry.cpp:35); if it still equals StartAddress, telemetry is disabled and the function returns (src/nv/telemetry/smb_telemetry.cpp:63).
  2. Fetches the product’s SmbDirectSensorMapping table (src/nv/telemetry/types.h:43) via the weak get_smbtelemetry_table() (src/nv/telemetry/smb_telemetry.cpp:41).
  3. For each mapping entry, computes its byte offset, bounds-checks it, then switches on TelemetryType (src/nv/telemetry/types.h:28):
    • Temperature pulls nsm_type3::getTemperatureTelemetry() and shifts right by 8 (src/nv/telemetry/smb_telemetry.cpp:88).
    • Power pulls getPowerTelemetry() (src/nv/telemetry/smb_telemetry.cpp:97).
    • Voltage pulls getVoltageTelemetry() (src/nv/telemetry/smb_telemetry.cpp:103).
    • Anything else routes to the weak refresh_product_specific_telemetries() hook (src/nv/telemetry/smb_telemetry.cpp:49, dispatched at src/nv/telemetry/smb_telemetry.cpp:113).

The host then reads the assembled bytes back through SmbDirect::get_cache() (src/nv/telemetry/smb_telemetry.cpp:119). Multi-byte integers are little-endian; the shared decoder is nv::telemetry::buffer_to_uint32() (src/nv/telemetry/utils.cpp:21).

Interpretation: the two telemetry paths are deliberately separate — the Cache table feeds MCTP/NSM consumers (and derives module-level aggregates), while SmbDirect presents a flat register map for legacy SMBus hosts. Both ultimately source the same NSM Type-3 sensors.


Control & telemetry data flow

flowchart TD
    subgraph SRC["Sensor sources"]
        NSM["NSM Type-3 sensors<br/>getTemperature / getPower / getVoltage"]
        PINS["Board GPIO pins"]
    end

    subgraph CACHE["nv::telemetry::Cache (singleton)"]
        TBL["_table (uint32 slots)<br/>set_cache / get_cache"]
        REF["refresh(): derive ModulePower,<br/>MaxModuleTemp, GPIO status bits"]
    end

    subgraph SMB["nv::smb_telemetry::SmbDirect"]
        RC["refresh_cache(): walk sensor map"]
        BUF["cache_ window @ 0x50, 160 bytes"]
    end

    subgraph ACT["Actuation"]
        THERM["Thermal / NSM control logic<br/>(out of scope)"]
        FAN["fancontrol::Driver<br/>set_fan_pwm -> PWM backend"]
    end

    NSM --> TBL
    PINS --> REF
    TBL --> REF
    REF --> GT["get_table()"]
    GT --> HOST1["MCTP / NSM host"]

    NSM --> RC
    RC --> BUF
    BUF --> HOST2["SMBus host"]

    NSM --> THERM
    THERM --> FAN
    FAN --> HW["Fan PWM hardware"]

Diagram: sensor readings fan out to both telemetry paths; a separate control path drives the fan actuator. The GPU power-compliance task is not shown because it is currently a stub.


See also

  • Monitoring & data — the sensor and monitoring subsystems that feed this cache (voltage monitor, perf monitor, power smoothing).
  • Bus drivers — the I2C/SMBus transport layer that carries SMBus Direct reads.
  • Architecture — task model and the IPC/supervisor framework that hosts these tasks.

Built with Just the Docs. Source-traced against NVIDIA/OpenSMA at 9b183d3 (v02.0020.0000). Methodology under appendices/trace-methodology.

This site uses Just the Docs, a documentation theme for Jekyll.