osdev-jpでは、OS開発に有用な情報を収集し公開しています
この例は 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 + slot
は cmdheader
の先頭から 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 を返して関数を終了する。