ESP32のULPアセンブリ言語入門 その2 環境構築と最小スケッチ

概要

前回はn進数と単位で終わってしまいました。今回はULPの概要についてまとめてみました。

ULPアセンブリの特徴

ULPはESP32のメインプロセッサではなく、省電力でちょっとしたことを動かすコプロセッサーです。最近の携帯電話なども歩数カウントなどの管理を行うコプロセッサーと、メインプロセッサーを分離することで省電力化しています。

ULPのコプロセッサーは、ESP32のメインプロセッサーとは独立して動いています。プロセッサー間の通信機能などはないのですが、どちらからでもアクセスできるメモリがあり、そのメモリをかいして、データのやり取りなどを行います。

スローメモリと呼ばれるメモリがあり、このメモリはリセットやディープスリープをしても内容が保持されている特徴があります。電源を切ると内容が消えてしまうので、通常のメモリなのですが、ディープスリープ中などにも電源を供給し、内容を保持しています。

ESP32には8KByteのスローメモリを搭載しており、1KByteは1,024Byteなので、8,192Byteになります。

このスローメモリはデータの受け渡しの他に、ULPで動くプログラムをロードするメモリでもあります。Arduino上では、プログラムとデータを区別して管理する必要は通常ありませんが、スローメモリでは、プログラムなのかデータなのかを自分で管理する必要があります。

スローメモリは8,192Byteありますが、ULPは32bitのプロセッサーのため、32bit単位で一般には管理します。32bitは4Byteなので、8,192Byteの場合4Byteが2,048個あることになります。

ULPでは1つの命令が4Byte、1つのデータも4Byteになります。そのため命令とデータをあわせて2,048個まで利用することができます。

ディープスリープしても消えない変数として、2,048個のデータとして使うこともできますし、プログラムとデータを好きな割合で使えます。

アセンブリの命令長とは?

ULPは32bitのプロセッサなので、4Byteの命令で動いています。最近のシンプルな動きをするプロセッサは動いているbit数と命令長が同じものが多いです。

パソコンなどで利用しているCPUに関しては、8bit時代の命令に、16bitの命令を追加してと拡張してきているので、単純な命令は短く、複雑な命令は長くなっています。

このあたりはRISCとCISCなどで検索すると解説サイトが出てくると思います。

環境構築

さて、実際にULPプログラミングの環境を作ってみたいと思います。このサイトの方針として、検索すると、良質なサイトがたくさんあるような話題については、細かく書きません。

環境構築はWindows、Mac、Linuxで方法が違うのと、画面などが変わりやすいために検索する場合のキーワードだけかく方針です。環境構築は検索しやすい項目であり、いろいろ気になった環境を構築してみると、学びが大きいのでぜひ自分で調べながらチャレンジしてもらいたいと思います。

また、今回は一般的な開発環境であるArduino IDE上での開発を紹介したいと思います。ESP32の開発ボードは何を使っても基本的には問題ありません。ただしLチカなどを行う関係上、外部にピンがでていないような開発ボードは避けてください。

説明ではM5StickCを利用していきますが、ピン番号を変えれば他の開発ボードでも動くと思います。

Arduino IDE

上記から環境に応じたArduino IDEをダウンロードしてきてください。WEB EDITORがありますが、現状はESP32の開発に対応していないので、Downloadを利用してください。

ESP32ボードの追加

Arduino IDEは、標準ではArduinoボードの開発しか対応していません。ESP32の開発をする場合には、ボードの追加という設定が必要になります。

  • https://github.com/espressif/arduino-esp32/blob/master/docs/arduino-ide/boards_manager.md

上記が英語ですが、Google翻訳などを使えば内容がわかると思います。ここで安定版のURLを設定から追加するのですが、過去変わったことがありますので、できれば設定方法などで検索したサイトの情報ではなく、上記の正式なURLが更新されていないかを確認してから設定を行ってください。

ESP32 ULP Debuggerのライブラリ追加

必須ではありませんが、ULPの勉強では便利なので追加してください。スローメモリの内容を解析して、シリアル出力してくれるライブラリです。

ライブラリマネージャからULPで検索することで、探すことができると思います。

ULPプログラミングの環境構築確認

スケッチ例からESP32 ULP Debuggerの中にある、UlpBlinkを開いてください。あとで内容の解説をするので、中身を理解する必要はありません。

ボードをESP32の接続している開発ボードに変更し、シリアルポートなどを設定してからこのスケッチを動かしてください。

スケッチが動かない場合には、環境構築が失敗していますので、もう一度環境を見直してください。

転送が終わったあとに、シリアルモニタを開いて、定期的に文字が流れているのかを確認してください。

====================================
0000 : 01430001 DATA     1				// ST ADDR:0x000A
0001 : 72800003 PROG MOVE   R3,   0			// R3 = 0
0002 : D000000C PROG LD     R0, R3,   0			// R0 = MEM[R3+0]
0003 : 820A0001 PROG JUMPR  +  5,   1, LT		// IF R0 < 1 THAN GOTO 0x0008
0004 : 1AD40500 PROG REG_WR 0x0000,  21,  21,   1	// RTC_IO[21:21] = 1
0005 : 72800000 PROG MOVE   R0,   0			// R0 = 0
0006 : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
0007 : 8000002C PROG JUMP   0x000B			// GOTO 0x000B
0008 : 1AD40100 PROG REG_WR 0x0000,  21,  21,   0	// RTC_IO[21:21] = 0
0009 : 72800010 PROG MOVE   R0,   1			// R0 = 1
000A : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
000B : 4000EA60 PROG WAIT   60000			// Delay 60000
000C : 4000EA60 PROG WAIT   60000			// Delay 60000
000D : 4000EA60 PROG WAIT   60000			// Delay 60000
000E : 4000EA60 PROG WAIT   60000			// Delay 60000
000F : 4000EA60 PROG WAIT   60000			// Delay 60000
0010 : 4000EA60 PROG WAIT   60000			// Delay 60000
0011 : 4000EA60 PROG WAIT   60000			// Delay 60000
0012 : 4000EA60 PROG WAIT   60000			// Delay 60000
0013 : 4000EA60 PROG WAIT   60000			// Delay 60000
0014 : 4000EA60 PROG WAIT   60000			// Delay 60000
0015 : 4000EA60 PROG WAIT   60000			// Delay 60000
0016 : 4000EA60 PROG WAIT   60000			// Delay 60000
0017 : 4000EA60 PROG WAIT   60000			// Delay 60000
0018 : 4000EA60 PROG WAIT   60000			// Delay 60000
0019 : B0000000 PROG HALT				// HALT

上記のような文字列が流れていれば、環境構築が成功しています。

ULP最小スケッチ例

環境構築が完了し、ULPプログラミングが可能になったところで、最低限のスケッチをベースに解説を行いたいと思います。

#include "esp32/ulp.h"
#include "UlpDebug.h"
void setup() {
  // シリアルの初期化
  Serial.begin(115200);
  // スローメモリの初期化
  memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM);
  // ULPのプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_HALT()  // 終了
  };
  // 300ミリ秒(300,000マイクロ秒)間隔でULPを実行
  ulp_set_wakeup_period(0, 300000);
  // プログラムのサイズを設定
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(0, ulp_prog, &size);
  // ULPを実行
  ulp_run(0);
}
void loop() {
  // デバッグ出力
  ulpDump();
  // 1秒待つ
  delay(1000);
}

上記が最小限のプログラムになります。

スケッチ解説

  // シリアルの初期化
  Serial.begin(115200);

ESP32 ULP Debuggerはシリアル出力で表示しますので、初期化をしています。

  // スローメモリの初期化
  memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM);

起動直後はランダムな数字が入っていますので、まずはスローメモリをすべて初期化します。本来は8Kなので8,192バイトなのですが、Arduino IDEからはCONFIG_ULP_COPROC_RESERVE_MEMで宣言されている512バイト(128命令)までしか利用することができません。

  // ULPのプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_HALT()  // 終了
  };

ULPのプログラム自体はあとで説明をしますが、I_HALT()はULPを終了する命令です。

  // 300ミリ秒(300,000マイクロ秒)間隔でULPを実行
  ulp_set_wakeup_period(0, 300000);

1つ目の引数で周期の方法を指定しますが、0のSENS_ULP_CP_SLEEP_CYC0_REG以外は通常利用しません。

2つ目の引数でマイクロ秒単位で、呼び出し間隔を設定します。例では300ミリ秒間隔ですがULPプログラムはあまり時間精度が高くないので、結構ずれた時間間隔で呼び出されます。

  // プログラムのサイズを設定
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(0, ulp_prog, &size);

プログラムの大きさを計算して、スローメモリの0番目(先頭)にプログラムをロードします。

  // ULPを実行
  ulp_run(0);

スローメモリの0番目(先頭)からプログラムを実行します。以後、ulp_set_wakeup_period()で設定した間隔ごとにULPプログラムが実行されます。

  // デバッグ出力
  ulpDump();

スローメモリの状況をシリアルに出力します。

  // 1秒待つ
  delay(1000);

あまり大量に出力しても、読みにくいので1秒間隔で状態を出力するようにウエイトを入れます。

実行例

====================================
0000 : B0000000 PROG HALT				// HALT

今回なにもしていないプログラムなので、HALTしか表示されていません。

実用的なスケッチ例

最小スケッチ例の場合には、ちょっと省略しすぎていてデータ管理などができていません。スケッチ例のESP32 ULP Debuggerの中にある、UlpBlinkを再度開いてもらい、差分を説明します。

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "UlpDebug.h"
// Slow memory variable assignment
enum {
  SLOW_BLINK_STATE,     // Blink status
  SLOW_PROG_ADDR        // Program start address
};
void ULP_BLINK(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);
  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM);
  // Blink status initialization
  RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0;
  // PIN to blink (specify by +14)
  const int pin_blink_bit = RTCIO_GPIO26_CHANNEL + 14;
  const gpio_num_t pin_blink = GPIO_NUM_26;
  // GPIO26 initialization (set to output and initial value is 0)
  rtc_gpio_init(pin_blink);
  rtc_gpio_set_direction(pin_blink, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(pin_blink, 0);
  // ULP Program
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_BLINK_STATE),           // R3 = SLOW_BLINK_STATE
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)]
    M_BL(1, 1),                             // IF R0 < 1 THAN GOTO M_LABEL(1)
    // R0 => 1 : run
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 1), // pin_blink_bit = 1
    I_MOVI(R0, 0),                          // R0 = 0
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)] = R0
    M_BX(2),                                // GOTO M_LABEL(2)
    // R0 < 1 : run
    M_LABEL(1),                             // M_LABEL(1)
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 0),// pin_blink_bit = 0
    I_MOVI(R0, 1),                          // R0 = 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)] = R0
    M_LABEL(2),                             // M_LABEL(2)
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_DELAY(60000),
    I_HALT()                                // Stop the program
  };
  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);
}
void setup() {
  // For debug output
  Serial.begin(115200);
  // Execute ULP program at 300ms intervals
  ULP_BLINK(300000);
}
void loop() {
  // For debug output
  ulpDump();
  // Wait
  delay(1000);
}

データ項目の管理

// Slow memory variable assignment
enum {
  SLOW_BLINK_STATE,     // Blink status
  SLOW_PROG_ADDR        // Program start address
};

上記のenumでデータを管理しています。スローメモリの先頭からデータをenumでデータを割り当てていき、最後のSLOW_PROG_ADDRがプログラムが開始するアドレスになります。

上記の場合には1つだけ変数があることになります。

ULP初期化関数分離

void ULP_BLINK(uint32_t us) {
  // Set ULP activation interval
  ulp_set_wakeup_period(0, us);
  // Slow memory initialization
  memset(RTC_SLOW_MEM, 0, CONFIG_ULP_COPROC_RESERVE_MEM);
  // Blink status initialization
  RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0;

ULP関連の処理が増えていきますので、関数を作って分離しています。標準的な処理のほかに、引数で読み出し間隔を設定しています。

RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0で、変数の初期化もしています。この初期化の前にmemset()ですべて0に設定しているので、必要ないのですが初期化もれを防ぐために、明示的に書いてあります。

ピンの初期化

  // PIN to blink (specify by +14)
  const int pin_blink_bit = RTCIO_GPIO26_CHANNEL + 14;
  const gpio_num_t pin_blink = GPIO_NUM_26;
  // GPIO26 initialization (set to output and initial value is 0)
  rtc_gpio_init(pin_blink);
  rtc_gpio_set_direction(pin_blink, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(pin_blink, 0);

ここは今後に細かい解説をしますが、pinMode()に近い設定を行っております。

プログラムロードと実行

  // Run the program shifted backward by the number of variables
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &size);
  ulp_run(SLOW_PROG_ADDR);

プログラムのサイズを計算し、enumのSLOW_PROG_ADDRがあらわすプログラムの開始アドレスにプログラムをロードして、実行も行います。

データ部分に関してはすべてを0に初期化して、個別に代入しているのでここではロードしません。

ULPのプログラムに関しては、enumを利用する方法以外でも可能です。しかしながら一般的には利用したほうが管理が楽だと思われますので、この方式で解説をすすめていきたいと思います。

まとめ

また、ULPプログラミングをすることなく終わってしまいました。次回は実際にULPプログラミングをしながら命令を解説したいと思っています。

続編

コメント