何比較してもfalseになるNaN

評価計画を立てているときに、浮動小数点演算の結果が無限大になったときの処理も確認しないとな。

というところでIEEE754形式の浮動小数点数の資料を見たら、

NaNというのもあることを思いだし、これがけっこうなくせ者だった。


特殊な浮動小数点数として +0, –0, +∞, –∞とNaNがある。

+0と-0はどちらも0なのだが、符号ビットの概念が存在する。

通常は+0を使うのだが、計算によっては-0が生じることがある。

とはいえ大概の場合、意識する必要はない。

+∞, -∞はゼロ除算で生じることが比較的多い。

1.0/0.0=+∞, -1.0/0.0=-∞ といった具合である。

数学的には正しいとは言えないけど、概念的にはわかりますよね。


NaNは “Not a Number”の略で、漢字で書けば「非数」ってことになる。

なんのこっちゃという話だが、何らか未定義の値が出てくるとこうなる。

ゼロ除算でも 0.0/0.0 の場合は±∞ではなく NaN となる。

∞が絡む演算では 0.0×(±∞) や ∞-∞ は NaNとなる。

また、虚数が生じる演算 sqrt(-1.0) のような計算も NaN となるそう。

NaNが絡む演算も通常はNaN になるので 0.0/0.0+1.0 も NaN+1.0=NaNとなる。


浮動小数点数をスケーリング・整数化して表示する処理がある。

ここに +∞, –∞とNaN をそれぞれぶち込んでみた。

+∞, -∞は上限・下限に貼り付くような動作で、これはこれでよい。

しかしNaNは0になってしまった。一体なぜか。

float fval = roundf(forg * fscale);
if(fval < (float)SHRT_MIN){
     shortval = (short)SHRT_MIN;  //(1)
}else if(fval > (float)SHRT_MAX){
     shortval = (short)SHRT_MAX;  //(2)
}else{
     shortval = (short)fval;      //(3)
}

まず、forg×fscaleを四捨五入で整数化した値をfvalとして求める。

この値が -32768(SHRT_MIN)~32767(SHRT_MAX) であればそのままshortvalに代入する。

-32768未満であれば-32768、32767超であれば 32767 に丸める。

この処理で forg=-∞, fscaleが適当な正の数だと fval=-∞ となる。

-∞<SHRT_MIN なので(1)に分岐し、-32768 となり下限に貼り付く。

forg=+∞もfval=+∞となれば +∞>SHRT_MAX なので(2)に分岐し 32767 に貼り付く。


問題は forg=NaNで、fval=NaNとなった場合である。

実はNaNと他の数の比較はすべてfalseとなるルールがあるそうで、

NaN<0 も NaN>0 も NaN==NaN すらもfalseとなるのである。

すなわち、上記の(1)にも(2)にも該当せず、

あたかも -32768~32767 の範囲にあるかのように(3)に入る。

こうなったNaNをどのように整数変換するかは処理系依存だが、ARMでは0となるようだ。


ところでここまでの処理で例外とか発生しないの? と気になった人もいるかもしれない。

ARMではゼロ除算など不正な演算をしても例外にならないのが通常である。

レジスタ設定により例外を発生させることもできるみたいだけど。

ARMでは整数演算で0除算をすると0になるというルールを持っており、

100/0 のような演算をうっかりやってしまうと 0になるんですね。

x86はゼロ除算で例外が発生するのだが、必ずしもそうではないと。


何を比較してもfalseになるのが功を奏したものもあって、

a<bの場合に動作する処理を if(a<b){ … }else{ … } と書くと、aかbがNaNだとelse側に入ることになる。

数値比較してfalse側にフォールバック的な動作があればよいのだが、

逆に範囲外の処理をtrue側に書くと、ひっかからないということが起こる。

とりあえずNaNを何か範囲外相当の処理に落とし込みたいわけである。

というわけで考えたのはこういう書き方である。

if(!(fval >= (float)SHRT_MIN)){

「SHRT_MIN未満」という書き方を、あえて「SHRT_MIN以上でなければ」としている。

NaN以外の数に対する動作は何ら変わらない。

NaN >= SHRT_MIN は falseになるので、これを反転するとtrue になる。

これにより従来の書き方では分岐しなかった(1)の処理に入り、

fval=NaNは便宜上 -32768 に変換されることになる。

いかにも異常な数字であることが気づける可能性が高まる。


実際のところ +∞, –∞, NaN なんて発生させちゃいけないんですけどね。

ただ、設定次第で発生する可能性は否定できないので、

その場合でもなんらか説明ができる動作にしておくべきという話ですね。

比較順序によってはあまり問題にならないんですけどね。

ただ、意識して作らないとなかなかうまくいかないだろうなとは思った。

TeraTermでバイナリデータを受信する

マイコンの処理時間の測定をして、処理能力の見積もりをしていた。

処理量が多いと勝手に遅くなっていくという構造で、

一体どのぐらいあると遅くなるのかというのが見えてなかった。

おそらくほとんどの場合は最速ペースで動けそうなのだが、

遅くなるケースもちゃんと評価しないといけないのでね。


で、取得したデータを吐き出す方法をどうするかという話があって、

UARTで‘0’を送ると、測定データをバイナリで吐き出すようにした。

バイナリなのは文字列への変換が面倒だったからなのだが、

果たしてバイナリデータを保存するのは容易に出来るのか?

と、TeraTermのログ機能を見ると「バイナリ」という出力モードがあった。

バイナリでログを有効化して、’0’をタイプして、ログ終了して、

ログファイルをバイナリエディタで開くと所望のデータが得られた。

これでいいのか。


バイナリデータを送るのは面倒なのに、受信するのは簡単なんだな。

と思ったが、よく考えればファイルをドラッグアンドドロップすれば、

そのデータをバイナリで送信するということはできるんだった。

(この送信というのはYMODEMなど使わずに、単純にデータ列として送るということ)

だからあらかじめバイナリデータのファイルをあれこれ用意しておけば、

決まったバイナリデータをTeraTermで送るのは容易だったらしい。


後処理など考えればPythonなのかVBAなのか、送受信してデータ処理するプログラムを作るべきかも知れないが、

とりあえずデータを取ってくるという点ではこれで楽に出来た。

おかげでサクッと取って進めることが出来た。

ICEでメモリダンプするというのもあるけど、それだと実態に合わないのでは? という懸念もあったので。

静的解析がstaticを付けろと言う

プログラムのソースコードの静的解析というのがある。

コードを解析して、バグの原因になりそうな場所をいろいろ抽出してくれる。

打率は1割どころかもっと低い気がしますが。


これはバグだろうと多くの人に納得してもらえる例としては、

if(uintval<0){...}

と符号無しの変数が0未満であることを評価しているのだが、

この条件は常に不成立である。すなわちこの中のコードは実行されない。

その中に何か必要と思って書いている処理があっても実行されないので、

何かミスをしているということである。

変数の型を間違えているのか、条件を間違えているのかはわかりませんが。

そういうので再確認するべきポイントをいろいろ教えてくれるわけですね。


まぁそうはいっても馬鹿らしいものは多いのである。

その1つにファイル内でしか使われていない関数・グローバル変数にstaticが付いていないというのがある。

今までは「意図通り」とか書いてスルーすることは多かったが、

今回はその指摘に従っても良いかもなとファイル内に閉じるものにstaticに付けて回った。

そしたら次は1つの関数でしか使われていない変数があるという指摘があった。

まぁ関数内のstatic変数にするという方法はあるんですけどね。


C言語のstaticというのは2つの意味がある。それぞれ意味が異なる。

関数・グローバル変数に付ける場合はファイル内でしか利用できない関数・変数であることを示す。

どうしてファイル内に閉じる関数をstaticで宣言することがバグ対策として推奨されるのか?

例えばfoo.cでこのように宣言された関数があったとする。

void foofunc1(int x, int y){ ... }

かたやbar.cでこういうプロトタイプ宣言を書いたとする。

int foofunc1(char x, char y);

するとそれはそれで通ってしまい、名前が同じだとリンカで結合されてしまう。

当然、ここまでミスをしているとより強い指摘になるのだが、

潜在的なリスクを減らすにはプロトタイプ宣言を共通のヘッダファイルに書くべきであると。

ヘッダファイルのプロトタイプ宣言と実際の記述が間違えているとコンパイラが気づけるから。

あるいは関数名・変数名をミスタイプしていたようなケースならば、

それはヘッダファイルに書かれていないということで気づける。

一方でヘッダファイルに書かない場合はstaticとしてファイル内に閉じると明示するべきだと。


今まではどちらかというとヘッダファイルに列挙する方を選ぶことが多かったが、

内部に閉じるならstaticにするのも手と考えてこちらにした。

実は他のファイルでも必要でしたということで起こると困るが、

そうなる可能性はもう低いんじゃないかという読みである。

すると、次には1つの関数でしか使われていないという指摘が出てきたわけである。


記述ミスやバグが潜んでいる可能性はあるんでよね。

int fooval;
int foofunc2(int y){
  if(y > 0){

    fooval = y;
  }
  return fooval*200;
}

この書き方だとy≦0でfoovalが実行された場合は、すでにメモリ上にあるfoovalの値に200を掛けた値が返る。

前回の値を保存して計算することが目的ならこれでよいのだが、

実はy≦0の場合は、別の値を入れるべきところを書き忘れていたとすると気づけない。

もしfoovalが関数内の一時変数ならば、foovalが初期化されない場合があると指摘される。(なんならコンパイラが教えてくれる)


一時変数と間違えていませんかという指摘でもあるのだが、

一方で関数内のstatic変数として宣言することもできますねという指摘でもある。

int foofunc2(_Bool init){
  static int cnt;
  if(init){
    cnt = 0;
  }else{
    cnt++;
  }
  return cnt;
}

こういう書き方をするとcntという変数は次回関数実行時にも残る。

グローバル変数として宣言したのとほぼ同じ効果が得られる。

あくまでもローカル変数なので他の関数からは参照できないんですけどね。


ただ、これはデバッグに不都合だろうということで意図通りなら修正しなくてよいと考えた。

実際にはstaticなローカル変数もグローバル変数のように割り付けられているのだけど、

実際のところ、このタイプのstatic変数ってわかりにくい気がするんだよな。


と、この2つのstaticは効果が異なるわけですね。

修飾子なしのグローバル変数→static付きのグローバル変数→static付きのローカル変数

と同じようなものがだんだんスコープが狭くなっていくという話ではあるのだが。

このことについてWikibooksでは「文脈に応じて異なる意味と効果を持つ多目的な修飾子」と書かれている。

C言語/static (Wikibooks)

こういうのしばしばあるんですよね。

C++だとstaticメソッド・変数ってのもあるんですよね。

これはインスタンスではなくクラスに属するメソッド・変数っていうので意味が全く異なる。

これはさらに理解に苦しむ話である。


staticを付けたグローバル変数はファイル内でしか参照されないので、

ファイル内で代入だけして読み出されない変数についてコンパイラに指摘された。

もしstatic付きでなければ他の誰かがリードするかもしれないけど、

static付きなのでファイル内になければないですねということである。

これはデバッグ目的で代入しているものなのでその通りなのだが。

そしたら最適化で消されちゃうのか? と思ったが、見た感じはメモリに格納しているようだった。

staticを付けることで最適化のされ方も少し変わっているように見えた。

一体どういうところに差があるのかよくわからないけど。

コンパイラの警告は __attribute__((unused)) とか付ければ回避できる。

同期パスキーの目的

SBI証券といえば不正アクセス問題で相当な被害を出して、

それ以後、ログインの追加認証が複雑化してめんどくさいなぁと思っていた。

複雑化はしたが本質的にメールアドレス認証であることは変わらず、

僕はそのメールアドレスを携帯電話のMMSにしているから、PCのメールアドレスを乗っ取っても無理だが、

メールも乗っ取られて被害が拡大するのはよくある話。本質的な解決には遠い。

という中でWebサイトのログインにもパスキーが導入され、早速設定した。


で、これをやったときに気づいたのですが、スマートフォンで設定したパスキーがWindowsでも使えて、

あれ? パスキーってデバイスに紐付くものではなかったのかと。

あまり考えていなかったのだが、Googleパスワードマネージャーにパスキーが格納されているようだ。

現在はChromeでは特別な設定をしない限りはそれが基本になっているよう。

このため同じGoogleアカウントを使用するAndroid端末・Chromeでは全て使えてしまうと。


パスキーについて生体認証でログインできる仕組みと思っている人もいるかもしれないが、

正確に言えばそれは正しくなくて、デバイスに登録された秘密鍵でログインする仕組みである。

ただ、それを利用する際に生体認証やPINコードを要求するのが通例であり、

結果として生体認証に見えていると。でも重要なのはそこではない。

この秘密鍵を格納された端末なのか外付けのセキュリティデバイスなのか、

これを奪われなければログインされないということが重要であると思っていた。

逆に物だけ奪えばログインできるんじゃないかと思うかも知れないが、そこは生体認証なりPINコードで対策していると。


でも、その秘密鍵がクラウド上にあっては意味がないのでは?

秘密鍵が格納された物を奪われなければログインできないはずだったのに、

秘密鍵をクラウドで保持しているなら、そこにログインできるアカウントを乗っ取ればことが済んでしまう。

うーん、どうしてだろうと考えたがよくわからない。


このクラウド上に保存する仕組みを同期パスキーと呼ぶそうである。

比較的新しい仕組みだが、2022年からAndroid同士・iOS同士ではこの方式をサポート、

2024年からはGoogleはAndroidとPCのChromeブラウザの間でもサポートするようになった。

冒頭書いたようなAndroidで設定して、Windowsで使うようなことができるようになったのは本当に最近だったと。

見ての通り、この同期パスキーはスマートフォン同士での導入が先行したが、

パスキーをデバイス固定にすると、端末変更のたびにパスキーを失うことになる。

これがかえって悪いんじゃないかという話があったようである。

端末を変える度にパスキーを再登録するのはかえって脆弱ではないかと。

Googleアカウント、Appleアカウントが乗っ取られるとそれこそ大変なわけで、そこは強固になっているだろう。

そのアカウントに紐付く形でパスキーを保持するのなら十分安全ではないか。

パスキーの秘密鍵は通常は本人であっても目視することはない。

フィッシングサイトでパスワードが奪われるような被害は考えにくい。


そんなわけで同期パスキーを各プラットフォーマーは推奨していると。

ただ、当然ですがこれはGoogleアカウントやAppleアカウントが強固であるのが前提になっている。

これが脆弱だと結局は1つ乗っ取られれば芋づる式にやられてしまう。

パスキーが広く普及するにつれて、そういう問題も起きそうですがどうでしょうね。

SBI証券ということについて言えば、取引パスワードや、出金時のメール認証というのはあるけど、

ログインパスワードと取引パスワードが同じで被害を受けたり、

メール認証ということはGmail乗っ取られたらやられる人はいるだろうと、

そういうところまで考えて強固化できている人がいるのかという話ですね。


とはいえ、これでログイン時の追加認証がなくなるのは楽である。

元々手間がかかる割に効果的とも思えないものでしたからね。

パスキーを使っておけばフィッシングサイトへの対策には十分というのはおそらく正しいとみられる。

そこが達成出来ればよいということにしたのだろう。

VFPレジスタの退避節約術

昨日、ARM Cortex-Mシリーズで割り込みからの復帰に使われるEXC_RETURNの話を書いた。

EXC_RETURNを使わないといけない

さらに面倒な話があって、それが浮動小数点数レジスタのことである。


今回使っているマイコンはCortex-M33なのだが、浮動小数点演算用のVFPが付いている。

ARMv7で言うところのM3, M4に対応するARMv8のプロセッサがM33で、

M3とM4はVFPを積んでいるかどうかがだいたいの差で、

M33のVFPはオプションだったはずだが、大概積んでいる印象はある。

今回は浮動小数点演算を行うシステムなのでVFPを活用している。

整数の汎用レジスタとは別にVFPには単精度だと16個の浮動小数点数レジスタとFPSCRという状態レジスタがある。


これらのレジスタは割り込み処理で退避する対象となっている。

Cortex-Mシリーズではレジスタの退避はハードウェア的に行われるが、

浮動小数点機能を使わなければ32bitレジスタ8個(R0~R3, R12, LR, PC, xPSR)で済むのに、

浮動小数点を使うと+17個ということで大変である。

ただ、ここをサボるための仕組みがあるんですね。


Cortex-M4(F) Lazy Stacking and Context Switching – Application Note 298

M4の話だが、M33も仕組みは全く同じである。

まず、CONTROLレジスタにFPCAというビットがある。

初期値は0だが、浮動小数点命令を使うと自動的に1にセットされる。

FPCA=0で割り込みが発生すると8レジスタのみの退避を行う。

FPCA=1で割り込みが発生すると8+17レジスタの退避を行う……わけではない。

FPCCRレジスタの設定によるのだが、初期値(LSPEN=1, ASPEN=1)の場合は、

スタックポインタに8+17レジスタ分の領域を確保して、基本の8レジスタだけの退避を行う。

その上でCONTROL.FPCA=0, FPCCR.LSPACT=1 に設定する。


割り込みハンドラに浮動小数点演算がないパターンでは、FPCA=0のまま、EXC_RETURNに到達する。

この場合は割り込みハンドラ中にVFPレジスタは一切変化しなかったので、

CONTROL.FPCA=1, FPCCR.LSPACT=0 に戻すだけで完了となる。

あとは基本レジスタの復帰を行って、スタックポインタを戻して割り込み元に戻る。

割り込み処理の多くは浮動小数点演算を含まないだろうから、このパターンが比較的多いはず。

では、浮動小数点演算があった場合はどうなるか。

浮動小数点演算を行おうとすると、FPCA=0→1への変化が生じるが、

このときにFPCCR.LSPACT=1がセットされていたら、この時点でVFPレジスタの退避が行われる。

このようにレジスタ退避を後回しにする仕組みを”Lazy state preservation”と呼んでいる。

FPCA=1でEXC_RETURNに到達した場合は、VFPレジスタも復帰して元の処理に戻る。


で、この仕組みでもEXC_RETURNのアドレスが重要な意味を持っていて、

M33ではEXC_RETURNの4bit目が割り込み時点でのFPCAによって変わる。

割り込み時にFPCA=0の場合は4bit目は1、復帰時にスタックポインタを8レジスタ分だけ戻すことも表す。

割り込み時にFPCA=1の場合は4bit目は0、こちらはVFPレジスタ分もスタックポインタが進んでいて、

EXC_RETURN到達時のCONTROL.FPCAの値に応じて復帰要否が変わると。


で、昨日書いたCPU例外から再びmain関数に戻ってくる場合の話だが、

具体的にはEXC_RETURN=0xFFFFFFB8へジャンプすると、

Threadモード、Mainスタック、Non-Secure、特権モード、FPCA=0というリセット時と同様の状態になる。

ただ、このまま動かし続けるとそれはそれで問題があって、

それはFPCCR.LSPACT=1がセットされていると言うことである。

この状態で浮動小数点命令を実行すると誤ったレジスタ退避をしようとする。

ジャンプ後にFPCCRレジスタの値を明示的に設定する処理を入れれば問題は解消した。


割り込みハンドラでFPCA=0にセットする仕組みは多重割り込みにも効いて、

浮動小数点演算をしない割り込みハンドラ実行中に割り込みが発生した場合、

割り込みハンドラのレジスタ退避という観点では8レジスタの退避のみでよい。

レジスタ退避用のスタックサイズの削減にも寄与してるわけですね。

Mainスタックだけ使う前提で言えば、

  • main関数(VFP使用) スタック消費200byte+レジスタ退避104byte
  • 優先度2の割り込み(VFP不使用) スタック消費8byte+レジスタ退避32byte
  • 優先度1の割り込み(VFP使用) スタック消費40byte+レジスタ退避104byte
  • 優先度0の割り込み(VFP不使用) スタック消費8byte+レジスタ退避32byte
  • MPU例外ハンドラ スタック消費0byte

で、合計528byte消費する可能性があるわけですね。


全体的にはあまり深く考えなくても効率よく動くように考えられているが、

通常と異なる操作をするといろいろ踏んでしまうんですね。

この失敗がなければ気づかなかったことは多かったわけですけど。

EXC_RETURNを使わないといけない

最近はまたARM Cortex-Mシリーズのマイコンを使って仕事をしていて、

CPU例外発生後にその情報を引数で渡してmain関数に戻ってくるコードを書いたら、どうもうまく動かない。

main関数を再度呼び出す


ARM Cortex-Mシリーズでは割り込み発生時にハードウェア的にレジスタの退避が行われる。

なので割り込みハンドラでレジスタ退避のことを考えなくてよい。

それは知っていたのだが退避させたレジスタは復帰させなければならない。

この復帰処理に入るための仮想的なアドレス(EXC_RETURN)がリンクレジスタに入る。

リンクレジスタというのは関数の呼び出し元を表すレジスタで、

関数からreturnするというのはここにジャンプすることを指す。

で、例外ハンドラから出るときには何らかのEXC_RETURNにジャンプするべきようですね。


とはいえ例外ハンドラを出るときに例外発生場所に戻っては仕方ないわけで、

そこを変えたければスタックに積まれている例外発生元アドレスを変える必要がある。

sp+0x18が例外発生元アドレス、すなわち復帰先アドレスになっている。

退避されているレジスタの値を変えたければここを変えなきゃいけないんですね。

EXC_RETURNのアドレスもまた例外発生時の状態が保存されている。

復帰先のモードやレジスタ復帰方法がここで決まるんですね。

このあたりの値を変える処理はアセンブラで書かないと無理なので、

例外ハンドラから出るアセンブラのコードを書いて、例外ハンドラの最後から呼び出すようにした。


ただ、これはこれで問題があって例外ハンドラを出ると再度例外が発生するという。

例外ハンドラの中で例外フラグを消しておく必要があったようで、

CFSRレジスタをリードして同じ値をライトして、消去するとうまくいった。

このことからわかるが、例外ハンドラを実行中は例外が起きないんですね。

例外ハンドラからEXC_RETURNにジャンプすると再度例外が起こるようになると。

さらに言えば例外ハンドラの優先度は他の割り込みより高いので、例外ハンドラ実行中は他の割り込みが起きない。


ただ、他にも問題はあったんですよね。

それは浮動小数点レジスタの退避ですね。

普通はあまり意識しなくても効率よくやってくれるのだが、けっこう複雑な仕組みで……

このあたりもまた改めて紹介しようかなと。

これもEXC_RETURNが関係していると言うね。

ハイパーリンクで駆動するVBA

来週不在になる前にそこそこ動くようにしないといけないなと、

いろいろやっていたが、それなりに動く環境が完成して一安心?


今回、作り込んだ機能の1つにメンテナンスポートがある。

メンテナンスポートのシリアル通信でコマンドを送るとあれこれできるのだが……

問題はこのコマンドがテキストベースではないということである。

テキストデータの送受信ならTeraTermなど使えばよいわけだが、

バイナリデータの送受信となるとそれでは面倒である。

将来的には操作用のツールを作る話はあるが、暫定的に送受信できるツールが必要と、

これをExcelでVBAを使って作ることにした。


通常はワークシート上からVBAのプログラムを呼び出すにはボタンを置くが、

コマンドが多数ある中でボタンを多数並べるのは大変である。

それで調べるとハイパーリンクを使った方法があるという。

適当なセルに向けてハイパーリングを貼る。

このリンクはHyperlink関数で生成したものではダメである。

そしてWorksheet_FollowHyperlinkルーチンを定義する。

ハイパーリンクのクリックでマクロを実行する (グローウィン)

するとクリックすると、ハイパーリンクの情報とともにこのルーチンが呼び出される。


この方法のよいところはハイパーリンクの情報をもとに処理を振り分けられることである。

例えばハイパーリンクのセルのテキストにコマンドの入力・出力に使うセル名などの情報を書いておいて、

Worksheet_FollowHyperlinkルーチンではこれを解析して実行すると。

リンクテキストに情報を書き込むのは見た目がよくないが、

リンクのあるセルの右隣に必要な情報を書いておくとか、

機能のバリエーションが多い場合には有効な方策ではないか。


この方策によりメンテナンスポートで実行できる機能はほとんどExcelのワークシートから実行できるようになった。

これを使っていろいろ動作確認をしていたけど、やりたいことはだいたいできているように見える。

未実装機能はいくつか残っているし、ソースコードもいろいろ残骸が残って汚いが、

不在中の他のメンバーの作業には足りるぐらいの機能はあるんじゃないかね。

charとuint8_tは違う?

マイコンのプログラムを書いていたが、そろそろ終わりが見えてきた?

来週中にはある程度動くものに仕上げたいところだが……


比較的後回しになっていた機能として製造検査・メンテナンス用のコンソールがある。

コマンド名を識別するにはこんなコードを書くわけですよね。

if(strcmp(cmdname_buf, "FOO")==0){

そしたらcmdname_bufの型がconst char*と互換性がないというワーニングが出る。

形式的な問題であるのは明らかなのだが、なんでそんなこと言われたんだろ。


cmdname_bufは uint8_t型の配列である。これがchar型と互換性がないという指摘である。

ただ、実態としては同じもののはずなんですよね。

どちらもサイズは1バイトなので一致を比較する点では差はない。

char型は符号無し設定になっているわけだから、表せる値も同じである。


このあたりは処理系によるところもあるが、GCCで実験してみた。

_Generic文を使うことで型を識別することができる。

#define TYPESTR(x)  _Generic((x), uint8_t:"uint8_t", int8_t:"int8_t",default: "other")

これを使って unsigned char, signed char, char型を識別すると、

それぞれ uint8_t, int8_t, other に分類された。

なんとchar型は設定によりsigned charかunsigned charと切り替わるわけではなく、

どちらでもない型で扱われているのだという。これには驚いた。


もちろんこれはコンパイラ次第という話ではある。

ただ、文字を表すことを主眼に置いているchar型と、

数値を表すことを主眼に置いている int8_t, uint8_t型は分けて考えている場合があると。

そしてstrcmpは文字列の比較を想定したものなので、

数値の型を持ってくるとちょっと違うとなってしまうようである。


一方が文字列リテラルなので実質問題ないような気もするが、

strcmpは終端が正しくNULL文字になっていないとバッファオーバーランの危険もある。

そういう事情も考えれば専用の比較関数を作った方がいいかもなと思い出してきた。

そうすると今度は文字列リテラルとconst uint8_t*の変換で何か起こるかも知れないけど。

明示的にキャストすればいいんですけどね。


今日でドキュメント作業が片付いてやれやれと思ったのだが、

最初にドラフト版に書いた日付を見ると1ヶ月前で、えらいかかったなと。

他のドキュメントの改訂とかもやってたので時間がかかるのも仕方ないが、

当初の見積もりに比べるとちょうど1ヶ月遅れかなぁと。

これが終わったらその次にはやらないといけないこともあるけど、とりあえずは動くことが優先ですね。

main関数を再度呼び出す

散々遅れていたマイコンのコーディング作業を進めている。

つぎはぎだらけだが部分的に動くことを優先したらこの有様。

後で綺麗に整理しますけど。


この機器にはボタン操作で設定情報を変更できる機能がある。

設定情報を変更したら再起動という動作になっている。

再起動という観点ではウォッチドッグタイマを作動させるという方法も考えられるが、

設定情報による再起動であることを認識できるという観点では、

スタートアップルーチンにジャンプさせるのがよいだろうと。


というわけでまず最初はmain関数を再度呼び出すものを書いたが、

よく考えたら、これではスタックポインタがだんだんズレていく。

ジャンプ元の関数に戻ってくることはないのに、無駄にスタックを残している。

というわけでスタックポインタを再設定するコードをアセンブラで書いて、

これを経由してmain関数を再呼び出しするようにした。


ところがこれでも問題があって、それはRAMの初期化をしていなかったこと。

本来0で初期化されているはずの領域が0に初期化されていないことにより、不思議な動作をしてしまった。

これはこれでコーディングミスという側面もあったのだが、やはり問題である。

ということでゼロ埋めして、初期値付き変数セクションを初期化する処理を追加した。


最初はこの処理は設定情報の書換にだけ使うつもりで考えていた。

ただ、後にCPU例外発生後にも使うとよいことに気づいた。

というのもCPU例外が発生した場合、エラー情報を表示する必要がある。

このエラー情報の表示がそこそこ複雑な処理なので、例外ハンドラから直接駆動するのは難しいのではないかと。

そこで、同様の方法でmain関数に戻ってくればよいじゃないかと。

このとき引数を渡すこともできるので、そこに例外情報を埋め込めば、

その情報を表示したり記録したり出来るねと。


他にもやり方はあるとは思いますが。

リセットしても残るレジスタに情報を残しておくとか。

というわけでけっこういろいろアセンブラでも書いているという話だった。

Raspberry Pi PicoのPIO

職場で製品評価用の治具を作る話があって、けっこうお金かかるが作ろうと動いているよう。

従来、ファンクションジェネレータを接続していたところ、

治具にマイコンを乗せて代替するアイデアを出したところそれでいこうと。

で、USB接続できると便利だよなとか色々考えると Raspberry Pi Pico を載せるのが手軽ではないかと、

治具にはそれ用のソケットを用意するということで話が動いている。


Raspberry Piといえばシングルボードコンピュータの代表例で、

その上でLinuxなど動かしたりして使われることが多い。

ただ、Picoは高級なOSが動くようなものではないので全く違うものである。

今回想定しているのはUSBインターフェースだけのものだが、

無線LAN, Bluetoothを搭載したものもあって、通常インターネットにつながらない機器をインターネット接続化できる。

すなわちIoTの担い手を想定して作られたマイコンボードである。


ファンクションジェネレータの代わりというだけなら、もっと別の方法もあるが、

もう1つやりたいことがあって、それがシフトレジスタからのデータ取得である。

外部からクロックを入力すると、シフトレジスタのデータが読み出せて、

それで機器の状態を取得できる仕組みがある。

クロックを入力することはファンクションジェネレータでもできる。

ところが、このシフトレジスタから読み出したデータを取得するとなれば、

オシロスコープで読めなくもないが、やはりマイコンの出番であろうと。


クロックを出してデータを読み出すというのはSPI通信そのものだが、

困ったことに1つのクロックで2つのシフトレジスタから同時に信号が出てくるのである。

こういうのなんかあったなと思ったらQuad SPIか。

そういうのに対応したマイコンもありますけど。


というわけで、結局はGPIOでガリガリ操作しないといけないのか、

と思ったのだが、いろいろ調べているとRaspberry Pi PicoにはPIOという機能があるそう。

GPIOに接続できるステートマシンが4台あって、その4台共有で32個の命令を格納できるメモリがある。

このステートマシンはメインのプログラムとは独立して動作する。

いろいろ制約はあるのだが、特殊なインターフェースの制御に使用されるようだ。


PIOのステートマシンはIN命令用の入力、JMP命令用の入力、

OUT命令用の出力、SET命令用の出力、Side Set出力とそれぞれ連番で複数ピン設定できる。

さっき書いた2bit入力のSPIみたいなのは入力を2つ連番で取っておけばよいと。

IN命令・OUT命令はGPIOからFIFO、FIFOからGPIOに数ビット動かす命令で、

シリアル通信のデータはこの命令で出し入れするのが通常である。

SET命令はそれとは別にON/OFFを制御する命令なのだが、

実は命令に付帯してピンの出力値を規定できるSide Setという機能があって、

SPIのクロック出力みたいなのはこちらの方が容易に書ける。


ステートマシンの動作クロックを1MHzに設定して、

IN命令+Side Set 0とJMP命令+Side Set 1をぐるぐる回すと、

非常に大雑把には500kHzのSPIとして機能するということになる。

こういうのはわかりやすいが、実際にループ・条件分岐と複雑な動きもあるので、

思ったタイミングにするにはいろいろ工夫が必要そうだが。

命令数が32個と限られるのも制約ではある。

すくない命令を上手くやりくりする工夫として全命令にSide Setと遅延を付けられるが、

あわせて5bitという制約があり、Side Setを1bit使うと、

遅延時間は最大4bit、すなわち15cycleまでの遅延しかできないという。


制約は多いのだが、複数のステートマシンを組み合わせたり、工夫の余地はいろいろある。

命令が最大32個だと複数ステートマシンなんて厳しいと思ってしまうが、

同じ命令で動くステートマシンを複数作ることもできる。

そういうのも上手く活用している例もあるのかな。


GPIOでも作れそうではあるけど、せっかくならこういうのを使うのもいいのかも。

Raspberry Pi Picoなのは入手性・価格・利便性などを考えた結果だが、

特殊なインターフェースを作り込むことが想定されてたんですね。