Reading an ATA Device Serial Number

04 May 1999

Introduction

This article describes the ATA (IDE drive) interface and how to read the Serial Number from Identify Device Command.

Included is an MS-DOS program in C that does it. The code is compatible with Microsoft, Borland, Watcom and DJGPP compilers. [It is a reprint of a February article linked from the home page.]

There is no equivalent of the code for Linux or for Windows. With Linux the Serial Number can be retrieved via an ioctl() call. With Windows there need be written a Kernel Mode device driver. The Linux ioctl() code would be trivial. The Windows code would be a nightmare.

References

"Infomation Technology - AT Attachment-3 Interface" (X3T10-2008D R6) Working Draft of the X3T10 Technical Committee of the Accredited Standards Committee X3.

ftp://fission.dt.wdc.com/pub/standards/ata/ata-3/
ftp://ftp.symbios.com/pub/standards/io/x3t10/drafts/ata3/

Every attempt has been made to verify the following text; errors, however, may have inadvertantly infested themselves. No gaurantees or warranties are implied. The author is responsible for spellng and grammer only.

Conventions

In this article a BYTE is 8-bits in length, a WORD is 16-bits, and a DWORD is 32-bits.

Interface

The ATA Interface is an I/O port interface for IDE hard disk drives for the IBM PC. Basically, commands are issued to the HDD by writing BYTES to a block of I/O ports, and data is read from the HDD by reading single or multiple WORDS from a data port in a serial fashion (that is, if 2 or more WORDS are to be read, the same I/O port is read 2 or more times).

There are 7 BYTE wide input I/O ports and 1 WORD wide output (or data) port. Six of the 7 input ports are for parameters and one is for the command to be issued.

ATA I/O ports are adapter based (even if motherboards have built in IDE support).

All I/O ports are contiguous for each adapter, and there can be up to two adapters, and each adapter supports two drives, or devices. Each adapter is based at a different I/O address. These addresses are:

Adapter #        Base Address

    1               0x1f0
    2               0x170

Registers

The I/O ports of the adapter are it's registers.

The 7 I/O ports that make up the parameter ports and the command port are known as the Command Block Registers, and are at offsets from the base address (starting at offset 1).

The Data Register is at offest 0 from the base address.

Offest          Description (Read / Write)

    1               Error / Features
    2               Sector Count
    3               Sector Number
    4               Cylinder Low byte
    5               Cylinder High byte
    6               Device-Head
    7               Status / Command

Notes

The Data Register appears to overlap the Error/Feature Register, and it does. What this means is that after a command is written, the Status Register is read and if it indicates and error the Data Register is invalid and the Error Register takes precedence.

The Device-Head Register is used to select the adapter device by a bit in the register:

IDE Device Base Address Device Bit 1 0x1f0 0 2 0x1f0 1 3 0x170 0 4 0x170 1

Commands

Commands cover such things as reading a sector, writing sector, formatting sectors, etc. This program only issues the Identify Device command, from which the HDD serial number (a vendor, or manufacturer, specific ASCII string) is extracted.

Before a command is issued to a HDD (or device as the specifications call a drive) from 1 to all 6 parameters ports are written first, then the command port is written; after which the data, if any, is read from the data port.

The ATA specification defines each command as filling the Command Block Registers with data. The Command Register is always written last and when written tells the IDE device to perform the command. Not all registers are used for every command. The Identify Device Command is defined as:

Register                    Value

1  Features                 n/u (not used)
2  Sector Count             n/u
3  Sector Number            n/u
4  Cylinder Low byte        n/u
5  Cylinder High byte       n/u
6  Device-Head              A0h/B0h
7  Command                  ECh
The Device-Head Register actually shares several purposes, defined as bit-fields:
 +-----------------------------------------+
 |  7  |  6  |  5  |  4  |  3   2   1   0  |  BIT
 +-----------------------------------------+
 |  1  | LBA |  1  | DEV |      HEAD       |
 +-----------------------------------------+
In the case of the ID Command the LBA and HEAD fields are not used (and set to zero):
    A0 hexidecimal = 1010 0000 binary
    B0 hexidecimal = 1011 0000 binary

Status

After a command is written the Status Register is set with the status of the result of the command. It is defined as:
 +-----------------------------------------------+
 |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
 +-----------------------------------------------+
 | BSY | DRDY| DF  | DSC | DRQ | CORR| IDX | ERR |
 +-----------------------------------------------+

   Bit          Description if true (bit == 1)

    7           device is busy
    6           device is ready
    5           device fault error
    4           device seek complete
    3           data request is ready
    2           correctable data error
    1           vendor specific
    0           error occured
The Status Register should not be read for at least 400 nano seconds after issuing a command. The device sets BSY or DRQ to one within 400ns.

Transfers

Data is transfered in parallel to or from the device to or from memory. The Data Register is a WORD read or write. The BYTE order of a WORD is most significant BYTE first.

When a DWORD is transfered the least significant WORD of the DWORD is transfered and then the most significant WORD.

Notes

Some commands do not return data in the Data Register but in one or more of the registers of the command block.

The ID Command returns 256 WORDs of data. When the ID Command is issued, the device sets the BSY bit to 1, prepares the 256 WORDs of data, sets the DRQ bit to 1, and generates an interrupt.

Code

Here is the .C source code, and .TXT source code.

Here is the <PRE></PRE> source code, but some browsers can screw with such text so get the .C file (or the .TXT file) if that is the case with yourn.

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

typedef unsigned short ushort;      /* 16-bit */
typedef unsigned long ulong;        /* 32-bit */

#pragma pack(1)

/*
 * returned identify device data format
 */

typedef struct {   /* word */
    ushort gcfg;    /* 0    general configuration */
    ushort fcyl;    /* 1    number of fixed cyls */
    ushort rcyl;    /* 2    number of removable cyls */
    ushort hds;     /* 3    number of heads */
    ushort bpt;     /* 4    bytes/track */
    ushort bps;     /* 5    bytes/sector */
    ushort spt;     /* 6    sectors/track */
    ushort isg;     /* 7    inter-sector gap */
    ushort plo;     /* 8    PLO sync field */
    ushort vsw;     /* 9    vendor status word */
    char sn[20];    /* 10   serial number */
    ushort v1;      /* 20   (vendor specific) */
    ushort v2;      /* 21 */
    ushort lbytes;  /* 22   vendor specific bytes for LONG cmds */
    char fr[8];     /* 23   firmware revision */
    char mn[40];    /* 27   model number */
    ushort v3;      /* 47 */
    ushort r1;      /* 48   (reserved) */
    ushort cap;     /* 49   capabilities */
    ushort r2;      /* 50 */
    ushort piot;    /* 51   pio data transfer cycle timing mode (8:15) */
    ushort r3;      /* 52 */
    ushort r4;      /* 53 */
    ushort lcyl;    /* 54   number of current logical cylinders */
    ushort lhd;     /* 55   number of current logical heads */
    ushort lsec;    /* 56   number of current logical sectors per track */
    ulong secs;     /* 57   current capacity in sectors */
    ushort secset;  /* 59    */
    ulong tsecs;    /* 60   total number of user addressable sectors (LBA mode) */
    ushort r5;      /* 62 */
    ushort dmam;    /* 63   multiword DMA transfer mode */
    ushort r6;      /* 64 */
    ushort mdmat;   /* 65   minimum multiword DMA transfer cycle time per word */
    ushort rdmat;   /* 66   recommended multiword DMA transfer cycle time */
    ushort mpiot;   /* 67   minimum PIO transfer cycle time without flow control */
    ushort mpioit;  /* 68   minimum PIO transfer cycle time with IORDY flow control */
    ushort r7[11];  /* 69 */
    ushort majorv;  /* 80   major version number */
    ushort minorv;  /* 81   minor version number */
    ushort supp;    /* 82   command set supported */
    ushort eh;      /* 83 */
    ushort r8[44];  /* 84 */
    ushort secur;   /* 128  security status */
    ushort v4[31];  /* 129 */
    ushort r9[96];  /* 160 */
} ID_PARMS;

/*
 * identify drive data
 */

union idbuf {
    ID_PARMS parms;
    unsigned short data[256];
};

#define ATA_PRI_M  0x1f0        /* base address of primary controller */
#define ATA_SEC_M  0x170        /* base address of secondary controller */

#define ATA_REG_DATA    0       /* data register */
#define ATA_REG_DEV     6       /* device register */
#define ATA_REG_CMD     7       /* command register */
#define ATA_REG_STATUS  7       /* status register */

#define DEV_OFF         4
#define ATA_DEV_NUM(dev) (0xA0 + ((dev)<<DEV_OFF))

#define ATA_CMD_GETP    0xec    /* get drive parameters */

#define DELAY           50

#ifdef _MSC_VER
void delay(unsigned int n);
#undef DELAY
#define DELAY           1
#endif

int main()
{
int n,s;
char serial[21];
union idbuf iddata;


#define DEVICE      ATA_DEV_NUM(0)      /* Set IDE to read here */
#define ADAPTER     ATA_PRI_M           /* Set IDE to read here */


#ifdef TEST_IF_I_DID_STRUCT_RIGHT
    if (sizeof(ID_PARMS) != 256 * sizeof(ushort)) {
        printf("\nThe ID_PARMS struct is of the wrong size! ");
        printf("(%u)\n",sizeof(ID_PARMS) / sizeof(ushort));
        exit(1);
    }
#endif

    /*
     * This code is hardly ideal. It was written to understand
     * how to interface with an IDE device. This code does work
     * (for my machine anyway).
     */

    outp(ADAPTER + ATA_REG_DEV, DEVICE);
    delay(DELAY);
    outp(ADAPTER + ATA_REG_CMD, ATA_CMD_GETP);
    delay(DELAY);

    s = inp(ADAPTER + ATA_REG_STATUS);

    for (n = 0; n < 256; n++)
        iddata.data[n] = (unsigned short)inpw(ADAPTER + ATA_REG_DATA);

    swab(iddata.parms.sn,serial,20);
    serial[20] = '\0';

    printf("\"%s\"",serial);

    return 0;
}

#ifdef _MSC_VER

/*
 * 19 millisecond resolution delay
 */

void delay(unsigned int n)
{
static unsigned int __far * volatile const pitick =    /* pointer const */
      (unsigned int __far *)0x0000046cL;               /* object volatile */
register unsigned int u;

   u = n + *pitick;                 /* add ticks to current counter */
   if (u < n)                    /* if the addition overflowed */
   {
      while (*pitick > 1)        /* wait for counter to overflow */
         ;
      u = n + *pitick;              /* and start again */
   }

   while (*pitick < u)
        ;
}

#endif