第3章 シリアルドライバを作る

デバッグ用にテキストの表示などが行えると便利です。ここではシリアル通信*1のドライバを作成し、シリアル通信により文字の送受信を行うことで、QEMUのシリアル通信用の画面で文字の表示と入力された文字の取得を行ってみます*2

[*1] 送信(Tx)と受信(Rx)をそれぞれ1本の信号線でやり取りする通信方式です。Windowsでは「COMポート」と呼ばれていたり、Linuxでは「シリアルポート(ttySx)」等と呼ばれているやつです。昔のPCには「D-sub9ピン」と呼ばれるコネクタが搭載されていて、「RS-232C」規格のケーブルを使ってシリアル通信を行うことができました。

[*2] ここはあくまでもシリアル通信による文字の送受信なので、キーボードドライバ自体は後の章で作成します。

3.1 文字を送信して画面表示

それでは早速、シリアル通信による文字の送信を試してみましょう。この節のサンプルディレクトリは「031_ser_tx」です。

シリアル通信を行うには

シリアル通信には専用のコントロールIC(コントローラ)があります。コントローラにもCPU同様にレジスタがあり、そのレジスタへ値を書き込んだり、逆に値を読み出したりすることで、シリアル通信でデータを送信したり受信したりできます。

外部のICが持つレジスタは、基本的に「IOアドレス空間」というアドレス空間からアクセスします。これは、ポインタ等でアクセスするような通常のメモリのアドレス空間とは別で、「in」命令と「out」命令という専用の命令を使用して、そのアドレス空間へアクセスします。

シリアルでのデータ送信は「THR(Transmitter Holding Register)」というレジスタで行います。IOアドレスなどについては表3.1の通りです*3

[*3] マシンには複数のシリアルポートが搭載されていたりするのですが、これは1つ目のシリアルポートのアドレスです。複数のシリアルポートを使う予定も無いので本書では常に1つ目のシリアルポートを使います。

表3.1: THRについて

IOアドレス0x03f8
レジスタサイズ1バイト

レジスタサイズが1バイトなので、1文字ずつ書き込み、1文字ずつ送信します。

IOアドレス空間のレジスタへデータを書き込む「out」(命令18)

それでは、out命令を使って、文字をシリアル通信で送ってみます。

1バイトのデータ送信を行う際のout命令の構文は以下の通りです。

表3.2: 構文26: out: ALをDXが指すアドレス先へ書き込む

アセンブラout %al, %dx
機械語ee

使用できるレジスタは固定です。今回の場合、「AL」に送信したい文字のASCIIコードを格納し、「DX」へTHRのIOアドレス(0x03f8)を格納した状態でout命令を実施することで、データ送信を行えます。

8ビットレジスタと16ビットレジスタへそれぞれのビット幅の即値を格納する構文は2章で紹介しました。

再掲すると以下の通りです。

表3.3: 構文4: mov: 即値(8bit)をレジスタ(8bit)へ格納(再掲)

アセンブラmov 即値(8bit), レジスタ(8bit)
機械語b0+reg_ofs 即値(8bit)

表3.4: 構文5: mov: 即値(16bit)をレジスタ(16bit)へ格納(再掲)

アセンブラmov 即値(16bit), レジスタ(16bit)
機械語66 b9+reg_ofs 即値(16bit)

ALレジスタのreg_ofsは「0」で、DXレジスタのreg_ofsは「2」なので、ALレジスタとDXレジスタへの値格納までをsample.txtへ記述するとリスト3.1の通りです。

リスト3.1: 031_ser_tx/sample.txt

# ASCII文字'A'(0x41)をシリアルポートへ送信
b0 41           # mov $0x41,%al
66 ba f8 03     # mov $0x03f8,%dx

ここでは文字A(0x41)を送信してみることにしました。

そして、out命令を使い、ALに格納した文字をDXが指すTHRへ書き込んでみます(リスト3.2)。

リスト3.2: 031_ser_tx/sample.txt

# ASCII文字'A'(0x41)をシリアルポートへ送信
b0 41           # mov $0x41,%al
66 ba f8 03     # mov $0x03f8,%dx
ee              # out %al,%dx   (追加)

# 無限Halt                        (追加)
f4              # hlt           (追加)
eb fd           # jmp -1        (追加)

out命令の後にhltを使った無限ループを設置しました。

動作確認

kernel.binを生成し、run_qemuスクリプトで実行してみてください。

シリアル送信した文字は、QEMUをGUIで起動した場合「Ctrl-Alt-3」キーでシリアル通信用の画面へ切り替えることで確認できます。CUIで起動した場合、標準でシリアル通信画面になっているのでそのまま表示されます。

文字「A」が表示されれば成功です。

$ run_qemu fs -nographic
A

3.2 コントローラの状態を確認して送信する

前節では、コントローラの状態確認無しでTHRへ書き込みを行っていました。THRはコントローラが持つ送信バッファです。コントローラの側で送信バッファにデータを受け入れる準備ができていない時にこのバッファへ書き込みを行ってしまうと、最悪の場合、未送信のデータを上書きして消してしまう恐れがあります。

シリアル通信のコントローラには「送信バッファが空になった」事を示すフラグがあるため、ここではこのフラグを確認してからTHRへ書き込むように変更してみます。

この節のサンプルディレクトリは「032_ser_tx_chk_flg」です。

Line Status Register(LSR)

コントローラのステータスは「Line Status Register(LSR)」というレジスタで確認できます。

表3.5: LSRについて

IOアドレス0x03fd
レジスタサイズ1バイト

LSRは各ビットがコントローラのステータスを示しており、各ビットの意味は以下の通りです。

表3.6: LSRの各ビットについて

ビット意味
7受信バッファに何らかのエラーが発生
6送信バッファが空で内部の送信処理も全て完了
5送信バッファが空(データ受け入れ可能)
4ブレークシグナルを受信
3フレームエラー
2パリティエラー
1オーバーランエラー
0受信バッファに1つ以上のデータがある

使うのはビット5です。このビットがセットされていれば送信バッファへデータ書き込みを行って問題ありません。

IOアドレス空間のレジスタ値取得「in」(命令19)

それでは、LSRの値を読み出す処理を追加してみます。

IOアドレス空間上のレジスタの値を取得する命令は「in」命令で、構文は表3.7の通りです。

表3.7: 構文27: in: DXが指すレジスタ値をALへ格納

アセンブラin %dx, %al
機械語ec

試しに、単体のソースコード「read_lsr.txt」を作り、LSRの値を読み出してみます(リスト3.3)。

リスト3.3: 032_ser_tx_chk_flg/read_lsr.txt

# シリアルポートのステータス取得
66 ba fd 03     # mov $0x03fd,%dx
ec              # in %dx,%al

# 無限Halt
f4              # hlt
eb fd           # jmp -1

リスト3.3を実行し、QEMUモニターでレジスタ値を確認すると以下の通りでした。

(qemu) info registers
RAX=0000000000110060 RBX=00000000bef67f98 RCX=0000000000000000 RDX=00000000000003fd
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=0000000000210000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7dc4657

RAXレジスタの最下位1バイト(ALレジスタ)が0x60になっています。ビット6とビット5がセットされているので、このステータス値なら、送信して問題無さそうです。

フラグの待ち方「and」/「je」(命令20)

新たに2つの命令を使ってビットがセットされるまで待つ処理を実装します。流れとしては以下の2ステップです。

  1. and命令: AL(LSRの値)と0x20でANDを取る(ALのビット5以外を全て0にする)
  2. je命令: 1.の演算結果が0の場合、in命令まで戻る(再度LSR取得)

新たに登場した「je」命令は、直前の演算結果に応じてジャンプするか否かが決まる「条件付きジャンプ命令」の1つです。CPUでは、数値の演算を行うと、その結果に応じてCPU内の「フラグレジスタ」と呼ばれるレジスタの各ビットが変化します。条件付きジャンプ命令は、このフラグレジスタの状態に応じてジャンプするか否かが決まる命令です。

jeは「Jump if Equal」の略です。関係するのはフラグレジスタの中の「ゼロフラグ」のビットで、je命令の実行時、このビットがセットされていればオペランドで示した相対アドレスへジャンプし、ビットがセットされていなければ、ジャンプはせずに次の命令へ進みます。

上記の2ステップの場合、ステップ1のANDによって、LSRのビット5がセットされていた場合のみステップ2で次の命令へ進むようになり、LSRのビット5がセットされていなければLSR値を取得するin命令まで戻ることになります。

je命令の構文はjmp命令と同じで以下の通りです。

表3.8: 構文28: je: ゼロフラグがセットされていればジャンプ

アセンブラje <ラベルあるいは".">
機械語74 fe+rel_addr

フラグレジスタの動きを見てみる

ここで、フラグレジスタの動きを見てみます。

例えば、alに0を格納し、0x20とandを取るだけのコードを作ってみます(リスト3.4)。

リスト3.4: 032_ser_tx_chk_flg/zf.txt

# 0x00と0x20のANDでゼロフラグを立てる
b0 00   # mov $0x00,%al
24 20   # and $0x20,%al

# 無限Halt
f4      # hlt
eb fd   # jmp -1

実行し、QEMUモニターでレジスタ値を表示させてみます。

(qemu) info registers
RAX=0000000000110000 RBX=00000000bef67f98 RCX=0000000000000000 RDX=0000000000000000
RSI=0000000000407180 RDI=00000000bfe9b018 RBP=00000000bff0eb10 RSP=0000000000210000
R8 =00000000bff0e9dc R9 =0000000000000078 R10=00000000bfe6e080 R11=00000000e7dc4657
R12=00000000bef69540 R13=00000000bef69548 R14=00000000bff24b60 R15=00000000bef6a018
RIP=0000000000110005 RFL=00000046 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=1

「RFL=」に書かれているのがフラグレジスタの値です。すぐ右側の"[]"の中にどんなフラグが設定されているかが現れていて、"Z"が「ゼロフラグ」の設定を示しています。ゼロフラグは演算結果がゼロだった場合に設定されるフラグです。今回の場合、0x00と0x20のANDは0x00になるため、このフラグが設定されました。

この状態で「je」命令を実行するとオペランドで指定した相対アドレスへジャンプします。

フラグレジスタの算術演算に関わるビット(下位8ビット)の一覧を紹介しておきます(表3.9)。

表3.9: フラグレジスタ一覧

ビット意味
7符号フラグ。演算結果がマイナスの場合にセットされる
6ゼロフラグ。演算結果がゼロの場合セットされる
5予約
4調整フラグ。2進化10進(BCD)でのキャリー・ボローでセットされる
3予約
2パリティフラグ。演算結果最下位8ビットの1の数が偶数の場合セット
1予約
0キャリーフラグ。キャリー・ボローでセットされる

フラグレジスタ自体は64ビットCPUの場合、64ビットの大きさがあります。ここでは算術演算に関わるもののみ紹介しました。今後使用するビットは登場した際に改めて紹介します*4

[*4] フラグレジスタについて詳しくはSDMのVolume 1か、Wikibooksの記事が分かりやすいです。共に本書末尾の「参考情報」で紹介しています。

LSRのビット5を待つ処理を実装

以上をまとめると、前節の機械語コードにフラグ確認の処理を追加するとリスト3.5のようになります。

リスト3.5: 032_ser_tx_chk_flg/sample.txt

# 追加(ここから)
# シリアルポートのステータス取得
00: 66 ba fd 03 # mov $0x03fd,%dx
04: ec          # in %dx,%al

# 送信バッファが空でなければステータス取得まで戻る
05: 24 20       # and $0x20,%al
07: 74 fb       # je -3
# 追加(ここまで)

# ASCII文字'A'(0x41)をシリアルポートへ送信
09: b0 41       # mov $0x41,%al
0b: 66 ba f8 03 # mov $0x03f8,%dx
0f: ee          # out %al,%dx

# 無限Halt
10: f4          # hlt
11: eb fd       # jmp -1

実行結果は前節と変わりませんが、これでポーリングによるシリアル送信をちゃんと実装できました。

3.3 受信も追加しエコーバックプログラムを作る

この章の最後に、シリアルポートからの受信データの取得を行ってみます。そして、受信した文字をそのままシリアルポートへ送信することで、エコーバック処理*5を実装してみます。

[*5] 入力したキーに対応する文字がそのまま画面へ表示される処理。シリアルドライバの送受信の動作確認としてよく使われます。

この節のサンプルディレクトリは「033_echoback」です。

受信バッファにデータが来るのを待つ(命令21「jne」/命令22「pause」)

まずは、受信バッファにデータが来るのを待ちます。

シリアルポートの受信バッファにデータがあるか否かもステータスレジスタLSRで確認できます。受信バッファに1つ以上のデータがあれば、LSRのビット0がセットされます(表3.10)。

表3.10: LSRの各ビットについて(再掲)

ビット番号意味
ビット7受信バッファに何らかのエラーが発生
ビット6送信バッファが空で内部の送信処理も全て完了
ビット5送信バッファが空(データ受け入れ可能)
ビット4ブレークシグナルを受信
ビット3フレームエラー
ビット2パリティエラー
ビット1オーバーランエラー
ビット0受信バッファに1つ以上のデータがある

sample.txtに受信バッファに1つ以上のデータが来るまで待つ処理を追加するとリスト3.6の通りです。




リスト3.6: 033_echoback/sample.txt

# 追加(ここから)
# シリアルポートのステータス取得
00: 66 ba fd 03 # mov $0x03fd,%dx
04: ec          # in %dx,%al

# 受信バッファが空であればpauseしステータス取得まで戻る
05: 24 01       # and $0x01,%al
07: 75 04       # jne +6
09: f3 90       # pause
0b: eb f7       # jmp -7
# 追加(ここまで)

# シリアルポートのステータス取得
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec          # in %dx,%al

# 送信バッファが空でなければステータス取得まで戻る
# ・・・ 省略 ・・・

「シリアルポートのステータス取得」のコードブロックはシリアル送信の際と同じものです。

「受信バッファが空であればpauseしステータス取得まで戻る」のコードブロックは、これまでと少し異なります。

まず、新たに登場した「jne」命令は、「Jump if Not Equal」の略です。je命令の逆で、「ゼロフラグがセットされていなければジャンプ」という挙動になります。

構文もje命令等と同様で表3.11の通りです。


表3.11: 構文29: jne: ゼロフラグがセットされていなければジャンプ

アセンブラjne <ラベルあるいは".">
機械語75 fe+rel_addr

そして、ALへ格納したステータス値と0x01でANDの結果に対して、以下の振る舞いを実装しています。

  • ANDの結果が0x01 (ゼロフラグが0)
    • jne命令で6バイト先(このコードブロックの直後)へジャンプ
    • 意図: 受信バッファにデータ来たのでループを抜ける
  • ANDの結果が0x00 (ゼロフラグが1)
    • jne命令でジャンプせず、次の命令へ進む
    • 「pause」命令を実行後、jmp命令で7バイト前(LSR取得)へ戻る
    • 意図: 受信バッファにデータ無いので、pauseして再度ステータス取得

ここでは、「ビジーループで待つの際はpause命令を挟む」という事をするためにこのようなロジックにしています。

pause」命令は、「ビジーループしている」事をCPUへヒントとして伝える命令で、ビジーループにこの命令を挟んでおくことでCPUの実行効率を改善する効果があります。(詳しくは後述のコラムにまとめています。)

pause命令はオペランドが不要で、構文としては表3.12の通りです。

表3.12: 構文30: pause: ビジーループのヒントをCPUへ伝える

アセンブラpause
機械語f3 90

併せて、送信バッファが空になるのを待つ処理にも、pauseを挟んでおくことにします(リスト3.7)。

リスト3.7: 033_echoback/sample.txt

# ・・・省略・・・

# 受信バッファのデータを取得
0d: 66 ba f8 03 # mov $0x03f8,%dx
11: ec          # in %dx,%al
12: 88 c3       # mov %al,%bl

# シリアルポートのステータス取得
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec          # in %dx,%al

# 変更(ここから)
# 送信バッファが空でなければpauseしステータス取得まで戻る
19: 24 20       # and $0x20,%al
1b: 75 04       # jne +6
1d: f3 90       # pause
1f: eb f7       # jmp -7
# 変更(ここまで)

# ・・・省略・・・

メモリ順序違反とpause命令

フラグ設定待ちのようなビジーループにpause命令を入れておくとCPUの実行効率低下を防ぐことができるのは、「メモリ順序違反(memory order violation)」を防ぐことができるためです。

インテル等のCPUには「投機的実行」と呼ばれる機能があり、現在実行している命令よりも先の命令をCPU内のバッファへ予め読み込み、場合によっては先に実行しておくことで、CPUの性能最適化を図っています。

ただ、条件付きジャンプ命令等、命令を評価し終えるまでどこへ分岐するか分からない命令があると、その先をどのように先読みするかが問題になります。

この時、インテル等のCPUでは「過去にどちらのコードパスを多く通ったか」等の統計値を元に、分岐先を推測して先読みを続行します。

しかし、今回のようにCPUとしてはほとんどの時間を「フラグ設定待ち」で過ごしていると、命令先読みのバッファは「フラグ設定待ちの処理」の先読みで埋め尽くされ、それらの処理が予め実行される形で最適化が行われます。これは、「次もきっとフラグ待ちだろう」という推測で最適化されている状況です。

そのため、いざフラグが設定された時に、推測が外れ、先読みしたバッファの内容や予め実行した処理もムダになり、実行効率が低下します。これを「メモリ順序違反」と呼びます。

pause命令があるとCPUがこのような誤った先読みをしなくなり、メモリ順序違反を防ぐことができます。

詳しくは、インテルのCPUマニュアルである「Intel 64 and IA-32 Architectures Software Developer's Manual(SDM)」の「Volume 2」の「4.3 INSTRUCTIONS (M-U)」にあるpause命令の解説をご覧ください。(本書末尾の「参考情報」にPDFが公開されているページのURLを載せています。)

受信バッファのデータを取得する

次に受信バッファのデータを取得します。受信バッファのレジスタは「RBR(Receiver Buffer Register)」です。

表3.13: RBRについて

IOアドレス0x03f8
レジスタサイズ1バイト

RBRのIOアドレスはTHRと同じです。同一のアドレスに対して、in命令による読み出す時は「RBR」へ、out命令による書き込み時は「THR」へ、となるように内部の回路が組まれています。

RBRからデータを読み出す処理を追加するとリスト3.8の通りです。

リスト3.8: 033_echoback/sample.txt

# シリアルポートのステータス取得
00: 66 ba fd 03 # mov $0x03fd,%dx
04: ec          # in %dx,%al

# 受信バッファが空であればpauseしステータス取得まで戻る
05: 24 01       # and $0x01,%al
07: 75 04       # jne +6
09: f3 90       # pause
0b: eb f7       # jmp -7

# 追加(ここから)
# 受信バッファのデータを取得
0d: 66 ba f8 03 # mov $0x03f8,%dx
11: ec          # in %dx,%al
12: 88 c3       # mov %al,%bl
# 追加(ここまで)

# シリアルポートのステータス取得
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec          # in %dx,%al

# 送信バッファが空でなければpauseしステータス取得まで戻る
# ・・・ 省略 ・・・

in命令の箇所では、受信バッファからALレジスタへ受信データを格納しています。ALレジスタはその他のin/out命令の箇所でも使用するため、ALへ格納したデータはmov命令でBLレジスタへコピーしています。

mov命令の新構文(8ビットレジスタから8ビットレジスタへコピー)

ここで、8ビットレジスタから8ビットレジスタへ値をコピーするmov命令の構文を紹介します。

表3.14: 構文31: mov: 第1から第2オペランドへコピー(共に8ビットレジスタ)

アセンブラmov レジスタ(8bit,src), レジスタ(8bit,dst)
機械語88 c0+reg_src_dst

reg_src_dst」については表3.15の通りです。

表3.15: 8ビットレジスタのreg_src_dst一覧

src/dstALCLDLBLAHCHDHBH
AL0x000x010x020x030x040x050x060x07
CL0x080x090x0a0x0b0x0c0x0d0x0e0x0f
DL0x100x110x120x130x140x150x160x17
BL0x180x190x1a0x1b0x1c0x1d0x1e0x1f
AH0x200x210x220x230x240x250x260x27
CH0x280x290x2a0x2b0x2c0x2d0x2e0x2f
DH0x300x310x320x330x340x350x360x37
BH0x380x390x3a0x3b0x3c0x3d0x3e0x3f

2章のimul命令で紹介した表とは異なり、各行が「第1オペランド(src)」で、各列が「第2オペランド(dst)」となっている点に注意してください。




受信バッファから取得したデータを送信する

受信バッファからデータを取得できたので、今度はそれを送信してみます(リスト3.9)。

リスト3.9: 033_echoback/sample.txt

# ・・・ 省略 ・・・

# 送信バッファが空でなければpauseしステータス取得まで戻る
19: 24 20       # and $0x20,%al
1b: 75 04       # jne +6
1d: f3 90       # pause
1f: eb f7       # jmp -7

# 追加・変更(ここから)
# 受信バッファから取得したデータを送信
21: 88 d8       # mov %bl,%al
23: 66 ba f8 03 # mov $0x03f8,%dx
27: ee          # out %al,%dx

# 先頭へ戻る
28: eb d6       # jmp -0x28
# 追加・変更(ここまで)

特に新たな要素はありません。受信したデータはBLレジスタにあるため、それをALへコピーし、DXレジスタへ送信バッファアドレスを設定の上、out命令でシリアルポートへ送信しています。

動作確認

これまで通り、ビルドし、動作確認してみてください。

入力したキーに対応する文字がシリアルの画面へ表示されます。

ただ、改行を入力すると、次の行へ移らずに行頭へ戻ってしまいます。

$ ../tools/run_qemu ../fs -nographic
...
hogeo world
↑"hello world"と入力後、改行の後、
 "hoge"と入力したが、次の行へ移らない。

CRを受け取ったらLFを追加で送信するようにする(命令23「cmp」)

これは、シリアルポートで受信する改行文字がCR(Carriage Return、復帰コード)のみであるためです。次の行への改行を行うLF(Line Feed)を送信していないため、改行キーを入力した際に、行頭に移動するのみで次の行へ移動しなかったのでした。

受信バッファから取得したデータを送信後、受信したデータがCR(0x0d)であった場合、LF(0x0a)を追加で送信するようにします(リスト3.10)。

リスト3.10: 033_echoback/sample.txt

# ・・・ 省略 ・・・

# シリアルポートのステータス取得(*1)
14: 66 ba fd 03 # mov $0x03fd,%dx
18: ec          # in %dx,%al

# 送信バッファが空でなければpauseしステータス取得まで戻る
19: 24 20       # and $0x20,%al
1b: 75 04       # jne +6
1d: f3 90       # pause
1f: eb f7       # jmp -7

# 受信バッファから取得したデータを送信
21: 88 d8       # mov %bl,%al
23: 66 ba f8 03 # mov $0x03f8,%dx
27: ee          # out %al,%dx

# 追加・変更(ここから)
# 送信したデータがCR(0x0d)でなければ先頭まで戻る
28: 3c 0d       # cmp $0x0d,%al
2a: 75 d4       # jne -0x2a

# LF(0x0a)を受信データとして(*1)まで戻る
2c: b3 0a       # mov $0x0a,%bl
2e: eb e4       # jmp -0x1a
# 追加・変更(ここまで)

新たに「cmp」命令が登場しました。これはオペランドで指定した数値の比較を行う命令です。挙動としては「第2オペランド(dst)を書き換えないsub命令」です。sub命令は第2オペランドから第1オペランド減算した結果を第2オペランドへ格納しますが、cmp命令は減算まで行ってフラグレジスタのみ変化させた後、減算結果は捨てます。

構文としては、cmp命令もsub命令同様、レジスタとしてALが対象の場合は1バイト少ない構文があります(表3.16)。

表3.16: 構文32: cmp: 即値(8bit)とALを比較

アセンブラcmp 即値(8bit), %al
機械語3c 即値(8bit)

ALを含むその他のレジスタについては表3.17の通りです。




表3.17: 構文33: cmp: 即値(8bit)とレジスタ(8bit)を比較

アセンブラcmp 即値(8bit), レジスタ(8bit)
機械語80 f8+reg_ofs 即値(8bit)

追加した処理でやっていることとしては、送信処理後、cmp命令を使用して、ALレジスタに残っている送信済みの文字と0x0d(CR)を比較し、等しくなかった場合はjne命令で先頭まで戻り、等しかった場合は、以降のLF送信処理へ進みます。

LF送信処理は、BLへ0x0a(LF)を格納し、送信のためのシリアルポートステータス確認処理(*1)までジャンプするだけです。

動作確認

この状態で動作確認してみると、改行まで反映されているはずです。

$ ../tools/run_qemu ../fs -nographic
...
hello world
hoge