AHCI奮闘記 - Read/Write編
AHCIの読み出し書き込みに成功した
こんにちは、突撃隊です。 AHCIを用いたSATAディスクへの読み書き(QEMU上ですが)が無事成功したので、メモを残しておこうかと思います。
実機確認はまだできていませんので話半分にお願いします。
実機確認できました!
以下の記事 totsugekitai.hatenablog.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コマンドの部分の記述は省略します。
- ポートの空いているスロットを見つける
- 空いているスロットは、
PxCI
とPxSACT
の両方がクリアされているビットに対応するスロット - 空いているスロットのことを以下では
pFreeSlot
とする
- 空いているスロットは、
- メモリ上にcommand FISを構築する
- 開始アドレスは
PxCLB[CH(pFreeSlot)]:CFIS
- command typeも指定する
- 開始アドレスは
PxCLB[CH(pFreeSlot)]
にcommand headerを構築する。条件は以下の通りPRDTL
はPRD tableの数を格納するCFL
はCFIS領域のサイズを指定するA
ビットはクリアする(ATAPIコマンド識別ビット)W
ビットはwrite処理ならセット、read処理ならクリアするP
ビットはオプショナル(今回はやめておく)
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がよろしくやってくれて、コマンドが実行されます
最後は後処理を忘れずに
最後の終了処理を忘れると痛い目を見るので、しっかり後片付けをしましょう。 では手順を説明します。
IS.IPS
を監視して、割り込みが来たことを確認- 対応するポートの
PxIS
レジスタを確認し、正常な割り込みが来ているか確認 - 正常だったら
PxIS
をクリアする IS.IPS
をクリアする- 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, ¶ms); 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, ¶ms); wait_interrupt(port); clear_pxis(port); clear_ghc_is(portno); wait_pxci_clear(port); return 1; }
ahci_read_test
では、 portno
番のポートで作業します。
SATAの start
番地から count
セクタ分を読み出して、メモリ上の buf
番地へコピーします。
ahci_write_test
では、メモリ上の buf
番地からデータを読み出して、SATAの start
番地から count
セクタ分だけ書き込みます。
これで読み書きができると思います。 自分はできました。