概要
前回は環境構築まで行いました。今回は命令の説明と、プログラムを実際に動かしてみたいと思います。
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 | – | – |
代入 | 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() | – |
サンプルスケッチ
#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チカを行う予定です。
コメント