ESP32のULPアセンブリ言語入門 その4 ジャンプ

概要

前回は初歩的な命令までやりましたので、今回はLチカを行いたいと思います。

ULPでのGPIOアクセス

ULPからGPIOのアクセスする場合にはArduinoの標準とは違い、RTCを経由して通信をする必要があります。

RTCはおそらくはReal Time Clockあたりの略だと思います。もしくはWebRTCなどと同じくReal-time communicationでリアルタイム通信の略の可能性がありますが特定できていません。

RTCを利用したGPIOアクセスで気をつけることは、すべてのGPIOへのアクセスはできません。PIN設定でRTC_GPIOが設定されているPINのみになります。

ざっくりいうと、ADCに接続されているPINはRTC経由でアクセスが可能です。

RTC_GPIOについて

PINの並びが通常のGPIOともADCとも違い、独自の並びで並んでいます。

RTC_GPIOGPIOBitArduinoでの推奨Bit記述方法
RTC_GPIO0GPIO3614RTCIO_GPIO36_CHANNEL + 14
RTC_GPIO1GPIO3715RTCIO_GPIO37_CHANNEL + 14
RTC_GPIO2GPIO3816RTCIO_GPIO38_CHANNEL + 14
RTC_GPIO3GPIO3917RTCIO_GPIO39_CHANNEL + 14
RTC_GPIO4GPIO3418RTCIO_GPIO34_CHANNEL + 14
RTC_GPIO5GPIO3519RTCIO_GPIO35_CHANNEL + 14
RTC_GPIO6GPIO2520RTCIO_GPIO25_CHANNEL + 14
RTC_GPIO7GPIO2621RTCIO_GPIO26_CHANNEL + 14
RTC_GPIO8GPIO3322RTCIO_GPIO33_CHANNEL + 14
RTC_GPIO9GPIO3223RTCIO_GPIO32_CHANNEL + 14
RTC_GPIO10GPIO0424RTCIO_GPIO04_CHANNEL + 14
RTC_GPIO11GPIO0025RTCIO_GPIO00_CHANNEL + 14
RTC_GPIO12GPIO0226RTCIO_GPIO02_CHANNEL + 14
RTC_GPIO13GPIO1527RTCIO_GPIO15_CHANNEL + 14
RTC_GPIO14GPIO1328RTCIO_GPIO13_CHANNEL + 14
RTC_GPIO15GPIO1229RTCIO_GPIO12_CHANNEL + 14
RTC_GPIO16GPIO1430RTCIO_GPIO14_CHANNEL + 14
RTC_GPIO17GPIO2731RTCIO_GPIO27_CHANNEL + 14

上記が対応表ですが、実際の指定はRTC_GPIOをビットで指定して行います。複数のGPIOをまとめて指定することも可能ですが、通常使うときには一番右側のRTCIO_GPIOXX_CHANNEL + 14の形式で単独のGPIOへのアクセスのみを使ったほうが無難だと思います。

内部的にはRTCIO_GPIO4_CHANNELはRTC_GPIO10ですので10が定義されており、+14しているので10+14で24になります。

範囲指定する場合には、少ないビット数から多いビット数を指定する必要があり、内部のビットを把握しながら順番を決める必要があります。そこまでするのであれば、単独GPIO単位で設定したほうが安全だと思います。

esp32_gpioMux[GPIO_NUM_24].rtcでも10が定義されています。こちらの構造体の場合にはRTCアクセスできないGPIOには-1が入っているので注意してください。

事前準備

上記ではM5StickCを利用していますが、どのESP32でも動くと思います。

LEDはADC(RTC)が使えるピンであればどこでも構いません。利用するピンにLEDの足の長いプラス(アノード)を接続し、GNDにマイナス(カソード)を接続してください。抵抗入りLEDがない場合には、抵抗とLEDでも問題ありません。M5StickCは普通のLEDを抵抗なしで使っても壊れませんでしたが、なるべく抵抗を入れたほうがいいと思います。

命令紹介

今回追加された命令のみを紹介します。

ラベル

この命令はCマクロオリジナルの命令で、実行時には対応する命令はありません。アドレスを特定するための疑似命令で、ulp_process_macros_and_load()関数にて取り除かれます。

M_LABEL(label_num)

label_numには0から65535までのラベルを識別する番号を設定します。

    M_LABEL(1),

上記の場合には1番のラベルを定義しました。ジャンプ命令などでラベル番号を指定してジャンプすることができるようになります。

ウエイト – WAIT

指定サイクル停止する命令です。

I_DELAY(cycles_)

cycles_には0から65535までの値が入ります。ULPは8MHzで動いているので、最大値の65535を指定してもそれほど長い時間は停止しません。

また、クロックは温度などの影響を受けるため、それほど正確な時間を指定することはできません。

    I_DELAY(60000),

上記で60000サイクルのウエイトを追加しています。

レジスタ書込 – REG_WR

レジスタに書き込みを行います。実際にレジスタがどう対応しているのかはESP32 Technical Reference ManualのRegister Summaryの項目などを見る必要がありますが、非常に難解です。

I_WR_REG(reg, low_bit, high_bit, val)

regで書き込むアドレス、low_bitとhigh_bitで書き込む範囲、valで値を指定します。

    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 1)

上記でRTC_GPIO_OUT_REGはデジタル出力のレジスタアドレスに、pin_blink_bitで指定したピンに1を書き込んでいます。

low_bitとhigh_bitが同じなので、単独PINの出力をHIGH(1)に設定していることになります。

REGには大きく分類すると以下の4種類があります。

  • RTC_CNTL
  • RTC_IO
  • SENS
  • RTC_I2C

CNTLはRTC自体のタイマーなどの設定。IOはGPIOの設定や読み書き。SENSはADCなどの設定。RTC_I2CはI2Cなどの設定になります。

ジャンプ – JUMP

ジャンプ命令はCマクロでは条件により別の命令にわかれており、非常にわかりにくいです。Mからはじまるマクロ関数はulp_process_macros_and_load()関数にてラベルのアドレスに飛び先を書き換える処理が入っています。

JUMP Rdst        // 無条件でレジスタのアドレスにジャンプ
JUMP ImmAddr     // 無条件で指定アドレスにジャンプ
JUMP Rdst, EQ    // 最終演算がゼロの場合レジスタのアドレスにジャンプ
JUMP Rdst, OV    // 最終演算がオーバーフローの場合レジスタのアドレスにジャンプ
JUMP ImmAddr, EQ // 最終演算がゼロの場合指定アドレスにジャンプ
JUMP ImmAddr, OV // 最終演算がゼロの場合指定アドレスにジャンプ

内部では上記に展開されます。

M_BX(label_num)

ラベルに無条件でジャンプする命令です。JUMPのCマクロがBXなのには違和感がありますが、ARM系のアセンブラに合わせている命名なのでしょうか?

    M_BX(2),
    (省略)
    M_LABEL(2),

上記で、M_LABEL(2)にジャンプします。

M_BXZ(label_num)

最後の計算結果がゼロだった場合にジャンプする命令です。

    M_LABEL(1),
    I_SUBI(R0, R0, 1), // R0 = R0 - 1
    M_BXZ(2), // 直前が0だったらラベル2にジャンプ
    M_BX(1), // 無条件でラベル1にジャンプ
    (省略)
    M_LABEL(2),

上記でカウントダウンしていって、0になったところでジャンプする処理ができます。

M_BXF(label_num)

最後の計算結果がオーバーフローした場合にジャンプする命令です。

    M_LABEL(1),
    I_ADDI(R0, R0, 1), // R0 = R0 + 1
    M_BXZ(2), // 直前がオーバーフローだったらラベル2にジャンプ
    M_BX(1), // 無条件でラベル1にジャンプ
    (省略)
    M_LABEL(2),

上記でカウントアップしていって、オーバーフローになったところでジャンプする処理ができます。

I_BXR(reg_pc), I_BXZR(reg_pc), I_BXFR(reg_pc)

ラベルのかわりに、レジスタが指定するアドレスにジャンプする命令です。Cマクロからだとアドレスが特定しにくいため、あまり利用することはありません。

R0比較ジャンプ – JUMPR

R0レジスタの値と、しきい値を比べてジャンプを行う命令です。

JUMPR label, imm, LT // R0 < 100
JUMPR label, imm, GE // R0 >= 100

内部では上記に展開されます。

M_BL(label_num, imm_value)

R0がしきい値より小さい場合にジャンプする命令です。less thanで小さい場合。

  M_BL(1, 100), // IF R0 < 100 THAN GOTO M_LABEL(1)

上記でR0が100より小さいときにラベル1にジャンプします。

M_BGE(label_num, imm_value)

R0がしきい値と同じか、大きい場合にジャンプする命令です。greater than or equal toで大きいか同じ場合。

  M_BGE(1, 100), // IF R0 >= 100 THAN GOTO M_LABEL(1)

上記でR0が100か、100より大きいときにラベル1にジャンプします。

命令まとめ

命令アセンブリCマクロ処理
ノーオペレーションNOP
代入MOVEI_MOVI(reg_dest, imm_)
I_MOVR(reg_dest, reg_src)
reg_dest = imm_
reg_dest = reg_src
メモリロードLDI_LD(reg_dest, reg_addr, offset_)reg_dest = MEM[reg_addr + offset_]
メモリストアSTI_ST(reg_val, reg_addr, offset_)MEM[reg_addr + offset_] = reg_val
足し算ADDI_ADDI(reg_dest, reg_src, imm_)
I_ADDR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src + imm_
reg_dest = reg_src1 + reg_src2
引き算SUBI_SUBI(reg_dest, reg_src, imm_)
I_SUBR(reg_dest, reg_src1, reg_src2)
reg_dest = reg_src – imm_
reg_dest = reg_src1 – reg_src2
停止HALTI_HALT()
ラベルM_LABEL(label_num)
ウエイトWAITI_DELAY(cycles_)Delay cycles_
レジスタ書込REG_WRI_WR_REG(reg, low_bit, high_bit, val)REG_WR[reg][high_bit:low_bit] = val
ジャンプJUMPM_BX(label_num)
M_BXZ(label_num)
M_BXF(label_num)
GOTO label_num
IF zero_flag THAN GOTO label_num
IF overflow_flag THAN GOTO label_num
R0比較ジャンプJUMPRM_BL(label_num, imm_value)
M_BGE(label_num, imm_value)
IF R0 < imm_value THAN GOTO label_num
IF R0 >= imm_value THAN GOTO label_num

サンプルスケッチ

#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);
}

サンプルスケッチはESP32 ULP Debuggerに入っているスケッチ例のUlpBlinkです。

  // 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);

上記で、あらかじめ該当PINの初期設定を行っています。この処理もULPから行うこともできますが、一度しか呼び出さないのと、呼び出すアドレスを調べるのが大変なのでArduino側から初期化をしています。

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

ESP32 ULP Debuggerの出力は上記です。0000のDATA部分がLEDの点灯状況を保存している変数で、時間によって数値が変わり、それとともにSTをしたアドレスも変化していくのがわかります。

0000 : 00C30000 DATA     0				// ST ADDR:0x0006
0000 : 01430001 DATA     1				// ST ADDR:0x000A

上記が両方の状態ですが、書き込みアドレスが0x0006と0x000Aで変化しています。これによってどこで書き込まれたのかをデバッグすることが可能です。

プログラム的には変数の値をJUMPRを使って分岐しているぐらいで、複雑なことはしていません。I_DELAY()が大量に並んでいるもので、時間の調整を行っています。

本来はループにしてもう少しスマートに回したほうがきれいですが、説明しやすい方法を採用しています。

まとめ

ジャンプ命令は本来はM_LABEL()を利用せずに、相対アドレスを指定するIからはじまる命令もあります。しかしながらCマクロからだと使いにくいので、最終的に同じコードが出力されるMからはじまるマクロ命令のみを紹介しています。

ジャンプ命令だけで13種類ありますが、実質使うのは5種類だけで問題ないはずです。

本当はカウントだけするステージカウントレジスタというものがあり、そのカウントを使ってジャンプする命令もありますが、残念ながらCマクロでは実装されていません。

次回は四則演算あたりの命令を説明したいと思いますが、すでに事前に準備していたスケッチがなくなったのと、そろそろESP32 ULP Debuggerを修正しないといけない気がしますので時間があくかもしれません。

続編

コメント