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.
- Fan control driver
- GPU power controller
- Telemetry cache
- SMBus Direct telemetry
- Control & telemetry data flow
- 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:
- Brings up the PWM peripheral selected at compile time. The backend is chosen
by the project
config.hconstantFanPwm, compared against thePwmBackendenum (src/nv/fancontrol/common.h:38). ACtimerbackend routes tosys::pwm_ctimer, otherwise tosys::pwm0(src/nv/fancontrol/driver.cpp:44). - Writes each channel’s power-on default from the
config.harrayFanDefaultDuty(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–100using the localMaxPwmDuty = 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 atsrc/nv/fancontrol/driver.cpp:80). - Dispatches to the CTIMER or FlexPWM0 backend
(
src/nv/fancontrol/driver.cpp:83). For the FlexPWM0 path, fan index0maps to channel B and index1to 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 inTelemIndexMapList(project-supplied), skips items mapped to-1, andmemcpys the 4-byte value into_table(src/nv/telemetry/cache.cpp:46).get_cache(item)is the mirror image, returningInvalidItem(0xFFFFFFFF) for unmapped orMaxItemrequests (src/nv/telemetry/cache.cpp:63).set_error()/clear_error()flip the per-item alert bit; the only alert today isI2cSensorAlert(src/nv/telemetry/cache.h:30, handlers atsrc/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:
- Reads the project’s
get_end_address()— a weak default that a product overrides intelemetry.cpp(src/nv/telemetry/smb_telemetry.cpp:35); if it still equalsStartAddress, telemetry is disabled and the function returns (src/nv/telemetry/smb_telemetry.cpp:63). - Fetches the product’s
SmbDirectSensorMappingtable (src/nv/telemetry/types.h:43) via the weakget_smbtelemetry_table()(src/nv/telemetry/smb_telemetry.cpp:41). - For each mapping entry, computes its byte offset, bounds-checks it, then
switches on
TelemetryType(src/nv/telemetry/types.h:28):Temperaturepullsnsm_type3::getTemperatureTelemetry()and shifts right by 8 (src/nv/telemetry/smb_telemetry.cpp:88).PowerpullsgetPowerTelemetry()(src/nv/telemetry/smb_telemetry.cpp:97).VoltagepullsgetVoltageTelemetry()(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 atsrc/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
Cachetable feeds MCTP/NSM consumers (and derives module-level aggregates), whileSmbDirectpresents 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.