So-net無料ブログ作成
検索選択
前の8件 | -

continue 文の次に実行されるのは? [PSoC]このエントリーを含むはてなブックマーク#

本日のお題

Twitterで、お題をいただきました。 これは、いっちょ試してみなくては。 題材に取り上げたのは、 Cortex-M0 搭載の PSoC 4 M-Series です。

GCC の場合

まず、 PSoC Creator のデフォルトツールチェインである GCC で試しました。 使用したコードは以下の通りです。

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

    for (;;) {
        int i;
        for (i = 0; i < 10; ++i) {
            if (i == 0) continue;
        }
    }
}

for 行の中に変数宣言が入れられないので、外で宣言しています。 あれ、もしかしたら、これは C のお題じゃなかったのかな? 気にせず、 LST ファイルを見てみます。

  30 0000 62B6     		CPSIE   i
  33              	.L4:
  37 0002 0023     		mov	r3, #0
  38 0004 00E0     		b	.L2
  40              	.L3:
  42 0006 0133     		add	r3, r3, #1
  44              	.L2:
  46 0008 092B     		cmp	r3, #9
  47 000a FCDD     		ble	.L3
  48 000c F9E7     		b	.L4

変数 i は、 r3 に割り当てられています。 i を 0 と比較する部分は、存在しません。 すでに最適化されてしまっているようです。

continue 文の次に実行されるものは、そもそも if 文さえ存在しないので、問い自体が無意味である。

最適化しない GCC の場合

この結論じゃ面白くないので、最適化されないように -O0 オプションを付けました。

  28 0000 80B5     		push	{r7, lr}
  32 0002 82B0     		sub	sp, sp, #8
  34 0004 00AF     		add	r7, sp, #0
  38 0006 62B6     		CPSIE   i
  41              	.L5:
  44 0008 0023     		mov	r3, #0
  45 000a 7B60     		str	r3, [r7, #4]
  46 000c 06E0     		b	.L2
  47              	.L4:
  49 000e 7B68     		ldr	r3, [r7, #4]
  50 0010 002B     		cmp	r3, #0
  51 0012 00D1     		bne	.L3
  52 0014 C046     		mov	r8, r8
  53              	.L3:
  55 0016 7B68     		ldr	r3, [r7, #4]
  56 0018 0133     		add	r3, r3, #1
  57 001a 7B60     		str	r3, [r7, #4]
  58              	.L2:
  60 001c 7B68     		ldr	r3, [r7, #4]
  61 001e 092B     		cmp	r3, #9
  62 0020 F5DD     		ble	.L4
  65 0022 F1E7     		b	.L5

すると、変数をレジスタに割り当てる事もしないコンパイル結果となりました。 i を 0 と比較する部分は、50行目にあります。 その後、51行目の "bne" で条件分岐を行います。 continue 文に相当するのは、52行目の "mov r8, r8" です。 その後は、55行目から始まる ++i が実行されます。 つまり、 continue 文の次に実行されるのは、 "++i" であるという事がわかりました。

continue 文の次に実行されるものは、 ++i である。

MDK-ARM の場合

同じプロジェクトを MDK-ARM でコンパイルしてみました。

0x000006E4 BF00      NOP      
0x000006E6 B662      CPSIE    I
0x000006E8 BF00      NOP      
0x000006EA BF00      NOP      

0x000006EC 2000      MOVS     r0,#0x00
0x000006EE E004      B        0x000006FA

0x000006F0 2800      CMP      r0,#0x00
0x000006F2 D100      BNE      0x000006F6
0x000006F4 E000      B        0x000006F8

0x000006F6 BF00      NOP      
0x000006F8 1C40      ADDS     r0,r0,#1

0x000006FA 280A      CMP      r0,#0x0A
0x000006FC DBF8      BLT      0x000006F0

0x000006FE E7F5      B        0x000006EC

機能制限版では、 LST ファイルが出力されないようなので、デバッガの "Disassembly" ウィンドウから取ってきました。 変数 i は、 r0 に割り当てられています。

if 文は 0x000006F0 番地にあります。 continue 文に相当するのは、 0x000006F4 番地の "B 0x000006F8" です。 分岐先の 0x000006F8 番地には、 ++i があります。

continue 文の次に実行されるものは、 ++i である。

IAR Embedded Workbench の場合

IAR のコンパイラでも、同じようにコンパイルしてみました。

   \                     main: (+1)
   \   00000000   0xB500             PUSH     {LR}

   \   00000002   0xB662             CPSIE   i
   \                     ??main_0: (+1)
   \   00000004   0x2000             MOVS     R0,#+0
   \                     ??main_1: (+1)
   \   00000006   0x280A             CMP      R0,#+10
   \   00000008   0xDAFC             BGE      ??main_0
   \   0000000A   0x2800             CMP      R0,#+0
   \                     ??main_2: (+1)
   \   0000000C   0x1C40             ADDS     R0,R0,#+1
   \   0000000E   0xE7FA             B        ??main_1

変数 i は、 R0 レジスタに割り当てられています。 変数 i と 0 を比較する部分は、 0000000A 番地の "CMP R0,#+0" です。 しかし、比較をしているだけで後に続く条件分岐が見当たりません。 次に実行されるのは、 0000000C 番地の ++i です。

continue 文に相当する部分が存在しないが、結果的に次に実行されるものは、 ++i である。

最適化をがんばったら

これまでは、 "-O0" オプションで最適化を抑制していました。 普通は、多少の最適化を行わせますので、ためしに GCC の再弱最適化オプションである "-O1" を使ってコンパイルさせてみました。

  30 0000 62B6     		CPSIE   i
  33              	.L2:
  35 0002 FEE7     		b	.L2

すると、最適化の結果、何も残りませんでした。 後にあるのは、一番外側の無限ループのみです。

最適化されると、 for 文がまるごと省略される。

関連商品

PSoC 4200M CY8CKIT-043 Prototyping Kit

PSoC 4200M CY8CKIT-043 Prototyping Kit

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

Clock_Start() メソッドでクロックが起動しない [PSoC]このエントリーを含むはてなブックマーク#

ボタンでブザーを操作する回路

PSoC 4200 を使って、ブザーをボタンにより操作しようとしているのだけど、うまく動かないと相談を受けました。 さて、何が原因でしょうね。

ソフトウェアでで Clock を制御する

使われた回路は、上にある通りです。 ソフトウェアで入力端子の状態を読み出し、それにしたがって、 Clock コンポーネントを動かしたり止めたりするというコンセプトです。 ソフトウェアでの判断は、以下のようになっています。

#include <project.h>

uint8   SW2_state;

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

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

    for (;;) {
        /* Place your application code here. */
        SW2_state = Pin_SW2_Read();
        if (SW2_state) {
            // SW2 OFF
            Clock_880Hz_Stop();
        } else {
            // SW2 ON
            Clock_880Hz_Start();
        }
    }
}

メインループの中で入力端子の状態を読み取り、状態にしたがって Stop() メソッドまたは Start() メソッドを呼び出すという仕組みです。 ボタンを押し続けた場合、このループでは繰り返し Start() メソッドが呼び出されます。 「 Start() メソッドにより、 Clock コンポーネントが起動される」という目的で作られているのであれは、これでも問題ないのですが、どうも起動する以外の副作用が有りそうです。

実験開始

実験回路

そこで、実験回路を作成しました。

左半分の回路では、 Pwm_Start タイマにより周期的な割り込み int_Start を発生させて Pin_LED 端子に接続されている Clock コンポーネントの Start() メソッドを発行しています。 LED は、 Clock コンポーネントの周期 200ms で点滅を行います。 タイマに PWM コンポーネントを使用しているのは、出力を波形で見るためです。

一方、右側の回路は、左の回路で生成された信号を取りこむロジック・アナライザとして機能します。 信号を取りこむタイミングは、割り込み int_Sample で決定します。 Status Register で取りこんだ信号は、 UART コンポーネントで送り出します。 UART で送信されたデータは Bridge Control Panel で波形として表示します。 この動作は、 CY8CKIT-042 でロジアナを作った ~UART編~ で紹介した通りです。

// Re-start Clock component ISR
CY_ISR(int_Start_isr) {
    Clock_5Hz_Start();
}

// Logic sampling ISR
CYBIT  int_Sample_flag = 0;
uint8   probe = 0;

CY_ISR(int_Sample_isr) {
    probe = SR_Probe_Read();
    int_Sample_flag = 1;    
}

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

    // Start method calling timing
    Pwm_Start_Start();
    Pwm_Start_WritePeriod(489);
    int_Start_StartEx(int_Start_isr);
    
    // Logic sampling timing
    int_Sample_StartEx(int_Sample_isr);
    
    // Logic analyzer output
    UART_Start();

    // Main loop
    for(;;) {
        // Sample logic level periodically        
        if (int_Sample_flag) {
            UART_UartPutChar(probe);
            int_Sample_flag = 0;
        }
    }
}

ソフトウェアは、上記のとおりです。 Start() メソッドを発行する周期を Pwm_Start タイマで規定しています。 この実験では、周期を変化させて動作を確認するために、周期をソフトウェアで決定しています。 こうすると、プロジェクトを Build した時に回路の配置配線を行う必要が無いため、実験に必要な時間を減らす事ができます。

実験の結果

490ms周期

まず、 Start() メソッドの周期を 490 とした場合、このような波形になりました。 Start() メソッドを発行してから、ちょうど 100ms 後に LED 出力が立ち下がり、さらに 100ms 後に立ちあがっているのがわかります。 100ms は、 LED を駆動する Clock コンポーネントの周期の半分です。 この様子から、 Start() メソッドの発行により、 Clock コンポーネントの内部カウンタがリセットされたと考えられます。 内部カウンタがリセットされたのに Clock 出力そのものがリセットされないのは妙です。 そこで、 Start() メソッドの周期を 390ms としてみました。


390ms周期

すると、 Start() メソッド発行後 100ms の位置にあった立ち下がりエッジが無くなりました。 さらに 100ms あと、つまり Start() メソッド発行後 200ms には立ち上がりエッジがあります。 これらの実験から以下の推論が導き出されます。

  1. Start() メソッドを発行すると Clock の内部カウンタがリセットされるが、クロック出力は以前の状態を保持する。
  2. Start() メソッドの発行からクロック半周期分の時間が経過すると、クロック出力が "0" に設定される。
  3. さらに半周期分の時間が経過するとクロック出力は "1" に設定される。
  4. 以降、クロック出力を "0" と "1" で交互に設定するとクロック出力が変化する。

以上の推測から、 Start() メソッドの発行周期を 200ms よりも短くすると、クロック出力は "0" に設定されたままで、クロックとしては機能しなくなると考えられます。


199ms周期

そこで、 Start() メソッドの周期を 199ms にしました。 すると、みごとに LED が点滅しなくなりました。 ブザーの音が出なくなったのは、 Start() メソッドの周期が短すぎて、クロック出力が出なくなってしまったものと推測されます。

解決策

ここからは、どうやったらブザー出力が出てくるかという解決策を探ります。 実験の結果から、 Start() メッソドの呼び出し周期が 1.2ms (880Hz) よりも短ければ、クロックは出力されます。 そこで、ボタンの判定の後、20ms ほど遅延を入れて Start() メッソドの呼び出しが頻繁に発生しないようにします。

    for (;;) {
        /* Place your application code here. */
        SW2_state = Pin_SW2_Read();
        if (SW2_state) {
            // SW2 OFF
            Clock_880Hz_Stop();
        } else {
            // SW2 ON
            Clock_880Hz_Start();
        }
        CyDelay(20);
    }

これで、ボタンに連動してブザーが鳴るようになります。 しかし、音が「にごる」ようになってしまいました。 これは、追加した遅延時間が短すぎて、クロック出力が歪んでしまったのが原因です。

遅延時間を長くすると、音の濁りが少なくなりますが、ボタンを押してから音が出る・止まるまでの遅延も大きくなり、ボタンに対する反応が遅くなったように感じます。

#include <project.h>

uint8   SW2_state;
uint8   SW2_last_state = 1u;    // OFF as default

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

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

    for (;;) {
        /* Place your application code here. */
        SW2_state = Pin_SW2_Read();
        if (SW2_state && !SW2_last_state) {
            // SW2 ON to OFF
            Clock_880Hz_Stop();
        } else if (!SW2_state && SW2_last_state) {
            // SW2 OFF to ON
            Clock_880Hz_Start();
        }
        SW2_last_state = SW2_state;
    }
}

根本的な解決法として、以前のボタンの状態を記憶しておいて、状態が変化したら Clock コンポーネントの制御を行うようにしました。 ボタンの状態が変化した時だけ Start()/Stop() メソッドを呼び出すので、頻繁にメソッドの呼び出しが起こる事も無くなりました。 また、ボタンに対する反応も良くなりました。

参考商品

PSoC 4200 Prototyping Kit

PSoC 4200 Prototyping Kit

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

なぜ、 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件 | -