ESP32のULPアセンブリ言語入門 その3 演算と基本命令

概要

前回は環境構築まで行いました。今回は命令の説明と、プログラムを実際に動かしてみたいと思います。

ULPプログラミングの方式

ULPのプログラムには2つの方式があります。今回はCマクロ方式で行いますが、アセンブリ方式も説明したいと思います。

アセンブリ方式

環境構築が難しいので今回採用していません。アセンブリ言語をアセンブラを使ってアセンブルします。一般的なアセンブリ言語の開発方法ですがESP32の場合にはちょっと使うのがむずかしいです。

ESP32 ULP Debuggerでデバッグ出力をした場合に表示されるのは、逆アセンブラしたアセンブリ言語になります。内部ではこの状態で動いています。

Cマクロ方式

Arduino IDEではC言語のコンパイルはできますが、ULPアセンブリはそのままでは利用できません。そのためC言語のマクロ形式でプログラミングすることができる環境が準備されています。

ただし、すべての命令がCマクロ方式で準備されているわけではないので、現状は利用できない命令があります。

ULPコプロセッサの構造

ULPでは変数となる汎用レジスタがR0、R1、R2、R3の4つあります。基本的に命令は汎用レジスタにしたいして実行するので、この4つの汎用レジスタと上手に使う必要があります。

計算などをする場合にはスローメモリなどの変数から汎用レジスタに代入し、計算後にスローメモリに計算結果を戻すなどの処理を行う必要があります。

また、汎用レジスタ以外にもループなどに利用できる8ビットカウンターレジスタもありますが、Cマクロ方式だと利用することができません。

命令紹介

今回使う命令をかんたんに紹介します。

ノーオペレーション – NOP

何もしない命令です。ほぼすべてのプロセッサで0を指定した場合には、なにもしない命令になっているはずです。

一番代表的な命令ですが、Cマクロだと定義されていませんので利用することができません。ESP32 ULP Debugger側で今度マクロを追加してみたいと思います。

代入 – MOVE

汎用レジスタに代入をする命令です。Cマクロの場合には代入する値によって2つに分かれています。

I_MOVI(reg_dest, imm_)

最後がIのものは引数がimm_になっており、符号付き16ビッチの整数(int16_t)を汎用レジスタに代入するCマクロです。

I_MOVI(R0, 0),  // R0 = 0

上記の命令で、R0レジスタに0を代入しています。

I_MOVR(reg_dest, reg_src)

最後がRのものは引数が両方汎用レジスタで、汎用レジスタ同士の代入のCマクロです。

I_MOVI(R0, R1),  // R0 = R1

上記の命令で、R0レジスタにR1レジスタの内容を代入しています。

メモリロード – LD

汎用レジスタにスローメモリの内容を代入する命令です。書式は1種類のみです。

I_LD(reg_dest, reg_addr, offset_)

dest汎用レジスタにsrc汎用レジスタのアドレス+offset_の内容を代入します。

I_LD(R0, R3, 0), // R0 = RTC_SLOW_MEM[R3+0]
I_LD(R1, R3, 1), // R1 = RTC_SLOW_MEM[R3+1]
I_LD(R2, R3, 2), // R2 = RTC_SLOW_MEM[R3+2]

上記にように利用します。あらかじめ汎用レジスタにスローメモリの配列番号を入れておき、その値をdestの汎用レジスタに代入します。

オフセットも引数に持っているので、連続したデータなどを代入する場合にR3などのアドレスを保存した汎用レジスタを操作することなく、指定することができます。

オフセットはわかりにくいので、なれていないうちはオフセットは0固定で、I_MOVI()で取得元アドレスを設定してから、I_LD()を呼び出したほうが安全だと思います。

メモリストア – ST

汎用レジスタからスローメモリに内容を代入する命令です。書式は1種類のみです。

I_ST(reg_val, reg_addr, offset_)

LDとほぼ同じ書式なのですが、STの場合には左側から右側に代入をしますので注意してください。

I_ST(R0, R3, 0),  // RTC_SLOW_MEM[R3+0] = R0
I_ST(R1, R3, 0),  // RTC_SLOW_MEM[R3+1] = R1

上記のように最初のレジスタの内容を、真ん中のレジスタが指定するアドレスに保存します。データの方向性がここだけ違うので注意してください。

足し算 – ADD

汎用レジスタの足し算をする命令です。こちらも2種類に分かれています。

I_ADDI(reg_dest, reg_src, imm_)

Iがついているので数値指定の命令です。

I_ADDI(R1, R0, 1)  // R1 = R0 + 1
I_ADDI(R2, R2, -1)  // R2 = R2 + (-1)

上記のように汎用レジスタに数値を足しています。符号付きですのでマイナスを指定することもできます。

I_ADDR(reg_dest, reg_src1, reg_src2)

汎用レジスタ同士の足し算です。

I_ADDR(R0, R1, R2)  // R0 = R1 + R2

こちらはシンプルです。

引き算 – SUB

ほぼ足し算と同じ構造です。

I_SUBI(reg_dest, reg_src, imm_)

Iがついているので数値指定の命令です。

I_SUBI(R0, R0, 1)  // R0 = R0 - 1

I_SUBR(reg_dest, reg_src1, reg_src2)

汎用レジスタ同士の引き算です。

I_SUBR(R0, R0, R1)  // R0 = R0 - R1

停止 – HALT

ULPを停止する命令です。プログラムの最後には必ず追加してください。

I_HALT()

Cマクロも引数はありません。

命令まとめ

命令アセンブリ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()

サンプルスケッチ

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "UlpDebug.h"

// Slow memory variable assignment
enum {
  SLOW_COUNT,           // Counter
  SLOW_COUNT2,          // Counter2

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

  // Init
  RTC_SLOW_MEM[SLOW_COUNT] = 0;
  RTC_SLOW_MEM[SLOW_COUNT2] = 0;

  // ULP Program
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_COUNT),                 // R3 = SLOW_COUNT
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_COUNT)]
    I_LD(R1, R3, 1),                        // R1 = RTC_SLOW_MEM[R3(SLOW_COUNT)+1]
    I_ADDI(R0, R0, 1),                      // R0 = R0 + 1
    I_SUBI(R1, R1, 1),                      // R1 = R1 - 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)] = R0
    I_ST(R1, R3, 1),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)+1] = R1
        
    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);
  delay(50);

  // Execute ULP program at 1000ms intervals
  ULP_BLINK(1000000);
}

void loop() {
  // For debug output
  ulpDump();

  // Wait
  delay(1000);
}

ここまで紹介した命令を組み合わせたスケッチになります。

抜粋

enum {
  SLOW_COUNT,           // Counter
  SLOW_COUNT2,          // Counter2

  SLOW_PROG_ADDR        // Program start address
};

変数定義です。2つのカウンターを準備しています。

  // Init
  RTC_SLOW_MEM[SLOW_COUNT] = 0;
  RTC_SLOW_MEM[SLOW_COUNT2] = 0;

初期値は両方とも0です。

  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_COUNT),                 // R3 = SLOW_COUNT
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_COUNT)]
    I_LD(R1, R3, 1),                        // R1 = RTC_SLOW_MEM[R3(SLOW_COUNT)+1]
    I_ADDI(R0, R0, 1),                      // R0 = R0 + 1
    I_SUBI(R1, R1, 1),                      // R1 = R1 - 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)] = R0
    I_ST(R1, R3, 1),                        // RTC_SLOW_MEM[R3(SLOW_COUNT)+1] = R1
        
    I_HALT()                                // Stop the program
  };

R3にカウンターのアドレスを代入して、R0にカウンターをロード、R1にはオフセット1しているのでカウンター2をロードしています。

その後R0は1を足して、R1は1を引いています。

その後スローメモリにカウンターとカウンター2をストアして保存しています。

実行結果

====================================
0000 : 00E30001 DATA     1				// ST ADDR:0x0007
0001 : 0103FFFF DATA    -1				// ST ADDR:0x0008
0002 : 72800003 PROG MOVE   R3,   0			// R3 = 0
0003 : D000000C PROG LD     R0, R3,   0			// R0 = MEM[R3+0]
0004 : D000040D PROG LD     R1, R3,   1			// R1 = MEM[R3+1]
0005 : 72000010 PROG ADD    R0, R0,   1			// R0 = R0 + 1
0006 : 72200015 PROG SUB    R1, R1,   1			// R1 = R1 - 1
0007 : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
0008 : 6800040D PROG ST     R1, R3,   1			// MEM[R3+1] = R1
0009 : B0000000 PROG HALT				// HALT
====================================
0000 : 00E30002 DATA     2				// ST ADDR:0x0007
0001 : 0103FFFE DATA    -2				// ST ADDR:0x0008
0002 : 72800003 PROG MOVE   R3,   0			// R3 = 0
0003 : D000000C PROG LD     R0, R3,   0			// R0 = MEM[R3+0]
0004 : D000040D PROG LD     R1, R3,   1			// R1 = MEM[R3+1]
0005 : 72000010 PROG ADD    R0, R0,   1			// R0 = R0 + 1
0006 : 72200015 PROG SUB    R1, R1,   1			// R1 = R1 - 1
0007 : 6800000C PROG ST     R0, R3,   0			// MEM[R3+0] = R0
0008 : 6800040D PROG ST     R1, R3,   1			// MEM[R3+1] = R1
0009 : B0000000 PROG HALT				// HALT

シリアルモニタを開くと、上記のような出力がされます。1秒間隔で一番上のDATAの2列の数字が変わっていくのがわかると思います。

一番左の0000からはじまるのがスローメモリの配列番号です。16進数で0から始まって07ff(10進で2047)まであります。デバッグ出力の場合には、NOPが複数続いていたらそれ以降のデータは出力しないようにしています。

その次の00E30001などの8桁が、スローメモリに保存されているデータになります。16進数で8桁ですので32ビットになります。この数値を解析したのが、これより右側にかかれているものになります。

数値自体は理解する必要はありませんので、DATAかPROGより右側の情報だけ見てください。DATAの場合には入っている数値になります。ここの中身は符号ありの16ビット(int16_t)ですので-32768から32767の数値になります。

DATAの場合一番右側にADDRと書かれています。これはこのデータを保存した命令が実行されたアドレスになります。0x0007と0x0008がありますが、そのアドレスにあるSTからスローメモリに保存されたことがわかります。

複数の場所からスローメモリを変更する場合に、どこから書き込まれた数値なのかを判別することができます。

PROGの場合にはアセンブリ言語で表示されています。//以下はC言語風の簡易的なコメントになります。ESP32 ULP Debuggerは汎用的なツールとして作ったので、C言語マクロ方式ではなく、アセンブリ方式での出力になってしまっています。

まとめ

やっとプログラミングまでたどり着きました。自作ライブラリのESP32 ULP Debuggerはまだおかしなところがあるので、手を入れていく予定です。このブログを書きながらマイナスの数値がおかしかったので修正を入れています。。。

次回はLチカを行う予定です。

続編

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。