概要
前回までで、Cマクロを使ったULPの解説が終わりました。今回はCマクロではなく、Arduino IDEから直接ULPアセンブラを使うことができるulptoolの紹介をしたいと思います。
ulptoolとは?
ulptoolとはArduino IDEからULPアセンブラを呼び出すことができるツール群です。オフィシャルのツールではなく、有志が作ったツールになります。
ulptool自体にはArduino IDEからビルドツールを呼び出す仕組みのみが提供されており、実際のビルドツールは別にあります。
Binutils fork with support for the ESP32 ULP co-processor
Binutilsとは、各種バイナリファイルを作成するツール群です。多くの場合gccを改造して、特殊なCPUなどに対応した実行ファイルを作成することができます。
BinutilsはESP32の開発元であるEspressif社が提供しています。
セットアップ方法
基本的にはulptoolのトップに書いてありますが、英語でちょっとわかりにくいところがあるので、説明したいと思います。
Python2.7のセットアップ
ulptoolはPython2.7で動きますので、あらかじめPython2.7を入れておく必要があります。
注意しておく点として、現在Pythonは3.x系が主流であり、2.7は過去のプログラムを動かすためにだけ存在しており、2.xと3.xはプログラムの互換性はありません。
すでに3.xが入っている環境の場合、2.7も入れることができますが標準で利用するのは3.xにしたほうがいいと思います。
上記がPython2.7の最新版のダウンロードページです。Windowsの場合「Windows x86-64 MSI installer」あたりが、一番一般的だと思います。
2.7の場合、もう新しいバージョンはできないとは思いますが以下のページで確認ができます。
上記のようにプラットフォーム別のダウンロードページにいくと、最新バージョンが書いてあるので、一応確認してから入れてみてください。
セットアップする場所は好みの場所でよいのですが、「C:\Python\Python27」に私は入れました。デフォルトのセットアップ先の場合には書き込み権限がなかったりして、追加ツールを入れるのに失敗する可能性があるので、他の場所にしたほうがおすすめです。
また、Windowsの場合にはPathに追加するというオプションがありますが、2.7系はPathの追加を行わず、3.x系の最新バージョンをそのあと入れて、そちらのPathを追加するのをおすすめします。
ulptoolのダウンロード
上記のリリースページから最新版のソースコードをダウンロードします。
ulptoolの展開
ダウンロードしたzipファイルを展開します。そのまま展開するとulptool-2.4.1などのようにバージョン番号ついたフォルダができあがるので、バージョン番号を消してulptoolと名前を変更します。
ulptoolのコピー
- (Mac OS) ~/Library/Arduino15/packages/esp32
- (Windows) C:\Users\\AppData\Local\Arduino15\packages\esp32
- (Linux) ~/.arduino15/packages/esp32
コピー先は環境によって異なるので注意してください。

Arduino IDEの環境設定から、上記の赤で囲われたpreferences.txtをクリックすると、該当フォルダのArduino15が開くと思います。
packages\esp32\toolsと開いて、展開したulptoolをコピーします。
「Arduino15\packages\esp32\tools\ulptool」などのフォルダ構成になると思います。
Binutilsのダウンロード
上記から最新バージョンのBinutilsをダウンロードします。環境ごとにダウンロードするファイルが異なるので注意してください。
また、esp32ulpとesp32s2ulpがあるので注意してください。esp32s2ulpはESP32の独自ULPではなく、RISC-Vベース新しいチップ用のツールになります。
Windowsの場合には「binutils-esp32ulp-win32-x.xx.xx-esp-xxxxxxxx.zip」などのファイルになると思います。
Binutilsの展開
ダウンロードしたzipファイルを展開します。esp32ulp-elf-binutilsフォルダに展開されると思います。
Binutilsのコピー
先程のulptoolの中にあるsrcフォルダにesp32ulp-elf-binutilsをコピーします。
「Arduino15\packages\esp32\tools\ulptool\src\esp32ulp-elf-binutils」などのフォルダ構成になると思います。
platform.local.txtのコピー
このままでは、コピーしたulptoolが使われないので、追加の設定ファイルである「platform.local.txt」を指定フォルダにコピーします。
こちらも環境により異なりますが「Arduino15\packages\esp32\hardware\esp32\1.0.4」などの場所にあります。「platform.txt」があるフォルダです。
最後のバージョン番号は今後変わるとおもいますので、今入っているバージョン番号のフォルダに入れます。ここに入れるので、ESP32のライブラリのバージョンが変わるとulptoolも消えてしまうのでセットアップし直しになります。
また、すでに「platform.local.txt」がある場合には上書きすると既存の設定が消えてしまうので、ファイルの中身をコピーして追記する必要があると思います。
「platform.local.txt」の編集
version=2.3.0 ## paths compiler.ulp.path={runtime.tools.ulptool.path}/esp32ulp-elf-binutils/bin/ tools.ulptool.path={runtime.tools.ulptool.path}/ ## tool name tools.ulptool.cmd=esp32ulp_build_recipe.py ## ulp build tool #compiler.s.cmd=python "{tools.ulptool.path}{tools.ulptool.cmd}" compiler.s.cmd=C:\Python\Python27\python "{tools.ulptool.path}{tools.ulptool.cmd}" ## ulp_main.ld file address compiler.c.elf.extra_flags="-L{build.path}/sketch/" -T ulp_main.ld "{build.path}/sketch/ulp_main.bin.bin.o" (以下略)
上記が私の環境に合わせた設定ファイルです。「python」とPath指定無しだったプログラムに対して、「C:\Python\Python27\python」と明示的に2.7のPythonを使うように指定しています。
私の環境は「C:\Python\Python38\python」は標準で使う最新版のPythonで、3.x系で動かない古いスクリプトのみ2.7で動かすようにしています。
動作確認
「ulptool\src\ulp_examples\ulp_README」
上記にサンプルスケッチがあるので開いてみます。Arduino IDEのメニューのスケッチ例からは探しに行かない場所になるので注意してください。
ulp count: 0 ulp count: 1 ulp count: 2 ulp count: 3 ulp count: 4
スケッチを動かしてみて、上記のような出力がシリアルモニタに出力されていれば環境構築は完了です。
ESP32 ULP Debugger組み込み
ちょっとこのままだとわかりにくいと思うので、ESP32 ULP Debuggerも同時に使ってみたいと思います。
#include "esp32/ulp.h" // include ulp header you will create #include "ulp_main.h" // include ulptool binary load function #include "ulptool.h" #include "UlpDebug.h" // Unlike the esp-idf always use these binary blob names extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start"); extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end"); static void init_run_ulp(uint32_t usec); void setup() { Serial.begin(115200); memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM); delay(1000); init_run_ulp(100 * 1000); // 100 msec } void loop() { // ulp variables data is the lower 16 bits Serial.printf("ulp count: %u\n", ulp_count & 0xFFFF); ulpDump(); delay(100); } //hash: 5c389934265d8016df226704091cd30a static void init_run_ulp(uint32_t usec) { // initialize ulp variable ulp_set_wakeup_period(0, usec); esp_err_t err = ulptool_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t)); // ulp coprocessor will run on its own now ulp_count = 0; err = ulp_run((&ulp_entry - RTC_SLOW_MEM) / sizeof(uint32_t)); if (err) Serial.println("Error Starting ULP Coprocessor"); }
色がついている行が追記した行です。UlpDebug.hを読み込んで、memsetでULPで使うRTC_SLOW_MEM領域を0クリアしています。
loop()の中でulpDump()を呼び出すことで、ULPの逆アセンブラ結果を出力しています。
ちなみに、このままだと100ミリ秒ごとに出力されるので、delay(100)の数は大きくしたほうが見やすいと思います。
ulp count: 2 ==================================== 0000 : 72800053 PROG MOVE R3, 5 // R3 = 5 0001 : D000000C PROG LD R0, R3, 0 // R0 = MEM[R3+0] 0002 : 72000010 PROG ADD R0, R0, 1 // R0 = R0 + 1 0003 : 6800000C PROG ST R0, R3, 0 // MEM[R3+0] = R0 0004 : B0000000 PROG HALT // HALT 0005 : 00630002 DATA 2 // ST ADDR:0x0003
上記のような出力結果になれば成功です。
/* Define variables, which go into .bss section (zero-initialized data) */ .data /* Store count value */ .global count count: .long 0 /* Code goes into .text section */ .text .global entry entry: move r3, count ld r0, r3, 0 add r0, r0, 1 st r0, r3, 0 halt
上記が元のULPアセンブリコードになります。逆アセンブラの結果と比べてみると、データが一番上で「.global count」を宣言していますが、プログラムの後ろに実際は配置されているのがわかります。
ulptoolの変数アクセス
これまでのESP32 ULP Debuggerを使ったCマクロでは、最初にenumで変数を宣言し、RTC_SLOW_MEM[SLOW_COUNT]などの形でアクセスしていました。
しかし通常のアセンブラではプログラムの後ろに変数が置かれているので、実際にアセンブラでマシン語にしたあとでないと、変数のアドレスが確定しません。
そこで、ulptoolではちょっと特殊なことをしています。
/* Put your ULP globals here you want visibility for your sketch. Add "ulp_" to the beginning of the variable name and must be size 'uint32_t' */ #include "Arduino.h" extern uint32_t ulp_entry; extern uint32_t ulp_count;
上記はulp_main.hというファイルです。ここで、externを使って外部で変数が確保されていると宣言しています。
ulp_entryとulp_countの2つありますが、これはULPアセンブリコードの「.global count」と「.global entry」の行に対応しています。
内部のコードを見てみたところ、マップファイルを分析して、シンボルをulp_main.ldに定義する処理がesp32ulp_mapgen.pyで実行されていました。
/* Variable definitions for ESP32ULP linker * This file is generated automatically by esp32ulp_mapgen.py utility. */ PROVIDE ( ulp_count = 0x50000014 ); PROVIDE ( ulp_entry = 0x50000000 );
上記が実際に生成されていたulp_main.ldファイルでulp_entryがULPが実行開始されるアドレスです。0x50000000はRTC_SLOW_MEMのアドレスなので、先頭から開始。
0x50000014は先頭から0x14(20)バイト進んだところで、RTC_SLOW_MEMは4バイト(32ビット)単位なので、5個進んだ場所である、RTC_SLOW_MEM[5]になります。
逆アセンブラの結果と一致しています。
そのため、Arduino側からはulp_countと書くことで、RTC_SLOW_MEM[5]と同じ値にアクセスすることができます。
ちなみに、処理の中でulp_main.h自体も作成してくれているのですが、コンパイルはすでに終わっているので読み込んでいません。そして、そのファイルは実際のプロジェクトファイルを更新していないので意味がないファイルとなっています。
その他のスケッチを確認
よく見るとADCなど苦労したところのスケッチ例がありました。ほぼ同じことをしていたので良かったです。
I2CのサンプルもあるのですがI2Cの専用命令を使っていません。自分でGPIOを直接叩いてソフトウエア的にI2Cプロトコルを実装しています。
ちょっと調べてみたところ、ULP標準のI2CコマンドはGPIO4,GPIO0かGPIO2,GPIO15の組み合わせでしか使えないようです。
ESP32テクニカルマニュアルの29.6 RTC_I2C Controllerからの項目で解説があり、4.11 RTC_MUX Pin Listに使えるPINの一覧が書いてあります。
実際のところ、GPIO0もGPIO2もブートに関するPINなので、あまり外部接続したくありません。そのためULPハードウエアのI2Cプロトコルは使わずに、ソフトウエア実装のI2Cをほとんどの人が使っているようでした。
ulp_i2c_bitbangというスケッチ例が入っており、こちらを参考に実装している人が多いです。しかしながらかなり面倒な処理が大量にあるので、これをCマクロで実装するのはちょっと大変そうです。
ESP32 ULP Debuggerあたりにヘルパーマクロを用意して、気軽に使えるようにしたほうがいいのかな、、、
まとめ
かなりディープな内容になってきていますので、普通の人が読んでみ意味がない記事になってきているような気がします。
ESP32-S2が販売されたら、RISC-V版のULPを使っての一般的なアセンブラ解説を書こうと思いますが、この記事はどんどんディープな方向に進んでいきたいと思います。
次はESP32 ULP Debuggerをそろそろ修正したいと思いますので、少し時間があくかもしれません。
コメント