読者です 読者をやめる 読者になる 読者になる

HDDの内容を読み込めるようになったよ〜

自作OS

ということで,自作カーネルATAのPIOデータ転送(LBAとCHS)を使ってデータの読み込みが出きるようになりました(∩´∀`)∩ワーイ

ソースはこちら http://github.com/masami256/miko
参考文献は「インターフェース増刊 ATA(IDE)/ATAPIの徹底研究」です.
これは分かりやすくて非常に参考になりました.
ここに載っているソースコードは使ってないです.ちなみに,amazonのリンクはアフィリエイトじゃないです.

自作のカーネルに関しては,性能はあまり気にしていないので一番簡単そうな方法で実装しました.
PIOデータ転送という方法です.これも2種類あって,CHS(シリンダ・ヘッド・セクター)を指定する方法と,
LBA方式(論理セクタ番号を使う)があるのですが,両方で作って見ました.
でも,LBA方式のほうがデータの場所を指定しやすいと思うので,そのうちLBA方式のみにしよーかなと思ってます.

まずは,ATAの初期化あたりのコードから.これは,マスター(0)とスレーブ(1)を調べる処理です.
ATAPIは現状サポートしてません.

static bool initialize_ata(void)
{
	bool ret = false;

	ret = initialize_common(0);
	if (ret) {
		ret = initialize_common(1);
		if (!ret) {
			printk("Don't setup slave\n");
			ret = true; // master is ATA so kernel won't stop startup.
		}
	}

	return ret;
}

実際のコードはここからです.
まずは,デバイスのタイプを示す定数があります.

// It represents device type.
enum {
	DEV_TYPE_UNKNOWN = -1,
	DEV_TYPE_ATA = 0,
	DEV_TYPE_ATAPI,
};

初期化の最初にソフトリセットでデバイスをリセットします.

/**
 * Main initialize routine.
 * @param device is master or slave.
 * @return true or false.
 */
static bool initialize_common(int device)
{
	u_int8_t high, low;
	u_int16_t buf[32];
	int dev = 0;
	bool ret = false;

	memset(buf, 0x0, sizeof(buf));

	high = low = 0;

	do_soft_reset(device);

次に,Cylinder HighとLowレジスタからデータを読み出します.これは初期化後にこれらレジスタATA/ATAPIデバイスを示すシグネチャが書き込まれるので,これを取得するためです.
get_cylinder_regster()は単にinb()をラップしているだけです.
そして,デバイスの種類を調べて,ATA以外なら続きの処理はしません.

	// Read Cylinder register high and low before use.
	low = get_cylinder_regster(CYLINDER_LOW_REGISTER);
	high = get_cylinder_regster(CYLINDER_HIGH_REGISTER);

	// Is this device ATA?
	dev = get_device_type(high, low);
	switch (dev) {
	case DEV_TYPE_ATA: // Only supports ATA device yet.
		break;
	case DEV_TYPE_ATAPI:
		printk("ATAPI device is not supported yet.\n");
		return false;
	default:
		printk("Unknown device\n");
		return false;
	}

デバイスの種類はこんな感じで調べてます.

/**
 * Check this device is ATA or ATAPI.
 * @param high data from Cylinder high register.
 * @param low data from Cylinder low register.
 * @return device type.
 */
static int get_device_type(u_int8_t high, u_int8_t low)
{
	if (high == 0x0 && low == 0x0)
		return DEV_TYPE_ATA;
	else if (high == 0xeb && low == 0x14)
		return DEV_TYPE_ATAPI;
	else
		return DEV_TYPE_UNKNOWN;
}

そしたら,do_identify_device()でIDENTIFY DEVICEコマンドを発行します.

	ret = do_identify_device(device, buf);
	if (!ret) {
		printk("identify device failed\n");
		return false;
	}

	return true;
}

初期化はマスターとスレーブ両方に対して行います.

この関数はデバイス セレクション プロトコルの実装です.
これは,ATAデバイスでは基本的かつ色々なところで使ってる処理です.

static bool do_device_selection_protocol(int device)
{
	bool ret = false;

	ret = wait_until_BSY_and_DRQ_are_zero(ALTERNATE_STATUS_REGISTER);
	if (ret) {
		set_device_number(device);
		wait_loop_usec(2);
		ret = wait_until_BSY_and_DRQ_are_zero(ALTERNATE_STATUS_REGISTER);
	}

	return ret;
}

処理内容は,ALTERNATE STATUSレジスタを読んで,BSYとDRQビットが0になるまで待ちます.
両方のビットが0になったら,set_device_number()を使って,DEVICE/HEADレジスタに,デバイス番号を書き込みます.
その後は,400nsほど待つ必要があるので,適当にwaitを入れてます.(これはタイマー割り込みを利用して時間を計ってます)
そしたら,もう一度wait_until_BSY_and_DRQ_are_zero()でデバイスの準備が整うまで待ちます.

さて,Identify Deviceコマンドの発行は以下の関数がやってます.
デバイスの準備が整ったら,コマンド(0xec)を書き込むことで,コマンドの実行が行われます.
その後は,定型的な処理で,待ったり,エラーが無いか調べたり,レジスタの空読みなどします.
この辺は,HDDのデータを読み込むときも同じようなことをしてます.

static bool do_identify_device(int device, u_int16_t *buf)
{
	bool ret = false;
	u_int8_t data;
	int i, addr;

	do_device_selection_protocol(device);
	ret = get_DRDY();
	if (ret) {

		write_command(0xec);

//	read_bsy: // unused.
		wait_loop_usec(5);

		if (!wait_until_BSY_is_zero(STATUS_REGISTER)) {
			printk("wait failed\n");
			return false;
		}

		data = inb(ALTERNATE_STATUS_REGISTER);

	read_status_register:
		data = inb(STATUS_REGISTER);

		if (is_error(data)) {
			printk("error occured:0x%x\n", data);
			print_error_register(device);
			return false;
		}

		if (!is_drq_active(data))
			goto read_status_register;

		if (is_device_fault()) {
			printk("some error occured\n");
			return false;
		}

		for (i = 0, addr = DATA_REGISTER; i < 32; i++)
			buf[i] = inw(addr);

#if 1
		for (i = 0; i < 32; i++) {
			printk("%x ", buf[i]);
			if (i >= 16 && i % 16 == 0)
				printk("\n");
		}
		
		printk("\n");
#endif

	} 

	return true;

}

本題の,データの読み込み部分ですが,以下の関数です.
今は,LBA,CHS両方に対応するようにマクロで切り替えが入ってます.

#ifdef USE_PIO_CHS_READ_SECTOR
static inline bool read_one_sector(int device, u_int16_t cylinder,
				   u_int8_t head, u_int8_t sector,
				   u_int8_t count)
#else
static inline bool read_one_sector(int device, u_int32_t sector, u_int8_t count)
#endif
{
	int i;
	bool b = false;
	u_int8_t status;
	u_int16_t buf[32];
	int addr;
	int loop = 0;

	memset(buf, 0x0, sizeof(buf));

	b = wait_until_device_is_ready(device);
	if (!b) {
		printk("Failed read sector 1\n");
		return false;
	}

ここまでは,単にデバイス セレクション プロトコルを実施してるだけです.

割り込みは使わないので,Device Controlレジスタの1bit目を立てて上げます.
他のbitは使わないので0にします.

	// nIEN bit should be enable and other bits are disable.
	outb(DEVICE_CONTROL_REGISTER, 0x02);

Features Registerは使わないけど0にしといた方が良いと本に書いてあったので,素直に従ってます.

	// Features register should be 0.
	outb(FEATURES_REGISTER, 0x00);

こちらはCHS方式でデータを読み出す設定で,設定内容は以下の通りです.
・引数のシリンダ番号の上位/下位8bitをそれぞれ,Cylinder High/Lowレジスタに設定.
・セクタ番号をSector Numberレジスタに設定
・ヘッド番号をDevice Headレジスタに設定
・読み出すセクタ数をSector Countレジスタに設定

#ifdef USE_PIO_CHS_READ_SECTOR

	outb(CYLINDER_LOW_REGISTER, cylinder & 0xff);
	outb(CYLINDER_HIGH_REGISTER, (cylinder >> 8) & 0xff);
	
	outb(SECTOR_NUMBER_REGISTER, sector);

	outb(DEVICE_HEAD_REGISTER, head & 0xf);

	printk("device:0x%x high:0x%x low:0x%x header:0x%x sector:0x%x count:0x%x\n",
	       device,
	       (cylinder >> 8) & 0xff,
	       cylinder & 0xff,
	       head & 0xf,
	       sector,
	       count);

こっちはLBA方式です.
論理セクタ番号は28bitあるのでそれを8・8・8・4bitに分けて各レジスタに設定してます.
DEVICE HEADレジスタのとこだけbit演算量が多いのですが,これは,下位4bit(0〜3)が論理セクタ番号の24-27bit目を設定して,
bit4にデバイス番号(今は0で固定)をセットし,bit6を1にしてLBA方式というのを指定するためです.

#else // Using PIO LBA READ.
	outb(SECTOR_NUMBER_REGISTER, sector & 0xff);
	outb(CYLINDER_LOW_REGISTER, (sector >> 8) & 0xff);
	outb(CYLINDER_HIGH_REGISTER, (sector >> 16) & 0xff);
	outb(DEVICE_HEAD_REGISTER, ((sector >> 24) & 0x1f) | 0x40);
	outb(SECTOR_COUNT_REGISTER, count);

	printk("device:0x%x secnum:0x%x low:0x%x high:0x%x head:0x%x count:0x%x\n",
	       device,
	       sector & 0xff,
	       (sector >> 8) & 0xff,
	       (sector >> 16) & 0xff,
	       (((sector >> 24) & 0x1f) | 0x40),
	       count);

#endif // USE_PIO_CHS_READ_SECTOR

後はIdentify Deviceと一緒なので,そのうち綺麗にしようと思ってますwww

	// Read data.
	outb(COMMAND_REGISTER, 0x20);

	wait_loop_usec(4);

	inb(ALTERNATE_STATUS_REGISTER);

read_status_register_again:
	status = inb(STATUS_REGISTER);

	if (is_error(status)) {
		printk("error occured:0x%x\n", status);
		print_error_register(device);
		return false;
	}
	
	if (!is_drq_active(status)) {
		if (loop > 5) {
			printk("DRQ didn't be active\n");
			return false;
		}
		loop++;
		goto read_status_register_again;
	}

	for (i = 0, addr = DATA_REGISTER; i < 32; i++)
		buf[i] = inw(addr);

	for (i = 0; i < 32; i++) {
		printk("%x ", buf[i]);
		if (i >= 16 && i % 16 == 0)
			printk("\n");
	}
	
	printk("\n");

	inb(ALTERNATE_STATUS_REGISTER);
	inb(STATUS_REGISTER);

	printk("Done.\n");

	return true;
}		

あ,忘れてたけど,ATAはデータを16bitずつ読み出します.
今のコードは勘違いしていた部分も残っているのですが,16bitずつ読み出すときに,1回読み出す毎に読み出し先のアドレスは変えなくて大丈夫です.

実際に,仮想HDDをminixファイルシステムでフォーマットして,そこにこんな感じでテキストを置き,読めるか試してみました.

echo "ABCDEFG" > test.txt

これの仮想HDDをダンプしたのが下です.

hexdump -C test/img/hda.img
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000400  20 0d 60 27 01 00 02 00  6e 00 00 00 00 1c 08 10  | .`'....n.......|
00000410  8f 13 01 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000420  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800  07 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000810  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000009a0  00 00 00 00 fe ff ff ff  ff ff ff ff ff ff ff ff  |................|
000009b0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00000c00  07 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000c10  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000010d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 f8 ff  |................|
000010e0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00001400  ed 41 e8 03 60 00 00 00  ba 9f 7a 4b e8 02 6e 00  |.A..`.....zK..n.|
00001410  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00001420  a4 81 e8 03 08 00 00 00  ba 9f 7a 4b e8 01 6f 00  |..........zK..o.|
00001430  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
0001b800  01 00 2e 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
0001b810  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
0001b820  01 00 2e 2e 00 00 00 00  00 00 00 00 00 00 00 00  |................|
0001b830  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
0001b840  02 00 74 65 73 74 2e 74  78 74 00 00 00 00 00 00  |..test.txt......|
0001b850  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
0001bc00  41 42 43 44 45 46 47 0a  00 00 00 00 00 00 00 00  |ABCDEFG.........|
0001bc10  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*

この仮想HDDをBocsh上の自作カーネルから読み出したのが下の絵です.

hexdumpで見たときのアドレスが,0001bc00なので,これを1セクタ分(512)で割った値(222)を論理セクタとして指定して読み出してます.
そんなわけで,ATAのデバドラがなんとなく出来てきました〜♪
まだ読み込みしかできないですけどorz