2018年04月09日

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

pinclass.png

少しずつ進めているRaspberry Pi用ベアメタルMicroPythonですが、今日は基本中の基本のPinクラスを実装し、Lチカができるようになりました。
I2CやSPIを実装するにも、まずどのPinを使うか指定する必要がありますので、とにかくPinクラスが使えるようにしなければなりません。

クラスの定義はモジュールの定義と似ています。
モジュールの定義方法は、下記の記事で以前紹介した通りです。

MicroPythonからラズパイのフレームバッファへ描画する: 楽しくやろう。

これと対比してクラスの定義の記述の仕方をまとめておきます。

●クラスの追加
・新しいクラスが所属するモジュールのためのCソースファイルを(無ければ)作り、Makefileに追加する。
・Cソースの中で、mp_obj_type_t型(構造体)の定数を宣言する。これがクラスの実体となる。メンバ変数はいろいろあるが(py/obj.hで定義されている)、base(この定数自体の型)、name(クラス名)、print(REPLでオブジェクトを文字列として出力する)、make_new(新しいインスタンスを作る)が必須。locals_dict(インスタンスが共有するクラス変数やインスタンスメソッドのリスト)もほぼ必須。baseは必ず&mp_type_typeとする。(mp_type_typeはobjtype.cの中で定義されている定数で、型を作るクラス、つまりメタクラス。)
・このクラスが所属するモジュールの定義の中で、そのモジュール内のグローバルオブジェクトのリストであるmp_rom_map_elem_t型の定数に
{ MP_ROM_QSTR(MP_QSTR_クラス名), MP_ROM_PTR(&上記の定数) }
を追加する。

●クラス内グローバルオブジェクトの追加
・mp_rom_map_elem_t型の定数配列を宣言する。これがクラス内グローバルオブジェクトのリストとなる。
・MP_DEFINE_CONST_DICT(定数名,上記の定数配列の名前)でmp_obj_dict_t型の定数を宣言する。
・mp_obj_dict_t型の定数を、クラスを表すmp_obj_type_t型の定数(上記で定義済み)のlocals_dictからポイントする。
・グローバルオブジェクトのリストへ、
{ MP_ROM_QSTR(MP_QSTR_オブジェクト名), マクロ(オブジェクトの実体) }
という形式でオブジェクトを追加する。
「マクロ」はオブジェクトの実体がintならMP_ROM_INT(整数値)、それ以外ならMP_ROM_PTR(ポインタ)。

●インスタンスの定義とインスタンスメソッドの追加
・インスタンスを表す型を構造体として定義する。このとき、最初のメンバ変数は必ず
mp_obj_base_t base;
とし、その値は常にクラスを表すmp_obj_type_t型の定数とする。
・コンストラクタは、クラス・positional argsの個数・keyword argsの個数・引数の配列を受け取り、インスタンスのためのメモリを確保し、初期化してmp_obj_t型を返す関数として定義する。
その関数へのポインタを、mp_obj_type_t型のメンバ変数make_newへ代入する。
メモリ確保のための手段はpy/misc.hに各種定義されている。mallocは使えないことに注意。最もシンプルな方法としては、インスタンスを表す構造体の型をmyobj_tとすると、m_new_obj(myobj_t)でインスタンスオブジェクトが生成できる。
・インスタンスメソッドの実体は、mp_obj_t型を返す関数として定義する。
・マクロMP_DEFINE_CONST_FUN_OBJ_XXで関数をMicroPythonのオブジェクトとして定義する。XXの部分は関数の引数の個数により異なる。(0〜3:0個〜3個、VAR:可変長、VR_BETWEEN:最小個数と最大個数が決まっている、KW:キーワード=値の形式を用いる)
・グローバルオブジェクトのリストmp_rom_map_elem_t型配列へ
 { MP_ROM_QSTR(MP_QSTR_関数名), MP_ROM_PTR(&関数名オブジェクト名) }
を追加する

こんな感じです。インスタンスメソッドの追加はモジュールへの関数追加と全く同じですね。
以下のリンクも参照してください。

Adding a Module − MicroPython Development Documentation 1.0 documentation

今回はPinクラスを定義することが主眼ですが、MicroPythonのお作法では、Pin、I2C、SPIといったハードウェア寄りのクラスはmachineモジュールが持つようになっていますので、machineモジュールも定義しています。
追加したコードはこんな感じです。

added Pin class (machine.Pin), init(), value(), constants(IN, OUT, PU… ・ boochow/micropython-raspberrypi@e744494

Pinのインスタンスの実体は構造体(machine_pin_obj_t型として定義)ですが、インスタンスと物理的なGPIOとが1対1に対応しますので、無限にインスタンスを作ることはできません。
その代わり、スタティック変数として構造体のGPIOのピン個数(54個)分の配列を用意し、コンストラクタはPin番号に応じてこの配列のいずれかを返すようになっています。

他に初期化(init)と値の読み書き(value)の2つのインスタンスメソッドを定義しています。
これらのメソッドはGPIOレジスタにアクセスして処理を行います。
レジスタの操作方法については、以下の記事で紹介済みですので省略します。

Raspberry Pi Zero WでベアメタルLチカ: 楽しくやろう。
Raspberry Pi Zero + ベアメタルMicroPythonでのLチカ: 楽しくやろう。

上記で触れていない、入力ピンとして使用する場合のプルアップ・プルダウンについては以下のリンクが参考になりました。

bare metal - Modifying GPIO memory for one pin turns multiple on - Raspberry Pi Stack Exchange

現状では、Pi Zero WのLEDの点灯・消灯がGPIO47への出力(0で点灯)と入力(LEDを通じてVCCに接続されているので1になる)ができることが確認できています。

Raspberry PiのGPIOには、シンプルな入出力のほかに、SPIやI2Cとして使うためのAlternate Functionの指定や、割り込み、クロック出力などの機能があります。
いずれ、これらの機能のサポートをPinクラスに追加していきたいと思います。
ラベル:MicroPython
posted by boochow at 00:08| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年03月29日

Raspberry Piベアメタルプログラムへのパラメータの渡し方

Raspberry PiのベアメタルプログラムをQEMUで動作させる場合には、シリアルポートはMini-UARTではなく通常のUARTを使う必要があります。
これについて以前書いたときは、コンパイル時に#ifでどちらのUARTを使うかを指定するようにしていました。

これを、コードは同一にして、main関数に与えるパラメータで分岐させられないか? と考えたのが今回の記事です。
結果的には、全く同じバイナリだとちょっと難しそう・・・ということなのですが、調査・実験した結果をまとめておきます。

(1)Raspberry PiおよびQEMUでのパラメータの記述方法

Raspberry Piでは、起動用SDカードに「cmdline.txt」というファイルを置くと、その内容がパラメータとして実行ファイルに渡されます。

The Kernel Command Line - Raspberry Pi Documentation

QEMUでは、-append "string" というオプションを付加することにより、stringが実行ファイルに渡されます。
-appendを使う場合は、実行ファイルは-kernelで指定します。
または、-semihosting-config arg=string というより新しい形式もあります。

QEMU version 2.11.90 User Documentation

実行ファイルにパラメータを渡したい場合、実行ファイルはrawバイナリでなければなりません。ELFは不可で、objcopyでバイナリに変換する必要があります。
これについては、以下の記事に書かれています。
So I downloaded the QEMU source code and found the relevant code in hw/arm_boot.c. The code there is pretty straightforward, and it turns out that QEMU ARM will only install a tag list if it determines kernel to be Linux. This is fine in itself, but the way it figures out if a given kernel is Linux is really stupid - as a comment in the file reads:

/* Assume that raw images are linux kernels, and ELF images are not. */
QEMU ARM boot tags - Season of Code


(2)パラメータを渡すインタフェース(ARM Boot Tags)

上記で記述されたパラメータは、シェルからコマンドを起動する場合のようにargc, argvで渡されるのではありません。
ARMバイナリへパラメータを渡すには、「boot tag」というデータ形式を使用します。

boot tagはTag-Length-Value形式(実際にはLength-Tag-Value)の配列です。
Tagはあらかじめ値が決められており、文字列パラメータを渡すTagは0x54410009です。
また、末尾を表すTagは0と決められています。

先頭のTagのアドレスが、バイナリが実行される際にレジスタR2に渡されます。
ただ、伝統的にこのアドレスは0x100(0xffまではジャンプテーブルなので)に決まっているようです。

Booting ARM Linux

なお、(1)で書いたとおり、QEMUではboot tagは実行ファイルがバイナリ形式のときのみ与えられます。

(3)プログラム開始アドレス

開始アドレスは、実機では0x8000ですが、QEMUでは0x10000になります。QEMUのオプション等では、このアドレスは指定できないようです。
フォーラムの以下の書き込みでは「-kernelと-initrdで同一のバイナリを指定するとアドレスが0x8000になる」というQEMUが存在するように書かれていますが、そのQEMUのリンク先は既に消失していました。

Debugging with qemu? - Raspberry Pi Forums

指定できないのであれば、開始アドレスを0x10000にしたQEMU用のバイナリを作らざるを得ません。
objcopyコマンドにはアドレスのオフセットを与えるオプションがありますが、試してみた限りでは開始アドレスが0x8000のELFから開始アドレスが0x10000のバイナリを作ることはできないようでした。

開始アドレスはリンカスクリプトに書かれています。
これは
SECTIONS {
. = 0x8000;

のように定数になっていますが、これを
SECTIONS {
. = DEFINED(_load_addr) ? _load_addr : 0x8000;

のようにすれば、_load_addrが定義されていればその値を、定義されていなければ0x8000を使うように指定できます。
変数の値はldコマンドのオプションで
--defsym=_load_addr=0x10000
のように与えます。

変数の値をどう与えるかは、Makefileで制御します。
QEMU用と実機用とでMakefileのターゲットを分け、ターゲットによってldコマンドのオプションを変えて、開始アドレスの異なるバイナリを生成するようにしました。


以上で、(やや苦しいですが)QEMU用と実機用のコードをかなり共有することができました。
Raspberry PiベアメタルMicroPythonに適用してみたものが以下のパッチです。

feat: use UART for QEMU when qemu runs img file with -append "qemu" ・ boochow/micropython-raspberrypi@994849b

cmdlineオプションが「qemu」であればシリアルポートをQEMU用に初期化し、そうでなければ普通にMini-UARTを初期化します。
ラベル:MicroPython
posted by boochow at 23:56| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年03月24日

MicroPythonからラズパイのフレームバッファへ描画する

gpumoduletest.jpg

前回試した、Raspberry Piのベアメタルプログラミングでの画面表示を、ベアメタル版MicroPythonに載せてみました。
Raspberry PiのフレームバッファにMicroPythonから描画できました。

MicroPythonにはハードウェア非依存のフレームバッファ実装があります(extmod/modframebuf.c)。

framebuf − Frame buffer manipulation − MicroPython 1.9.3 documentation

framebufモジュールは、オブジェクト生成時にフレームバッファのメモリを外部からbytearrayオブジェクトとして与えることができます。
従って、前回のやり方でRaspberry PiのGPUにフレームバッファを確保させ、そのバッファのアドレスをMicroPythonのオブジェクトとして取得できれば、描画はframebufモジュールへ任せることができます。

従って、以下のような関数を作れば画面表示ができると考えました。

・fb_init(w=640, h=480, bpp=16, screen_w=0, screen_h=0, offset_x=0, offset_y=0)
フレームバッファを初期化する。スクリーンの解像度は、0が与えられたときはフレームバッファの解像度と同じとする。

・fb_data()
フレームバッファ用のメモリをbytearrayオブジェクトで返す。

・fb_rowbytes()
フレームバッファの1ライン分のバイトサイズを返す。(framebufではstrideというパラメータがあるが、単位はピクセル数なので、ピクセル数に変換する必要あり)

これをgpuというモジュールとして実装して、以下のように動作させてみた結果が冒頭のスクリーンショットです。
MicroPython 7db564a-dirty on 2018-03-24; Raspberry Pi with ARM1176JZF-S
>>> import gpu
>>> import framebuf
>>> gpu.fb_init(480,270,screen_w=1920,screen_h=1080)
>>> fb = framebuf.FrameBuffer(gpu.fb_data(),480,270,framebuf.RGB565)
>>> fb.text('MicroPython!',0,0,0xffff)
>>> fb.text('MicroPython!',32,8,0xf800)
>>> fb.text('MicroPython!',16,16,0x07a0)
>>> fb.text('MicroPython!',48,24,0x001f)
>>> fb.line(0,0,480,270,0x7689)
>>> fb.rect(200,100,90,45,0x3333)
>>> fb.fill_rect(200,200,90,45,0x1128)
>>> gpu.fb_rowbytes()
960
>>> fb.scroll(40,0)
>>> fb.scroll(0,30)


このモジュールの現時点の実装はこちらにあります。

new feature: gpu module implements video core mailbox interface and f… ・ boochow/micropython-raspberrypi@d9e09b6

ポイントは、GPUが確保したメモリをMicroPythonのbytearrayに変換する部分ですが、mp_obj_new_bytearray_by_refという関数があらかじめ用意されていました(こちらの記事)ので、これを使いました。

MicroPythonへモジュールを追加する方法については、以前こちらの記事でもちょっと書きましたが、あらためて関数を追加する方法なども含めてまとめておきます。

なお、以下の項目に対応する型やマクロの定義はMicroPythonのpy/obj.hにまとまっています。

●モジュールの追加
・新しいモジュールのためのCソースファイルを作り、Makefileに追加する。
・Cソースの中で、mp_obj_module_t型(構造体)の定数を宣言する。メンバ変数はbaseとglobalsの2つ。これがこのモジュールを表す内部定数となる。
・その定数をmpconfigport.hの中でextern宣言して参照できるようにする。
・mpconfigport.hの中でMICROPY_PORT_BUILTIN_MODULESへ、
{ MP_ROM_QSTR(MP_QSTR_モジュール名), MP_ROM_PTR(&上記の定数) }
を追加する。

●モジュール内グローバルオブジェクトの追加
・mp_rom_map_elem_t型の定数配列を宣言する。これがモジュール内グローバルオブジェクトのリストとなる。
・MP_DEFINE_CONST_DICT(定数名,上記の定数配列の名前)でmp_obj_dict_t型の定数を宣言する。
・mp_obj_dict_t型の定数を、モジュールを表すmp_obj_module_t型のglobalsからポイントする。
・グローバルオブジェクトのリストへ、
{ MP_ROM_QSTR(MP_QSTR_オブジェクト名), マクロ(オブジェクトの実体) }
という形式でグローバル変数を追加する。
「マクロ」はオブジェクトの実体がintならMP_ROM_INT(整数値)、それ以外ならMP_ROM_PTR(ポインタ)。

●関数の追加
・関数の実体は、mp_obj_t型を返す関数として定義する。
・マクロMP_DEFINE_CONST_FUN_OBJ_XXで関数をMicroPythonのオブジェクトとして定義する。XXの部分は関数の引数の個数により異なる。(0〜3:0個〜3個、VAR:可変長、VR_BETWEEN:最小個数と最大個数が決まっている、KW:キーワード=値の形式を用いる)
・グローバルオブジェクトのリストmp_rom_map_elem_t型配列へ
 { MP_ROM_QSTR(MP_QSTR_関数名), MP_ROM_PTR(&関数名オブジェクト名) }
を追加する

こんな感じです。

今回実装したモジュールの、インタフェース部分だけを抜き出した雛形的なものを作ってみましたので、上記の解説と併せてご覧ください。
ラベル:MicroPython
posted by boochow at 22:36| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年03月21日

Raspberry Piベアメタルプログラミングで画面表示をやってみた

baremetal_fb.jpg


ラズパイでのベアメタルプログラミング、再開しました。
これまで、LチカUARTUSBキーボードUSBキーボード(その2)と試してきましたが、今回は画面への描画です。
画面出力を制御するGPUから、フレームバッファのアドレスを取得し、フレームバッファに値を書き込むことで画面にピクセルを表示させます。

ベアメタルでのフレームバッファの使い方も、以前紹介したBaking Piというチュートリアルに解説されていますので、この通りやってみました。

Computer Laboratory – Raspberry Pi: Lesson 6 Screen01

今回のコードも以下のGitHubにアップロードしてあります。

bare_matal_rpi_zero/video at master ・ boochow/bare_matal_rpi_zero

予備知識として、グラフィックスを実際に処理するGPUと、メインのCPUとの関係を知っておく必要があります。
GPUは、独自のソフトウェアが動作する独立したCPUを持っています。これはPCのグラフィックスカード等とは別のシステムで、「VideoCore」と呼ばれています。

CPUとVideoCoreとの連携は、共有メモリを介して行います。
BCM2835 ARM PeripheralsのP5に、メモリマップが掲載されています。

rpi-memory-map.png


図の左側がVideoCore、中央がCPUのメモリマップです。右はLinuxでのメモリマップですが、今回は関係ありません。
VideoCoreとCPUでは、VC/ARM MMUを介して、SDRAMおよびI/O Peripheralsの2つのメモリ領域が共有されています。

注意が必要なのは、VideoCoreのメモリマップでは同じもの(SDRAMおよびI/O Peripherals)が4回登場している点です。
これについては後で述べます。

SDRAMにはフレームバッファが置かれ、VideoCoreとCPUとの通信はI/O Peripheralsを介して行います。
この通信は「Mailbox」という仕組みを使います。
簡単にいうと「送信用レジスタ」と「受信用レジスタ」と「ステータスレジスタ」があり、

送信:ステータスレジスタをチェックし、送信可なら送信用レジスタに値を書き込む
受信:ステータスレジスタをチェックし、受信可なら受信用レジスタから値を読み出す

というものです。
SDRAMを共有していますので、レジスタに書き込む値はポインタでもよく、大きなデータは送受信バッファを指すポインタを送信し、そのバッファから受信データを読み取ることも可能です。
また、Mailboxには「チャネル」があり、送受信レジスタの最下位4ビットはチャネル番号を表すことになっています。
従って、実際にやり取りできる情報は28ビットということになります。

このMailboxの仕組みを実装すると、以下のような感じになります。



画面出力を行うのに必要なのは、「VCへ画面の設定(解像度等)を送信し、VCが確保したフレームバッファのアドレスを受信する」という処理です。
アドレスを受け取れれば、あとはフレームバッファへ書き込めばGPUが画面へ反映してくれます。

Mailboxには7つのチャネルがありますが、今回使うのは1番のチャネルだけです。
このチャネルは、フレームバッファに関する情報交換を行います。
以下のページにマニュアル(?)があります。

Mailbox framebuffer interface ・ raspberrypi/firmware Wiki

フレームバッファのアドレスを取得するには、フレームバッファを表す構造体を用意し、その構造体へのポインタをVCへ渡します。
VCは、必要なメモリを確保し、そのメモリへのポインタを構造体のメンバ変数に書き込みます。
上のマニュアルにもあるとおり、構造体は以下のような構成になっています。
(変数名は、マニュアルとはちょっと変えています)



初期化処理では、フレームバッファのアドレス(fb_info->buf_addr)に0を書き込んでから、チャネル1にmailbox_writeし、mailbox_readしてチャネル1への応答をチェックします。
これをfb_info->buf_addrにアドレスが返るまで繰り返します。
VideoCoreの処理はCPUとは並列に行われていますので、実際に結果が得られるまでにはタイムラグがあり、結果が得られるまでループすることが必要です。

上記のコードでは、フレームバッファ構造体のアドレスに0x40000000を加えています。
これが、最初にメモリマップのところで「注意が必要」と書いた点です。

VideoCoreのメモリ空間では、SDRAMとI/O Peripheralsが4回出てきますが、アドレスによってキャッシュとメモリの値の一貫性(キャッシュコヒーレンシ)が異なります。
最初のメモリマップの図をよく見ると、以下のようになっている(らしい)ことが分かります。

・00000000〜 物理メモリにキャッシュの内容が未反映の場合がある
・40000000〜 物理メモリとL2キャッシュの値は常に一致している
・80000000〜 L2キャッシュされているメモリ空間のみ
・C0000000〜 キャッシュは利用されない(DMA用)

ARM側では、メモリ空間は00000000〜3FFFFFFFまでの1GBです。従ってポインタは常に00000000〜のエリアを指していますが、これをVideoCoreに渡すと、VideoCoreが書き込んだ応答はVideoCore側のL2キャッシュに入り、ARM側から読み取れません。
そのため、VideoCoreに渡すポインタは40000000〜のエリアを指すようにして、VideoCoreからの応答が物理メモリに反映されるようにします。

VideoCoreが確保するフレームバッファは、上で示した構造体の変数で表すと、論理的には「幅w」×「高さh」で1ピクセルあたりのビット長がbppになります。
また、横1ラインで消費するバイト数はrow_bytesになります。
y座標をフレームバッファ上のアドレスに変換する際には、y * row_bytesを使えばいいことになります。

この論理的なフレームバッファは、HDMIの解像度とは異なって構いません。
HDMIの解像度は「幅display_w」×「高さdisplay_h」であり、フレームバッファはこの解像度に拡大されて表示されます。

例えば
fb_info_t fb_info = {1920, 1080, 480, 270, 0, 16, 0, 0, 0, 0};

というパラメータを渡すと、480x270ピクセル、16 bits per pixelのフレームバッファを1920x1080に拡大して表示することになります。
最初に載せた画面写真は、以前紹介した3.5インチのLCDへこの設定で出力したものです。
posted by boochow at 15:48| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする

2018年03月18日

Respeaker 2-Mic pHAT

respeakerhat.jpg

昨日試したSpeaker pHATと同時に、Seeed StudioのReSpeaker 2-Mics Pi HATも購入していたので、こちらもつないでみました。

こちらはステレオコーデックにステレオイヤホンジャック、2つのMEMSマイク、タクトスイッチ(GPIO17に接続)、SPIに接続した3つのRGB LED、という構成のデバイスで、Raspberry Pi上でGoogle AssistantやAmazon Alexaを動作させることを想定しているそうです。
LEDのドライバはrespeaker/mic_hatのリポジトリにあります。

ボード上にはコネクタが見えますが、GroveのI2Cコネクタ、GPIOコネクタ、スピーカー接続コネクタです。
MicroUSBポートはスピーカー利用時の電力供給用だそうです。
GroveコネクタはGoogle AssistantやAmazon Alexaと外部デバイスを連携させるためのものでしょう。

メーカーの解説どおりにドライバをインストールしてみましたが、昨日のSpeaker pHATのソフトウェアと干渉するのか(一応アンインストールしたのですが)、システムログにi2s関係のエラーが出るので、Raspbianをクリーンインストールして動作を確認しました。
ALSA関連のコマンドを使うのもすごく久しぶりです。
以下のまとめが役に立ちました。

Raspberry Pi 音声まわりコマンドまとめ - Qiita

Google Assistantのほうもインストールしてみたのですが、Pi Zero W上でコンパイルが走るので非常に時間がかかりました。
かつ、まだdevice_mode_idを入力しろというエラーでうまく動作していませんので、気が向いたときにやり直してみる積もりです。
posted by boochow at 17:54| Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする
人気記事