概要
前回は初歩的な命令までやりましたので、今回は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_GPIO | GPIO | Bit | Arduinoでの推奨Bit記述方法 |
RTC_GPIO0 | GPIO36 | 14 | RTCIO_GPIO36_CHANNEL + 14 |
RTC_GPIO1 | GPIO37 | 15 | RTCIO_GPIO37_CHANNEL + 14 |
RTC_GPIO2 | GPIO38 | 16 | RTCIO_GPIO38_CHANNEL + 14 |
RTC_GPIO3 | GPIO39 | 17 | RTCIO_GPIO39_CHANNEL + 14 |
RTC_GPIO4 | GPIO34 | 18 | RTCIO_GPIO34_CHANNEL + 14 |
RTC_GPIO5 | GPIO35 | 19 | RTCIO_GPIO35_CHANNEL + 14 |
RTC_GPIO6 | GPIO25 | 20 | RTCIO_GPIO25_CHANNEL + 14 |
RTC_GPIO7 | GPIO26 | 21 | RTCIO_GPIO26_CHANNEL + 14 |
RTC_GPIO8 | GPIO33 | 22 | RTCIO_GPIO33_CHANNEL + 14 |
RTC_GPIO9 | GPIO32 | 23 | RTCIO_GPIO32_CHANNEL + 14 |
RTC_GPIO10 | GPIO04 | 24 | RTCIO_GPIO04_CHANNEL + 14 |
RTC_GPIO11 | GPIO00 | 25 | RTCIO_GPIO00_CHANNEL + 14 |
RTC_GPIO12 | GPIO02 | 26 | RTCIO_GPIO02_CHANNEL + 14 |
RTC_GPIO13 | GPIO15 | 27 | RTCIO_GPIO15_CHANNEL + 14 |
RTC_GPIO14 | GPIO13 | 28 | RTCIO_GPIO13_CHANNEL + 14 |
RTC_GPIO15 | GPIO12 | 29 | RTCIO_GPIO12_CHANNEL + 14 |
RTC_GPIO16 | GPIO14 | 30 | RTCIO_GPIO14_CHANNEL + 14 |
RTC_GPIO17 | GPIO27 | 31 | RTCIO_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が入っているので注意してください。
事前準備
- ESP32ボード(M5StickCやESP32 DevKitCなど)
- 抵抗入りLED
上記では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 | – | – |
代入 | MOVE | I_MOVI(reg_dest, imm_) I_MOVR(reg_dest, reg_src) | reg_dest = imm_ reg_dest = reg_src |
メモリロード | LD | I_LD(reg_dest, reg_addr, offset_) | reg_dest = MEM[reg_addr + offset_] |
メモリストア | ST | I_ST(reg_val, reg_addr, offset_) | MEM[reg_addr + offset_] = reg_val |
足し算 | ADD | I_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 |
引き算 | SUB | I_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 |
停止 | HALT | I_HALT() | – |
ラベル | – | M_LABEL(label_num) | – |
ウエイト | WAIT | I_DELAY(cycles_) | Delay cycles_ |
レジスタ書込 | REG_WR | I_WR_REG(reg, low_bit, high_bit, val) | REG_WR[reg][high_bit:low_bit] = val |
ジャンプ | JUMP | M_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比較ジャンプ | JUMPR | M_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を修正しないといけない気がしますので時間があくかもしれません。
コメント