コンテック社のデジタル入出力ボードを使う(トリガ編)

コンテック社のデジタル入出力ボード PIO-16/16L(LPCI)H または DIO-1616L-LPE を使って、外部機器からコンピュータに対してのトリガ入力を検知する方法を紹介します。

この2種類のボードの違いは、前者が PCI バスタイプ、後者が PCI-Express タイプの違いです。その他の電気的仕様は全く同じです。

まずは下記の記事をご覧になってから、本記事を読むことをお勧めします。プログラミングを開始するまでの注意点や、必須ファイルのプロジェクトへの追加方法などが紹介されています。

コンテック社のデジタル入出力ボードを使う(初級編)

パソコンで装置を動かしたりする場合はデジタル入出力ボードを使う必要があります。コンテック社のデジタル入出力ボードの使い方と外部回路の接続方法を解説します。

初級編で作成したアプリケーションと違って、本記事で作成するアプリケーションは下記のようにあっさりした外観です。トリガの入力回数だけを確認できればいいので、デジタル出力機能はありません。したがってボタンなどもありません。

Fig. 1 トリガをカウントするソフト

トリガ信号について

初級編では、ディスパッチタイマの一定間隔の中で InpByte() というメソッドを発行して、入力信号の Low/High を確認していました。これを Fig. 2 に示します。

タイマ間隔で InpByte() を発行しているので、もしタイマ間隔の途中で Low/High が変化していても、初回が High と判定されて、次回も High と判定されていたら、トリガが入っていないことと同じになります。

信号の Low/High だけを気にするアプリケーションならばそれでもいいですが、トリガが入った回数が重要なアプリケーションの場合は、それでは使えません。

Fig. 3 に、トリガの検出の概念を示します。コンテック社のデジタル入出力ボードの多くは、信号のRISE(アップエッジ、立ち上がり)、信号のFALL(ダウンエッジ、立下がり)、RISE と FALL 両方のトリガを検出することができます。

Fig. 2 信号の Low/High の状態を知る
Fig. 3 信号の RISE/FALL を知る

トリガ入力信号について

トリガ信号を入力するにはメカスイッチでも構わないのですが、メカスイッチには俗に「チャタリング」や「バウンス」と呼ばれる現象があるので、ユーザが意図するよりもかなり多いトリガが入ってしまいます。例えば、スイッチボタンを1回しか押していないのに10回ぐらいトリガが入ってしまうのはざらにあります。

したがって本記事の内容を再現するには、メカスイッチを用いるのは適さないのでフォトマイクロセンサを用います。

本記事ではオムロン社の EE-SX770 をチョイスしましたが、パナソニックサンクス社、キーエンス社、いろいろなメーカから似たような製品が出ていますので、それらを使っても構いません。

デジタル入出力ボードの端子台に Fig. 4 のようにセンサを接続してください。

Fig. 4 トリガ入力センサの接続図

トリガ検出のときのプログラム構造について

もう一度 Fig. 2 と Fig. 3 をごらんください。

任意のタイミングで入力されるトリガを検出するにはどのように InpByte() を発行してやればいいでしょうか?

ひとつの方法としては、きわめて短いディスパッチタイマ間隔で常に InpByte() を発行してやれば、ごく短時間の信号の Low/High の変化を、トリガの RISE と FALL に置き換えることができます。

しかし、この方法はいちいち Low/High を RISE と FALL に書き換えるロジックを書くのが面倒です。また、常に InpByte() を使って入力信号をウォッチングしているので、CPU資源の無駄使いですしプログラムも重い動作になります。

まったくいいことがありません。

理想は、トリガが入力されたときだけ誰かが知らせてくれるというのが一番楽です。その仕組みがコンテック社のドライバには存在します。

誰かが知らせてくれるという、その「誰か」とは具体的にはなんでしょうか。実は Windows がそれにあたります。おおまかに下記のような流れになります。

(0) デジタル入出力ボードへ電気的にトリガを入力する.
(1) コンテック社のドライバがトリガ入力を検知する.
(2) コンテック社のドライバが Windows に「トリガが入った」というメッセージを発行するように依頼する.
(3) Windows が「トリガが入った」というメッセージをブロードキャスト発行する.
(4) ユーザが「トリガが入った」というメッセージを拾う.(メッセージフック)
(5) 拾ったタイミングで任意の処理を実施する.

本記事では上記の (5) で入力ビットに応じたトリガ計数用のカウンタをインクリメントします。

プログラムの大まかな流れについて

昔から Windows のプログラムを作っている人達にはおなじみの作法であるメッセージフックを使います。WndProc という Windows SDK のメッセージループでよくみるアレです。

コンテック社のドライバではそのやり方なだけであって、他社のドライバではコールバック関数をつかって WndProc を巧みに隠蔽しているやり方もあります。

ナマの WndProc が丸見えなのがいいか、コールバック関数で隠蔽されているのがいいか、人それぞれですが、プログラムが意図通りに動かないときにメッセージの値をデバッガで止めて確認したりするのは WndProc が丸見えなほうがラクだと思います。

プログラムはだいたい下記のような流れです。

(10) アプリケーションを起動する.
(11) メインウインドウのハンドルを取得する.
(12) メッセージフックができるようにする.
(13) デジタル入出力ボードクラスを生成 new する.
(14) デジタル入出力ボードクラスを初期化 Init() する.
(15) トリガを検出する入力ビットを必要な数だけ登録 NotifyTrg() する.
(16) そのとき RISE なトリガだけ検知するか、FALL なトリガだけ検知するか、RISE or FALL 両方検知するか決める.
(17) WndProc の中でトリガ検知メッセージ CdioConst.DIOM_TRIGGER が通知されるのを見張る.
(18) 見張ってメッセージをつかまえたらそれに応じた処理をする.
(19) アプリを終了するまえにトリガ検知をとめる StopNotifyTrg() する.
(20) デジタル入出力ボードクラスを終了 Exit() する.
(21) アプリケーションを終了する.

ソースコード解説

XAML の解説をします。入力点数は16点あって、そのトリガ回数を表示したいので、テキストブロックを16回改行してその回数を表示します。

<Window x:Class="aaa.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:aaa"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="480">

    <DockPanel>

        <Menu DockPanel.Dock="Top">

            <MenuItem x:Name="menuApplication" Header="Application">
                <MenuItem x:Name="menuApplicationQuit" Header="Quit" Click="menuApplicationQuit_Click"/>
            </MenuItem>

        </Menu>

        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>

                <Border BorderBrush="DarkViolet" BorderThickness="1" >
                    <Button x:Name="tbnApplicationQuit" Content="Quit" Width ="48" Height="48" Click="menuApplicationQuit_Click"></Button>
                </Border>

            </ToolBar>
        </ToolBarTray>

        <StatusBar DockPanel.Dock="Bottom" Height="30">
            <Label x:Name="stbLabel000"/>
        </StatusBar>

        <Border BorderBrush="DarkViolet" BorderThickness="1" >
            <ScrollViewer x:Name="TheScv" Background="DarkViolet" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible">
                <Grid x:Name="TheGrid">
                    <Image x:Name="TheImage" Stretch="Uniform" />
                    <Canvas x:Name="TheCanvas" Background="Transparent">
                        <TextBlock x:Name="TheTbk" Foreground="White" Background="Transparent"/>
                    </Canvas>
                </Grid>
            </ScrollViewer>
        </Border>

    </DockPanel>

</Window>

つぎに MainWindow.xaml.cs のソースコードを解説します。

下記の名前空間の追加をお忘れなく.
using CdioCs; (コンテック社のデジタル入出力を使うため)
using System.Windows.Interop; (メッセージフック WndProc を使うため)

29行目、トリガの検知間隔を設定します。初級編のディスパッチタイマの間隔と同じイメージでとらえて問題ありませんが具体的に InpByte() を発行することはありません。

同じコンテック社のボードであっても、トリガの最短検知間隔が違います。製品のマニュアルを見て最短の検知間隔より短い値にならないように注意してください。

32~34行目、どの入力ビットのトリガを検出したいか定数宣言します。const でなくてもかまいません。

37行目、デバイスの名称はデバイスマネージャを確認して一言一句違わず文字列を指定してください。

116行目、入力信号ぶんのカウンタのリストを作成します。本記事で採用したボードは16点の入力ができるので16要素のリストを作成しています。もちろんすべてのリスト要素の初期値はゼロです。

127行目、デジタル入出力ボードクラスを初期化 Init() します。

136行目、WPF は Windows Forms アプリケーションとちがってフォームの OnLoad が標準で準備されていないので、ユーザが作成します。

なぜ OnLoad のイベントハンドラが必要なのかというと、メインウインドウのコンストラクタである MainWindow() の中では、適切なウインドウハンドルが取得できないからです。

ウインドウハンドルはメッセージフック WndProc を扱うために必須のアイテムです。

コンストラクタの中で作成されていないウインドウのハンドルを取得すること自体はエラーなく可能ですが、そのハンドル値は無効な数値で意味がありません。

146行目は、ウインドウが作成されてから実行されます。ここならば有効なウインドウハンドルが取得できます。

147行目、メインウインドウで生じるメッセージのフックができるように依頼します。フックする場所は WndProc の中です。

150、160、165行目、トリガを検知するようにドライバに依頼 NotifyTrg() します。
bit0 は RISE なトリガを検知します。
bit1 が FALL なトリガを検知します。
bit2 が RISE と FALL なトリガを検知します。

184~203行目、入力16要素のカウンタリストの内容をテキストブロックに表示するところです。

209~211行目、アプリケーションを終了するので、その前にトリガ検知を停止 StopNotifyTrg() します。

229行目、デジタル入出力クラスを終了 Exit() します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using CdioCs;
using System.Windows.Interop;

namespace aaa
{

	public partial class MainWindow : Window
	{

		// トリガのカウンタを記録するリスト.
		List<int> ListCounter = new List<int>(0);

		// トリガの検知間隔.
		public int MSEC_TRG_WATCH_LOOP = 100;

		// トリガを検出したいビット番号.
		public const short TRG_BIT_NUMBER_INP00 = 0;
		public const short TRG_BIT_NUMBER_INP01 = 1;
		public const short TRG_BIT_NUMBER_INP02 = 2;

		// デバイスマネージャでデジタル入出力ボードのデバイス名称を調べること.
		public const String DEV_NAME_DIO = "DIO000";

		public Cdio MyDio;
		public short MyId;

		/////////////////////////////////////////////////////////////
		/////////////////////////////////////////////////////////////
		/////////////////////////////////////////////////////////////

		public IntPtr Handle
		{
			get
			{
				Window wnd = Window.GetWindow( this );
				var wih = new System.Windows.Interop.WindowInteropHelper( wnd );
				return wih.Handle;
			}
		}

		private IntPtr WndProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled )
		{

			// コンテックのライブラリが発生させたメッセージかどうか調べる.
			if ( msg == (int)( CdioConst.DIOM_TRIGGER ) )
			{

				uint zero_mask_upper = 0x0000ffff;
				uint zero_mask_lower = 0xffff0000;
				int sft = 16;

				int v = lParam.ToInt32();

				// トリガを検出したビット.
				int trg_bit = (int)( ( v & zero_mask_upper ) );

				// トリガの種類、ライズかフォールか.
				int trg_kind = (int)( ( v & zero_mask_lower ) >> sft );

				// ライズなトリガ、ライズフォールなトリガのライズの場合.
				int msk_rise = (int)( CdioConst.DIO_TRG_RISE );
				if ( ( trg_kind & msk_rise ) == msk_rise )
				{
					if (( 0 <= trg_bit ) && ( trg_bit < ListCounter.Count )) 
					{
						// 該当のビットのカウンタをインクリメントする.
						( ListCounter[trg_bit] )++;
					}
				}

				// フォールなトリガ、ライズフォールなトリガのフォールの場合.
				int msk_fall = (int)( CdioConst.DIO_TRG_FALL );
				if ( ( trg_kind & msk_fall ) == msk_fall )
				{
					if (( 0 <= trg_bit ) && ( trg_bit < ListCounter.Count )) 
					{
						// 該当のビットのカウンタをインクリメントする.
						( ListCounter[trg_bit] )++;
					}
				}

				// 再描画を要請する.
				this.InvalidateVisual();

			}

			return IntPtr.Zero;

		}

		/////////////////////////////////////////////////////////////
		/////////////////////////////////////////////////////////////
		/////////////////////////////////////////////////////////////

		public MainWindow()
		{

			InitializeComponent();

			// トリガを検知したらカウントアップするリスト.
			for ( int k = 0; k < 16; k++ )
			{
				// 全要素をゼロリセット.
				const int INIT_VALUE = 0;
				ListCounter.Add( INIT_VALUE );
			}

			// コンテックのDIOクラスを生成する.
			MyDio = new Cdio();

			// DIOを初期化する.
			int iret = MyDio.Init( DEV_NAME_DIO, out MyId );
			if ( iret != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.Init();" );
			}

			// ウインドウがロードされたら実行するイベントハンドラを登録する.
			// イベントハンドラの名前はなんでもいい.
			// MainWindowLoaded とか MyMainWindowLoaded とか.
			Loaded += MainWindow_Loaded;

			this.Title = "GazoYaro";

		}

		private void MainWindow_Loaded(object sender, RoutedEventArgs e)
		{

			// ウインドウズのメッセージフックが使えるようにする. 
			HwndSource source = HwndSource.FromHwnd( new WindowInteropHelper( this ).Handle );
			source.AddHook( new HwndSourceHook( WndProc ) );

			// NotifyTrg() に使う引数.
			int hwnd = (int)( this.Handle );

			// 論理ライズトリガ(アップエッジ)の検出を開始する.
			// 論理電源電圧 → ゼロボルト.
			short trg_kind0 = (short)( CdioConst.DIO_TRG_RISE );
			int iret0 = MyDio.NotifyTrg( MyId, TRG_BIT_NUMBER_INP00, trg_kind0, MSEC_TRG_WATCH_LOOP, hwnd );

			// 論理フォールトリガ(ダウンエッジ)の検出を開始する.
			// ゼロボルト → 論理電源電圧.
			short trg_kind1 = (short)( CdioConst.DIO_TRG_FALL );
			int iret1 = MyDio.NotifyTrg( MyId, TRG_BIT_NUMBER_INP01, trg_kind1, MSEC_TRG_WATCH_LOOP, hwnd );

			// 論理ライズフォールトリガ(アップエッジとダウンエッジ両方)の検出を開始する.
			// 論理電源電圧 → ゼロボルト → 論理電源電圧.
			short trg_kind2 = (short)( trg_kind0 | trg_kind1 );
			int iret2 = MyDio.NotifyTrg( MyId, TRG_BIT_NUMBER_INP02, trg_kind2, MSEC_TRG_WATCH_LOOP, hwnd );

			if ( iret0 != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.NotifyTrg(TRG_BIT_NUMBER_INP0); rise." );
			}

			if ( iret1 != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.NotifyTrg(TRG_BIT_NUMBER_INP1); fall." );
			}

			if ( iret2 != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.NotifyTrg(TRG_BIT_NUMBER_INP2); both." );
			}

		}

		protected override void OnRender( DrawingContext dc )
		{

			// カウンタの全要素の内容を参照して表示用ストリングビルダに追加する.
			StringBuilder sb = new StringBuilder();
			foreach( var v in ListCounter.Select(( value, index ) => new { value, index })) 
			{
				int idx = v.index;
				int tmp = v.value;
				String s = String.Format( "[{0:d2}]\t{1}", idx, tmp );
				sb.AppendLine( s );
			}

			// テキストブロックにすべてのビットのカウンタを表示する.
			String str = sb.ToString().Trim();
			TheTbk.Text = str;

			this.stbLabel000.Content = DEV_NAME_DIO;

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{

			// トリガの検出をやめる.
			int iret0 = MyDio.StopNotifyTrg( MyId, TRG_BIT_NUMBER_INP00 );
			int iret1 = MyDio.StopNotifyTrg( MyId, TRG_BIT_NUMBER_INP01 );
			int iret2 = MyDio.StopNotifyTrg( MyId, TRG_BIT_NUMBER_INP02 );

			if ( iret0 != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.StopNotifyTrg(TRG_BIT_NUMBER_INP0);" );
			}

			if ( iret1 != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.StopNotifyTrg(TRG_BIT_NUMBER_INP1);" );
			}

			if ( iret2 != (int)( CdioConst.DIO_ERR_SUCCESS ))
			{
				MessageBox.Show( "Error: MyDio.StopNotifyTrg(TRG_BIT_NUMBER_INP2);" );
			}

			// DIOを終了する.
			MyDio.Exit( MyId );

			// ウインドウを閉じてアプリケーション終了する.
			this.Close();

		}

	}

}

入力 INP0(35番ピン)、INP1(34番ピン)、INP2(33番ピン) に接続されたセンサが反応したら、上から3つの数値が増えていくはずです。

INP0 は、センサがオンした瞬間に数値が増えます。
INP1 は、センサがオフした瞬間に数値が増えます。
INP2 は、センサがオンした瞬間と、センサがオフした瞬間に数値が増えます。

最後に、Fig. 2 と Fig. 3 の波形は、あくまでも論理的なものです。RISEというのは「論理がRISE」、FALLというのは「論理がFALL」という意味です。実際の電気回路ではローアクティブとして扱うので、オシロスコープで入力パルス波形を観測すると、Fig. 2 と Fig. 3 で示したものと極性が逆になります。

センサがオンするとボードに入力される電圧は0ボルトです。
センサがオフするとボードに入力される電圧は+24ボルトです。

本記事のプログラムをZIPアーカイブで用意しました。下記のページからダウンロードしてビルド実行して動作確認することをお勧めします。