SSブログ

なぜ、 PSoC 5LP はクロックを LED に直結できるのか。 [PSoC]このエントリーを含むはてなブックマーク#

クロック直結 LED

PSoC 5LP で L チカをやるとき、一番簡単なのはクロック出力を出力端子に接続する事です。 これだけで、 LED を点滅させることが出来ます。 でも、このワザは、 PSoC 4 では使えません。 この記事では、なぜ PSoC 5LP だけがクロックを LED に直結できるのかを探ります。

クロックは、どこへ行く

まず、クロックを出力端子に直結して、回路を合成してみます。 合成の結果は、 "rpt" ファイルに詳しく書かれています。

Clock group 0: 
    Clock Block @ F(Clock,0): 
    clockblockcell: Name =ClockBlock
        PORT MAP (
            imo => ClockBlock_IMO ,
            pllout => ClockBlock_PLL_OUT ,
            ilo => ClockBlock_ILO ,
            clk_100k => ClockBlock_100k ,
            clk_1k => ClockBlock_1k ,
            clk_32k => ClockBlock_32k ,
            xtal => ClockBlock_XTAL ,
            clk_32k_xtal => ClockBlock_XTAL_32KHZ ,
            clk_sync => ClockBlock_MASTER_CLK ,
            clk_bus_glb => ClockBlock_BUS_CLK ,
            clk_bus => ClockBlock_BUS_CLK_local ,
            dclk_glb_0 => Net_1 ,
            dclk_0 => Net_1_local );
        Properties:
        {
        }

ディジタルクロックブロックが一つ使用されており、 dclk_0 出力に 2Hz のクロック信号 Net_1_local が出てくるようになっています。 この信号を出力端子に引きこむ事で、 LED を点滅させることが出来ます。

ペリフェラルクロック

クロックシステムのブロック図

PSoC 5LP で任意のクロックを生成する場合、8系統のクロック生成器でペリフェラルクロックと呼ばれるクロックを生成します。 クロック生成器の入力は、7種類のクロック源に加えて、一般のディジタル信号 (Digital Signal Interconnect: DSI) からも取り入れる事が出来ます。 これを使うと、ユーザの作成した回路から出てくる信号を別のシステムのクロックに使ったりなど、かなり融通の利く(ゆるい)取りまわしが出来ます。


クロック生成器

入力されたクロックは、16ビットの分周器で分周されます。 この出力は、一般のディジタル信号と同じように出てゆきます。 つまり、クロックと一般の信号の区別が非常にゆるい構成になっているのです。

出て行った信号が出力端子に接続されれば、 LED に直結する事も出来ます。 非常に簡単です。

PSoC 42xx のクロック生成器

PSoC 42xx のクロックシステム

一方、 PSoC 42xx の場合、 HFCLK をクロック源とする分周器が4系統あります。 クロックを必要とするペリフェラルは、16個のブロックに分割されます。 そして、それぞれのペリフェラルが、この4系統からクロックを選んで使用するという構成になっています。


ペリフェラルクロックの行き先

それぞれのペリフェラルに供給されたクロックの用途は、クロックに限られます。 PSoC 5LP のように一般の信号としては使用できません。 これが、クロックを LED に直結できない最大の理由です。 直結するルートが無いので直結できないのです。

PSoC 42xxM のクロック生成器

PSoC 42xxM のクロックシステム

クロックシステムは、 PSoC 4 であっても品種によって少しずつ異なっています。 この図は、 PSoC 42xx M-Series のブロック図です。 25ブロックのクロックを21系統の分周器から選択するようになっています。 もちろん、これらの用途もクロックに限られていますので、一般の信号としては使用することが出来ません。 いずれにしても、ペリフェラルクロックは必ず HFCLK を分周した信号なので、 HFCLK で同期しやすくなっています。

それでも L チカしたい

PSoC 4 でも L チカ

それでも、お手軽に PSoC 4 で L チカをしたいとなったら、 Toggle Flip-Flop (TFF) を使ってクロックを一般の信号に変換する方法があります。 この方法を使うと、 TFF を実現するためのマクロセルを一個消費してしまいます。 さらに、この TFF が含まれる UDB は、このクロック以外は使う事ができません。 条件によっては、厳しくなるでしょう。

同様にクロックを Interrupt コンポーネントに接続して周期割り込みを構成する場合なども、この手法が使えます。 もし、 UDB が足りなくなったら、別の方法が無いか考えましょう。

参考文献

PSoC® 5LP Architecture TRM
PSoC の内部構成に関する情報は、 Technical Reference Manual (TRM) という文書に記述されています。 これは、 PSoC 5LP の Architecture TRM です。
PSoC 4100 and 4200 Family: PSoC® 4 Architecture Technical Reference Manual (TRM)
これは、 PSoC 42xx の Architecture TRM です。 兄弟である PSoC 41xx の情報も入っています。
PSoC 4100M/4200M Family: PSoC® 4 Registers Technical Reference Manual (TRM)
これは、 PSoC 41xx M-SeriesPSoC 42xx M-Series の Architecture TRM です。

関連商品

SparkFun FreeSoC2 開発ボード - PSoC5LP

SparkFun FreeSoC2 開発ボード - PSoC5LP

  • 出版社/メーカー: Sparkfun
  • メディア: エレクトロニクス
PSoC 4200 Prototyping Kit

PSoC 4200 Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス
PSoC 4200M CY8CKIT-043 Prototyping Kit

PSoC 4200M CY8CKIT-043 Prototyping Kit

  • 出版社/メーカー: スイッチサイエンス
  • メディア: エレクトロニクス

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

FM0+ 評価ボード

前回は、ループの配置アドレスを変えながら、サイクル数を測定しました。 今回は、ループ内の命令数によってサイクル数に差が出るかについて調べました。

ソースコード

ループ内の命令数を変化させるために、ループ内に "nop" 命令を追加します。 以下のコードで、 "nop" 命令の数を調整します。

void func9(reg8_t *reg, uint8_t val0, uint8_t val1) @ ".text.func9" {
    for (;;) {
        __asm(
            "nop\n"     // 29
            "nop\n"     // 28
            "nop\n"     // 27
            "nop\n"     // 26
            "nop\n"     // 25
            "nop\n"     // 24
            "nop\n"     // 23
            "nop\n"     // 22
            "nop\n"     // 21
            "nop\n"     // 20
            "nop\n"     // 19
            "nop\n"     // 18
            "nop\n"     // 17
            "nop\n"     // 16
            "nop\n"     // 15
            "nop\n"     // 14
            "nop\n"     // 13
            "nop\n"     // 12
            "nop\n"     // 11
            "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 = val0;
        *reg = val1;
    }
}

このコードでは、ループの配置アドレスは固定されますが、代わりに分岐命令のアドレスが変化していきます。

     50          void func9(reg8_t *reg, uint8_t val0, uint8_t val1) @ ".text.func9" {
   \                     func9: (+1)
   \   00000000   0xB500             PUSH     {LR}
     83              for (;;) {
     84                  __asm(
    110                      "nop\n"     // 4
    111                      "nop\n"     // 3
    112                      "nop\n"     // 2
    113                      "nop\n"     // 1
    114                      "label_2:"
    115                  );
   \                     ??func9_0: (+1)
   \   00000002   0xBF00             nop
   \   00000004   0xBF00             nop
   \   00000006   0xBF00             nop
   \   00000008   0xBF00             nop
    116                  *reg = val0;
   \                     ??label_2: (+1)
   \   0000000A   0x7001             STRB     R1,[R0, #+0]
    117                  *reg = val1;
   \   0000000C   0x7002             STRB     R2,[R0, #+0]
   \   0000000E   0xE7F8             B        ??func9_0
    118              }
    119          }

上記は、 "nop" 命令を4個入れた場合のコードです。 分岐先アドレスは "0002" のままですが、分岐命令は "000E" に移動しています。

測定結果

サイクル数を測定しました。 "nop" ひとつあたり1サイクルほど命令実行時間が増えますので、 "nop" による影響を除いたサイクル数を計算しました。

NOP数サイクル数
(除NOP)
分岐先アドレス分岐命令
040206
140208
24020A
34020C
44020E
560210
660212
760214
860216
960218
106021A
116021C
126021E
1370220
1470222
1570224
1670226
1770228
187022A
197022C
207022E
2180230
2280232
2380234
2480236
2580238
268023A
278023C
288023E
2990240

8命令(16バイト)ごとにサイクル数が一つずつ増えているのが分かります。 言い方を変えると、8個の "NOP" を実行するために9サイクルの時間を要しているのです。 ちょっと、効率が悪くありませんか?

推測するに、16バイト境界を超えるごとに命令フェッチに1サイクルの実行時間が必要となっていると考えられます。 通常、プリフェッチとパイプラインの構造がうまく働いていれば、命令フェッチの時間はパイプラインに隠れてしまうはずです。 しかし、この実験結果からは、命令フェッチが見えてしまっています。 しかも、命令フェッチサイクルが増えるのは、分岐命令アドレスの下4ビットが 0000 になった時です。 本当にプリフェッチしていないのでしょうか?


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

FM0+ 評価ボード

前回は、最小ループのサイクル数を測定しました。 今回は、ループの配置アドレスによってサイクル数に差が出るかについて調べました。

ソースコード

配置アドレスを変更する考え方は、 PSoC で行った方法と同じです。 ループの前に "nop" 命令を追加して、ループのアドレスを移動します。 以下のコードで、 "nop" 命令の数を調整します。

void func9(reg8_t *reg, uint8_t val0, uint8_t val1) @ ".text.func9" {
    __asm(
        "nop\n"     // 29
        "nop\n"     // 28
        "nop\n"     // 27
        "nop\n"     // 26
        "nop\n"     // 25
        "nop\n"     // 24
        "nop\n"     // 23
        "nop\n"     // 22
        "nop\n"     // 21
        "nop\n"     // 20
        "nop\n"     // 19
        "nop\n"     // 18
        "nop\n"     // 17
        "nop\n"     // 16
        "nop\n"     // 15
        "nop\n"     // 14
        "nop\n"     // 13
        "nop\n"     // 12
        "nop\n"     // 11
        "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:"
    );
    for (;;) {
        *reg = val0;
        *reg = val1;
    }
}

func9() 関数は、 @ 指示子によって、セクション名が決められています。 このセクション名を "icf" ファイルで指定する事で、 func9() 関数の境界を定めることが出来ます。

define block HEAP      with alignment = 8, size = __ICFEDIT_size_heap__     { };
define block FUNC9     with alignment = 32 { section ".text.func9" };
define block MAIN      with alignment = 32 { section ".text.main" };
:
:
place in ROM_region   { readonly, block FUNC9, block MAIN };

このプロジェクトの場合、上記の記述が入っています。 "define block" でブロック名を定義します。 FUNC9 ブロックには ".text.func9" セクションが入っており、 alignment を 32 と定義しています。 この記述により、 func9() 関数は 32 バイト境界に配置されます。

測定結果

サイクル数を測定した所、周期的に4サイクルと6サイクルのパターンを繰り返す事がわかりました。

NOP数サイクル数分岐先アドレス分岐命令
040206
140408
24060A
34080C
440A0E
560C10
660E12
741014
841216
941418
104161A
114181C
1241A1E
1361C20
1461E22
1542024
1642226
1742428
184262A
194282C
2042A2E
2162C30
2262E32
2343034
2443236
2543438
264363A
274383C
2843A3E
2963C40

サイクル数が6サイクルになるのは、分岐先アドレスの下4ビットが 11xx になるときです。 このとき、分岐先のアドレスと分岐命令アドレスの上位部分が異なっているのがわかります。 例えば、 NOP 数が 5 の時、分岐先アドレスの上位4ビットは 0000 ですが、分岐命令アドレスの上位4ビットは 0001 です。 このように、分岐先と分岐命令が16バイト境界を超えるとサイクル数が増えます。

これは、16バイト境界を超えると分岐先命令をフェッチするために追加サイクルが必要になり、サイクル数が伸びたと考えられます。 また、 Flash ROM のフェッチバッファのサイズは16バイトであるとも推測できます。

ひとつだけ腑に落ちない点もあります。 例えば、 NOP数が 4 の時、分岐命令は 0E にあります。 分岐命令が実行される時、命令がプリフェッチされているのであれば、バッファには 10 から 17 までの内容がロードされているはずです。 その場合、分岐先の命令がバッファに入っていないので、当然、再度フェッチが発生し、サイクル数が伸びるはずです。 しかし、実際には、4サイクルのままです。 この事実から推測されるのは、分岐実行の前にプリフェッチが行われていないのか、プリフェッチバッファが多数存在するかのどちらかです。 この測定結果からは、どちらとも言えません。


サイクルタイムを測定しよう (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バイト境界の前半に配置するようにオプションを付けると処理時間が短くなりそうです。


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