フロッピーディスク用のデバイスドライバ入門
※最後にソースコード載せてます。(はりぼてOS用のデバイスドライバなので、読者ではない方だと分かりづらいかもしれません。)この記事では、フロッピーディスク用のデバイスドライバを書いていこうぜという記事です。
BIOSの機能は使わずにフロッピーディスクを制御していきます。
30日でできる!OS自作入門では、BIOSを使ってFDの読み書きを行なっていました。
しかし、プロテクトモードに移行してしまうと、BIOSの機能が使えません。
そこで、プロテクトモードでもFDの読み書きができるように、デバイスドライバを書くことにしました。
あまり、フロッピーディスクの仕様には触れずに、とにかく動けばいいやのスタンスです。また、QEMUで動かす予定なので、FDの初期化方法も詳しくは述べません。(QEMUでは、BIOSが初期化はやってくれているので、FDの初期化は不要)とにかく動かしてみようという記事になります。
ネット上には、フロッピーディスクに関する記事が存在しますが、詳細に書かれすぎていて、結局何をすればいいのか分からずじまいになってしまう恐れがあります。
なので、この記事で取り敢えず動くこのコードを片手に、他の文献を読んでみてください。
また、ここでの知識はHDDのデバイスドライバにも活かせるはずです。(筆者は原因不明のエラーで止まっていますが…)
フロッピーディスクとは?
フロッピーディスクとは、ハードディスクの劣化版だと思ってくれればOKです。 最低限、ヘッド、シリンダー、セクターを知っていればOKです。ちなみに、参考文献ではシリンダーではなくトラックと表現している場合がありますが、
1枚のディスクの場合は同じことを指しています。
以下は、FDCのI/Oマップです。
レジスタ名 | IOポート |
---|---|
STATUS REGISTER A | 0x3F0 |
STATUS REGISTER B | 0x3F1 |
DIGITAL OUTPUT REGISTER | 0x3F2 |
TAPE DRIVE REGISTER | 0x3F3 |
MAIN STATUS REGISTER | 0x3F4 |
DATARATE SELECT REGISTER | 0x3F4 |
DATA INFO | 0x3F5 |
DIGITAL INPUT REGISTER | 0x3F7 |
CONFIGURATION CONTROL REGISTER | 0x3F7 |
よく使うのは、
DATA INFOとMAIN STATUS REGISTERです。
DATA INFOは、データの読み書きに使います。DATA INFOは1byteなので1byteずつ読み書きします。
MAIN STATUS REGISTERは、FDの状態が分かるレジスターです。FDと通信する際に、こちらをチェックしないといけません。
上の2つのレジスタを使います。(実機では、初期化のために他のレジスタを必ず使います!!)
DMAとは?
DMAとは、Direct Memory Accessの略で、CPUの代わりに二次記憶装置とやり取りをおこなってくれる便利な野郎です。この記事では、DMAを利用してフロッピーディスクと通信を行なっていきます。CPUが二次記憶装置と通信を行うには二つの方式が存在します。
1.PIO方式
2.DMA方式
の2点です。
PIO方式でフロッピーディスクとCPUがやり取りを行う場合、1byteずつCPUとFDとの間でデータのやり取りをおこなっていくことになります。FDは電気的ではなく、機械的に動作するデバイスなのでCPUから見るとクッソ遅い野郎なんです。
PIO方式で、データを読み込んでいく場合を説明します。
FDの場合、bit単位でFDCがデータを読み込み内部バッファ(FD内)に格納します。指定されたブロック分だけ内部バッファに書き込みを入れてから、割り込み(IRQ6)をしてCPUにFDからデータの読み込みが終了したことを伝えます。
CPUは、1byteごとFDC(正確にはDATA INFOレジスタ)からデータを受け取りその度にRAMにデータを書き込んでいきます。
これは、明らかに遅い作業ですよね。
だって、CPUが1byteごとFDCと通信をして、RAMにデータを書き込んでいくのですから。
CPUの時間を無駄にしてしまうんです。
DMAは、上の問題を解決するために出てきたものです。(多分)
DMA方式だと、FDからデータを読み込む場合、
CPUさんは、FDのデータの開始番地とデータの大きさをFDに伝えるだけでそのあとは、
DMAがFDと通信のやり取りを行なってくれます。
CPUはその間他の仕事ができるので、CPUさんから見るとこれはとても嬉しいんですよ。
FDに対して書き込む場合も同じ要領です。
あ、ここで疑問なのが、DMA方式だとFDからデータを読み込む場合、DMAさんはRAMのどこに書き込むのかということです。
これはあらかじめDMAさんにデータの読み書きに使うRAMの開始番地を教えてあげる必要があります。
DMAに対して伝えてやらないといけないのは2点です。
1.RAM上のどこを使うのか(開始番地)
2.RAM上で使用する領域の大きさ
DMAの使用するレジスタのI/Oマップは、
0x04(RAM上のどこを使用するのか用)と0x05(RAM上で使用する領域の大きさ)と0xd8(リセット用)と0x0A(リセット用コマンド発行のたびにリセット)と0x0B(DMAのモード変更)
ちなみに0x04と0x05(それぞれRAM上の開始アドレスと領域の大きさ)
にデータを送信する際1byteずつしか送信できません。しかし、開始アドレスと領域の大きさは2byteなので、2回続けて行います。
先に下位1byte送信します。
また、FDに対してデータを読み込むのか書き込むのかは、
IO空間の0x05番地に対して、0x5a(FDに対して読み込みモード)、0x56(FDに対して書き込みモード)を送信してあげればOKです。
読み書きの大まかな流れ
FDに対する読み書きの大まかな流れは以下の通りです。1.DMAさんにモードを伝える。(READ or WRITE)
2.コマンドフェーズ
3.IRQ発生待ち(IRQ6番 この割り込みがくると読み書き完了)
4.結果フェーズ(ここ必ず読み込むこと!!)
コマンドフェーズとは、FDに対して命令を送る段階です。
例えば、FDに対して読み込みなのか?書き込みなのか?トラックのどこなのか?
シリンダーは何番?
セクター番号は?
これらを伝える段階です。
結果フェーズは、省略します。(ここは読み込めばいい。詳細は参考文献を読んでみてください。)
ちなみに、
結果フェーズが読み書き完了の段階ではなく、3番の割り込みがきた時に読み書きは完了しています。
(動きだけ見たいので飛ばします。)
読み書きコマンド発行方法
読み込みも書き込みも同じ形式のコマンドを発行します。コマンドフェーズでは、DATA INFOレジスタに1byteずつ計9回命令を送ります。
また、送るたびにMSRの状態を読み込む必要があります。これに関しては、参考資料をみてください。
最初の1回目は、どういう命令なのかを伝える段階です。例えば、FDのデータを読み込みたいならば、0x06をDATA INFOに送ります。書き込みたいならば、0x05を送信します。
2~9回は、パラメータを送ります。先ほど申したデータの位置情報なんかを伝える段階です。
DATA INFOに9回送信した後、DMAがフロッピーディスクに対して読み込みor書き込みを行います。この後、必ず割り込み(IRQ6番)が発生するので、割り込みチェックしてください。
そして最後に、サボりがちなのが結果フェーズです。ここを怠ると次からのコマンド発行できなくなるので、気をつけましょう。
以下、コマンドの具体的な形式です。
READ DATAの場合
times/bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | ヘッド番号(1or0) | ドライブ番号上位桁 | ドライブ番号下位桁 |
2 | シリンダー番号(7bit分全て) | |||||||
3 | ヘッド番号(7bit分全て) | |||||||
4 | セクター番号(7bit分全て) | |||||||
5 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
6 | トラック長(セクター数、7bit分全て) | |||||||
7 | ギャップサイズ(7bit分全て) | |||||||
8 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
はりぼてOSで動かしてみる。
※I/Oマップのアドレスそのまま書いているので見づらいかもしれません。後で直します。
ただ単にコマンドを発行するだけのコードになっています。
# include "bootpack.h"
# include <stdio.h>
//IRQ6検知
int ReceivedIRQ = 0;//1:割り込みきた 0:割り込みきてない
void inthandler26(int *esp){
//IRQの受付を完了したことを伝える
io_out8(PIC0_OCW2, 0x66);
ReceivedIRQ = 1;
return;
}
void MSR_check(){
for(;;){
//MSRの7bit目と6bit目をチェックする
if( (io_in8(0x3F4)&0xc0) == 0x80){
break;
}
}
}
//result phaseの段階をチェックする
void MSR_result_check(){
for(;;){
//MSRの7bit目と6bitをチェックする
if( (io_in8(0x3F4) & 0x50) == 0x50){
break;
}
}
}
//IRQ発生待ち
void flpyirq_wait(){
//IRQ発生待ち
while(ReceivedIRQ != 1){
//何もしない
}
ReceivedIRQ = 0;
return;
}
void issue_command_read(short addr, short size){
init_dma_flp(addr, size);
dma_read_mode();
//command_phase
MSR_check();
io_out8(0x3F5, 0x06);//READ_DATA発行する
MSR_check();
io_out8(0x3F5, 0x04);//ヘッド番号0で、ドライブ番号0を読み込む
MSR_check();
io_out8(0x3F5, 0x00);//シリンダー番号を発行する
MSR_check();
io_out8(0x3F5, 0x01);//ヘッド番号を発行する
MSR_check();
io_out8(0x3F5, 0x02);//セクター番号を発行する
MSR_check();
io_out8(0x3F5, 0x02);//セクターサイズを発行する
MSR_check();
io_out8(0x3F5, 0x12);//トラック長(1トラック中のセクター数)を発行する
MSR_check();
io_out8(0x3F5, 0x1b);//ギャップ3サイズを発行する
MSR_check();
io_out8(0x3F5, 0xff);//データ長を発行する
//IRQ待ち
flpyirq_wait();
//result_phase
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
return;
}
void issue_command_write(short addr, short size){
init_dma_flp(addr, size);
dma_write_mode();
//command_phase
MSR_check();
io_out8(0x3F5, 0x05);//WRITE_DATA発行する
MSR_check();
io_out8(0x3F5, 0x04);//ヘッド番号0で、ドライブ番号0を読み込む
MSR_check();
io_out8(0x3F5, 0x00);//シリンダー番号を発行する
MSR_check();
io_out8(0x3F5, 0x01);//ヘッド番号を発行する
MSR_check();
io_out8(0x3F5, 0x02);//セクター番号を発行する
MSR_check();
io_out8(0x3F5, 0x02);//セクターサイズを発行する
MSR_check();
io_out8(0x3F5, 0x12);//トラック長(1トラック中のセクター数)を発行する
MSR_check();
io_out8(0x3F5, 0x1b);//ギャップ3サイズを発行する
MSR_check();
io_out8(0x3F5, 0xff);//データ長を発行する
//IRQ待ち
flpyirq_wait();
//result phase
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
MSR_result_check();
io_in8(0x3F5);
return;
}
void init_dma_flp(short addr, short size){
//DMA初期化
//maskする
io_out8(0x0A, 0x06);
//DMAのリセットする
io_out8(0x0C, 0xFF);
//RAMのアドレスを指定する
io_out8(0x04, addr);
io_out8(0x04, addr >> 8);
//reset
io_out8(0xd8, 0xff);
//RAM上の大きさ指定
io_out8(0x05, size);
io_out8(0x05, size >> 8);
//この一行書いたら、なぜか動いた。これ書かないと読み書きできない。これなんなの?
io_out8(0x81, 0);
//mask解除する
io_out8(0x0A, 0x02);
return;
}
void dma_read_mode(){
//readモードにする
io_out8(0x0A, 0x06);
io_out8(0x0B , 0x56);
io_out8(0x0A, 0x02);
}
void dma_write_mode(){
//writeモードにする
io_out8(0x0A, 0x06);
io_out8(0x0B, 0x5a);
io_out8(0x0A, 0x02);
}
//バッファ開始番地
short addr = 0x1111;
//バッファの大きさ
unsigned short size = 0x23ff;
unsigned char *ni;
ni = addr;
int j = 0;
for(i=0x41; i < 0x4C; i++){
*(ni+j) = i;
j++;
}
for(i = 0; i < 20; i++){
*(s) = *(ni+i);
putfonts8_asc(binfo->vram, binfo->scrnx, 0+i*20, 10, COL8_FFFFFF, s);
}
issue_command_read(addr, size);
for(i = 0; i < 11; i++){
*(s) = *(ni+i);
putfonts8_asc(binfo->vram, binfo->scrnx, 0+i*20, 50, COL8_FFFFFF, s);
}
_asm_inthandler26:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler26
POP EAX
POPAD
POP DS
POP ES
IRETD
IDTに登録したり、マスク解除したりと他にやることありますが、「30日でできる!OS自作入門」にその辺りは書かれてあるので参照してください。
上のbootpackでは、最初にABCDEFGHIJを画面に表示させます。(文字列は0x1111番地から並んでいる。)
その後、
HARIBOTESYS文字列をFDから読み込んだものを0x1111番地以降にRAMに書き込みます。
以下、結果です。
うまく、読み込めていることがわかります。
応用編
デバイスドライバができれば、テキストエディタが作れるし、その先はコンパイラなんかも頑張れば作れちゃうんじゃよ。みんな頑張ってくれなのじゃ。最後に
この記事で分かりづらかったことがありましたら、ぜひ質問してください。参考文献
以下の参考文献を読まれることをお勧めします。
下のURLが、特に分かりやすい。(DMAについての解説もあります。)
http://www.brokenthorn.com/Resources/OSDev20.html
下のURLは、各コマンドの形式について細かく掲載されています。
(READコマンドの形式は、間違っているので注意)
http://softwaretechnique.jp/OS_Development/kernel_development11.html