新版FM音源プログラム (25)
armcc のエンベデッド・アセンブリ関数で書いた「アキュムレータ機能」付きのスロット計算関数「acc_calc_slot()」の Cortex-M0 版のリストを下に示します。
#include "slot.h"
#define DEF_OFS(x) _##x equ __cpp(offsetof(op_prop_t, x))
//
// armcc embedded assembly language function
//
__asm int32_t acc_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)
DEF_OFS(L_vol)
DEF_OFS(R_vol)
; 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)
; L/R volume shift amount
vol_sft equ __cpp(LR_VOL_SHIFT)
;
; function entry point
;
align 16
push {r4-r7, lr} ; save registers
movs r6,#0 ; clear r6 = acc_R
mov r7,r6 ; clear r7 = acc_L
loop
;
; series FM modulator 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)
lsrs r4, r4,#acc_sft ; r4 >> 8
ands r3, r3,r4 ; r3 & r4
ldr r2,[r0,#_ol_lin] ; r2 = ol_lin
ldrsh r3,[r5,r3] ; r3 = *(int16_t *)(stab_p+r3)
;
; 2-tap FIR filter for feedback
;
ldrh r5,[r0,#_op_out] ; r5 = op_out (= prev_out)
sxth r5, r5 ; sign extend
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
muls r5, r4,r5 ; r4 = out * mod_mul
adds r2, r5,r2 ; r2 = ph_inc+ph_acc+mod (= phh)
ldrh r3,[r0,#_ind_mask+S] ; r3 = ind_mask
ldr r5,[r0,#_stab_p+S] ; r5 = stab_p
lsrs r2, r2,#acc_sft ; r2 >> 8
ands r3, r2,r3 ; r3 & r2
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
;
; accumulator
;
; r0 = *op_prop, r1 = num_slot
; r2 = op_out1, r4 = op_out0
; r6 = acc_R, r7 = acc_L
; r3, r5 = free
;
ldr r3,[r0,#_R_vol+S] ; r3 = R_vol0:R_vol1
lsrs r5, r3, #16 ; r5 = R_vol0
uxth r3, r3 ; r3 = R_vol1
muls r3, r2,r3 ; r3 *= op_out1
muls r5, r4,r5 ; r5 *= op_out0
adds r6, r6,r3 ; acc_R += r3
adds r6, r6,r5 ; acc_R += r5
ldr r3,[r0,#_L_vol+S] ; r3 = L_vol0:L_vol1
lsrs r5, r3, #16 ; r5 = L_vol0
uxth r3, r3 ; r3 = L_vol1
muls r3, r2,r3 ; r3 *= op_out1
muls r5, r2,r5 ; r5 *= op_out0
adds r7, r7,r3 ; acc_L += r3
adds r7, r7,r5 ; acc_L += r5
;
adds r0, r0,#(2*S) ; advance to next slot pair
subs r1, r1,#2 ; decrement loop counter
bgt loop ; more to do
;
; post scaling and saturate to 16-bit
;
ldr r3,=0x7fff ; r3 = 0x00007fff
mvns r4, r3 ; r4 = 0xffff8000
; ; saturate acc_R
asrs r6, r6, #vol_sft ; acc_R post scaling
bmi neg_r ; branch if negative
cmp r6, r3 ; compare to 0x00007fff
blt sat_l ; no pos overflow
movs r6, r3 ; r6 = 0x00007fff
b sat_l
;
neg_r
cmp r6, r4 ; compare to 0xffff8000
bge sat_l ; no neg overflow
movs r6, r4 ; r6 = 0xffff8000
sat_l ; saturate acc_L
asrs r7, r7, #vol_sft ; acc_L post scaling
bmi neg_l ; branch if negative
cmp r7, r3 ; compare to 0x00007fff
blt pack ; no pos overflow
movs r7, r3 ; r7 = 0x00007fff
b pack
;
neg_l
cmp r7, r4 ; compare to 0xffff8000
bge pack ; no neg overflow
movs r7, r4 ; r7 = 0xffff8000
pack ; pack (acc_L:acc_R) to r0
lsls r7, r7, #16 ; acc_L in top halfword of r7
uxth r0, r6 ; acc_R in bottom halfword of r0
orrs r0, r0,r7 ; combine them
pop {r4-r7, pc} ; return
} // __asm void calc_slot()これは、まず、もとの calc_slot() 関数のループ内でオペレータ 0 の出力を R4 に残して、スロット 1 の計算が終了して結果が得られるまで保持するように途中のレジスタ割り付けを修正しました。
この段階では追加の命令/計算サイクル数の増加はありません。
その後、SIMD (Single Instruction Multiple Data) 命令や積和命令のない Cortex-M0 で、Cortex-M4 での SIMD 計算や積和計算を「バラ」す形で以下の命令を追加しました。
- ldr r3,[r0,#_R_vol+S] — 2 cycle
- lsrs r5, r3, #16 — 1 cycle
- uxth r3, r3 — 1 cycle
- muls r3, r2,r3 — 1 cycle
- muls r5, r4,r5 — 1 cycle
- adds r6, r6,r3 — 1 cycle
- adds r6, r6,r5 — 1 cycle
- ldr r3,[r0,#_L_vol+S] — 2 cycle
- lsrs r5, r3, #16 — 1 cycle
- uxth r3, r3 — 1 cycle
- muls r3, r2,r3 — 1 cycle
- muls r5, r2,r5 — 1 cycle
- adds r7, r7,r3 — 1 cycle
- adds r7, r7,r5 — 1 cycle
追加された 14 個の命令によるループ 1 回 (スロット 2 個) 当たりの追加サイクル数は「16」となります。 実測値でも 16 サイクルの増加でした。
(LPC1114 (Cortex-M0), 20 MHz, flash wait=0, NSLOT=64 の条件で、アキュムレータ処理なしで 63 サイクル、アキュムレータ処理ありで 79 サイクル)
「アキュムレータ」として R6 / R7 を使っています。
最初の ldr/lsrs/uxth 命令でパックされたボリューム値 (L_vol / R_vol) をロード/アンパックしています。
Cortex-M0 では、ldr 命令を連続させてもクロック数は削減されないので、データが必要な時期になってからデータをロードしています。
Cortex-M0 では積和命令がないので、個別の muls/adds 命令を組み合わせて積和演算を構成しています。
「飽和演算命令」も Cortex-M0 にはないので、「プログラム」によって -32768 〜 +32767 への「クリッピング」を行なっています。