新版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]

のように記述できます。