シリアル通信の上限は?

ふと気になったことがあって、マイコンのデバッグで使うUART通信、

評価ボードだとUSB-UARTブリッジICが搭載されていて接続できたり、

あるいはRS-232Cに変換してPCと接続したり……

このときよく使われる通信速度として115200bpsというのがあるが、なんでこのスピードなんでしょうね?


そもそもUARTというのは調歩同期式シリアル通信の代表的なもので、

調歩同期式というのは信号線1本で通信を完結できる仕組みである。

一般的には送信・受信を分けて全二重にすることが多いが、それでも2本である。

SPI通信だとクロック・チップセレクト信号も伝達することが通常だが、

UARTではスタートビットの送信で通信開始を示し、

それを合図に取り決めたボーレートで順番にデータをやりとりして、

最後にストップビットをやりとりしたら1byte送信完了となっている。


UART通信を正しくやる上で肝心なのが時間精度である。

例えば9600bpsで通信する場合、104.2us周期で送信側は信号を切替、受信側はサンプリングしなければならない。

スタートビットからストップビットまでの間に1bitズレると通信に失敗する。

高速になるほど、これがシビアになるので難しい?


とはいえ、その上限が115200bpsとも思えないところはある。

確かにあるシステムでは同じ基板の上に乗ったマイコン同士が1MbpsのUART通信をしている。

なぜUARTなのかというと、この間に絶縁素子があるからである。

信号線1本(往復で2本)で伝達できるというのは絶縁という点でも有利である。

UARTというのはシンプルな方式なので、それ自体に上限はなさそうで、

問題は経路とインターフェースということになろうかと思う。

さっきの例だと絶縁素子のスピードで決まる部分が多いかと思う。

あるいはマイコンのUART機能はクロック周波数の8分周が最速とか決められてるので、

そういうところで最高速度が決まるということである。


通信相手がPCの場合に問題となりそうなのは、RS-232Cという伝送経路と、

USB-UARTブリッジICの性能というところが問題ではないかと思う。

今どきPC自体にRS-232Cが搭載されているということはそうそうなくて、

一般的にはUSB接続のRS-232Cインターフェースと組み合わせて使うが、

その内部にはUSB-UARTブリッジICが搭載されているのが通常である。

それをRS-232Cで引き出した先におくか、評価ボード上に置くかの違いである。


まず、RS-232Cの最高速度だが、規定上は20kbpsとなっているらしい。

この範囲内に入るボーレートでよく使われるのが9600bpsと19200bpsだが……

明らかにこれは実態に合っていなくてRS-232Cはもっと高速な通信で使われることは多い。

無難な数字として9600bpsとか19200bpsが使われることはあるが。

現実的な上限は150~250kbpsぐらいの範囲にあることが多いようだ。

ある基板に搭載されていたRS-232Cのドライバ/レシーバICではMaximum Data Rateは150kbps(min.)となっていた。

このICを介して行うRS-232Cの通信は150kbpsが上限になるということで、

冒頭に書いた115200bpsはその上限に近い数字である。

というか115200bpsの通信は通せる仕様ということでこうしたんじゃないか。


もう1つの要素としてはUSB-UARTブリッジICの対応速度がある。

1Mbpsぐらいは対応しているものが多いようである。

もう少し速くて2Mbpsとか3Mbpsとか対応したものもあるようだけど。

基板上で完結する通信ならば少なくとも1Mbpsぐらいは対応できるとみられるので、

USB-UARTブリッジICを基板上に置けばそのあたりが現実的な上限なのだろう。


というわけで115200bpsというのは現代的なシステムではほぼ対応できるボーレートで比較的速いものとして選ばれたのではないかと思う。

RS-232Cを通すとなると、さらに倍速の230400bpsが通るか通らないかというところ。

堅いのは115200bpsまでということなんだろう。

ただ、UARTという通信方式そのものは1Mbps程度までは対応可能で、

インターフェースや通信経路をよくよく精査すればより高速化は可能かもしれないと。


なんでこんな話を書いたかというと10MbyteぐらいあるデータをROMに書くのに、

マイコンにUARTで送って書き込んでもらうことを考えたのだが、

10Mbyteのデータを115200bpsで送ることを考えると、

UARTで1byte送るにはスタートビット+8bit+ストップビット=10bit送る必要があるので、

所要時間は 10×106×10/115200=868sと求まる。

868秒と言われてもピンとこないが、60で割ると14.5分である。

データ送信プロトコル(例えばYMODEM)のオーバーヘッドなど加味せずにこれだけかかるのである。


で、速くする余地はあるのかと考えたのだが、

まず最初に考えたのはデータ圧縮、ただ、すでに圧縮されているのか効果が見いだせなかった。

効果があればzlibとか組み込んでDeflateしたデータを送るとか考えたけど。

となると、通信速度自体を上げたいと調べたがRS-232Cを介する場合はこれが現実的な上限であると。

1Mbpsまで速くできれば100sだから、2分ぐらいで送信できる計算で、

これだとそんなに悪くない気がするが、さすがに15分は遅いなと。

というわけでシリアル通信で送る作戦は手軽でよいと思ったのだが、

こうも遅いとあまりよい方法ではないと思った。

他の手段がなければ15分かかっても仕方ないですけど。


結論から言えば、以前紹介したICEからプログラムでは無いものを書く方式が速かった。

ICEからプログラムでは無いものを書く

書き込み先のROMはマイコンのプログラムを格納するROMではなかったが、

マイコンには接続されているので、同じような書き込み方をすることはできた。

マイコンのデバッグインターフェースの速度次第だとは思うのだが、

一般的に考えればUARTよりは速いわけで、データ転送だけ考えれば速い。

あとはオーバーヘッドなど総合的に見てどうかという話ではあるけど、

数分で書き込み完了できたので、この方法がよいと思った。

ただ、ICEで接続できる場合に限られるので、やはりシリアル通信で書き込みたい話もあるかもしれない。

Cでレジスタを使ってやらかしてる

コンパイラのバージョンを変えたら正しく動かないという話があり、

なんのこっちゃと調べていたらレジスタやスタックの使い方が変わったことで、

既存のバグ(かそれに近いもの)が露呈した形である。


1つはスタックに確保した配列の範囲外を操作したことによるもので、

なぜこれが見逃されてきたかというのは気になるところだが、

配列サイズを適切に設定すれば解消する問題なので、これはこれでよい。

問題は残り2つで、元々スタックの使用方法に依存する処理と、

どうしてもレジスタ上で処理してほしい処理である。


スタックの使用方法に依存する処理はコンパイル結果に依存することは口伝されていたので、

新しいコンパイル結果に見合った調整を行えば回避できる。

が、そもそもスタックの使用方法に依存する処理というのが問題じゃないかという話で、

そこに依存しないように作っておくべきだったのではというのは……

この問題への僕の考える答えは後で書く。


もう1つのどうしてもレジスタ上で処理したい処理の話だが、

端的に言えばコーディングの問題である。

Cのコーディングでこの変数はレジスタ上に配置することを保証することはできない。

変数宣言時にregisterキーワードを付けるとレジスタ上に配置するよう努めるが、

必ずしもレジスタ上に配置しなくてもよいとされている。

それでコンパイラの独自拡張で変数の配置場所をレジスタに指定することができて、

これを使うことでCでコーディングしながらレジスタ上での処理が保証できるようになっていたが、

問題はこのレジスタ上に配置される変数がグローバル変数扱いだったことである。

グローバル変数といっても当該Cファイルを超えて、extern宣言でアクセスできるわけではないはずだが。


もう1つの問題が使用していたレジスタがスクラッチレジスタではなかったことである。

各アーキテクチャではABI(Application Binary Interface)の取り決めによりレジスタの使い方が決まっている。

32bit版のARMの場合、r0~r3は引数・返り値の受け渡しに使い、r14(lr)は復帰先のアドレスを格納するなど。

で、呼びされた関数で破壊してもよいスクラッチレジスタの規定があり、

r0~r3, r12の5つのレジスタは各関数で退避せず破壊してもよいことになっている。

これ以外のr4~r11, r13(sp), r14(lr)は使用する場合は各関数でスタックなどに退避する必要がある。


どうしてもレジスタ上でやりたい処理をスクラッチレジスタを使ってやっていればよかったのだが、

上を見てもわかるように32bit版のARMの場合、スクラッチレジスタの大半は引数・返り値の受け渡し用でもあり、

そういうレジスタをグローバル変数に割りあてるというのは到底認められないわけである。

そこでこれ以外のレジスタを使っていたのだが、退避させる処理が抜けていたと。

unsigned int reg4; //コンパイラ独自拡張でr4に配置
void something(void){
   unsigned int reg4_keep;
   reg4_keep = reg4;
   //reg4を使った処理
   reg4 = reg4_keep }

こう書いていればよかったのだが、そうなっていなかったのである。

というわけでこういう記述に修正してとりあえず回避できた。

なんでこれで従来問題になってなかったのかが不思議だが。

なお、コンパイル結果を確認するとスクラッチレジスタに退避させているようだった。


最大の失敗はこの処理をアセンブラで書かなかったことではないかと思う。

アセンブラで書いていれば、スクラッチレジスタを使って素直に書けたはずなので。

そもそもスクラッチレジスタとそうでないレジスタについて知識がなかったのでは? というのも気になるけど。

そんな処理があることは以前から知っていた(詳しくコードは見てなかったが)

ただ、ローカル変数をレジスタに割りあてる機能だと勘違いしてて退避処理も入れてくれると思っていたが、

そうではなく自分で退避させる必要があったというのも勘違いなのかもしれないが。


冒頭に書いたスタックの使用方法に依存する処理も、

スタックを使わないようにアセンブラで工夫して書くことはできるわけで、

そうしておけばコンパイル結果に依存しないコードになってたのになと。

もちろんアセンブラで書くことでコーディングのミスを起こしやすいとか、

長期的に見れば移植性の問題などあるにはあるけど、

結局コンパイラのバージョン違いで移植性の問題が起きてるじゃないかと。

最近別の仕事でアセンブラでコーディングせざるを得ないところをいろいろやって、

もうそれアセンブラで書けばと言ってしまいがちなのはある。

難しいことをアセンブラで書いちゃダメですけどね。

シリアル通信からプログラムをロードしたい

デバッグ用にシリアル通信でプログラムをロードして実行できるようにしたいという話があった。

以前のシステムではそのための特別なプログラムをマイコンに書き込んで、

そういうことをできるようにしていたのだが、今回はそれを通常のプログラムに入れた方がいいねと。


話によればU-Bootのようなブートローダーの機能を参考に作ったのだという。

今回のシステムでも当初はU-Bootが必要なのでは?

という話があったのだが、結局は不要となった経緯がある。

U-Bootにはシリアル通信でプログラムをロードするコマンドがあって、

当初の実験時に loadyコマンドでYMODEMで転送した覚えがあるような。

確かそのときはメモリ上に転送した後、mmc writeコマンドでSDカード(MMC)に書き込んだっただったか。

プログラムを保存する機能は持っていないけど、転送機能はほぼそれで、

あとはU-Bootで言うところの bootmコマンド、単純に指定アドレスにジャンプするだけだけど、

それを実装しておけばテスト用のプログラムをシリアル通信で転送して実行できるわけ。


元々マイコンに焼いていたコードを流用して作り始めたのだが、

かなり改変が大きく、跡形もないぐらいに変わってしまったかも。

アルゴリズム的にはあまり変わってないんですけどね。

内部的にはprintfもどきの関数を自作して持っていたりする。

他のデバッグでも表示にはこれを使うかと付替をしたりなんとか。


あとはそこにロードするプログラムのひな形も作ってみて、

実運用上、こういう使い方ができるといいなというのを多少加えた。

できたものを動かしてみると、確かにけっこう便利かもしれない。

最初はデバッグ用のプログラムをROMに書いて動かせばよいと思ってたが、

いろいろ特殊な事情もあるので、単純にROMに書いても動かない問題があった。

なのでデバッグ機能を作り込んだ方がよいねとなったのだけど、

それ以外のメリットとしてはROM書換用にICEなど用意する必要が無いこと。

ロードするプログラムを変えればいろいろな評価に活用できること。

(現在もそういう使い方をしているらしい)


従来よりこの機能の使い道も増えたので、新しい使い方もあるかもねとは言っているが果たしてどうか。

そこはわからんけど当初の機能の代替としては十分でしょう。

uintptr_t型を使う

以前、データ型モデルの話を書いた。

long型のサイズの方がアテにならない

64bitのシステムではポインタ型が64bitになるのはともかくとして、

それに伴ってlong型も64bitになるLP64というモデルがある。

UNIX系のシステムのコーディングではポインタを格納する整数にlong型を使う慣例があり、

その場合は修正量が少ないのだが、そういう使い方でなければ面倒なわけである。


元々、32bitのシステムのデータ型モデルであるILP32では、int型もlong型も同じ型で、

ゆえに使い分けもなく漫然とintとlongを混在させていたのが実情である。

移植にあたってはとりあえずはint型に統一してから考えようということになった。

で、新年早々に「全部、longからintに置き換えたコード作りましたよ」

というのでソースコードを見るとコメントのlongまでintに置換されていたので、

一括置換してから、ビルドエラーになる部分を調整してやったんだろう。


ただ、ワーニングはいろいろあるわけで……

int* ptr1, ptr2;
ptr2 = (int*)((unsigned int)ptr1 + (unsigned int)size2);

ポインタ型をint型にキャストして足し算して、ポインタ型に戻しているが、

64bit→32bit→64bitという変換になってしまっている。

元々は全て32bitだったから特に問題はなかった。

元がintだったかlongだったかはわからないけど、特に規則性はないので。


というわけでこの計算は64bit幅のlong型でやるべきなのだけど、

単純にlongに書き換えると修正漏れなのかパッと見てわからない。

で、こういう場合に使える表現があって、それが uintptr_t型である。

stdint.hで定義されている型の1つで、ポインタ型と同じ幅を持った符号無し整数型である。

これを使えばさっきのはこう書ける。

ptr2 = (int*)((uintptr_t)ptr1 + (uintptr_t)size2);

これだと意図が明確ですよね。


ところで他にもポインタ型と同じ整数型を表してそうなものがある。

stddef.hで定義されている size_t と ptrdiff_t である。

size_tはsizeof演算子の結果が格納できる符号無し整数型ということになっている。

現実的に配列などのサイズがそんなに巨大であることは考えにくいけど、

アドレス空間が64bitのシステムならば char arr[0x4000000000000]; みたいな配列を表現することはできる。

1PB(ペタバイト)のメモリを確保できるシステムが存在するかはさておき。

なので size_t型はポインタ型と同じ幅の符号無し整数型、すなわちuintptr_t であるのが通常である。

この配列の先頭と末尾の添字の差は -0x3FFFFFFFFFFFF~0x3FFFFFFFFFFFF  の範囲となるが、

そのような数値を格納できる型というのはポインタ型と同じ幅の符号付き整数型、すなわち intptr_t であるのが通常だという。


size_t と uintptr_t が同じ、ptrdiff_t と intptr_t が同じである保証はない。

違う型にする理由がないので実態としては同じ型であると考えてよいのだが。

配列の添字を表現する型としては size_t が最も適しているから、

for(size_t i=0; i<len; i++){
   sum += arr[i]; }

というようなときに使うとよいけど、そういう書き方が一般的とは思わない。


さっきのポインタ演算だけど、char型のポインタを使ってこう書いても同じ意味になる。

ptr2 = (int*)((char*)ptr1 + (size_t)size2);

ポインタ型は1進めると、配列要素1個分進むことになっている。

なのでint型のポインタにy加算すると、ポインタの数値は4×y増えることになる。

これは面倒な話なのだが、1byte型であるchar型のポインタだと加算した数値がそのまま進むことになる。

なので一旦char*型にキャストして加算して、目的のポインタ型にキャストするという方法が考えられる。

さっき書いたように配列の添字を表す型としてはsize_t型が最も適しているので、

size2はsize_t型にキャストしてchar*型に加算している。


ただ、この書き方もそれはそれでわかりにくいなと思ったので、

冒頭に書いたように uintptr_t型を使うのが最もわかりやすいと思った。

結局のところはポインタ型のサイズの整数型で演算が行われるわけですからね。

というわけで、多分こう言う形になるんじゃないか。

ICEからプログラムでは無いものを書く

今日は忘年会ということで、明日は休暇にして仕事納め。

そんな仕事納めの日に、ICEからROMにプログラムではないデータを書き込むという方法を検討していた。


プログラムが格納されるROMの一部に各種の情報を保存しておく機能があるが、

書き込み機能はとりあえずなくても動くということで、

試作品の評価の初期ではその部分を作り込まずに開始する予定だという。

ところがその保存しておくデータの1つにユニークな番号があり、

この番号が試作品間で重複すると正常に動かないのだという。

このため、この番号を仮に固定値とすると、試作品ごとに異なる固定値を設定したプログラムを焼かなければならない。


そこでこの番号を読み出す機能については最初から作り込んでおき、

書き込み機能はデバッグ環境のICEからプログラムを焼く機能で代替しようという話になった。

ROMにプログラムを書き込むことが出来るならば、

同じROMにプログラム以外のデータを書き込むこともできるはずじゃないかというわけである。


デバッグ環境もいろいろだと思うのだけど、今回使っている環境はELF形式のオブジェクトファイルでなければ書き込めない。

バイナリデータを与えればダウンロードできるようなデバッグ環境もありましたが。

それでFAQとか調べてみたのだけど、バイナリデータをダウンロードするにも、

プログラムデータという体裁を作らないといけないようである。

バイナリファイルを読み込んで指定のセクションに配置する機能があるので、

この機能を使ってバイナリデータをROM上の所定の場所に配置させる。

その上でダミーのCファイルとともにビルドしてやると、

ROM上にバイナリデータが配置されただけのELFイメージができる。

これをダウンロードしてやれば、ICEからROMに任意のデータを書き込めるわけである。


今回の場合、書き込みたいデータというのはユニークな番号なので、

それならもはやCのコードで書いてしまえばよいのである。

const char DATA[4]={0xDE,0xAD,0xBE,0xEF};

これでDATAをROM上の所定のアドレスに配置すればよいと。

この番号を書き換えて、試作品1台1台にダウンロードしていけばよいわけである。


というわけでユニークな番号を振る作業については、

まさに1台1台に異なるプログラムを書き込む作業になってしまった。

ただ、この番号が書き込まれる領域はプログラムが格納される領域とは別だから、

プログラムの修正を行っても揮発しないわけで、最初の1回だけで済むという点では確かにメリットはある。

というわけで一件落着と。

マニュアルも書いて年内の仕事を終えることが出来てよかった。

フィッシングではなかったメール

こんなタイトルのメールが届いていた。

【PayPay】3月22日までに本人確認情報の定期確認・最新化をお願いします / Announcement from PayPay: Please update your identification information by 3/22

なんかフィッシング詐欺っぽいなと思ったのだが、本物だったらしい。


と、気づいたのはメールを精査したからでなく、PayPayアプリでプッシュ通知が来ていたからなんだけど。

さすがにそれはなりすませないよねって話である。

やること自体は簡単で登録情報が正しいか確認するだけである。

もし、変更があれば変更に応じて書類提出ってことになるんだろうけど、

変更がなければそれで終わりということである。簡単だね。


メールを精査すると、送信元が info@paypay-corp.co.jp で、

DMARCがpassなので、送信元が paypay-corp.co.jp であることはある程度確からしい。

問題はこれがPayPayなのかという話で、paypay.ne.jp じゃないのかと思ったが、

ユーザーへの連絡はこのドメインを使っているようである。


あと中身もフィッシング詐欺と思われないような工夫はあって、

1. PayPayアプリホーム画面右下の[アカウント]をタップ

2. 画面上部の「本人確認情報の確認期限が近づいています。3月 22日までに確認してください」の表示をタップ

3. 画面の案内に沿って本人確認をしてください

と手順の説明にはリンクも入れずに、アプリの操作方法だけが書かれている。


他の金融機関でもそうで、SBI証券からの電子交付の通知も

「汎用累投売買報告書」を、電子交付(記録)いたしましたのでお知らせいたします。
当社WEBサイト(ログイン後の「口座管理」>「電子交付書面」画面)にてご確認ください。

なんて書き方ですからね。

入り組んだアドレスを指定してくるようだと怪しいとも言えるが、

そこら辺の区別が付くかというのはけっこう難しい話である。


金融機関になりすましたメールって多いんですよね。

前にJCBのなりすましメールが精度が良いという話を書きましたが。

JCB Webmasterからのメール

うっかり開いてしまっても、本物の金融機関はそんな指示はしてこないだろうと、

そういう感覚が持てるかどうかも重要なのだが、これはより難しい話である。

リンク自体貼らないのが最も本物らしいとは言えますが、

そうでなくてもリンクは踏まなくてもなんとかなるというのが通常ですかね。

インライン関数が展開されない

コンパイラのWarningを確認していたのだが、

その中で関数のプロトタイプ宣言がないことに対する警告があった。

よくある警告なのだけど、その関数ってインライン関数なんだけど?

なのにリンカーでエラーにならずに生成物が出来ていて、なんだこれ? と。


それで気になってmapファイルを見てみたら、なんとインライン関数のはずの関数が何個も存在した。

本来ならばインライン関数というのは呼び出し元にインライン展開されるはず。

なのに、そうなっていないのである。

というわけで一体これはなんだとなったのである。


インライン関数だからといってインライン展開される保証はないのは知ってたけど。

static inline void dmb(void){
   __asm volatile("dmb" : : : "memory"); }

この関数宣言に inline というキーワードを付けているが、

こうするとこの関数はインライン展開できるということを表す。

インライン展開しなければならないという意味ではない。

ただ、一般的にインライン関数というのはごく小さな処理であることが多く、

この例の場合、dmb命令1つに変換すればよいだけなので、インライン展開したほうが明らかにお得である。

にも関わらずinlineという指示を無視して、一般的な関数として扱ったわけである。

でも、inlineという指示を無視してはいけないという法はないのである。


ところでインライン関数というのは、他の関数とは異なりヘッダファイルで定義する。

インライン展開するためにはコードの中身が必要だと言うことなんだろうな。

その都合でインライン関数を含むヘッダファイルを読み込んだCファイルごとに、

インライン関数が通常の関数として生成されてしまったようである。

この結果として同名の関数が何個も存在している状況が起きたようだ。

staticを付けているので、インライン関数はそのファイル限り有効となるので、こういうことになったようだ。


どうにも不気味なのでマニュアルを調べたのだが、

コンパイラ独自の指示を付けることで強制的なインライン化が可能なようで、

それを付けたら同名の関数が何個も作られることはなくなった。

命令1個なんだからインライン展開した方が明らかにお得なのは当然。

でも、そういう判断はできなかったようだ。


このインライン関数はあるライブラリで定義されているのを活用したのだが、

もしも自分で新規に作れと言われたら、マクロで作ってたんだろうな。

#define DMB() do{ __asm volatile("dmb" : : : "memory"); }while(0)

これでいいじゃないって。

これで DMB(); と書くと、その場にインラインアセンブラが展開される。

マクロは文字列の置き換えで実現する都合、様々な罠があるので、

マクロよりもインライン関数を使う方が安全ですよという話はある。

文字列の置き換えだからこそ実現できる処理があることも事実だが、

一般的な関数の表現で表せるものならばインライン関数の方がよいよと。

ただし、インライン関数がインライン展開される保証はないのだけど。


マクロも書き方を工夫すればそれなりに安全ではある。

#define maxint(a,b) ({int _a = (a), _b = (b); _a > _b ? _a : _b; })

マクロの引数は()で囲ってまず変数に代入する。

そして、この変数を使って大小比較して、大きい方の値を返すと。

この一旦変数に代入するという操作をせずに、

#define MAX(a,b) ((a) > (b) ? (a) : (b) )

と書くと、もし引数a,bに副作用があったときに特に問題になる。

もっともこの({…})という構文はGCCの拡張構文らしい。

使用しているコンパイラではGCCにならって対応しているという扱いで問題ないが。

対応していないコンパイラだとインライン関数しか手はないかも。

返り値がない場合は do{…}while(0) でいいんだけど。

コンパイラにサイズ計算させる

以前、こんな話を書いた。

long型のサイズの方がアテにならない

32bitのプロセッサで一般的なILP32と、64bitのプロセッサで一般的なLP64を比較すると、

long型とポインタ型のサイズが異なるという話。

想定されているような使い方ならばコード修正は少なくて済むのだが、

データ構造の都合、サイズが変わっては困るとなればなかなかそうもいかない。


というわけでこのあたりの見直し作業を進めていたのである。

long→intの置き換えで済むのではないかと思ったかも知れないが、

実はいろいろ複雑な事情があり、それだけでは済まない点が多々ある。

この移行作業が正しく出来ているかの確認を何らかの方法でしないとなとは考えていた。


そこで前々から考えていたことを実行に移すことにした。

コンパイラのオプション指定でリストファイルを出力することができる。

ビルド結果をアセンブラのコードで確認出来るというものである。

この機能を有効化してこんなコードをビルドした。

const size_t size_FOO = sizeof(FOO);
const void* addr_DEVX_REGY = &DEVX.REGY;
const size_t ofst_FOO_var1 = (size_t)&((FOO*)NULL)->var1;

size_FOO は FOO型のサイズを格納した定数となる。

マイコンのプログラムではペリフェラルのレジスタ群を構造体で定義して、

#define DEVX (*((DEVX_T *)0x22101000))

のようにして、DEVX.REG1=0x0001; のように使うことがある。

DEVX.REG1のアドレスは定数演算で求められるので、addr_DEVX_REGYはこれを格納した定数となる。

3つ目も構造体のメンバーに着目したもので、構造体内のオフセットアドレスを調べている。

NULLポインタを構造体のポインタに変換して、メンバーのアドレスを取得している。

NULLポインタに構造体先頭からのメンバーまでのオフセットアドレスを加算したものは定数演算で求められる。

NULLポインタ=0なので、構造体内のオフセットアドレスが ofst_FOO_var1に格納されるというわけ。


こんな形で新旧の環境でのビルド結果を並べて比較すると、

ちょこちょこミスが見つかって、多くは単純な計算ミスなのだが……

アライメントの都合で想定外のパディングが行われたり、

定義にそれなりの見直しが必要な点もあった。

というわけで、この作戦はかなりうまくいったのではないかと思う。


もっとひどいミスもあるんじゃないかと思っていたが、

思っていたほどのミスはなかったかな。

でも、このままデバッグに進んでたら惨事だっただろうから、今発見できたのはよかったかな。

memset関数の意味

Cの標準ライブラリ関数にmemsetというのがある。

というのを最近まで知らなかったんだけど。

移植作業をしているプログラムではわりと見るので知ったんだけど。


memset((void*)&foo, 0x00, sizeof(foo));

構造体を全部0x00で埋めて初期化するというのでこんな書き方がある。

こういう書き方をすることは知らなかったけど、なるほどなと。


配列を0埋めするという使い方が多いのかと思ったのだが、

構造体に対して適用されている例が多い印象はあった。

ちょっと変な使い方としてはこんなものあって……

memset((void*)&foo.reserved2, 0xFF, 6);

構造体のメンバーreserved2を含む6byteを0xFFで埋めるという内容。

6byteを埋めるならmemsetを呼び出す理由もわからなくはないが、

ところによっては1byteとか2byte埋めるだけにmemsetが使われているところも。

それはもはや foo.reserved3=0xFFFF; と書いた方がよいのでは?

他のmemsetと表記を合わせたかったのだろうか。


memset関数というのは内部的には2byte, 4byteアクセスなどに変換しているとみられる。

シンプルに言えばmemsetというのはこういう処理ではある。

unsigned char* p=s;
for(size_t i=0; i<n; i++){ p[i]=c; }

ただ、1byteずつポロポロと書き込みを行うのは効率が悪いので、

上記でpが4バイトアライメントされていて、かつnが4の倍数とすれば、

unsigned int cx4 = (c<<24)|(c<<16)|(c<<8)|c;
for(size_t i=0; i<n; i+=4){ *((unsigned int*)(p+i))=cx4; }

のような効率化が可能ということになる。

そういうのを内部的に判断してやっているという点で価値があるのかもしれない。


実は移植作業の中でmemsetは数を減らしていく方向ではある。

というのもバイトオーダーの差などで従来通り適用できないケースがあるため、

少量であればバラして表記する方が確実性が高いという判断である。

初期化サイズが大きいところでは従来通り使うとは思うが、

サイズが小さい場合はあえてmemsetを使うメリットも少なくて、

冒頭に書いた例にしてもfooの構造体メンバーが3つならば、

foo.a=0; foo.b=0; foo.c=0;

で特に問題ないという理屈である。

パディング部分も含めて初期化することを狙っているケースもあるが、

そういうのもよく検討すれば問題にならないのではないか。

char型は符号ありでよかったか

以前のマイコンで使っていたソースコードを移植して、

コンパイラのWarningを調べていたら符号なしの数値と負数の比較をしていると出てくる。

なんでそんなことになってるんだ? と調べて判明したのはchar型の取扱だった。


移植後のコンパイラでは char型を符号無しとして扱う設定がされていた。

そんな設定した覚えないのだが……と確認したら、標準設定がそれだった。

で、元々のマイコンのコンパイラはchar型は符号ありだったそうで、

ここが食い違いの原因だったらしい。


Cで単にintとかlongとか書けば符号ありなのだが、

charというのは符号の有無は環境依存だと定義されている。

このため符号ありと明示するには signed char、符号なしと明示するには unssigned char と書くべきとなっている。

それぞれ int8_t, uint8_t と書いた方が意図は明確かもしれない。


なぜこういうルールなのかというので調べるとこんなのが見つかった。

char の符号が処理系定義な理由 (Zenn)

‘a’のような文字リテラルはchar型で負数ではないと規定されていて、

ASCIIコードの範囲であれば0~127なので符号付きの1byte型でも、符号無しの1byte型でも問題ない。

文字を表すという観点で言えば、0~255が表現できる符号無し型の方が有利である。

とはいえ、char型は文字の表現だけではなく1byteの数値表現にも使うことができる。

その観点では単にintと書けば符号ありになるのと同様、charも符号ありの方がよいという考えもある。


環境ごとにどちらが標準かというのはだいたい決まっているらしく、

確かに基本的にcharはunsignedと規定されていた。

旧来のマイコンは基本的にcharはsignedと規定されていたのだろう。

(少なくともコンパイラはそう解釈していたことは裏が取れている)

ゆえに流用コードをビルドするには signedと設定した方がよいようだ。


標準が符号なしというのは今まで気づいてなかったのだが、

char型が符号ありかなしか一意に決まらないということは知っていて、

Warningが出てる場所を少し調べたらすぐ見当は付きましたね。

くれぐれもご注意を。