無駄なレジスタ退避が付いてくる

昨日、マイコンのプログラムを3つのプロジェクトに分けてビルドするようにしたという話を書いた。

詳しい事情は書かなかったのだが、背景の1つには割り込みハンドラがある。


割り込みハンドラではレジスタの退避を適切に行う必要があり、

最初はこの退避処理をアセンブラで記述していた。

全ての割り込み要因に対して共通の割り込みハンドラを用意して、

アセンブラで退避処理をした後に、要因ごとの処理に分岐して、

処理が終わったらアセンブラに戻ってきて復帰処理をするという流れである。


で、実はこれを作っているときに __interrupt とか付けて関数を記述すると

コンパイラがレジスタの退避処理などを含めたコードを生成するのでは?

という教えてもらって、コンパイラのマニュアルを見たら、確かにあった。

それで生成された機械語を確認してみたのだが、どうも思ったのと違って、

要因ごとの分岐処理も必要だし、従来通りアセンブラで退避処理を書く方法で行くかと一旦は考えた。


その後、割り込みハンドラについて、いろいろ見直しが入って、

ベクタテーブルを使って割り込みハンドラを振り分ける方がよさそうだとなった。

命令が並んだベクタテーブル

従来は1つの割り込みハンドラにアセンブラで退避処理を書けばよかったが、

要因ごとに割り込みハンドラを作ると、それぞれに退避処理が必要になる。

アセンブラのマクロで同じ処理を何回も書かせてもよいのだが、

そういえば関数に __interrupt  を付けたらコンパイラが生成してくれるんだよなと思い出して、

適用してみたら、振り分け処理がない前提ならそう悪くない気がしたのだが、

浮動小数点レジスタの退避が必ず入る点がうーん……となった。


特に浮動小数点演算とは関係ない処理なので浮動小数点レジスタの退避はいらないのだが、

コンパイラとしては割り込み元で浮動小数点演算をしている中で、

浮動小数点演算を行う割り込みハンドラが入る可能性があると考えてしまうようだ。

浮動小数点レジスタの退避・復帰というのは比較的大きな処理なので、

退避・復帰を省略したり後回しにするという手段がしばしば使われる。

ただ、OSレスだとこういう効率的な手法はやりにくいのかもしれない。


浮動小数点レジスタの退避をなくすだけならば、FPU無効の構成でビルドすればよいのだが、

そうすると別の問題が発生してしまうので、これもNGだった。

その問題を回避する手段として、プロジェクトを分割して、

FPU有効のプロジェクトと、FPU無効のプロジェクトに分けることにしたと。

だいたいそんな感じですね。実際はもっと複雑な事情があるのだけど。


結果として、アセンブラでのレジスタ退避を手で書くところはなくなった。

ただ、全ての割り込みハンドラを __interrupt を付けた関数にしたわけでなく、

特殊な事情があり割り込みハンドラの入口をアセンブラで書いてあるものがある。

ただ、その割り込みハンドラは元に戻れなくてよいというものなので、レジスタ退避そのものが不要である。

最初は元に戻れなくてよい割り込みもレジスタ退避を行っていたのだが、

よく考えたらいらないねというところから見直しが進んでいった事情がある。

いろいろ普通ではない事情が重なって現在に至るという感じはある。

複数プロジェクトを連続ビルドする

もうだいぶ前の話になるのだが、現在、開発中のプログラムで、

RAM展開するプログラムのコードをROM化して格納する設定を行ったが、

どうにも思ったような生成物が作れずにコンパイラのメーカーとやりとりしていたことがあった。

わりと難航して行き着いた先が2つのプロジェクトに分けるという方法だった。


だいたいこんな感じですね。

  1. RAMコード用のプロジェクトでビルド
  2. RAMデータのbinファイルを出力する
  3. ローダーのプロジェクトで2.のbinデータをセクションに配置させる
  4. ROM上で動くローダーのコードをビルド
  5. ROMデータをIntel Hex形式で出力

といったフローである。

これで2つのプロジェクトを連続して実行すればよくて、

それはバッチビルドで2つのプロジェクトを指定した順序で実行するようにすればOKと。


で、最近いろいろ手を加えていたら、RAMコードのプロジェクトを2分割することになった。

このマイコンのかなり特有の事情によるものなので詳細は書かないが、

特定部分のコードを生成したら一旦binファイルに出力して、

同じアドレスで別プロジェクトに貼り付ける方法がよいだろうと。

結果として3つのプロジェクトを連続してビルドすることとなった。


上記を見てもわかるのだが、各プロジェクトの最後にはbinファイルで出力するとか、

Intel Hex形式で出力するという処理が入っている。

このあたりの出力はビルド後に実行するコマンドの中でやっている。

出力範囲を指定したり、オフセットがあったり、単純な出力機能では対応できないことも理由である。


この出力ファイルの拡張子なのだが、あまり定まったものがないのだなと最近気づく。

というのもIntel Hex形式の拡張子について、

あるプログラムでは.hexで、あるプログラムでは.ihexで、あるプログラムでは.ihxで、

わりとバラバラでどの拡張子で出力するのか錯綜していた。

同じような話はモトローラのSフォーマット、従来よく使っていた形式で、

.mot という拡張子で見ることが多かったが、.srecとか.sとか他の拡張子も使われている。

.sってのはアセンブラ言語の拡張子で見ることが多いから、出力ファイルで使うのはどうかと思うが。

拡張子はなんでもいいんでしょうけどね。

Shift_JISというよりCP932

先日、Shift_JISのダメ文字の話を紹介した。

コメントにあったダメ文字

結局はShift_JISを使うからダメなんだよということでUTF-8に変換することに。


昔、Pythonで変換用スクリプト書いたよなと思ったらこれだわ。

どの文字コードでドイツ語を表している?

CP437という古い文字コードをUTF-8に変換して読めるようにしたと。

こういうことができるツールはなかなかなかった。

Shift_JISからUTF-8への変換はできるツールもいろいろあるけどさ。

encoding=’shift_jis’ で開いて、encoding=’utf-8′ で保存すると。

これでOKだろうと流すと、can’t decode byte とエラーが出る。

バイト数で言われてもわかんねぇよと思いつつ、バイナリエディタで開いて確認すると、

「No.」 の合字のところでひっかかってた。

あ、厳密にShift_JISの範囲外だとハネられるのね。


Windowsで使われているShift_JISというのはJIS X 0208に含まれない字が入っている。

かつて機種依存文字なんて言われたのはこのあたりの文字である。

Windowsで使われている拡張されたShift_JISのことはCP932と呼ばれることがある。

CP932にもいろいろ歴史があるらしいのだが、今はこの理解でよいらしい。

Pythonでは encoding=’cp932′ を指定すれば読み書きできる。

というわけで encoding=’cp932′ で開いて、encoding=’utf-8′ で保存するというのが答えだった。


なんでそんな字をコメントに使ってんだよとは思うけど、

そこを今さら問うても仕方ないし……UTF-8にしてしまえば何も問題なくなるし。

Windowsで作ったShift_JISのファイルの移行としては、CP932指定の方が安全なようであるというオチかなと。


移行したソースコードを使っていろいろ実験していたけど、

移植する上でめんどくさい要素はいろいろありそうですね。

今までノーマークだったペリフェラルに依存する機能があったり、

ポインタ型の使い方が複雑怪奇だったり……

どちらもなんともならない話ではないのだが、丁寧に整理しないと間違うところなので、

ただでさアーキテクチャの違いでいろいろ修正が必要なのに、

それに加えてこういう要素を全部丁寧に見ていくのはいかにも大変である。

果たしてどうなることやら。

コメントにあったダメ文字

古いマイコンで動いていたソースコードを、

とりあえず新しいマイコン用のコンパイラに突っ込んだら、当然エラーがたくさん出てくる。

コンパイラ依存の表記などは文字列の一括置き換えでドカンとつぶしていく。

ワーニングはともかく、エラー数はかなり減ってきたところで、

よくわからないところで “unexpected …”というエラーが出てきた。

一般的には括弧の対応関係が合わないと発生するエラーだが、そうは見えない。


このような謎を解明する手段としてプリプロセッサの結果を出力するオプションを有効にした。

すると、if文の閉じ括弧の行が消失していることがわかった。

なんで? と思ってソースコードを確認してみるとこんなのだった。

if(foobar){
   some(); //○○機能 }

あっ!? そういえばこのソースコードの文字コードってShift_JISだったよな。

と「能」のShift_JISの文字コードを確認すると{0x94, 0x5C}だった。

これ典型的なダメ文字じゃないか。


0x5CはASCIIコードで言えば”\”(バックスラッシュ)に相当する。

すなわち、Shift_JISを解釈する機能を持たないコンパイラにとっては

    some(); //■■■■@■\

と書いてあるのと同等である。(ASCII・半角カナ範囲外の文字は「■」で代用)

それでC言語で行末の\は継続行であることを表す。

すなわち閉じ括弧の書かれた次の行もコメントに含まれてしまったのである。

とりあえずはこうやって回避した。

    some(); //○○機能//←ダメ文字//

末尾が\にならなければ回避できるので適当な文字(上の例では “/”)を置けばよい。


コメント部分ならどんな文字コードでもよいと思ってたが、行末の\はダメなんですね。

ダメ文字が問題となりやすいとされるのが文字列リテラルである。

典型例として “表示” というのが “■\■ヲ” という並びになり、

文字列リテラルに\が含まれるとエスケープシーケンスに当てはめようとするが、

この場合はエスケープシーケンスに該当しないので \■ はそのまま ■ として解釈される。

結果として “■■ヲ” となり、この ■■ という部分はShift_JISで解釈しなおすと 侮 という文字になる。

というわけで “表示” が “侮ヲ” になるというのが昔はCGIなどであるあるネタだったのだという。

今回のシステムではこういう問題はない。ASCII範囲外の文字はコメントだけである。


ダメ文字問題は EUC-JP や UTF-8 では発生しない。

どちらもASCII範囲外の文字は 0x80~0xFF でエンコードされるためである。

Shift_JISは半角カナを1byteに押し込むために漢字の2byte目がASCIIコードに被らせてるんですよね。

こういう構成が問題を引き起こしかねないことは昔から知られていたが、

1byteカナの移植性を重視したShift_JISがWindowsで長らく標準的に使われていたのは知っての通り。

今だとUTF-8がいいでしょうね。漢字が3byteになっても困らないし。

というわけで移植にあたってはUTF-8に変換した方がよいという発見だった。


まさか2024年になってダメ文字に遭遇するとは思わなかった。

今までプライベートでも仕事でもダメ文字が問題になった覚えはないな。

Shift_JISを正しく解釈できる環境を使ってたのか、EUC-JPかUTF-8を使ってたのか。

どっちが主因なのかよくわかりませんけどね。どっちもありそうだな。

オール0の初期化データが作られて困る

最近、リンカスクリプトをいろいろいじっていたのだが、

その中で少し困ったことがあって、それはオール0の初期値あり変数セクションが生成されてしまうことである。


一般的に使われるセクション名としては下記のものがある。

  • .text : マシンコード
  • .rodata : 定数
  • .data : 初期化データのある変数
  • .bss : 初期値0の変数

.text と .rodata は不変なのでマイコンではROMに割り付けることができて、

.data と .bss は変数なのでRAMに割り付ける必要があり、

その中で.dataは起動時に初期化データをRAMに転送しなければならない。

おおざっぱにはこういう理解でよい。


ただ、いろいろな都合でこれ以外のセクション名を指定することもある。

今回問題となったのはスタック領域について .stack というセクション名を付けたものだった。

T_STK __stack[STACK_SIZE] __attribute__((section(".stack"),nocommon));

こんなコードが自動生成されていた。

これにより .stackセクションに__stack[]という配列が確保されるわけだが、

なぜか初期値0の変数ではなく、オール0の初期化データのある変数という扱いになってしまう。


よくわからないのでいろいろ実験してみたのだが、

attributeで指定しているセクション名を .bss.stack としたら、初期値0の変数として扱われることがわかった。

自分で指定するセクション名が.bssから始まると、.bss同様の扱いになるが、

それ以外の名前を指定すると変数の場合は .dataセクション同様の扱いになってしまうと。

どうもそういうことらしい。


コンパイラ(gcc)で生成されたオプジェクトファイルの解析結果からすると、

上記の差異はgccで発生しているように見えるのだが、対策はリンカスクリプトで行う。

.stack (NOLOAD) :{
   *(.stack*) } > RAM_WORK

.stackセクションをRAMに割り付けるときにNOLOADと指定すると、

初期化データを転送する処理がいらないという意味になるらしい。


特別な事情がなければ冒頭に書いたセクション名を使うとよくて、

エントリポイントのセクション名として .text.startup を指定して、

.text.startupセクションをROM先頭に割り付けるという使い方があった。

こういうやり方にした方がいろいろな問題を回避しやすいように思える。

.bss.stackセクションならば今回の問題は発生しなかったわけだしね。


とはいえ、最終的にはリンカスクリプトでなんとでもという話のようだ。

最初はコンパイラ側でどうにかするのかと思ったのだが、

リンカ側で対応するべきことであると気づくまでに時間を要してしまった。

サンプルコードに付属していたリンカスクリプトはこのあたり対策されてなくて、

オール0の初期化データが大量発生していたのだが、サンプル書いた人はおかしいと思わなかったのだろうか?

動くという点では当然問題ないのだけど……

AQUOS R9を買う

先週思い立ってスマートフォンを買い換えた。

Androidスマートフォンを使うようになってずっとSHARPで、

今回購入したのもSHARP製だが、AQUOS R9という新発売のハイエンドモデルである。

従来使っていたのはAQUOS senseシリーズ(相当するAndroid Oneを含む)で、

これは比較的安価なスタンダードモデルだが、今回は奮発して乗り換えた。


正直なところ、今まで使っていたAQUOS sense6は不満が多かった。

有機ELディスプレイ(IGZO OLED)の色ムラや短期的な焼き付きが気になったり、

屋外で長時間使ったときなど発熱が気になったり……

ただ、乗り換えるにも決め手がなかった。

SamsungのGalaxyとかも考えたのだが、なかなかピンと来なかった。


そんな中でAQUOS R9の話を知ったのだが、このあたりの不満が解消されてそうに見えた。

SIMフリータイプで108000円ほど、今までのに比べると高いけど……

でもスマートフォンとしては普通の金額ではあるんですよね。

Yahoo!ショッピングのキャンペーン合わせでPayPayボーナスが2万円ぐらい付いた。

さらに8月中に購入すれば1万円のキャッシュバックがあるので、正味7.7万円である。

発売直後で入荷時期未定という店が多かったが、納期が見える店があって、

それで購入したら今日に届いた。ちょうど休暇だったので半日かけて立ち上げ作業をしていた。


AQUOSシリーズはその時々でよい方法を選んでいるのか各種機能の配置がコロコロ変わる。

最初に手にした指紋認証対応のAQUOS sense3は画面下にホームボタン兼用の指紋認証器があったが、

AQUOS sense6では画面内蔵になり、AQUOS R6は側面の電源ボタンに組み込まれた。

今までの中では一番便利ではあるけど、同じメーカーでここまでコロコロ変わるのもねという気はする。

電池容量が大きいことぐらいしか共通的な特色はない気がする。


少なくとも今まで不満だった画面についてはかなり改善されたようだった。

従来のIGZO OLEDがひどかったという側面もありそうだけど。

熱についてもベイパーチャンバーを搭載と、発熱を分散する機能は強化されたよう。

これが実際どれぐらい効果を発揮するかですが。

どっちも今までより悪くならんだろうとは思うが、期待通りにいくか。

カメラもレンズ部分がかなり大きいだけあって綺麗ですね。

気になるのは今までより少し大きく、少し重くなったことぐらいか。


いろいろ不満はあったけど、今のAQUOS sense6は、

前に使っていたAQUOS sense3の画面が割れたときに急いで調達したもので、

期待外れな面は多々あったが、中古品とはいえ即納で、2年弱活躍してくれた。

もっと早く変えてもよかったと思っていたが、それなりに活躍してくれた。

ラクウルで買取に出すけど、果たしてどれぐらいの値段が付くか。

でも、案外値段が付きそうな気もしていて、かなりコストパフォーマンスはよかった。

AQUOS R9が期待に応える端末かはまだわからないところはあるけど、

ここまで引っ張ってくれたなら悪くない活躍だったのでは。

譲り合いのサービスコール

新しいシステムで使うリアルタイムOSのテスト用プログラムを動かしていた。

タスクの切り替わっている様子をコンソールで確認出来るというもので、

よく使われているテストプログラムなのかな?

と思いながら使っていたのだが、最初は動作がよくわからず苦戦した。


ただ、いろいろ触っていてわかったのは、これから構築するシステムとは少し違うということである。

このテストプログラムはいろいろなテストができるのだが、

その中でも同じ優先度のタスクをローテーションする動作の確認がいろいろある。

例えば、定周期で同じ優先度のタスクをローテーションするといった動作である。

移植元のシステムでは同じ優先度というのは存在しないので、こういう動作はない。


同じ優先度のタスクをローテーションするというのは、

μITRON系のリアルタイムOSのrot_rdqというサービスコールである。

rotate ready queueの略でレディーキューの回転を行うとある。

タスクが起床できるようになれば優先度ごとに設けられたレディーキューに入る。

今動いているタスクより高い優先度に誰か並べば、そちらに譲るわけだけど、

同じ優先度のタスク同士だと勝手にはタスクは切り替わらない。

もちろん待ち状態になれば、同じ優先度で待っているタスクに譲られるが、

そうでない場合でもrot_rdqを実行すると、今実行しているタスクはレディーキューの最後尾に並び直しになり、

もしも同じ優先度で待っている他のタスクがあればそちらに譲られる。

他に同じ優先度で待っているタスクがなければ、同じタスクに戻ってくる。


同じ優先度で複数のタスクを実行しておいて、定周期などでrot_rdqを実行すると、

その同じ優先度で実行されているタスクの間でゆずり合いが発生する。

これだけならば低い優先度のタスクが実行されることはない。

そのタスクより高い優先度のタスクが全て待ち状態にならなければ実行されない。

あくまでも同じ優先度で複数のタスクが待っている場合のみ有効である。

こういうテストプログラムが存在するってことは、典型的な使い方の1つなんだろうな。


今後は移植元のシステムのタスク切替を模擬するようなプログラムを作りたいが、

実際のシステムはms単位よりも細かく切替が発生していくので、

こうなるとコンソールで動作確認とはいかないのでやり方は考えなければならない。

優先度の上下関係は明確に決められていて、そこがはっきりわかるようなテストにするのかなと。


今までリアルタイムOSがある上で動くものを作ることがなくて、

これをやる前にはいろいろ勉強はしてるんだけど、この仕組みは知らなかったな。

キャッシュ領域が最強だった

以前、こんなことを紹介した。

予約してメモリを書き換える

Load-Link/Store-Conditional(LL/SC)方式というのがあって、

メモリ上のあるアドレスを予約して、他のコアが触っていないことを確認してからライトする。

このような形で排他制御を実現しようとしているのだが、この方式には制約があり、

この方式は予約したアドレスへの他のコアのアクセス有無がわかればよいが、

この仕組みが働く領域と働かない領域があるようで、

その都合でcmpxchg関数でプログラムが停止する問題が起きていた。

というわけでどの領域でも使えるわけではなかったという話。


で、いろいろ資料を見ていて判明したのは、この仕組みはキャッシュが働く領域だけ働くということだった。

これはCPUによっても違うんだろうけど、典型的にはそうらしい。

一見するとキャッシュがある方が排他制御はやりにくそうなのだが、

キャッシュを使う場合、キャッシュコヒーレンシが求められる。

複数のコアで各々キャッシュを持っていて、あるコアがキャッシュ上のデータを更新したとき、

他のコアはその領域のキャッシュデータを無効化したり更新しなければならない。

元々そういう仕組みを持っているキャッシュ領域ではLL/SCはやりやすいと。


LL/SCはあるアドレスを予約してから、そのアドレスを他のコアが触っていないことを把握できればよいので、

キャッシュコヒーレンシの仕組みに頼らずに実現する方法もありそうだが、

もっとも典型的なのはキャッシュコヒーレンシの仕組みに乗っかる方法らしい。


同じような排他制御が求められる処理にアトミック操作というのがあって、

例えばメモリスワップを1命令で排他的に行う仕組みを持っている場合がある。

メモリのデータをリードしてr0に格納して、r1の値に書き換える

というのを排他的に一息でやってしまうような命令である。


こういう命令はシングルコアでも価値があって、

なぜかというとリード・ライトを2命令でやると、その間に割り込まれる可能性があるから。

(これを防ぐためにはこの間を割り込み禁止にする必要がある)

ところが、1命令でやればその間に割り込まれることはなくなる。

なのでシングルコアの時代からこういう命令は活用されてきたらしい。

ここにマルチコアの時代が来て「排他的に」実現することが必要になったと。


これもキャッシュコヒーレンシが働く領域だと対応しやすいようだ。

キャッシュ上でスワップして、他のコアのキャッシュを無効化・更新すればよいから。

これも他の実現方法はありそうだが、典型的にはこうやるらしい。


というわけでキャッシュ領域が最強なわけですね。

キャッシュを介することで実メモリとの整合は取りにくくなるが、

コア間の整合はもともと意識されているので排他制御はやりやすいと。

そういう意味だったのかといまさら理解した。

_Generic文の罠

互換性の問題で、変数の値に応じて異なる形の構造体を使いたいという話がある。

イメージとしてはこんな感じ。

ftype==1 ? ((X1*)ptr)->id : ((X2*)ptr)->id

X1型とX2型で同じidというメンバでもオフセットが異なるんですね。


こういうのを簡略化するマクロを作れないかということで考えていた。

見ての通り、キャストが入っているので、上の書き方はptrの型によらず合法になってしまう。

それはちょっと怖いなと思って、なにかいい手はないかと調べていた。

そしたらC11から_Genericという型に応じて式を選択する構文が導入されていて、

それが使用予定のコンパイラでも使えることが判明した。

C11って2011年版のC言語って意味で、なんとC言語は近年もエンハンスメントされているのである。


_Genericの例題として、型に応じて関数を使い分けるというのがある。

#define XFUNC(VAL) _Generic((VAL), int: xfunc_int, short: xfunc_short )(VAL)

とすると、マクロ引数の型によって xfunc_int, xfunc_short を選択して、その関数に対して引数を渡すという内容になる。

なお_Genericの入力は型名の判定を静的に行うので、VALの中身に関数など書いたとしても、

VALの中身が評価されるのは_Genericで選択された関数の引数に渡されるときの1回のみである。

こういうのはいいんですけどね。


そんなわけで冒頭に書いたような処理を2つの型について書いてみた。

#define ATTR(OBJ, ATTR) *_Generic((OBJ), \
     X : (ftype==1 ? &(((X1*)&(OBJ))->ATTR) : &(((X2*)&(OBJ))->ATTR)), \
     Y : (ftype==1 ? &(((Y1*)&(OBJ))->ATTR) : &(((Y2*)&(OBJ))->ATTR))  \
   )

そしたらうまくいかないんですよね。

ATTR(xval, id) と書く。xvalはX型の変数である。

そうすると X:… として書いた部分だけが使われると思いきや、

実際には選ばれない他の型の式も合法でなければならない。

Y1型にidというメンバがいない場合、((Y1*)&xval)->id という表記は認められない。

この結果、コンパイルエラーになってしまうのである。


なんでそうなるのかという感じはあるが、そういうもんらしい。

関数名を選択するケースでもそういう問題はあって、

_Genericに書かれる関数名は全て存在する関数名でなければならない。

#define FUNC1(FUNC, OBJ) _Generic((OBJ), \
     X : __X1_##FUNC, Y : __Y1_##FUNC)(OBJ)

として、FUNC1(add, xval) とすると __X1_add関数が選択されることを期待しているが、

Y型のときに選択される __Y1_add関数も存在しないとコンパイルエラーになる。

上の問題から逃れるのにインライン関数が使えないかと模索したが、この問題でダメだった。


今のところ解決したのは入力の型によらず合法になってしまう部分の打開ぐらいで、

#define ATTR_X(OBJ, ATTR) *_Generic((OBJ), \
    X : (ftype==1 ? &(((X1*)&(OBJ))->ATTR) : &(((X2*)&(OBJ))->ATTR)))

と定義しておけば、ATTR_XマクロのOBJ引数の型がXでなければコンパイルエラーになる。

不適正なキャストを回避できるのでこれだけでも一応価値はある。

ただ、型により自動的に関数などが選択されるのが本来の_Genericの役目なので、

こういう書き方に留まるのは中途半端だなという気がする。

あと、こういう用途なら_Generic以外の方法もないんかな? とも思う。


というわけで、どうにも納得がいかない話だった。

実はC++のテンプレートだとこういう問題は回避できそうな気はするが、

C++を導入するとなるとそれはそれで面倒な話だと思う。

_Generic文というのもどうにも中途半端だという指摘はあって、

でもそれでもCという枠組みの中でこういう機能が導入されたこと自体は大きいと、

そういう考えられているようである。確かにそれなりには役立つ。

バイトオーダーの境目でアドレスが変わる?

今日は久々に在宅勤務をした気がする。

荷物が届くのを待ってたんですよ。時間指定しにくい荷物だったのでね。

またこの話は改めてBlogに書くが、けっこう大きな買い物である。


以前、バイトオーダーの話を書いた。

バイトオーダーに悩む

新しいマイコンはリトルエンディアン、旧来からのペリフェラルはビックエンディアン、

一体どうするかという話をやっていたのだが、32bit幅のバスをビット順につなぐことで決着した。

すなわち旧来からのシステムにある共有メモリに{12 34 56 78}と並んだデータは、

4byteアクセスすると新しいマイコンでも0x12345678とリードできて、

それを自身のメモリに格納すると{78 56 34 12}となるという形である。

ハードウェア的にも作りやすく、ソフトウェアの修正も少ない見込みで、

万々歳とは思うが、うまくやるにはいろいろ工夫が必要である。


で、これは職場の人に教えてもらったのだが、この方法で1byte, 2byteアクセスに対応する場合、

境界部でアドレスを調整することでつじつまを合わせることができると言われた。

ビックエンディアンのシステムにある共有メモリの0x100番地から{12 34 56 78}というデータが格納されているとき。

ビックエンディアンのシステムで0x100から4byteリードすると0x12345678、

0x100から2byteリードすると0x1234、0x102から2byteリードすると0x5678となる。


一方でリトルエンディアンのシステムにとってみれば、

0x100から4byteリードして0x12345678と読めるメモリというのは、

2byte単位で区切って読むと、下位16bit(=0x5678), 上位16bit(=0x1234)の順でリードできるべきである。

そこでリトルエンディアンとビックエンディアンの境界部で、

2byteアクセスの場合は0x100→0x102, 0x102→0x100とアドレス変換を行う。

共有メモリから0x102から2byteリードすると0x5678、0x100からリードすると0x1234となるからつじつまが合う。

同様に1byteアクセスも0x100⇔0x103, 0x101⇔0x102と変換するとつじつまが合う。

こうするとバイトオーダーの違いを吸収できるというわけである。


すなわちビックエンディアンとリトルエンディアンをまたぐ場合、

  1. 2,4byteアクセスしたときのビット順は正しい
    4byte単位でアクセスしたときのアドレスは変わらない
    バイト順と1,2byteアクセス時のアドレスは異なる
  2. バイト順とアドレスは変わらない
    2,4byteアクセスしたときのビット順が異なる

の2つの選択肢があり、今回は1.を選択したということになる。


今回の場合、多くはこれで問題ないと考えているのだが、

4byteアクセスに統一できない1byte, 2byteアクセスも多少ありそうで、

その場合にはアドレス調整が必要になるのが課題の1つ。

そういう処理をできるだけ減らすこと。残る部分は確実に対応することが求められている。

そしてバイナリデータの転送時にバイト順が変わる問題もある。

全体としてはあまり多くないが一部に存在する。

そこはどこかでバイトスワップする処理を差し込むことになる。


なかなかこの仕組みを理解するのは難しいようで、

メンバー同士の議論でもよく振り出しに戻るということが起きている。

まぁなかなか教科書的な話とは違うなとは言っていたが。