SSブログ

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サイクルです。 このくらいだったら気にすることも無いですかね。


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

実験回路

今回は、個別のインストラクションについて、実行サイクルを測定します。

NOP の実行サイクル

最初は、何もしない命令 "NOP" を入れてみます。 測定関数 measure() は、以下のようになりました。

uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    s = *reg;
    asm(
        "nop\n"
    );
    e = *reg;
    
    return e - s;
}

"asm()" を使って、インストラクションを直接入れます。 この実験では、関数の最初の部分に別の NOP 列を入れることにより配置箇所を変化させましたが、いずれの場合も5サイクルになりました。 NOP 命令が無い場合は4サイクルでしたから、 NOP 命令の実行は1サイクルであったことがわかります。

イミディエイト MOV 命令

次は、イミディエイト・アドレッシングモードの MOV 命令を入れてみます。 Cortex-M0 では、8ビットで表現できる値をレジスタに入れるイミディエイト・アドレッシングモードの MOV 命令があります。

    s = *reg;
    asm(
        "mov r3,#170\n"
    );
    e = *reg;

この命令で CPU の r3 レジスタに170という値を入れます。 値に意味はありません。 また、使用したレジスタも他の部分に影響のないものという事で r3 を選んでいます。 実験の結果、 NOP の場合と同様にすべて5サイクルとなりました。 この命令も1サイクルで実行されます。

かけ算命令 MUL の場合

三つ目は、かけ算命令 MUL を実行してみます。

    s = *reg;
    asm(
        "mul r3,r0\n"
    );
    e = *reg;

ここで実行しているかけ算の内容にも意味はありません。 実験の結果は、これも5サイクルになりました。 かけ算も1サイクルで実行してしまいます。

メモリアクセスさえ無ければ

以上のように、メモリアクセスの無い命令であれば、ほとんど1サイクルで実行する事が出来ます。 しかし、実行順序に影響を与える場合には、そうはいきません。


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

実験回路

タイマを使用して、サイクルタイムを測定するしかけができたので、具体的にサイクルタイムを測定していきます。 今回は、命令フェッチのサイクル数にこだわってみます。

測定ルーチンをちょっと変更

前回の記事では、 measure() 関数の中でカウンタレジスタを直接読み出していました。 そのため、以下のように .L5 に格納されたカウンタレジスタのアドレスを CPU のレジスタ r3 に格納する命令が入ってしまいます。

  38:.\main.c      ****     uint32  s;
  39:.\main.c      ****     uint32  e;
  40:.\main.c      ****     
  41:.\main.c      ****     s = Timer_COUNTER_REG;
  88 0000 024B     		ldr	r3, .L5
  89 0002 1868     		ldr	r0, [r3]
:
:
  43:.\main.c      ****     e = Timer_COUNTER_REG;
  92 0004 1B68     		ldr	r3, [r3]
  44:.\main.c      ****     
  45:.\main.c      ****     return e - s;
  95 0006 181A     		sub	r0, r3, r0
  46:.\main.c      **** }
  98              		@ sp needed
  99 0008 7047     		bx	lr
:
:
 102              	.L5:
 103 000c 08012040 		.word	1075839240

インストラクションもレジスタアドレスも Flash ROM に格納されますので、レジスタアドレスの取り出しが命令フェッチに影響を与える可能性があります。 そこで、レジスタアドレスを関数の引数として渡す方式に改めました。

// Measure the execution cycle time
uint32 measure(reg32 *reg) __attribute__((aligned(256)));
uint32 measure(reg32 *reg) {
    uint32  s;
    uint32  e;
    
    s = *reg;
:
:
    e = *reg;
    
    return e - s;
}

:
:

    // Measure the cycle time
    for (i = 0; i < N_DIFF; i++) {
        diffs[i] = measure(Timer_COUNTER_PTR) & Timer_16BIT_MASK;
    }

こうすると、レジスタアドレスは CPU のレジスタに格納されたまま使用されます。

  39:.\main.c      ****     uint32  s;
  40:.\main.c      ****     uint32  e;
  41:.\main.c      ****     
  42:.\main.c      ****     s = *reg;
  89 0000 0268     		ldr	r2, [r0]
:
:
  43:.\main.c      ****     e = *reg;
  92 0002 0368     		ldr	r3, [r0]

また、 "aligned(256)" という属性を追加して、関数の配置アドレスがブレないようにしています。

レジスタ読み出しが連続した場合

まず、タイマレジスタのアクセスが連続した場合について調べます。

  42:.\main.c      ****     s = *reg;
  89 0000 0268     		ldr	r2, [r0]
  43:.\main.c      ****     e = *reg;
  92 0002 0368     		ldr	r3, [r0]

この場合、タイマカウンタの値の差分をとると、 "ldr" 命令一回分のサイクル数を求めることが出来ます。 UART への出力は、以下のようになりました。

Freq:48
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004

Freq:24
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004

Freq:12
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004
0004 0004 0004 0004 0004 0004 0004 0004

CPU の周波数が変わっても4サイクルでした。 もしかしたら、プログラムの配置アドレスに依存するかもしれないと考えて、これらの前に "nop" 命令を入れて、配置を変更してみました。

    asm(
        "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:\n"
    );
    s = *reg;
    e = *reg;

結果は、いずれの場合も4サイクルのままでした。 PSoC 4 の Flash ROM は、バス周波数が高くなる場合には最大2サイクルのウェイトサイクルが入ります。 しかし、いずれのバス周波数の場合でも同じサイクル数を示しています。 これは、 Flash ROM から読み出した命令を先読みするプリフェッチ機構が存在するためです。 プリフェッチのおかげで、命令を連続して実行する場合には、 Flash ROM のアクセス時間が見えなくなります。


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

実験回路

前回の記事では、 CyDelay() 関数のサイクルタイムを測定しました。 今回は、 CyDelay() が無かった場合のサイクルタイムを測定します。

ソースコードとバイナリー

ソースコード

前回のプロジェクトを改造して測定を行います。 ソースコードからは、単純に CyDelay() 関数を削除します。


バイナリー

すると、 "ldr r0,[r3]" インストラクションと "ldr r3,[r0]" インストラクションが連続して並びます。 つまり、 "ldr" インストラクション一回分のサイクルタイムが測定できることになります。 パイプラインが理想的に動作していれば、サイクルタイムは1サイクルになるはずです。

測定値が一定しない

サイクルタイム

さっそくサイクルタイムを測定してみましたが、測定結果が一定しない事がわかりました。 様子を見るために、少し長めに出力させると、4 と 5 が混在しているのがわかります。

この現象を引き起こした原因はいくつか考えられます。


  1. バスが使用されていたため、データの取得が遅れた。
  2. Flash コントローラのプリフェッチバッファがリロードされた。
  3. CPU がカウンタのデータを取りに行く時に他のバスマスタの嫌がらせを受けた。
  4. Flash ROM からインストラクションを持ってくる時に「スカ」をつかまされた。
  5. 働き過ぎた CPU が、休みたがっている。
  6. 宇宙線が降り注いできた。

そこで、何か法則がないかと、それぞれを "0" と "1" に置き換えて右に書きだしてみました。

Freq:48
0004 0004 0004 0005 0004 0004 0005 0004  00010010
0005 0004 0004 0004 0004 0005 0005 0004  10000110
0004 0004 0004 0004 0005 0005 0005 0004  00001110
0004 0004 0004 0004 0005 0005 0005 0004  00001110
0005 0004 0004 0005 0005 0005 0005 0004  10011110
0004 0004 0004 0004 0004 0005 0004 0004  00000100
0005 0004 0004 0004 0004 0004 0005 0004  10000010
0004 0004 0004 0004 0005 0005 0005 0004  00001110
0004 0005 0004 0004 0005 0005 0005 0004  01001110
0005 0004 0004 0005 0004 0005 0005 0004  10010110
0004 0004 0005 0005 0004 0005 0005 0004  00110110
0004 0004 0004 0004 0004 0005 0004 0004  00000100
0004 0005 0005 0004 0004 0004 0005 0004  01100010
0005 0005 0005 0005 0004 0005 0005 0004  11110110
0005 0005 0005 0005 0004 0005 0005 0004  11110110
0004 0004 0005 0005 0004 0005 0005 0004  00110110
0005 0004 0004 0004 0004 0005 0004 0004  10000100
0005 0004 0004 0004 0004 0005 0004 0004  10000100

法則が見えてきた

どうやら、特定のパターンが現れてきているように見えます。 さらに、16進数に変換してみました。

Freq:48
0004 0004 0004 0005 0004 0004 0005 0004  00010010  0x48
0005 0004 0004 0004 0004 0005 0005 0004  10000110  0x61
0004 0004 0004 0004 0005 0005 0005 0004  00001110  0x70
0004 0004 0004 0004 0005 0005 0005 0004  00001110  0x70
0005 0004 0004 0005 0005 0005 0005 0004  10011110  0x79
0004 0004 0004 0004 0004 0005 0004 0004  00000100  0x20
0005 0004 0004 0004 0004 0004 0005 0004  10000010  0x41
0004 0004 0004 0004 0005 0005 0005 0004  00001110  0x70
0004 0005 0004 0004 0005 0005 0005 0004  01001110  0x72
0005 0004 0004 0005 0004 0005 0005 0004  10010110  0x69
0004 0004 0005 0005 0004 0005 0005 0004  00110110  0x6C
0004 0004 0004 0004 0004 0005 0004 0004  00000100  0x20
0004 0005 0005 0004 0004 0004 0005 0004  01100010  0x46
0005 0005 0005 0005 0004 0005 0005 0004  11110110  0x6F
0005 0005 0005 0005 0004 0005 0005 0004  11110110  0x6F
0004 0004 0005 0005 0004 0005 0005 0004  00110110  0x6C
0005 0004 0004 0004 0004 0005 0004 0004  10000100  0x21
0005 0004 0004 0004 0004 0005 0004 0004  10000100  0x21

ここまで来たら、あと一息です。 ASCII コードで読んでみましょう。

Freq:48
0004 0004 0004 0005 0004 0004 0005 0004  00010010  0x48  H
0005 0004 0004 0004 0004 0005 0005 0004  10000110  0x61  a
0004 0004 0004 0004 0005 0005 0005 0004  00001110  0x70  p
0004 0004 0004 0004 0005 0005 0005 0004  00001110  0x70  p
0005 0004 0004 0005 0005 0005 0005 0004  10011110  0x79  y
0004 0004 0004 0004 0004 0005 0004 0004  00000100  0x20   
0005 0004 0004 0004 0004 0004 0005 0004  10000010  0x41  A
0004 0004 0004 0004 0005 0005 0005 0004  00001110  0x70  p
0004 0005 0004 0004 0005 0005 0005 0004  01001110  0x72  r
0005 0004 0004 0005 0004 0005 0005 0004  10010110  0x69  i
0004 0004 0005 0005 0004 0005 0005 0004  00110110  0x6C  l
0004 0004 0004 0004 0004 0005 0004 0004  00000100  0x20   
0004 0005 0005 0004 0004 0004 0005 0004  01100010  0x46  F
0005 0005 0005 0005 0004 0005 0005 0004  11110110  0x6F  o
0005 0005 0005 0005 0004 0005 0005 0004  11110110  0x6F  o
0004 0004 0005 0005 0004 0005 0005 0004  00110110  0x6C  l
0005 0004 0004 0004 0004 0005 0004 0004  10000100  0x21  !
0005 0004 0004 0004 0004 0005 0004 0004  10000100  0x21  !
失礼いたしました。 明日は、まじめにやります。

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

サイクルタイム測定キット

PSoC 4 にも CPU として Cortex-M0 が搭載されています。 普段は気にすることもないのですが、プログラムの実行時間をタイマを使って測定してみました。

実験回路

回路は、タイマと UART で構成されています。 タイマを16ビットのフリーランニングカウンタとして使用して、プログラムの実行前と実行後に読み出したカウンタの値の差を計算します。 この測定を何回か繰り返したら、まとめて UART から測定値を表示します。

実験では、 Internal Main Oscillator (IMO) の周波数を変更しながら、実行時間を測定します。 そのため、 UART の駆動クロックは、外部から与える構成にしました。

メイン関数

プログラムは、いくつかの部分で構成されています。 まずは、メイン関数から見ていきます。

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

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    Timer_Start();
    UART_Start();
        
    loop(48);
    loop(24);
    loop(12);

    for(;;) {
        /* Place your application code here. */
    }
}

コンポーネント "Timer" と "UART" の初期化を行ったら、 loop() 関数で測定を三回行います。 loop() 関数の引数には、 IMO の周波数を MHz 単位で与えます。 この実験では、 48MHz, 24MHz, 12MHz の三種類の周波数について測定を行います。 測定が終わったら、無限ループに突入します。

測定ループ

loop() 関数は、三つの部分で構成されています。

// Measure the cycle time repeatedly
// The CPU frequency is set as specified in argument
void loop(uint32 freq) {
    uint32  i;
    char    sbuf[32];
    
    // Set CPU frequency
    setFreq(freq);
    
    // Measure the cycle time
    for (i = 0; i < N_DIFF; i++) {
        diffs[i] = measure() & Timer_16BIT_MASK;
    }

    // Show the measurement results
    sprintf(sbuf, "\r\nFreq:%ld\r\n", freq);
    UART_UartPutString(sbuf);
    for (i = 0; i < N_DIFF; i++) {
        sprintf(sbuf, "%04X", diffs[i]);
        UART_UartPutString(sbuf);
        if ((i & 7) < 7) {
            UART_UartPutChar(' ');
        } else {
            UART_UartPutCRLF(' ');
        }
    }

    // Wait for UART communication completed
    while (!(UART_GetTxInterruptSource() & UART_INTR_TX_UART_DONE));
}
IMO の設定
最初は、 IMO の周波数を設定するために setFreq() 関数を呼び出します。 中身は、のちほど。
サイクルタイムの測定
サイクルタイムの測定を measure() 関数で行い、 測定結果は diff[] 配列に格納していきます。 測定は、 N_DIFF で示される回数だけ繰り返されます。
測定結果の表示
測定結果は、 UART から出力されます。 出力形式は、16進数です。

IMO の設定関数

IMO の設定を変える時には、発振周波数の変更以外に、いくつかパラメータを調整する必要があります。

// Set the CPU clock frequency
void setFreq(uint32 freq) {
    // Select a longest wait cycle
    CySysFlashSetWaitCycles(48);
    
    // Change the IMO and related parameters
    CySysClkWriteImoFreq(freq);
    CySysFlashSetWaitCycles(freq);
    CyDelayFreq(freq * 1000000u);

    // Reconfigure the UART clock frequency
    Clock_UART_SetDivider(cydelayFreqHz/BAUDRATE/UART_UART_OVS_FACTOR-1);
}

まず、 CySysFlashSetWaitCycles(48) で、 Flash ROM のウエイトサイクル数を最大設定にします。 PSoC 4 に内蔵されている Flash ROM は、システムクロックが早い場合には、ウエイトサイクルを入れる必要があります。 この関数で、最大周波数 48MHz の時に使用されるウエイトサイクル、具体的には2サイクルのウエイトが挿入されます。

次に CySysClkWriteImo() 関数で IMO の周波数を変更します。 さらに、この周波数に適切なウエイトサイクルを Flash ROM に設定します。 引き続き、 CyDelayFreq() 関数を使って、サイクルタイムの測定で使用されている CyDelay() 関数などで使用される周波数パラメータを変更します。

最後に UART に使用されるクロックの周波数を変更します。 この時に、別途宣言されている BAUDRATE を使用して、 UART のボーレートを調整しています。 この実験では、 IMO を 12MHz まで落としているので、 UART のボーレートは 9600bps と低めの値に設定されています。

サイクルタイムの測定

プログラムのサイクルタイムを測定するには、プログラムの実行前と実行後にカウンタの値を読み出します。

// Measure the execution cycle time
uint32 measure(void) {
    uint32  s;
    uint32  e;
    
    s = Timer_COUNTER_REG;
    CyDelay(1);
    e = Timer_COUNTER_REG;
    
    return e - s;
}

実行前のカウンタ値を s に格納し、実行後のカウンタ値を e に格納します。 そして、これらの差分を関数の値として返します。

この実験では、 CyDelay(1) の実行時間(約1ミリ秒)を測定しています。

実行結果

この実験し使用した機材は、 CY8C4247AZI-M485 を搭載した CY8CKIT-044 です。 実行結果は、以下のようになりました。

Freq:48
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8
BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8 BBB8

Freq:24
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0
5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0 5DF0

Freq:12
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A
2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A 2F0A

48MHz のとき 0xBBB8 (48056) サイクル、 24MHz のとき 0x5DF0 (24048) サイクル、 12MHz のとき 0x2F0A (12042) サイクルを要しています。 CyDelay() 関数は、 CPU の実行時間を利用して遅延時間を作り出していますので、このくらいの精度が有れば十分でしょう。

プロジェクトアーカイブ

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


新・ウォッチドッグタイマを使ってみる ~PSoC 40xx 編~ [PSoC]このエントリーを含むはてなブックマーク#

低速クロック設定ダイアログ

前回の記事「新・ウォッチドッグタイマを使ってみる ~PSoC 42xx 編~」では、 PSoC 42xx を題材に Interrupt Service Routine (ISR) を自動生成させて楽をする方法をさぐりました。 今回は、 PSoC 40xx で試してみます。

WDT 設定は簡易版

WDT 設定部

PSoC 40xx の低速クロック設定ダイアログにも Watch-Dog Timer (WDT) 設定が追加されました。 ただし、 PSoC 40xx は、 PSoC 42xx とは異なり、 WDT カウンタを1系統しか持っていません。 そのため、配置されている設定ブロックも一つだけです。

また、16ビットのフリーランニングカウンタが使用されている所も異なっています。 このカウンタの値が MATCH レジスタの値と等しくなったら割り込みが発行されます。 デフォルトでのカウンタの周期は 65536/40kHz=1.6sec となります。

MATCH レジスタとの比較では、上位ビットの比較結果を無視する事によって、割込みの周期を短く設定することが出来ます。 例えば、上位2ビットを無視すると周期は 16384/40kHz=0.4sec になります。 ドロップダウンリスト "Period" で周期を設定して、何ビットを無視するのか決定する事ができます。 この実験では、約 0.5 秒周期で割り込みを発生させたかったので、 409.6ms を使用します。

WDT 設定部では、動作モードを "Free Running Timer" と "Watchdog (w/ Interrupt)" から選択する事ができます。 これら二つのモードには、ウォッチドッグリセットが有効になるか無効になるかの違いが有ります。 "Free Running Timer" ではウォッチドッグリセットが無効に、 "Watchdog (w/ Interrupt)" ではウォッチドッグリセットが有効になります。 つまり、 "Free Running Timer" を使うと餌をやる手間がかかりません。 どちらにしても、フリーランニングカウンタの周期で割り込みフラグがセットされるところは同じです。 また、チェックをはずして WDT 設定そのものを無効にするとウォッチドッグリセットが無効になります。

ウォッチドッグリセット機能は、フリーランニングカウンタが MATCH レジスタの値と一致するイベントが3回発生するまでにウォッチドッグに餌をやらないとリセットが発生する仕組みです。 餌をやるためには、割り込みフラグをクリアします。 割込み機能を使う場合、通常は割込みルーチン内でフラグをクリアします。 そのため、メインループが暴走しても割り込みさえ正しく処理できればウォッチドッグリセットは発生しません。 どうやら、割込み機能も使用する場合、ウォッチドッグとしての機能は期待できないようです。

ウォッチドッグリセットの機能は、 Technical Reference Manual (TRM) によるとリセット直後は有効になっています。 ところが、 PSoC Creator が生成しているコードでは、デフォルト状態で無効になっているように見えます。 これは、 "Cm0Start.c" ファイルの中でリセット直後にウォッチドッグリセットを無効にしているのが原因でした。 ウォッチドッグリセットの機能から考えると、デフォルトでは有効にすべきですが、そうはなっておらず、デフォルトの状態を設定することも出来ないように見えます。 ウォッチドッグリセットを使う時には、注意が必要です。

ISR はあるけれど

割り込み関連コンポーネント

PSoC 40xx でも、 "CYLFClk.c" に Interrupt Service Routine (ISR) が作成されています。 ところが、 ISR 設定部が存在しないのに加えて、割込みベクタとして設定もされていないので、デフォルトのままでは使用できません。 そのため、従来通りに Global Signal コンポーネントの出力を Interrupt コンポーネントに接続して割込み機能を実現します。

ソースコードが簡単になったかな?

LED 出力部

ウォッチドッグタイマを使った「Lチカ」の完成です。 ソースコードは、以下のようになりました。

#include <project.h>

void Wdt_Callback(void) {
    Pin_LED_Write(~Pin_LED_Read());
}

int main()
{
    // Initialize WDT interrupt
    Wdt_int_StartEx(CySysWdtIsr);
    CySysWdtSetInterruptCallback(Wdt_Callback);
    CySysWdtUnmaskInterrupt();
        
    CyGlobalIntEnable; /* Enable global interrupts. */

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

    for(;;)
    {
        /* Place your application code here. */
    }
}

初期設定部分で使用している関数 CySysWdtUnmaskInterrupt() で、ウォッチドッグタイマによる割り込みを受け付けます。 コード量は、多少減ったんじゃないかな?

今回の記事で使用した手法は、ウォッチドッグタイマのオーバーフローを利用しています。 これは、割込みが発生するたびに MATCH レジスタを書き換えていた以前の記事とは異なり、周期の解像度で劣ります。 もっと解像度を上げたい場合には、自前で ISR を記述する必要があるでしょう。

プロジェクトアーカイブ

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

参考文書

PSoC 4 Low-Frequency Clock (cy_lfclk), Version 1.0
現在の PSoC Creator では、 "Watchdog Timer" などの LFCLK を利用した機能は、 cy_lfclk という独立したコンポーネントにまとめられています。 "Watchdog Timer" の使い方などの詳しい情報は、この文書に書いてあります。
PSoC 4000 Family: PSoC[レジスタードトレードマーク] 4 Architecture Technical Reference Manual (TRM)
PSoC 40xx の内部構成について書かれた文書です。 残念ながら、ウォッチドッグについての記述は充実しているとは言い難い状態です。

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