Nios® II ソフトウェア上での時間計測の精度について調査してみた

#目次
「1.(事前準備)概要」～「4.(事前準備)ハードウェアデザインの作成」については、そもそもの話であったり環境構築についてになるので、わかっている人は「5.(ここから本題)**タイマーの動作確認**」から読み進めて下さい。

[1.(事前準備)概要](#1-概要)
[2.(事前準備)そもそも Nios® II って？](#2-そもそも-nios-ii-って)
[3.(事前準備)検証環境](#3-検証環境)
[4.(事前準備)ハードウェアデザインの作成](#4-ハードウェアデザインの作成)
　[4.1.(事前準備)Platform Designer の編集](#41-platform-designer-の編集)
　[4.2.(事前準備)Quartus の編集](#42-quartus-の編集)
[5.(ここから本題)**タイマーの動作確認**](#5-タイマーの動作確認)
　[5.1.**システムクロックでの時間測定**](#51-システムクロックでの時間測定)
　[5.2.**タイムスタンプでの時間測定**](#52-タイムスタンプでの時間測定)
　[5.3.**システムクロックとタイムスタンプの違い**](#53-システムクロックとタイムスタンプの違い)
[6.**まとめ**](#6-まとめ)

#1. 概要
Nios® II を使用する際、時間計測する方法はいくつかあると思います。
ただ、その使い方によって精度が大きく変わることが分かったので、その使い方についてまとめてみました。

#2. そもそも Nios® II って？
簡単にだけ説明すると、インテルの全ての FPGA に実装可能な 32bit の RISC ソフトコア・プロセッサーです。
ソフトコアなので、1 つの FPGA に複数の Nios II を実装することができます。
[こちら](https://www.intel.co.jp/content/www/jp/ja/products/programmable/processor/nios-ii/design-tools.html)にメーカーの Nios II のページをリンクしましたので、詳細はこちらを参照してください。

#3. 検証環境
確認に使用した環境は、以下の通りです。
OS:Windows®10(64bit) バージョン1803
ツール：Quartus® Prime standard Version 18.1.0 Build 625 09/12/2018 SJ Standard Edition
キット：Cyclone® V GX Starter Kit

ベースとなるデザインは、Terasic にあるキットのページ([こちら](https://www.terasic.com.tw/cgi-bin/page/archive.pl?Language=English&CategoryNo=167&No=830&PartNo=4))からユーザー登録すると無償でダウンロードできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/16d428b9-29f9-1ca9-0129-13f107c9ac9b.png)


ファイルを解凍して、./C5G_v.1.2.2_SystemCD/Demonstrations/C5G_LPDDR2_Nios_Test を使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/0aa9d580-9a5c-1fa4-fd59-ed0d0373631c.png)

#4. ハードウェアデザインの作成
Quartus を起動して、デザインを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/cffd9001-47b5-aa6f-18e3-011ceeacd339.png)

##4.1. Platform Designer の編集
Platform Designer を起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/9beddc57-ae5c-925d-3149-7d5d12a4e564.png)

"timer" をダブルクリックし、Timeout period を 10us に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/274a719a-8058-5cc3-4d7a-4c1a2d565757.png)

また、Nios II から LED の制御をするので、"PIO (Parallel I/O) Intel FPGA IP" を追加します。
今回使用する Cyclone V GX Starter Kit で、緑の LED が 8 つついているので、デフォルトの 8 つのままにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/b3133bee-6114-5c8c-8e3f-c1e5ac58b56c.png)

"System メニュー→Create Global Reset Network" をクリックし、リセットを接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/b7bbadf9-f98f-8f7e-2ddf-5281eecdae84.png)

PIO の各信号を接続し PIO の external_connection の Export をダブルクリックして Platform Designer 外に出力すると、アドレスがオーバーラップしているとエラーが出るので、追加した PIO のアドレスを変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/61dab398-eb4b-c7de-eeac-3bcffea1fd99.png)

右下の "Generate HDL" をクリックし、Generate します。

##4.2. Quartus の編集
Platform Designer の Generate 完了後、Quartus に戻り top.v を以下のように編集します。
編集箇所は、Export した PIO を LEDG に接続しているだけなので、最下行の追記と、その前行の ',' の追加です。

```verilog:top.v
C5G_QSYS u0 (
	.clk_clk          (CLOCK_125_p),
	.reset_reset_n    (reset_n),
	.memory_mem_ca    (DDR2LP_CA),
	.memory_mem_ck    (DDR2LP_CK_p),
	.memory_mem_ck_n  (DDR2LP_CK_n),
	.memory_mem_cke   (DDR2LP_CKE),
	.memory_mem_cs_n  (DDR2LP_CS_n),
	.memory_mem_dm    (DDR2LP_DM),
	.memory_mem_dq    (DDR2LP_DQ),
	.memory_mem_dqs   (DDR2LP_DQS_p),
	.memory_mem_dqs_n (DDR2LP_DQS_n),
	.oct_rzqin        (DDR2LP_OCT_RZQ),
	.mem_if_lpddr2_emif_pll_ref_clk_clk          (CLOCK_50_B5B),
	.mem_if_lpddr2_emif_status_local_init_done   (lpddr2_local_init_done),
	.mem_if_lpddr2_emif_status_local_cal_success (lpddr2_local_cal_success),
	.mem_if_lpddr2_emif_status_local_cal_fail    (lpddr2_local_cal_fail),
	.key_external_connection_export              (KEY),
	.lpddr2_status_external_connection_export    ({lpddr2_local_cal_success, lpddr2_local_cal_fail, lpddr2_local_init_done}),
	.ledg_external_connection_export             (LEDG)
);
```

編集後、コンパイルすることで、ハードウェアデザインの作成は完了です。

#5. タイマーの動作確認
システムクロックを使用する場合と、タイムスタンプを使用する場合の2パターンを確認します。

##5.1. システムクロックでの時間測定
###5.1.1. プロジェクトの作成
ソフトは新しく記述するので、./C5G_LPDDR2_Nios_Test/software フォルダにあるファイルは全て削除します。
その後 Eclipse を起動し、ワークスペースを先ほどファイルを削除した software フォルダを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/48d1fbc7-d687-0748-35e3-73dcf2cb93a3.png)

App/BSP プロジェクトを先ほど Platform Designer の Generate で生成された C5G_QSYS.sopcinfo を指定して作成します。
その際、Project Template は "Hello World" を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/50b6aac7-809c-9acf-594d-c413ef38d81b.png)

###5.1.2. BSP Editor の設定
BSP プロジェクトを右クリックし、BSP Editor を起動します。
そこで、sys_clk_timer に timer を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/fc4308b2-372b-19b8-269d-93e01a0036c7.png)
また、"Linker Script" タブへ移動し、OnChip RAM で動作させるため全てのセクションを "onchip_memory2" へ変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/35aef491-815b-bf5f-9c18-252dc6cb02f7.png)

その後、Generate をクリックして終了します。

###5.1.3. ソースコード
ソースコードは以下です。

```c:sysclk.c
#include <stdio.h>
#include "system.h"
#include "altera_avalon_pio_regs.h"
#include "sys/alt_alarm.h"				// for alt_nticks()

#define N 10000000

void function(void){
	int i;
	for(i=0; i<N; i++){
		;
	}
}

int main()
{
  int tick_start_time, tick_finish_time, tick_total_time;

  printf("Hello from Nios II!\n");

  while(1){
	  IOWR_ALTERA_AVALON_PIO_DATA(LEDG_BASE, 0x00);

	  tick_start_time = alt_nticks();
	  function();
	  tick_finish_time = alt_nticks();

	  tick_total_time = ((tick_finish_time - tick_start_time) * 1000) / alt_ticks_per_second();
	  printf("TIME RESULT by ticks = %d ms\n", tick_total_time);

	  IOWR_ALTERA_AVALON_PIO_DATA(LEDG_BASE, 0xff);

	  tick_start_time = alt_nticks();
	  function();
	  tick_finish_time = alt_nticks();

	  tick_total_time = ((tick_finish_time - tick_start_time) * 1000) / alt_ticks_per_second();
	  printf("TIME RESULT by ticks = %d ms\n", tick_total_time);

  }

  return 0;
}
```
実行すると以下ようになり、だいたい 1.57sec という結果でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/536c9a5f-4e10-7cbb-981e-d0c4f4bf5959.png)

##5.2. タイムスタンプでの時間測定
プロジェクトの作成は、システムクロックと同じです。
###5.2.1. BSP Editor の設定
システムクロックの際と同様に、BSP Editor を起動します。
そこで、今度は timestamp_timer に timer を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/1b19eeb8-8118-5abc-5c19-f692711b4769.png)
"Linker Script" タブについてもシステムクロックと同様に設定し、Generate をクリックして終了します。

###5.2.2. ソースコード
ソースコードは以下です。

```c:timestamp.c
#include <stdio.h>
#include <unistd.h>
#include "system.h"
#include "altera_avalon_pio_regs.h"
#include "sys/alt_timestamp.h"			// for alt_timestamp()
#include "alt_types.h"

#define N 10000000

void function(void){
	int i;
	for(i=0; i<N; i++){
		;
	}
}

int main()
{
  alt_u32 stamp_start_time, stamp_finish_time, stamp_total_time;

  printf("Hello from Nios II!\n");

  if(alt_timestamp_start() < 0){
	  printf("No timestamp device !!!\n");
	  return 0;
  }

  while(1){
	  IOWR_ALTERA_AVALON_PIO_DATA(LEDG_BASE, 0x00);

	  stamp_start_time = alt_timestamp();
	  function();
	  stamp_finish_time = alt_timestamp();

	  stamp_total_time = stamp_finish_time - stamp_start_time;
	  printf("TIME RESULT by timestamp = %.*f ms\n", 2, ((float)stamp_total_time/(float)alt_timestamp_freq())*(float)1000 );

	  IOWR_ALTERA_AVALON_PIO_DATA(LEDG_BASE, 0xFF);

	  stamp_start_time = alt_timestamp();
	  function();
	  stamp_finish_time = alt_timestamp();

	  stamp_total_time = stamp_finish_time - stamp_start_time;
	  printf("TIME RESULT by timestamp = %.*f ms\n", 2, ((float)stamp_total_time/(float)alt_timestamp_freq())*(float)1000 );

  }

  return 0;
}
```
実行すると以下ようになり、だいたい 1.20sec という結果でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726218/52b612a6-b06d-1e18-6dcd-798440230aed.png)

##5.3. システムクロックとタイムスタンプの違い
タイマー IP は同じものを使用しているのに、同じ処理の測定時間になぜ違いが発生するのでしょうか？
・システムクロックを使用した場合：1.57sec
・タイムスタンプを使用した場合：1.20sec

###5.3.1. システムクロックの実装
BSP でのシステムクロックの実装を確認してみます。
alt_nticks() は、_alt_nticks の値を返していました。

```c:alt_alarm.h
/*
 * alt_nticks() returns the elapsed number of system clock ticks since reset.
 */
static ALT_INLINE alt_u32 ALT_ALWAYS_INLINE alt_nticks (void)
{
  return _alt_nticks;
}
```

\_alt_nticks は、関数 alt_tick() でカウントアップされていて、この関数は alt_avalon_timer_sc_irq() ハンドラで呼ばれていました。

```c:alt_tick.c
/*
 * alt_tick() is periodically called by the system clock driver in order to
 * process the registered list of alarms. Each alarm is registed with a
 * callback interval, and a callback function, "callback". 
 *
 * The return value of the callback function indicates how many ticks are to
 * elapse until the next callback. A return value of zero indicates that the
 * alarm should be deactivated. 
 * 
 * alt_tick() is expected to run at interrupt level.
 */
void alt_tick (void)
{
  alt_alarm* next;
  alt_alarm* alarm = (alt_alarm*) alt_alarm_list.next;

  alt_u32    next_callback;

  /* update the tick counter */

  _alt_nticks++;

  /* process the registered callbacks */

  while (alarm != (alt_alarm*) &alt_alarm_list)
  {
(省略)
```

```c:altera_avalon_timer_sc.c
/* 
 * alt_avalon_timer_sc_irq() is the interrupt handler used for the system 
 * clock. This is called periodically when a timer interrupt occurs. The 
 * function first clears the interrupt condition, and then calls the 
 * alt_tick() function to notify the system that a timer tick has occurred.
 *
 * alt_tick() increments the system tick count, and updates any registered 
 * alarms, see alt_tick.c for further details.
 */
#ifdef ALT_ENHANCED_INTERRUPT_API_PRESENT
static void alt_avalon_timer_sc_irq (void* base)
#else
static void alt_avalon_timer_sc_irq (void* base, alt_u32 id)
#endif
{
  alt_irq_context cpu_sr;
  
  /* clear the interrupt */
  IOWR_ALTERA_AVALON_TIMER_STATUS (base, 0);
  
  /* 
   * Dummy read to ensure IRQ is negated before the ISR returns.
   * The control register is read because reading the status
   * register has side-effects per the register map documentation.
   */
  IORD_ALTERA_AVALON_TIMER_CONTROL (base);

  /* ALT_LOG - see altera_hal/HAL/inc/sys/alt_log_printf.h */
  ALT_LOG_SYS_CLK_HEARTBEAT();

  /* 
   * Notify the system of a clock tick. disable interrupts 
   * during this time to safely support ISR preemption
   */
  cpu_sr = alt_irq_disable_all();
  alt_tick ();
  alt_irq_enable_all(cpu_sr);
}
```

alt_avalon_timer_sc_irq() ハンドラは関数 alt_avalon_timer_sc_init() で登録されていて、システムクロックに設定されているタイマー割込みごとに呼び出されていることが分かります。

```c:altera_avalon_timer_sc.c
/*
 * alt_avalon_timer_sc_init() is called to initialise the timer that will be 
 * used to provide the periodic system clock. This is called from the 
 * auto-generated alt_sys_init() function.
 */

void alt_avalon_timer_sc_init (void* base, alt_u32 irq_controller_id, 
                                alt_u32 irq, alt_u32 freq)
{
  /* set the system clock frequency */
  
  alt_sysclk_init (freq);
  
  /* set to free running mode */
  
  IOWR_ALTERA_AVALON_TIMER_CONTROL (base, 
            ALTERA_AVALON_TIMER_CONTROL_ITO_MSK  |
            ALTERA_AVALON_TIMER_CONTROL_CONT_MSK |
            ALTERA_AVALON_TIMER_CONTROL_START_MSK);

  /* register the interrupt handler, and enable the interrupt */
#ifdef ALT_ENHANCED_INTERRUPT_API_PRESENT
  alt_ic_isr_register(irq_controller_id, irq, alt_avalon_timer_sc_irq, 
                      base, NULL);
#else
  alt_irq_register (irq, base, alt_avalon_timer_sc_irq);
#endif  
}
```

つまり、Platform Designer で設定した "timer" の Timeout period の時間である 10us ごとに割込みが発生することで _alt_nticks をカウントアップしていて、その値を確認していることになります。
そうすると、Timeout period よりもとても長い時間を測定しようとすると割込みハンドラの呼び出しや元に戻る処理にかかる時間が積み重なってしまうため、誤差として大きなものになってしまいます。

###5.3.2. タイムスタンプの実装
次は、タイムスタンプの実装を確認してみます。

こちらは、"timer" のレジスタ値を読んでいることが分かります。

```c:altera_avalon_timer_ts.c
/*
 * alt_timestamp() returns the current timestamp count. In the event that
 * the timer has run full period, or there is no timestamp available, this
 * function return -1.
 *
 * The returned timestamp counts up from the last time the period register
 * was reset. 
 */

alt_timestamp_type alt_timestamp(void)
{

  void* base = altera_avalon_timer_ts_base;

  if (!altera_avalon_timer_ts_freq)
  {
#if (ALT_TIMESTAMP_COUNTER_SIZE == 64)
        return 0xFFFFFFFFFFFFFFFFULL;
#else
        return 0xFFFFFFFF;
#endif
  }
  else
  {
#if (ALT_TIMESTAMP_COUNTER_SIZE == 64)
        IOWR_ALTERA_AVALON_TIMER_SNAP_0 (base, 0);
        alt_timestamp_type snap_0 = IORD_ALTERA_AVALON_TIMER_SNAP_0(base) & ALTERA_AVALON_TIMER_SNAP_0_MSK;
        alt_timestamp_type snap_1 = IORD_ALTERA_AVALON_TIMER_SNAP_1(base) & ALTERA_AVALON_TIMER_SNAP_1_MSK;
        alt_timestamp_type snap_2 = IORD_ALTERA_AVALON_TIMER_SNAP_2(base) & ALTERA_AVALON_TIMER_SNAP_2_MSK;
        alt_timestamp_type snap_3 = IORD_ALTERA_AVALON_TIMER_SNAP_3(base) & ALTERA_AVALON_TIMER_SNAP_3_MSK;
        
        return (0xFFFFFFFFFFFFFFFFULL - ( (snap_3 << 48) | (snap_2 << 32) | (snap_1 << 16) | (snap_0) ));
#else
        IOWR_ALTERA_AVALON_TIMER_SNAPL (base, 0);
        alt_timestamp_type lower = IORD_ALTERA_AVALON_TIMER_SNAPL(base) & ALTERA_AVALON_TIMER_SNAPL_MSK;
        alt_timestamp_type upper = IORD_ALTERA_AVALON_TIMER_SNAPH(base) & ALTERA_AVALON_TIMER_SNAPH_MSK;
        
        return (0xFFFFFFFF - ((upper << 16) | lower)); 
#endif
  }
}
```
そのため、こちらはあまり誤差なく測定できているようです。

#6. まとめ
システムクロック・タイムスタンプと 2 つの時間測定を確認してみましたが、まとめると以下の特徴があることが分かりました。

・システムクロック
　　長所：長時間の計測が可能！！(タイマータイムアウト周期 x カウンタ上限値(32bit))
　　短所：誤差多め。。(タイマー割込みを伴うのでソフト処理の誤差が発生)
・タイムスタンプ
　　長所：誤差が少ない！！( IP のレジスタ値を直接読むため)
　　短所：計測可能な時間が短い。。(タイマークロック周期 x カウンター上限値(64bit))

実際に時間測定する場合は、精度だけではなくどの程度の時間まで測定したいかも合わせて検討してどちらが最適かを検討して選択して下さい。