Detecting SSD in Windows

You can use the Microsoft WMI Class MSFT_PhysicalDisk. The mediatype of 4 is SSD and SpindleSpeed will be 0.


I believe you are using the wrong tool. Instead of making assumptions based on a drive being an SSD you should make your code work well with slow and fast drives, for example by loading the essential objects first and the rest later. In three years the invention of [...] may make regular hard drives faster than SSDs which would break your code. Going purely based on speed will also work for RAM discs, NFS, USB3.0-sticks and other stuff you didn't or cannot thing about.

EDIT: A HDD is not actually the same as a slow SSD. While they are both fast at reading and writing a HDD needs significant time for seeking. It thus makes sense to use two different access strategies: picking the important data via random access for the SSD and sequentially reading for the HDD. You will probably get away with only implementing the sequential strategy as that should still work ok with SSDs. It makes more sense to check for a HDD instead of a SSD though, because you need to treat the HDD special while SSD, RAMdisc, NFS and so on should not suffer from seek times and can thus be treated the same.


Yes, there is a high chance of determining whether a drive is an SSD. SSD typically support the TRIM command, so I would check to see if the drive supports the TRIM command.

In Windows, you can use IOCTL_STORAGE_QUERY_PROPERTY to get the DEVICE_TRIM_DESCRIPTOR structure which will tell you if TRIM is enabled.

If you really know what you're doing, you can get the raw IDENTIFY DEVICE package, and interpret the data yourself. For SATA drives it would be word 169 bit 0.


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.