新版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 上に配置して実行させるように指定することもできます。
サイン波テーブルの位置と併せると、

  • フラッシュ上に関数、フラッシュ上にサイン波テーブル
  • フラッシュ上に関数、SRAM 上にサイン波テーブル
  • SRAM 上に関数、フラッシュ上にサイン波テーブル
  • SRAM 上に関数、SRAM 上にサイン波テーブル

という 4 種類の構成を考えることができますが、いずれの場合の所要サイクル数は 63 程度で、差は誤差程度でした。
関数 + サイン波テーブルの合計で 8 K バイト足らずなので、すべてがキャッシュにおさまっているものと思われます。 実際のプログラムの規模になった場合には、関数/テーブルを SRAM 上に置くことが有利になるかも知れません。
上記の逆アセンブル・リストでは、ループ内の命令数は 54 で、平均すると 1 命令当たり 1.17 サイクルで実行されていることになります。