LPC1114FN28/102 (15) -- リニア PCM プレイヤー (3)

LPC1114 では GPIO (汎用入出力ポート) は AHB に接続されており、ウェイト・ステートなしで動作します。
そのため、ポートに対して出力する場合の Cortex-M0 の STR 命令の実行クロック数は、本来の値の「2」から増えることはありません。
逆に言えば、GPIO に一回出力するのにも必ず 2 クロックを要し、これ以下に短縮することはできません。
ソフト SPI の「SCK」として、単純にポートを 0→1→0→1→... と「トグル」するのも、1 周期あたり 4 クロックが必要になります。
SPI としては SCK だけではなく、DAC へシリアル・データを送るための「MOSI」出力も必要です。
SCK 端子と MISI 端子を別々の GPIO ポートに割り付けると、MOSI 出力ポートに書き込むための 2 クロックも追加されることになります。
実際には、ポート出力操作以外にも、パラレル・データをシリアル化するための操作も必要になりますから、クロック数はさらに増えます。
そんなわけで、SCK と MOSI は同じ GPIO ポートの違うビットに割り当て、MOSI 出力と SCK の立ち下りエッジ部分の出力をひとつの STR 命令で行うようにしました。
SPI 1ビット分の操作を #define で「マクロ」にまとめ、それを必要な数だけ並べて DAC への 16 ビット・シリアル・データ転送を行うようにしました。
ソフト SPI のプログラム (の断片) を下に示します。

#define SOFT_SPI_GPIO      LPC_GPIO0
#define SOFT_SPI_SCK_POS   7
#define SOFT_SPI_MOSI_POS  3
#define SOFT_SPI_SCK_MASK  (1 << SOFT_SPI_SCK_POS)
#define SOFT_SPI_MOSI_MASK (1 << SOFT_SPI_MOSI_POS)

#define SOFT_SPI_OUT_1B(sft) \
    w &= (~SOFT_SPI_SCK_MASK); \
    *(p+SOFT_SPI_MOSI_MASK) = w; \
    w = (w1 >> (sft)); \
    if (0 == (sft)) {__NOP();} \
    *(p) = SOFT_SPI_SCK_MASK;
           
void soft_SPI_out_16b(uint32_t w1)
{
// soft SPI CLOCK bit only access pointer
  register volatile uint32_t *p = 
    &(SOFT_SPI_GPIO->MASKED_ACCESS[SOFT_SPI_SCK_MASK]);

  register uint32_t w;

    w1 <<= SOFT_SPI_MOSI_POS; // adjust position
    w = (w1 >> 15); // extract MSB (b15)

    SOFT_SPI_OUT_1B(14);
    SOFT_SPI_OUT_1B(13);
    SOFT_SPI_OUT_1B(12);
    SOFT_SPI_OUT_1B(11);
    SOFT_SPI_OUT_1B(10);
    SOFT_SPI_OUT_1B( 9);
    SOFT_SPI_OUT_1B( 8);
    SOFT_SPI_OUT_1B( 7);
    SOFT_SPI_OUT_1B( 6);
    SOFT_SPI_OUT_1B( 5);
    SOFT_SPI_OUT_1B( 4);
    SOFT_SPI_OUT_1B( 3);
    SOFT_SPI_OUT_1B( 2);
    SOFT_SPI_OUT_1B( 1);
    SOFT_SPI_OUT_1B( 0);
    SOFT_SPI_OUT_1B( 0);

// last SCK edge
    w = 0;
    *(p+SOFT_SPI_MOSI_MASK) = w;
} // void soft_SPI_out_16b()

これを Kei/ARM MDK-Lite μVision V4.20 でコンパイルした結果の soft_SPI_out_16b() 関数の部分を逆アセンブルしたものを下に示します。

00000334 <soft_SPI_out_16b>:
     334: 00c0          lsls	r0, r0, #3
     336: 49ff          ldr	r1, [pc, #1020]	;(734)
     338: 0bc3          lsrs	r3, r0, #15
     33a: 2280          movs	r2, #128
     
     33c: 4393          bics	r3, r2
     33e: 620b          str	r3, [r1, #32]
     340: 0b83          lsrs	r3, r0, #14
     342: 600a          str	r2, [r1, #0]

     344: 4393          bics	r3, r2
     346: 620b          str	r3, [r1, #32]
     348: 0b43          lsrs	r3, r0, #13
     34a: 600a          str	r2, [r1, #0]

          . . . . . < 中略 > . . . . .
	  
     39c: 4393          bics	r3, r2
     39e: 620b          str	r3, [r1, #32]
     3a0: 0883          lsrs	r3, r0, #2
     3a2: 600a          str	r2, [r1, #0]

     3a4: 4393          bics	r3, r2
     3a6: 620b          str	r3, [r1, #32]
     3a8: 0843          lsrs	r3, r0, #1
     3aa: 600a          str	r2, [r1, #0]

     3ac: 4393          bics	r3, r2
     3ae: 620b          str	r3, [r1, #32]
     3b0: bf00          nop
     3b2: 600a          str	r2, [r1, #0]

     3b4: 4390          bics	r0, r2
     3b6: 6208          str	r0, [r1, #32]
     3b8: bf00          nop
     3ba: 600a          str	r2, [r1, #0]

     3bc: 2000          movs	r0, #0
     3be: 6208          str	r0, [r1, #32]
     3c0: 4770          bx	lr

     734: 50000200 	.word	0x50000200

ソフト SPI 1 ビット出力分のマクロ SOFT_SPI_OUT_1B() では、ポートのビット割り付けの変更が容易なように抽象度の高い書き方にしてあって分かりにくいので、もっと具体的に等価な形で書き直したのが次のリストです

    w &= (~0x080); 
    LPC_GPIO0->MASKED_ACCESS[0x080 + 0x008] = w;
    w = (w1 >> (sft));
    LPC_GPIO0->MASKED_ACCESS[0x080]         = 0x080;

まず、

LPC_GPIO0->MASKED_ACCESS[]

というのは、ポートの特定のビットのみを読み/書きする場合のアクセス方法です。
出力の場合、配列のインデクスの値の「1」が立っているビットのみが書き換わり、「0」になっているビットにたいしては変更されません。
通常のポートの全ビットを変更する場合の書き方は

LPC_GPIO0->DATA

となりますが、実はこの定義は union (共用体) を使っていて、

LPC_GPIO0->MASKED_ACCESS[0xFFF]

と書くのと等価です。
上のリストの 2 行目は、PIO0_7 に割り付けられた SCK 出力の立ち下りエッジと、PIO0_3 に割り付けられた MOSI 出力を同時にするタイミングで、PIO0 の 7 ビット目と 3 ビット目のみを変更するために b7 のマスクパターン 0x080 と、b3 のマスクパターン 0x008 との和を MASKED_ACCESS 配列のインデクスとしています。
上のリストの 4 行目は、PIO0_7 に割り付けられた SCK 出力の立ち上がりエッジを出力するタイミングで、PIO0 の 7 ビット目のみを変更するために b7 のマスクパターン 0x080 を MASKED_ACCESS
配列のインデクスとしています。
書き込むデータ自体も 7 ビット目のみ「1」となった 0x080 を使っていますが、これは例えば 0xFFF であっても b7 以外のビットには影響を及ぼしません。
上のリストの 3 行目は、次のビットのサイクルで使用する MOSI 出力のためのデータのシリアライズです。
soft_SPI_out_16b() 関数本体の最初の部分で、まず、引数 w1 として渡されてきた 16 ビットのパラレル・データの b0 が MOSI 出力に割り付けられた b3 の位置に揃うように w1 をあらかじめ 3 ビット左シフトして準備します。
DAC に転送するデータは MSB ファーストの必要があるので、一番最初のビット、つまり b15 を変数 w に準備するためには、変数 w1 を 15 ビット右シフトしたものを変数 w に代入すればよいことになります。
Cortex-M0 プロセッサにはバレル・シフタがあるので、何ビットのシフトでも 1 サイクルで実行することが可能です。
同様にして、b14, b13, ... , b1, b0 と転送していくには変数 w1 の右シフト量を 14, 13, ... , 1, 0 と変えながら繰り返せばよいことになります。
上のリストの 1 行目は、SCK の立ち下りエッジのための準備です。
変数 w には MOSI 出力のためにシフトされた出力データが入っており、そのデータにより SCK 位置の b7 の値は 0 とは限りません。
そのため、変数 w の b7 を確実にクリアするために 0x080 の 1 の補数の 0xF7F と AND を取ります。
逆アセンブル・リストの右に、タイミング・チャートを時計回りに 90° 回転させたものを配置した図を下に示します。

レジスタ r2 には、SCK のビット位置 b7 に「1」が立った 0x80 のビットパターンの定数が格納されており、レジスタ r1 には、soft_SPI_out_16b() 関数本体の最初の部分で

    p = &(LPC_GPIO0->MASKED_ACCESS[0x080])

によって代入されたアドレス (具体的には 0x50000200 ) が入っています。
リスト 4 行目の

    str     r2, [r1, #0]

により、SCK 出力として割り付けられているビット b7 にのみ「1」が出力され、SCK の立ち上がりエッジとなります。
レジスタ r0 には変数 w1、レジスタ r3 には変数 w の値が入っており、リスト 3 行目の

    lsrs    r3, r0, #sft

で「sft」ビットだけ右シフトされたレジスタ r0 の値がレジスタ r3 にコピーされます。
もとの SOFT_SPI_OUT_1B() のマクロの中で if 文で (0 == sft) の場合に nop を挿入しているのは、コンパイラの最適化によって、lsrs r3,r0,#0 が削除され、r0 を直接使うようなコードに変換されることへの対策です。
消される lsrs の代わりに余分な nop を挿入して、最後の b0 付近の出力波形も他の部分と揃うようにしています。
1 行目の

    bics    r3, r2

では、レジスタ r3 の b7 だけがクリアされます。
2 行目の

    str     r3, [r1, #32]

は、C 言語での

 *(p + 0x008) = w;

に相当します。
Cortex-M0 では、一般のレジスタに対する定数オフセットを持つ str 命令のバリエーションはひとつだけで、オフセット値は 5 ビット幅に限られます。
そのオフセットは 32 ビット・レジスタに対応する 4 バイト単位の刻みとなっており、命令の中のオフセット・フィールドの値としては 0 〜 31、バイト単位のメモリアドレスとしては、4 バイト・ステップで 0 〜 124 バイトとなります。
オフセットがこの範囲に入るなら、この定数オフセットを使う命令にコンパイルされます。
オフセットがこの範囲を超えると、汎用レジスタを使ってアドレス計算を行うようなコードが出力されます。
実際には、2 種類のアドレスだけが何回も使われるので、オフセットが大きい場合には、

p = &(LPC_GPIO0->MASKED_ACCESS[SOFT_SPI_SCK_MASK]);
q = &(LPC_GPIO0->MASKED_ACCESS[SOFT_SPI_SCK_MASK+SOFT_SPI_MOSI_MASK]);

「q」に相当するような、ポインタをもう一本自動的に割り付けて余計な計算を省くようなコードが生成されます。
そのため、MOSI 出力はビット 0 〜 4 のいずれかに割り付けておくと、余分なレジスタを消費せずにすみます。
bics 命令と、lsrs 命令がふたつの str 命令の間にはさまるようにして、SCK = 1 の部分のクロック数 3、SCK = 0 の部分のクロック数も 3 の、SPI 1 ビット転送あたり合計 6 クロックとなるようにしています。
MOSI 出力と SCK 出力をドット蓄積モードで観測した波形写真を再掲します。

SCK 波形を良く見ると、2 周期に 1 回、「H」レベルの幅が広くなっていることが分かります。
これはフラッシュ読み出しのウェイト・ステートの影響です。
コア・クロックが 36 MHz なので、ウェイト・ステート数の設定は「1」にしています。
LPC11xx のフラッシュ・メモリ・ブロックでは、要求されたアドレスのデータがバッファにない場合、そのアドレスを含む領域のデータを一度に 128 ビット分を読み出し、バッファに格納します。
この読み出し時にウェイト・ステート数だけのウェイトが発生します。
すでにデータがバッファ上にあればウェイトは発生しません。
128 ビットを換算すれば 16 バイト分で、Thumb の 16 ビット命令を単位に数えれば 8 命令分になります。
ソフト SPI では 4 命令で SPI 転送 1 ビット分で、プログラムにループはなく、直線状の流れですから、SPI 転送 2 ビット分である 8 命令を実行するたびにフラッシュのバッファを更新するためのウェイト・ステートが発生することになります。
フラッシュのウェイトが発生すれば、1 クロック分待たされるわけで、その寄与が波形に現れてきます。
したがって、SPI 転送 2 ビット分に対して
3 + 3 + 3 + 3 + 1 = 13
クロックを要することになり、SPI 転送 1 ビット分では 6.5 クロックかかることになります。