Controlling a Mac’s fans (AppleSMC)

Since the iMac found its way onto my desk I’ve been bothered with the heat generated when playing Diablo 3 (or possibly any other game) – just as with the MacBook Pro. The only difference: The MacBook’s fans were howling to stop the torment, the iMac just swallows the pain it seems. Don’t get me wrong, the system is not overheating and temperature is well within its limits. In fact, the CPU doesn’t even really heat up. It’s more the graphics chip and the power supply. But for me as a former PC user who built all the PCs himself and always had a good (and mostly quiet) cooling this is just a bit unusual.

I know the iMac can do some better cooling if it would just use its fans a bit more. When increasing the fan speeds the flow of air isn’t even that loud. Absolutely nothing compared to the MacBook…
So I thought: „There must be some way to exert influence on the cooling system“. And there is. I found a great tool name iStat Menus which can monitor quite everything built into the Mac and even lets you specify a custom fan speed. I like! Unfortunately it costs 16$. Well, being a programmer I thought I can built a poor-man’s fan control tool myself, having already done so with a Netbook, and save the money. The difference however, for the Netbook I found some information on the ACPI implementation which allowed insight on how to interact with the device. The result was some low-level I/O port communication. On the other hand there’s the Apple device with no docs on that topic at all (or so very well hidden that even Google can’t find them). As far as I found out Apple doesn’t even want anybody to manipulate the cooling system. To come to the point: I failed. I’ll buy myself a licence of iStat.
Despite of that I’ll still elaborate on what I have learned researching that topic. At least I know how to read the sensor data. I was just not able to set new values for the fans.
Since the Intel switch – I think – Apple includes a „System Management Controller“ („SMC“ for short) with their Macs. That’s the target which to aim at. The tool of choice is Apple’s IOKit library.
The rough procedure is like this:

  • Connect to the kernel via an I/O port
  • Query for the AppleSMC driver
  • Open a connection to the SMC
  • Query for meta-information about the value you want to read
  • Query for the value’s data
  • Close the connection

Doesn’t sound that hard does it?
Just as a note: the upcoming code is not fully bloated with error checking. It contains the very basic tests where necessary for correct program flow, but nothing more. It’s just to demonstrate what needs to be done.
Here’s the skeleton (king?) for the application. This is just basic stuff needed for any kind of application-driver communication (read the section on „Device Discovery and Notification“).

#include

io_connect_t
 connect_smc()
 {
 io_service_t smc = IOServiceGetMatchingService(
 kIOMasterPortDefault,
 IOServiceMatching("AppleSMC"));
 if (IO_OBJECT_NULL == smc)
 return IO_OBJECT_NULL;

io_connect_t connection = IO_OBJECT_NULL;
 IOReturn result = IOServiceOpen(smc, mach_task_self(), 1, &connection);
 return (kIOReturnSuccess == result ? connection : IO_OBJECT_NULL);
 }

void
 disconnect_smc(io_connect_t connection)
 {
 IOServiceClose(connection);
 }

int
 main(int argc, char* argv[])
 {
 io_connect_t connection = connect_smc();
 if (IO_OBJECT_NULL != connection) {
 disconnect_smc(connection);
 }

return 0;
 }

Well, that’s all there is to connect to the System Management Controller. The tricky part is getting data from it. Here’s where I’ve spent hours of searching the web. I had sample code from a quite popular tool (so popular I forgot the name… I still found part of its source which is used in many other tools as well) but it uses some structs but no explanation why they are like they are. I played with them for a while and discovered that removing just on line would break the communication. That means that this is a predefined format and not just free will of the original author of that code. Finding the answer to my question „Where does that stuff come from“ took a lot of time until I finally stumbled over some sample code from Apple that demonstrates reading some other stuff from the SMC. This code had the releaving comments:

/* Do not modify - defined by AppleSMC.kext */

Helleluja Finally! These structs are defined by the SMC driver. A well hidden info.
So here they are, directly from the sample. Just put them in a header file.

enum {
    kSMCSuccess = 0,
    kSMCError   = 1
};
enum {
 kSMCUserClientOpen = 0,
 kSMCUserClientClose = 1,
 kSMCHandleYPCEvent = 2,
 kSMCReadKey = 5,
 kSMCWriteKey = 6,
 kSMCGetKeyCount = 7,
 kSMCGetKeyFromIndex = 8,
 kSMCGetKeyInfo = 9
 };

typedef struct SMCVersion
 {
 unsigned char major;
 unsigned char minor;
 unsigned char build;
 unsigned char reserved;
 unsigned short release;

} SMCVersion;

typedef struct SMCPLimitData
 {
 uint16_t version;
 uint16_t length;
 uint32_t cpuPLimit;
 uint32_t gpuPLimit;
 uint32_t memPLimit;

} SMCPLimitData;

typedef struct SMCKeyInfoData
 {
 IOByteCount dataSize;
 uint32_t dataType;
 uint8_t dataAttributes;

} SMCKeyInfoData;

typedef struct {
 uint32_t key;
 SMCVersion vers;
 SMCPLimitData pLimitData;
 SMCKeyInfoData keyInfo;
 uint8_t result;
 uint8_t status;
 uint8_t data8;
 uint32_t data32;
 uint8_t bytes[32];
 } SMCParamStruct;

The important parts that we need to use are:

  • key – What kind of value is accessed
  • keyInfo
    • dataType – The data type of the data returned
    • dataSize – The number of bytes for that key’s data
  • data8 – The kind of operation we want to perform for key
    • This is either getting meta info about a key or retrieving data for a key.
  • Bytes – The data

Time to use them, right? So here’s some code. Just add these two functions between disconnect_smc() and main(). With the short description of the struct the code should not mystify you.

IOReturn
read_key(
         io_connect_t connection, 
         uint32_t     key, 
         uint8_t*     out, 
         uint8_t*     size)
{
    SMCParamStruct input;
    SMCParamStruct output;
bzero(out, *size);
 bzero(&input, sizeof(SMCParamStruct));
 bzero(&output, sizeof(SMCParamStruct));

// Advise the SMC to get meta-info about a key
 input.data8 = kSMCGetKeyInfo;
 input.key = key;

IOReturn result = call_smc(
 connection,
 kSMCHandleYPCEvent,
 &input,
 &output);
 if (kSMCSuccess != result)
 return result;

// Get the value for the key
 input.data8 = kSMCReadKey;
 input.key = key;
 input.keyInfo.dataSize = output.keyInfo.dataSize;

bzero(&output, sizeof(SMCParamStruct));

result = call_smc(connection, kSMCHandleYPCEvent, &input, &output);
 if (kSMCSuccess != result)
 return result;

// Copy the data to out
 if (*size > input.keyInfo.dataSize)
 *size = input.keyInfo.dataSize;

memcpy(out, output.bytes, *size);
 return kSMCSuccess;
 }

The first call_smc() queries the driver for the meta-information (input.data8 = kSMCGetKeyInfo;) whereas the second call finally gets the current value for key (input.data8 = kSMCReadKey;). Some things like kSMCHandleYPCEvent just have to be swallowed as necessary because that’s just the way to talk to SMC.

IOReturn
call_smc(
         io_connect_t    connection, 
         int             which,
         SMCParamStruct* input,
         SMCParamStruct* output)
{
    IOReturn result = IOConnectCallMethod(
                                          connection, 
                                          kSMCUserClientOpen, 
                                          NULL, 
                                          0, 
                                          NULL, 
                                          0, 
                                          NULL, 
                                          NULL, 
                                          NULL, 
                                          NULL);
    if (kIOReturnSuccess != result)
        return result;

size_t outSize = sizeof(SMCParamStruct);
 result = IOConnectCallStructMethod(
 connection,
 which,
 input,
 sizeof(SMCParamStruct),
 output,
 &outSize);
 IOConnectCallMethod(
 connection,
 kSMCUserClientClose,
 NULL,
 0,
 NULL,
 0,
 NULL,
 NULL,
 NULL,
 NULL);
 return result;
 }

As far as I’ve noticed you could omit the IOConnectCallMethod() calls and just work with the IOConnectCallStructMethod() function. But since this is in the Apple sample code I think it has its rights. I can only guess that they prepare a kind of a transaction when used with kSMCUserClientOpen and end the „transaction“ with kSMCUserClientClose. I haven’t investigated any further to be sure about this.
Now this looks really nice but where do we get the keys from which to read data? Well, that’s part of the SMC and not easy to find. Luckily I did find something although it does not seem to be the most recent version of the list. The document states v1.30f1 and my iMac reports v1.71f22 for the SMC. The keys I’ve tried and I was interested in did work, however. Unfortunately the datatypes specified for the keys are not explained anywhere so I have to relate to the code I’ve found. Some are obvious though.
Taking small steps the first thing I wanna have is the number of fans. From this value we can then query each fan individually. The next function does all the rest and will be expanded as we move on.

void
print_fans(io_connect_t connection)
{
    uint8_t buf[1];
    uint8_t size = 1;
    IOReturn result = read_key(connection, 'FNum', buf, &size);
    if (kSMCSuccess != result) {
        printf("Could not read number of fans");
        return;
    }

int fans = 0;
 memcpy(&fans, buf, 1);

printf("Number of fans found: %d\n", fans);
 }

Now if you look up „FNum“ in the list of keys you can see its datatype, the size of data returned and a short description.

Key Type  Size Description
FNum ui8 1 Number of supported fans

Before any more can be implemented a few helper functions are needed. Two convert the byte data into integer or float values respectively and the third just hides some code to create keys with varying contents. The byte conversion are taken from the source code I linked some lines further up and reduced to what is needed for this example.

uint32_t
byte_to_int(char *str, int size)
{
    uint32_t total = 0;

for (int c = 0; c < size; c++) {
 total += str[c] << (size - 1 - c) * 8;
 }
 return total;
 }

float
 byte_to_float(uint8_t* str, int size)
 {
 float total = 0;

for (int i = 0; i < size; i++) {
 if (i == (size - 1))
 total += (str[i] & 0xff) >> 2;
 else
 total += str[i] << (size - 1 - i) * (8 - 2);
 }

return total;
 }

uint32_t
 create_key(const char* str, int index)
 {
 char keyStr[4];
 sprintf(keyStr, "F%dAc", index);
 return byte_to_int(keyStr, 4);
 }

Now with that in place we can actually read the fan speed of all our fans. Insert following code into print_fans(), right before the closing curly brace.

uint32_t key = 0;
for (int c = 0; c < fans; c++) {
    printf("\nFan #%d:\n", c);

size = 2;
 read_key(connection, create_key("F%dAc", c), buf, &size);
 printf(" Current speed : %.0f\n", byte_to_float(buf, size));
 }

Last but not least, to demonstrate how to read temperatures, I show a method to get the CPU temp. The only interesting thing about this code is how to convert the value returned to something human readable.

void
print_cpu_temp(io_connect_t connection)
{
    uint8_t buf[2];
    uint8_t size = 2;
    read_key(connection, 'TC0H', buf, &size);

int temp = (buf[0] * 256 + buf[1]);

printf("\nCPU heatsink temp: %d\n", temp / 256);
 }

Now put the print_fans(connection) and print_cpu_temp(connection) into your main(), run it and be happy.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s