シンセ・アンプラグド RSSフィード

2018-04-04

新版FM音源プログラム (28)

gcc の「インライン・アセンブラ」で書いた ARMv7E-M (Cortex-M4) 版の acc_calc_slot() 関数のリストを下に示します。 「中身」は armcc 版と同等です。

#include "slot.h"
#define DEF_OFS(x) (offsetof(op_prop_t, x))

//
// gcc inline assembly language function
//
int32_t acc_calc_slot(op_prop_t *o_p, int num_slot)
{
  __asm__ __volatile__ (
#if defined(__ARM_ARCH_7EM__)     // ARMv7E-M (Cortex-M4)
// Use UAL (Unified Assembler Language) syntax    
  " .syntax  unified             \n\t"
  "  movs    r7,#0               \n\t" // clear r7=acc_R
  "  mov     r8,r7               \n\t" // clear r8=acc_L
  "1:                            \n\t"
//
//  series FM modulator slot
//
  "  ldr     r2,[r0,%[ph_inc]]   \n\t" // r2=ph_inc
  "  ldr     r3,[r0,%[ph_acc]]   \n\t" // r3=ph_acc
  "  ldr     r4,[r0,%[mod_in]]   \n\t" // r4=mod_in
  "  ldr     r5,[r0,%[stab_p]]   \n\t" // r5=stab_p
  "  adds    r2, r2,r3           \n\t" // r2=ph_inc+ph_acc
  "  ldrh    r3,[r0,%[ind_mask]] \n\t" // r3=ind_mask
  "  str     r2,[r0,%[ph_acc]]   \n\t" // update ph_acc
  "  adds    r4, r4,r2           \n\t" // r4=ph_inc+ph_acc+mod_in (=phh)
  "  and     r3, r3,r4,lsr %[acc_sft] \n\t" // r3 & (r4 >> 8)
  "  ldr     r2,[r0,%[ol_lin]]   \n\t" // r2=ol_lin
  "  ldrsh   r3,[r5,r3]          \n\t" // r3=*(int16_t *)(stab_p+r3)
//
// 2-tap FIR filter for feedback
//
  "  ldrsh   r5,[r0,%[op_out]]   \n\t" // r5=op_out (=prev_out)
  "  muls    r2, r3,r2           \n\t" // r2=r3*ol_lin
  "  asrs    r4, r2,#15          \n\t" // r4=(r2>>15) (=out)
  "  ldrh    r3,[r0,%[mod_mul]]  \n\t" // r3=mod_mul
  "  strh    r4,[r0,%[op_out]]   \n\t" // op_out=out
  "  adds    r5, r5,r4           \n\t" // r5=out+prev_out
  "  muls    r3, r5,r3           \n\t" // r3=r5*mod_mul
  "  str     r3,[r0,%[mod_in]]   \n\t" // mod_in=r3
//
// series FM carrier slot
//
  "  ldr     r2,[r0,%[ph_inc]+%c[S]]  \n\t" // r2=ph_inc
  "  ldr     r3,[r0,%[ph_acc]+%c[S]]  \n\t" // r3=ph_acc
  "  ldrh    r5,[r0,%[mod_mul]+%c[S]] \n\t" // r5=mod_mul
  "  adds    r2, r2,r3            \n\t" // r2=ph_inc+ph_acc
  "  str     r2,[r0,%[ph_acc]+%c[S]]  \n\t" // update ph_acc
  "  mla     r2, r4, r5, r2       \n\t" // r2=(r4*r5)+r2
  "  ldrh    r3,[r0,%[ind_mask]+%c[S]] \n\t" // r3=ind_mask
  "  ldr     r5,[r0,%[stab_p]+%c[S]]  \n\t" // r5=stab_p
  "  and     r3, r3,r2,lsr %[acc_sft] \n\t" // r3&(r2>>8)
  "  ldr     r2,[r0,%[ol_lin]+%c[S]]  \n\t" // r2=ol_lin
  "  ldrsh   r3,[r5,r3]           \n\t" // r3=*(int16_t *)(stab_p+r3)
  "  ldr     r5,[r0,%[R_vol]+%c[S]] \n\t" // r5=R volume
  "  ldr     r6,[r0,%[L_vol]+%c[S]] \n\t" // r6=L volume
  "  muls    r2, r3,r2            \n\t" // r2=r3*ol_lin
  "  asrs    r2, r2,#15           \n\t" // r2>>=15 (=out)
  "  bfi     r2, r4,#16,#16       \n\t" // r2[31:16]=r4[15:0]
  "  str     r2,[r0,%[op_out]+%c[S]]  \n\t" // op_out=out
// acc_R += (op_out0*R_vol0) + (op_out1*R_vol1)
  "  smlad   r7, r2, r5, r7           \n\t"
// acc_L += (op_out0*L_vol0) + (op_out1*L_vol1)
  "  smlad   r8, r2, r6, r8           \n\t"
  "  adds    r0, r0,%[S]*2    \n\t" // adv. to next slot pair
  "  subs    r1, r1,#2        \n\t" // decrement loop counter
  "  bgt     1b               \n\t" // more to do
// post scaling and saturate to 16 bit
  "  ssat    r1, #16,r8,asr %[vol_sft] \n\t" // L-ch
  "  ssat    r0, #16,r7,asr %[vol_sft] \n\t" // R-ch
// pack two 16-bit halfwords to single 32-bit word
  "  bfi     r0, r1,#16,#16   \n\t" // result[31:16]=acc_L[15:0]

// output reg list
  : "+r" (o_p),      // [arg1] = r0 = (op_prop_t *) o_p
    "+r" (num_slot)  // [arg2] = r1 = (int) num_slot
// input parameter list ("I" for offset constant)
  : [acc_sft]  "I" (PH_ACC_FRAC_BITS-1),
    [vol_sft]  "I" (LR_VOL_SHIFT),
    [ph_inc]   "I" (DEF_OFS(ph_inc)),
    [ph_acc]   "I" (DEF_OFS(ph_acc)),
    [mod_in]   "I" (DEF_OFS(mod_in)),
    [stab_p]   "I" (DEF_OFS(stab_p)),
    [ind_mask] "I" (DEF_OFS(ind_mask)),
    [op_out]   "I" (DEF_OFS(op_out)),
    [ol_lin]   "I" (DEF_OFS(ol_lin)),
    [mod_mul]  "I" (DEF_OFS(mod_mul)),
    [L_vol]    "I" (DEF_OFS(L_vol)),
    [R_vol]    "I" (DEF_OFS(R_vol)),
    [S]        "I" (sizeof(op_prop_t))
// clobber reg list
  : "r2", "r3", "r4", "r5", "r6", "r7", "r8",
    "cc", "memory"
  );// __asm__
  return((int32_t) o_p);
} // int32_t acc_calc_slot()

STM32F4-Discovery (STM32F407VGT6) および Nucleo-F303K8 (STM32F303K8T6) での所要サイクル数の測定結果は下のようになっています。

// STAB in SRAM
// Atollic TrueSTUDIO for STM32 v9.0.0 (gcc 6.3.1)
// 48 cycle / 2slot (STM32F407 (CM4) @  30 MHz, Flash latency=0, NSLOT=256)
// 48 cycle / 2slot (STM32F407 (CM4) @ 168 MHz, Flash latency=5, NSLOT=256)

// 48 cycle / 2slot (STM32F303 (CM4) @  24 MHz, Flash latency=0, NSLOT=64)
// 52 cycle / 2slot (STM32F303 (CM4) @  64 MHz, Flash latency=2, NSLOT=64)

// STAB in flash
// Atollic TrueSTUDIO for STM32 v9.0.0 (gcc 6.3.1)
// 52 cycle / 2slot (STM32F407 (CM4) @  30 MHz, Flash latency=0, NSLOT=256)
// 62 cycle / 2slot (STM32F407 (CM4) @ 168 MHz, Flash latency=5, NSLOT=256)

// 48 cycle / 2slot (STM32F303 (CM4) @  24 MHz, Flash latency=0, NSLOT=64)
// 62 cycle / 2slot (STM32F303 (CM4) @  64 MHz, Flash latency=2, NSLOT=64)

2018-03-06

新版FM音源プログラム (21)

STM32F4 シリーズには、「Adaptive real-time accelerator」(ART Accelerator) と称するフラッシュ・メモリ読み出しのためのキャッシュ機構が備えられています。

フラッシュの読み出し幅は 128 ビット (16 バイト) で、フラッシュ上の命令 (I-Code) に対しては 64 ライン分の命令キャッシュが用意されており、プリフェッチされた命令が格納されます。

命令キャッシュ・メモリ容量としては、16 × 64 = 1024 バイトとなり、90 バイト足らずの calc_slot() 関数は全体がキャッシュ・メモリ内におさまります。

フラッシュ上のデータ (D-Code) に対しては 8 ライン分のデータ・キャッシュが用意されています。

サイン波テーブルを SRAM 上に置いて、フラッシュからは命令を読み出すだけの構成に対する測定結果では、フラッシュ・レイテンシが 0 でも 5 でも 2 スロット当たりの実行サイクル数は 44 となり、命令キャッシュがほぼ完璧に作用していることが分かります。

サイン波テーブルをフラッシュ・メモリ上に置いた場合には、フラッシュ・レイテンシが 0 の場合でも実行サイクル数の測定結果は 47 となり、命令のフェッチとデータの読み込みとで何らかの競合が生じているのが推測されます。

フラッシュ・レイテンシが 5 の場合には、実行サイクル数の測定結果は 57 程度となり、レイテンシ 0 の場合との差は約 10 となります。

各スロット・プロパティの初期設定で、各スロットのサイン波テーブルアクセスに対して、なるべくランダムなアドレスを発生するようにして、なるべくデータ・キャッシュが「効かない」ように考慮してあります。

2 スロット分の計算で、約 10 サイクルの差ということは、1 スロット当たり約 5 サイクルということで、これはデータ・キャッシュが有効でない場合のレイテンシ 5 がそのまま現れていると見ることができます。

「stm32f4xx_hal_flash.h」内で定義されているマクロ

   __HAL_FLASH_DATA_CACHE_DISABLE();

を使ってデータ・キャッシュを無効にしてやっても、測定結果は同様に約 57 サイクルとなることで確認できます。

STM32F303 は、CPU は STM32F4 シリーズと同じ Cortex-M4 ですが、キャッシュ機構が簡略化されています。

フラッシュ・メモリは 64 ビット幅 (8 バイト幅) で読み出され、プリフェッチ・バッファとして 64 ビット幅のバッファが 2 本用意されています。

データ (D-Code) キャッシュは省略されているようです。

実行サイクル数の測定結果は、サイン波テーブルを SRAM 上に置いてフラッシュ・レイテンシが 0 の場合 43 サイクル、レイテンシが 2 の場合 46 サイクルとなっています。

フラッシュ・レイテンシにより変化しているので、キャッシュ機構がレイテンシを完全には隠蔽できていないことを示しています。

サイン波テーブルをフラッシュ上に置いた場合、レイテンシ 0 で 46 サイクル、レイテンシ 2 で 55 サイクルとなっています。

その差は 55 - 46 = 9 サイクルで、サイン波テーブル・アクセス 1 回当たりでは 4.5 クロックが追加されていることになります。

2018-03-04

新版FM音源プログラム (20)

Cortex-M4 用の結果では、フラッシュのキャッシュ機構の構成が同じ STM32F446 と STM32F407 とがほぼ同じ結果で、別のキャッシュ機構を持つ STM32F303 がやや異なる結果となりました。

PSoC5LP にはフラッシュのキャッシュのヒット/ミスの回数を計測するレジスタがあり、それを利用して測定すると、サイン波テーブルをフラッシュ上に置いた場合と、SRAM 上に置いた場合との差が非常にきれいに現れました。

ARMv7-M アーキテクチャ (Cortex-M3/M4) 用にコンパイルしたアセンブリ言語プログラムオブジェクトを逆アセンブルしたものを下に示します。

00000000 <calc_slot>:
   0:   b430            push    {r4, r5}
   2:   6802            ldr     r2, [r0, #0]
   4:   6843            ldr     r3, [r0, #4]
   6:   6884            ldr     r4, [r0, #8]
   8:   68c5            ldr     r5, [r0, #12]
   a:   18d2            adds    r2, r2, r3
   c:   8a03            ldrh    r3, [r0, #16]
   e:   6042            str     r2, [r0, #4]
  10:   18a4            adds    r4, r4, r2
  12:   ea13 2314       ands.w  r3, r3, r4, lsr #8
  16:   6942            ldr     r2, [r0, #20]
  18:   5eeb            ldrsh   r3, [r5, r3]
  1a:   f9b0 5012       ldrsh.w r5, [r0, #18]
  1e:   435a            muls    r2, r3
  20:   13d4            asrs    r4, r2, #15
  22:   8b03            ldrh    r3, [r0, #24]
  24:   8244            strh    r4, [r0, #18]
  26:   192d            adds    r5, r5, r4
  28:   436b            muls    r3, r5
  2a:   6083            str     r3, [r0, #8]
  2c:   69c2            ldr     r2, [r0, #28]
  2e:   6a03            ldr     r3, [r0, #32]
  30:   8e85            ldrh    r5, [r0, #52]   ; 0x34
  32:   18d2            adds    r2, r2, r3
  34:   6202            str     r2, [r0, #32]
  36:   fb04 2405       mla     r4, r4, r5, r2
  3a:   8d83            ldrh    r3, [r0, #44]   ; 0x2c
  3c:   6a85            ldr     r5, [r0, #40]   ; 0x28
  3e:   ea03 2314       and.w   r3, r3, r4, lsr #8
  42:   6b02            ldr     r2, [r0, #48]   ; 0x30
  44:   5eeb            ldrsh   r3, [r5, r3]
  46:   435a            muls    r2, r3
  48:   13d2            asrs    r2, r2, #15
  4a:   85c2            strh    r2, [r0, #46]   ; 0x2e
  4c:   3038            adds    r0, #56 ; 0x38
  4e:   3902            subs    r1, #2
  50:   dcd7            bgt.n   2 <calc_slot+0x2>
  52:   bc30            pop     {r4, r5}
  54:   4770            bx      lr

これには、ループ内に

  • ブランチ命令 (2 サイクル) — 1 個
  • ロード命令 (2 サイクル) — 16 個
  • ストア命令 (2 サイクル) — 5 個
  • その他の命令 (1 サイクル) — 14 個

が含まれています。 単純にトータルのサイクル数を計算すると、

2 + (2 × 16) + (2 × 5) + 14 = 58

となります。 Cortex-M3 の場合は MLA 命令の実行に 2 サイクルかかるので、これより 1 増えて 59 サイクルになります。

ARMv7-M アーキテクチャの場合には、ロード/ストア命令を連続させることによりパイプライン化して、実行サイクル数を削減できます。

まず、定数オフセットの STR 命令 (STR R2, [R0, #4] のような場合) は 1 サイクルで実行することができます。 スロット計算プログラムでは、すべてのストア命令は定数オフセットの形になっているので、これで 5 サイクル削減になります。

また、LDR 命令を連続させると、最初の LDR には 2 サイクル必要ですが、後続の LDRパイプライン化されて 1 サイクルで実行できます。 これによる削減が 9 サイクル分あります。

合計すると、

58 - 5 - 9 = 44

となります。 サイン波テーブルをフラッシュ上に置いた場合は、フラッシュ・レイテンシが 0 の条件で 47 サイクルと、少し差がありますが、サイン波テーブルを SRAM 上に置いた場合には、(フラッシュ・レイテンシが 5 でも) 測定結果が 44 サイクルとなり、サイクル数の計算と一致しました。

これは、サイン波テーブルがフラッシュ上にあると、テーブル・データの読み出しが (たとえレイテンシが 0 であっても) 命令フェッチと競合するためと思われます。

PSoC5LP にはキャッシュのヒット/ミスをカウントすれレジスタがあって、これを利用して測定してみたところ、非常にきれいな結果が出ました。

測定のためのプログラムを下に示します。

  #define N_HIT_MISS (22)

  uint32_t hit_miss[N_HIT_MISS];

. . . . . <中略> . . . . . 

  for (i = 0; i < N_HIT_MISS; i++) {
    CY_SET_REG32(CYREG_CACHE_HITMISS, 0);
    calc_slot(&op_prop[0], NSLOT);
    hit_miss[i] = CY_GET_REG32(CYREG_CACHE_HITMISS);
  } // for (i = 0; ...

対象のレジスタのアドレスはデフォルトでインクルードされるファイルの中で「CYREG_CACHE_HITMISS」として定義されています。

    CY_GET_REG32(CYREG_CACHE_HITMISS)  

で対象のカウンタを読み出すことができ、上位 16 ビットがヒット・カウント、下位 16 ビットがミス・カウントとなっています。 カウントが 16 ビットでの最大値 65535 に達するとラップ・アラウンドはせず、そこで停止するようです。

測定の前には、

    CY_SET_REG32(CYREG_CACHE_HITMISS, 0);

として、カウンタをゼロ・クリアしておきます。

測定プログラムの影響を最小にするために、レジスタを読み取った結果はループ中では hit_miss[] 配列に書き込むだけとし、表示などは後で行なっています。 ループ回数が 22 回となっているのは、結果を 80 桁 × 25 行のコンソールに表示した場合に 1 画面中におさまるように考慮したためです。

まず、サイン波テーブルを SRAM 中に置いた場合の結果を示します。

STAB in RAM
HIT = 362,  MISS = 12, (HIT + MISS) = 374
HIT = 373,  MISS =  2, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
HIT = 375,  MISS =  0, (HIT + MISS) = 375
TOTAL HIT = 8235, TOTAL MISS= 14, TOTAL (HIT + MISS) = 8249

calc_slot() プログラム全体が命令キャッシュに入りきれていない最初の 2 回の呼び出しではキャッシュ・ミスが生じていますが、全体がキャッシュに入ったと思われる 3 回目以降の calc_slot() 呼び出しでは、ミスがゼロで、すべてがヒットしています。

サイン波テーブルは SRAM に置いたので、命令フェッチとは全く競合せず、calc_slot() 呼び出し 1 回当たりの 375 回のフラッシュ読み出し要求はすべて命令フェッチによるものです。

サイン波テーブルをフラッシュ上に置いた場合の結果を下に示します。

STAB in FLASH
HIT = 383,  MISS = 22, (HIT + MISS) = 405
HIT = 405,  MISS =  2, (HIT + MISS) = 407
HIT = 403,  MISS =  4, (HIT + MISS) = 407
HIT = 404,  MISS =  3, (HIT + MISS) = 407
HIT = 401,  MISS =  6, (HIT + MISS) = 407
HIT = 403,  MISS =  4, (HIT + MISS) = 407
HIT = 398,  MISS =  9, (HIT + MISS) = 407
HIT = 405,  MISS =  2, (HIT + MISS) = 407
HIT = 403,  MISS =  4, (HIT + MISS) = 407
HIT = 404,  MISS =  3, (HIT + MISS) = 407
HIT = 403,  MISS =  4, (HIT + MISS) = 407
HIT = 404,  MISS =  3, (HIT + MISS) = 407
HIT = 399,  MISS =  8, (HIT + MISS) = 407
HIT = 402,  MISS =  5, (HIT + MISS) = 407
HIT = 397,  MISS = 10, (HIT + MISS) = 407
HIT = 399,  MISS =  8, (HIT + MISS) = 407
HIT = 397,  MISS = 10, (HIT + MISS) = 407
HIT = 390,  MISS = 16, (HIT + MISS) = 406
HIT = 392,  MISS = 14, (HIT + MISS) = 406
HIT = 395,  MISS = 11, (HIT + MISS) = 406
HIT = 392,  MISS = 14, (HIT + MISS) = 406
HIT = 395,  MISS = 11, (HIT + MISS) = 406
TOTAL HIT = 8774,  TOTAL MISS= 173, TOTAL (HIT + MISS) = 8947

22 回の calc_slot() 呼び出しのすべてでキャッシュ・ミスが起きています。

calc_slot() 呼び出し 1 回当たりのフラッシュ読み出し要求は 407 回で、SRAM の場合の 375 回との差は、

407 - 375 = 32

となり、サイン波テーブルをスロットひとつ当たり 1 回フラッシュから読み出している分に相当していることが分かります。

2018-02-28

新版FM音源プログラム (18)

アセンブリ言語によるスロット (オペレータ) 計算プログラムでは、ARMv6-M (Cortex-M0) の 16 ビット Thumb 命令を主に使うこととし、ARMv7-M (Cortex-M3/M4) に対しては一部を 32 ビット Thumb2 命令で記述することにより効率化しています。

以下に、その「一部分」を示します。

#if defined(__thumb2__) // ARMv7-M (Cortex-M3/M4)
    "  ands    r3, r3,r4,lsr #8    \n\t"
#else                   // ARMv6-M (Cortex-M0)
    "  lsrs    r4, r4,#8           \n\t"
    "  ands    r3, r3,r4           \n\t"
#endif
    "  ldrsh   r3,[r5,r3]          \n\t"

これは、フェーズ・アキュムレータ (ph_acc) の値からシフトおよび AND によってサイン波テーブルアクセスのためのバイト・オフセットを切り出す部分です。

Thumb 命令の範囲では、シフト命令 lsrs と AND 命令 ands の 2 命令必要ですが、Thumb2 命令では ands 命令の「シフト付きオペランド」でシフト命令の役割を果たすことができます。

#if defined(__thumb2__)  // ARMv7-M (Cortex-M3/M4)
    "  ldrsh   r5,[r0,%[op_out]]   \n\t"
#else                    // ARMv6-M (Cortex-M0)
    "  ldrh    r5,[r0,%[op_out]]   \n\t"
    "  sxth    r5, r5              \n\t"
#endif

これは、オペレータ出力 (op_out) を読み出している部分ですが、Thumb 命令には符号付きハーフワード (int16_t) 型のデータを 32 ビットに符号拡張して、定数オフセット・アドレッシングで読み出す ldrsh 命令がないため、まず、1 命令目で、「ゼロ拡張」である ldrh 命令で int16_t データを読み出し、2 命令目の sxth で 16 ビットデータを 32 ビットに符号拡張しています。

Thumb2 命令には定数オフセットの ldrsh 命令があるので、(命令長は 32 ビットになりますが) それを使います。

ここは、「レジスタオフセット形式」の ldrsh 命令は Thumb にもあるので、

    "  movs    r5,%[op_out]       \n\t"
    "  ldrsh   r5,[r0,r5]         \n\t"

としても (レジスタを 1 個使ってしまいますが) 実現できます。

#if defined(__thumb2__) // ARMv7-M (Cortex-M3/M4)
    "  mla     r4, r4, r5, r2           \n\t"
#else                   // ARMv6-M (Cortex-M0)
    "  muls    r4, r5,r4                \n\t"
    "  adds    r4, r4,r2                \n\t"
#endif

これはキャリア・オペレータのフェーズ・アキュムレータの計算で、モジュレータ側からのモジュレーションを掛けている部分です。

r2 には (ph_acc + ph_inc) の値、r4 にはモジュレータの op_out の値、R5 にはモジュレーションありかなしかを決める mod_mul の値が入っています。

      r4 = (r4 * r5) + r2
         = (op_out * mod_mul) + (ph_acc + ph_inc)

を計算しています。

Thumb 命令では「積和命令」がないので、乗算の muls 命令と、加算の adds 命令の 2 命令で実現しています。

Thumb2 命令では「Multiply Accumulate 」命令ひとつ

      mla  rd, rn, rm, ra

      rd = (rn * rm) + ra 

という計算が実行できます。

Cortex-M3 の場合には、mla 命令の実行サイクル数は 2 で、Thumb 命令の muls / adds の 2 命令バージョンと変わりありませんが、Cortex-M4 の場合には実行サイクル数 1 となって、少し得をします。

2018-02-26

新版FM音源プログラム (17)

armcc の「エンベデッド・アセンブラ」では、関数まるごとアセンブリ言語で記述し、関数冒頭のレジスタ・セーブや、関数最後のレジスタ・リストアやリターン命令まですべてを記述する必要がありました。

gcc の「インライン・アセンブラ」では、C 言語での記述に混ざって「asm 文」が存在し、関数のプロローグ/エピローグ・コードは asm 文内では記述する必要がなく、C 側で自動的に生成されます。

また、デフォルトでは最適化の対象になっています。 (最適化オフにも設定できます)

アセンブラ命令も「裸」で記述することはできず、文字列の中に「閉じ込め」られた形で記述します。

下に gcc のインライン・アセンブラ機能を使って書いたプログラム・リストを示します。 (2018 年 2 月 28 日追記: ラベルを「ローカル・ラベル」(数字のみで構成されるラベル) に変更しました)

(2018 年 3 月 2 日追記: パイプライン・ストール解消のためプログラムの一部を変更しました)

// PSoC Creator 4.0 SP1 (gcc 4.9.3)
// 64 cycle / 2slot (CY8C4245 (CM0) @ 24 MHz, Flash wait=0)
// 67 cycle / 2slot (CY8C4245 (CM0) @ 48 MHz, Flash wait=1)
// 49 cycle / 2slot (CY8C5888 (CM3) @ 16 MHz, flash wait=0)
// 55 cycle / 2slot (CY8C5888 (CM3) @ 80 MHz, flash wait=4)

#define DEF_OFS(x) (offsetof(op_prop_t, x))

//
// gcc inline assembly language function
//
void calc_slot(op_prop_t *o_p, int num_slot)
{
  __asm__ __volatile__ (
// Use UAL (Unified Assembler Language) syntax    
    " .syntax  unified             \n\t"
    "1:                            \n\t"
//
//  series FM modulater slot
//
    "  ldr     r2,[r0,%[ph_inc]]   \n\t" 
    "  ldr     r3,[r0,%[ph_acc]]   \n\t" 
    "  ldr     r4,[r0,%[mod_in]]   \n\t" 
    "  ldr     r5,[r0,%[stab_p]]   \n\t" 
    "  adds    r2, r2,r3           \n\t" 
    "  ldrh    r3,[r0,%[ind_mask]] \n\t" 
    "  str     r2,[r0,%[ph_acc]]   \n\t" 
    "  adds    r4, r4,r2           \n\t" 
#if defined(__thumb2__) // ARMv7-M (Cortex-M3/M4)
    "  ands    r3, r3,r4,lsr %[acc_sft] \n\t"
#else                   // ARMv6-M (Cortex-M0)
    "  lsrs    r4, r4,%[acc_sft]   \n\t"
    "  ands    r3, r3,r4           \n\t"
#endif
    "  ldr     r2,[r0,%[ol_lin]]   \n\t"
    "  ldrsh   r3,[r5,r3]          \n\t"
//
// 2-tap FIR filter for feedback
//
#if defined(__thumb2__) // ARMv7-M (Cortex-M3/M4)
    "  ldrsh   r5,[r0,%[op_out]]   \n\t"
#else                   // ARMv6-M (Cortex-M0)
    "  ldrh    r5,[r0,%[op_out]]   \n\t"
    "  sxth    r5, r5              \n\t"
#endif
    "  muls    r2, r3,r2           \n\t"
    "  asrs    r4, r2,#15          \n\t"
    "  ldrh    r3,[r0,%[mod_mul]]  \n\t"
    "  strh    r4,[r0,%[op_out]]   \n\t"
    "  adds    r5, r5,r4           \n\t"
    "  muls    r3, r5,r3           \n\t"
    "  str     r3,[r0,%[mod_in]]   \n\t"
//
// series FM carrier slot
//
    "  ldr     r2,[r0,%[ph_inc]+%c[S]]  \n\t"
    "  ldr     r3,[r0,%[ph_acc]+%c[S]]  \n\t"
    "  ldrh    r5,[r0,%[mod_mul]+%c[S]] \n\t"
    "  adds    r2, r2,r3                \n\t"
    "  str     r2,[r0,%[ph_acc]+%c[S]]  \n\t"
#if defined(__thumb2__) // ARMv7-M (Cortex-M3/M4)
    "  mla     r4, r4, r5, r2           \n\t"
#else                   // ARMv6-M (Cortex-M0)
    "  muls    r4, r5,r4                \n\t"
    "  adds    r4, r4,r2                \n\t"
#endif
    "  ldrh    r3,[r0,%[ind_mask]+%c[S]] \n\t"
    "  ldr     r5,[r0,%[stab_p]+%c[S]]  \n\t" 
#if defined(__thumb2__) // ARMv7-M (Cortex-M3/M4)
    "  and     r3, r3,r4,lsr %[acc_sft] \n\t" 
#else                   // ARMv6-M (Cortex-M0)
    "  lsrs    r4, r4,%[acc_sft]        \n\t" 
    "  ands    r3, r4,r3                \n\t" 
#endif
    "  ldr     r2,[r0,%[ol_lin]+%c[S]]  \n\t" 
    "  ldrsh   r3,[r5,r3]               \n\t" 
    "  muls    r2, r3,r2                \n\t" 
    "  asrs    r2, r2,#15               \n\t" 
    "  strh    r2,[r0,%[op_out]+%c[S]]  \n\t" 
    "  adds    r0, r0,%[S]*2            \n\t" 
    "  subs    r1, r1,#2                \n\t" 
    "  bgt     1b                       \n\t" 
   
// output parameter list
    : "+r" (o_p),      // [arg1] = r0 = (op_prop_t *) o_p
      "+r" (num_slot)  // [arg2] = r1 = (int) num_slot
    : [acc_sft]  "I" (PH_ACC_FRAC_BITS-1),
// input parameter list ("I" for immediate constant)
      [ph_inc]   "I" (DEF_OFS(ph_inc)),
      [ph_acc]   "I" (DEF_OFS(ph_acc)),
      [mod_in]   "I" (DEF_OFS(mod_in)),
      [stab_p]   "I" (DEF_OFS(stab_p)),
      [ind_mask] "I" (DEF_OFS(ind_mask)),
      [op_out]   "I" (DEF_OFS(op_out)),
      [ol_lin]   "I" (DEF_OFS(ol_lin)),
      [mod_mul]  "I" (DEF_OFS(mod_mul)),
      [S]        "I" (sizeof(op_prop_t))
// clobber reg list
    : "r2", "r3", "r4", "r5", "cc", "memory" // clobber reg list
  );// __asm__
} // void calc_slot()

(拡張) インライン・アセンブラの構文は、

  __asm__ __volatile__ ( 
    アセンブラ命令ストリング 
  : 出力パラメタ・リスト  
  : 入力パラメタ・リスト 
  : クロバー・リスト
)

となっています。 (「__asm__」は「asm」でもよい)

デフォルトでは最適化の対象になりますが、「__volatile__」(あるいは「volatile」) を付けると最適化の対象から外れます。

アセンブラ命令ストリング」はアセンブリ言語の命令を記述している文字列です。 

これは「単一の文字列」の必要がありますが、「ホワイト・スペース」のみで区切られた複数の文字列を連結して単一の文字列にする C コンパイラの機能を利用して、

"  ldr  r2, [r0, #4] \n\t"
"  adds r3, r3, r2   \n\t"

のように、複数行に渡って書くこともできます。

「出力パラメタ・リスト」、「入力パラメタ・リスト」は、アセンブラレジスタ名などと、C 言語での変数名などとの対応を指示するものです。

古いフォーマットでは、

  制約文字列 (C の式), 制約文字列 (C の式), ...

というように記述します。

最初の要素に対するアクセスでは、アセンブラ記述としては「%0」として表します。 以下、同様に %1, %2, ... となります。

「制約文字列」は、そのパラメタの種別を表すもので、代表的には "r" が「汎用レジスタ」を表します。

「C の式」はそのパラメタに対応する C の式です。 出力パラメタがレジスタの場合には、「変数名」などの「左辺値」の必要があります。

「クロバー・リスト」は、入出力パラメタとしては扱われないけれど、アセンブラプログラム中で使用されるレジスタなどのリストを C コンパイラに示して、レジスタ割り付けやセーブ/リストア処理などの参考にするものです。

新しいフォーマットでは、

  [シンボル名]  制約文字列 (C の式),
  [シンボル名]  制約文字列 (C の式), ...

と記述します。

「シンボル名」はアセンブラ記述として参照するときのシンボル名を定義するもので、アセンブラ記述中では %[シンボル名] としてアクセスします。

上のスロット計算関数では、呼び出し側から r0 に第一引数の (op_prop_t *) o_p 、r1 に第二引数の (int) num_slot がセットされて関数が呼ばれた状態のまま、関数側ではレジスタの移動などはありません。

一応、出力パラメタ・リストに制約文字列 "+r" として、入出力に使うレジスタとして %0, %1 を指定していますが、アセンブラプログラム中では r0, r1 に決め打ちで記述しており、 C 側としてもプロローグ/エピローグ・コードだけで、r0, r1 については触らないコードが出力されています。

制約文字列に "I" を指定すると、レジスタではなく、0 〜 255 の範囲のイミディエイト数値として扱われます。 この場合、対応する C の式はコンパイル時に定数値にまで還元可能な式の必要があります。

たとえば、

 [ph_acc] "I" (offsetof(op_prop_t, ph_acc))

と指定すると、アセンブラ命令としては、

    ldr  r2, [r0, %[ph_acc]]

という記述で最終的には、

    ldr  r2, [r0, #4]

に変換されます。

(offsetof(op_prop_t, ... )) を並べるのは面倒なので、armcc のエンベデッド・アセンブラの場合と同様に、「DEF_OFS()」マクロ

#define DEF_OFS(x) (offsetof(op_prop_t, x))

を定義しています。 armcc の場合との違いは、「__cpp()」があるかないかです。

%[] の指定で、イミディエイト数値を表す "#" (ナンバー記号) が先頭に付加されて数値に置き換えられます。

%c[] と指定すると、"#" が付加されずに数値のみに変換されます。

op_prop_t のサイズを

[S] "I" (sizeof(op_prop_t))

として定義してあるので、op_prop[] ポインタを動かさずにオペレータ 1 側の構造体メンバにアクセスするのに

    ldr r2, [r0, %[ph_acc] + %c[S]]

という記述で、最終的に

    ldr r2, [r0, #4 + 28]

という形に変換されます。

2018-02-24

新版FM音源プログラム (16)

オペレータ処理をアセンブリ言語で書く場合、C プログラム・ソースの構造体アクセスの

  o_p->ph_acc 

のような表現は、アセンブリ言語ソースとしては最終的には

 ldr r2, [r0, #4] 

のように「定数オフセット」にまで変換される必要があります。

C プログラム・ソースと独立してソースをアセンブラに喰わせる場合には、C 側で構造体のメンバの配置に変更があった場合にはアセンブリ言語ソースでの定義を手作業で修正する必要があります。

そのような事態を避けるため、「インライン・アセンブラ」のような、C コンパイラの機能に含まれたアセンブリ言語機能を利用することにしました。

ARM/Keil の μVison (armcc)では「エンベデッド・アセンブラ」、gcc では「インライン・アセンブラ」を使っています。

armcc 版のソースを下に示します。 (2018 年 3 月 2 日追記: 2 ヵ所ある

  ldrsh   r3, [r5, r3]

の部分でパイプライン・ストールが生じていたのを命令の順序の入れ替えにより解消しました。)

#include "slot.h"
    
#define DEF_OFS(x) _##x equ __cpp(offsetof(op_prop_t, x))

//
// armcc embedded assembly language function
//
__asm void calc_slot(op_prop_t *o_p, int num_slot)
{ 
; define offset of op_prop_t struct members
DEF_OFS(ph_inc)
DEF_OFS(ph_acc)
DEF_OFS(mod_in)
DEF_OFS(stab_p)
DEF_OFS(ind_mask)
DEF_OFS(op_out)
DEF_OFS(ol_lin)
DEF_OFS(mod_mul)

; op_prop_t struct size
S       equ     __cpp(sizeof(op_prop_t))
    
; phase accum. shift amount 
acc_sft equ     __cpp(PH_ACC_FRAC_BITS-1)

;
; function entry point
;
        align   16
        push    {r4, r5, lr}        ; save registers
loop
;
; series FM modulater slot
;
        ldr     r2,[r0,#_ph_inc]    ; r2 = ph_inc
        ldr     r3,[r0,#_ph_acc]    ; r3 = ph_acc
        ldr     r4,[r0,#_mod_in]    ; r4 = mod_in
        ldr     r5,[r0,#_stab_p]    ; r5 = stab_p
        adds    r2, r2,r3           ; r2 = ph_inc + ph_acc
        ldrh    r3,[r0,#_ind_mask]  ; r3 = ind_mask
        str     r2,[r0,#_ph_acc]    ; update ph_acc
        adds    r4, r4,r2           ; r4 = ph_inc + ph_acc + mod_in (= phh)
#if (4 == __TARGET_ARCH_THUMB)      // ARMv7-M (Cortex-M3/M4)
        and     r3, r4,lsr #acc_sft ; r3 & (r4 >> 8)
#else                               // ARMv6-M (Cortex-M0)
        lsrs    r4, r4,#acc_sft     ; r4 >> 8
        ands    r3, r3,r4           ; r3 & r4
#endif      
        ldr     r2,[r0,#_ol_lin]    ; r2 = ol_lin
        ldrsh   r3,[r5,r3]          ; r3 = *(int16_t *)(stab_p + r3)
;
; 2-tap FIR filter for feedback
;
#if (4 == __TARGET_ARCH_THUMB)      // ARMv7-M (Cortex-M3/M4)
        ldrsh   r5,[r0,#_op_out]    ; r5 = op_out (= prev_out)
#else                               // ARMv6-M (Cortex-M0)
        ldrh    r5,[r0,#_op_out]    ; r5 = op_out (= prev_out)
        sxth    r5, r5              ; sign extend
#endif      
        muls    r2, r3,r2           ; r2 = r3 * ol_lin
        asrs    r4, r2,#15          ; r4 = (r2 >> 15) (= out)
        ldrh    r3,[r0,#_mod_mul]   ; r3 = mod_mul
        strh    r4,[r0,#_op_out]    ; op_out = out
        adds    r5, r5,r4           ; r5 = out + prev_out
        muls    r3, r5,r3           ; r3 = r5 * mod_mul
        str     r3,[r0,#_mod_in]    ; mod_in = r3
;
; series FM carrier slot
;
        ldr     r2,[r0,#_ph_inc+S]  ; r2 = ph_inc
        ldr     r3,[r0,#_ph_acc+S]  ; r3 = ph_acc
        ldrh    r5,[r0,#_mod_mul+S] ; r5 = mod_mul
        adds    r2, r2,r3           ; r2 = ph_inc + ph_acc
        str     r2,[r0,#_ph_acc+S]  ; update ph_acc
#if (4 == __TARGET_ARCH_THUMB)      // ARMv7-M (Cortex-M3/M4)
        mla     r4, r4, r5, r2      ; r4 = (r4 * r5) + r2
#else                               // ARMv6-M (Cortex-M0)
        muls    r4, r5,r4           ; r4 = out * mod_mul
        adds    r4, r4,r2           ; r4 = ph_inc + ph_acc + out (= phh)
#endif      
        ldrh    r3,[r0,#_ind_mask+S] ; r3 = ind_mask
        ldr     r5,[r0,#_stab_p+S]  ; r5 = stab_p
#if (4 == __TARGET_ARCH_THUMB)      // ARMv7-M (Cortex-M3/M4)
        and     r3, r4,lsr #acc_sft ; r3 & (r4 >> 8)
#else                               // ARMv6-M (Cortex-M0)
        lsrs    r4, r4,#acc_sft     ; r4 >> 8
        ands    r3, r4,r3           ; r3 & r4
#endif      
        ldr     r2,[r0,#_ol_lin+S]  ; r2 = ol_lin
        ldrsh   r3,[r5,r3]          ; r3 = *(int16_t *)(stab_p + r3)
        muls    r2, r3,r2           ; r2 = r3 * ol_lin
        asrs    r2, r2,#15          ; r2 >>= 15 (= out)
        strh    r2,[r0,#_op_out+S]  ; op_out = out
        adds    r0, r0,#(2*S)       ; advance to next slot pair
        subs    r1, r1,#2           ; decrement loop counter
        bgt     loop                ; more to do
        pop     {r4, r5, pc}        ; return
} // __asm void calc_slot()

インクルードされている「slot.h」ファイルの主要な部分を下に示します。

#include <stdint.h>
#include <stddef.h>

#define MAXSLOT  (32)

// phase accumlator bit width assign
#define PH_ACC_INT_BITS  (10) // integer part (sine ROM address part)
#define PH_ACC_FRAC_BITS (9)  // fraction part
#define PH_ACC_BITS      (PH_ACC_INT_BITS + PH_ACC_FRAC_BITS)

// operator property record
typedef struct tag_op_entry_t {
  uint32_t  ph_inc;    // phase accum. increment
  uint32_t  ph_acc;    // phase accumulator
  int32_t   mod_in;    // phase modulation input
  int16_t  *stab_p;    // (full) sine tale pointer
  uint16_t  ind_mask;  // table index mask
  int16_t   op_out;    // operator output value
  uint32_t  ol_lin;    // linear output level
  uint16_t  mod_mul;   // feedback / modulator connection
}  op_prop_t;

// SLOT calculation
#if !defined(__CC_ARM) // attribute for gcc
__attribute__((aligned(16)))
#endif
void calc_slot(op_prop_t *o_p,       // pointer to op_prop[] array
               int        num_slot); // number of slot

「エンベデッド・アセンブラ」は、

  __asm 型名 関数名(引数並び) { ... }

というフォーマットの宣言で定義され、関数全体をアセンブリ言語で記述します。 C コンパイラによる最適化は適用されません。

エンベデッド・アセンブラでは、

  __cpp( 式 )

というビルトイン関数が用意されています。

この関数には、コンパイル時に定数にまで変換可能な C 言語で表現した式を評価して、その評価結果の定数値で置き換える機能があります。 これを利用して、

  ldr r2, [r0, #__cpp(offsetof(op_prop_t, ph_acc))]

と書くと、最終的には

  ldr r2, [r0, #4]

のように変換されます。

「offsetof()」関数の定義は「stddef.h」ファイルに含まれているので、このファイルをインクルードしておく必要があります。

すべての構造体メンバに対してこのような表現を書き連ねるのは面倒なので、C のマクロとして、

#define DEF_OFS(x) _##x equ __cpp(offsetof(op_prop_t, x))

と定義してあります。 たとえば、

DEF_OFS(ph_acc)

とすれば、

_ph_acc equ __cpp(offsetof(op_prop_t, ph_acc))

と展開され、アセンブラ側の「_ph_acc」というシンボルが定義され、

  ldr r2, [r0, #_ph_acc]

のように記述できます。

2018-02-22

新版FM音源プログラム (15)

今回は各種のチップ/コンパイラの組み合わせのオブジェクトに対する逆アセンブル・リストを掲載します。

もとの C ソース・ファイルでの記述を少し変えると、コンパイル後のオブジェクトはかなり大きく変化するので、あまり細部に渡る追及はしないでおきます。

μVision V5.21a (armcc コンパイラ) でコンパイルした calc_slot() 関数を逆アセンブルしたものを下に示します。

Cortex-M4 コアの STM32F446、STM32F407、STM32F303 に共通です。

# uVision V5.21a (armcc v5.06 update 3)
# options: -O3 -cpu Cortex-M4.fp

08000ed4 <calc_slot>:
 8000ed4:   e92d 4030       stmdb   sp!, {r4, r5, lr}
 8000ed8:   e9d0 2300       ldrd    r2, r3, [r0]
 8000edc:   441a            add     r2, r3
 8000ede:   6042            str     r2, [r0, #4]
 8000ee0:   6883            ldr     r3, [r0, #8]
 8000ee2:   68c4            ldr     r4, [r0, #12]
 8000ee4:   441a            add     r2, r3
 8000ee6:   8a03            ldrh    r3, [r0, #16]
 8000ee8:   1e89            subs    r1, r1, #2
 8000eea:   ea03 2312       and.w   r3, r3, r2, lsr #8
 8000eee:   5ee3            ldrsh   r3, [r4, r3]
 8000ef0:   6942            ldr     r2, [r0, #20]
 8000ef2:   435a            muls    r2, r3
 8000ef4:   13d3            asrs    r3, r2, #15
 8000ef6:   f9b0 2012       ldrsh.w r2, [r0, #18]
 8000efa:   8243            strh    r3, [r0, #18]
 8000efc:   8b04            ldrh    r4, [r0, #24]
 8000efe:   441a            add     r2, r3
 8000f00:   4354            muls    r4, r2
 8000f02:   6084            str     r4, [r0, #8]
 8000f04:   8e84            ldrh    r4, [r0, #52]   ; 0x34
 8000f06:   f100 021c       add.w   r2, r0, #28
 8000f0a:   435c            muls    r4, r3
 8000f0c:   e9d2 3500       ldrd    r3, r5, [r2]
 8000f10:   442b            add     r3, r5
 8000f12:   6203            str     r3, [r0, #32]
 8000f14:   4423            add     r3, r4
 8000f16:   8d84            ldrh    r4, [r0, #44]   ; 0x2c
 8000f18:   6a85            ldr     r5, [r0, #40]   ; 0x28
 8000f1a:   ea04 2413       and.w   r4, r4, r3, lsr #8
 8000f1e:   6b03            ldr     r3, [r0, #48]   ; 0x30
 8000f20:   5f2c            ldrsh   r4, [r5, r4]
 8000f22:   4363            muls    r3, r4
 8000f24:   13db            asrs    r3, r3, #15
 8000f26:   85c3            strh    r3, [r0, #46]   ; 0x2e
 8000f28:   3038            adds    r0, #56 ; 0x38
 8000f2a:   2900            cmp     r1, #0
 8000f2c:   dcd4            bgt.n   8000ed8 <calc_slot+0x4>
 8000f2e:   bd30            pop     {r4, r5, pc}

gcc ツールチェインの「objdump」を使っているため、armcc の .o オブジェクト・ファイルには対応しておらず、リンクずみの .axf ファイルを入力としています。

アーキテクチャは 32/16 ビット命令の thumb2 インストラクション・セットを持つ ARMv7-M ですが、多くが 16 ビット thumb 命令へコンパイルされていて、ARMv6-M にはない 32 ビット thumb2 命令は少数となっています。

Atollic TrueSTUDIO for STM32 v9.0.0 (gcc 6.3.1) によるコンパイル結果を逆アセンブルしたものを下に示します。

# Atollic TrueSTUDIO for STM32 v9.0.0 (gcc 6.3.1)
# options: -Os -mcpu=cortex-m4

00000000 <calc_slot>:
   0:   b530            push    {r4, r5, lr}
   2:   3038            adds    r0, #56 ; 0x38
   4:   f850 2c34       ldr.w   r2, [r0, #-52]
   8:   f850 3c38       ldr.w   r3, [r0, #-56]
   c:   f850 5c18       ldr.w   r5, [r0, #-24]
  10:   4413            add     r3, r2
  12:   f850 2c30       ldr.w   r2, [r0, #-48]
  16:   f840 3c34       str.w   r3, [r0, #-52]
  1a:   4413            add     r3, r2
  1c:   f830 2c28       ldrh.w  r2, [r0, #-40]
  20:   ea02 2313       and.w   r3, r2, r3, lsr #8
  24:   f850 2c2c       ldr.w   r2, [r0, #-44]
  28:   5ed2            ldrsh   r2, [r2, r3]
  2a:   f850 3c24       ldr.w   r3, [r0, #-36]
  2e:   4353            muls    r3, r2
  30:   f930 2c26       ldrsh.w r2, [r0, #-38]
  34:   13db            asrs    r3, r3, #15
  36:   189c            adds    r4, r3, r2
  38:   f830 2c20       ldrh.w  r2, [r0, #-32]
  3c:   f820 3c26       strh.w  r3, [r0, #-38]
  40:   4362            muls    r2, r4
  42:   f840 2c30       str.w   r2, [r0, #-48]
  46:   f850 2c1c       ldr.w   r2, [r0, #-28]
  4a:   f830 4c04       ldrh.w  r4, [r0, #-4]
  4e:   442a            add     r2, r5
  50:   f840 2c18       str.w   r2, [r0, #-24]
  54:   fb03 2204       mla     r2, r3, r4, r2
  58:   f830 3c0c       ldrh.w  r3, [r0, #-12]
  5c:   ea03 2312       and.w   r3, r3, r2, lsr #8
  60:   f850 2c10       ldr.w   r2, [r0, #-16]
  64:   5ed2            ldrsh   r2, [r2, r3]
  66:   f850 3c08       ldr.w   r3, [r0, #-8]
  6a:   3902            subs    r1, #2
  6c:   4353            muls    r3, r2
  6e:   13db            asrs    r3, r3, #15
  70:   2900            cmp     r1, #0
  72:   f820 3c0a       strh.w  r3, [r0, #-10]
  76:   f100 0038       add.w   r0, r0, #56     ; 0x38
  7a:   dcc3            bgt.n   4 <calc_slot+0x4>
  7c:   bd30            pop     {r4, r5, pc}

32 ビット thumb2 命令が多用されていますが、そのほとんどがマイナス・オフセットを持つ ldr/ldrh/ldrsh/str/strh 命令で、シフト付きオペランドなどの 32 ビット thumb2 固有の命令は少数となっています。

リンク前のオブジェクト・ファイル「calc_slot.o」を逆アセンブルしています。

μVision V5.21a (armcc v5.06 update 3) で LPC1114 (Cortex-M0, ARMv6-M) 用にコンパイルした結果の逆アセンブル・リストを下に示します。

# uVision v5.21a (armcc v5.06 update 3)
# options: -O3 -cpu Cortex-M0

0000049c <calc_slot>:
     49c:   b530            push    {r4, r5, lr}
     49e:   c80c            ldmia   r0!, {r2, r3}
     4a0:   3808            subs    r0, #8
     4a2:   189a            adds    r2, r3, r2
     4a4:   6042            str     r2, [r0, #4]
     4a6:   6883            ldr     r3, [r0, #8]
     4a8:   68c4            ldr     r4, [r0, #12]
     4aa:   189a            adds    r2, r3, r2
     4ac:   8a03            ldrh    r3, [r0, #16]
     4ae:   0a12            lsrs    r2, r2, #8
     4b0:   4013            ands    r3, r2
     4b2:   5ee3            ldrsh   r3, [r4, r3]
     4b4:   6942            ldr     r2, [r0, #20]
     4b6:   1e89            subs    r1, r1, #2
     4b8:   435a            muls    r2, r3
     4ba:   13d3            asrs    r3, r2, #15
     4bc:   2212            movs    r2, #18
     4be:   5e82            ldrsh   r2, [r0, r2]
     4c0:   8243            strh    r3, [r0, #18]
     4c2:   8b04            ldrh    r4, [r0, #24]
     4c4:   18d2            adds    r2, r2, r3
     4c6:   4354            muls    r4, r2
     4c8:   6084            str     r4, [r0, #8]
     4ca:   8e84            ldrh    r4, [r0, #52]   ; 0x34
     4cc:   4602            mov     r2, r0
     4ce:   321c            adds    r2, #28
     4d0:   435c            muls    r4, r3
     4d2:   ca28            ldmia   r2!, {r3, r5}
     4d4:   3a08            subs    r2, #8
     4d6:   18eb            adds    r3, r5, r3
     4d8:   6053            str     r3, [r2, #4]
     4da:   191b            adds    r3, r3, r4
     4dc:   8a14            ldrh    r4, [r2, #16]
     4de:   0a1b            lsrs    r3, r3, #8
     4e0:   401c            ands    r4, r3
     4e2:   68d5            ldr     r5, [r2, #12]
     4e4:   6953            ldr     r3, [r2, #20]
     4e6:   5f2c            ldrsh   r4, [r5, r4]
     4e8:   3038            adds    r0, #56 ; 0x38
     4ea:   4363            muls    r3, r4
     4ec:   13db            asrs    r3, r3, #15
     4ee:   8253            strh    r3, [r2, #18]
     4f0:   2900            cmp     r1, #0
     4f2:   dcd4            bgt.n   49e <calc_slot+0x2>
     4f4:   bd30            pop     {r4, r5, pc}

TrueSTUDIO for STM32 v9.0.0 は Atollic が STMicro に買収されてからリリースされたバージョンで、制限なし版が無償で提供されていますが、当然 STM32 シリーズのサポートのみとなり、他社のチップには対応していません。

そんなわけで、LPC1114 版は μVision によるコンパイル結果のみを示しています。

細かいことは言わないと言っておきながら、細かい部分に触れますが、冒頭部分、

     49e:   c80c            ldmia   r0!, {r2, r3}
     4a0:   3808            subs    r0, #8

は、連続されて格納されている ph_inc、ph_acc を r2、r3 にロードするコードですが、16 ビット thumb 命令の範囲では、LDM (LoaD Multiple) 命令は「IA」(Increment After) 付き、および「!」(ライトバック・サフィックス) 付きに限られ、命令の実行後に op_prop[] ポインタの r0 が 8 進んでしまうのを、次の subs r0,#8 で戻しています。

もとの C ソースで、サイン波テーブルのアクセスに、

phh >>=  (PH_ACC_FRAC_BITS-1);
out = *(int16_t *)((uint8_t *)o_p->stab_p + (o_p->ind_mask & phh));

のように、元々 (int16_t *) である o_p->stab_p を (uint8_t *) でキャストし、バイト・アドレッシングでポインタのオフセットを計算してから、それを再び (int16_t *) にキャストし直すという、一見変な処理をしています。

これは、Cortex-M0 (ARMv6-M) 用にコンパイルした場合に余計なシフト命令が挿入されないようにするためです。

Cortex-M3/M4 (ARMv7-M) では、thumb2 命令の中に「シフト付きオペランド」を持つ命令があるので、r4 にサイン波テーブルアドレス、r3 に「ハーフワード・インデクス」としてのオフセットが格納されている場合、

  ldrsh  r2, [r4, r3, lsl #1]

のように、1 命令で「ハーフワード・インデクス」から「バイト・オフセット」に変換しながらアクセスすることができます。

それに対して「シフト付きオペランド」を持たない Cortex-M0 (ARMv6-M) では、

  lsls   r3, r3, #1
  ldrsh  r2, [r4, r3]

のようにシフト命令が強制的に挿入されて「ハーフワード・インデクス」から「バイト・オフセット」に変換されます。

フェーズ・アキュムレータの値からサイン波テーブルのオフセットを求めるのに、シフトおよびマスクが欠かせないので、それらの処理をあらかじめバイト・オフセットの領域で計算しておけば、「余分」なシフト命令が不要になります。