2018年04月23日

MicroPythonにTimerクラスを追加する(RPiベアメタル)

timerclass-sample.png

先日の実験でRaspberry Piのタイマ割込処理がだいたい分かったので、MicroPythonにこのタイマ割り込みを実装してみました。

MicroPythonにはTimerクラスがあり、タイマーを使ってコールバック関数を呼ぶことができます。
タイマー起動後は、コールバック関数は自動で呼ばれますので、マルチタスクのようなことができます。
例えば、一定周期でLEDを点滅させるという処理をしながら、シリアルポートでデータを読み書きできます。
また、一定時間後に一度だけ実行するONE_SHOTモードもあります。

以下は今回作成したTimerクラスを使った簡単なサンプルです。


System Timerには4つのタイマーがありますが、今回はとりあえずタイマー3だけを実装しています。
inc_ctr()がコールバックさせる関数で、引数tにはタイマーオブジェクトが入ります。
この関数はグローバル変数counterを1ずつ増やします。

タイマーをinitメソッドで初期化するとタイマーが動作を開始します。
今回の実装ではクロックを1MHzとしているので、periodの単位は1マイクロ秒です。
period=1000000だと、1,000,000マイクロ秒つまり1秒周期でinc_ctr()が繰り返し呼ばれます。

一方、直後のwhileループはcounterが3以上になるまでループします。
ループしている間も、inc_ctr()は1秒おきに呼ばれていますので、3秒後にこのループをから抜けます。
すると今度は、ワンショットモードでタイマーを起動していますので、1秒後に"done!"とプリントアウトします。


Timerクラスはハードウェアタイマに強く依存しているため、同時使用できるタイマーの個数などの詳細仕様はCPU毎に異なっています。
Raspberry Piでは、以前解説したように、使用できるタイマはARM Timerが一つと、System Timerが2つです。
今回は、System Timerを1つだけ使い、インスタンスも1つしか持てないTimerクラスを実装してみました。

MicroPythonへのクラスの追加については以前も書きましたので省略します。

今回のポイントは割り込みの処理です。
まず、MicroPythonのメイン関数に割り込みベクタの設定とIRQ有効化の処理を追加します。
そして、IRQの割り込みハンドラの中でTimerクラスに関する処理を行います。
割り込みハンドラのコードは以下のようにしました。


割り込みの原因がシステムタイマ3だった場合、timer_root.callbackに保存されているコールバック関数を実行します。
ただし、割り込みハンドラの中で実行するのではなく、この関数を実行するというタスクを実行待ちキューへ追加します。
この処理はmp_sched_schedule()で行います。

コールバック関数が実際に実行されるのは、MicroPythonのメインの実行ループの中で関数mp_handle_pending()が呼ばれたときです。
つまり、適切なタイミングでmp_handle_pending()を呼んでやる必要があります。

MicroPythonのREPLが動いているときは、シリアルポートの入力待ちをしている間、ずっとIdleループになっています。
入力待ちの間もコールバック関数が実行されるように、ループの中からもmp_handle_pending()を呼ぶように処理を追加しました。
ちなみに他のプラットフォームでのMicroPythonの実装を見ると、ループの中で行うべき諸々の処理をまとめてmpconfigport.hの中でMICROPY_EVENT_POLL_HOOK というマクロを定義しています。
そして、上記のシリアルポート入力や、delay_ms()など、様々なループからこのマクロを呼び出すようになっています。

mp_sched_schedule's scheduled callbacks don't run on empty REPL lines ・ Issue #3273 ・ micropython/micropython


このような実装なので、タイマ割り込みのタイミングはあまり正確ではありません。
ESP8266の実装に関して、D.P.Georgeは「100 usec以下の精度を求めるならC、アセンブラ、あるいはViperを使うべき」と書いています。
ちなみにViperとは、組み込み向けリアルタイムPythonだそうで、現在はZerynthiに改名されているようです。

esp8266: can't time pulses with good accuracy ・ Issue #2052 ・ micropython/micropython


なお、今回実装にあたってSTM32、ESP8266、ESP32のTimerクラスの実装も調べました。
Timerクラスはどうしてもハードウェアタイマに依存しますので、プラットフォームごとの仕様の違いがかなりありました。

STM32シリーズは制御用マイコンなので、ハードウェアタイマーの個数も機能も多く、定期実行やワンショットなどの他に、PWMにも使えます。(AVRと似ています。)
MicroPythonの実装も、ハードウェアタイマーのパラメータをいろいろ設定できますし、コールバック関数を単に待ち行列に追加するのでなく、割り込みハンドラの内部で実行できるようになっています。
ただ、割り込みハンドラ内でMicroPythonを実行することになりますから、コンテキストの保存には十分配慮する必要があると思われます。
実装を見ると、nlr(non-local return)関連の関数が使われています。
これはUNIX等におけるsetjmp/longjmpのプラットフォームに特化した実装のようです。

class Timer – control internal timers − MicroPython 1.9.3 documentation

対照的に、ESP8266はハードウェアタイマのスペックが比較的貧弱で、EspressifのSDKにも依存するためか、機能はかなり削られています。
また、バーチャルタイマーという概念が導入されています。
これはESP8266でリアルタイムOSを動作させ、リアルタイムOS上のタスクとしてタイマのカウントをソフトウェア的に行うもののようです。
周期は1msec単位で指定できますが、精度は粗く、周期があまり一定していないという評価がフォーラムでもいくつか出ていました。
仮に精度が1msecあったとしても、60Hzの信号でも周期が2~4%ずれることになりますので、用途によってはちょっと微妙な場合もありそうです。

class Timer – control hardware timers − MicroPython 1.9.3 documentation

ESP32はハードウェアタイマが4つありますが、周期の指定は同様に1msec単位のようです。

ESP32 MicroPython: Timer interrupts | techtutorialsx

また、delay_ms()を実行中にmp_handle_pending()が呼ばれるのは10msecに一度であり、これはESP32で使用しているFreeRTOSのタスクスイッチの単位が10msecであるためだそうです。
delay_ms()でなくforループの場合は、ループ一周ごとにpendingされているタスクを実行できるようです。

esp32: time.sleep_us() doesn't check for pending events ・ Issue #3493 ・ micropython/micropython

posted by boochow at 01:21| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年04月20日

Raspberry PiにおけるBASIC IRQペンディングレジスタの構成

Raspberry Piの割り込みでは、割り込みハンドラの中でペンディングレジスタを調べて適切な処理をする必要があります。
前回書いたように、ペンディングレジスタは各ビットが「未処理の割り込み要因」を表しています。
特に、BASICペンディングレジスタは主要な割り込み要因を集めてありますので、ここでその内容をまとめておきます。

CPUから見ると、割り込みというのは入力端子の一種です。リセットボタンがつながるリセット端子がその代表的なものです。
Raspberry Piでは32ビットのディングレジスタが3つありますが、各レジスタの1ビットが、割り込み信号線1本のHigh/Lowの状態を反映しています。

下の図は、左側が割り込み信号線で、それぞれの信号線が異なる割り込み要因を表しています。
マニュアル p110)

pending-regs.png


図は右側の3つのレジスタと左側の割り込み信号線の関係を表しています。
上から32本(IRQ32-63)はGPUからの割り込み信号で、その状態はIRQペンディングレジスタ2(図ではIRQ1となっていますが、マニュアルのこれ以降のページではIRQ2となっています)に反映されます。
この中の何本か(A few selected GPU IRQs)は、同時にBASICペンディングレジスタにも入力されていますので、どちらのレジスタからも状態を確認できます。

IRQ32-63のうち、BASICペンディングレジスタに入力されていない信号全てのORを取ったものが、BASICペンディングレジスタに入力されています。
これにより、BASICペンディングレジスタでは「IRQペンディングレジスタ2のどれかに割り込みがあったこと」を知ることができます。

次の32本(IRQ0-31)も同様に、Video Coreからの割り込み信号で、その状態はIRQペンディングレジスタ1に反映されます。
またこれらの信号全てのORを取ったものが、BASICペンディングレジスタに入力されています。


そしてBASICペンディングレジスタですが、マニュアルによると

「A few selected GPU IRQs」:10本(bit10-19)
IRQペンディングレジスタ2の全信号のOR(bit9)
IRQペンディングレジスタ1の全信号のOR(bit8)
ARM Timerなど8つの割り込み信号(bit0-7)

が入力信号となっています。

前回紹介したLinuxカーネルのソースコードを見ると、「A few selected GPU IRQs」の具体的な中身がわかります。
また、実際には10本ではなく、11本(bit10-20)がGPU IRQの信号となっているようです。
マニュアル(p114)の情報と、Linuxのカーネルソースコードの情報を合わせると、BASICペンディングレジスタの各ビットの意味は以下のようになっています(赤字はマニュアルには無い情報です)。

basic-pending-reg.png


I2CやSPIなど主要な入出力関係の割り込みは、このレジスタだけで把握できそうです。
なおbit 20の「ARASANSDIO」は、Arasan社製SDIOコントローラということで、マイクロSDカードで使用していないもう一つのSDIOインタフェース(GPIO22-27)からの割り込みです。
posted by boochow at 01:41| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年04月19日

Raspberry Piでのシステムタイマ割り込みを試してみた

timer-irq2.png

Raspberry Piには「System Timer」と「ARM Timer」の2系統のタイマがあります。
前回は、このうちARM Timerのほうを使いましたが、今回は、同様にSystem Timerでの割り込みを使い、周期の異なる2つのタイマを並列に動かしてみました。
コードは以下に置いてあります。

bare_matal_rpi_zero/timer-irq2 at master ・ boochow/bare_matal_rpi_zero

System Timerの解説はマニュアルの172ページから記載があります。
ARM Timerは1つのタイマしか使えませんでしたが、System Timerは4つのタイマを使うことができます。

その代わり機能的にはARM Timerよりもさらにシンプルになっています。
要約すると

・値が1ずつ増えていく64bit カウンタが1つある。クロックは1MHz固定
・32bitのタイマが4つあり、上記のカウンタの下位32bitと値が一致したら割り込みがかかる

これだけです。
ちなみに、この64bitカウンタはベアメタルの最初のLチカのときに、時間計測用に使っています。

32bitのタイマは、それ自体の値は時間が経過しても変化しません。
タイマというより目覚まし時計に似ていて、設定した時刻に割り込みをかけられるアラームが4つある、みたいな感じです。

周期的に割り込みをかけたい場合は、割り込みがかかったときに、次に割り込みをかけたいタイミングを新たに設定しなおす必要があります。逆に、使い方によってはGPIOを複雑なリズムでオン・オフできますので、PWM的なこともできそうではあります。

使い方も単にレジスタにアラーム時刻を設定するだけです。
タイマ0〜3の4つのタイマがありますが、0と2はGPUで使用しているそうです。
そのため、実際に使えるのはタイマ1と3の二つです。


今回は、タイマ1を960000カウント、タイマ3を720000カウントの周期にしてみました。
前回同様、スタティック変数の値を上記の周期でインクリメントします。
割り込みハンドラは以下のようになります。



IRQはいろいろな要因で起こりますので、割り込みハンドラ内でどんな原因による割り込みなのかを調べています。
このために使うのが、割り込み制御レジスタの一つで割り込み要因を表す「ペンディングレジスタ」です。
このレジスタは「発生したが処理されていない割り込み」のフラグが立っています。

前回も書いたように、割り込み制御レジスタは3つ(Basic、IRQ1、IRQ2)あります。
どのレジスタのどのビットが、何の割り込み要因に対応するのかはマニュアルの113ページに一覧があります。
しかし、この表は不完全でシステムタイマ割り込みについての記載も抜けています。

irq-table1.png


ビットと割り込み要因の関係の完全なリストは、Linuxカーネルのソースコードにあります。

linux/platform.h at rpi-3.6.y ・ raspberrypi/linux

システムタイマ0〜3は、割り込み制御レジスタのbit0〜3に割り当てられていることが分かります。
割り込みを有効にする、割り込みハンドラの中でペンディングレジスタを確認する、等はこの情報に基づいて行います。

注意点としては、「同時に複数のIRQが起こった場合」、ペンディングレジスタにはそのうち1つの割り込み要因しか反映されないようです。
例えば、タイマー1を周期20、タイマー3を周期30で、初期値0から動かしたとします。
割り込みハンドラ内では、割り込みがかかる都度、タイマーの値を更新(+20または+30する)します。
すると、タイマー1の3回目およびタイマー3の2回目で、

タイマー1: 0 20 40 60
タイマー3: 0 30 60

どちらのタイマーも最小公倍数である60になってしまいます。
このとき、割り込みがかかるとペンディングレジスタには片方(私が試した限りでは、常にタイマー1)だけがフラグが立ちます。

従って、このフラグをチェックしてタイマーの値を更新していると、タイマー3の値が更新されなくなってしまいます。
初期値を少しだけずらせば、タイマーの値が一致する可能性を排除できます。
例えば、初期値を5にしておけば、

タイマー1: 0 20 40 60
タイマー3: 5 35 65

というようになり、値がぴったり一致することはありません。
ただし、初期値の差があまりに小さいと、割り込み処理が完了する前に次の割り込みが起こってしまうケースもあるかもしれません。
posted by boochow at 01:05| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年04月17日

ベアメタルRaspberry Piの例外処理を書いてみた

timer-irq.png

前回調べた情報に基づいて、ラズパイのベアメタルでの割り込み処理を試してみました。
コードは以下に置いてあります。

bare_matal_rpi_zero/timer-irq at master ・ boochow/bare_matal_rpi_zero

行った内容自体は

・タイマで0.5秒ごとに割り込みをかける
・グローバル変数(整数)を割り込みハンドラから変更
・メインルーチンは無限ループでグローバル変数を監視。値が変化したらシリアルポートに出力

というシンプルなものです。

割り込みベクタは0番地ではなく、スタティック変数を指定することにしました。
データ構造は以下のようにしています。



割り込みベクタの構造は前回紹介したインターフェース2017年2月号や、dwelch67さんのサンプルと同じで、ジャンプ命令の直後にアドレステーブルを置いています。
アドレステーブルは実際には関数のポインタ(exception_hander_t型として定義)です。

重要なのは
__attribute__((aligned(32)))
の部分で、割り込みベクタは32バイト境界に配置する必要があります。
最初、4バイト境界にしていて、想定と違う割り込みハンドラが呼ばれてハマリました。
考えてみれば、割り込みベクタは32バイトでセットなので、32バイト境界でも不思議は無いですね。

gccでは割り込みハンドラを書くための__attribute__が使えますので、割り込みハンドラはCのみで書いています。
ただ、なぜか__attribute__((interrupt("FIQ")))で関数を書くとコンパイラの内部エラーになったので、FIQのハンドラはFIQではなくIRQを使っています。
ちなみにエラーはこんな内容でした。
main.c: In function 'fiq_handler':
main.c:189:1: error: insn does not satisfy its constraints:
}
^
(insn/f 13 12 14 (set (reg/f:SI 13 sp)
(plus:SI (reg/f:SI 11 fp)
(const_int 4 [0x4]))) main.c:189 4 {*arm_addsi3}
(expr_list:REG_CFA_ADJUST_CFA (set (reg/f:SI 13 sp)
(plus:SI (reg/f:SI 11 fp)
(const_int 4 [0x4])))
(nil)))
main.c:189:1: internal compiler error: in extract_constrain_insn, at recog.c:2246
Please submit a full bug report,
with preprocessed source if appropriate.
See for instructions.


スタティック変数として定義した割り込みベクタを使用するには、CP15のVBARレジスタに設定しますが、これは前回のmrvnさんのset_vbar()をそのまま借用しています。

タイマの設定についてはdwelch67さんのものをほぼそのまま使用しています。
このタイマの解説は、毎度おなじみのマニュアル「BCM2835 ARM Peripherals」の196ページに掲載されています。簡単に要約すると、

・値が減算されていくカウンタである
・値がゼロになったとき、割り込みをかけることができる
・そのあと、また初期値から減算していく

というシンプルなタイマです。

クロックソースは250MHzですが、それをプリディバイダで分周することができます。
今回はプリディバイダを0xF9(=249)にしているので、1MHzクロックになります。
カウンタの初期値は(500,000 - 1)にしているので、50万カウント(=0.5秒)ごとに割り込みがかかります。
割り込み処理後、処理が終わったことを示すために、ARM_TIMER_CLIをクリアする必要があります。

割り込みを有効にするのは、ちょっと面倒です。
CPUのレジスタとメモリマップドレジスタの両方を設定してやる必要があります。

CPUでは、CPSRレジスタのbit 7がIRQ、bit 6がFIQのイネーブル・ビットになります。
これが割り込み全体の有効・無効の制御になります。

Raspberry Piでは、これに加えて割り込み制御レジスタで割り込み要因(タイマー割り込みを許可するか等)ごとの有効・無効設定を行う必要があります。
マニュアルでは112ページあたりから解説されています。

割り込み制御レジスタは6つあります。
割り込みは全てが同等なのではなく、8つの基本割り込みと、それ以外の割り込みに分かれます。
基本割り込み用レジスタが1つ、それ以外の割り込み用レジスタが2つ割り振られています。
さらに、それぞれのレジスタは「有効化」「無効化」で別のレジスタになっています。
従って、割り込み制御レジスタは計6個となります。

このほかに、割り込み要因を表すためのレジスタがやはり3つと、FIQ(高速割り込み)を使う割り込み要因を指定するためのレジスタが一つあります。
今回はタイマ割り込みを有効にするだけなので、使うレジスタは1つだけ(IRQ_ENABLE_BASIC)です。


最後に、割り込みがかかったときにはCPUは「割り込みモード」になりますので、割り込みモードのためのスタックを設定しておく必要があります。
これは初期化コード(ソースコード中ではInit_Machine()という関数)で行っています。
ブートローダがプログラムを読み込むアドレスは0x8000ですので、0x0000〜0x8000までの32KBが空いています。
この空間をIRQモードとFIQモードに16KBずつ割り当てています。(ここの部分はdwelch67さんのコードを借用しました。)
posted by boochow at 00:58| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年04月14日

ベアメタルRaspberry Piの例外処理の書き方

前回はRPiベアメタル版MicroPythonにPinクラスを追加しましたが、実装したのは入出力ができるだけの最低限の機能でした。
今後、GPIO周りの様々な機能を実装していくには、まず先に割り込みを実装しておくほうが良さそうです。
データ転送や並列処理は割り込みがあると無いとでは大違いなので、割り込み無しで実装しても、割り込み機能を追加したら結局大幅に書き直しになりそうだからです。

というわけで、インターフェース誌2017年2月号の割り込みの解説を読んでみました。

昔のイメージだと、割り込みというのは以下のような感じでした。

・アドレス空間の0番地または最高番地(最近は少ないですが、昔だと6502はそうでした)に割り込みベクタがある
・割り込みベクタはジャンプ先アドレスの一覧(ジャンプテーブル)になっており、割り込み種別に応じてジャンプテーブルのアドレスがプログラムカウンタ(PC)に読み込まれる
・レジスタの設定で割り込みの禁止・許可ができる

上記のインターフェース誌の記事を読んだ結果、以下のようなことが分かりました。

・ARMの割り込みベクタは0番地から置くか、あるいは任意のアドレスに置くことが可能(CP15コプロセッサのレジスタに設定)。サイズは32bit×8個
・割り込みベクタはアドレスではなく命令を書く(分岐命令など)。ジャンプ先等を表すオペランドは最大24bitという制限あり
・ARMで割り込みを使うには、CPUの動作モードを割り込みモード/高速割り込みモード/ユーザモードのいずれかにする必要がある(起動時はスーパーバイザモード)
・動作モードが変わるとスタックポインタも切り替わる
・Raspberry PiではARMの標準とは異なる割り込み制御が行われている(GPUと共有されているなど)
・割り込み要因を正確に知るには、割り込みハンドラの中でさらに関連レジスタの値を調べる必要がある

GitHubの公開されているコードで、Raspberry Piのベアメタルで割り込みを使っている例もいくつか見つかりました。
ベアメタルではおなじみのdwelch67さんのblinker05は、タイマ割り込みでLEDを点滅させます。

raspberrypi/blinker05 at master ・ dwelch67/raspberrypi

この例では、割り込みハンドラ(void c_irq_handler ( void ))がGPIOをオン・オフしています。
割り込みハンドラの設定はvectors.sの中で行われていますが、ここでちょっとトリッキーなことをしています。

ベアメタルのプログラムはブートローダによって0x8000へロードされますが、その0x8000に割り込みベクタが置かれています。
割り込みベクタは、上で書いたように、ジャンプ先アドレスではなくジャンプ命令が書かれています。
割り込みベクタの直後に32bit×8個のアドレステーブルがあり、ジャンプ命令はこのアドレステーブルに記載されたアドレスへジャンプするように書かれています。

プログラムが起動される0x8000に書かれている命令は、割り込みベクタの先頭=リセットベクタであり、その内容は
ldr pc,reset_handler
となっています。reset_handlerはリセットハンドラのアドレスです。
つまり、ブート直後に必ずリセットハンドラへ飛ぶようになっています。

リセットハンドラの先頭は
reset:
mov r0,#0x8000
mov r1,#0x0000
ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
となっています。このコードは0x8000からの64バイト(割り込みベクタとジャンプテーブル)を0x0000へコピーしています。

これによって、割り込みベクタが初期化されますが、リセット処理で実行される内容は0x8000からブートされたときとリセットで0x0000から実行されたときで、全く同じになります。

ただ、このコードはr2を壊してしまっているので、このままだと以前紹介したARM Boot Tagsは使えません。

ジャンプテーブルのIRQハンドラは、以下のコードを指しており、この中からCの関数(c_irq_handler)が呼ばれるようになっています。
irq:
push {r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
bl c_irq_handler
pop {r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
subs pc,lr,#4
最後のPC←LR-4という処理ですが、割り込みが起こったときには、CPUは次に実行する命令の処理に取り掛かっています。割り込み処理後に実行を再開するときには、処理途中だった命令を再度やり直す必要がありますので、プログラムカウンタを1命令だけ巻き戻しています。

ちなみに、Raspberry PiではタイマーがCPU側に1つとGPU側に4つの2系統存在し、このサンプルではCPU側のタイマーを使用しています。


もう一つの例としてmrvnさんのものを見ていきます。「006-you-are-exceptional」で未定義命令などの例外処理を行っています。

mrvn/RaspberryPi-baremetal: Baremetal exampels for Raspberry Pis

この例では、割り込みベクタを0番地ではなく、別のアドレスに設定しています。この場合は、割り込みベクタを指定するためにCP15コプロセッサのVBAR (Vector Base Address Register)を使います。
その処理はexceptions.cの中の以下のコードで行われています。
void set_vbar(uint32_t *base) {
asm volatile ("mcr p15, 0, %[base], c12, c0, 0"
:: [base] "r" (base));
}

void exceptions_init(void) {
set_vbar(exception_vector);
}


このexception_vectorは、entry.Sの中でブランチ命令として以下のように定義されています。
exception_vector:
b stub_reset
b stub_undef
b stub_svc
b stub_prefetch_abort
b stub_data_abort
b stub_hypervisor_trap
b stub_irq
b stub_fiq

この実装ではジャンプテーブルは無く、直接stub_xxxというアドレスへ分岐するようになっています。
この分岐先からさらに、Cで書かれた割り込みハンドラ「handler_xxx」を呼び出すようになっています。

handlerを呼び出すためのstubのコードはentry.Sの中でマクロとして記述されています。
.macro  make, num, offset, name
.global stub_\name
stub_\name:
saveall \num, \offset
bl handler_\name
restoreall
.endm

// num, offset, name
make 0, 0, reset
make 1, 4, undef
make 2, 0, svc
make 3, 4, prefetch_abort
make 4, 8, data_abort
make 5, 0, hypervisor_trap
make 6, 4, irq
make 7, 4, fiq

これによって、例えば
    .global stub_irq
stub_irq:
saveall 6, 4
bl handler_irq
restoreall
といったコードが生成されます。
saveallとrestoreallもマクロで、numは割り込み要因、offsetは戻りアドレスの補正値です。
posted by boochow at 21:58| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする
人気記事