新版FM音源プログラム (29)
Cortex-M シリーズを離れて、Espressif Systems 製の ESP-WROOM-32 モジュール (Cadence/Tensilica の Xtensa LX6 デュアル・コア内蔵) に対してプログラムを作成し、所要サイクル数を測定してみました。
具体的には、ハードウェアとしては「ESP32-DevKitC ESP-WROOM-32 開発ボード」(秋月通販コード M-11819 →こちら)、開発環境としては Esspressif が提供する「ESP-IDF」を使用しています。
ESP-WROOM-32 は WiFi / Bluetooth を内蔵していることが大きな特長ですが、ここでは単に 240 MHz クロックの RISC プロセッサとして利用しているだけです。
Xtensa LX6 は「ガチ」な RISC であり、そのアーキテクチャに不慣れなこともあり、無理にアセンブリ言語でプログラムを書いてもパイプラインをストールさせずに高速化できるかどうか不明なため、C 言語で記述することにしました。
アキュムレータ機能組み込みの acc_calc_slot() 関数のソース・リストを下に示します。
#include "slot.h" int32_t acc_calc_slot(op_prop_t *o_p, // pntr to op_prop[] int num_slot) // number of slots { uint32_t phh; // phase accumulator high int32_t out0; // op0 output int32_t out1; // OP previous output / op1 out uint32_t vol; // op0_vol:op1_vol int32_t acc_L; // accum. for L-ch int32_t acc_R; // accum. for R-ch acc_L = 0; // clear accumulator acc_R = 0; do { //-------------------- // Series FM modulator //-------------------- // phase generator calculation phh = o_p->ph_inc; // get phase increment phh += o_p->ph_acc; // add to phase accum. o_p->ph_acc = phh; // update phase accum. phh += o_p->mod_in; // phase modulation input phh >>= (PH_ACC_FRAC_BITS-1); // byte offset // sine table lookup with the Wave Select out0 = *(int16_t *)((uint8_t *)o_p->stab_p + (o_p->ind_mask & phh)); // operator output scaling out0 *= o_p->ol_lin; // multiply linear output level out0 >>= 15; // reduce to 16-bit range // feedback with 2-tap FIR LPF out1 = o_p->op_out; // save last output sample o_p->op_out = out0; // update op output value out1 += out0; // 1st order FIR LPF for feedback ( H(z) = (1 + z^(-1)) ) o_p->mod_in = (out1 * o_p->mod_mul); // for feedback o_p++; // advance operator property pointer //------------------ // Series FM carrier //------------------ // modulator to carrier connection out1 = (out0 * o_p->mod_mul); // phase generator calculation phh = o_p->ph_inc; // get phase increment phh += o_p->ph_acc; // add to phase accum. o_p->ph_acc = phh; // update phase accum. phh += out1; // phase modulation input phh >>= (PH_ACC_FRAC_BITS-1); // byte offset // sine table lookup with the Wave Select out1 = *(int16_t *)((uint8_t *)o_p->stab_p + (o_p->ind_mask & phh)); // operator output scaling out1 *= o_p->ol_lin; // multiply linear output level out1 >>= 15; // reduce to 16-bit range o_p->op_out = out1; // update operator output value // accumulator vol = o_p->L_vol; // vol = op0_vol:op1_vol acc_L += (((vol >> 16)*out0)+((uint16_t)vol)*out1); vol = o_p->R_vol; // vol = op0_vol:op1_vol acc_R += (((vol >> 16)*out0)+((uint16_t)vol)*out1); // o_p++; // advance operator property pointer num_slot -= 2; } while (0 < num_slot); // past end of slots? acc_L >>= LR_VOL_SHIFT; // post scaling acc_L = __CLAMPS(acc_L, 15); // sat. to 16 bit acc_R >>= LR_VOL_SHIFT; // post scaling acc_R = __CLAMPS(acc_R, 15); // sat. to 16 bit return((acc_L << 16) | ((uint16_t)acc_R)); } // int32_t acc_calc_slot()
Xtensa アーキテクチャは、細かくコンフィギュレーション可能で、ESP32 に搭載されているバージョンでは、「MAC16」拡張が選択されていて、40 ビット・アキュムレータに対する積和演算が可能です。
しかし、アキュムレータはひとつしかないので、ステレオ出力に対応するにはアキュムレータと汎用レジスタ間のセーブ/リストアの手間がかかるので、MAC 拡張は使っていません。
「飽和演算」を実現する「CLAMPS」命令は利用していますが、Cortex-M の CMSIS ライブラリのようにイントリンシック関数としては定義されていないので、Cortex-M シリーズに習って下のように「自前」で定義して利用しています。
#include <stdint.h> #if defined ( __GNUC__ ) #define __ASM __asm #define __INLINE inline #define __STATIC_INLINE static inline #endif __attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __CLAMPS(uint32_t op1, uint32_t op2) { uint32_t result; __ASM volatile ("clamps %0, %1, %2" : "=r" (result) : "r" (op1), "I" (op2) ); return(result); }
前に示した acc_calc_slot() 関数をコンパイルして得られたオブジェクトを逆アセンブルしたものにコメントを付加したリストを下に示します。
00000000 <acc_calc_slot>: ; ; a2 = (* op_prop_t) o_p ; a3 = (int) num_slot ; 0: 004136 entry a1, 32 ; sp=a1, frame=32 3: d30b addi.n a13, a3, -1 ; a13=lcnt=num_slot-1 5: 0c0c movi.n a12, 0 ; a12 = acc_R = 0 7: 41d1d0 srli a13, a13, 1 ; lcnt >>= 1 a: fec332 addi a3, a3, -2 ; num_slot -= 2 d: 209cc0 or a9, a12, a12 ; a9 = acc_L = 0 10: 01cdd2 addi a13, a13, 1 ; lcnt++ 13: 0203e6 bgei a3, -1, 19 ; if (num_slot < -1) { 16: 01a0d2 movi a13, 1 ; lcnt = 1 19: 848d76 loop a13, a1 ; a13 = lcnt 1c: 0238 l32i.n a3, a2, 0 ; a3=o_p->ph_inc 1e: 1288 l32i.n a8, a2, 4 ; a8=o_p->ph_acc 20: 22a8 l32i.n a10, a2, 8 ; a10=o_p->ph_mod 22: 838a add.n a8, a3, a8 ; ph_acc+=ph_inc 24: 1289 s32i.n a8, a2, 4 ; o_p->ph_acc=a8 26: 88aa add.n a8, a8, a10 ; ph_acc+=ph_mod (=phh) 28: 0812a2 l16ui a10, a2, 16 ; a10=o_p->ind_mask 2b: 3238 l32i.n a3, a2, 12 ; a3=o_p->stab 2d: 418880 srli a8, a8, 8 ; phh >>= 8 30: 1088a0 and a8, a8, a10 ; phh &= ind_mask 33: 838a add.n a8, a3, a8 ; a8=((uint8_t)(o_p->stab)+phh) 35: 009882 l16si a8, a8, 0 ; a8=*((int16_t *)a8) 38: 6238 l32i.n a3, a2, 24 ; a3=o_p->ol_lin 3a: a2b8 l32i.n a11, a2, 40 ; a11=o_p[1].ph_acc 3c: 828830 mull a8, a8, a3 ; a8*=ol_lin (=out0) 3f: 5238 l32i.n a3, a2, 20 ; a3=o_p->op_out (=out1) 41: 218f80 srai a8, a8, 15 ; a3 >>= 15 44: a83a add.n a10, a8, a3 ; a10=out0+out1 46: 091232 l16ui a3, a2, 18 ; a3=o_p->mod_mul 49: 5289 s32i.n a8, a2, 20 ; o_p->op_out=out0 4b: 823a30 mull a3, a10, a3 ; a3=(out0+out1)*mod_mul 4e: 1b12a2 l16ui a10, a2, 54 ; a10=o_p[1].mod_mul 51: 2239 s32i.n a3, a2, 8 ; o_p->ph_mod=a3 53: 823a80 mull a3, a10, a8 ; a3=out0*mod_mul 56: 92a8 l32i.n a10, a2, 36 ; a10=o_p[1].ph_inc 58: aaba add.n a10, a10, a11 ; ph_acc+=ph_inc 5a: a2a9 s32i.n a10, a2, 40 ; o_p[1].ph_acc=a10 5c: 1a12b2 l16ui a11, a2, 52 ; a11=o_p[1].ind_mask 5f: a3aa add.n a10, a3, a10 ; ph_acc+=ph_mod (=phh) 61: 41a8a0 srli a10, a10, 8 ; phh >>= 8 64: c238 l32i.n a3, a2, 48 ; a3=o_p[1].stab_p 66: 10aab0 and a10, a10, a11 ; phh &= ind_mask 69: a3aa add.n a10, a3, a10 ; a10=((uint8_t *)stab_p+phh) 6b: 1022b2 l32i a11, a2, 64 ; a11=o_p[1].L_vol 6e: 009aa2 l16si a10, a10, 0 ; a10=*((int16_t *)a10) 71: f238 l32i.n a3, a2, 60 ; a3=o_p[1].ol_lin 73: f5e0b0 extui a14, a11, 16, 16 ; a14=(L_vol>>16) 76: 82aa30 mull a10, a10, a3 ; a10=out1*ol_lin 79: 82ee80 mull a14, a14, a8 ; a14=L_vol0*out0 7c: 21afa0 srai a10, a10, 15 ; out1 >>= 15 7f: f4b0b0 extui a11, a11, 0, 16 ; a11=(uint16_t)a11 82: 82bba0 mull a11, a11, a10 ; a11=L_vol1*out1 85: 9e9a add.n a9, a14, a9 ; acc_L+=(L_vol0*out0) 87: 1122e2 l32i a14, a2, 68 ; a14=o_p[1].R_vol 8a: 99ba add.n a9, a9, a11 ; acc_L+=(L_vol1*ou1) 8c: f5b0e0 extui a11, a14, 16, 16 ; a11=(R_vol>>16) 8f: 82bb80 mull a11, a11, a8 ; a11=(R_vol0*out0) 92: e2a9 s32i.n a10, a2, 56 ; o_p[1].op_out=out1 94: 8bca add.n a8, a11, a12 ; a8=(R_vol0*out0)+acc_R 96: f4c0e0 extui a12, a14, 0, 16 ; a12=(uint16_t)R_vol 99: 82aca0 mull a10, a12, a10 ; a10=(R_vol1*out1) 9c: 48c222 addi a2, a2, 72 ; o_p += 2 9f: c8aa add.n a12, a8, a10 ; acc_R=(R_vol0*our0)+acc_R+(R_vol1*out1) a1: 219790 srai a9, a9, 7 ; acc_L >>= 7 a4: 339980 clamps a9, a9, 15 ; sat. acc_L to 16 bit a7: 2127c0 srai a2, a12, 7 ; acc_R >>= 7 aa: 338280 clamps a8, a2, 15 ; sat. acc_R to 16 bit ad: 119900 slli a9, a9, 16 ; acc_L <<= 16 b0: f42080 extui a2, a8, 0, 16 ; a2=(uint16_t)acc_R b3: 202920 or a2, a9, a2 ; combine (acc_L:acc_R) b6: f01d retw.n
ESP32 に内蔵されている Xtensa LX6 ではオーバーヘッドなしでループ可能な「ハードウェア・ループ機能」拡張が選択されており、その loop 命令を使う形にコンパイルされています。
ループ 1 回あたり、つまりスロット 2 つ分の実行に要するサイクル数は、NSLOT=256 の条件の測定で 63 サイクルとなりました。
ESP32 チップ内部にはフラッシュ・メモリは内蔵されておらず、QSPI フラッシュ・メモリを外付けして使うようになっています。
ESP-WROOM-32 「モジュール内部」には 4 Mバイト QSPI フラッシュが実装されており、ESP-WROOM-32 モジュール外部からみれば「フラッシュ内蔵」という形になります。
ユーザ・プログラムは、通常この外部フラッシュに書き込まれて実行されることになりますが、内蔵 SRAM の一部 (最大 64 KB) をフラッシュ・アクセスに対する「キャッシュ」として割り当てることができます。
その他、関数のアトリビュートとして、内部 SRAM 上に配置して実行させるように指定することもできます。
サイン波テーブルの位置と併せると、
という 4 種類の構成を考えることができますが、いずれの場合の所要サイクル数は 63 程度で、差は誤差程度でした。
関数 + サイン波テーブルの合計で 8 K バイト足らずなので、すべてがキャッシュにおさまっているものと思われます。 実際のプログラムの規模になった場合には、関数/テーブルを SRAM 上に置くことが有利になるかも知れません。
上記の逆アセンブル・リストでは、ループ内の命令数は 54 で、平均すると 1 命令当たり 1.17 サイクルで実行されていることになります。