_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つ。

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

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

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

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


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

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

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

JCB Webmasterからのメール

先日、みずほJCBデビットの発行を受けた話を書いた。

キャッシュカードをきれいにしたくてJCB

使ってみたところ「JCB Webmaster」からメールが届いて、

今まで時々迷惑メールの送信元で見てた名前じゃないかと思ったのだった。


クレジットカード会社になりすました迷惑メールが届くことは多い。

明らかに僕にとって関係のない楽天カードとかAmerican Expressとか、

まぁそういう送信元なら迷惑メールとすぐに気づくことが出来る。

イオンカード(?)になりすましたメールが届くことも多いのだが、

本当のイオンカードに関するメールは「イオンフィナンシャルサービス」とか「イオンスクエア」とかが差出人なので、

普段から正当なメールに見慣れていればすぐにおかしいことには気づける。


ただ、この「JCB Webmaster」からのメールはわりと精度がいい気がする。

内容もそんな変な内容が書いているわけではない。

ただ、リンク先がjcb.co.jpに見せて全然違うアドレスになっていたり、

よくよく見るとおかしなところが多々あるのだが。

それだけに本物の「JCB Webmaster」からのメールが届いたときに、

「これ本物か?」とかなり疑って見るハメになった。


明らかにおかしいとわかればさっさと除けてしまうのだけどね。

この「明らかにおかしい」という感覚を持てるかも世間的には課題だが、

そこは経験則でなんとかなるとする。

しかし、一見そこまでおかしくないとなると、

送信元のドメインとか実際のリンク先とか注意深くみないといけない。

今までJCBからメールが届くことはおおよそ考えられなかったので、

「JCB Webmaster」の時点で迷惑メールと判断がついたが、

今後はそうもいかないのでより注意深く読まないといけなさそうだ。


なかなか迷惑メール対策には妙案がないのが実情である。

ホワイトリスト方式ならよいが、それでは届くべきメールが届かなくて困る問題が大きい。

昔は質の低い迷惑メールが多かったから簡単だったんだけどね。

Cプリプロセッサとアセンブラのマクロ

レジスタの初期化を最初はCのコードで書こうと思ったのだが、

まだスタックを使えない段階でレジスタの初期化をした方がよいとなって、

そうなるとアセンブラで書いた方が安全かもしれないなと思った。

所定の値をレジスタにセットするだけなのでアセンブラで書くこと自体は難しくない。

が、条件によってセットする値が変わるのが難点である。

(その条件自体はすでにアセンブラのコードで使っているものである)


で、今どきのアセンブラというのはCのプリプロセッサが使える。

定数の定義をCとアセンブラで共通のヘッダファイルにすることもできるし、

アセンブラよりも高度なCのプリプロセッサのマクロが使えるのもある。

今回のケースはまさにこの高度なプリプロセッサが活用できるものである。

設定するパラメータがCFG1, CFG2…と複数あり、

そのパラメータのセットが SET1, SET2…と複数あるので、下記のように定義しておく。

#define CFG_SET1_CFG1 0x11111111

そしてマクロの引数にセット名を取り、トークン連結演算子##を活用して CFG_[セット名]_CFG1 という定数名を作って、

その値をパラメータCFG1にセットするという処理を書けばよいわけだ。


一方のアセンブラのマクロはlocalに列挙したラベルを重複しないように処理するなど、

アセンブラの事情に即したマクロ展開が行われる。(cf. アセンブラのマクロ)

この2つのマクロ展開を組み合わせる場合、一体どうなるのか?

確認したところCプリプロセッサの処理が先に行われ、その後にアセンブラ処理になる。

当たり前ではあるのだが、アセンブラのマクロの引数をCプリプロセッサに処理させることはできないと。

上記の定数展開はアセンブラのマクロ処理より前に完了させなければならない。


で、いろいろ考えたのだがこんな形になった。

まず、パラメータを取ってセットするアセンブラのマクロを書く。

.macro CFGSET_RAW cfg1, cfg2
      LDR R0, =cfg1
      ...

このマクロは CFGSET_RAW 0x11,0x12 のように使うことができる。

で、このマクロを使うマクロをCプリプロセッサのマクロで書く

#define CFGSET(SET) CFGSET_RAW CFG_##SET##_CFG1, CFG_##SET##_CFG2

これでCFGSET(SET1)と書くと、CFG_SET1_CFG1, CFG_SET1_CFG2 という定数が呼び出される。

これらの定数を#defineで定義しておけば、その定数を展開してを引数に並べたCFGSET_RAWマクロ呼び出しが作られる。


さらにはこのCプリプロセッサのマクロを使ってアセンブラのマクロを書く。

.macro CFG_SELECT_AND_SET
     local COND1, COND2, END
     TST R0, #1
     BEQ COND1
     TST R0, #2
     BEQ COND2
     ... COND1:
     CFGSET(SET1)
     B   END COND2:
     CFGSET(SET2)
     B   END
     ... END: .endm

これも先にCプリプロセッサで展開が行われる。

CプリプロセッサのCFGSETマクロの引数を変えると、

展開される定数の異なるアセンブラのCFGSET_RAWマクロの呼び出しが作られる。

で、アセンブラではCFG_SELECT_AND_SETマクロの中に、CFGSET_RAWマクロが入れ子になった形になる。

アセンブラのマクロにアセンブラのマクロが入れ子になっているので問題ない。


思ったよりあっさり動いてしまい拍子抜けという感じもあるのだが、

Cプリプロセッサのマクロの処理順序は時に注意が必要である。

マクロの引数はトークン連結演算子##、文字列化演算子#がない限りは、

#defineの定数置き換えがあればまず置き換えが行われる。

#define SQU(X) (X)*(X)
#define TWO 2

ここでSQU(TWO)と書くと、まず引数のTWOを2と解釈して、Xに2を当てはめるという扱いになる。

XにTWOを当てはめて(TWO)*(TWO)としてから、TWOを2に置き換えるわけではない。

この場合はどちらの順序でやっても差は見えないが、トークン連結演算子や文字列化演算子との組み合わせで問題になる。


上記の定数をCのプログラムで利用することを考えるとこんなマクロを作る可能性がある。

#define SETCFG1(SET)    cfg[1]=CFG_##SET##_CFG1
#define SETCFG2(SET) cfg[2]=CFG_##SET##_CFG2 #define SETCFGS(SET) do{ SETCFG1(SET); SETCFG2(SET); }while(0)

それでSETCFGS(SET1)としても思った通りには動いてくれない。

なぜならばSETCFGSマクロの解釈時に、まず引数のSET1を解釈しようとするから。

もしも #define SET1 100 とか定義されていたら、SETを100に置き換えて、

SETCFG1(SET)→SETCFG1(100)→cfg[1]=CFG_100_CFG1 となる。


こういう目的をもってマクロを入れ子にする場合もあるのだが……

#define STR2(X)          #X
#define STR(X)           STR2(X)
#define SAYCFG(SET,CFG)  puts( #SET "." #CFG "=" STR(CFG_##SET##_##CFG) )

SAYCFG(SET2,CFG2)と書くと SET2.CFG2=に続いて CFG_SET2_CFG2 で定義した定数がputsで出力されるマクロである。

#SETは引数SET2をそのまま文字列化して “SET2” に置き換えられる。

これはよいのだが、CFG_SET2_CFG2を定数の文字列にして表示するには、

STRマクロの引数とすると、トークン連結演算子, 文字列化演算子で使われない引数ということで解釈され、

CFG_SET2_CFG2が#defineで定義した定数(例えば0x02)に置き換えられる。

それをSTR2マクロの引数に渡すとその定数を文字列化して、”0x02″ のような文字列が得られると。

これを2段階にせず、直接STR2マクロの引数にすると “CFG_SET2_CFG2” という文字列を生成されてしまう。


こういうのはあまり一般的な話ではなく、文字列を結合させるというのは、

結合させた名前の定数の値を直接利用したいからだろうと。

一番外側のマクロで定数の値が展開されるようになっていればよいと。

ただし、その定数を他のマクロに渡す場合、引数を##や#で使っていないことが前提である。

最後に書いたような特殊なケースでは入れ子にしてあれこれする。

展開して文字列化するというのはデバッグ目的では多少あるかもしれない。

でも、今回の本題のアセンブラではまず関係ないですけどね。

8進数の使い道

マイコンのリセット要因を表示するレジスタの実験をしていたとき、

とりあえずレジスタの値を表示するために8進数を使うことを思いついた。

というのもリセット直後の値を読む都合もあり、デバッガを接続せず読み出せる必要があり、

コンソールにすぐ表示できる方法として思いついたのがそれだったと。


8進数の何がいいってビットシフトと足し算で簡単に作れることである。

2進数のデータを4bit単位で区切れば16進数、3bit単位で区切れば8進数、

この点ではどちらもあまり差はないのだが、画面に表示するにはASCIIコードにする必要がある。

16進数だと0~9とA~Fは文字コードが連続していないので、

各桁の値が9以下なら’0’の文字コード+値、10以上なら’A’の文字コード+(値-10)を出力する必要がある。

一方の8進数は各桁の値は0~7なので、単純に’0’の文字コード+値でASCIIに変換できる。

というので、他のライブラリなど使わずにサッと数値を文字にして表示するのに便利だったと。


しかし8進数というのは普段そんなに使うものではない。

歴史的な経緯もありCでは 0741 のように先頭に0を付けるだけで8進数リテラルとして扱われるなど扱いはよいが、

この機能が活用されているのはUNIXのパーミッション設定ぐらいしか見ない。

読み込み・書き込み・実行の権限を3bitで表すので3bitで1桁になる8進数が好都合と。

かなり特殊な使い方のような気がする。


歴史的に8進数の扱いがよいのは、昔のコンピュータは3の倍数のビット数をよく使っていたかららしい。

初期のコンピュータでは36bitあるいはその半分の18bitが使われることが多かったらしい。

36bitあれば10進数の10桁を格納できることが理由だったという。

ようは10桁表示の電卓のイメージである。ああいうのを作りたかったと。

符号ビットを考慮すれば最低35bitあればよく、6の倍数に切り上げて36bitと。

この時代は6bitの文字コードも使われていたという。

6bitあれば数字+アルファベットが入るから。(大文字・小文字の区別は付かないが)

だから6bitを2桁で表せる8進数のニーズがけっこうあったのだという。


ただ、その後に浮動小数点演算が導入されて、固定長整数にこだわる理由も減り、

8bitを単位とした体系に移行していったという。

4bitで10進数の1桁、8bitで2桁と二進化十進数としてもこっちの方が好都合だったのかも。

(二進化十進数は今どきはあまり使わないけど、昔はよく使われていたとか)

文字コードも7bitのASCIIコードが標準となり、残り1bitはASCIIの拡張に使われた。

8bit単位なら16進数2桁で8bit表せるのがよいとなる。

今となっては8進数も見る影もないのはこのような事情がある。


今回は横着して8進数表示にしたわけだが、その甲斐あってリセット要因レジスタの仕様が判明していった。

ちょっと不思議なレジスタで通電時にはほぼ全てのビットが立っていて、

これをクリアしておくと、次回からは発生したリセット要因が残ると。

例えばウォッチドッグでリセットした場合はウォッチドッグのビットが立つと。

なのでレジスタを読んだらゼロクリアするということを前提とすれば、

ほぼ全てのビットが立っている=パワーオンリセット、

特定のビットだけ立っている=それがリセット要因ということになる。

あまり見ないやり方だがハードウェア的には作りやすいのかも知れない。

パワーオンリセットの途上であれこれガチャガチャとリセットしても、

全部立った状態からスタートするとすれば証拠隠滅できてしまうので。

ただ、ちょっと解せない仕様もあるので要調査である。

バイトオーダーに悩む

古いマイコンで使っていたプログラムを移植するに当たって、

バイトオーダーをどうすればいいんだろうという話をしていた。

新しいマイコンはバイトオーダーが変わるのは確定していて、

外部とのインターフェースは従来のバイトオーダーであるというのも決まっている。

どこでバイトオーダーを切り替えるかという問題である。

というのもマイコンにつながるバス部分はある程度自由度があるからである。


バイトオーダーというと2バイト以上の数値をメモリ上にどう並べるかという話である。

0x1234という数値をメモリ上に{12 34}と並べるのがビッグエンディアン、

{34 12}と下位ビットを含むバイトから順番に並べるのがリトルエンディアンである。

リトルエンディアンの方が処理性能を出しやすいとか見たことがある。

人間が足し算・引き算・かけ算をやるときも下の桁から順番に計算するように、

コンピュータにとってもLSBから順番に計算する方が好都合なのだ。


一方でネットワーク上を流れるデータというのはビッグエンディアンが多いらしい。

例えばデータ長が0x1230というのを伝達するために{12 30}と送信して、

受信した側は届いた順番にメモリに格納してそのデータを4byteの整数として解釈すると、

ビッグエンディアンのシステムでは0x1230と解釈できるが、

リトルエンディアンのシステムでは0x3012となってしまうのでこれではいけないと。

なので実際には読み出し後にビット順を入れ換えたり(ex. 0x3012→0x1234)

メモリの順番を入れ換えてから読んだり(ex. {12 30}→{30 12}と入れ換えてからリードすると0x1230)、

そういう変換処理を挟まなければいけなくなるわけである。


とはいえ、外部とのやりとりでも、そういう変換が必ずしも必要とも言えないところはある。

例えば、バスに接続されるペリフェラルの内部レジスタへのアクセス、

32bit幅のバスで、必ず4byteアクセスするという約束もあるので、

レジスタに0x80000000をライトすれば、レジスタのMSBがセットされる。

こういう作りにする方が自然ではないかと思う。


問題が起きやすいのは、データの順番が決まっている場合と、メモリを共有する場合ではないかと思う。

データの順番が決まっているというのは冒頭に書いた例の通りである。

届いた順に前から書く、前に書いてあるデータから順番に送るというのは、そうならざるを得ない。

メモリを共有する場合も、ビックエンディアンのシステムで0x12345678を書いて、

リトルエンディアンのシステムで読めば0x78563412となるのは当然である。


ただ、回避策がないとも言えないところはある。

データの順番が決まっている場合、後ろから書く、後ろから送るという方法が

バイトオーダーの雑記 (へっぽこ実験室)

Intelはメモリ構造を図示するときに、右下を最下位アドレスとして、

右から左にバイトを並べて、上の行に進んでいくという書き方をしているらしい。

この方法でメモリダンプを表示するとあまり違和感なく読めますねとある。

このようにアドレスを減らす方向に使って行くという考えは1つある。


メモリを共有する場合も必ず32bit幅でアクセスするとか約束して、

結線時に入れ換えておけばそれはそれでうまくいきそうな気がする。

共有メモリの実データはビックエンディアンで格納されていて、

必ず4byte単位でリード/ライトして32bit幅のバスとやりとりする。

共有メモリ上に{12 34 56 78}と並んでいれば、エンディアンによらず必ず0x12345678と読める形にすると。

4byte単位でしかデータを使わないならば、これで全く問題ない。


ただ、このデータを1byte単位で使いたいとなれば面倒である。

ローカルのRAM上にデータを転写して操作する必要があるが、

ビックエンディアンのシステムでは{12 34 56 78}と共有メモリと同じ順に並ぶ一方、

リトルエンディアンのシステムでは{78 56 34 12}と共有メモリとは異なる並びになる。

読み出したデータを0x12345678→0x78563412と入れ換えて格納すれば、

{12 34 56 78}と共有メモリと同じ順番で格納することはできる。


これを書きながらいろいろ考えていたけど……

うまくやればバイトオーダーを入れ換える操作を意図して書くべき場所は減らせそうな気がした。

ちょっと実験してみないとわからないところはあるんだけど。

メモリ上のデータの取扱単位を4byte単位に揃えることが出来れば、

バイトオーダーの入れ替えを意図的にあれこれする場面は減りそうな気がした。

実際に並ぶデータの順番を意識してやらないといけないところはそこを考えて作る必要はあるが。

あとはメモリダンプみたいに何らか取得できればよいというものもありますからね。

読む側で解釈すればよいからという理屈である。

アセンブラのマクロ

ここのところ、アセンブラを書くことがけっこうある。

以前、実験用に使っていた無償提供されている開発環境から、

実際の製品に使う開発環境、これは有償で買ったのだが、

この乗換のときにアセンブラの方言の差がかなり大きくて全書換ぐらいになってしまった。

Cのコードもいろいろ書換はあったけど、こちらは限定的である。


その中で以前紹介した1bとか2fのようなラベル表記ができなくなった。

ちなみに bne命令の分岐先1bというのは上にあるラベル1という意味らしい。

逆に下にあるラベル2は2fと書ける。そういうアセンブラの記法があると。

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

こういうのも方言なんですね。

マニュアルを見たのだが、少し表記を変えれば済むというわけではなく、

数字のラベルはすべて名前を付け直すこととなってしまった。


で、アセンブラにはマクロというのがあって、

繰り返し使う命令の並びをあらかじめ書いておくことをやっている。

元々、マクロ内でも数字のラベルが多用されていたのだが、

使えなくなったらまともにラベルを振らないといけない。

それでラベルを振ったらラベルが重複するというエラーが出てくる。


で、こういうのはどうやったら回避できるのかとマニュアルを見たら、

マクロの定義の中でローカルのラベルであることを規定すればよいと。

.macro CHECK_AND_STORE
     local store1, store2, hang, exit
     teq   r0, #1
     beq   store1
     teq   r0, #2
     beq   store2 hang:
     b     hang store1:
     str   #1, [r1]
     b     exit store2:
     str   #2, [r1] exit: .endm

exitとかいうラベルを使うことはけっこう多い気がするけど、

localと書いておけば、マクロを実体化したときに重複しないように処理してくれる。


アセンブラも書いてある順番に命令並べるだけじゃないんですよね。

疑似命令というのもあって、実際には存在しない命令を書けることがある。

ARMだとビットシフトの命令がそうで、MOV命令にビットシフトを付加したものとして処理されたりする。

それはそういう別名みたいなものですけど。

実際に使われる命令を意識して書かれたものもありますけどね。

アドレスをロードするとか、1命令で書けない場合に複数命令に展開したり、

そういう疑似命令もある。1行1命令とは限らないと。


アセンブラを使って記述する理由というのは、

命令の並び順を厳密に決める必要があるというのが理由のほとんどではないか。

割り込みハンドラのレジスタ退避なんていうのはまさにそういう話で、

正しい順番でやらないと割り込み前の状態を保存できなくなってしまう。

スタックを使わないで処理するのもアセンブラで書けば確実である。

そういう用事が済んでしまえばCのコードにジャンプさせてしまう。


今回の開発プロジェクトでアセンブラで書く部分って僕が全部書きそうだけど、

これを見てレビューできる人って他のメンバーにいるのかなというのは気になる。

機械語の挙動を把握してレビューしないといけないわけだからね。

そういう意味でも用事が済んだら早々Cのコードに飛ばすのは重要で、

そうしておけばCのコードは多くの人が平易に理解できるし、

コンパイラはそれを正しく機械語に落とすことが出来るはず。

それでも残るアセンブラ部は極小といいたいところだけど、数行というわけにはいかない。


あと別の話として、既存のシステムのアセンブラで書かれたコードを解読しないといけない話がある。

「これ設計資料あるの?」と聞いたら「ないでしょう」とのことで、

ああやっぱりリバースエンジニアリングかと。

元はアセンブラだけど、書き直すときはCで書いてよいところのはず。

元々のシステムだといろいろ初期化されるまでなのでアセンブラだが、

今回は手順が違うので、Cのコードで書ける部分になるはず。

それはちょっと救いだな。

割り込みマスクせずにwfi命令を使うと

以前、wfi命令で割り込みを待つときには割り込みマスクをする話を書いた。

wfi命令で割り込みを待つ意味

フラグがなければwfi命令で割り込み待ちする場合、

フラグがないことを確認した後に割り込みが発生してフラグが立ったらうまくいかないので、

割り込みマスク→フラグ確認→wfi命令→割り込みマスク解除 の順番でやらないといかんねという話である。


で、本当にそうなるんかいとICEでブレークポイントを置いて実験をしてみた。

そしたら、確かに割り込み処理の復帰先がwfi命令の場所になってしまい、

さらにその次の割り込みが来るまで待たされるケースがあることが確認できた。

というわけで、ちゃんと割り込みマスクしてwfi命令しないといけませんねと。


ただ、割り込みの復帰先を書き換えてやれば問題は回避できるんだなと。

  1. 休止状態のフラグを立てる
  2. 処理するべきフラグが立っていたら休止状態のフラグを消して処理に遷移する
  3. 何もフラグがなければwfi命令を実行して休止する
  4. 割り込みハンドラで1.のフラグを確認し、休止状態の場合は割り込みの復帰先を2.にする

こんなことをやれば割り込みマスクをしなくても正しく動くはず。


何でこんな話を書いたのかというと、ある種の割り込みをマスクする期間を作りたくないという話があって、

割り込み処理の仕様上、最低限のレジスタ退避の間だけはマスクせざるを得ないが、

それ以外は可能な限り割り込みマスクせずにやりたいねと。

でも、wfi命令って割り込みマスクしないと正しく使えないよね、

と気になって、上のようなことを試して気づいたわけである。


ただ、実際のシステムでこういうプログラムを書くのかというと、

それはよくわからないんだけど。休止でwfi命令を使わない可能性もある。

(元々そういう処理をしてなかったし、電力的にシビアなシステムではない)

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

ベクタテーブルというと、割り込みのジャンプ先のアドレスを格納するものだと思っていたが、

割り込みでジャンプする命令が並ぶタイプのものもあるらしい。


というのがARMのCortex-M以外のベクタテーブルなんですけどね。

Exception vectors and the exception base address (ARM)

例えばVBARレジスタの値がベースアドレスで、IRQ割り込みの場合は、

IRQのオフセットが0x18なので PC←VBAR+0x18 にジャンプすると。

(このとき、割り込み前のPCなど復帰に必要な情報は別のレジスタに保存する)

ただ、ジャンプ先のアドレスは4byte間隔で並んでいるわけで、テーブルに置ける命令は1個だけ。

なので、ここに置くのはジャンプ命令というのが通例である。

ジャンプ先が近い場合は命令1個でジャンプできるのでよい。

(例えばA32のB命令の場合、PC(プログラムカウンタ)から±32MB の範囲でジャンプできる)


ただ、遠い場合はそうもいかないわけですよね。

実際のところ、ARMではベクタテーブルの後ろにジャンプ先のアドレスを並べているケースが多いようだ。

これを参照してジャンプする命令を並べる形である。

ベクタテーブルの前後32MBの範囲に割り込みハンドラがあればその必要はないのだが。

それならアドレスを並べたテーブルから割り込みハンドラのアドレスを得るところまで自動でやってほしいものだが、

PCをセットするだけなら割り込み処理が高速化できるケースもあるので、こうしているのでは?


ベクタテーブルの後ろに並べたアドレスにジャンプするというのは、

PCからの相対アドレスで指定したメモリ上のデータをPCに格納するという形で書ける。

LDR PC,=IRQHandler

という形でアセンブラで書くのだが、実際には

0x0018: MOV PC,[PC,#0x20]
0x0038: DCD IRQHandler

というような形でデータが生成されるようだ。

これを1命令で書けるのはARMらしいのかもしれない。

ARMならそれでいいんですけどね。


PCの値を振り分けるというのはプロセッサ内部で済む話だが、

PCの値を外部のメモリから取得した値にするのは確かに複雑で、

そこはユーザーが書くコードでなんとかしてよというのは理由はあると思った。

でも、あんまり嬉しくない作りだよなとも思う。

フラッシュドライバを作る

最近、新しいマイコンの開発環境の整備をあれこれとやっている。

メモリのマッピングを定義したり、デバッグの設定を確認したり。

そういう作業をやる中でフラッシュドライバの作成をした。


今までフラッシュドライバというのはデバッガに付属するものと思っていたが、

ワンチップマイコンの内蔵ROMを焼くなら決まり切っているが、

今回は外付けのシリアルフラッシュを使うというのもあるし、

いろいろな事情でツールが充実していないという事情もある。

で、そんなの自作できるんか? と思ったが、思ったよりは楽だった。


今回はフラッシュドライバ用のプログラムをマイコンのRAM上に転送する方法にした。

レジスタ操作をマクロに書いてフラッシュドライバにする方法もあるらしいが。

フラッシュドライバのテンプレートがあって、初期化・イレース・プログラム・ベリファイの操作を穴埋めしていくと完成する仕組みである。

すでにフラッシュ読み書き用のプログラムを作っていたので、これを切り貼りして作ればそこまで大変ではなかった。


しかし、難しかったのがフラッシュドライバのデバッグである。

フラッシュドライバを使ってプログラムをダウンロードして失敗したところで強制終了して、

フラッシュドライバのプロジェクトを開いて、デバッガをアタッチしてブレークすると、

どこでハングアップしたのか判明するとかそんなのである。

そんなのを何度か繰り返して、清々と流れるようになった。


実際、フラッシュドライバがデバッグに必要なのかはよくわからないが、

デバッガが接続できる環境なら、他の方法でプログラムを書き込むよりも楽だろう。

フラッシュへの書き込みができたら、それをRAM上にロードして動かしてデバッグ開始だが、

そこをどうやって繋げるかというのはまた考えないといけない要素があって、

そのあたりの処理を埋めるためのマクロも必要かとか、そんなこともやっていた。


今回のシステムではいろいろなツールを用意しないといけないことはわかっており、

実験的にいろいろ作っているのだが、まだ一本につながるには至っていない。

実際に動かしたり作ってみないとわからない部分がいろいろあるんだよな。

仕様案を作ったはよいものの、前後の仕様が変わってしまい、見直しが必要だとか。

とはいえ、このあたりも固まってきたし、全体像を明らかにしていかないとね。

もうちょっと実験したらそのあたりできるようになるかな。


今回のフラッシュドライバの実験でもいろいろわかったことがあって、

それに合わせて見直すべき点が多々あった。

実際のデバッグで使うかはよくわからないと書いたが、

今、デバッグするならこれが最善であるとは言えるので、それはそれでよいと。