なぜ、 PSoC 5LP はクロックを LED に直結できるのか。 [PSoC]
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 の場合、 HFCLK をクロック源とする分周器が4系統あります。 クロックを必要とするペリフェラルは、16個のブロックに分割されます。 そして、それぞれのペリフェラルが、この4系統からクロックを選んで使用するという構成になっています。
それぞれのペリフェラルに供給されたクロックの用途は、クロックに限られます。 PSoC 5LP のように一般の信号としては使用できません。 これが、クロックを LED に直結できない最大の理由です。 直結するルートが無いので直結できないのです。
PSoC 42xxM のクロック生成器
クロックシステムは、 PSoC 4 であっても品種によって少しずつ異なっています。 この図は、 PSoC 42xx M-Series のブロック図です。 25ブロックのクロックを21系統の分周器から選択するようになっています。 もちろん、これらの用途もクロックに限られていますので、一般の信号としては使用することが出来ません。 いずれにしても、ペリフェラルクロックは必ず HFCLK を分周した信号なので、 HFCLK で同期しやすくなっています。
それでも 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-Series と PSoC 42xx M-Series の Architecture TRM です。
関連商品
SparkFun FreeSoC2 開発ボード - PSoC5LP
- 出版社/メーカー: Sparkfun
- メディア: エレクトロニクス
PSoC 4200M CY8CKIT-043 Prototyping Kit
- 出版社/メーカー: スイッチサイエンス
- メディア: エレクトロニクス
サイクルタイムを測定しよう (12) [FMx]
前回は、ループの配置アドレスを変えながら、サイクル数を測定しました。 今回は、ループ内の命令数によってサイクル数に差が出るかについて調べました。
ソースコード
ループ内の命令数を変化させるために、ループ内に "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) | 分岐先アドレス | 分岐命令 |
---|---|---|---|
0 | 4 | 02 | 06 |
1 | 4 | 02 | 08 |
2 | 4 | 02 | 0A |
3 | 4 | 02 | 0C |
4 | 4 | 02 | 0E |
5 | 6 | 02 | 10 |
6 | 6 | 02 | 12 |
7 | 6 | 02 | 14 |
8 | 6 | 02 | 16 |
9 | 6 | 02 | 18 |
10 | 6 | 02 | 1A |
11 | 6 | 02 | 1C |
12 | 6 | 02 | 1E |
13 | 7 | 02 | 20 |
14 | 7 | 02 | 22 |
15 | 7 | 02 | 24 |
16 | 7 | 02 | 26 |
17 | 7 | 02 | 28 |
18 | 7 | 02 | 2A |
19 | 7 | 02 | 2C |
20 | 7 | 02 | 2E |
21 | 8 | 02 | 30 |
22 | 8 | 02 | 32 |
23 | 8 | 02 | 34 |
24 | 8 | 02 | 36 |
25 | 8 | 02 | 38 |
26 | 8 | 02 | 3A |
27 | 8 | 02 | 3C |
28 | 8 | 02 | 3E |
29 | 9 | 02 | 40 |
8命令(16バイト)ごとにサイクル数が一つずつ増えているのが分かります。 言い方を変えると、8個の "NOP" を実行するために9サイクルの時間を要しているのです。 ちょっと、効率が悪くありませんか?
推測するに、16バイト境界を超えるごとに命令フェッチに1サイクルの実行時間が必要となっていると考えられます。 通常、プリフェッチとパイプラインの構造がうまく働いていれば、命令フェッチの時間はパイプラインに隠れてしまうはずです。 しかし、この実験結果からは、命令フェッチが見えてしまっています。 しかも、命令フェッチサイクルが増えるのは、分岐命令アドレスの下4ビットが 0000 になった時です。 本当にプリフェッチしていないのでしょうか?
サイクルタイムを測定しよう (11) [FMx]
前回は、最小ループのサイクル数を測定しました。 今回は、ループの配置アドレスによってサイクル数に差が出るかについて調べました。
ソースコード
配置アドレスを変更する考え方は、 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数 | サイクル数 | 分岐先アドレス | 分岐命令 |
---|---|---|---|
0 | 4 | 02 | 06 |
1 | 4 | 04 | 08 |
2 | 4 | 06 | 0A |
3 | 4 | 08 | 0C |
4 | 4 | 0A | 0E |
5 | 6 | 0C | 10 |
6 | 6 | 0E | 12 |
7 | 4 | 10 | 14 |
8 | 4 | 12 | 16 |
9 | 4 | 14 | 18 |
10 | 4 | 16 | 1A |
11 | 4 | 18 | 1C |
12 | 4 | 1A | 1E |
13 | 6 | 1C | 20 |
14 | 6 | 1E | 22 |
15 | 4 | 20 | 24 |
16 | 4 | 22 | 26 |
17 | 4 | 24 | 28 |
18 | 4 | 26 | 2A |
19 | 4 | 28 | 2C |
20 | 4 | 2A | 2E |
21 | 6 | 2C | 30 |
22 | 6 | 2E | 32 |
23 | 4 | 30 | 34 |
24 | 4 | 32 | 36 |
25 | 4 | 34 | 38 |
26 | 4 | 36 | 3A |
27 | 4 | 38 | 3C |
28 | 4 | 3A | 3E |
29 | 6 | 3C | 40 |
サイクル数が6サイクルになるのは、分岐先アドレスの下4ビットが 11xx になるときです。 このとき、分岐先のアドレスと分岐命令アドレスの上位部分が異なっているのがわかります。 例えば、 NOP 数が 5 の時、分岐先アドレスの上位4ビットは 0000 ですが、分岐命令アドレスの上位4ビットは 0001 です。 このように、分岐先と分岐命令が16バイト境界を超えるとサイクル数が増えます。
これは、16バイト境界を超えると分岐先命令をフェッチするために追加サイクルが必要になり、サイクル数が伸びたと考えられます。 また、 Flash ROM のフェッチバッファのサイズは16バイトであるとも推測できます。
ひとつだけ腑に落ちない点もあります。 例えば、 NOP数が 4 の時、分岐命令は 0E にあります。 分岐命令が実行される時、命令がプリフェッチされているのであれば、バッファには 10 から 17 までの内容がロードされているはずです。 その場合、分岐先の命令がバッファに入っていないので、当然、再度フェッチが発生し、サイクル数が伸びるはずです。 しかし、実際には、4サイクルのままです。 この事実から推測されるのは、分岐実行の前にプリフェッチが行われていないのか、プリフェッチバッファが多数存在するかのどちらかです。 この測定結果からは、どちらとも言えません。
サイクルタイムを測定しよう (10) [FMx]
今回は、 Cypress の FM0+ 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" 命令の数だけサイクルタイムを減じて、サブルーチンコールに起因する違いが見えるようにしています。
分岐先 | ||||||||||
00 | 02 | 04 | 06 | 08 | 0A | 0C | 0E | 10 | ||
分岐命令 | 06 | 19 | 19 | 20 | 21 | 19 | 19 | 20 | 21 | 16 |
08 | 19 | 19 | 20 | 21 | 19 | 19 | 20 | 21 | 16 | |
0A | 20 | 20 | 21 | 22 | 20 | 20 | 21 | 22 | 19 | |
0C | 21 | 21 | 22 | 23 | 21 | 21 | 22 | 23 | 16 | |
0E | 21 | 21 | 22 | 23 | 21 | 21 | 22 | 23 | 16 | |
10 | 21 | 21 | 22 | 23 | 21 | 21 | 22 | 23 | 16 | |
12 | 22 | 22 | 23 | 24 | 22 | 22 | 23 | 24 | 21 | |
14 | 21 | 21 | 22 | 23 | 21 | 21 | 22 | 23 | 16 | |
16 | 21 | 21 | 22 | 23 | 21 | 21 | 22 | 23 | 16 | |
18 | 21 | 21 | 22 | 23 | 21 | 21 | 22 | 23 | 16 | |
1A | 22 | 22 | 23 | 24 | 22 | 22 | 23 | 24 | 21 |
特徴的なのは、分岐先が "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ビットに分割して渡されます。
まとめ
本日のまとめです。
- 構造体が32ビット以内で表現できる時には、パッキングされて通常の変数と同じようにやり取りされる。パッキング・アンパッキングのコストは安くない。
- 32ビットを超えるサイズの構造体を返す関数は、呼び出し前にスタックにメモリ領域を確保し、そのアドレスをレジスタ r0 に与えて呼び出す。残りの引数は、 r1, r2, r3 レジスタ、スタックの順に格納される。
- 関数の引数に構造体を与えた場合、通常の引数と同じルールでレジスタおよびスタックに展開される。
参考サイト
- Chapter 3. The Cortex-M0 Instruction Set
- ARM が提供するサイトで、 Cortex-M0 の命令が参照できます。
参考文献
サイクルタイムを測定しよう (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サイクルでした。
分岐先 | ||||||||||||
00 | 02 | 04 | 06 | 08 | 0A | 0C | 0E | 10 | 12 | 14 | ||
分岐命令 | 02 | 8 | ||||||||||
04 | 8 | 8 | ||||||||||
06 | 8 | 8 | 8 | |||||||||
08 | 8 | 8 | 8 | 8 | ||||||||
0A | 8 | 8 | 8 | 9 | 8 | |||||||
0C | 8 | 8 | 8 | 9 | 8 | 8 | ||||||
0E | 8 | 8 | 8 | 9 | 8 | 8 | 8 | |||||
10 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | 8 | ||||
12 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | 9 | 8 | |||
14 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | 9 | 8 | 8 | ||
16 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | |
18 | 8 | 8 | 9 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | ||
1A | 8 | 9 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | |||
1C | 9 | 8 | 8 | 8 | 9 | 8 | 8 | 8 | ||||
1E | 8 | 8 | 8 | 9 | 8 | 8 | 8 | |||||
20 | 8 | 8 | 9 | 8 | 8 | 8 | ||||||
22 | 8 | 9 | 8 | 8 | 8 | |||||||
24 | 9 | 8 | 8 | 8 | ||||||||
26 | 8 | 8 | 8 | |||||||||
28 | 8 | 8 | ||||||||||
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" の数を変えながらサイクル数を測定し、分岐命令の位置と分岐先アドレスによってで測定されたサイクル数を並べると、以下の表のようになります。
分岐命令 | ||||||||||||
02 | 04 | 06 | 08 | 0A | 0C | 0E | 10 | 12 | 14 | 16 | ||
分岐先 | 04 | 7/7/7 | ||||||||||
06 | 7/7/7 | 7/7/7 | ||||||||||
08 | 7/7/7 | 7/7/7 | 7/7/7 | |||||||||
0A | 7/7/7 | 7/7/7 | 7/7/7 | 7/7/7 | ||||||||
0C | 7/7/7 | 7/7/7 | 7/7/7 | 7/7/7 | 9/8/7 | |||||||
0E | 8/7/7 | 8/7/7 | 8/7/7 | 8/7/7 | 9/8/7 | 9/8/7 | ||||||
10 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 7/7/7 | 7/7/7 | 7/7/7 | |||||
12 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 7/7/7 | 7/7/7 | 7/7/7 | 7/7/7 | ||||
14 | A/8/7 | A/8/7 | A/8/7 | A/8/7 | 7/7/7 | 7/7/7 | 7/7/7 | 7/7/7 | 9/8/7 | |||
16 | B/9/7 | B/9/7 | B/9/7 | B/9/7 | 8/7/7 | 8/7/7 | 8/7/7 | 8/7/7 | 9/8/7 | 9/8/7 | ||
18 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 7/7/7 | 7/7/7 | 7/7/7 | |
1A | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 7/7/7 | 7/7/7 | 7/7/7 | ||
1C | A/8/7 | A/8/7 | A/8/7 | A/8/7 | A/8/7 | A/8/7 | 7/7/7 | 7/7/7 | 7/7/7 | |||
1E | B/9/7 | B/9/7 | B/9/7 | B/9/7 | B/9/7 | 8/7/7 | 8/7/7 | 8/7/7 | ||||
20 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | |||||
22 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | 9/8/7 | ||||||
24 | A/8/7 | A/8/7 | A/8/7 | A/8/7 | A/8/7 | |||||||
26 | B/9/7 | B/9/7 | B/9/7 | B/9/7 | ||||||||
28 | 9/8/7 | 9/8/7 | 9/8/7 | |||||||||
2A | 9/8/7 | 9/8/7 | ||||||||||
2C | A/8/7 |
この表からは、以下の事がわかります。
- 分岐先が分岐命令に近く、アドレスの下3ビットが 000 から 100 である場合、サイクル数は最小を保ちます。 これは、追加でプリフェッチを必要とするためです。
- アドレスの下3ビットが 110 であるアドレスに分岐する場合、どの条件であってもコストが最大になります。 これは、分岐先の8バイトブロックに続いて次の8バイトブロックもプリフェッチする必要があるためと考えられます。
- その次にコストが高いのは、アドレスの下3ビットが 100 である場合です。 プリフェッチのタイミングを1クロックだけ遅らせる事ができるので、一回分のプリフェッチが見えなくなるためと考えられます。
- 分岐先が遠い場合、分岐命令のアドレスとは無関係に、分岐先のアドレスに依存したサイクル数を要します。 これにより、純粋にプリフェッチの時間が見えているのだとわかります。
以上の考察より、サイクル数を減らしたければ、分岐先のアドレスが8バイトブロックの前半になるように配置を考える必要があります。 関数内で分岐先アドレスを制御するのは困難ですが、せめて関数の入り口アドレスを8バイト境界の前半に配置するようにオプションを付けると処理時間が短くなりそうです。