FRAMを使用した走行ログの記録方法と可視化方法について
この記事は、マイクロマウス Advent Calendar 2025の22日目の記事です。
昨日の記事は、SHIMOTORI,Harukiさんの『「今年は色々初めてを経験したなぁ」を振り返る 』でした。
学生時代にはロボトレース競技でブイブイ言わせていたSHIMOTORIさんですが、社会人になって講演や展示等の経験をされ、より活動範囲が広がったようですね。一つのことばかりでなく、色々と経験することは自分の生活を豊かにする源だと思っていますので、私もまだまだ頑張ってみたいと思います。
はじめに
さて、最初のカレンダー登録時には「FRAMの簡単な使い方(予定)」と、とりあえず入れました。しかし、最近はAIを活用してコードを生成したり、マイコンメーカーから提供されているライブラリを使用することで、簡単にSPI通信を実装して外部メモリを接続出来るようで、このネタで記事を書く意味がないかとも思い、「未定」と変更しました。何を書いたらいいか悩んでいたところ、東日本地区大会の控室で、一部の方からその内容で読みたいとの声を頂きましたので、やはりFRAMについて書くことにしました。
マウス内部データの保存
マイクロマウスもロボトレーサーも走行速度が上がってくると、プログラム通りに上手く動いているか、それとも何か問題が発生しているのか、動作を目で見ているだけではわからなくなります。そんなときは、センサ値やエンコーダ値、モーターを駆動するためのPWM値等々を、一定時間毎(例えば制御周期毎)に記録しておき、それを後からパソコン上でグラフ化する等して確認することで、ロボットの動作がプログラムした通りに動いているのか、どうかが、簡単に確認出来るようになります。私のマウスはSHマイコンに移行した2010年頃の機体から、走行ログを記録するようにすることで、より高速に走行することが出来るようになりました。
しかし、例えば32種類の2byteデータを記憶するには64byteのメモリが必要となります。そして、その64byteのデータを制御周期の1ms毎に毎回記録するようにすると、1秒間の記録のためには64000byte、約64KBのメモリが必要です。マイクロマウス等に使用されているマイコンに搭載されている内蔵RAMは、最近は容量も増えてきましたが、数秒間の動作の内部データを記憶させるためには、全く容量が足りません。
そのため、マイコンの外部にEEPROMやフラッシュメモリ等の書き換え可能な大容量の不揮発性メモリを搭載することが考えられますが、これらのメモリは一度に書き込むブロックサイズが決まっていたり、書き込みを指示してから内部のメモリ素子への書き込みが終了するまでに数ミリ秒の時間がかかるものが殆どで、大きなサイズの記録には便利ですが、少ないデータを短い間隔で絶え間なく書き込むためには不向きです。
FRAMとは
そこで、マウスに搭載する外部メモリとしてオススメなのが、FRAM(Ferroelectric Random Access Memory(強誘電体メモリ))です。これは、1バイト単位で書き込みが出来る上に、書き込み指示の終了から内部のメモリへの書き込み完了までのタイムラグが全くありません。ただし、デメリットとしては、フラッシュメモリほどの大容量の製品がないことと、素子の価格が高価なことです。フラッシュメモリは4Mbitだと秋月電子で100円以下で買えますが、FRAMは4Mbitでも3000~4000円と、驚くほどの値段です。
……と、ここのあたりまでを東日本地区大会の控室で頑張って書いていたんですが、ググってみたらRSのサイトにFRAMの説明が詳しく書いてありました(涙) 特性やらなにやらは、以下をご参照下さい。
https://jp.rs-online.com/web/content/discovery/ideas-and-advice/fram-guide
ということで、FRAMの特徴は把握出来たと思いますので、マウス(トレーサー)にFRAMを乗せて走行ログを取る方法を説明します。
FRAMの入手
FRAMは、現在は数社から出ているようですが、マイクロマウスのログ記録に使用出来る容量の2~8Mbit(256KB~1MB)のものを入手するとなると、RSやDigikeyで容易に購入出来るInfineon社のものがいいでしょう。最近のマイクロマウス全日本大会のスポンサー様でもありますし(ヨイショ)。2Mbitの頃は、RAMTRONという会社が出していましたが、4Mbitが出た頃にはCypress社に吸収(?)され、そしていつのまにかInfinion社になってました(型番に、Cypress時代の名残が…)。
パッケージや容量、電源電圧で色々種類がありますが、私は、クラシックマウス(雪風8AS)では4Mbitの「CY15B104Q-LHXI」、トレーサーの新作(しまかぜ)では8Mbitの「CY15B108QN-40SXI」を使ってます。私の使用しているものは、どちらもピン間隔が1.27mmのですが、一般的なSOP8とはパッケージが違います。詳しくはデータシートを見て下さい。ハーフサイズマウスに使用するなら、8Mbitで約3mm×3mmのパッケージのものもありますので、こちらがオススメです。ただし、非常に高価です。
FRAMとマイコンとの接続
FRAMを入手出来たら、それをマイコンに搭載されているSPI通信機能のポートに接続します。RXシリーズだと、RSPIユニットですね。汎用シリアルポート(SCI)の簡易SPIモードでも読み書き可能ですが、RSPIの方がビットレートを速く出来る(高速に読み書きできる)場合が多いので、RSPIの方へ接続した方が良いでしょう。また、FRAMのCS端子への接続は、RSPIのSSL(スレーブセレクト)出力を接続して制御することも出来ますが、一度、書き込み先アドレスを設定した後にデータを連続して書き込むバースト転送モードを使用するときは、CS端子をアサート(Lに固定)したままにしてSPIデータ送信を連続で行いますので、汎用IOポートの出力端子でコントロールした方が、制御が判りやすいです。接続の絵は、FRAMのデータシートにある図を参考にしましょう。
FRAMの電源電圧は3.3Vなので、最近の3.3V系マイコンには各信号線を直結するだけでOKです。CS端子はマイコンのリセット時に不安定になると困るので、プルアップ抵抗を付けて、先に書いた通り汎用I/Oポートの出力端子の一つに繋ぎます。上の図ではWP端子やHOLD端子がマイコンと接続されていますが、ハードウェアでの書き込み禁止等は使用しないので、それらはVDD直結で大丈夫です。なお、HOLD端子は、8Mbit品はDNU(使用禁止)端子となっていますが、この場合もVDDに直結しておけば大丈夫です。
FRAMの簡単な使い方
マイコンとの接続が出来たらプログラムからデータの読み書きを行いますが、Infinionで公開されているデータシートを読めば、自分で1から制御プログラムを書くことは、それほど難しいことはありません。…と、データシートへのリンクを張ろうとしたら、ちょっと前まではInfinionのサイトに日本語のデータシートが公開されていたんですが、今見てみると登録しないと見られなくなったようです。登録してInfinionからダウンロードするか、RSオンラインの4Mbit品のページにCypress時代の日本語データシートがありますので、そちらをご参照下さい。
要約すると、書き込みは、
「書き込み許可コマンドを送信」→「書き込みコマンドと書き込み先アドレスを送信」→「データを送信」→「書き込み禁止コマンドを送信」
で終了です。読み込みは、
「読み込みコマンドと読み込み先アドレスを送信」→「データを受信」
だけです。もちろん、各コマンドやデータの送受信前に、CS端子を適宜アクティブにする必要があります。細々と説明するのも面倒なので、雪風8ASで使ってるFRAMの制御ソースを公開します。ご自由にお使い下さい。
大体のコメントが書いてあるので、それを見ていただければ、何をやってるか判ると思いますが、
FRAM_read(unsigned long addr, short num) が、指定のアドレスから必要バイト数(バッファ容量分)読み込む関数
FRAM_write(unsigned long addr, short num) が、指定のアドレスから必要バイト数(バッファ容量分)書き込む関数
です。ただし、RSPIの送信データレジスタに1バイトずつデータを転送すると、毎回、送信データレジスタが空になる(送信可能になる)のを待つため、CPUの処理時間(占有時間)が増えてしまいます。そこで、私のプログラムでは、RSPIユニットに搭載されている128bitのバッファを活用して、16バイト分のデータ転送を一気にバッファに書き込むことで、CPUの処理時間を短縮させる工夫がしてあります。FRAM_bw_start(unsigned long addr)という関数ですね。これで、RSPIに内蔵されているバッファの容量分のデータ(16byte)を書き込んだ後にRSPIユニットに送信開始を指示することで、FRAMとのSPI通信が終了することを待つこと無く、プログラムは次の処理を行うことが可能となります。
私のマウスでは、1ミリ秒の制御周期毎に64byteのデータをFRAMに保存していますが、バッファは16byte分しかありませんので、制御周期を5分割し、200マイクロ秒毎に割り込みを発生させて、モーターの制御やセンサ類の読み込み、FRAMへデータ書き込み等を分割して行っています。5分割しているので、バッファを使用したFRAMへの書き込み量は16×5=80byteまで可能ですが、FRAMに記録していたデータをマイコンから外部にシリアルで転送するときにデータの管理単位、というか、後述するバイナリ転送のブロックサイズの計算が面倒になるので、1周期での記録は64byteとしています。
ちょっと省略してますが、CMTで200マイクロ秒毎の周期割り込みでの処理は、こんな感じです。
---------------------------------------------------------------------------------------------------
// コンペアマッチタイマ(CMT0)による割り込み -------------------------------------------------------
void int_cmt0()
{
int i;
disable(); // 割込禁止
int_cmt0_count++;
// 以下、1ms毎に処理するイベントを200us毎に分割して実行
// 1 --------------------------------------------------------------------------------------
if (int_cmt0_count == 1) {
scan_sensor1(); // バッテリ電圧、モータ電流の読み込み
calc_position(); // エンコーダ読み込み
motor_control(); // モータPID制御
scan_spigyro_start(); // ジャイロのFIFOモードでの読込み開始を指示
}
// 2 --------------------------------------------------------------------------------------
else if (int_cmt0_count == 2){
scan_sensor2(); // 横壁センサの読み込み
make_gyro_data(); // ジャイロデータの補正
if (BB_Flag) BlackBox(1);
}
// 3 --------------------------------------------------------------------------------------
else if (int_cmt0_count == 3) {
scan_sensor3(); // 前壁センサの読み込み
make_gyro_data2();
if (BB_Flag) BlackBox(2);
}
// 4 --------------------------------------------------------------------------------------
else if (int_cmt0_count == 4) {
scan_sensor4(); // 斜め壁センサの読み込み
make_accelerometer_data();
if (BB_Flag) BlackBox(3);
}
// 5 --------------------------------------------------------------------------------------
else if (int_cmt0_count == 5) {
DBG_WDT_WDTSR = WDT.WDTSR.WORD;
WDT.WDTRR = 0x00;
WDT.WDTRR = 0xFF; // WDTのカウンタをリフレッシュ
//}
if (BB_Flag) BlackBox(4);
int_cmt0_count = 0;
ms_counter++;
}
// ----------------------------------------------------------------------------------------
if (int_cmt0_exec_wait != 0) int_cmt0_exec_wait--; // 1ms割り込み処理待ちカウンタ
int_cmt0_exec_time[int_cmt0_count] = CMT0.CMCNT; // 割込処理に掛かった時間を保存(for debug)
enable(); // 割込許可
}
---------------------------------------------------------------------------------------------------
BlackBox(?)が、内部データをFRAMへ記録する関数です。マイコンのメモリ上の変数のうち、指定したものをFRAMに書き込む関数ですが、内容については面倒なのでソースを公開しておきます。
MMblackbox.c
最初の方の#define文で、保存しておきたい変数を記述しておいて、BlackBox()関数で、ミリ秒単位での記録位置をFRAM上でのメモリアドレスに変換して、保存したいデータを16byteずつFRAMに書き込んでます。FRAMのメモリがいっぱいになるまで書き込んだら(可能な記録秒数だけFRAMに書き込んだら)おしまいです。
…と、小細工をすることでCPUの処理時間を減らそうと努力していますが、RXマイコンにはDMAやDTCが搭載されていますので、それを使えばCPUの処理時間を簡単に減らすことが出来ます。それらを使用出来る方は、使った方が良いでしょう(これ、今回のオチです)。
記録したマウス内部データの転送
さて、マウスの内部情報をFRAMに保存出来るようになりましたが、保存されたデータは、外部のPCに転送しないと可視化して解析することが出来ません。つまり、今度は逆にFRAMから必要なデータを読み込み、それをシリアルポート等でPCにアップロード(転送)します。
このとき、保存されているデータをCSV形式に変換して転送すれば、PCでテキストデータを受信するだけなので簡単ですが、1バイトのデータでもテキスト変換した数値に加えて区切りの「,」を送信しないといけないため、データ送信量が2倍以上になってしまいます。
そこで、速度向上のためには、バイナリデータをそのまま送信するのが効率的です。ただし、通信エラー等が発生して、正しいデータを受信したつもりでも違うデータを受信していたりする恐れもありますので、エラー検出を行うバイナリ転送プロトコルを実装する必要があります。
このあたりを語り出すと老害の昔語りになって長くなりますので省略しますが、PC側でTeraTermを使ってバイナリデータを受信するなら、パソコン通信時代に各種生み出されたようなファイル転送プロトコルを実装するのが手っ取り早いです。
一番簡単なのは、XMODEMですが、これには、チェックサムを用いるものと、CRCを用いるものがあります。実装が簡単なのはチェックサムですが、パケットサイズが128byteなので、あまり転送効率がよろしくありません。CRCはパケットサイズが128byteと1024byteの2種類がありますので、CRC1024を頑張って実装したのが、上記ソース内のBB_Upload(short t)関数です。goto文とか使ってますが、気にしちゃダメです。実装が楽になるなら、使わない手はありません。ただ、私のマウス用に特化した関数であり、純粋なXMODEM転送ルーチンではないので、自力で実装したい方はXMODEMでググって調べて下さい。
あと、マウス-PC間の通信速度は、パソコン通信時代とは比べものにならない位に速いので、パケットサイズは1024byteではなく、4096byteとか、もっと大きい方が、効率的に、速く転送出来ますが、そうすると、パソコン側の受信プログラムも新たに作成する必要があるので、普通のターミナルソフトでは受信出来なくなります。
また、バイナリデータでマウスの内部データを転送するようにすると、転送しているデータが何なのか判りませんので、マウスからPCにデータを転送するときに、保存しているデータの変数名も転送出来るように、変数名を文字列データに変換して保存しています。このとき、#defineで定義している変数名を単に文字列変換しただけだと、二重に#defineした変数だったりしたときにその変数名が保存されなかったりするので、一手間掛けています。
ということで、バイナリ転送したファイルは、こんな感じです。000000~0003FFまでは、16バイト毎に保存してある変数名(32個)、000400~は、制御周期毎の2byte×32個のデータです。バイナリデータは、そのままではエクセル等で使用出来ませんので、私の場合は、エクセルのマクロ(VBA)で数値データに変換しています。
バイナリデータの可視化
エクセルのVBAというと、あまりいい顔をしない方も多いですが、ちょっとしたことをやらせるなら便利なものです。便利なものは使わない手はありません。
ということで、こんなエクセルの「開発」タブを開いて、Visual Basicでこんな関数を作れば、バイナリデータをエクセルのシートに貼り付けることが出来るようになります。
---------------------------------------------------------------------------------------------------
Sub load_blackbox_file()
'バイナリ形式のBlackBoxデータファイルの読み込み
Dim Title(64) As String
Dim BB_Data(64) As Integer
ChDrive ThisWorkbook.Path
ChDir ThisWorkbook.Path
BBFilename = Application.GetOpenFilename("BlackBoxDataFile,*.dat,全てのファイル,*.*")
If BBFilename = False Then End
If Dir(BBFilename) = "" Then End
Open BBFilename For Binary Access Read As #1
Worksheets("Sheet1").Cells(1, 1).Value = " "
Worksheets("Sheet2").Cells(1, 1).Value = BBFilename
'項目名の読み込み(64項目)
For i = 0 To 63
Title(i) = ""
For j = 0 To 15
BinData = AscB(InputB(1, #1))
If BinData <> 0 Then
Title(i) = Title(i) + Chr(BinData)
End If
Next
Next
Worksheets("Sheet1").Range(Cells(1, 2), Cells(1, 65)) = Title
y = 2
Do While Loc(1) < LOF(1)
BB_Data(0) = y - 1
For i = 1 To 64
LSB_BinData = AscB(InputB(1, #1))
USB_BinData = AscB(InputB(1, #1))
BB_Data_Hex = USB_BinData * 256 + LSB_BinData
If 32768 < BB_Data_Hex Then BB_Data(i) = BB_Data_Hex - 65536 Else BB_Data(i) = BB_Data_Hex
Next
Worksheets("Sheet1").Range(Cells(y, 1), Cells(y, 65)) = BB_Data
y = y + 1
Loop
Close
End Sub
Sub data_clear()
i = 1
Do While (Worksheets("Sheet1").Cells(i, 1).Value <> "")
Worksheets("Sheet1").Range(Cells(i, 1), Cells(i, 65)).Clear
i = i + 1
Loop
Worksheets("Sheet1").Range(Cells(1, 1), Cells(1, 65)).Font.Size = 9
Worksheets("Sheet1").Range(Cells(1, 1), Cells(1, 65)).HorizontalAlignment = xlCenter
Worksheets("Sheet2").Cells(1, 1).Value = ""
End Sub
---------------------------------------------------------------------------------------------------
これだけです。これを、適当なシート(ここではSheet1)に貼り付けたボタンに関連付けて、そのボタンをワンクリックするだけで、バイナリデータが数値化されてエクセルのセルに代入されるようになりました。なお、私はマウス内部の変数は整数値の変数しか使ってませんので、単純に数値変換だけしてセルに代入していますが、浮動小数点等のデータの場合は、4byte/8byteのバイナリデータから適宜変換して、各セルに代入すれば良いでしょう。
エクセルで数値を見ることが出来るようになれば、自由にグラフ化することが出来ますね。もちろん、エクセルなんか使わずに、各自が使い慣れている言語を使用してツールを作成し、データを可視化すれば良いと思います。
以上、余裕を持って投稿日を決めたはずなのに、結局書き始めたのは20日の東日本地区大会会場という体たらくなので、文章も内容も上手くまとまっていませんが、皆さんのマウス開発に少しでも参考になれば幸いです。質問等があれば、Xでコメントするか、大会の時にお伝え下さい。
明日は、エアプ回路レビュワー@Full "Stuck" Engineerさんの2回目の記事です。「私は天邪鬼です」って、書く内容でしょうか、それとも何か他のコトでしょうか。楽しみにしています。

























最近のコメント