1. Motivation & Research
First, I researched how to get CPU information like temperature, voltage, and wattage. I studied Model Specific Registers (MSRs) and read Intel and AMD manuals to understand the exact register layouts for power and energy counters. Along the way, I learned that:
- Accessing MSRs requires Ring 0 privileges.
- Many legacy APIs (WinRing0, WMI WMI‑RAPL) are now deprecated or require disabling secure boot.
- There are many limitations when trying to access telemetry information on MSRs for Mobile CPUs (laptops)
So I decided to build my own KMDF driver reliable, low‑latency CPU telemetry as other solutions would have required sign testing and secure boot disabled anyway.
This is just a super summarized, high level view of the basic layout of my driver. This was developed specifically with Intel and AMD CPU wattage monitoring in mind.
While I have added extra features such as SMU mailing and EC access, most of them were unable to be utilized due to this being only deployed on mobile CPUs
+------------+ +------------+ +-----------------+
| User-mode | <----> | Kernel | <----> | MSR Registers |
| Application| IOCTL | Driver | __readmsr() | (Intel/AMD)
+------------+ +------------+ +-----------------+
2. Basic Driver Layout
At a high level, WattsKernelDriver implements the standard KMDF entry points and a dispatch table for IOCTLs:
- DriverEntry
- Create the device object & symbolic link
- Hook up IRP_MJ_CREATE / CLOSE / CLEANUP / DEVICE_CONTROL
- Register DriverUnload
- DispatchCreate
- Call
__cpuid(0)
to read the 12‑byte vendor string - Set
machine_type
to Intel, AMD, or Unknown - Query
KeQueryMaximumProcessorCountEx
to support processor groups
- Call
- DispatchDeviceControl
- Validate input/output buffer sizes
- Switch on IOCTL code → call handler for MSR, PCI, EC, SMBus, or SMU mailbox
- Complete the IRP with
IofCompleteRequest
- DriverUnload
- Delete symbolic link & device object
3. Technical Details & Edge Cases
Under the hood, each IOCTL handler deals with its own quirks:
- MSR Reads – Whitelist per‑vendor registers, pin thread to the target core, handle IRQL & affinity.
- PCI Config Space
– Call
HalGetBusDataByOffset
, cope with partial reads. - Embedded Controller (EC) – Busy‑wait loops on IBF/OBF flags; no built‑in timeout.
- AMD SMU MMIO Mailbox – Read MSR 0xC0011020 → map MMIO → write cmd/arg → poll status → read response → unmap.
- SMBus Byte Read – Map/unmap per byte; expensive but simple.
I paid special attention to error paths (e.g. zero MSR base, insufficient resources) and added DbgPrint
traces to make testing easier.
4. User‑Mode Interaction
From user space, applications simply open the device and issue IOCTLs. For example, to read package energy:
HANDLE h = CreateFile(
L"\\\\.\\WattsKernelDriver",
GENERIC_READ, 0, NULL,
OPEN_EXISTING, 0, NULL);
UINT64 energy = 0;
DWORD bytes = 0;
DeviceIoControl(
h,
IOCTL_RAPL_READ_PKG,
NULL, 0,
&energy, sizeof(energy),
&bytes, NULL);
Handle STATUS_BUFFER_TOO_SMALL
and STATUS_INVALID_PARAMETER
to ensure robust clients.
Now any C/C++, .NET, or scripting tool can pull CPU wattage with a simple IOCTL call.