Thursday, May 14, 2020

Using SpeedFan's driver to call Dell BIOS functions for fan control

SpeedFan uses a signed driver to talk to hardware for temperature measurement and fan control. The fact it's signed is important because Windows 10 makes running unsigned drivers difficult. I wasn't satisfied with how SpeedFan fights with Dell Inspiron 6400 laptop's built in Fan control. I8kfanGUI was better, but didn't have a signed driver, so I only used that in Windows 7 and earlier.

I also wasn't satisfied with fan control options in Linux. The Linux kernel provides access to Dell BIOS temperature measurement and fan control functions via the i8k driver, but attempting fan control led to the same issues. So, I first created a small fan control program in C in Linux. Getting it to run in Windows was surprisingly easy, thanks to SpeedFan's driver. First one needs to open the driver:

    sfdrv = CreateFile("\\\\.\\SpeedFan",
                       FILE_ALL_ACCESS, FILE_SHARE_READ, NULL,
                       OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

Then one can use the IOCTL to call Dell BIOS functions:

#define SFD_CALL_DELL 0x9C402424

int call_dell(uint32_t eax, uint32_t ebx)
{
    uint32_t inbuf[4], outbuf[4];
    DWORD readbytes = 0;

    inbuf[0] = eax;
    inbuf[1] = ebx;
    inbuf[2] = 0;
    inbuf[3] = 0;

    if (DeviceIoControl(sfdrv, SFD_CALL_DELL,
                        &inbuf, sizeof(inbuf),
                        &outbuf, sizeof(outbuf),
                        &readbytes, NULL) == 0 ||
        readbytes < 4) {
        return -1;
    } else {
        return outbuf[0] & 0xFF;
    }
}

I don't know if you need to read and write 16 bytes. SpeedFan always did it this way. I guess they're probably eax, ebx, ecx and edx, in little endian order of course. Certainly the first two seem to be eax and ebx. The i8k Linux driver, now part of dell-smm-hwmon.c, will show you what to do with this.

#define I8K_SMM_SET_FAN     0x01a3
#define I8K_SMM_GET_FAN     0x00a3
#define I8K_SMM_GET_TEMP    0x10a3

int get_temp(unsigned int which)
{
    return call_dell(I8K_SMM_GET_TEMP, which);
}

int get_fan(void)
{
    return call_dell(I8K_SMM_GET_FAN, 0);
}

int set_fan_real(int speed)
{
    if (speed < 0 || speed > 2) return -1;
    return call_dell(I8K_SMM_SET_FAN, (speed << 8) | 0);
}

In my program I used the following rules to reduce fighting between my fan control and the built in fan control: Read fan speed setting (not RPM, but off / low / high) before setting it. Only write it to change it (because otherwise it's pointless). It is always okay to raise fan speed. Once the program raises fan speed, it is allowed to lower it. If the program reads a speed that is higher than the last one it set, it is not allowed to lower it anymore until after the next time it raises fan speed.

I used the rohitab.com API Monitor to figure this out by watching the DeviceIoControl calls that SpeedFan was using.

Other people have also used the SpeedFan driver. Although you need to have Administrator rights to talk to the driver, the driver seems to be a bit of a security hole because even Administrator isn't supposed to have such absolute total control in Windows. Here's one example: https://github.com/SamLarenN/SpeedFan-Exploit/

Here's an example of MSR reading to get the temperature from the CPU's internal thermal sensor:

#define SFD_READ_MSR 0x9C402438

int read_msr(uint32_t msr, uint64_t *dest)
{
    DWORD readbytes = 0;
    if (DeviceIoControl(sfdrv, SFD_READ_MSR,
                        &msr, sizeof(msr),
                        dest, sizeof(*dest),
                        &readbytes, NULL) == 0 ||
        readbytes != sizeof(*dest)) {
        return -1;
    } else {
        return 0;
    }
}

int get_coretemp(void)
{
    uint64_t msrdata;
    if (read_msr(0x19C, &msrdata) < 0) return -1;
    if (msrdata & 0x80000000) {
        return 100 - ((msrdata >> 16) & 0x7F);
    } else  {
        /* Reading not vaid */
        return -1;
    }
}

I'm not using that because the CPU sensor reported by the Dell BIOS gives the temperature of the hottest core. It seems if I read the MSR directly I would have to run the code on each core I want to measure. Calling Dell BIOS once is simpler.

I hope the code didn't get mangled by blogger. I haven't written anything here in a long time. Trying to format stuff as code, in a fixed font, made it too wide, so I didn't bother.

1 comment:

Unknown said...

lot of thanks, very useful!