├── README.md ├── doc ├── esp32_asm_guide.md ├── step_01_01.md └── step_01_02.md ├── platformio.ini └── src ├── asm.S └── main.cpp /README.md: -------------------------------------------------------------------------------- 1 | # ESP32S3 VSCode+PlatfotmIO アセンブラ サンプル 2 | 3 | - デバッガが動作しない場合は Zadig や UsbDriverToolを使って各自調整する。 4 | - USB ID 303A / 1001 / 02 のデバイスを見つけて、ドライバを変更。 5 | - WinUSB (v6.1.7600.16385) または libusb に置き換えて再度試す。 6 | 7 | -------------------------------------------------------------------------------- /doc/esp32_asm_guide.md: -------------------------------------------------------------------------------- 1 | # ESP32 ( 含む S2, S3 ) アセンブラの書き方ガイド 2 | ※ ESP32-C3などの、コアが Xtensa ではないものは本記事の対象から外れます。 3 | 4 | 本記事の想定読者 5 | - ArduinoIDE または VSCode + PlatformIO で ESP32 用のC/C++プログラムを記述し実行できる。 6 | - アセンブラというものがあることは何となく知っているが、具体的に書いたことはない。 7 | 8 | ### 公式情報の入手 9 | 10 | - ESP32のコアは Xtensa LX6 、 ESP32-S2とESP32-S3のコアは Xtensa LX7 です。 11 | - アセンブラで使用できる命令の詳細を知るためには Xtensa の公式資料を探して下さい。 12 | - Google等で「Xtensa Instruction Set Architecture」と検索し、`Overview` や `Reference Manual` の PDF を見つけて下さい。 13 | - 公式資料の企業名が複数存在しますが、ケイデンス「cadence」がテンシリカ「tensilica」を買収したためです。 14 | 15 | ### 記事一覧 16 | 17 | #### step.1 インライン・アセンブラを書いてみよう 18 | 19 | [step.1-1 : とりあえずインライン・アセンブラを書いてみよう](step_01_01.md) 20 | 21 | [step.1-2 : 複数行のインライン・アセンブラを書いてみよう](step_01_02.md) 22 | 23 | [step.1-3 : インライン・アセンブラのみの関数を書いてみよう](step_01_03.md) 24 | 25 | #### step.2 アセンブラコードを .S ファイルに分けてみよう 26 | 27 | #### step.3 C/C++のコンパイル結果を参考にしてみよう 28 | 29 | Compiler explorerを使ってみる 30 | PlatformIO のデバッガで Disassemble 機能を使ってみる 31 | 32 | -------------------------------------------------------------------------------- /doc/step_01_01.md: -------------------------------------------------------------------------------- 1 | ## step.1-1 : とりあえずインライン・アセンブラを書いてみよう 2 | 3 | 実際に実行可能なアセンブラを含んだ簡単なコードを書いてみます。 4 | 5 | ### ソースコード 6 | ``` 7 | #include 8 | 9 | void setup(void) 10 | { delay(1000); } 11 | 12 | void loop(void) 13 | { 14 | delay(1000); 15 | printf("loop:\n"); 16 | 17 | for (int32_t y = 0; y < 5; ++y) { 18 | for (int32_t x = 0; x < 5; ++x) { 19 | int32_t val; 20 | __asm__ ( // インライン・アセンブラの記述開始 21 | "add %0, %1, %2 \n" // アセンブラコードを含む文字列 val = x + y; 22 | : "=r" ( val ) // output-list: 変数への出力: val = %0 となる 23 | : "r" ( x ), "r" ( y ) // input-list: 変数から入力: %1=x、%2=y となる 24 | : // clobber-list: この例では使用しない 25 | ); // インライン・アセンブラの記述終了 26 | printf(" %d + %d = %d", x, y, val); 27 | } 28 | printf("\n"); 29 | } 30 | } 31 | ``` 32 | 33 | ### 実行結果 34 | ``` 35 | loop: 36 | 0 + 0 = 0 1 + 0 = 1 2 + 0 = 2 3 + 0 = 3 4 + 0 = 4 37 | 0 + 1 = 1 1 + 1 = 2 2 + 1 = 3 3 + 1 = 4 4 + 1 = 5 38 | 0 + 2 = 2 1 + 2 = 3 2 + 2 = 4 3 + 2 = 5 4 + 2 = 6 39 | 0 + 3 = 3 1 + 3 = 4 2 + 3 = 5 3 + 3 = 6 4 + 3 = 7 40 | 0 + 4 = 4 1 + 4 = 5 2 + 4 = 6 3 + 4 = 7 4 + 4 = 8 41 | ``` 42 | 43 | ### 概要説明 44 | この例はC/C++のソースコード内にアセンブラコードを `__asm__ ();` のカッコで囲んで埋め込む「インライン・アセンブラ」と呼ばれる記述方法になります。 45 | この例でのアセンブラの内容は、C/C++では ` int32_t val = x + y; ` と記述できる内容です。 46 | 二重のforループの中で val=x+y を実行した結果を printf で標準出力に表示するコードです。 47 | ``` 48 | __asm__ ( // インライン・アセンブラの記述開始 49 | "add %0, %1, %2 \n" // アセンブラコードを含む文字列 val = x + y; 50 | : "=r" ( val ) // output-list: 変数への出力: val = %0 となる 51 | : "r" ( x ), "r" ( y ) // input-list: 変数から入力: %1=x、%2=y となる 52 | : // clobber-list: この例では使用しない 53 | ); // インライン・アセンブラの記述終了 54 | ``` 55 | この例では `add` の行のみがアセンブラで、続く3行は C/C++とアセンブラとの値の受け渡しのための記述です。 56 | アセンブラコード内にC/C++の変数名をそのまま記述することはできないため、こういった記述が必要になります。 57 | 58 | ### C/C++の変数とアセンブラの汎用レジスタ 59 | C/C++の世界での変数の役割を果たすものは、アセンブラの世界では一般に「汎用レジスタ(general register)」と呼ばれます。 60 | Xtensa ではこれを「アドレスレジスタ」と呼称しており、32bitの値を扱える `a0` ~ `a15` の合計16個が用意されています。 61 | int32_t,uint32_t型として使える変数のようなものが16個ある、とイメージして頂ければ良いと思います。 62 | 63 | ### output-list / input-list / clobber-list 64 | アセンブラコードの文字列の後に `:` コロンで区切って、`output-list`、`input-list`、`clobber-list` と呼ばれる記述をします。 65 | 各リストはカンマで区切って複数記述できます。使用しない場合は `:` コロンのみ記述して中身を省略してもよいです。 66 | `output-list` 、 `input-list` に記述した変数は、`%0` から順に番号が割当てられ、アセンブラコード内に記述が可能になります。 67 | 今回の例では `val` が `%0` 、 `x` が `%1` 、`y` が `%2` になります。 68 | 実際にはコンパイラが `a0` ~ `a15` の中からどれかを選択してコンパイル時に置き換えて処理しています。 69 | どのレジスタが使用されるかはコンパイラ任せとなります。 70 | 71 | アドレスレジスタの個数は16個ですから、インライン・アセンブラとの受渡しに使える変数の個数にも上限があります。 72 | インライン・アセンブラの外側の処理の都合もあるため、16個すべてをC/C++との受け渡しに使用することはできませんから、なるべく個数を少なく抑えることが望ましいです。 73 | 74 | `clobber-list` は今回の例では使用していませんが、アセンブラコード内で値を変えたレジスタがある場合に、そのことをコンパイラに知らせるために使います。 75 | 76 | ### アセンブラコード部 77 | 今回の例では1行だけですが、`add` の行がアセンブラコードの記述になります。 78 | `add` 命令には続けて3つのアドレスレジスタの記述が必要で、後ろ2つのアドレスレジスタを合算した値が、1つめのアドレスレジスタに代入されます。 79 | 今回の例では `add %0,%1,%2` ですから、`%0 = %1 + %2` という意味になります。`output-list` `input-list` の記述により、`%0` は `val` 、 `%1` は `x` 、 `%2` は `y` ですから、`val = x + y` という動作になります。 80 | 81 | ### 複数行のアセンブラコードの記述 82 | アセンブラの記述は基本的には 1行に 1つの命令を書き、改行で区切って次の命令を記述します。 83 | インライン・アセンブラではC/C++のコード内に文字列の形で書込むため、各行の文字列の末尾に `\n` を置いて改行とするのが良いでしょう。 84 | 85 |
86 | 今回はここまでです。C/C++のソースコード内にアセンブラコードを書込む方法の紹介でした。 87 | -------------------------------------------------------------------------------- /doc/step_01_02.md: -------------------------------------------------------------------------------- 1 | ## step.1-2 : 複数行のインライン・アセンブラを書いてみよう 2 | 3 | step.1-1の例は短すぎたので、今回は複数行を含むアセンブラコードを書いてみます。 4 | 5 | ### ソースコード 6 | ``` 7 | #include 8 | 9 | void setup(void) 10 | { delay(1000); } 11 | 12 | void loop(void) 13 | { 14 | delay(1000); 15 | printf("loop:\n"); 16 | 17 | uint32_t array_size = 6; // 配列の要素数 18 | int32_t src[array_size]; // 元データの配列 19 | int32_t dst[array_size]; // 結果データの配列 20 | 21 | for (int x = 0; x < array_size; ++x) { 22 | src[x] = x; // 元データの準備 23 | } 24 | 25 | for (int y = 0; y < 6; ++y) { 26 | __asm__ ( 27 | " loop %3, LABEL_LOOP \n" // 次の行からLABEL_LOOPの前の行まで array_size の回数ループする 28 | " l32i a15,%1, 0 \n" // a15 = src[0]; 元データ 4Byte読取り 29 | " add a14,a15,%2 \n" // a14 = a15 + %2 a14の中身は y と元データ配列の値の合計値になる 30 | " s32i a14,%0, 0 \n" // dst[0] = a14; 結果データの配列に4Byte保存 31 | " addi %1, %1, 4 \n" // %1 += 4; 元データのアドレスを 4 進める 32 | " addi %0, %0, 4 \n" // %0 += 4; 結果データのアドレスを 4 進める 33 | "LABEL_LOOP: \n" // ラベル。この前の行がループの終端となる 34 | : // output-list この例では不使用 35 | : // input-list レジスタに値を渡す変数を列挙 36 | "r" (dst), // %0 = dst; 結果データの配列アドレス 37 | "r" (src), // %1 = src; 元データの配列アドレス 38 | "r" (y), // %2 = y; 加算する値 39 | "r" (array_size) // %3 = array_size; 40 | : // clobber-list 使用したレジスタを列挙 41 | "a14","a15" // a14とa15の値が変わった事をコンパイラに知らせる 42 | ); 43 | 44 | for (int x = 0; x < array_size; ++x) { // 結果をprintfで出力 45 | printf(" %d + %d = %d", src[x], y, dst[x]); 46 | } 47 | printf("\n"); 48 | } 49 | } 50 | ``` 51 | 52 | ### 実行結果 53 | ``` 54 | loop: 55 | 0 + 0 = 0 1 + 0 = 1 2 + 0 = 2 3 + 0 = 3 4 + 0 = 4 5 + 0 = 5 56 | 0 + 1 = 1 1 + 1 = 2 2 + 1 = 3 3 + 1 = 4 4 + 1 = 5 5 + 1 = 6 57 | 0 + 2 = 2 1 + 2 = 3 2 + 2 = 4 3 + 2 = 5 4 + 2 = 6 5 + 2 = 7 58 | 0 + 3 = 3 1 + 3 = 4 2 + 3 = 5 3 + 3 = 6 4 + 3 = 7 5 + 3 = 8 59 | 0 + 4 = 4 1 + 4 = 5 2 + 4 = 6 3 + 4 = 7 4 + 4 = 8 5 + 4 = 9 60 | 0 + 5 = 5 1 + 5 = 6 2 + 5 = 7 3 + 5 = 8 4 + 5 = 9 5 + 5 = 10 61 | ``` 62 | 63 | ### 概要説明 64 | 内容的には前回と同じく単純な加算処理ですが、今回はアセンブラ側でもループ処理を行います。 65 | C/C++側で配列を2つ用意しておき、`src` は元データ、`dst` は結果データとして使用します。 66 | アセンブラに渡す変数は、`dst` のアドレス・`src` のアドレス・加算する値・配列のサイズです。 67 | `src` 配列から読み出した値に加算した結果を `dst` 配列に保存します。 68 | 69 | 70 | ### clobber-listの記述とアドレスレジスタの使用 71 | 今回の例では、アドレスレジスタ `a14` `a15` を使用しています。 72 | これらのレジスタの値を変えたことをコンパイラに知らせるため、 `clobber-list` に記述が必要です。 73 | ``` 74 | __asm__ ( 75 | ~~ 省略 ~~ 76 | : // output-list 77 | : // input-list 78 | ~~ 省略 ~~ 79 | : // clobber-list 使用したレジスタを列挙 80 | "a14","a15" // a14とa15の値が変わった事をコンパイラに知らせる 81 | ``` 82 | これを怠ると、アセンブラコードを実行した後の処理が正常に動作しなくなる可能性があります。 83 | 84 | なお、アドレスレジスタは16個あるので、わかりやすく若い番号 `a0` 側から順に使おう…と考えてしまうところですが、現時点ではひとまず、`a15` 側から逆順に使用した方が良い、と考えてください。 85 | 詳細は別の機会に説明したいと思いますが、 `a0` と `a1` には特別な役割があり、 `clobber-list`に記述しても、値を書き換えた場合 正しく動作しなくなります。`a2` 以降は `clobber-list` に記述すれば正しく動作しますが、若い番号の方は C/C++側で既に使用中の可能性が高く、コンパイラの調整によって余分な迂回処理が追加される可能性があります。 86 | 87 | 他の手段として、`input-list` にダミーの変数を追加しておき、`%4` 等の代替記述を用いることで、どのアドレスレジスタを使用するかをコンパイラに任せる方法もあります。 88 | 89 | 90 | ### アセンブラコード部 91 | 今回の例はラベルの行を含めて7行あります。 1行に 1つの命令を書き、改行で区切って次の命令を記述します。 92 | 各行末尾のコメントから大体の処理の流れはご理解頂けると思いますが、順に追って説明していきます。 93 | 94 | #### loop命令とラベルによるループ処理 95 | 今回の例では、ループ処理を行う `loop` 命令を使用しています。 96 | 他のCPUのアセンブラを知っている人は、分岐命令と回数カウンタの減算(または加算)を組み合わせる方法をご存知かも知れません。ESP32も同じ方法を使うことができますが、`loop` 命令の方が動作が速く、記述が簡単なため、今回はこれを使用しています。 97 | 98 | `loop` 命令には続けて1つのアドレスレジスタと、ループ終端位置の記述が必要です。アドレスレジスタに入っている値の回数だけループします。 99 | 今回の例では `loop %3, LABEL_LOOP` です。 `input-list` の記述により `%3` は `array_size` 、C/C++側の記述で `array_size = 6;` ですから、6回ループします。 ループの範囲は `loop` 命令の次の行から `LABEL_LOOP:` の前の行まで、となります。 100 | ``` 101 | uint32_t array_size = 6; // 配列の要素数 102 | ~~ 省略 ~~ 103 | __asm__ ( 104 | " loop %3, LABEL_LOOP \n" // ループ開始。この次の行からLABEL_LOOPの前の行まで array_size の回数ループする 105 | ~~ 省略 ~~ 106 | "LABEL_LOOP: \n" // ラベル。先頭のloopでこれを指定しているので、この前の行がループの終端となる 107 | : // input-list レジスタに値を渡す変数を列挙 108 | "r" (dst), // %0 = dst; 結果データの配列アドレス 109 | "r" (src), // %1 = src; 元データの配列アドレス 110 | "r" (y), // %2 = y; 加算する値 111 | "r" (array_size) // %3 = array_size; 112 | ~~ 省略 ~~ 113 | ); 114 | ``` 115 | なお `loop` 命令を複数使ったネストできません。詳細は別の機会に説明したいと思いますが、`loop` 命令専用の特殊レジスタが(アドレスレジスタとは別に)存在しており、`loop` 命令を実行すると、この特殊レジスタの値が更新されます。そのためネストさせた場合は外側の `loop` が無効化し、内側の `loop` のみが動作します。 116 | 117 | `loop` 命令に使用したアドレスレジスタは、ループ内で別の用途に使用して値を書き換えても構いません。ループカウンタの役割として使われそうに見えるかも知れませんが、実際には `loop` 命令専用の特殊レジスタにループ回数が記録されるため、アドレスレジスタは最初にループ回数を知らせる目的でのみ使用されています。 118 | 119 | #### l32i命令による 4Byte読み出し 120 | C/C++側から受け取った `src` 配列から値を読み出すために、 `l32i` 命令を使用しています。メモリからアドレスレジスタに読み出す命令を、ロード命令と言います。 121 | `l32i` 命令には続けて2つのアドレスレジスタと、アドレスのオフセット量の記述が必要です。 122 | 2つめのアドレスレジスタの値にオフセット量を加えたアドレスのメモリから 4Byte 読取り、結果が 1つめのアドレスレジスタに代入されます。 123 | 今回の例では `l32i a15,%1,0` です。 `input-list` により `%1` には `src` 配列のアドレスが入っています。オフセット量は `0` ですから、`src` の指し示すアドレス +0 の位置のメモリから 4Byte 読取って `a15` に代入されます。 124 | 125 | #### add命令による加算 126 | 前回の例でも使用した `add` 命令を使い、今回も加算を行います。 127 | 今回の例では `add a14,a15,%2` 、`input-list` により `%2` は `y` ですから、 `a14 = a15 + y` となります。 128 | なお 1つめのアドレスレジスタも `a15` にして `add a15,a15,%2` としても構いません。今回の例では `clobber-list` に複数のレジスタ記述を例示したかったので、敢えて `a14` に結果を入れましたが、実際の場面では、使用するレジスタを削減するため `a15` に結果を入れたほうが良いでしょう。 129 | 130 | #### s32i命令による 4Byte書き込み 131 | C/C++側から受け取った `dst` 配列に値を書き込むために、 `s32i` 命令を使用しています。アドレスレジスタからメモリに書き込む命令を、ストア命令と言います。 132 | `s32i` 命令には続けて2つのアドレスレジスタと、アドレスのオフセット量の記述が必要です。 133 | 2つめのアドレスレジスタの値にオフセット量を加えたアドレスから 4Byte の範囲のメモリに、1つめのアドレスレジスタの値が書き込まれます。 134 | 今回の例では `s32i a14,%0,0` です。 `input-list` により `%0` には `dst` 配列のアドレスが入っています。オフセット量は `0` ですから、`dst` の指し示すアドレス +0 から4Byteの範囲のメモリに `a14` の値が書き込まれます。 135 | 136 | #### addi命令による固定値の加算 137 | ループ中に `%0` と `%1` のアドレスを進めるために `addi` 命令を使用しています。 138 | `addi` 命令には続けて2つのアドレスレジスタと、加算する値の記述が必要です。2つめのアドレスレジスタに加算した結果が、1つめのアドレスレジスタに代入されます。 139 | 今回の例では `addi %0, %0, 4` と `addi %1, %1, 4` ですから、`%0 = %0 + 4` と `%1 = %1 + 4` となります。 140 | C/C++的には `src ++` や `dst += 1` と書ける内容ですが、各配列の中身は `int32_t` 型ですから、配列の各要素は 4Byteあります。アセンブラコード側では C/C++側の型情報の影響を受けないため、4Byte単位でアドレスが進むように、`%0` と `%1` は `+4` して4Byte単位で進むようにしておく必要があります。 141 | 142 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32-s3-devkitc-1] 12 | platform = espressif32 13 | board = esp32-s3-devkitc-1 14 | board_build.mcu = esp32s3 15 | board_build.f_cpu = 240000000L 16 | board_build.f_flash = 80000000L 17 | framework = arduino 18 | build_type = debug 19 | debug_speed = 12000 ; 12000[kHz] つまり12MHzの設定.速すぎるとデータが化けることがあるらしい? 20 | debug_init_break = tbreak setup 21 | debug_tool = esp-builtin 22 | upload_protocol = esp-builtin 23 | ; upload_protocol = esptool 24 | 25 | upload_speed = 921600 26 | monitor_speed = 115200 27 | -------------------------------------------------------------------------------- /src/asm.S: -------------------------------------------------------------------------------- 1 | 2 | .section .text 3 | .align 4 4 | .global asm_test_func 5 | .type asm_test_func,@function 6 | 7 | // 汎用レジスタ(Xtensaではアドレスレジスタと呼ばれる) は a0~a15 の合計16個ある。 8 | // ただしa0とa1の扱いは注意。 9 | // a0 = リターンアドレス 10 | // a1 = スタックポインタ 11 | // 通常この2つは使用を避ける。 12 | // a0に関しては内容を変更しても良いが、retwを実行する前に元に戻すこと。 13 | 14 | // 引数のうち先頭から6個まではレジスタ a2-a7 に入っている。 15 | // 戻り値はretw命令実行時点でa2にセットされた値が使用される。 16 | 17 | // uint32_t asm_test_func(uint32_t value) 18 | asm_test_func: // 関数 asm_test_func の先頭アドレスにラベルを配置 19 | entry a1, 16 // C言語から呼ばれる関数の先頭は必ずentry命令を配置する。 20 | movi a10,asm_test_data // a10 = asm_test_data; データのアドレスを代入 21 | extui a11,a2, 0, 4 // a11 = (a2 >> 0) & 0x0F; 下位4ビットマスク。これでa11の値はa2の下位4ビットの値となる 22 | add a12,a11,a10 // a12 = a11 + a10; a12の内容は asm_test_data[a2 & 0x0F] のアドレスとなる 23 | l8ui a2, a12,0 // a2 = asm_test_data[a2 & 0x0F]; a12の示すアドレスの値をa2に代入する 24 | retw // return a2; retw命令でa2の値を戻り値とし関数を終了する 25 | 26 | // 関数ここまで。 27 | 28 | // 以下 データ 29 | 30 | .align 4 31 | .section .rodata // ROM配置を指定 32 | asm_test_data: 33 | // 文字列データを配置 34 | .string "\xDE\xAD\xBE\xEF HELLO WORLD TESTSTRING." 35 | 36 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | extern "C" { 4 | // アセンブラコード内の関数宣言 5 | uint32_t asm_test_func(uint32_t); 6 | }; 7 | 8 | void setup() { 9 | printf("setup\n"); 10 | } 11 | 12 | void loop() { 13 | delay(500); 14 | static uint32_t count; 15 | uint32_t result = asm_test_func(count); 16 | printf("loop %d : char %c : %02x\n", count, (char)result, result); 17 | ++count; 18 | } 19 | --------------------------------------------------------------------------------