INtime上でADA16-32/2(PCI)Fからアナログデータを出力するアプリ

# はじめに
[こちら](https://qiita.com/mine820/items/1b2ffeb9789a5388acb9)の記事で、アナログ入力を行うINtimeアプリの説明を書きましたので、今回はさらにその続きとして、I/Oボードからデータを出力するところを書きたいと思います。

今回も、対象となるI/OボードはPCI入出力ボード「[ADA16-32/2(PCI)F](https://www.contec.com/jp/products-services/daq-control/pc-helper/pci-card/ada16-322(pci)f/)」になります。
アナログ32ch入力／2ch出力、デジタル8ch入出力が可能です。

基本的には、I/Oボードの説明書やテクニカルリファレンスを参考にする必要があるのですが、様々な理由で、結構開発は苦労します。

# 開発
今回の開発は、テンプレートとしてポーリングスレッドを使用します。
（割り込み処理でもいいのですが、ここではそこまでのことはしないので）

追加する処理としては、以下の2つの機能を作成します。

- 初期化処理
    - ボードの初期化
    - 初期設定
- 逐次処理
    - アナログデータの出力

## 初期化処理
プログラムが汚くならないように、初期化処理は別関数にしておきます。
呼び出しはこんな感じです。

```Poll1.c
	// 本スレッドが生存していることを登録
	gInit.htPoll1	= GetRtThreadHandles(THIS_THREAD);

	// スレッドカタログ、ただし失敗は無視
	Catalog(NULL_RTHANDLE, gInit.htPoll1, "TPoll1");

	// PCIボードの初期化 ★★★ここを追加★★★
	InitPCIBoard();

	// 外から止めないと終わらない
	while (!gInit.bShutdown)
	{
```

以下、InitPCIBoard()内の説明をしていきます。

### ベースアドレスの取得
今回もPCIのI/Oボードを使用していますので、ボードとやり取りを行う場合、PCIデバイスに対してレジスタの値を書き換えることになります。
[参考]http://www.mnc.co.jp/english/INtime/faq07-2_kanren/PCIconfigurationregister.htm

それぞれのI/Oボードごとに、ベースアドレスからのマップが決まっていますので、そこに値を設定したり、そこから値を取得したりします。

そのために、まず、ベースアドレスを取得します。

```c
	// 設定されたパラメータを用いて、デバイスを検索します
	dev.wVendorId		= 0x1221;
	dev.wDeviceId		= 0xC103;
	dev.wDeviceIndex	= 0; // Instance;
	if (!PciFindDevice(&dev))
		Fail("PCI device %04x/%04x(%u) not found\n", dev.wVendorId, dev.wDeviceId, dev.wDeviceIndex);
 
 	if (dev.bUnusable)
		Fail("PCI device %04x/%04x(%u) not assigned to this node", dev.wVendorId, dev.wDeviceId, dev.wDeviceIndex);
 
	// 電源状態をD0状態(作業状態)に移行させ、メモリ及びI/Oへのアクセスを可能にします
	PciEnableDevice(&dev);

	// 必要に応じて、バス マスタ機能を設定します
	PciSetMaster(&dev);

	// ベースアドレスの取得
	port = (WORD)dev.dwBaseAddr[0] & 0xfffc;
```

ベンダーID／デバイスIDが各I/Oボードごとに異なりますので注意してください。
また、ベースアドレスにはI/O空間とメモリ空間の2種類がありますので、気を付けてください。

### 初期化
ベースアドレスがわかったら、いよいよボードの初期設定を行います。

### リセット
まず、ボードの設定をリセットします。

```c
	// リセット
	outword( port + 0x80, 0x00000000 );	// イベントコントローラ
	outword( port + 0x08, 0x00000000 );	// アナログ入力
	outword( port + 0x18, 0x00000000 );	// アナログ出力
	outword( port + 0x28, 0x00000000 );	// デジタル入力
	outword( port + 0x38, 0x00000000 );	// デジタル出力
	outword( port + 0x48, 0x00000000 );	// カウンタ
	outword( port + 0x58, 0x00000000 );	// メモリ
```

ポートに値を出力する関数は「outpd()」「outpw()」などがありますが、INtime APIではそれぞれ「outword()」「outhword()」を使用します。

### イベントコントローラ　パルス入出力
次に、イベントコントローラのパルス入出力の設定を行います。
これは、各機能の入出力信号を結び付ける操作になります。

```c
	// イベントコントローラ　パルス入出力設定
	outword( port + 0x80, 0x00000003 );
	outhword( port + 0x84, 0x0020 );	// アナログ出力 更新許可トリガ
	outhword( port + 0x86, 0x0180 );	// 汎用コマンド1
	outword( port + 0x80, 0x00000003 );
	outhword( port + 0x84, 0x0022 );	// アナログ出力 更新不許可トリガ
	outhword( port + 0x86, 0x0181 );	// 汎用コマンド2
	outword( port + 0x80, 0x00000003 );
	outhword( port + 0x84, 0x0024 );	// アナログ出力 サンプリングクロック
	outhword( port + 0x86, 0x0042 );	// 内部標準タイマ
```

アナログ出力の場合、以下のように処理を行います。
（簡単に説明しています）

1. データ出力（1回分）
2. 内部ゲートをオープン
3. イベントコントローラからの更新を許可
4. データ出力
5. イベントコントローラからの更新を不許可
6. 内部ゲートをクローズ

ここでは、ソフトウェアから更新許可／更新不許可を行うようにしています。
特に、事前出力（ビフォートリガ更新）終了時にビフォートリガ更新回数終了フラグが立ち、それにより更新不許可トリガが発火するのが通常なのですが、ここでは無限サンプリングを可能とするために汎用コマンド2と接続して、ビフォートリガ更新回数終了フラグが立っても更新不許可トリガが発火しないようにしています。

ところが、この設定だけだと事前出力分を送出した時点で、なぜか内部ゲートがクローズされてしまいます。
試行錯誤の結果、この後の設定（無限リピート）が必要になります。

出力データ数が決まっている場合は、ビフォートリガ更新回数終了と接続します。

```c
	outhword( port + 0x84, 0x0022 );	// アナログ出力 更新不許可トリガ
	outhword( port + 0x86, 0x0050);		// ビフォートリガ更新回数終了
```

### アナログ出力設定
次にアナログ出力の設定を行います。

```c
	// アナログ出力設定
	outword( port + 0x18, 0x00000003 );	// 内部クロック（ここで設定した間隔で送出される）
	outword( port + 0x1C, interval*1000*1000/25-1 );	// 外部指定（msec -> usec）
	outword( port + 0x18, 0x00000005 );	// 格納チャンネル数（-1して設定）
	outword( port + 0x1C, 0x00000000 | (ch-1) );	// 外部指定（複数チャンネル、同時更新）
	outword( port + 0x18, 0x00000008 );	// レンジ（ADA16-32/2(PCI)Fだけの設定）
	for (i=0; i < ch; i++){
		outhword( port + 0x1C, i );		// iチャンネル
		outhword( port + 0x1E, 0x0001 );	// 0～+10V
	}
	outword( port + 0x18, 0x0000000B );	// リピート回数
	outword( port + 0x1C, 0x80000000 );	// 0回、無限
```

ここで「interval」は出力したいタイミング間隔（msec）としています。
この間隔で、バッファにたまっているデータをボードから出力します。
そのため、事前にバッファにデータをためておく必要があります。

※ここで設定する間隔は、ポーリングスレッド内にある無限ループのRtSleep()で指定している間隔とは関係ありません
　（こっちはバッファにデータをため込む間隔）

また「ch」は使用するアナログ入力のチャンネル数になります。
ここで指定したチャンネル数だけ、バッファから出力されます。
例えば2ch使用する場合は、1,2,1,2,・・・と出力されていきます。
当然バッファに設定するときも、1,2,1,2,・・・と入れる必要があります。

レンジはアナログ出力の各チャンネルごとに設定できます。

最後に、リピート回数を無限にします。
なお、テクニカルリファレンスでは、初期値が「0（無限）」となっていますが間違いです。
ここが有限になっていると、なぜか内部ゲートがクローズされてしまいます。
なぜこれでうまく動くのかもよくわかっておらず、この件に関してはモヤモヤしたままとなっています。

なお、無限サンプリングではなく、指定回数データを出力する場合は、ビフォートリガ更新回数等を指定します。

```c
	outword( port + 0x18, 0x00000009 );	// ビフォートリガ更新回数
	outword( port + 0x1C, 0x000003E7 );	// 1000回
	outword( port + 0x18, 0x0000000B );	// リピート回数
	outword( port + 0x1C, 0x00000000 );	// 0回、有限
```

### 値の調整

ボード固有の性格があるため、その調整を行います。

```c
	for (i=0; i < ch_a; i++){
		// 調整値の取得
		outword( port + 0x18, 0x00000021 );	// 調整用データ確認

		outhword( port + 0x1C, 0x0202 | i<<4 );	// 0～+10V、調整箇所A、iチャンネル、工場出荷時
		do{	// EPROMBusyの間、待ち
			ao_sts = inword( port + 0x14 ) & 0x00000200;
		}while( ao_sts != 0x00000000 );
		eepromdata = inhword( port + 0x1E );	// 設定データ読み込み
		digpot_offset = eepromdata & 0x00FF;		// オフセット値
		digpot_gain =( eepromdata >> 8) & 0x00FF;	// ゲイン値

		// 調整値の設定
		outword( port + 0x18, 0x00000020 );	// キャリブレーション条件設定
		outhword( port + 0x1C, 0x0002 );		// オフセット、0～+10V、調整箇所A
		outhword( port + 0x1E, digpot_offset );
		do{	// Calibration Busyの間、待ち
			ao_sts = inword( port + 0x14 ) & 0x00000100;
		}while( ao_sts != 0x00000000 );
		outword( port + 0x18, 0x00000020 );	// キャリブレーション条件設定
		outhword( port + 0x1C, 0x0003 );		// ゲイン、0～+10V、調整箇所A
		outhword( port + 0x1E, digpot_gain );	// ゲイン値
		do{	// Calibration Busyの間、待ち
			ao_sts = inword( port + 0x14 ) & 0x00000100;
		}while( ao_sts != 0x00000000 );
	}
```

基本的には、チャンネルごとにオフセットとゲインを調整します。

### メモリ設定

最後にメモリの設定を行います。

```c
	// メモリ設定
	outword( port + 0x58, 0x00000001 ); // アナログ入力転送設定
	outword( port + 0x5C, 0x00000001 ); // I/O転送
	outword( port + 0x58, 0x00000002 ); // アナログ出力転送設定
	outword( port + 0x5C, 0x00000001 ); // I/O転送
```

I/O転送しかないくせに、デフォルト値は未設定なので、いちいち設定する必要があります。
（アナログ入力しない場合も、必ず設定する必要があります）

## 逐次処理

いよいよアナログデータを出力する処理になります。

ただ、出力する前に「内部ゲートのオープン」と「アナログ出力格納許可」を行う必要があります。

```c
	// アナログ出力更新開始
	outword( iobase + 0x18, 0x00000001 );	// アナログ出力 内部ゲートオープン
	outword( iobase + 0x80, 0x00000005 );	// アナログ出力格納許可（汎用コマンド1を使用）
```

入力と異なり、コマンド指定後、特にフラグとかは見ていません。

なお、これを行う前に、1回分の出力データをバッファに入れておく必要があります。

その後、無限ループ内でアナログデータを出力します。

```c
		// アナログ値の出力
		for(i=0; i < ch; i++ ){
			outword( iobase + 0x10, valData[i]);
		}
```

なお、テクニカルリファレンスでは、

```text
出力例
outpw( port + 0x10, 00005555);	// アナログ出力データを出力
outpw( port + 0x10, 0000AAAA);	// アナログ出力データを出力
```

とWORD（2byte）になっていますが、実際はDWORD（4byte）で渡します。

# まとめ
事前必ず設定しなくてはいけないもの、やりたいことを実現するために設定するもの、動作の途中で設定／発火されるフラグ／トリガ／イベントなど、いろいろ気にしなくてはいけないのが面倒です。

また、リファレンスの間違いや指定しても期待と動作が違うなどありますので、結構厳しいです。

なお、INtimeを使用せずWindowsからI/Oボードにアクセスする場合は、もっと便利な[ライブラリ](https://www.contec.com/jp/products-services/daq-control/pc-helper/daq-software/api-aio(wdm)/support/)があるようです。

# おまけ
苦労したおかげで、いろいろ知見が溜まりましたので、同様にお困りに方がいらっしゃいましたらお気軽にご相談ください。
（できればお仕事として．．．（笑））