2018年05月02日

Control-Cでプログラムを止める(RPiベアメタルMicroPython)

kbd-intr.png

前回のシリアルポートの割り込み処理ですが、一番やりたかったのが「^Cでプログラムを止める」ことです。これができないと、無限ループになったらリセットするしかありません。

プログラム実行中に^Cでプログラムを止めるには、割り込み処理で入力を受け付け、入力された文字が^Cかどうかチェックします。
^Cだったら実行中のプログラムを中断する処理を走らせます。

この後段の部分はMicroPythonでは
lib/utils/interrupt_char.c
の中の
mp_keyboard_interrupt()
で実装されているので、呼び出すだけです。
特にひっかかるところもなく実装できました。
やはり^Cでプログラムがビシッ!と止まるのは良いですね(笑)。

ちなみに、既存の実装を見比べてみると、ESP8266とESP32はmp_keyboard_interrupt()を呼び出すだけの処理ですが、STM32では1度目は呼び出し、それで処理がされていなければ例外処理を使って直接最上位のスレッドに例外を伝達するという処理になっていました。
これはpendsv.cの中のpendsv_kbd_intr()という関数で実現されています。
また、STM32は複数のUARTを持てるので、REPLに使われるUARTだけで^Cを有効にするといったことも考慮されていました。
ラベル:MicroPython
posted by boochow at 19:15| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

ラズパイのシリアルポート(mini-UART)の割り込み処理を書く

連休で割と時間が取れるので、Raspberry PiベアメタルMicroPythonでこれまで気になっていながら、手をつけていなかった部分をいろいろいじっています。

タイマー割り込みを作ったおかげで、Raspberry Piの割り込み処理が分かってきたので、シリアルポート受信の割り込み処理を行ってみました。
割り込み処理ができると、ループ処理中などREPL以外のタイミングでのキーボード入力をバッファに貯めておくことができるようになります。
実装方法としては、リングバッファを用意して、UARTの割り込みハンドラではリングバッファに追記し、MicroPython側もリングバッファから読み取るようにします。

UARTの割り込みが有効にでき、割り込みハンドラを呼び出せるようになれば、それほど難しくはありません。

と思ったのですが・・・マニュアル(P.12)の記載ミスに泣かされました。
なんと、mini-UARTの割り込みイネーブルレジスタ(IER)のRxとTxの割り込みの記載が逆になっていました。

mini-uart-ier.png


タイトルが「IER」ではなく「IIR」になっているのはまだ笑って許せるレベルです。
上の表では、データ受信時の割り込み許可ビットはbit 1になっていますが、実はbit 0とbit 1は逆で、bit 1は送信バッファが0のときの割り込みを有効にします。
うまく受信できないのに、なぜか割り込みハンドラは連続的に呼ばれるという現象が起きていたのですが、これならそうなって当然です。

手がかりは、いつものdwelch67さんのサンプルの中にありました。

raspberrypi/uart04.c at master ・ dwelch67/raspberrypi

こちらのコードでは、IERの設定が

PUT32(AUX_MU_IER_REG,0x5); //enable rx interrupts

となっています。私が最初に書いたコードは2を設定(bit 1を1にするために)していたので、明らかにおかしいですね。

いろいろ調べた結果、Broadcomのマニュアルに対する正誤表がこちらにありました。
(とても多くの誤りがあることが判ります。)

BCM2835 datasheet errata - eLinux.org

P.12に関する記載を見ると
Bits 3:2 are marked as don't care, but are actually required in order to receive interrupts.
Bits 1:0 are swaped. bit 0 is receive interrupt and bit 1 is transmit.

とあります。

確かに、dwelch67さんのコードでは、bit 0とbit 2を1にしていますが、Bits 3:2の役割が分かりません。
また、試しにbit 0だけを有効にして動かしてみたところ、ちゃんと受信できました。

マニュアルの記載によると、このmini-UARTは昔よく使われていたシリアルポート制御チップの16550をベースにしているようです。
そこで、ネットで16550の情報を調べてみました。

すると、16550のIERは

bit 0: データ受信時に割り込み
bit 1: 送信バッファが空になったとき割り込み
bit 2: ラインステータスレジスタ(LSR)が変化したとき割り込み
bit 3: モデムステータスが変化したとき割り込み

という仕様であることが分かりました。
mini-UARTのIERもこれを踏襲しているものと仮定すると、LSRの変化とは何を示しているのでしょう。
ラインステータスレジスタについては、マニュアルには以下のように記載されています。

mini-uart-lsr.png


bit 0: 受信FIFOバッファにデータあり
bit 1: 受信FIFOバッファが溢れた

ということで、IERのbit 2は「データ無し→あり」になった瞬間、およびバッファがあふれた瞬間に割り込みをかけたい場合に使用すると思われます。
一方、bit 0はとにかく1文字受信したら割り込みがかかるということかと思われます。

IERのbit 2では検出できるがbit 0では検出できないシチュエーションは、あまり無いように思いますので、とりあえずIERはbit 0だけを使うことにしました。


mini-UARTの割り込みはIRQ #29のAUX割り込みになりますが、AUX割り込みはSPI1、SPI2とも共用しています。
そのため、割り込みの原因がこの3つのうちどれだったのかを示すのがAUX_IRQレジスタです。

mini-uart-pending.png


AUXラインに割り込みがあったときは、AUX_IRQレジスタのbit 0が立っているときのみmini-UARTの割り込みハンドラを呼ぶようにしています。
mini-UARTのIIRレジスタでは、さらに割り込み要因が送信受信のどちらだったかが確認できます。

mini-uart-iir.png


bit 0が、mini-UARTで処理が必要な割り込みがあるかどうか、bit 1がmini-UARTの送信バッファが空でないかどうか、bit 2がmini-UARTの受信データがあるかどうかを示すフラグになっています。
ラベル:MicroPython
posted by boochow at 01:00| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年05月01日

ガベージコレクタを追加(RPiベアメタルMicroPython)

mandelbrot-rpi.jpg

Raspberry Pi上のMicroPythonで、フレームバッファにマンデルブロ集合を描かせてみました。
コードはこちらです。

このコードは、以前ESP32でも試したこちらのコードの移植版です。

しかし、半分ほどまで進んだところでMemory Errorで止まってしまいました。
ガベージコレクタを実装していなかったので、ヒープを使い切ると止まってしまうのでした。(実は現状ではヒープには128MBしか割り当てていません)
Raspberry Piはメモリが多いので、あまり気にしていなかったのですが、上記のように長いループを回すのはさすがに無理があったようです。

ガベージコレクタはMicroPythonの処理系に機能は含まれていて呼び出すだけですが、呼ぶ前にレジスタを保存する必要があります。
この部分は、STM32用の実装をほぼ変更無しにそのまま利用できました。

上記のマンデルブロ集合を描かせた後、手動でGCさせてみたところ、以下のようにかなりメモリを使っていました。
(これ以前に、おそらく実行中に1度はGCしているはずです。)

>>> import gc
>>> gc.mem_free()
12535552
>>> gc.mem_alloc()
119617360
>>> gc.collect()
>>> gc.mem_free()
132151200
>>> gc.mem_alloc()
1712
>>>


ちなみにMicroPythonのガベージコレクタは、マーク・アンド・スイープ方式です。
他のオブジェクトから参照されていないオブジェクトは開放されます。
参照関係の大元はroot pointerで、root pointerから辿れないオブジェクトはGCの対象になります。

Memory allocation and deallocation - MicroPython Forum

このあたりのことは、以前USBホストコントローラを実装するときにちょっと書きましたが、Cで動的に確保したメモリをGCの対象から外すには、MICROPY_PORT_ROOT_POINTERSにポインタを追加した上で、ポインタの参照には常にMP_STATE_PORT()マクロで囲う必要があります。
ラベル:MicroPython
posted by boochow at 01:52| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年04月29日

Timerの精度(RPiベアメタルMicroPython)

昨日のTimerクラスの割り込み処理を使ったLチカはこんな感じになります。
Raspberry Pi Zero Wなので、GPIO47がボード上のLEDです。


このタイマ割り込みの精度はどの程度になるのか、測定してみました。
1000マイクロ秒(1msec)周期の割り込みをかけて、コールバック関数の中でそのときの時刻を測定します。
割り込み処理が実行される時刻が、割り込みが起こった時刻からどれくらい遅れるか統計を取ってみます。



Timer.compare_register()は、「次に割り込みが起きるタイミング」を示しています。System Timerでは、フリーランニングする1MHzクロックのカウンタ(utime.ticks_usec()を実現している64bitカウンタ)の下位32bitが、タイマのコンペアレジスタと一致したときに割り込みが起こります。

割り込みが起きるタイミング自体は、utime.ticks_usec()の計測値で見ればきっかり1000マイクロ秒間隔になります。
しかし、コールバック関数は割り込みハンドラ内ではなく、ハンドラの処理が終わった後に呼ばれますので、割り込みから割り込みハンドラの実行までにはオーバーヘッドがあり、その分遅延が出ます。

この遅延の量を、logへ蓄積していきます。
遅延の値は、「現在時刻 - (コンペアレジスタの値 - 1000)」になります。
「-1000」が付いているのは、Pythonで書いた割り込みハンドラが呼ばれたタイミングでは、既に割り込みが起きてコンペアレジスタの値が更新され、次回割り込みが起こるべきタイミングを示す値になっているからです。

試しに1000回測定してみた結果は以下のようになりました。

timer-accuracy.png

47	317
48 260
49 187
50 143
51 56
52 38


割り込みそのものからの遅延は50マイクロ秒前後です。
割り込みの間隔そのものは概ね5マイクロ秒以内の変動で納まっています。
今回は割り込みタスク以外にはwhileループしか走っていませんが、もっと重い処理(実行時間が長くかかるCの関数の実行)をさせればもちろん悪化する可能性はあります。
上の例はUSBキーボードのサポートをオフにして測定したものですが、USBキーボードを接続した状態だと以下のように少し変動が増えます。

timer-accuracy-with-usb.png

48	248
49 217
50 160
51 108
52 91
53 37
54 37
55 37
56 36
57 30


遅延の最大値が5マイクロ秒ほど増えています。
現在の実装では、割り込みハンドラの実行は特に優先されないので、定期実行するタスクが増えるほど、変動が増えていくと思われます。
ラベル:MicroPython
posted by boochow at 12:40| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

なぜRaspberry PiにMicroPython?

Raspberry Piでは、Raspbian上で普通のPythonを使ってI/Oを制御することができます。
なのに敢えて、ベアメタルでMicroPythonを実装することにどんな意味があるのか、最近思っていることをちょっと書いておきます。

まず、起動時間短縮、これはあまり意味がないと思います。
Raspbianは確かに起動に時間がかかりますが、Buildrootなどで軽量なLinuxシステムを作れば、5秒以内に起動するシステムを作ることは可能です。

ハードを直接叩くような実装をすること自体が楽しい、これはまあ当たっています。しかし、これはMicroPythonではなくベアメタルプログラミングの楽しさです。
その結果できあがったものを使う意味があるかどうかは別の話です。

同様の議論がMicroPythonのフォーラムでも行われていました。

Raspberry Pi Zero - MicroPython Forum

最近感じているのは、Raspbian上でPythonを使用するよりも、MicroPythonを使用するほうがよりベアメタルプログラミング的であるということです。
MicroPythonの面白さは、ハードウェアを抽象化する層がMicroPythonそのものしかないところにあります。
実際のベアメタルプログラミングにかなり近い形でハードウェアに触れることができます。

Raspbian + Pythonでは、Linuxがハードウェアを抽象化しているため、ハードウェアからちょっと距離があります。
例えば、UNIXなのでデバイスがデバイスファイルとして見えていますし、スーパーユーザ権限無しにできることにも限りがあります。
もちろん、その結果として楽ができるわけですが。

MicroPythonでは、ハードウェアを抽象化するレイヤはMicroPython以外にはありません。プラットフォームによってはSDKで隠蔽される部分がある程度です。基本的に、ハードウェアはMicroPythonのモジュールないしクラスで抽象化されます。

その結果、MicroPythonでのプログラミングは、Cでハードウェアを叩くベアメタルプログラミングに近くなります。ボードに接続された周辺デバイスを直接制御でき、その反応も直接返って来るので、デバイスの生の挙動を観察することができます。

そのようなベアメタルプログラミングの環境として、MicroPythonは非常に優れています。デバイスは実際に動かして調べないと挙動が把握できないことも多いので、REPLで試行錯誤的にハードウェアを叩けるのは、コンパイルとダウンロードの手間が省けて大変便利です。関数定義もその場で追加修正できるので、デバッグ効率はC言語でのベアメタルプログラミングとは雲泥の差があります。

つまり「Raspberry Piに様々なデバイスをつないで制御するには、MicroPython on ベアメタルRaspberry Piは価値がある」ということになります。


逆に言うと、MicroPythonの良さを活かすためには、ハードウェアを過度に抽象化しないことが重要になります。
前回のTimerクラスの実装では、この抽象化をするかどうか、ちょっと悩みました。

Raspberry Piでは、System Timerは4つ(0, 1, 2, 3)ありますが、2つ(0と2)は使用済みです。
MicroPythonからは、実質的にはTimer 1と3しか使えないことになります。

そのため、Timerクラスの設計として

・利用可能な2つだけをサポートするか、4つともサポートした上で0と2は使うな、とするか
・そもそもTimerが2つしか使えないのは不便なので、TimerクラスではVirtual Timerだけにして、Virtual Timerの実現にハードウェアタイマを1つだけ使ってはどうか

といったことも考えました。

しかし結局、TimerクラスではRaspberry PiのSystem Timerの仕様をそのまま素直に反映させることにしました。
その結果、Timerクラスでは4つのタイマが指定できますが、0と2は実質的に使えないという(若干不便な)仕様になります。
ですが、これがRaspberry Piの仕様であり、それを隠すような実装をMicroPython側で行うことは、かえってMicroPythonの魅力を削いでしまうのではないかと思いました。


というわけで、現在思っていることをまとめると、以下のようになります。

・Raspbian + Pythonでは、Raspberry Piにつないだハードウェアを制御する時にLinuxが邪魔になる場合もある
・MicroPythonは、REPLの中でハードウェアを直接制御・観察できるところにメリットがある
・MicroPythonのメリットを享受するには、ハードウェアをMicroPythonが過度に抽象化しないことが重要

従って、今後も基本的にはまずBCM2835のリファレンスマニュアルにある内容をそのまま実装した上で、ハードウェアに無い機能(Timerクラスで言えばコールバックやカウンタ)は適宜、利便性のために追加するという方針で行こうと思っています。
ラベル:MicroPython
posted by boochow at 08:36| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする
人気記事