ソフト S/PDIF トランスミッタ (10)
今回はオーディオ・データの BMC エンコード・プログラムの本体についてです。
DMA 機能を持つ Cortex-M0 プロセッサとして、手持ちの Nuvoton NUC120LE3AN についてもプログラムを作ってみたところ、Cortex-M3/M4 用と同じプログラム記述では効率がよくなかったので、プログラムの一部を M0 用と M3/M4 用とで違う記述にしてあります。
ちなみに (DDA によるサイン波発生部も含んで) CPU 負荷率は 84 MHz クロックの STM32F401 で 6.2 %、50 MHz クロックの NUC120LE3AN で 20.3 % でした。
第一引数に 16ビット・オーディオ・データの配列を取り、第二引数にサブフレーム数を取る関数
spdif_encode_subframe_data()
のリストを下に示します。
// encode audio data for specified number of subframe(s) uint16_t spdif_encode_subframe_data(int16_t dat[], uint16_t nsf) { int i; uint16_t bmc_L, bmc_H, aux; // bitmap of biphase mark code const uint32_t BMC_LSB_MASK = (1U << 15); register volatile uint16_t *p; // TX buffer pointer p = spdif_txind.ptr; // get S/PDIF TX buffer pointer for (i = 0; i < nsf; i++) { // for subframe(s) // encode LSByte (d[0:7]) of audio data bmc_L = bmc8b[(uint8_t) dat[i]]; // encode MSByte (d[8:15]) of audio data bmc_H = bmc8b[(uint8_t)(dat[i] >> 8)]; #if (__CORTEX_M >= 0x03) // Cortex-M3/M4 processor ? // invert bmc of d[0:7] if (0 == bmc_H[15]) bmc_L ^= (~((int16_t)bmc_H) >> 16); // gimmick for Cortex-M3/M4 processor (ARMv7-M arch) to avoid conditional aux = (BMC_1000_0000 ^ (((uint32_t)((int16_t)bmc_L)) >> 17)); #else // Cortex-M0 processor (ARMv6-M arch) // invert bmc of d[0:7] if (0 == bmc_H[15]) bmc_L ^= ((bmc_H >> 15) + 0xFFFFU); // straightforward implementation for Cortex-M0 if (BMC_LSB_MASK & bmc_L) { // detect "1 start" aux = BMC_0000_0000; // no need to adjust parity } else { aux = BMC_1000_0000; // adjust parity } // if (BMC_LSB_MASK & bmc_L) { ... #endif // SPDIF TX buffer update for this subframe data // VUCP (4 bit) and preamble (4 bit) not changed (pointed by *(p)) // AUX (4 bit) + audio ext (4 bit) for parity correction *(p + 1) = aux; *(p + 2) = bmc_L; // audio data d[0:7] (8 bit) *(p + 3) = bmc_H; // audio data d[8:15] (8 bit) p += 4; // advance TX buffer pointer by 4 } // for (i = 0; // advance S/PDIF TX buffer pointer spdif_txind.ptr = p; // 4 halfword (64 bit) used per subframe spdif_txind.room -= (4 * nsf); return(spdif_txind.room); } // uint16_t spdif_encode_subframe_data()
2 ch ステレオ・データは 2 サブフレーム分を占めますから、ステレオ・データひとつ当たり 2 サブフレームを指定することになります。
出力側との同期のために、下のような構造体を使用しています。 詳しい説明は後の記事に回すことにして、ここでは簡単に述べます。
// S/PDIF TX buffer index structure typedef struct tag_spdif_txind_t { // room for ouput data (count in terms of halfword) volatile uint16_t room; // S/PDIF TX buffer pointer volatile uint16_t *ptr; } spdif_txind_t; static spdif_txind_t spdif_txind;
「spdif_txind.room」の値がゼロのときは出力バッファに「空き」がなく、データ発生側が待つ必要があることを示しています。
「spdif_txind.room」の値がゼロでないときは出力バッファに「空き」があり、その値はハーフワード (16 ビット・ワード)単位で数えた空きの要素数になっています。
「spdif_txind.room」の値は spdif_encode_subframe_data() 関数を呼び出す側ですでにチェック済みとして、関数の中ではチェックしていません。
「spdif_txind.ptr」は出力バッファの中の (空いていて) 書き込み可能なメモリへのポインタです。
関数の最後の部分で、「spdif_txind.room」の値と「spdif_txind.ptr」の値が更新されます。
前回の記事で述べたように、オーディオ・データの MS バイト側の符号化パターン (変数 bmc_H に格納される) は「0 終わり」パターンなので、変換テーブル bmc8b[] から読み出した後に値をいじる必要はありません。
オーディオ・データの LS バイト側の符号化パターン (変数 bmc_L に格納される) については bmc_H の b15 の値により論理反転が必要になりますが、ちょっと技巧的な方法で実現しています。
パリティを調整するパターン (変数 aux に格納される) については M3/M4 用についてはさらに技巧的な方法、M0 に対しては素直な条件分岐で実現しています。
Cortex-M3/M4 用のプログラムを逆アセンブルし、コメントを追加したリストを下に示します。
Keil/ARM MDK-ARM Lite V4.53.0.0 コンパイラを使い、最適化オプションは「-O2」を指定してあります。
08002518 <spdif_encode_subframe_data>: 8002518: b5f0 push {r4, r5, r6, r7, lr} 800251a: f8df c058 ldr.w ip, [pc, #88] ; 8002574 800251e: 2300 movs r3, #0 8002520: f8df e054 ldr.w lr, [pc, #84] ; 8002578 8002524: f24b 3633 movw r6, #45875 ; 0xb333 8002528: f8dc 2004 ldr.w r2, [ip, #4] 800252c: e014 b.n 8002558 ; ; r0 = &(dat[0]), r1 = nsf, r2 = p, r3 = i, ; r4 = bmc_L, r5 = bmc_H, r6 = #0xb333, r7 = aux, ; r12(ip) = &spdif_txind.room, r14(lr) = &(bmc8b[0]) ; ; (note: r4,r5,r6,r7 used for misc. purpose) ; 800252e: f830 5013 ldrh.w r5, [r0, r3, lsl #1] 8002532: b2ec uxtb r4, r5 8002534: f3c5 2507 ubfx r5, r5, #8, #8 8002538: f83e 4014 ldrh.w r4, [lr, r4, lsl #1] 800253c: f83e 5015 ldrh.w r5, [lr, r5, lsl #1] 8002540: b22f sxth r7, r5 8002542: 43ff mvns r7, r7 8002544: ea84 4427 eor.w r4, r4, r7, asr #16 8002548: b227 sxth r7, r4 800254a: ea86 4757 eor.w r7, r6, r7, lsr #17 800254e: 8057 strh r7, [r2, #2] 8002550: 8094 strh r4, [r2, #4] 8002552: 80d5 strh r5, [r2, #6] 8002554: 3208 adds r2, #8 8002556: 1c5b adds r3, r3, #1 8002558: 428b cmp r3, r1 800255a: dbe8 blt.n 800252e 800255c: f8cc 2004 str.w r2, [ip, #4] 8002560: f8bc 0000 ldrh.w r0, [ip] 8002564: 4249 negs r1, r1 8002566: eb00 0081 add.w r0, r0, r1, lsl #2 800256a: f8ac 0000 strh.w r0, [ip] 800256e: f8bc 0000 ldrh.w r0, [ip] 8002572: bdf0 pop {r4, r5, r6, r7, pc} 8002574: 20000000 .word 0x20000000 8002578: 20000c9c .word 0x20000c9c
0x800252e から 0x8002554 までが i に関する for ループの内側の部分で、bmc_L に対する補正は
8002540: b22f sxth r7, r5 8002542: 43ff mvns r7, r7 8002544: ea84 4427 eor.w r4, r4, r7, asr #16
の 3 命令、aux に対する補正は
8002548: b227 sxth r7, r4 800254a: ea86 4757 eor.w r7, r6, r7, lsr #17
の 2 命令にコンパイルされています。
Cortex-M0 用のプログラムを逆アセンブルし、コメントを追加したリストを下に示します。
GCC 4.5.2 コンパイラを使い、最適化オプションは「-Os」を指定してあります。
00000570 <spdif_encode_subframe_data>: 570: b5f0 push {r4, r5, r6, r7, lr} 572: 4b13 ldr r3, [pc, #76] (5c0) 574: 685c ldr r4, [r3, #4] 576: 2300 movs r3, #0 578: 1c22 adds r2, r4, #0 57a: e015 b.n 5a8 ; ; r0 = &(dat[0]), r1 = nsf, r2 = p, ; r3 = i, r4 = &(spdif_txind.room), r5 = bmc_H, ; r6 = bmc_L, r7 = aux ; ; (note: r5,r6,r7 used for misc. purpose) ; 57c: 8807 ldrh r7, [r0, #0] 57e: 4d11 ldr r5, [pc, #68] (5c4) 580: b2fe uxtb r6, r7 582: 0a3f lsrs r7, r7, #8 584: 007f lsls r7, r7, #1 586: 0076 lsls r6, r6, #1 588: 5b76 ldrh r6, [r6, r5] 58a: 5b7d ldrh r5, [r7, r5] 58c: 0bef lsrs r7, r5, #15 58e: 3f01 subs r7, #1 590: 407e eors r6, r7 592: 0437 lsls r7, r6, #16 594: d501 bpl.n 59a 596: 4f0c ldr r7, [pc, #48] (5c8) 598: e000 b.n 59c 59a: 4f0c ldr r7, [pc, #48] (5cc) 59c: 8017 strh r7, [r2, #0] 59e: 3301 adds r3, #1 5a0: 80d6 strh r6, [r2, #6] 5a2: 3002 adds r0, #2 5a4: 8095 strh r5, [r2, #4] 5a6: 3208 adds r2, #8 5a8: 428b cmp r3, r1 5aa: d1e7 bne.n 57c 5ac: 4a04 ldr r2, [pc, #16] (5c0) 5ae: 0099 lsls r1, r3, #2 5b0: 8810 ldrh r0, [r2, #0] 5b2: 00db lsls r3, r3, #3 5b4: 1a41 subs r1, r0, r1 5b6: 8011 strh r1, [r2, #0] 5b8: 18e4 adds r4, r4, r3 5ba: 8810 ldrh r0, [r2, #0] 5bc: 6054 str r4, [r2, #4] 5be: bdf0 pop {r4, r5, r6, r7, pc} 5c0: 20000078 .word 0x20000078 5c4: 20000124 .word 0x20000124 5c8: 0000cccc .word 0x0000cccc 5cc: 0000b333 .word 0x0000b333
0x57c から 0x5a6 までが i に関する for ループの内側の部分です。
主に Thumb 命令しか使えない ARMv6-M アーキテクチャの Cortex-M0 では、多くの命令で自由に使えるのは R0 〜 R7 の 8 個のレジスタに限られるという制約があり、また、ARM/Thumb2 命令にはあるシフト付きオペランドが使えないので、どうしてもステップ数が多くなります。
それでも bmc_L に対する補正は 3 命令におさまっています。
58c: 0bef lsrs r7, r5, #15 58e: 3f01 subs r7, #1 590: 407e eors r6, r7