借り初めのひみつきち

仮ブログです。

ハイブリッドアーキテクチャ

第12世代 Intel プロセッサの最大の特徴は、高性能コア (Performance の P) と高効率コア (Efficient の E) によるハイブリッドアーキテクチャを採用していることです。

これは Arm で big.LITTLE と呼ばれていたものによく似ていて、 ひとつのプロセッサで高性能と省電力を両立させるのは難しいので、 高性能プロセッサと省電力プロセッサを両方同じパッケージに入れて切り替えて使おうという考え方です。

一見乱暴に見えますが、現代のプロセッサはマルチコア構成が当たり前で OS もマルチコア前提でコア間のマイグレーション機能が既に実装されているので、スケジューリングする際にどのプロセッサを優先的に割り当てるのか調整するだけで割と簡単に高性能と省電力を両立することができます。

実際にこの機能が使えるプロセッサが登場したので MYOS でも対応したいと考えていたところ、なんとか入手できたので対応してみたいと思います。

起動まで

早速電源を入れて OS のインストールされた USB を挿入して待つこと数秒、起動しませんでした。

どうもページング処理に問題があったようで、今回の件とあまり関係がないのでとりあえずサクッと修正します。

すると次は別の問題が発生しました。なぜか一部のコアが起動に失敗しているようです。

最初は P コアだけ起動して E コアは何か特別な処理をしないと起動できないのかと思いましたが、 CPU のスペックを確認するとなんと P コアの個数とも関係ないことがわかりました。

さらに色々調査したところ、単に最大コア数を制限する処理のバグでした…

今まで8コア以上のCPUを使ったことがなかったので、こんな簡単なバグに気づいてなかったようです。

というわけで、 APICID の振り方の癖が従来のCPUと少々異なる点が気になるものの、起動してコアを認識するところまでは従来の SMP と特に変わりませんでした。

P コアと E コアの識別

単純な SMP として動作すると、 P コアと E コアが特に区別することなくスケジューリングされます。

P コアと E コアを意識してスケジューリングするには、起動後にコアを識別する必要があります。

まず、 EAX = 7 で CPUID 命令を実行して EDX の Hybrid ビットを確認します。

ここが 1 になっていればハイブリッドアーキテクチャ対応 CPU なので、 EAX = 1A で CPUID 命令を実行します。

ここで得られた EAX の値で Core ベースのコア(P)か、 Atom ベースのコア(E)か区別できるので、スケジューラーの初期化時にコアを P-Core と E-Core に分類します。*1

なお、第12世代プロセッサの中には P コアのみや E コアのみの製品も存在していますが、これらの値がどういう動作になるか未検証です。

スケジューラーの調整

P コアと E コアを区別できるようになったら、システムの負荷状況によって P コアに割り当てたり E コアに割り当てたり制御できるようにします。

MYOS のスケジューラーは SMT 対応するために負荷測定して負荷によってコアの割り当てを制御する機能が実装済みなので、そこを改修します。

どのように分配するのがベストかはコアの構成やユースケースによると思うので、完璧な正解というのは難しいかもしれません。 とりあえず、負荷が少ないときは E コアを優先的に割り当てるようにしておきました。

なお、ハイブリッドアーキテクチャ対応の Intel プロセッサには Hardware Guided Scheduling や Intel Thread Director という支援機能が実装されていますが、詳しい使い方がマニュアルに書かれておらずよくわからなかったので今回は対応しません。

いかがでしたか?

自作 OS でも SMP 対応済みであればハイブリッドアーキテクチャ対応はそんなに難しくないことがわかりました。

とはいえ、最近の CPU は動的にクロックが変化するのでハイブリッドアーキテクチャを最大に活かすためには次のステップとしてクロック変更対応も視野に入れると良いと思います。

なお、今回対応したバージョンのリリース予定は未定です。 *2

*1:ハイブリッド対応 Intel プロセッサは俗に Core と Atom を統合したものであると言われていましたが、この書き方によると本当に Core ベースのコアと Atom ベースのコアが使われているようです。 P と E の区別がもう少しわかりやすい名前を使って欲しかったです。

*2:現在の MYOS は最近の Rust nightly の仕様変更の影響でビルドが通らず、とりあえず古いバージョンを指定すればビルドは通りますが、今後の新機能への追従が難しく、今回の変更の影響が大きすぎて対応するモチベーションも大幅に下がっているので、正式な対応時期を約束できない状態です。

非可逆圧縮作ってみた

シンプルな非可逆圧縮を作ってみました。簡単な処理しかしていません。

github.com

組み込み向けに設計したのでデコードのメモリ消費は抑えてあり、圧縮後の画像は16bitカラー相当になります。 *1

また、以下のような画像は苦手です。

  • モノクロ画像
    • もともと色差がないので圧縮効果はあまり期待できません。
  • 小さいピクセルアート (数十ピクセル以下)
    • 2x2 の範囲の色差を非可逆圧縮しているので、小さいピクセルアートでは色の変化が激しいので境目のノイズが目立つ場合があります。
  • 大きい画像 (FullHDなど)
    • この形式は 8x8 のブロック単位で圧縮しますが、大きい画像は近隣のピクセルと変化が少ないため同じようなブロックが大量に発生します。しかし、この形式はブロックを超えて圧縮できないため相対的に圧縮効率が悪くなります。

まとめると、数十〜数百ピクセルくらいのサイズの写真では PNG と JPG の間くらいの圧縮率になります。 また、イラスト調画像でも JPG のようなブロックノイズはほぼ発生しません。

しくみ

まずは処理をしやすくするために画像を 8x8 のブロックに分割します。 以降の処理は 8x8 ブロック単位で処理するためメモリをあまり消費しません。 また、今のところサイズが 8 の倍数でない画像は処理できません。

次に、各ピクセルの色を RGB → YUV に変換し、同時に 24bit (8x3) → 18bit (6x3) に精度を落とします。 その後、人間の目は輝度の変化には敏感だけど色差の変化には鈍感な性質を利用して YUV 4:2:0 に近い間引き処理で色差 (UとV) を1/4にします。 色差情報を間引くため、最悪でも生のビットマップの約半分のサイズの圧縮が保証されます。一連の色変換処理が非可逆になります。

最後にスライド辞書を使った単純なLZ法で圧縮します。

追記:差分エンコーディングはLZ圧縮を採用した現在ではむしろ圧縮率が悪化するケースが多いことがわかったので削除しました。

いかがでしたか?

YUV 圧縮は実際に画像や映像処理でよく使われている定評のある圧縮方法で、単純な処理で結構効果があることがわかりました。

今回の圧縮方式ではあえて仕様をシンプルにするため採用しなかった仕様がいくつかあります。 例えば、ブロック単位の処理をやめて Y, U, V をそれぞれチャンネルごとに圧縮すると多くのメモリが必要になる代わりに良好な圧縮効果が期待できると思います。 機会があったらもっと強い圧縮を考えてみると良いかもしれません。

*1:厳密には 16bit カラーとちょっと違いますが、 24bit カラーよりも色情報を落としているので 16bit カラーでグラデーションを表示したときとよく似た段差が発生します。

自作OSの描画が遅い理由 - MTRR と WriteCombining

自作OSで描画重視のプログラムを作っていると、本来スペックが高いはずの機種で思ったほど描画速度が出ない現象に遭遇することがあります。

memtest などでベンチマークを取ってみるとメモリの転送速度は十分に出ているはずなのに、実際に描画してみると単純な塗り潰しでさえ時間がかかっています。

なぜでしょうか?

おそらく犯人はキャッシュの設定です。

-

現代のCPUは技術の進歩によってものすごく高速に計算できますが、主要なメモリ素子であるDRAMは原理上あまり高速化できないのでCPUから見て遅いデバイスです。 そのためメモリアクセスはキャッシュが非常に重要になります。

キャッシュメモリは一度読み込んだメモリの内容を覚えておいて二度目の読み込みを高速化したり、書き込みを一定時間キャッシュメモリにため込んで実際の書き込みを遅延することでメモリアクセスによる速度低下を低減させるもので、さらに現代のCPUではDRAMとキャッシュの間の同期に必要な時間をCPUがメモリアクセスの順番を調整して隠蔽することでパフォーマンスを向上させています。

-

しかし、キャッシュに頼るとうまく動作しなくなるものがあります。 MMIOバイスです。

MMIOバイスはメモリ空間の一部を使用して通常のメモリアクセス命令でアクセスできるデバイスです。 MMIOバイスの読み出しにキャッシュを経由すると、デバイスの状態が変化してもキャッシュの内容が更新されずに最新の状態が読み込めなかったり、コマンドを書き込んでもキャッシュによって書き込みが遅延されたり書き込みの順番が変わってコマンドがうまく実行できない問題が発生します。

そのような問題を避けるため、MMIOバイスへのアクセスはキャッシュを無効化する必要があります。

-

ところで、ページテーブルにはキャッシュ制御の属性がありますが、自作OSを作るときに意識したことがあるでしょうか?

実は x86 CPU は非常に賢いので MMIOバイスへのアクセスを自動的にキャッシュ無効化する機能があります。

この設定をしているのが MTRR (Memory-Type Range Registers) です。

MTRR は特定のメモリアドレス範囲のキャッシュ属性を設定できる MSR のレジスタ群で、通常は MMIOバイスをキャッシュ無効にするように BIOS が設定します。

MTRR の設定とページ属性をCPUが総合的に判断してキャッシュ制御します。*1

-

さて、主題の描画の話に戻ります。

画面描画は実際には VRAM と呼ばれるメモリに書き込むことで行われます。 VRAM はディスプレイコントローラーが画面表示のために定期的に読み出す必要があるため、一般に CPU 側からのアクセスが制限された特殊なメモリになっていて、キャッシュの制御は WC (Write-Combining) が最も適しています。

WC は特殊なモードなので多くの機種ではデフォルトで設定されていません。

x86 のページ属性には WC が存在せず、 PAT で WC を設定しても手元の環境ではうまく動作しませんでした。 追記:PAT がうまく動作しなかったのはおそらく起動時のページング初期化処理のバグです。

ということで MTRR の設定を調整します。

#

MTRR はデフォルトのタイプと特定の範囲のタイプを指定することができます。

Intel ではデフォルトを UC (Uncacheable) 、実際にメモリが搭載されてる範囲に WB (WriteBack) 設定するのを推奨しています。 この設定になっている場合、 VRAM の範囲に WC 設定を追加するだけで見違えたように描画が高速になります。

しかし、 Intel の推奨と逆にデフォルトを WB 、 MMIOバイスの範囲を UC に設定になっている機種も多く存在します。 例えば、いくつかの Intel チップでは C000_0000〜FFFF_FFFF の間に VRAM が存在してまとめて UC 設定になっていることがあります。 この範囲には重要な MMIOバイスAPIC や HPET などが存在するので MTRR を分割して追加する必要があります。

しかし、 MTRR はエントリ数に上限があり、範囲指定はビットマスクによって行うため、すでに設定されてる範囲から特定の範囲を除外した設定をうまく指定できないことがあります。

特定の機種用に MTRR を調整することは可能ですが、全ての機種に汎用的に設定する万能な方法はありません。

いかがでしたか?

自作 OS で画面描画が思ったより遅い場合、 MTRR で VRAM を WC に設定すると驚くほど高速化することができます。

実際に簡単な描画ベンチマークで WC 設定前は2桁FPSしか出なかったものが WC 設定するだけで 500 FPS 以上出るようになります。

しかし、 MTRR の変更が困難な機種もあり、 PAT を使った方がより柔軟に設定できます。

*1:なお、現代のCPUはMTRRとページ属性の間にさらにPATというものが挟まっています。

最小限の USB Hub 対応 ver.2

以前 USB Hub の制御方法について記事を書きましたが、当時は USB2 Hub しか制御できていなかったのでアップデートします。

USB Hub の基本

USB ではそれぞれのデバイスに 1〜127 の 7bit のアドレスを割り当て、バス全体で最大 127 台までの USB デバイスを扱うことができます。 なお、アドレス 0 はコンフィグレーション用に予約されています。

実際の PC には多くても数個のポートしか繋ぐことができません。 それ以上のデバイスを接続するには USB Hub を使ってポートを拡張する必要があります。

USB Hub は Ethernet Hub とは異なり、独立したアドレスを持っているごく普通の USB デバイスです。 専用のドライバで制御しないとデバイスを接続してもホストと通信できません。

つまり 127 台の制限の中には Hub 自身も含まれるので、現実には Hub 以外のデバイスだけで 127 台接続することはできません。

FS / LS / HS / SS

USB 規格には大きく分けると4種類の転送速度が定められています。

  • FS / FullSpeed は元々 USB 規格で想定された規格で 12Mbps で通信可能です。 LS 以外の全ての USB デバイスは互換性のために FS として動作可能なモードを持つはずです。現代では中途半端な速度のため FS ネイティブのデバイスはマイナーとなっています。
  • LS / LowSpeed はキーボードやマウスなど低速なデバイスを低コストで実現するための規格で 1.5Mbps で通信可能です。電気仕様が FS と若干異なっていて接続した時点で認識できるようになっています。
  • HS / HighSpeed は USB 2 からサポートされた規格で 480Mbps で通信可能です。 FS を拡張したような規格で、接続直後は FS デバイスとして動作し、 Chirp という特殊なプロトコルネゴシエーションを行なってホストやハブとデバイスがお互いに HS 対応だった場合に HS に切り替わります。 XHCI ではこの動作はコントローラー側で隠蔽されるのでドライバ層で通常意識する事はありません。
  • SS / SuperSpeed は USB3 からサポートされた規格で 5Gbps 以上の速度で通信可能です。 USB3 系は転送速度の規格が若干複雑になっていて、まとめて ESS (Enhanced SuperSpeed) と表現することがあります。 USB3 は USB2 と物理層が全く異なっており、 USB3 デバイスは USB2 コネクタと別に追加された信号線で通信を行います。 *1

USB2 Hub と USB3 Hub

USB2 を乱暴に表現すると USB1 を高速化させた規格であるのに対し、 USB3 は様々な面で USB2 系とは異なるテクノロジーで構成されていて SS/ESS デバイスと FS/LS/HS デバイスの間にはいくつか明確な違いが存在します。

このことは USB Hub においても重要な違いがあり、 USB2 系 (FS/LS/HS) のデバイスツリーと USB3 系 (SS/ESS) のデバイスツリーはルートハブの時点でそれぞれ別のツリーとして扱われます。

つまり、HS Hub の下には FS/LS/HS デバイス のみが接続され、 SS デバイス が接続されることはありません。 同様に、SS Hub の下には SS/ESS デバイスのみが接続され、 FS/LS/HS デバイスが接続されることはありません。

実際の物理的な USB3 Hub には USB2 デバイスを接続することができますが、これは内部的に HS Hub と SS Hub の2つの Hub として別々に動作しているためで、 SS Hub に直接 USB2 デバイスが繋がっているわけではありません。

SS Hub は USB 規格ではオプションとなっている uuid を必ず持っていることになっていて、同一のデバイスであれば接続するプロトコルや動作モードに関わらず同じ uuid を返す決まりがあり、 uuid を比較することで相方になっている SS Hub と HS Hub を特定することが可能です。

ルートハブ

XHCI などの USB ホストコントローラー直下にあるポートのことを特別にルートハブと呼びます。ルートハブは USB デバイスとしての通常の USB Hub とは若干異なり、ホストコントローラーで直接制御します。

XHCI のルートハブのポートは USB2 のポートと USB3 のポートに分かれていて、 USB2 ポートには FS/LS/HS デバイスのみが、 USB3 ポートには SS/ESS デバイスのみが接続できます。 ステータスレジスタは USB2 ポートと USB3 ポートで同じインターフェースを使う関係上、 USB Hub のステータスレジスタと主要なフラグの相対関係は似ていますが若干内容が異なります。

通常、外部に公開されている USB3 コネクタはルートハブの USB2 ポートと USB3 ポートを組み合わせて物理的にはひとつのコネクタになっていますが、内部的な制御の流れは USB2 系と USB3 系で別になっています。

これらの接続状況は ACPI で定義されています。 myos では ACPI の対応が追いついていないのでこの辺りの情報は未対応です。

Compound Device

USB Hub 内蔵のキーボードや USB 接続の Dock のことを Compound Device と呼びます。

USB Hub は Compound Device を構成するための重要な要素にもなります。 Compound Device は物理的にはひとつのデバイスですが、 USB バスに接続されるとまずは USB Hub として接続され、 Hub のコンフィグレーションが終わるとその下にいくつかの追加の USB デバイスとして認識されます。

通常の USB Hub と Compound Device は Hub Descriptor に含まれる属性値で区別することができますが、実際のところデバイスマネージャの見た目に影響するだけで、通常の USB Hub とデバイスとして扱っても特に問題ないと思います。

Compound Device とよく似た言葉に Composite Device がありますが、 Composite Device は USB バス上では完全にひとつのデバイスとして振る舞う点が Compound Device と異なります。

RouteString

USB3 では Hub に繋がっているデバイスのルーティングのために RouteString という値を使います。

RouteString はルートハブ以外の経路のハブのポート番号を 4bit で表現したパスのような値で、最大5つまでの Hub を表現するため 4 x 5 = 20bit の値になっています。

SS Hub は RouteString を使ってルーティングするため、最大ポート数が 4bit で表現可能な 15 までに制限されます。

HS Hub は RouteString に依存しないため規格上 16 以上のポート番号を扱うことが可能で、その場合 RouteString は正確な経路を表現できません。

現実的には USB 規格でポートの消費電流の最低 100mA と最大 500mA の要件を満たすため、アップストリームから 500mA を供給してもらってハブ自身が消費する 100mA と各ダウンストリームポートに 100mA を分配できる4ポートが通常の Hub の上限となり、それ以上のポートをサポートするには電源を確保する必要があり扱いづらくなるため、4ポートより多い USB Hub はあまり存在しません。


HS Hub (USB2) の使い方

USB Hub にはひとつの Interrupt 転送エンドポイントがあり、ポートの抜き差しや異常発生時などのイベントを通知するのに使用します。 基本的な USB Hub の制御は、コントロール転送で各ポートに対して GET_STATUS でステータスを取得し、 SET_FEATURE または CLEAR_FEATURE でオン・オフを制御します。

コンフィグレーション

まずは、通常の USB デバイスと同じように Device Descriptor や Configuration Descriptor を使って通常のコンフィグレーションを行います。 Device Descriptor のクラスコードを見て USB Hub であることを認識したら、クラスドライバをロードして USB Hub 固有の初期化を行います。

USB Hub はデバイスクラスのベースクラスが 09 で、 09_00_00 が FS hub、 09_00_01 と 09_00_02 が HS hub、 09_00_03 が SS Hub を指しています。

Base Class Sub Class Protocol
09 00 00 FS Hub
09 00 01 HS Hub with single TT
09 00 02 HS Hub with multi TTs
09 00 03 SS Hub

FS Hub は基本的に HS Hub と同じ方法で扱えると思いますが、現代では入手困難なので確認できていません。 HS Hub の TT というのは FS/LS デバイスを接続した場合の速度変換用バッファのことで、 multi TTs 対応の Hub は複数の FS デバイスを接続したときの性能がシングルの時よりも若干向上するようです。 SS Hub は HS Hub と違いが多いので別項にまとめます。

Hub Descriptor

USB Hub 固有の初期化では、まず GET_DESCRIPTOR リクエストで Hub Descriptor を取得します。 Hub Descriptor のサイズはポート数によって可変長なので正確なデータを得るためには取得方法を工夫する必要があります。*2

Hub Descriptor にはいくつかの属性がありますが、最も重要なメンバーはポート数を表す bNbrPorts です。

USB2 Hub ではポート数が実質無制限 *3 なので Hub Descriptor も可変長になりますが、現実の Hub は4ポート以下が主流です。

XHCI ではデバイスが Hub の場合に特別な設定が必要なので、 Hub Descriptor を元に Slot Context の Hub、 MTT、 Number of Ports、 TTT あたりの Hub に関する設定を行い、最後に EVALUATE_CONTEXT コマンドを XHCI に送信して設定を反映します。

GET_STATUS SET_FEATURE CLEAR_FEATURE と各ポートの初期化

ハブの構成が終わったら各ポートの初期化を行います。

まずは、全てのポートに対して SET_FEATUREPORT_POWER を送信して電源をオンにします。

for port in self.hub_desc.ports() {
    self.set_port_feature(UsbHub2PortFeatureSel::PORT_POWER, port).await?;
}

次に、全てのポートに対して CLEAR_FEATUREC_PORT_CONNECTION を送信します。 これは念の為で送らなくてもいい気がします。

最後に、全てのポートに対して GET_STATUS でステータスを取得して PORT_CONNECTION ビットを調べ、1だった場合は最初からデバイスが繋がっているので個別にポートリセット(後述)します。

Interrupt 転送ループ

USB Hub の初期化が完了したら Interrupt 転送エンドポイントのデータを読み込む無限ループに突入します。

Interrupt 転送エンドポイントから読み出せるデータは各ポートで状態変更イベントがあったことを示す数バイトのビットマップとなり、 0 以外の値を読み出した場合はどこかのポートで何かしらのイベントが発生したことを示しています。

ポート番号は1から始まるのでビット番号も1から始まり、ビット0は Hub 本体でイベントが発生したことを示す特殊なビットになります。

状態変更イベントがあったことを検知したら、該当するポートのステータスを GET_STATUS で取得します。

GET_STATUS で取得するデータは 32bit 値で、下位 16bit は現在のステータス、上位 16bit はステータスビットに変更があったことを示すフラグとなります。基本的には下位 16bit と上位 16bit は同じビットが同じステータスに対応しています。 例えば、ポートの接続状態が変化すると PORT_CONNECTION ビットが変化し、同時に C_PORT_CONNECTION ビットが1になります。

ステータスの C_PORT_CONNECTION ビットが1になっていた場合は、次に PORT_CONNECTION ビットの値を調べます。 PORT_CONNECTION が1になっていた場合はデバイスが接続されたイベントなのでポートリセット(後述)を行なってデバイスのコンフィグレーションを行います。 0の場合はデバイスが切断されたのでデバイスツリーから該当のポートのデバイスを削除します。

ただし、 C_PORT_CONNECTIONPORT_CONNECTION だけで抜き差しを判断しようとすると、チャッタリングやノイズの影響でデバイスが存在しないのにデバイスを抜いたと判断するような誤動作が発生することが稀にあるので、実際の抜き差しの判断はドライバ内部で管理している接続状態も考慮した方が良さそうです。

C_PORT_CONNECTION 以外も本来はエラーのリカバリなどを行うべきですが、いずれかのビットが1の間は常にデータを送信してくるので、 C_ で始まるビットはとりあえず最後に CLEAR_FEATURE でクリアしておきましょう。

ポートリセットとコンフィグレーション

USB Hub の各ポートにデバイスが接続されたことを検知したら、デバイスを使うための準備が必要です。

まずは、 SET_FEATUREPORT_RESET を送信します。

少し待ってから GET_STATUS でポートのステータスを取得し、 PORT_ENABLE が1になっていることを確かめます。 デバイスのリセットが成功して Default フェーズになり、 USB バス上では「アドレス 0 」で通信できる特殊な状態になっています。

次に、先ほど取得したステータスからデバイスの速度を判別します。 PORT_LOW_SPEED が1の場合は LS デバイスPORT_HIGH_SPEED が1の場合は HS デバイス、それ以外の場合は FS デバイスです。 先述の通り HS Hub に SS デバイスが接続されることはないので考慮する必要はありません *4

ホストコントローラーに Hub 自身の情報とポート番号とデバイスの速度を伝えて新しいデバイスを接続する準備をします。

XHCI の場合、ここで ENABLE SLOT コマンドでデバイススロットを割り当て、 Hub からもらった情報をもとに Device Context や Slot Context を作ります。 ルートハブに繋がっているデバイスと違い、ルーティング情報としてルートハブに繋がっているポート番号や RouteString を設定する必要があります。 デバイスの速度によっては parent Hub の情報も設定する必要があります。 最後に ADDRESS DEVICE コマンドを実行すると Device Context が有効になり、この時デバイスSET_ADDRESS リクエストが送信されてアドレスが割り当てられます。

ここまで終わると該当の USB デバイスAddress フェーズになっているので、通常の USB デバイスと同じようにコンフィグレーションを進めると Configured になってデバイスが使えるようになります。

なお、 Default フェーズのデバイスが存在する状態で他のスロットでコマンドを実行すると宛先がよくわからなくなって USB Transaction Error が起こりやすくなります。ポートリセットからコンフィグレーションの間は排他制御した方が良いです。


SS Hub (USB3) の使い方

SS Hub では Hub Descriptor 、ステータスレジスタ、 Feature セレクタなど HS Hub と内容が異なりますが、基本的な考え方は HS Hub とあまり変わらないので差分を調整する程度で同じような流れで対応できます。

USB3 には FS/LS/HS の区別がないなど削除された機能や代わりに追加された機能もあるためステータスのビット位置も若干変わっています。

また、 SS Hub はポート数の上限が 15 までと決まっているため Hub Descriptor のサイズが固定長となります。

HS Hub と SS Hub の制御で大きく異なる点は、 SET_HUB_DEPTH リクエストで Hub 自身の Route String の長さを設定しないと動作しません。


いかがでしたか?

ここまで実装することで、 myos はとりあえず USB Hub が動くようになって多くの USB デバイスを接続できるようになりました。

*1:USB2 は1対の差動信号で半二重通信します。 USB3-A では追加された2対の差動信号で全二重通信し、 USB-C ではさらに追加の2対の差動信号を使って転送速度を増やしたり映像信号を流すことができます。

*2:実際のところ後ろの方の DeviceRemovable 等は無視して最小サイズで取得しても実害はあまりないです

*3:bNbrPorts が 8bit で上限 255 に対して USB アドレスは 7bit で上限が 127 なのでバスの上限を超えます

*4:実は USB3-A コネクタをゆっくり挿入すると USB2 の回路が先に繋がって USB2 デバイスとして認識する場合があり、その場合はおそらく HS デバイスとして動作します

最近の myos

最後にこの記事書いたの何ヶ月前だったかな?な感じなのでタイトルも変更です。

綺麗なウィンドウシステムがそれなりに動くようになって逆に見た目の進化が乏しくなってしまった myos ですが、今も中身は進化しています。

外部アセンブラの廃止

高級言語で OS を作るにあたり、アセンブラに一切頼らないで作るのはおそらく不可能でしょう。

myos も 99% の処理は rust で作っていますが、一部にアセンブラが必要な処理があります。

myos を作り始めた頃の rust でインラインアセンブラを使うには、汚名高い AT&T 表記と gcc っぽいレジスタ指定が必要で正直使いやすいとは言えなかったので、外部アセンブラで作ったモジュールを build.rs を使ってビルド時にリンクするというやり方で対応していました。

しかし、この方法は解消したいと思っていました。

やがて rust 側のインラインアセンブラの文法が整理されてだいぶ使いやすくなったので、アセンブラのモジュールを廃止してインラインアセンブラに統一することにしました。

ここで問題となるのが、 SMP 初期化部分です。

x64 の SMP の初期化ではリアルモードからロングモードに遷移するコードを実行するためインラインアセンブラで記述できるかどうかも微妙で対応が難しそうです。 一方、コードとしてはカーネルと独立しているのでインライン化する意味も薄く、外部アセンブラモジュールをリンクする必要もあまりありません。

SMP 初期化部分は頻繁に更新しないしサイズも小さいので、別途外部アセンブラでバイナリファイルを作って include_bytes! マクロで読み込み実行時に配置して関数ポインタを使って呼び出すように変えました。 フルビルド時には全て更新しますが、通常のビルドではレポジトリにある smpinit.bin をそのまま使います。

ファイルシステムUNIX 要素

myos の設計目標のひとつに「UNIX 互換にしない」というのがあります。

汎用 OS は UNIX 系かそれ以外かに大別できるほど UNIX の影響力が強いです。

myos は UNIX 互換ではないので UNIX 系 OS の有名なシステムコールである fork には対応していません。 その他の UNIX の代表的な要素もほとんどはサポートされていないし、サポートする予定もありません。

しかし、 UNIXファイルシステムの仕組みはよくできています。 あの MS-DOS でさえ、ファイルシステムUNIX の影響を強く受けているほどです。 *1

というわけで、裏で動いてる仕組みは UNIX とだいぶ違いますが、 マウントによるファイルシステム拡張とデバイスファイルをサポートしました。

現状デバイスの種類によってインターフェースがバラバラになっていますが、デバイスファイルで整理できるものは整理したいです。

今後の予定

現在の API はとりあえず動けばいいという感じであまり洗練されてない部分も多いです。 ファイルシステムの改修が落ち着いたら API を整理したいです。

*1:MS-DOS version 2 最大の特徴のひとつが、 CP/MファイルシステムUNIX(XENIX) のファイルシステムを当時の技術限界でハイブリッドに融合させたことだと思ってます。

Kano PC

教育用パソコンとかいう奴らしいです

元は5万円くらいしたようですが、現在は2万円くらいで手に入るので購入してみました。 結論から言うと、ハードウェアはゴミです。これを買うくらいなら mouse の E10 の方がはるかにマシです。

windows がギリギリ動くスペックで実用性はありません。 見た目は 2in1 の形に見えますが、分厚く重いのでタブレットとして使うのは無理があります。

独特なアプリが付属しているのは興味深いですが、 CPU の説明に嘘があったり色々最適化が甘いです。

以降は自作 OS (myos) の観点から

Landscape 液晶です (超重要)
Portrait 液晶のような小細工しなくてもちゃんと画面描画できます。
しかし、同じ CPU で Portrait 液晶の mouse E10 の方が gbench が高速に動作します。謎です。

キーボードはボゴピンで接続する USB HID となっています。 独自コネクタで作りが甘いせいか、接続不良で Address Device コマンド発行時に USB Transaction Error になりやすいです。

この HID デバイスには Interface が2個あって、1個目は HID Boot Keyboard となっていて概ね普通のキーボードとして振る舞います。

顔文字の刻印された謎のキーを押すと 08 00 37 00 00 00 00 00 という謎のデータが送られてきます。 これは Left GUI + "." と解釈され、 windows 上では絵文字パットが起動します。 *1

問題は2番目の Interface で、クラスコードは HID Boot Mouse になっているのでトラックパッドのように見えますが、 実際の内容をみるとそれ以外のデータ (キーボードのマルチメディア制御キーなど) も送られてきます。

ファームウェアのバグなのか、特定のキーを押したときに 02 00 00 という謎のデータが一緒に送られてくることがあります。 これはレポートの定義と矛盾した謎データに見えます。

なお、執筆時点の myos では肝心のトラックパッドのデータを取得できていません。

windowsバイスマネージャの状況証拠から USB HID 接続の可能性が高く、 chromeOS でも普通に使えたので特殊なドライバは必要ないと思います。

HID デバイスに Usage 000D_000E (Device Configuration) というものがあって、そこで何か設定しないとダメな感じがしますが、レポートのパースに失敗しているのか有効なデータを設定することができていません。

繰り返しになりますが、これを買うくらいなら mouse の E10 の方がマシです。

*1:厳密には最初に 08 00 00 00 00 00 00 00 が送られて、 Left GUI を押したと認識させた一瞬後に "." のデータが来ます

raspberry pi ベアメタルでマルチコアを利用する方法

入手困難とうわさの raspberry pi を偶然にも入手たので最近触っています。

raspi の CPU は4コアで動作しますが、現在のファームウェアは最初のコア (ID 0) のみ起動します。*1

他のコアがどうしているかというと、ファームウェア物理アドレス 0x0000_00E0, 0x0000_00E8, 0x0000_00F0 の値を監視しているので、ここを書き換えてやれば起動できます。

例えば 0x0000_00E0 に 0x0008_0000 (_start のアドレス) を書き込むと ID 1 のコアが _start から実行開始します。

実際に書き込む場合、マルチコアでメモリ書き込みを保証するために Release のメモリーオーダーで書き込み、 sev 命令でイベントを通知します。

Rust だとこのような感じになります。

    for p in [0xE0, 0xE8, 0xF0] {
        let p = &*(p as *const AtomicUsize);
        p.store(_start as usize, Ordering::Release);
        asm!("sev");
    }

このコードを実行すると各コアが _start から実行開始するので、それぞれ mpidr_el1 レジスタを読み出して下位2ビットからコア ID を取得し、シフトした決め打ちのスタックポインタを設定し、 bl 命令で初期化関数を呼び出します。

_start:
    mrs     x1, mpidr_el1
    and     x1, x1, #3
    cbz     x1, 2f

    lsl     x2, x1, #16
    add     x2, x2, #0x10000
    mov     sp, x2

    bl      _smp_main

1:  wfe
    b       1b
2:
    (省略)

これだけでマルチコアが起動できました。

APIC の設定を弄ったりロングモードの用意が必要な x86-64 に比べると、 Arm64 のマルチコア起動はスッキリしていて簡単にできますね!

と、言いたいところですが、このままでは atomic 変数の read-modify-write や CAS 操作に必要な命令 (stxrなど) が正しく動作しません。 CAS 操作ができないと spinlock を実装できないのでコア間で同期を取る事ができません。 つまり、コアごとに完全に役割分離しているような特殊な用途を除いてマルチコアを活用する事ができません。 CAS 操作を正しく実行するためには、 MMU とキャッシュの設定が必要になります。

この辺り x86 は適当に書いてもなんとなく動きますが、 arm では真面目にちゃんと設定しないと意図した通りに動いてくれないので難しいです。

なお、 Arm 系 CPU は基本的に同様のプロトコルで起動できるようで、各コアの監視アドレス 0xE0, 0xE8, 0xF0 は DeviceTree の各 CPU にある cpu-release-addr で確認できます。

  cpus {
        #address-cells = <0x01>;
        #size-cells = <0x00>;
        enable-method = "brcm,bcm2836-smp";
        phandle = <0xdb>;

        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x00>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xd8>;
            phandle = <0x28>;
        };

        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x01>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xe0>;
            phandle = <0x29>;
        };

        cpu@2 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x02>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xe8>;
            phandle = <0x2a>;
        };

        cpu@3 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x03>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xf0>;
            phandle = <0x2b>;
        };
    };

*1:過去のファームウェアは全てのコアが一斉に起動していた時代があるようで、ネットで raspi ベアメタルのやり方を調べるとスタートアップ時にコアを判別して ID 0 以外のコアで無限ループするやり方がよく紹介されています