無限遠まで突撃中

ネット日記書きの徒然。

AHCI奮闘記 - Read/Write編

AHCIの読み出し書き込みに成功した

こんにちは、突撃隊です。 AHCIを用いたSATAディスクへの読み書き(QEMU上ですが)が無事成功したので、メモを残しておこうかと思います。

実機確認はまだできていませんので話半分にお願いします。 実機確認できました!

以下の記事 totsugekitai.hatenablog.com

で解説されている初期化を行ったものとして以降は解説していきます。

また、一挙一足説明していると時間が足りないので、そこらへん足りない部分は自分のリポジトリを見て下さい。

github.com

仕様書の該当部分を読む

AHCIの仕様書の 5.5 System Software Rules に記述されているので、それのとおりやればできます。

でも落とし穴が多いので解説します。

まずはポートのセッティング

ポートの設定をまずはします。 ここは仕様書の 5.5 には載っておらず、実装の上で必要になったので追加していることをお伝えします。

以下のような処理を行います。

static inline void start_cmd(HBA_PORT *port)
{
    puts_serial("start_cmd start\n");
    port->cmd &= 0xfffffffe;    // PxCMD.ST = 0
    // wait until CR is cleared
    while (port->cmd & 0x8000) { asm volatile("hlt"); }

    // set FRE and ST
    port->cmd |= 0x10;
    port->cmd |= 0x01;
    puts_serial("start_cmd end\n");
}

最初に PxCMD.ST をクリアします。 するとAHCIの状態が遷移して PxCMD.CR が0にクリアされるので、それを待ちます。

それが終わったら、また PxCMD.ST を上げ直しますが、 PxCMD.FRE を上げてから PxCMD.ST を上げます。 順番が大事です。

コマンドをつくる

次にコマンドと呼ばれるものを作ります。 コマンドをAHCIコントローラに送ることによって、AHCIコントローラに命令を送ることができます。

それでは手順を記します。 ATAPIコマンドの部分の記述は省略します。

  1. ポートの空いているスロットを見つける
    • 空いているスロットは、 PxCIPxSACT の両方がクリアされているビットに対応するスロット
    • 空いているスロットのことを以下では pFreeSlot とする
  2. メモリ上にcommand FISを構築する
    • 開始アドレスは PxCLB[CH(pFreeSlot)]:CFIS
    • command typeも指定する
  3. PxCLB[CH(pFreeSlot)] にcommand headerを構築する。条件は以下の通り
    • PRDTL はPRD tableの数を格納する
    • CFL はCFIS領域のサイズを指定する
    • A ビットはクリアする(ATAPIコマンド識別ビット)
    • W ビットはwrite処理ならセット、read処理ならクリアする
    • P ビットはオプショナル(今回はやめておく)
  4. PxCI.CI(pFreeSlot) のビットを立てて、コマンドがアクティブであることをAHCI側に通知する

以上の処理を以下のコードのようにまとめました。 build_command が本体です。

static int find_free_cmdslot(HBA_PORT *port)
{
    uint32_t slots = (port->ci | port->sact);
    for (int i = 0; i < 32; i++) {
        if ((slots & 1) == 0) {
            return i;
        }
        slots >>= 1;
    }
    puts_serial("Cannot find free command list entry\n");
    return -1;
}

#define CMD_TBL_BASE 0x10010000
static inline void build_cmd_table(CMD_PARAMS *params, uint64_t *table_addr)
{
    memset((void *)table_addr, 0, 0x80 + 16 * 65536); // zero clear
    HBA_CMD_TBL *table = (HBA_CMD_TBL *)table_addr;

    // build CFIS
    if (params->fis_type == 0x27) { // if Register H2D
        FIS_REG_H2D *h2dfis = (FIS_REG_H2D *)table_addr;
        h2dfis->fis_type = 0x27;    // H2D FIS type magic number
        h2dfis->c = 1;              // This is command
        // command type is referenced in ATA command set
        h2dfis->command = params->cmd_type;
        // device
        //h2dfis->device = 0xe0;
        h2dfis->device = 1 << 6;
        // LBA
        h2dfis->lba0 = (uint8_t)(((uint64_t)(params->lba) >>  0) & 0xff);
        h2dfis->lba1 = (uint8_t)(((uint64_t)(params->lba) >>  8) & 0xff);
        h2dfis->lba2 = (uint8_t)(((uint64_t)(params->lba) >> 16) & 0xff);
        h2dfis->lba3 = (uint8_t)(((uint64_t)(params->lba) >> 24) & 0xff);
        h2dfis->lba4 = (uint8_t)(((uint64_t)(params->lba) >> 32) & 0xff);
        h2dfis->lba5 = (uint8_t)(((uint64_t)(params->lba) >> 40) & 0xff);
        // block count
        h2dfis->countl = (uint8_t)((params->count >> 0) & 0xff);
        h2dfis->counth = (uint8_t)((params->count >> 8) & 0xff);
    } else {
        puts_serial("fis type error\n");
        return;
    }

    // build PRD Table
    // 8 KB (16 sectors) per PRD Table
    // 1 sectors = 512 KB
    uint16_t count = params->count;
    int prdtl = (int)((params->count - 1) >> 4) + 1;
    uint8_t *buf = (uint8_t *)params->dba;
    int i;
    for (i = 0; i < prdtl - 1; i++) {
        table->prdt_entry[i].dba  = (uint32_t)buf;
        table->prdt_entry[i].dbau = 0;
        table->prdt_entry[i].dbc = 8 * 1024 - 1;
        table->prdt_entry[i].i = 1; // notify interrupt
        buf += 8 * 1024; // 8K bytes
        count -= 16; // 16 sectors
    }

    // Last entry
    table->prdt_entry[i].dba = (uint32_t)buf;
    table->prdt_entry[i].dbc = (count << 9) - 1;
    table->prdt_entry[i].i = 1;
}

static inline void build_cmdheader(HBA_PORT *port, int slot, CMD_PARAMS *params)
{
    HBA_CMD_HEADER *cmd_list = ((HBA_CMD_HEADER *)port->clb + slot);
    memset((void *)cmd_list, 0, 0x400);
    cmd_list->ctba = (uint32_t)CMD_TBL_BASE;
    cmd_list->ctbau = 0;
    cmd_list->prdtl = (uint16_t)(((params->count - 1) >> 4) + 1);
    cmd_list->cfl = params->cfis_len;
    cmd_list->w = params->w;
}

static inline void notify_cmd_is_active(HBA_PORT *port, int slot)
{
    port->ci |= 1 << slot;
}

static inline void build_command(HBA_PORT *port, CMD_PARAMS *params)
{
    int slot = find_free_cmdslot(port);
    // step 1:
    // build a command FIS in system memory at location PxCLB[CH(pFreeSlot)]:CFIS with the command type.
    build_cmd_table(params, (uint64_t *)CMD_TBL_BASE);
    // step 2:
    // build a command header at PxCLB[CH(pFreeSlot)].
    build_cmdheader(port, slot, params);
    // step 3:
    // set PxCI.CI(pFreeSlot) to indicate to the HBA that a command is active.
    notify_cmd_is_active(port, slot);

    puts_serial("build command is over\n");
}

1つの関数で1つのステップを処理しています。

コマンドテーブルの開始アドレスは面倒だったので決め打ちにしました。 ここは malloc などで確保してやったほうがお行儀が良さそうです。 後々直したいと思います。

コマンド特有のパラメータ(例えば W ビットが立っているかどうか)は CMD_PARAMS 型にまとめました。

さて

これでAHCIがよろしくやってくれて、コマンドが実行されます

最後は後処理を忘れずに

最後の終了処理を忘れると痛い目を見るので、しっかり後片付けをしましょう。 では手順を説明します。

  1. IS.IPS を監視して、割り込みが来たことを確認
  2. 対応するポートの PxIS レジスタを確認し、正常な割り込みが来ているか確認
  3. 正常だったら PxIS をクリアする
  4. IS.IPS をクリアする
  5. AHCI側が PxCI をクリアするのを待つ

以上の処理をこんなふうに書いてみました。

static inline void wait_interrupt(HBA_PORT *port)
{
    puts_serial("while waiting interrupt\n");
    while (port->is == 0) {
        asm volatile("hlt");
    }
    puts_serial("interrupt comes\n");
}

static inline void clear_pxis(HBA_PORT *port)
{
    port->is |= port->is;
    puts_serial("while clear PxIS\n");
    while (port->is) {
        asm volatile("hlt");
    }
    puts_serial("clearing PxIS is over\n");
}

static inline void clear_ghc_is(int portno)
{
    HBA_MEM_REG *ghc = (HBA_MEM_REG *)abar;
    ghc->is |= 1 << portno;
    puts_serial("while clear IS.IPS\n");
    while (ghc->is) {
        asm volatile("hlt");
    }
    puts_serial("clearing IS.IPS is finished\n");
}

static inline void wait_pxci_clear(HBA_PORT *port)
{
    puts_serial("wait PxCI\n");
    while (port->ci) {
        asm volatile("hlt");
    }
    puts_serial("wait PxCI end\n");
}

いくつか注意点を述べます。

PxIS をクリアする方法は、 立っているビットに対して1を書き込んでAHCI側がクリアするのを待つ ことです。 これは仕様書にも載っています。 0クリアするために1を書き込むというのは感覚的にむむっ?と来ましたが、まあそういうもんだと思って従います。 IS.IPS のクリア方法も同様ですので承知しておいて下さい。

また、AHCI側が PxCI をクリアするまで待つというのも意外な落とし穴ポイントです。 クリアする前に次のコマンドをアクティブにしてしまうとエラーがおきてしまいますので注意です。

以上をまとめてRead/Write処理を書く

ということでまとめです。 以下のようなコードになりますた。

int ahci_read_test(HBA_PORT *port, int portno, uint64_t start, uint16_t count, uint16_t *buf)
{
    start_cmd(port);
    CMD_PARAMS params;
    params.fis_type = 0x27;
    params.cmd_type = READ_DMA_EXT;
    params.cfis_len = 5;
    params.lba = (uint64_t *)start;
    params.count = count;
    params.dba = (uint64_t *)buf;
    params.w = 0;
    build_command(port, &params);
    wait_interrupt(port);
    clear_pxis(port);
    clear_ghc_is(portno);
    wait_pxci_clear(port);
    return 1;
}

int ahci_write_test(HBA_PORT *port, int portno, uint64_t start, uint16_t count, uint16_t *buf)
{
    start_cmd(port);
    CMD_PARAMS params;
    params.fis_type = 0x27;
    params.cmd_type = WRITE_DMA_EXT;
    params.cfis_len = 5;
    params.lba = (uint64_t *)start;
    params.count = count;
    params.dba = (uint64_t *)buf;
    params.w = 1;
    build_command(port, &params);
    wait_interrupt(port);
    clear_pxis(port);
    clear_ghc_is(portno);
    wait_pxci_clear(port);
    return 1;
}

ahci_read_test では、 portno 番のポートで作業します。 SATAstart 番地から count セクタ分を読み出して、メモリ上の buf 番地へコピーします。

ahci_write_test では、メモリ上の buf 番地からデータを読み出して、SATAstart 番地から count セクタ分だけ書き込みます。

これで読み書きができると思います。 自分はできました。