新版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]
のように記述できます。