概要
前回はジャンプを中心に説明をしました。今回はビット演算系を解説します。
ビット演算とは?
一般的なビット演算について、かんたんに解説します。
アンド – AND
数値1と数値2の各桁のビットを比べ、両方1である場合のみ1になります。
uint8_t c = a & b;
Arduinoでは上記のように表記します。利用例としては、ビットマスクと呼ばれる、必要な場所のビットを1、不要部分を0にした変数とANDすることにより、不要部分が0になる効果があります。
オア – OR
数値1と数値2の各桁のビットを比べ、片方でも1である場合に1になります。
uint8_t c = a | b;
Arduinoでは上記のように表記します。必要な場所だけ1にした変数をORすることによって、特定のビットを1にするときに使うことが多いです。
ノット – NOT
各桁のビットを、反転します。
uint8_t c = ~a;
Arduinoでは上記のように表記します。ビット反転のNOTはあまり利用しませんが、通信などで受け取ったデータが、アクティブLOWの場合などは反転して利用します。
エクスクルーシブオア – XOR
数値1と数値2の各桁のビットを比べ、違う値の場合に1になります。(=同じ値の場合には0になる)
uint8_t c = a ^ b;
Arduinoでは上記のように表記します。必要な場所だけ1にした変数をXORすることによって、特定のビットを反転するときに使うことが多いです。
左シフト – LEFT SHIFT
左に4ビットシフトした場合の結果です。ビットが移動して、空いた場所には0が入ります。
uint8_t c = a << 4;
Arduinoでは上記のように表記します。1バイト左シフトすることで、数値は2倍になります。そのため4バイトシフトすると16倍にすることができます。ただしオーバーフローと呼ばれる、左側にあった数字がなくなるので大きな数字を左シフトした場合には、必ずしも大きな数字になるわけではありません。
右シフト – RIGHT LEFT
右に4ビットシフトした場合の結果です。ビットが移動して、空いた場所には0が入ります。今回は触れませんが、本来は論理シフトと算術シフトがあり、マイナスの数値を扱う場合には2の補数という、表現方法を使うのでもう少し複雑になります。
uint8_t c = a >> 4;
Arduinoでは上記のように表記します。1バイト右シフトすることで、数値は半分になります。そのため4バイトシフトすると16分の1にすることができます。もともと右側にあったビットは捨てられてしまうので、もとの数字よりは精度が落ちてしまいます。
命令紹介
アンド – AND
I_ADDR(reg_dest, reg_src1, reg_src2)
reg_destにreg_src1とreg_src2をANDした結果を代入します。
I_ANDR(R2, R0, R1), // R2 = R0 & R1
I_ADDI(reg_dest, reg_src, imm_)
reg_destにreg_src1とimm_をANDした結果を代入します。
I_ANDI(R2, R0, 0x2222), // R2 = R0 & 0x2222
オア – OR
I_ORR(reg_dest, reg_src1, reg_src2)
reg_destにreg_src1とreg_src2をORした結果を代入します。
I_ORR(R2, R0, R1), // R2 = R0 | R1
I_ORI(reg_dest, reg_src, imm_)
reg_destにreg_src1とimm_をORした結果を代入します。
I_ORI(R2, R0, 0x1111), // R2 = R0 | 0x1111
左シフト – LSH
I_LSHR(reg_dest, reg_src, reg_shift)
reg_destにreg_src1を左にreg_src2ビットシフトした結果を代入します。
I_LSHR(R2, R0, R1), // R2 = R0 << R1
I_LSHI(reg_dest, reg_src, imm_)
reg_destにreg_src1を左にimm_ビットシフトした結果を代入します。
I_LSHI(R2, R0, 0x0008), // R2 = R0 << 0x0008
右シフト – RSH
I_RSHR(reg_dest, reg_src, reg_shift)
reg_destにreg_src1を右にreg_src2ビットシフトした結果を代入します。
I_RSHR(R2, R0, R1), // R2 = R0 >> R1
I_RSHI(reg_dest, reg_src, imm_)
reg_destにreg_src1を右にimm_ビットシフトした結果を代入します。
I_RSHI(R2, R0, 0x0008), // R2 = R0 >> 0x0008
ULPタイマー停止
I_END()
実際にはULPのアセンブリ言語の仕様ではなく、ULPのタイマーを停止して、次回起動を停止させます。
この命令だけだとULPプログラムは動いたままなので、最後にI_HALT()を呼び忘れないようにしましょう。
命令まとめ
命令 | アセンブリ | 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 |
レジスタ書込 | REG_WR | I_WR_REG(reg, low_bit, high_bit, val) | REG_WR[reg][high_bit:low_bit] = val |
ラベル | – | M_LABEL(label_num) | – |
ジャンプ | 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 |
停止 | HALT | I_HALT() | – |
ウエイト | WAIT | I_DELAY(cycles_) | Delay cycles_ |
アンド | AND | 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 |
オア | OR | I_ORI(reg_dest, reg_src,
imm_) I_ORR(reg_dest, reg_src1, reg_src2) | reg_dest = reg_src | imm_ reg_dest = reg_src1 | reg_src2 |
左シフト | LSH | I_LSHI(reg_dest, reg_src,
imm_) I_LSHR(reg_dest, reg_src, reg_shift) | reg_dest = reg_src <<
imm_ reg_dest = reg_src1 << reg_src2 |
右シフト | RSH | I_RSHI(reg_dest, reg_src,
imm_) I_RSHR(reg_dest, reg_src, reg_shift) | reg_dest = reg_src >>
imm_ reg_dest = reg_src1 >> reg_src2 |
ULPタイマー停止 | – | I_END() | – |
スケッチ例
#include "esp32/ulp.h" #include "driver/rtc_io.h" #include "UlpDebug.h" // Slow memory variable assignment enum { SLOW_BIT_DATA0, // Data SLOW_BIT_DATA1, // Data SLOW_BIT_DATA2, // Data SLOW_BIT_DATA3, // Data SLOW_BIT_DATA4, // Data SLOW_BIT_DATA5, // Data SLOW_BIT_DATA6, // Data SLOW_BIT_DATA7, // Data 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); // ULP Program const ulp_insn_t ulp_prog[] = { I_MOVI(R0, 0xAAAA), // R0 = 0x8888 I_MOVI(R1, 0x8888), // R1 = 0xAAAA I_MOVI(R3, SLOW_BIT_DATA0), // R3 = SLOW_BIT_DATA0 I_ANDR(R2, R0, R1), // R2 = R0 & R1 I_ST(R2, R3, 0), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+0] = R2 I_ANDI(R2, R0, 0x2222), // R2 = R0 & 0x2222 I_ST(R2, R3, 1), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+1] = R2 I_ORR(R2, R0, R1), // R2 = R0 | R1 I_ST(R2, R3, 2), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+2] = R2 I_ORI(R2, R0, 0x1111), // R2 = R0 | 0x1111 I_ST(R2, R3, 3), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+3] = R2 I_MOVI(R1, 0x0008), // R1 = 0x0008 I_LSHR(R2, R0, R1), // R2 = R0 << R1 I_ST(R2, R3, 4), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+4] = R2 I_LSHI(R2, R0, 0x0008), // R2 = R0 << 0x0008 I_ST(R2, R3, 5), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+5] = R2 I_RSHR(R2, R0, R1), // R2 = R0 >> R1 I_ST(R2, R3, 6), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+6] = R2 I_RSHI(R2, R0, 0x0008), // R2 = R0 >> 0x0008 I_ST(R2, R3, 7), // RTC_SLOW_MEM[R3(SLOW_BIT_DATA0)+7] = R2 I_END(), // Stop the program I_HALT() }; // 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); // Wait delay(1000); // For debug output ulpDump(); } void loop() { // Wait delay(1000); }
8個の変数を確保して、8種類の演算結果を保存しています。何度計算しても結果は同じなので、内部でI_END()を呼び出して、次回起動タイマーを停止させてから、I_HALT()でULPを終了させています。
実行結果
0000 : 01838888 DATA -30584 // ST ADDR:0x000C 0001 : 01C32222 DATA 8738 // ST ADDR:0x000E 0002 : 0203AAAA DATA -21846 // ST ADDR:0x0010 0003 : 0243BBBB DATA -17477 // ST ADDR:0x0012 0004 : 02A3AA00 DATA -22016 // ST ADDR:0x0015 0005 : 02E3AA00 DATA -22016 // ST ADDR:0x0017 0006 : 032300AA DATA 170 // ST ADDR:0x0019 0007 : 036300AA DATA 170 // ST ADDR:0x001B 0008 : 728AAAA0 PROG MOVE R0, -21846 // R0 = -21846 0009 : 72888881 PROG MOVE R1, -30584 // R1 = -30584 000A : 72800003 PROG MOVE R3, 0 // R3 = 0 000B : 70400012 PROG AND R2, R0, R1 // R2 = R0 & R1 000C : 6800000E PROG ST R2, R3, 0 // MEM[R3+0] = R2 000D : 72422222 PROG AND R2, R0, 8738 // R2 = R0 & 8738 000E : 6800040E PROG ST R2, R3, 1 // MEM[R3+1] = R2 000F : 70600012 PROG OR R2, R0, R1 // R2 = R0 | R1 0010 : 6800080E PROG ST R2, R3, 2 // MEM[R3+2] = R2 0011 : 72611112 PROG OR R2, R0, 4369 // R2 = R0 | 4369 0012 : 68000C0E PROG ST R2, R3, 3 // MEM[R3+3] = R2 0013 : 72800081 PROG MOVE R1, 8 // R1 = 8 0014 : 70A00012 PROG LSH R2, R0, R1 // R2 = R0 << R1 0015 : 6800100E PROG ST R2, R3, 4 // MEM[R3+4] = R2 0016 : 72A00082 PROG LSH R2, R0, 8 // R2 = R0 << 8 0017 : 6800140E PROG ST R2, R3, 5 // MEM[R3+5] = R2 0018 : 70C00012 PROG RSH R2, R0, R1 // R2 = R0 >> R1 0019 : 6800180E PROG ST R2, R3, 6 // MEM[R3+6] = R2 001A : 72C00082 PROG RSH R2, R0, 8 // R2 = R0 >> 8 001B : 68001C0E PROG ST R2, R3, 7 // MEM[R3+7] = R2 001C : 1C600006 PROG REG_WR 0x0006, 24, 24, 0 // RTC_CNTL[24:24] = 0 001D : B0000000 PROG HALT // HALT
ESP32 ULP Debuggerの表記が少しおかしいので、後日ライブラリを修正したいと思います。
0000 : 01838888 DATA -30584 // ST ADDR:0x000C
上記の場合には、-30584と表示されていますが、実際の保存されているのは0x01838888です。下4桁の0x8888がデータ部分で、上位4桁を分解するとSTで書き込まれたアドレスがわかるようになっています。
マイナスの値で表記していますが、マイナスかどうかはデータの扱い方次第になります。ADDやSUBに関しては数値を指定した場合にはマイナスを入力できます。それ以外の場所では符号なし扱いが多いのですが、今度ESP32 ULP Debuggerの修正とともに研究しなおしたいと思います。
まとめ
ビット演算は重要な概念ですが、ULPではほとんど使わないと思います。使うとしてもレジスタが4つしかないので、レジスタではなく数値指定がほとんどだと思います。
NOTやXORなどの一般的なビット演算命令も搭載していませんので、ビット演算があやふやな人は検索などをして、調べてみてください。
次回は残ったCマクロ命令を解説する予定です。ただしCマクロで利用できない命令群が残っていますので、そちらはESP32 ULP Debuggerなどで宣言をして、使えるようにしてから紹介する予定です。
コメント