ESP32のULPアセンブリ言語入門 その5 ビット演算

概要

前回はジャンプを中心に説明をしました。今回はビット演算系を解説します。

ビット演算とは?

一般的なビット演算について、かんたんに解説します。

アンド – 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
代入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
レジスタ書込REG_WRI_WR_REG(reg, low_bit, high_bit, val)REG_WR[reg][high_bit:low_bit] = val
ラベルM_LABEL(label_num)
ジャンプ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
停止HALTI_HALT()
ウエイトWAITI_DELAY(cycles_)Delay cycles_
アンドANDI_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
オアORI_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
左シフトLSHI_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
右シフトRSHI_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などで宣言をして、使えるようにしてから紹介する予定です。

続編

コメント