osdev-jpでは、OS開発に有用な情報を収集し公開しています

View My GitHub Profile

ディスクのセクタを読み込む例

この例は Read hard disk sectors - OSDev.org からの引用である。 コード例はインデントを調整してある。

下のコード例は “count” 分のセクタを “starth:startl” が指すオフセットから読み取り、”buf” へ書き込むプログラムである。LBA48 モードで “port” が示すポートから読み取る。すべての PRDT エントリは 8KB の大きさまでのデータを保持できるとする。

#define ATA_DEV_BUSY 0x80
#define ATA_DEV_DRQ 0x08

BOOL read(HBA_PORT *port, DWORD startl, DWORD starth, DWORD count, WORD *buf)
{
    port->is = (DWORD)-1;    // Clear pending interrupt bits
    int spin = 0;    // Spin lock timeout counter
    int slot = find_cmdslot(port);
    if (slot == -1)
        return FALSE;

    HBA_CMD_HEADER *cmdheader = (HBA_CMD_HEADER*)port->clb;
    cmdheader += slot;
    cmdheader->cfl = sizeof(FIS_REG_H2D)/sizeof(DWORD);    // Command FIS size
    cmdheader->w = 0;    // Read from device
    cmdheader->prdtl = (WORD)((count-1)>>4) + 1;    // PRDT entries count

    HBA_CMD_TBL *cmdtbl = (HBA_CMD_TBL*)(cmdheader->ctba);
    memset(cmdtbl, 0, sizeof(HBA_CMD_TBL) +
         (cmdheader->prdtl-1)*sizeof(HBA_PRDT_ENTRY));

    // 8K bytes (16 sectors) per PRDT
    for (int i=0; i<cmdheader->prdtl-1; i++)
    {
        cmdtbl->prdt_entry[i].dba = (DWORD)buf;
        cmdtbl->prdt_entry[i].dbc = 8*1024;    // 8K bytes
        cmdtbl->prdt_entry[i].i = 1;
        buf += 4*1024;    // 4K words
        count -= 16;    // 16 sectors
    }
    // Last entry
    cmdtbl->prdt_entry[i].dba = (DWORD)buf;
    cmdtbl->prdt_entry[i].dbc = count<<9;    // 512 bytes per sector
    cmdtbl->prdt_entry[i].i = 1;

    // Setup command
    FIS_REG_H2D *cmdfis = (FIS_REG_H2D*)(&cmdtbl->cfis);

    cmdfis->fis_type = FIS_TYPE_REG_H2D;
    cmdfis->c = 1;    // Command
    cmdfis->command = ATA_CMD_READ_DMA_EX;

    cmdfis->lba0 = (BYTE)startl;
    cmdfis->lba1 = (BYTE)(startl>>8);
    cmdfis->lba2 = (BYTE)(startl>>16);
    cmdfis->device = 1<<6;    // LBA mode

    cmdfis->lba3 = (BYTE)(startl>>24);
    cmdfis->lba4 = (BYTE)starth;
    cmdfis->lba5 = (BYTE)(starth>>8);

    cmdfis->countl = LOBYTE(count);
    cmdfis->counth = HIBYTE(count);

    // The below loop waits until the port is no longer busy before issuing a new command
    while ((port->tfd & (ATA_DEV_BUSY | ATA_DEV_DRQ)) && spin < 1000000)
    {
        spin++;
    }
    if (spin == 1000000)
    {
        trace_ahci("Port is hung\n");
        return FALSE;
    }

    port->ci = 1<<slot;    // Issue command

    // Wait for completion
    while (1)
    {
        // In some longer duration reads, it may be helpful to spin on the DPS bit
        // in the PxIS port field as well (1 << 5)
        if ((port->ci & (1<<slot)) == 0)
            break;
        if (port->is & HBA_PxIS_TFES)    // Task file error
        {
            trace_ahci("Read disk error\n");
            return FALSE;
        }
    }

    // Check again
    if (port->is & HBA_PxIS_TFES)
    {
        trace_ahci("Read disk error\n");
        return FALSE;
    }

    return TRUE;
}

// Find a free command list slot
int find_cmdslot(HBA_PORT *port)
{
    // If not set in SACT and CI, the slot is free
    DWORD slots = (m_port->sact | m_port->ci);
    for (int i=0; i<cmdslots; i++)
    {
        if ((slots&1) == 0)
            return i;
        slots >>= 1;
    }
    trace_ahci("Cannot find free command list entry\n");
    return -1;
}

解説

ここからの説明は OSDev.org にはない独自のものである。

まず主要な構造体の定義を示す。

typedef struct tagHBA_CMD_TBL
{
	// 0x00
	BYTE	cfis[64];	// Command FIS

	// 0x40
	BYTE	acmd[16];	// ATAPI command, 12 or 16 bytes

	// 0x50
	BYTE	rsv[48];	// Reserved

	// 0x80
	HBA_PRDT_ENTRY	prdt_entry[1];	// Physical region descriptor table entries, 0 ~ 65535
} HBA_CMD_TBL;

typedef struct tagHBA_PRDT_ENTRY
{
	DWORD	dba;		// Data base address
	DWORD	dbau;		// Data base address upper 32 bits
	DWORD	rsv0;		// Reserved

	// DW3
	DWORD	dbc:22;		// Byte count, 4M max
	DWORD	rsv1:9;		// Reserved
	DWORD	i:1;		// Interrupt on completion
} HBA_PRDT_ENTRY;

次にソースコードの各行を説明する。

BOOL read(HBA_PORT *port, DWORD startl, DWORD starth, DWORD count, WORD *buf)

まず関数の引数を見てみる。 port は HBA Memory Registers の中の 0x100 から始まるポート情報へのポインタである。32 個あるポートから読み込みするポートを指定する。このポインタから PxCLB や PxFB を読み出して利用する。

startl, starth は読み込むセクタの開始位置を指定するのに使う。48bit LBA で指定する。

buf は読み込んだデータを書き込むメモリ領域を指定する。領域の大きさは読み込むバイト数を十分格納できる大きさであることを仮定している。 バイト配列ではなく WORD の配列なのは、先頭アドレス値がワード境界でなければならないからである(DBA レジスタの制限。詳細は後述)。

port->is = (DWORD)-1;    // Clear pending interrupt bits

割り込みステータスフラグをすべてクリアする。 IS レジスタの各ビットは、ソフトウェアから 1 を書き込むと 0 になる仕様。-1 は全ビットを 1 にする書き方である(2 の補数表現)。

int slot = find_cmdslot(port);

...

// Find a free command list slot
int find_cmdslot(HBA_PORT *port)
{
    // If not set in SACT and CI, the slot is free
    DWORD slots = (m_port->sact | m_port->ci);
    for (int i=0; i<cmdslots; i++)
    {
        if ((slots&1) == 0)
            return i;
        slots >>= 1;
    }
    trace_ahci("Cannot find free command list entry\n");
    return -1;
}

空きスロットを見つける。空きスロットかどうかは SACT レジスタおよび CI レジスタのビットが 0 かどうかで判断できる。

HBA_CMD_HEADER *cmdheader = (HBA_CMD_HEADER*)port->clb;
cmdheader += slot;

先ほど見つけた空きスロットに対応するコマンドリストの要素(Command Header 構造)へのポインタを得る。

CLB(Command List Base)レジスタからコマンドリストの先頭を得て、そこに先ほど見つけた空きスロット番号 slot を加算する。C 言語のポインタ演算の仕様から、cmdheader + slotcmdheader の先頭から sizeof(HBA_CMD_HEADER) * slot バイト先を指すポインタとなる。

cmdheader->cfl = sizeof(FIS_REG_H2D)/sizeof(DWORD);    // Command FIS size
cmdheader->w = 0;    // Read from device
cmdheader->prdtl = (WORD)((count-1)>>4) + 1;    // PRDT entries count

セクタリードするためには、読み込むための ATA コマンドをデバイスに送る必要がある。そういう場合はホストからレジスタ方向の Register FIS を使うことになっている。そこで CFL(Command FIS Length)には FIS_REG_H2D の大きさを設定する。(FIS_REG_H2D はホストからレジスタ方向の Register FIS の構造体の型名である。)

データ転送の方向はデバイスからの読み込み方向なので W ビットは 0 に設定する。また、読み込むセクタ数 count から PRDT エントリの数を計算して PRDTL へ設定する。

エントリ数の計算はこうなる。count はセクタ数である。つまり 512 バイト単位。今、各 PRD は 8KB のサイズと仮定しているので、1 つの PRD で 16 セクタを扱えることになる。したがって PRDTL = floor((count - 1) / 16) + 1。

HBA_CMD_TBL *cmdtbl = (HBA_CMD_TBL*)(cmdheader->ctba);
memset(cmdtbl, 0, sizeof(HBA_CMD_TBL) +
     (cmdheader->prdtl-1)*sizeof(HBA_PRDT_ENTRY));

Command Table 構造を 0 で初期化する。cmdheader->prdtl-1 と、PRDTL から 1 を引いている。これは HBA_CMD_TBL 構造体の定義の末尾に PRDT の 1 エントリ分が含まれていて、それと重複させないためである。

// 8K bytes (16 sectors) per PRDT
for (int i=0; i<cmdheader->prdtl-1; i++)
{
    cmdtbl->prdt_entry[i].dba = (DWORD)buf;
    cmdtbl->prdt_entry[i].dbc = 8*1024;    // 8K bytes
    cmdtbl->prdt_entry[i].i = 1;
    buf += 4*1024;    // 4K words
    count -= 16;    // 16 sectors
}
// Last entry
cmdtbl->prdt_entry[i].dba = (DWORD)buf;
cmdtbl->prdt_entry[i].dbc = count<<9;    // 512 bytes per sector
cmdtbl->prdt_entry[i].i = 1;

データの読み込みに必要となる PRDT エントリ群を設定する。

DBA レジスタに設定するアドレス値は WORD 境界である必要がある。 そのため buf を WORD の配列とし、C コンパイラおよびプログラマに対し、buf を WORD 境界に配置するよう(暗に)指示している。 そのため 8KB ずつ進めるのに 8*1024 ではなく 4*1024 を加算しており、ちょっと分かりにくいかもしれない。

I ビットを 1 に設定することで、各 PRDT エントリの受信完了時に割り込みを発生させるようにする。

// Setup command
FIS_REG_H2D *cmdfis = (FIS_REG_H2D*)(&cmdtbl->cfis);

cmdfis->fis_type = FIS_TYPE_REG_H2D;
cmdfis->c = 1;    // Command
cmdfis->command = ATA_CMD_READ_DMA_EX;

送信する FIS を構築する。今回はセクタリードなので READ_DMA_EX コマンドをデバイスに送りたい。 したがって READ_DMA_EX コマンドを持つ、ホストからデバイス方向の Register FIS を構築する。

cmdfis->lba0 = (BYTE)startl;
cmdfis->lba1 = (BYTE)(startl>>8);
cmdfis->lba2 = (BYTE)(startl>>16);
cmdfis->device = 1<<6;    // LBA mode

cmdfis->lba3 = (BYTE)(startl>>24);
cmdfis->lba4 = (BYTE)starth;
cmdfis->lba5 = (BYTE)(starth>>8);

cmdfis->countl = LOBYTE(count);
cmdfis->counth = HIBYTE(count);

starth:startl で示される 48 ビットの LBA を lba0 から lba5 に分割して書き込む。

Device レジスタのビット 6 に 1 を書き、アドレッシングモードを LBA とする。

最後に転送するセクタ数を counth:countl に設定する。

#define ATA_DEV_BUSY 0x80
#define ATA_DEV_DRQ 0x08

...

// The below loop waits until the port is no longer busy before issuing a new command
while ((port->tfd & (ATA_DEV_BUSY | ATA_DEV_DRQ)) && spin < 1000000)
{
    spin++;
}
if (spin == 1000000)
{
    trace_ahci("Port is hung\n");
    return FALSE;
}

Register FIS が構築できたので早速送信したい。ただ、その前に指定されたポートがビジー状態でなくなるのを待つ必要がある。PxTFD レジスタを見て、ビジー状態であるかデータ転送をしようとしているところなら、それらが終わるまで待つ。

port->ci = 1<<slot;    // Issue command

PxCI のスロットに対応するビットを 1 にして、HBA にコマンド送信を要請する。

// Wait for completion
while (1)
{
    // In some longer duration reads, it may be helpful to spin on the DPS bit
    // in the PxIS port field as well (1 << 5)
    if ((port->ci & (1<<slot)) == 0)
        break;
    if (port->is & HBA_PxIS_TFES)    // Task file error
    {
        trace_ahci("Read disk error\n");
        return FALSE;
    }
}

PxCI のビットが 0 になるまで、つまりコマンド実行が終了するまでループで待つ。

// Check again
if (port->is & HBA_PxIS_TFES)
{
    trace_ahci("Read disk error\n");
    return FALSE;
}

return TRUE;

最後に改めてエラービットを調べる。ループの中でエラービットを最後にチェックしてからループを抜けるまでの間にエラーが起こっても、ここで気づける。エラーがなければ成功なので、TRUE を返して関数を終了する。