STM32のMicroPythonでlwIPを使う

lwip-wiznet5500.png

現在のSTM32用のMicroPythonでは、TCP/IPとして利用できるのはWIZnetやCC3000などのTCP/IPモジュールだけです。
これに対して、以前の記事で、WIZnetのTCP/IPモジュールをイーサネットインタフェースとして使い、TCP/IP実装はlwIPを用いるような実装が進められていることを書きました。(lwIPはオープンソースの軽量なTCP/IP実装です。)

ただし、現時点ではこのコードはESP32/ESP8266のMicroPythonでないと動かないと思います。
urequestsは内部でsocket.readline()を使用していますが、CC3100やWIZnet5Kなど、上記以外のNICではreadline()が実装されていないからです。

そのため、WIZnet5Kに関してはTCP/IP実装はlwIPを使用し、WIZnet5Kは単なるイーサネットインタフェースとして使用するようなMicroPython側のドライバ実装が現在テスト中のようです。

stm32: Incorporate lwip stack and use Wiznet in MACRAW mode by dpgeorge ・ Pull Request #3379 ・ micropython/micropython

ただ、TCP/IPをメインCPUで処理するため、WIZnet5Kのソケット機能を使う場合よりも速度は低下するようです。

上記を書いた後、チューニングが進んでかなり高速化されたようなので、試してみることにしました。
この方式だと、WIZnetを使う場合と比べて、socketの機能がフル実装されますので、上に書いたようにurequestsなどの便利なモジュールが利用できるようになるというメリットがあります。

この実装はDamien George本人が行っていますが、現時点ではマスターブランチへのマージは行われていません。そのため、このブランチをダウンロードしてコンパイルする必要があります。
手順は以下の通りです。

まず、本家ではなくDamien GeorgeのMicroPythonリポジトリをcloneします。

mkdir micropython-lwip
cd micropython-lwip/
git clone https://github.com/dpgeorge/micropython.git

このリポジトリは沢山のブランチがありますので、ブランチの一覧を見ます。

cd micropython/
git branch -r

表示された中から、stm32-lwip-wiznetブランチを選択します。

git checkout -b stm32-lwip-wiznet origin/stm32-lwip-wiznet

ビルドします。

git submodule update --init
make -C mpy-cross
make -C ports/stm32 MICROPY_PY_WIZNET5K=5500 BOARD=NUCLEO_F401RE CROSS_COMPILE=~/gcc-arm-none-eabi-5_4-2016q3/bin/arm-none-eabi-

追記:以下の事象をリポートしたところ、速攻で修正されたので、現在は以下の修正は必要なくなっています。

すると、以下のようなエラーが出ました。

LINK build-NUCLEO_F401RE/firmware.elf
build-NUCLEO_F401RE/main.o: In function `rand':
main.c:(.text.rand+0x0): undefined reference to `rng_get'
build-NUCLEO_F401RE/lib/lwip/src/core/dhcp.o: In function `dhcp_create_msg':
dhcp.c:(.text.dhcp_create_msg+0x30): undefined reference to `rng_get'
build-NUCLEO_F401RE/lib/lwip/src/core/ipv4/igmp.o: In function `igmp_delaying_member':
igmp.c:(.text.igmp_delaying_member+0x1e): undefined reference to `rng_get'
build-NUCLEO_F401RE/lib/lwip/src/core/ipv4/igmp.o: In function `igmp_joingroup':
igmp.c:(.text.igmp_joingroup+0x72): undefined reference to `rng_get'
Makefile:447: ターゲット 'build-NUCLEO_F401RE/firmware.elf' のレシピで失敗しました

リンクに失敗しているようです。rng_getが見つからないと言われています。

ということでちょっと調べたところ、以下のことが分かりました:

・lwIPは、移植の際に32bitの乱数を得る関数LWIP_RAND()を#defineで与える必要がある。乱数がいいかげんだと脆弱性につながるので、あまり適当な関数を与えないほうが良い。
・今回のMicroPythonの実装では、LWIP_RANDは ports/stm32/lwip-include/lwipopts.h の中で与えており、その定義は以下のようである。

extern uint32_t rng_get(void);
#define LWIP_RAND()                     rng_get()

・このrng_get()の実体は ports/stm32/rng.c で定義されており、その実装はHAL_RNG_GetRandomNumber()を呼び出すだけである。
・ただし、rng.cの中身全体が #if MICROPY_HW_ENABLE_RNG で囲われているので、rng_get()がリンクエラーになったということはMICROPY_HW_ENABLE_RNGが設定されていない可能性が高い。

ということで、ports/stm32/boardsの各ボードでどのようになっているか調べてみます。

cd ports/stm32/boards/
grep MICROPY_HW_ENABLE_RNG */mpconfigboard.h

結果を見ると、ESPRUINO_PICO、PYBLITEV10、STM32F411DISCで値が0になっているほか、F401REでは値の設定もされていません。
F401REがハードウェア乱数発生器を持っているのであれば1、持っていないのであれば0に設定すべきです。
ハードウェア乱数発生器の有無は、STM32のHALの実装を見ると分かります。
micropython/lib/stm32lib/STM32F4xx_HAL_Driver/Src の中にある、stm32f4xx_hal_rng.c を見てみます。
すると

#if defined(STM32F405xx) || defined(STM32F415xx) || defined(STM32F407xx) || defi
ned(STM32F417xx) ||\
defined(STM32F427xx) || defined(STM32F437xx) || defined(STM32F429xx) || defi
ned(STM32F439xx) ||\
defined(STM32F410Tx) || defined(STM32F410Cx) || defined(STM32F410Rx) || defi
ned(STM32F469xx) ||\
defined(STM32F479xx) || defined(STM32F412Zx) || defined(STM32F412Vx) || defi
ned(STM32F412Rx) ||\
defined(STM32F412Cx) || defined(STM32F413xx) || defined(STM32F423xx)

となっていて、残念ながらF401REは対象外のようです。
STMicroのアプリケーションノート「AN4230」を見ても、やはりF401REにはハードウェア乱数発生器は載っていないように見えます。(このアプリケーションノート自体は、生成された乱数が暗号学的乱数になっているかテストしたレポートです。)

従って、F401REではLWIP_RAND()に用いる乱数発生器をソフトウェアで実装する必要があります。もちろん暗号学的乱数であることが望ましいですが、MicroPythonにそこまで求めるのはどうかという気もします。

そもそも、乱数生成のソフトはMicroPythonにもともと入っている可能性が高いのではないか? と思い、調べてみたところ、以下が見つかりました。

Pseudo-random number generator · Issue #965 · micropython/micropython

yasmarangという乱数生成の関数が、MicroPythonのurandomモジュールで実装されていますので、これをLWIP_RAND()の実体として使うことにします。
まず、ports/stm32/rng.cの最後のほうにコードを追加して、ハードウェア乱数生成器が無い場合はyasmarang()を使うようにします。

追記:このやり方だとurandomモジュールが生成する乱数列の再現性が失われてしまうため、修正されたコードではrng.cの内部にyasmarangのコードを複製して使っています。

ports/stm32/rng.c:

#if MICROPY_HW_ENABLE_RNG
//途中省略
MP_DEFINE_CONST_FUN_OBJ_0(pyb_rng_get_obj, pyb_rng_get);

#else

extern uint32_t yasmarang(void);

uint32_t rng_get(void) {
return yasmarang();
}

#endif // MICROPY_HW_ENABLE_RNG

また、yasmarang()はstatic宣言されていますので、これを外部から呼び出せるようにstaticを外します。

extmod/modurandom.c

//STATIC uint32_t yasmarang(void)
uint32_t yasmarang(void)
{

これでリンクが通るようになりました。
使い方ですが、以前のWIZNET5Kと結線は全く同じですが、ソフトウェアは完全互換ではなく微妙に違います。
サンプルが ports/stm32/wiznet_connect.py に入っているのでその通りやればOKです。
明示的にactiveにしたりdhcpでアドレス設定させたりという処理が必要になるようです。

SPI2を使う結線の場合のサンプルです。
例によってスターウォーズ視聴です。
DHCPのアドレス取得など、WIZnet側に任せる場合と比べて初期化にちょっと時間がかかる印象がありますが、F401REを使う場合は、通信速度自体はむしろこちらのほうが高速です。

コメント