Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Disable APM for Hard Drive in Windows

4.75/5 (10 votes)
20 Aug 2014CPOL4 min read 39.8K   1.5K  
Setting APM level for SATA hard drive

Image 1

Introduction

This tip is about setting the Advanced Power Management level for your laptop drive in Windows. Well, some hard drives create quite annoying sounds (like hissing and clicks) due to aggressive APM system. I've bought a Hitachi HGST drive recently and it creates these nasty sounds and when you are working in silence (or sleeping near), it's very annoying. It's said that it may also damage your HDD (at least it will decrease its life). There are several programs but they either aren't open or vague ports from Linux. I was surprised by the fact that there is no such program. So the old saying is true: If you want to do something better - do it by yourself.

Background

To set APM, you have to send ATA commands to a hard drive. This must be done with DeviceIoControl and for this, you should install Windows DDK with its libraries but we are not going to develop a driver so it would be better to create own small header (atahdd.h in source) with several IOCTL constants and data types. To learn more about ATA commands, see ATA spec.

Basically, you open HDD device with CreateFile("\\.\PhisycalDrive0 ...), fill in the ATA_PASS_THROUGH_EX structure (with the data buffer if necessary) and send it to drive with DeviceIoControl(hDrive,IOCTL_ATA_PASS_THROUGH...) with an appropriate IOCTL code.
here is an example of Disable APM command:

C++
hDrive = CreateFile(dskName, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
                0, OPEN_EXISTING, 0, NULL);
BOOL rez = false;
DWORD size1, bytesRet;
ATA_PASS_THROUGH_EX aptex;

aptex.Length = sizeof ATA_PASS_THROUGH_EX;
aptex.AtaFlags = ATA_FLAGS_DATA_IN;
aptex.DataTransferLength = 512;
aptex.DataBufferOffset = sizeof ATA_PASS_THROUGH_EX;    
aptex.TimeOutValue = 1;
aptex.CurrentTaskFile.bCommandReg = ATA_SETFEATURES;
size1 = sizeof ATA_PASS_THROUGH_EX;

aptex.CurrentTaskFile.bFeaturesReg = SETFEATURES_DIS_APM;
aptex.CurrentTaskFile.bSectorCountReg = 0;    

bytesRet = 0;
rez = DeviceIoControl(hDrive, IOCTL_ATA_PASS_THROUGH, &aptex, size1, &aptex,
            sizeof ATA_PASS_THROUGH, &bytesRet, NULL);

We place the ATA command and subcommand codes to CurrTaskFile member of ATA_PASS_THROUGH_EX
structure which is an IDEREGS struct. Not all fields are necessary - it depends on ATA command. You must understand what you're doing - for example don't send WRITE SECTORS command without a reason.

C++
typedef struct _IDEREGS
{
UCHAR bFeaturesReg;
UCHAR bSectorCountReg;
UCHAR bSectorNumberReg;
UCHAR bCylLowReg;
UCHAR bCylHighReg;
UCHAR bDriveHeadReg;
UCHAR bCommandReg;
UCHAR bReserved;
} IDEREGS;

For ATA SETFEATURES command, we don't need a data buffer so we use ATA_PASS_THROUGH_EX structure without the buffer. Other ATA commands, like ATA IDENTIFYDEVICE returns data from device, so we need to provide an output buffer. In our case, we might just add 512 bytes of data to the end of ATA_PASS_THROGH_EX
structure. You must set DataTransferLength to 512 for IDENTIFY DEVICE command and DataBufferOffset to sizeof ATA_PASS_THROUGH_EX.

C++
typedef struct _ATA_PASS_THROUGH_EX_WITH_BUFFERS 
{
USHORT    Length;
USHORT    AtaFlags;
BYTE     PathId;
BYTE     TargetId;
BYTE     Lun;
BYTE     ReservedAsUchar;
ULONG     DataTransferLength;
ULONG     TimeOutValue;
ULONG     ReservedAsUlong;
ULONG_PTR DataBufferOffset;
IDEREGS     PreviousTaskFile;
IDEREGS     CurrentTaskFile;
UCHAR ucDataBuf[512];
} ATA_PASS_THROUGH_EX_WITH_BUFFERS;

And here is an example of getting device identification data from HDD:

C++
BOOL rez = false;
DWORD size1, bytesRet;
ATA_PASS_THROUGH_EX_WITH_BUFFERS aptexb;
void *ptr;

aptexb.Length = sizeof ATA_PASS_THROUGH_EX;
aptexb.AtaFlags = ATA_FLAGS_DATA_IN;
aptexb.DataTransferLength = 512;
aptexb.DataBufferOffset = sizeof ATA_PASS_THROUGH_EX;    
aptexb.CurrentTaskFile.bCommandReg = ATA_IDENTIFYDEVICE;
aptexb.CurrentTaskFile.bSectorCountReg = 1;
aptexb.TimeOutValue = 1;
size1 = sizeof ATA_PASS_THROUGH_EX;

bytesRet = 0;
rez = DeviceIoControl(hDrive, IOCTL_ATA_PASS_THROUGH, &aptexb, size1, &aptexb,
            sizeof ATA_PASS_THROUGH_EX_WITH_BUFFERS, &bytesRet, NULL);

ptr = (void *)(&aptexb.ucDataBuf);
memcpy(hddid, ptr, sizeof IDENTIFY_DEVICE_DATA);

Don't miss with sizes in DeviceIoControl according to your command. See more on MSDN:

To get current APM level, you must read device identification data from drive. The format of this struct is quite tricky (it's called IDENTIFY_DEVICE_DATA in Windows DDK) but I took it from hdreg.h file for Linux because you won't find the CurAPMValues field in Microsoft version. The struct also contains a lot of useful data (for example APM support by HDD) - see atahdd.h in project.

Points of Interest

1. The difference in data alignment between OS and Hardware.

The funny thing about some data blocks which are returned by the drive is that they use 1-byte data alignment - but Windows don't know about that. So when you're defining these structures you must also define the data alignment with #pragma pack(push,1) and back #pragma pack(pop) like this:

C++
#pragma pack(push, 1)
typedef struct _SMART_ATTRIBUTE
{
	UCHAR Id;
	USHORT StatusFlags;
	UCHAR CurrValue;
	UCHAR WorstValue;
	UCHAR RawData[6];
	UCHAR Rezerved;
} SMART_ATTRIBUTE;

typedef struct _SMART_DATA
 {
	 WORD Version;
	 SMART_ATTRIBUTE Attributes[30];
 } SMART_DATA;
#pragma pack(pop)

2. Output buffer for DeviceIoControl IOCTL_ATA_PASS_THROUGH with 'Non-Data' ATA commands.

MS DDK says that you don't need to provide input/output buffers along with the ATA_PASS_THROUGH_EX structure if an ATA command doesn't require data. So for the ATA DISABLE/SET APM commands ( which are 'Non-Data' commands) you might use ATA_PASS_THROUGH_EX structure without buffers, like this:

C++
rez = DeviceIoControl(hDrive, IOCTL_ATA_PASS_THROUGH, &aptex, sizeof ATA_PASS_THROUGH_EX, &aptex,
	sizeof ATA_PASS_THROUGH_EX, &bytesRet, NULL);

This worked fine with the Intel SATA driver, but after I uninstalled it this DeviceIoControl call fails. Now I have a MS 'ATA Channel 0' driver and must provide an output buffer even if the ATA DISABLE/SET APM commands don't get/return any data. I checked this on the four PCs and result was the same.  It seems different ATA drivers have different understanding of ATA specification.

C++
rez = DeviceIoControl(hDrive, IOCTL_ATA_PASS_THROUGH, &aptexb, sizeof ATA_PASS_THROUGH_EX, &aptexb,
	sizeof ATA_PASS_THROUGH_EX_WITH_BUFFERS, &bytesRet, NULL);

// here aptexb is a ATA_PASS_THROUGH_EX_WITH_BUFFERS structure

And now the code works fine with both MS and Intel drivers. So it would be a good idea to provide an output buffer even if the driver does't return any data. And here is the full example from code:

C++
BOOL SetAPM(HANDLE hDrive, WORD APMVal, BOOL Disable)
{
	BOOL rez = false;
	DWORD size1, bytesRet;
	ATA_PASS_THROUGH_EX_WITH_BUFFERS aptex;

	aptex.Length = sizeof ATA_PASS_THROUGH_EX;
	aptex.AtaFlags = ATA_FLAGS_DATA_IN;
	aptex.DataTransferLength = 512;
	aptex.DataBufferOffset = sizeof ATA_PASS_THROUGH_EX;	
	aptex.TimeOutValue = 3;
	aptex.CurrentTaskFile.bCommandReg = WIN_SETFEATURES;
	aptex.CurrentTaskFile.bSectorCountReg = 0;
	size1 = sizeof ATA_PASS_THROUGH_EX;

		if (Disable)
		{
			aptex.CurrentTaskFile.bFeaturesReg = SETFEATURES_DIS_APM;	
		}
		else
		{
			aptex.CurrentTaskFile.bFeaturesReg = SETFEATURES_EN_APM;		
			aptex.CurrentTaskFile.bSectorCountReg = (BYTE)APMVal;
		}

	bytesRet = 0;
// This ATA command call actually doesn't return any data in output variable aptex!
// so the 512-byte output buffer are useless.
	rez = DeviceIoControl(hDrive, IOCTL_ATA_PASS_THROUGH, &aptex, size1, &aptex,
					sizeof ATA_PASS_THROUGH_EX_WITH_BUFFERS, &bytesRet, NULL);
	
	return rez;
}

Conclusion.

Well it looks scary but I think everyone can fill several fields in struct and call one or two API functions ... The main thing is - don't send wrong commands to your HDD.

And don't forget that you need the administrative rights in Windows7-8 to open HDD device. If you want to disable APM for HDD, then you'll have to create a task which will run "apmdisable.exe -dis" with maximum rights at startup (or at logon) because after powering off the hard drive will return to its default settings. To get command-line keys for apmdisable.exe, simple run it.

Future Improvements

You may also acquire the SMART data from the HDD in a similar way (you just use an another ATA command). It would be good to check your HDD at startup to see if there are any problems. And you may turn on/off the HDD features with other commands. We even might finish with the thing like hdparm for Linux.

History

  • 08-25-2014 I've updated source and article due to serious bug with the output buffer in DeviceIoControl
       which had led to the 'Invalid Parameters' error in DeviceIoControl (see "Points of Interest" section above).
  • 19-09-2013 There was a nasty bug with data alignment in structures so I've updated source. And now it can
        read the SMART data from device.
  • 19-09-2013 First release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)