Having done some research and using the info from the answers on this page, here's my implementation using C WinAPIs for Windows 7 and later:
//Open drive as such: "\\?\PhysicalDriveX" where X is the drive number //INFO: To get drive number from a logical drive letter, check this method: // (But keep in mind that a single logical drive, or a volume, // can span across several physical drives, as a "spanned volume.") // http://stackoverflow.com/a/11683906/843732 #include <WinIoCtl.h> #include <Ntddscsi.h> DWORD bytesReturned; //As an example, let's test 1st physical drive HANDLE hDevice = ::CreateFile(L"\\\\?\\PhysicalDrive0", GENERIC_READ | GENERIC_WRITE, //We need write access to send ATA command to read RPMs FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if(hDevice != INVALID_HANDLE_VALUE) { //Check TRIM -- should be Y for SSD _tprintf(L"TRIM="); STORAGE_PROPERTY_QUERY spqTrim; spqTrim.PropertyId = (STORAGE_PROPERTY_ID)StorageDeviceTrimProperty; spqTrim.QueryType = PropertyStandardQuery; bytesReturned = 0; DEVICE_TRIM_DESCRIPTOR dtd = {0}; if(::DeviceIoControl(hDevice, IOCTL_STORAGE_QUERY_PROPERTY, &spqTrim, sizeof(spqTrim), &dtd, sizeof(dtd), &bytesReturned, NULL) && bytesReturned == sizeof(dtd)) { //Got it _tprintf(L"%s", dtd.TrimEnabled ? L"Y" : L"N"); } else { //Failed int err = ::GetLastError(); _tprintf(L"?"); } //Check the seek-penalty value -- should be N for SSD _tprintf(L", seekPenalty="); STORAGE_PROPERTY_QUERY spqSeekP; spqSeekP.PropertyId = (STORAGE_PROPERTY_ID)StorageDeviceSeekPenaltyProperty; spqSeekP.QueryType = PropertyStandardQuery; bytesReturned = 0; DEVICE_SEEK_PENALTY_DESCRIPTOR dspd = {0}; if(::DeviceIoControl(hDevice, IOCTL_STORAGE_QUERY_PROPERTY, &spqSeekP, sizeof(spqSeekP), &dspd, sizeof(dspd), &bytesReturned, NULL) && bytesReturned == sizeof(dspd)) { //Got it _tprintf(L"%s", dspd.IncursSeekPenalty ? L"Y" : L"N"); } else { //Failed int err = ::GetLastError(); _tprintf(L"?"); } //Get drive's RPMs reading -- should be 1 for SSD //CODE SOURCE: https://emoacht.wordpress.com/2012/11/06/csharp-ssd/ _tprintf(L", RPM="); ATAIdentifyDeviceQuery id_query; memset(&id_query, 0, sizeof(id_query)); id_query.header.Length = sizeof(id_query.header); id_query.header.AtaFlags = ATA_FLAGS_DATA_IN; id_query.header.DataTransferLength = sizeof(id_query.data); id_query.header.TimeOutValue = 5; //Timeout in seconds id_query.header.DataBufferOffset = offsetof(ATAIdentifyDeviceQuery, data[0]); id_query.header.CurrentTaskFile[6] = 0xec; // ATA IDENTIFY DEVICE bytesReturned = 0; if(::DeviceIoControl(hDevice, IOCTL_ATA_PASS_THROUGH, &id_query, sizeof(id_query), &id_query, sizeof(id_query), &bytesReturned, NULL) && bytesReturned == sizeof(id_query)) { //Got it //Index of nominal media rotation rate //SOURCE: http://www.t13.org/documents/UploadedDocuments/docs2009/d2015r1a-ATAATAPI_Command_Set_-_2_ACS-2.pdf // 7.18.7.81 Word 217 //QUOTE: Word 217 indicates the nominal media rotation rate of the device and is defined in table: // Value Description // -------------------------------- // 0000h Rate not reported // 0001h Non-rotating media (e.g., solid state device) // 0002h-0400h Reserved // 0401h-FFFEh Nominal media rotation rate in rotations per minute (rpm) // (e.g., 7 200 rpm = 1C20h) // FFFFh Reserved #define kNominalMediaRotRateWordIndex 217 _tprintf(L"%d", (UINT)id_query.data[kNominalMediaRotRateWordIndex]); } else { //Failed int err = ::GetLastError(); _tprintf(L"?"); } _tprintf(L"\n"); ::CloseHandle(hDevice); }
In case you don't have driver DDK includes, here're some definitions:
#ifndef StorageDeviceTrimProperty #define StorageDeviceTrimProperty 8 #endif #ifndef DEVICE_TRIM_DESCRIPTOR typedef struct _DEVICE_TRIM_DESCRIPTOR { DWORD Version; DWORD Size; BOOLEAN TrimEnabled; } DEVICE_TRIM_DESCRIPTOR, *PDEVICE_TRIM_DESCRIPTOR; #endif #ifndef StorageDeviceSeekPenaltyProperty #define StorageDeviceSeekPenaltyProperty 7 #endif #ifndef DEVICE_SEEK_PENALTY_DESCRIPTOR typedef struct _DEVICE_SEEK_PENALTY_DESCRIPTOR { DWORD Version; DWORD Size; BOOLEAN IncursSeekPenalty; } DEVICE_SEEK_PENALTY_DESCRIPTOR, *PDEVICE_SEEK_PENALTY_DESCRIPTOR; #endif struct ATAIdentifyDeviceQuery { ATA_PASS_THROUGH_EX header; WORD data[256]; };
Lastly, conclusion of my tests.
I have several Samsung SSDs connected via a SATA cable, and one PCIe SSD drive that is connected directly to the logic board using PCIe slot. I also have one large internal Western Digital HDD (spinning drive) that is also connected via a SATA cable, and a couple of external spinning HDDs.
Here's what I get for them:
Samsung SSD 256GB: TRIM=Y, seekPenalty=N, RPM=1 Samsung SSD 500GB: TRIM=Y, seekPenalty=N, RPM=1 PCIs SSD: TRIM=Y, seekPenalty=?, RPM=0 Internal WD HDD: TRIM=N, seekPenalty=?, RPM=0 External WD HDD: TRIM=?, seekPenalty=?, RPM=? External Cavalry HDD: TRIM=?, seekPenalty=Y, RPM=?
So as you see, in my case, the only parameter that is correct for all 6 drives is TRIM. I'm not saying that it will be in your case as well. It's just my finding with the drives that I own.