So-net無料ブログ作成
検索選択

サイクルタイムを測定しよう (10) [FMx]このエントリーを含むはてなブックマーク#

FM0+ 評価ボード

今回は、 CypressFM0+ MCU S6E1A シリーズに焦点をあてて、サイクルタイムを測定してみます。 CPU は、 Cortex-M0+ だから、 PSoC 4 と大きな違いはないでしょう。

Fast GPIO を使ったループ

今回のプロジェクトでも、ループを回るごとに GPIO をトグルさせて外部からループの周期を測定します。 PSoC 4 の場合、 Control Register コンポーネントを配置して、データを書き込むだけで出力がトグルするハードウェアを組みました。 FM0+ の場合、ハードウェアを組む事はできませんので、すべてソフトウェアで GPIO を操作します。

FM0+ には、ハードウェアを作成する機能は有りませんが、ソフトウェアから高速に GPIO を操作する機能を持っています。 それが、 Fast GPIO (FGPIO) です。 FGPIO は、 CPU に近いバスに接続されているため、 GPIO の操作に1クロックしか要しません。 こういった、高速に GPIO 出力を操作する用途には最適です。

    Gpio1pin_InitOut(FGPIO1PIN_P61, Gpio1pin_InitVal(1));
    FM_GPIO->FPOER6_f.P1 = 1;  // Enable FGPIO

使用する端子は、評価ボードの LED が接続されている P61 です。 ディレイを入れると、 LED の明滅で動作を確認できます。 P61 を出力に設定するためには、上記のように Gpio1pin_InitOut() マクロを使用します。 このマクロは、 Peripheral Driver Library (PDL) と呼ばれる基本ライブラリで提供されています。

デフォルトの状態では、各端子は GPIO として機能します。 これを FPGIO で制御させるために FPOER6 レジスタの当該ビットを1に設定します。 これで、 P61 は、 FGPIO の出力端子として機能します。

      func9(&bFM_GPIO_FPDOR6_P1, 0u, 2u);

実際に FGPIO をトグルさせる部分は、前回同様 func9() 関数にまとめました。 P61 の出力値を変更するためのレジスタが、 bFM_GPIO_FPDOR6_P1 にあります。 今回は、書き込み動作一回でトグルさせるのではなく、 set/clear の二回の書き込みでトグルさせます。 そのため、 clear するための値と set するための値を引数に与えています。

typedef volatile uint8_t reg8_t;

void func9(reg8_t *reg, uint8_t val0, uint8_t val1) @ ".text.func9" {
    for (;;) {
        *reg = val0;
        *reg = val1;
    }
}

func9() 関数は、上記のようになっています。 "@" で示されている名前は、セクション名です。 リンカのスクリプトで、このセクションを 32 バイト境界から配置するように指定しています。 bFM_GPIO_FPDOR6_P1 レジスタのアドレスと書き込む値を引数で与えると、それぞれ CPU のレジスタに値が保持されて、アクセス時間が最短になります。

     83              for (;;) {
    116                  *reg = val0;
   \                     ??func9_0: (+1)
   \   00000002   0x7001             STRB     R1,[R0, #+0]
    117                  *reg = val1;
   \   00000004   0x7002             STRB     R2,[R0, #+0]
   \   00000006   0xE7FC             B        ??func9_0
    118              }

コンパイラで生成されたコードは、3命令になりました。

実行サイクル数

このプログラムのループ周期を測定したところ、4サイクルで実行されることがわかりました。 CPU クロックを40MHzで操作させているので、10MHzのパルスで LED が駆動されたことになります。 さらに実験したところ、 STRB 命令が1サイクル、 B 命令が2サイクルで実行されるということがわかりました。

参考サイト

FM0+ S6E1A Series 5V Robust ARM® Cortex®-M0+ MCU
今回実験に使用したのは、 FM0+ S6E1A というシリーズの MCU です。
FM0-V48-S6E1A1 ARM® Cortex®-M0+ FM0+ MCU Evaluation Board
実験には、評価ボードを使用しました。 書き込みツールとして CMSIS-DAP として機能する FM3 が搭載されています。
FM MCU Peripheral Driver Library (PDL)
FM0+ の各種ペリフェラルを操作するための基本ライブラリです。 今回は、 FGPIO の初期化で使用しましたが、高速にアクセスするためには、レジスタを直接操作した方が良さそうです。

参考文書

AN210985 - Getting Started with FM0+ Development
FM0+ でプログラム開発を行うための手順が書かれています。 この文書では、 Peripheral Driver Library (PDL) の使用を前提としています。

サイクルタイムを測定しよう (9) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

これまで、単純な分岐命令に着目して、サイクルタイムが長くなる場合と短くなる場合について調べてきました。 今回は、いわゆるサブルーチンコールのサイクルタイムについて調べてみました。

メインループ

実験には、以下のようなメインループを使用しました。 前回と同じように、 func9() 関数を呼び出したら、このメインループにはもどってきません。

int main(void) __attribute__((aligned(32)));
int main(void) {
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    PWM_1_Start();
    PWM_2_Start();

    for (;;) {
        /* Place your application code here. */
        func9(CR1_Control_PTR, 1u);
    }
}

被測定ループ

今回もループの周期を外部周波数カウンタで測定して、サイクルタイムを計測します。 ループ中の "nop" 命令の数を調節して、関数を呼び出す分岐命令のアドレスを変化させます。

void func9(reg8 *reg, uint8 val) __attribute__((aligned(32)));
void func9(reg8 *reg, uint8 val) {
    for (;;) {
        __ASM(
            "nop\n"         // 10
            "nop\n"         // 9
            "nop\n"         // 8
            "nop\n"         // 7
            "nop\n"         // 6
            "nop\n"         // 5
            "nop\n"         // 4
            "nop\n"         // 3
            "nop\n"         // 2
            "nop\n"         // 1
            ".label_2:"
        );
        func1_0();
        *reg = val;
    }
}

呼び出される関数は、この例では "func1_0()" となっていますが、飛び先を変える事で、分岐先のアドレスを変化させます。 ループの先頭アドレスは固定されていますので、ループの分岐先に起因する要因を除外する事ができます。

呼び出される関数

呼び出される関数は、以下のように "nop" 命令が並んだ構造になっていますが、これは "nop" の数を調整するためのものではありません。

void func1(void) __attribute__((aligned(32)));
void func1(void) {
    __ASM(
        "func1_0: nop\n"        // 8
        "func1_1: nop\n"        // 7
        "func1_2: nop\n"        // 6
        "func1_3: nop\n"        // 5
        "func1_4: nop\n"        // 4
        "func1_5: nop\n"        // 3
        "func1_6: nop\n"        // 2
        "func1_7: nop\n"        // 1
        "func1_8:\n"            // 0
    );
}

void func1_0(void);
void func1_1(void);
void func1_2(void);
void func1_3(void);
void func1_4(void);
void func1_5(void);
void func1_6(void);
void func1_7(void);
void func1_8(void);

func1() 関数は、独立した関数なのですが、内部に複数のラベルを定義する事で、飛び込み先の異なる複数の関数となるように細工されています。 例えば、 func1_0() を呼んだ場合はアドレス00に分岐し、 func1_4() を呼んだ場合はアドレス08に分岐します。 この部分のコンパイル結果は、以下のようになっています。

  74              	func1:
  80 0000 C046     		func1_0: nop
  81 0002 C046     	func1_1: nop
  82 0004 C046     	func1_2: nop
  83 0006 C046     	func1_3: nop
  84 0008 C046     	func1_4: nop
  85 000a C046     	func1_5: nop
  86 000c C046     	func1_6: nop
  87 000e C046     	func1_7: nop
  88              	func1_8:
  94 0010 7047     		bx	lr

実験結果

"nop" の数を変える事で分岐命令のアドレスを、関数の名前を変える事で分岐先を、それぞれ変化させてサイクルタイムを測定しました。 それぞれのサイクルタイムからは、実行された "nop" 命令の数だけサイクルタイムを減じて、サブルーチンコールに起因する違いが見えるようにしています。

分岐先
00020406080A0C0E10
分岐命令06191920211919202116
08191920211919202116
0A202021222020212219
0C212122232121222316
0E212122232121222316
10212122232121222316
12222223242222232421
14212122232121222316
16212122232121222316
18212122232121222316
1A222223242222232421

特徴的なのは、分岐先が "10" になっているとサイクルタイムが短くなっている点です。 このアドレスには、サブルーチンから戻るための命令が配置されています。 そのため、分岐先が分岐命令であった場合の特別なルールが働いたのではないかと推測しています。

また、分岐先のアドレスに応じてサイクルタイムが変化しているのがわかります。 アドレスの下3ビットが 110 である場合にサイクルタイムが長くなるというのは、前回までに判明したサイクルタイムが分岐先アドレスに依存しているのと合致します。 サブルーチンの分岐先についても例外ではなさそうです。

一方、分岐命令のアドレスについても依存関係が見られます。 こちらはアドレスの下3ビットが 010 であった場合にサイクルタイムが長くなっています。

この原因を考えた所、サブルーチンコールから戻ってきた時のアドレスに依存しているのではないかと推測しました。 サブルーチンコールに使用される分岐命令は4バイトです。 そのため、分岐命令のアドレス下3ビットが 010 であった場合、戻りアドレスの下3ビットは 110 となり、前回までの実験結果と一致します。

つまり、ここでも分岐先のアドレスに依存してサイクルタイムが長くなるという現象として説明できます。 分岐命令のアドレスに関連してサイクルタイムが伸びる条件は、いずれの場合も分岐先アドレスの下3ビットが 110 となる場合であると説明できます。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できるようになります。


構造体を返す関数と構造体を受け取る関数 [PSoC]このエントリーを含むはてなブックマーク#

twitter にて、 C の関数が構造体を返す事を教えてもらいました。 私が知ってる C と違う? GCC ARM と Cortex-M0 を題材に深めに調べてみました。

24ビットの構造体の場合

まず、みっつの8ビットフィールドが含まれた例を調べます。

struct point8 {
    int8    x, y, z;
};

そして、この構造体を返す関数を作成しました。

struct point8 createPoint8(int8 x, int8 y, int8 z) {
    struct point8 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}

三つの引数を受け取って、構造体を作成して返す単純な関数です。 コンパイルの結果、以下のようなコードが作成されました。

  23              	createPoint8:
  30 0000 FF23     		mov	r3, #255
  31 0002 1940     		and	r1, r3
  33 0004 1840     		and	r0, r3
  35 0006 0902     		lsl	r1, r1, #8
  36 0008 1340     		and	r3, r2
  37 000a 1B04     		lsl	r3, r3, #16
  38 000c 0843     		orr	r0, r1
  40 000e 82B0     		sub	sp, sp, #8
  43 0010 1843     		orr	r0, r3
  45 0012 02B0     		add	sp, sp, #8
  47 0014 7047     		bx	lr

三つの引数 x, y, z は、三つのレジスタ r0, r1, r2 にそれぞれ格納されます。 そして、8ビットマスクと左シフトを使いながら、24ビットの値を構成して、 r0 レジスタに格納して返します。 計算式は、以下のようになります。

r0 = ((x & 0xFF) | ((y & 0xFF) << 8) | ((z & 0xFF) << 16))

呼び出し側で関数からの返り値を変数に代入すると、以下のようなコードが生成されます。

 270 000a 0B20     		mov	r0, #11
 271 000c 1621     		mov	r1, #22
 272 000e 2122     		mov	r2, #33
 273 0010 FFF7FEFF 		bl	createPoint8
 275 0014 144C     		ldr	r4, .L14
 276 0016 2070     		strb	r0, [r4]
 277 0018 030A     		lsr	r3, r0, #8
 278 001a 6370     		strb	r3, [r4, #1]
 279 001c 000C     		lsr	r0, r0, #16
 280 001e A070     		strb	r0, [r4, #2]

構造体を8ビットごとに分解してからメモリ領域に格納しています。 コストが高そうです。

引数で構造体を渡す場合を以下の関数で確認します。

void printPoint8(struct point8 p) {
    printPoint(p.x, p.y, p.z);
}

これは、三つの値を表示する関数ですが、実際に表示する部分は別の関数になっています。 この関数からは、以下のようなコードが生成されます。

 103              	printPoint8:
 107 0000 011C     		mov	r1, r0
 108 0002 00B5     		push	{lr}
 111 0004 020C     		lsr	r2, r0, #16
 112 0006 090A     		lsr	r1, r1, #8
 113 0008 83B0     		sub	sp, sp, #12
 116 000a 40B2     		sxtb	r0, r0
 117 000c 49B2     		sxtb	r1, r1
 118 000e 52B2     		sxtb	r2, r2
 119 0010 FFF7FEFF 		bl	printPoint
 122 0014 03B0     		add	sp, sp, #12
 124 0016 00BD     		pop	{pc}

受け取った構造体は32ビットの値として扱う事ができるので、実際の処理は、32ビットの値に対するものと等価になります。 レジスタ r0 の値を8ビットごとに分解してレジスタ r0, r1, r2 に格納し、関数 printPoint() を呼び出します。 何だか、これもコストが高そうです。

 306 004c 2068     		ldr	r0, [r4]
 307 004e FFF7FEFF 		bl	printPoint8

呼び出し側では、レジスタ r0 に構造体を一括で読み出して関数 printPoint8() を呼び出します。 ここでは、構造体を構成するような事はしないようです。

48ビットの構造体の場合

次は、構造体のサイズを二倍にしてみました。

struct point16 {
    int16   x, y, z;
};

全体で48ビットありますので、レジスタひとつには入りません。 これも構造体を返す関数を作成してコードを生成させてみました。

struct point16 createPoint16(int16 x, int16 y, int16 z) {
    struct point16 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}
 134              	createPoint16:
 140 0000 0180     		strh	r1, [r0]
 141 0002 4280     		strh	r2, [r0, #2]
 142 0004 8380     		strh	r3, [r0, #4]
 145 0006 7047     		bx	lr

なんだか、ずいぶん簡単になってしまいました。 16ビットの格納命令が三つ並んでいるだけです。 それぞれのレジスタに何が入っているかは、呼び出し側を確認するとわかります。

 282 0020 04A8     		add	r0, sp, #16
 283 0022 1249     		ldr	r1, .L14+4
 284 0024 124A     		ldr	r2, .L14+8
 285 0026 134B     		ldr	r3, .L14+12
 286 0028 FFF7FEFF 		bl	createPoint16
 288 002c 201D     		add	r0, r4, #4
 289 002e 04A9     		add	r1, sp, #16
 290 0030 0622     		mov	r2, #6
 291 0032 FFF7FEFF 		bl	memcpy

三つの引数が、レジスタ r1, r2, r3 で渡されているほか、レジスタ r0 には、スタック上に確保されたメモリ領域のアドレスが渡されます。 このメモリ領域は、関数が返した構造体を格納するための一時メモリ領域です。 その後、一時メモリ領域を memcpy() 関数を使って変数に格納します。 これも受け渡しのためのコストが高そうです。

引数で構造体を渡す関数を作成してコードを生成させました。

void printPoint16(struct point16 p) {
    printPoint(p.x, p.y, p.z);
}
 155              	printPoint16:
 159 0000 00B5     		push	{lr}
 163 0002 03B2     		sxth	r3, r0
 165 0004 0A1C     		add	r2, r1, #0
 166 0006 83B0     		sub	sp, sp, #12
 169 0008 0114     		asr	r1, r0, #16
 170 000a 12B2     		sxth	r2, r2
 171 000c 181C     		mov	r0, r3
 172 000e FFF7FEFF 		bl	printPoint
 175 0012 03B0     		add	sp, sp, #12
 177 0014 00BD     		pop	{pc}

引数は、レジスタ r0, r1 で渡されて、三つの16ビットの値に再構成されて printPoint() 関数に渡されています。

 310 0052 6068     		ldr	r0, [r4, #4]
 311 0054 A168     		ldr	r1, [r4, #8]
 312 0056 FFF7FEFF 		bl	printPoint16

呼び出し側では、構造体変数の値をレジスタに r0, r1 に格納して関数 printPoint16() を呼び出しています。 呼び出し側のコストは抑制されているようです。

96ビット構造体の場合

構造体のサイズをさらに倍にしました。

struct point32 {
    int32   x, y, z;
};

構造体を返す関数と生成コードは、以下のようになりました。

struct point32 createPoint32(int32 x, int32 y, int32 z) {
    struct point32 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}
 202              	createPoint32:
 208 0000 0160     		str	r1, [r0]
 210 0002 4260     		str	r2, [r0, #4]
 212 0004 8360     		str	r3, [r0, #8]
 215 0006 7047     		bx	lr
 360 0036 04AD     		add	r5, sp, #16
 361 0038 281C     		mov	r0, r5
 362 003a 1F49     		ldr	r1, .L16+16
 363 003c 1F4A     		ldr	r2, .L16+20
 364 003e 204B     		ldr	r3, .L16+24
 365 0040 FFF7FEFF 		bl	createPoint32
 367 0044 231C     		mov	r3, r4
 368 0046 0C33     		add	r3, r3, #12
 369 0048 2A1C     		mov	r2, r5
 370 004a 43CA     		ldmia	r2!, {r0, r1, r6}
 371 004c 43C3     		stmia	r3!, {r0, r1, r6}

返された構造体を格納する場所をスタックに確保して、そのアドレスを r0 で渡しているのは、48ビットの構造体の場合と同じです。 違っているのは、返された値を変数に格納し直す部分です。

48ビット構造体では、 memcpy() 関数を使用して格納を行っていましたが、96ビットのばあいには、 ldmia, stmia 命令を使用しています。 この命令は、指定されたアドレスに対してレジスタ群を読み出し・書き込みを行います。 つまり、この1命令で96ビット分(12バイト)の読み書きができます。 ただし、32ビット単位でアクセスをするので、48ビットの構造体に対しては使用できませんでした。

引数で構造体を渡す関数と生成コードおよび呼び出し側のコードは、以下のようになりました。

void printPoint32(struct point32 p) {
    printPoint(p.x, p.y, p.z);
}
 225              	printPoint32:
 229 0000 00B5     		push	{lr}
 232 0002 85B0     		sub	sp, sp, #20
 235 0004 FFF7FEFF 		bl	printPoint
 240 000a 00BD     		pop	{pc}
 402 0082 E068     		ldr	r0, [r4, #12]
 403 0084 2169     		ldr	r1, [r4, #16]
 404 0086 6269     		ldr	r2, [r4, #20]
 405 0088 FFF7FEFF 		bl	printPoint32

構造体は、三つのレジスタ r0, r1, r2 で渡されます。 この時、レジスタにはそれぞれ x, y, z が入ってくるため、関数 printPoint() にはレジスタをそのまま渡せば良い事になります。 そのため、関数 printPoint32() では、一切のデータ操作を行っていません。

192ビット構造体の場合

さらに構造体のサイズを大きくします。

struct point64 {
    int64   x, y, z;
};

ついに一時レジスタで値を渡せるデータ量を超えました。 構造体を返す関数とそのコードおよび呼び出し側のコードは、以下のようになりました。

struct point64 createPoint64(int64 x, int64 y, int64 z) {
    struct point64 p;
    p.x = x;
    p.y = y;
    p.z = z;
    return p;
}
 250              	createPoint64:
 256 0000 0260     		str	r2, [r0]
 257 0002 4360     		str	r3, [r0, #4]
 259 0004 009A     		ldr	r2, [sp]
 260 0006 019B     		ldr	r3, [sp, #4]
 262 0008 8260     		str	r2, [r0, #8]
 263 000a C360     		str	r3, [r0, #12]
 265 000c 029A     		ldr	r2, [sp, #8]
 266 000e 039B     		ldr	r3, [sp, #12]
 267 0010 0261     		str	r2, [r0, #16]
 268 0012 4361     		str	r3, [r0, #20]
 271 0014 7047     		bx	lr
 373 004e 1D4A     		ldr	r2, .L16+28
 374 0050 1D4B     		ldr	r3, .L16+32
 375 0052 0092     		str	r2, [sp]
 376 0054 0193     		str	r3, [sp, #4]
 377 0056 1D4A     		ldr	r2, .L16+36
 378 0058 1D4B     		ldr	r3, .L16+40
 379 005a 0292     		str	r2, [sp, #8]
 380 005c 0393     		str	r3, [sp, #12]
 381 005e 281C     		mov	r0, r5
 382 0060 1C4A     		ldr	r2, .L16+44
 383 0062 1D4B     		ldr	r3, .L16+48
 384 0064 FFF7FEFF 		bl	createPoint64
 386 0068 201C     		mov	r0, r4
 387 006a 1830     		add	r0, r0, #24
 388 006c 291C     		mov	r1, r5
 389 006e 1822     		mov	r2, #24
 390 0070 FFF7FEFF 		bl	memcpy

この場合でも、スタック上に確保された一時メモリ領域のアドレスがレジスタ r0 を介して渡されます。 引数のうち、 x はレジスタ r2, r3 で渡されますが、 y, z は、スタック上のメモリを使用します。 r1 が使用されませんが、これは64ビットの値を扱う場合には r2:r3 のペアを使うという規約によるものです。

関数の処理は、単純になりました。 スタックで渡された引数をスタックに確保された一時メモリ領域にコピーするだけの処理が行われます。

一時メモリ領域に返された値は、関数 memcpy() で変数に格納されます。

一方、構造体を引数で渡す関数は以下のようになりました。

void printPoint64(struct point64 p) {
    printPoint(p.x, p.y, p.z);
}
 281              	printPoint64:
 285 0000 84B0     		sub	sp, sp, #16
 287 0002 10B5     		push	{r4, lr}
 291 0004 0290     		str	r0, [sp, #8]
 292 0006 0391     		str	r1, [sp, #12]
 293 0008 111C     		mov	r1, r2
 294 000a 0492     		str	r2, [sp, #16]
 295 000c 0593     		str	r3, [sp, #20]
 297 000e 069A     		ldr	r2, [sp, #24]
 298 0010 FFF7FEFF 		bl	printPoint
 302 0014 10BC     		pop	{r4}
 303 0016 08BC     		pop	{r3}
 304 0018 04B0     		add	sp, sp, #16
 305 001a 1847     		bx	r3
 408 008c 211C     		mov	r1, r4
 409 008e 2831     		add	r1, r1, #40
 410 0090 6846     		mov	r0, sp
 411 0092 0822     		mov	r2, #8
 412 0094 FFF7FEFF 		bl	memcpy
 414 0098 A069     		ldr	r0, [r4, #24]
 415 009a E169     		ldr	r1, [r4, #28]
 416 009c 226A     		ldr	r2, [r4, #32]
 417 009e 636A     		ldr	r3, [r4, #36]
 418 00a0 FFF7FEFF 		bl	printPoint64

引数は、レジスタ r0, r1, r2, r3 の128ビットとスタックの64ビットに分割して渡されます。

まとめ

本日のまとめです。

  1. 構造体が32ビット以内で表現できる時には、パッキングされて通常の変数と同じようにやり取りされる。パッキング・アンパッキングのコストは安くない。
  2. 32ビットを超えるサイズの構造体を返す関数は、呼び出し前にスタックにメモリ領域を確保し、そのアドレスをレジスタ r0 に与えて呼び出す。残りの引数は、 r1, r2, r3 レジスタ、スタックの順に格納される。
  3. 関数の引数に構造体を与えた場合、通常の引数と同じルールでレジスタおよびスタックに展開される。
こんなところでしょうか。

参考サイト

Chapter 3. The Cortex-M0 Instruction Set
ARM が提供するサイトで、 Cortex-M0 の命令が参照できます。

参考文献

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

  • 作者: B.W. カーニハン
  • 出版社/メーカー: 共立出版
  • 発売日: 1989/06/15
  • メディア: 単行本
苦しんで覚えるC言語

苦しんで覚えるC言語

  • 作者: MMGames
  • 出版社/メーカー: 秀和システム
  • 発売日: 2011/06/24
  • メディア: 単行本

サイクルタイムを測定しよう (8) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

これまで、実行サイクル時間を自身のタイマで測定してきました。 この方法を使うと、測定結果を出力するためのインターフェイスが必要になります。 今回は、外部の周波数カウンタを使って、純粋にループの周期を測定します。

実験回路

ループの周期を測定するためには、ループ内に外部信号を出力する命令を入れて、この外部信号の周期を測定してループの周期とします。 外部信号は、 Control Register の Pulse 出力を使用しており、レジスタへの書込み操作だけでパルスを発生させることができます。 このパルスは高速であるため、人間の目では直接観測できません。 そこで、 1000 サイクル周期の PWM コンポーネントを二つ使って百万分の一分周器を構成し、 LED を点滅させて人間用のモニタとしています。

メインループ

メインループは、以下のようになっています。 main() 関数では、メインループの体裁を残していますが、実際には、このループは使用しません。 コンポーネントの初期化の後、 func9() 関数を呼び出すと、 func9() 内の無限ループに入り、 main() には戻ってきません。

int main(void) __attribute__((aligned(32)));
int main(void) {
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    PWM_1_Start();
    PWM_2_Start();

    for (;;) {
        /* Place your application code here. */
        func9(CR1_Control_PTR, 1u);
    }
}

被測定ループ

測定対象のループは、以下のようになっています。 前回の記事と同様、ループの前とループの中に "nop"命令を挿入し、プログラムの配置位置を制御しています。 文 "*reg = val;" で Control Register を叩き、パルスを発生させます。 Control Register のアドレスと書き込むべき値は、この関数への引数としてレジスタを介して渡されます。 これで、 Control Register を叩くときに余計なコストがかからなくなります。

void func9(reg8 *reg, uint8 val) __attribute__((aligned(32)));
void func9(reg8 *reg, uint8 val) {
    __ASM(
        "nop\n"         // 10
        "nop\n"         // 9
        "nop\n"         // 8
        "nop\n"         // 7
        "nop\n"         // 6
        "nop\n"         // 5
        "nop\n"         // 4
        "nop\n"         // 3
        "nop\n"         // 2
        "nop\n"         // 1
        ".label_1:\n"
    );
    for (;;) {
        __ASM(
            "nop\n"         // 10
            "nop\n"         // 9
            "nop\n"         // 8
            "nop\n"         // 7
            "nop\n"         // 6
            "nop\n"         // 5
            "nop\n"         // 4
            "nop\n"         // 3
            "nop\n"         // 2
            "nop\n"         // 1
            ".label_2:"
        );
        *reg = val;
    }
}

"nop" を全く入れない場合には、以下のように2命令のループになりました。 すべての命令がプリフェッチバッファに入るため、純粋な命令実行時間が見えます。

  35              	.L2:
  29:.\main.c      ****     for (;;) {
  43:.\main.c      ****         *reg = val;
  42 0000 0170     		strb	r1, [r0]
  44:.\main.c      ****     }
  44 0002 FDE7     		b	.L2

測定結果

測定結果は、以下のようになりました。 それぞれの数値は、 "nop" によるサイクル数を減じた上記の2命令の正味実行時間を示しています。 "nop" を全く入れない場合の所要サイクル数は、8サイクルでした。

分岐先
00020406080A0C0E101214
分岐命令028
0488
06888
088888
0A88898
0C888988
0E8889888
1088898888
12888988898
148889888988
1688898889888
18 8898889888
1A 898889888
1C 98889888
1E 8889888
20 889888
22 89888
24 9888
26 888
28 88
2A 8

分岐先が近い場合には、プリフェッチの影響が見えません。 それ以外では分岐先アドレスの下3ビットが 110 になっている場合にサイクル数が1サイクルだけ伸びています。 分岐先のアドレスを気にしたプログラムを作成すると、サイクル数の節約が出来るようです。 普通は、しないけどね。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できるようになります。


サイクルタイムを測定しよう (7) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

サイクルタイムを測定した結果、プログラムの配置や分岐先のアドレスによって実行時間が異なっている事がわかりました。 今回は、もっと詳しくデータをとります。

実験に使ったソフトウェア

これまでと同じように関数に "nop" 命令を並べて、プログラムの配置位置を変更します。

// Measure the execution cycle time
uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    asm(
        "nop\n"   // 10
        "nop\n"   // 9
        "nop\n"   // 8
        "nop\n"   // 7
        "nop\n"   // 6
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_1:\n"
    );
    s = *reg;
    asm(
        "b label_2\n"
        "nop\n"   // 10
        "nop\n"   // 9
        "nop\n"   // 8
        "nop\n"   // 7
        "nop\n"   // 6
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_2:\n"
    );
    e = *reg;
    
    return e - s;
}

前半のアセンブラ表記で "nop" の数を変えると、分岐命令の配置アドレスが2バイト単位で変えられます。 さらに、後半のアセンブラ表記で "nop" の数を変えると、分岐先アドレスを2バイト単位で変えられます。 分岐先のアドレスは、分岐命令のアドレスにも依存します。 "nop" の数を変えながらサイクル数を測定し、分岐命令の位置と分岐先アドレスによってで測定されたサイクル数を並べると、以下の表のようになります。

分岐命令
020406080A0C0E10121416
分岐先047/7/7
067/7/77/7/7
087/7/77/7/77/7/7
0A7/7/77/7/77/7/77/7/7
0C7/7/77/7/77/7/77/7/79/8/7
0E8/7/78/7/78/7/78/7/79/8/79/8/7
109/8/79/8/79/8/79/8/77/7/77/7/77/7/7
129/8/79/8/79/8/79/8/77/7/77/7/77/7/77/7/7
14A/8/7A/8/7A/8/7A/8/77/7/77/7/77/7/77/7/79/8/7
16B/9/7B/9/7B/9/7B/9/78/7/78/7/78/7/78/7/79/8/79/8/7
189/8/79/8/79/8/79/8/79/8/79/8/79/8/79/8/77/7/77/7/77/7/7
1A 9/8/79/8/79/8/79/8/79/8/79/8/79/8/77/7/77/7/77/7/7
1C A/8/7A/8/7A/8/7A/8/7A/8/7A/8/77/7/77/7/77/7/7
1E B/9/7B/9/7B/9/7B/9/7B/9/78/7/78/7/78/7/7
20 9/8/79/8/79/8/79/8/79/8/79/8/79/8/7
22 9/8/79/8/79/8/79/8/79/8/79/8/7
24 A/8/7A/8/7A/8/7A/8/7A/8/7
26 B/9/7B/9/7B/9/7B/9/7
28 9/8/79/8/79/8/7
2A 9/8/79/8/7
2C A/8/7

この表からは、以下の事がわかります。

  1. 分岐先が分岐命令に近く、アドレスの下3ビットが 000 から 100 である場合、サイクル数は最小を保ちます。 これは、追加でプリフェッチを必要とするためです。
  2. アドレスの下3ビットが 110 であるアドレスに分岐する場合、どの条件であってもコストが最大になります。 これは、分岐先の8バイトブロックに続いて次の8バイトブロックもプリフェッチする必要があるためと考えられます。
  3. その次にコストが高いのは、アドレスの下3ビットが 100 である場合です。 プリフェッチのタイミングを1クロックだけ遅らせる事ができるので、一回分のプリフェッチが見えなくなるためと考えられます。
  4. 分岐先が遠い場合、分岐命令のアドレスとは無関係に、分岐先のアドレスに依存したサイクル数を要します。 これにより、純粋にプリフェッチの時間が見えているのだとわかります。

以上の考察より、サイクル数を減らしたければ、分岐先のアドレスが8バイトブロックの前半になるように配置を考える必要があります。 関数内で分岐先アドレスを制御するのは困難ですが、せめて関数の入り口アドレスを8バイト境界の前半に配置するようにオプションを付けると処理時間が短くなりそうです。


PSoC 3 で作る周波数カウンタ [PSoC]このエントリーを含むはてなブックマーク#

カウンタ部

サイクルタイムを測定するために、これまで同一チップのカウンタを利用してきました。 これでも測定は出来るのですが、外部で周期または周波数を測定して処理時間を測定しようと考えました。 測定のためには測定器が必要ですが、ここでは自分で作ってみます。 材料は、最近めっきり使わなくなってしまった評価ボード CY8CKIT-030 です。

コンセプト

周波数カウンタを作るにあたって、ふたつの測定方法が考えられます。

ひとつは、被測定パルスをカウンタで数えさせておき、カウンタの値を周期的に取得する方法です。 この方法では、たとえば1秒周期でカウンタの値を読み込むと 1Hz 単位で周波数を知ることが出来ます。 被測定パルスの周波数が高いとカウンタの動作周波数を上回ってしまう場合があります。 こんな場合には、プリスケーラと呼ばれる分周器に被測定パルスを与えて、分周された出力の周波数を測定し、分周比率を掛けて被測定パルスの周波数とします。

もう一つは、被測定パルス1周期分の時間を測定する方法です。 本当に1周期分の時間を測定すると測定側のクロック周波数を非常に高くしなくてはなりません。 そのため、被測定パルスをプリスケーラで分周して、分周された信号の周期を測定します。 今回は、こちらの方法をとります。

プリスケーラ

プリスケーラ

プリスケーラは、百分周から十億分周を行うことが出来る多段分周器で構成されています。 最後の百分周は、入りきらなかったので別のページに配置しています。

Pin_Probe 入力をクロックとして使って、分周を行います。 そのため、 Pin_Probe コンポーネントにはクロック同期機能を付けていません。

PSoC Creator には、 Frequency Divider と呼ばれるコンポーネントがあります。 このコンポーネントを多段に接続する事で、簡単に分周比の大きな分周器が出来そうですが、そうはいきませんでした。 これは、 Frequency Divider の div 出力のパルス幅が1クロック分ではなくもっと長くなってしまうのが原因でした。 そこで、パルス幅を1クロックパルスに制限して分周器のカスケード接続が簡単に行えるように新たに CascadeDivider コンポーネントを作成しました。


カスケード可能分周器

div 出力のパルス幅を制限するために en 入力と AND をとって簡単に次段に接続できるようにしました。 出力部に D Flip Flop が付いていますが、このコンポーネントが無くても分周自体は行えます。

D Flip Flop が無い状態で分周器を多段に接続すると、初段の div 出力から各段の AND 回路によって、 div 出力がどんどん遅れます。 すると、後段の en 入力のセットアップ時間が削られて、対応可能な周波数が下がります。 そこで、 div 出力に D Flip Flop を追加して出力を遅延させて、次段の en 入力でセットアップ時間をかせぎます。 この回路の目的は分周をする事なので、遅延が増えても問題になりません。 D Flip Flop を入れた効果により、最大周波数は 31MHz から 35MHz に上がりました。


分周比レジスタ

分周比は、 Control Register を使用して設定します。 Control Register は、 BUS_CLK で駆動されており、 Pin_Probe で駆動されるプリスケーラとはクロックが異なっています。 使用するクロックによって分割された範囲の事をクロックドメインと呼んでいます。 通常、クロックドメインを越えて信号をやり取りすると、タイミング上の問題が発生するため、間に受け側のクロックドメインに合わせるための仕掛けが必要です。

Control Register の出力に追加された Sync コンポーネントを使用すると、 BUS_CLK に同期した信号を Pin_Probe に同期した信号に変換します。 これで、クロックドメインを越えることができるので、安心して、プリスケーラの設定を CPU から行うことが出来ます。


キー入力回路

プリスケーラの分周比を変更するために評価ボード上のタクトスイッチを使用します。 実際の処理は、ソフトウェアで行います。 ここでは、スイッチの状態を正しく伝えるための回路が追加されています。

Debouncer コンポーネントは、機械的スイッチに特有のチャタリング(英語で Bounce)を除去(Debounce)するために使用されます。 タクトスイッチは、押すと "0" になる負論理で構成されているので、 Debouncer の出力にインバータを追加しています。 そして、 Status Register に導入して CPU からキーの状態を読み取ります。 キーによる分周比の設定変更は、ソフトウェアで行います。

タイマ

タイマの回路

タイマ部分では、プリスケーラの最終段の100分周で出力された信号を Sync コンポーネントを介して Timer コンポーネントの Capture 入力に導きます。 そして、分周された信号の立ち上がりエッジごとにタイマカウンタの値を記録していきます。 ふたつの立ち上がりエッジの時間差から信号の周期を知ることができます。

記録されたカウンタの値は、 Timer コンポーネントの FIFO に入ります。 FIFO の値を割り込み int_Capture の発生ごとにソフトウェアで読み取って、差分を計算します。 計算した結果は、ソフトウェアで LCD モジュールに表示させます。

このシステムでは、32ビットの Timer コンポーネントが律速となり、28MHzが最大駆動周波数となりました。

ソフトウェア

ソフトウェアは、かなり長くなりました。

#include <project.h>

#define     KEY_UP      (0x01)
#define     KEY_DOWN    (0x02)
#define     MAX_RANGE   (7)
#define     MAX_POWER   (10)
#define     CPU_FREQ    (48e6)

KEY_UP と KEY_DOWN で、 Status Register でのキーの配置を示しています。 MAX_RANGE は、分周器の設定の最大インデックスを示します。 インデックスは、0から始まります。 MAX_POWER は、 LCD に表示可能な十進数の最大桁数を示します。 CPU_FREQ は、 LCD に表示されるサイクル数の想定周波数を示します。

// Interrupt handler
CYBIT int_Capture_flag = 0;

CY_ISR(int_Capture_isr) {
	int_Capture_flag = 1;
}

割り込み処理は、フラグを立てる操作のみを行い、実際の処理はメインループで行います。

// Decimal number generation
CYCODE const uint32 power10[MAX_POWER+1] = {
1UL,
1UL,
10UL,
100UL,
1000UL,
10000UL,
100000UL,
1000000UL,
10000000UL,
100000000UL,
1000000000UL,
};

void LCD_PrintDecUint32(uint32 d, uint8 digits) {
	uint8  m;
	uint8  v;
    static char numbuf[32];
    
    for (m = MAX_POWER; m > 0; m--) {
	    for (v = 0; d >= power10[m]; ) {
	  	  d -= power10[m];
		  v++;
	    }
	    if (m <= digits) {
            numbuf[digits-m] = '0' + v;
	    }
	}
    numbuf[digits] = 0;
    LCD_PrintString(numbuf);
}

LCD に十進数で整数を表示します。 基になる値は、 uint32 の32ビット整数です。 8051 のために、極力乗除算を行わない方式としました。

// Parameters for frequency range
CYCODE const struct {
    double  divisor;
    uint8   mux;
} params[MAX_RANGE+1] = {
    {1e2,  0x0},    // x100 prescaler
    {1e3,  0x1},    // x1k prescaler
    {1e4,  0x2},    // x10k prescaler
    {1e5,  0x3},    // x100k prescaler
    {1e6,  0x4},    // x1M prescaler
    {1e7,  0x5},    // x10M prescaler
    {1e8,  0x6},    // x100M prescaler
    {1e9,  0x7},    // x1G prescaler
};

プリスケーラの設定は、この構造体で定義される8種類です。 ふたつのフィールドは、分周比(divisor)と Control Register に設定する値(mux)を示しています。

// Frequency range control
uint8   range;
double  resolution;
double  cpks;  // cycles per kilo-second
uint8   required;

void setRange(uint8 p_range) {
    range = p_range;
    int_Capture_Disable();
    CR1_Write(params[range].mux);
    resolution = 1e0 / (BCLK__BUS_CLK__HZ * params[range].divisor);
    cpks = CPU_FREQ * 1e3;
    required = 2;
    LCD_ClearDisplay();
    LCD_Position(0,15);
    LCD_PutChar('0' + range);
    LCD_Position(0,10);
    LCD_PrintString("Hz");
    LCD_Position(1,10);
    LCD_PrintString("mc");
    Timer_ClearFIFO();
    int_Capture_ClearPending();
    int_Capture_Enable();
}

プリスケーラの設定を変更する時には、 newRange() 関数を呼び出します。 この関数では、プリスケーラの分周比設定と周波数を計算する際に使用される係数、そして LCD の表示の初期化を行っています。

// Frequency and Period calculation
uint32 capture_last = 0;
uint32 capture_now;
uint32 capture_period;
double period;
uint32 freq;
uint32 cycles;

int main()
{
    uint8  key;
    
    CyGlobalIntEnable; /* Enable global interrupts. */

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    int_Capture_StartEx(int_Capture_isr);
    LCD_Init();
    Timer_Start();
    setRange(0);

    for(;;) {
        /* Place your application code here. */
        if (int_Capture_flag) {
            int_Capture_flag = 0;
            capture_now = Timer_ReadCapture();
            capture_period = capture_last - capture_now;
            capture_last = capture_now;
            if (required == 0) {
                period = (double)capture_period * resolution;
                freq = (uint32)(1e0 / period);
                cycles = (uint32)(period * cpks);
                LCD_Position(0,0);
                LCD_PrintDecUint32(freq, 10);
                LCD_Position(1,0);
                LCD_PrintDecUint32(cycles, 10);
            } else if (required == 1) {
                LCD_Position(1,15);
                LCD_PutChar('*');
                required = 0;
            } else {
                LCD_Position(1,14);
                LCD_PutChar('*');
                required = 1;
            }
        }
        key = SR1_Read();
        if (key == KEY_UP) {
            if (range < MAX_RANGE) {
                setRange(range + 1);
            }
            while (SR1_Read());
        }
        if (key == KEY_DOWN) {
            if (range > 0) {
                setRange(range - 1);
            }
            while (SR1_Read());
        }
    }
}

main() 関数のメインループでは、フラグを監視して周波数を計算・表示する機能とキー入力を検出してプリスケーラの設定を変更する機能が入っています。

プリスケーラの設定変更直後は、タイマから取得した値に正しい値が入っていないため、二回ほど計算と表示を見送っています。 LCD には、入力信号の周波数と CPU_FREQ のクロックで駆動したと仮定した場合のサイクル数が表示されます。

LCD への表示

LCD への表示

できたので、さっそく、 PSoC 4 M-Series のループ周期を測定してみます。 ほぼ、何も行っていない状態ですが、速度が 2.086MHz で、23サイクルを要している事がわかります。 下の段の表示の単位はミリサイクルとなっています。

右上は、プリスケーラの設定インデックスを示しています。 この場合、 "4" と表示されているので、プリスケーラの分周比は x1M である事がわかります。 また、表示間隔が約0.5秒になっており、この時間を利用して数値計算と LCD への表示を行っています。

数値計算と LCD への表示にかなり時間を取られていますので、あまり分周比を低くすると正しい周波数が表示されなくなります。 この約 2MHz の信号の場合には、プリスケーラの分周比を x10k よりも低くすると、上記の処理中に次の割り込みが発生してしまい、正しい値が得られませんでした。

逆にプリスケーラの分周比を x1M よりも大きくすると、表示間隔が数秒以上に伸びてしまい、使い勝手が悪くなってしまいます。 適切な分周比の設定を選ぶ必要があります。

プロジェクトアーカイブ

この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できるようになります。


サイクルタイムを測定しよう (6) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

前回の実験で、分岐命令のサイクルタイムが命令が配置されたアドレスに依存する事がわかりました。 今回は、さらに、他の要素が無いか調べます。

分岐距離による違い

前回は分岐命令のアドレスによりサイクルタイムが変化する様子が観測されました。 今回は、分岐命令のアドレスを固定して、分岐先のアドレスを変化させてみます。 分岐先を変化させるために、以下のようなソースコードを作成し、 NOP 命令の数で分岐距離を調整します。

// Measure the execution cycle time
uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    s = *reg;
    asm(
        "b label_2\n"
        "nop\n"   // 5
        "nop\n"   // 4
        "nop\n"   // 3
        "nop\n"   // 2
        "nop\n"   // 1
        "label_2:\n"
    );
    e = *reg;
    
    return e - s;
}

実験結果

NOP 命令の数を最大14まで変えてサイクルタイムを測定しました。

NOP数サイクル数
48/24/12
分岐先アドレス
07/7/70004
17/7/70006
27/7/70008
37/7/7000A
47/7/7000C
58/7/7000E
69/8/70010
79/8/70012
8A/8/70014
9B/9/70016
109/8/70018
119/8/7001A
12A/8/7001C
13B/9/7001E
149/8/70020

このように分岐命令を同じアドレスに配置した場合でも、分岐先アドレスによってサイクルタイムが影響を受ける事がわかりました。 おおよそ8バイトごとの繰り返しになっています。 中には、11サイクル (B) という結果も出ていました。 つまり、分岐命令の実行時間として3サイクルから7サイクルまで幅が観測された事を示しています。

NOP が7個までの部分では、それほどサイクル数が長くなっていません。 これは、プリフェッチがうまく機能したためと考えられます。


サイクルタイムを測定しよう (5) [PSoC]このエントリーを含むはてなブックマーク#

実験回路

今回は、分岐命令の実行サイクルタイムを測定します。

分岐命令を入れる

前回の記事で、 "nop" 命令を入れた所に "b" 命令を入れます。

    uint32  s;
    uint32  e;
    
    s = *reg;
    asm(
        "b label_2\n"
        "label_2:\n"
    );
    e = *reg;

分岐先は、 "label_2" ラベルで指示します。 まずは、次の命令に分岐した場合について調べます。

このプログラムをコンパイルすると、このようなコードが生成されます。

  39:.\main.c      ****     uint32  s;
  40:.\main.c      ****     uint32  e;
  41:.\main.c      ****     
  59:.\main.c      ****     s = *reg;
  95 0000 0268     		ldr	r2, [r0]
  60:.\main.c      ****     asm(
  99 0002 FFE7     		b label_2
 100              	label_2:
  61:.\main.c      ****         "b label_2\n"
  72:.\main.c      ****         "label_2:\n"
  73:.\main.c      ****     );
  74:.\main.c      ****     e = *reg;
 105 0004 0368     		ldr	r3, [r0]

次の命令に分岐するという事は、動作としては "nop" と変わりありません。 ところが、サイクルタイムは7サイクルと表示されました。

Freq:48
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

Freq:24
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

Freq:12
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

つまり、分岐命令に要したのは、3サイクルという計算になります。 この結果から、分岐命令の処理に3サイクル必要だと判断できます。

命令の配置アドレスを変えてみた

ところが、いかなる場合でも3サイクルになったわけではありませんでした。 分岐命令の配置アドレスによって、サイクルタイムが変わってきたのです。

  39:.\main.c      ****     uint32  s;
  40:.\main.c      ****     uint32  e;
  41:.\main.c      ****     
  42:.\main.c      ****     asm(
  90 0000 C046     		nop
  91 0002 C046     	nop
  92 0004 C046     	nop
  93 0006 C046     	nop
  94              	label_1:
  95              	
  53:.\main.c      ****         "nop\n"   // 4
  54:.\main.c      ****         "nop\n"   // 3
  55:.\main.c      ****         "nop\n"   // 2
  56:.\main.c      ****         "nop\n"   // 1
  57:.\main.c      ****         "label_1:\n"
  58:.\main.c      ****     );
  59:.\main.c      ****     s = *reg;
  99 0008 0268     		ldr	r2, [r0]
  60:.\main.c      ****     asm(
 103 000a FFE7     		b label_2
 104              	label_2:
 105              	
  61:.\main.c      ****         "b label_2\n"
  72:.\main.c      ****         "label_2:\n"
  73:.\main.c      ****     );
  74:.\main.c      ****     e = *reg;
 109 000c 0368     		ldr	r3, [r0]

例えば、このプログラムでは、先頭に "nop" 命令を4個挿入して、コードの位置を8バイト後ろにずらしました。 すると、以下のように表示されました。

Freq:48
0009 0009 0009 0009 0009 0009 0009 0009
0009 0009 0009 0009 0009 0009 0009 0009

Freq:24
0008 0008 0008 0008 0008 0008 0008 0008
0008 0008 0008 0008 0008 0008 0008 0008

Freq:12
0007 0007 0007 0007 0007 0007 0007 0007
0007 0007 0007 0007 0007 0007 0007 0007

このようにバス周波数によってサイクル数が異なっている原因は、 Flash ROM のウェイトサイクルが影響しているものと推測されます。 挿入する "nop" 命令の数を変えながらサイクル数を測定したのが、以下の表です。

NOP数サイクル数
48/24/12
分岐先アドレス
07/7/70004
17/7/70006
27/7/70008
37/7/7000A
49/8/7000C
59/8/7000E
67/7/70010
77/7/70012
89/8/70014
99/8/70016
107/7/70018
117/7/7001A
129/8/7001C
139/8/7001E
147/7/70020

このように NOP 命令が4個(8バイト)増えるごとにサイクルタイムの長いパターンが発生します。 この結果から、プリフェッチバッファは、8バイトで構成されているらしいことが推測されます。 さらに分岐先アドレスに着目すると、飛び先アドレスの LSB 部分が X1X0 であった場合に限りサイクルタイムが長くなっている事がわかります。 これは、8バイトバッファの後半に分岐した場合には、次の8バイトのプリフェッチが追加されているものと推測されます。

今回の実験で、分岐命令の分岐先のアドレスにより、必要なサイクル数が異なってくるらしい事がわかってきました。 実験で見えてきた違いは、1サイクルまたは2サイクルです。 このくらいだったら気にすることも無いですかね。


この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。