月刊ソフト作り!
Make The Software For VisualBasic


『 通信ソフトを作る!(その1) 』

2002.Aug.31
 Presented by kouta_y
感想等は掲示板、苦情はメールへ。


会社

パソコンである程度のプログラムが組めるようになると「外部の装置も制御してみたい!」なんて思いませんか?
RS-232Cは、そういった外部装置を制御する場合に用いられる常套手段です。
最近はUSBなど高速な伝送方式に乗っ取られつつありますが、マイコンなど低レベルで動作するものとの通信方式としては、
まだまだバリx2現役です。
そして筆者は、この外部機器との通信を「パソコン通信」などと呼び、微妙にズレた知識を持っていました。。。


シリアル通信を知る!

今回はソフトというよりハードウェア的な説明です。
ソフトを組む上では特に知らなくても良いです。(もちろん知ってた方がいい時もある)
興味ない人は飛ばしてください。

さて、「シリアル通信」というからには、「なにか単体で通信するもの」という感じがします。
その通りです。
外部装置とやりとりする場合には、なにかしらインターフェース(接続線)が必要です。
普通パソコンと通信する場合、1バイトづつデータのやり取りをするのが定石ですので、データバスは8本(8ビット)となります。
しかも、他にもそのデータを制御するためのビットが2,3本必要になりますので、最低でも10本以上のバス幅となります。
しかし、シリアル通信の場合は、そのデータのやり取りを1本の線で行おうという画期的な伝送方式なのです。(い、いや昔からあるんですけど)

実際どういうもので使われているか例をあげると、身近なものでは「電話回線」がそうです。
すると「ADSL」も当然シリアル通信という事になります。
ローカルエリアネットワーク「LAN」や、ユニバーサルシリアルバス「USB」もその名の通りシリアル通信です。
実際外部とのデータのやり取りはパラレル方式よりも、シリアル方式の方が多いのです。

では具体的な通信方法を説明します。


RS-232Cを知る!

シリアル通信といってもその方法は多種多彩です。
今回作るプログラムは、パソコンのCOMポートで使われているRS-232Cと呼ばれる伝送方式です。

「RS-232C」の略は「Recommended Standard - 232C」、正式名称は「EIA/TIA-232-E」
しかし「RS-232C」という呼称の方が、皆さん愛着があるみたいなので、「EIA」なんていうより「RS」の方が通じます。
ちなみに「RS-232C」は旧来の呼び名であり、別に間違いではありません。
このページでも「RS-232C」で説明します。

まず、パソコンの裏側を見てください。
線がいろいろ出ていると思いますが、その中で9本の端子があるコネクタがあるハズです。
それがCOMポートと呼ばれるもので、今回はこっからデータを送ったり貰ったりします。
(最近の機種にはたぶん付いてないと思います。そういう人は想像でまかなってください・・・。って言ってる筆者の「ばいお」にも付いてないんですが(笑))

あと、PC-9800シリーズを使っている人や、「コイツも年取ったなぁ(しみじみ)」という機種には25ピンタイプのコネクタになっていると思います。

まずそれぞれの端子の説明です。

形状

正面から見た図です。

信号
ピン番号 信号名 説明 入出力方向
DCD キャリア検出 入力
2 RxD 受信データ 入力
3 TxD 送信データ 出力
4 DTR データ端末レディ 出力
5 GND グランド(アース) 入/出力
6 DSR データセットレディ 入力
7 RTS 送信要求 出力
8 CTS 送信可 入力
9 RI リングインジケータ 入力


あまりピンとこないと思いますが、ここで本当に重要なのはRxDTxD端子だけです。
この2本の線で送/受信のデータやり取りを行います。
また下の図はパソコンと外部機器とを接続するケーブルの結線図です。
RS-232Cのケーブルには大きく分けて2種類あり、それぞれストレートケーブル・クロスケーブルという呼び方をします。


ストレートケーブルの結線図


クロスケーブルの結線図(一例)


クロスケーブルはほんの一例です。
この他にも微妙に違う結線をしているものもあります。
基本的に、「出力→入力」という接続をします。(だからクロスケーブル)

※ 「RI」と「DCD」は通常は使う機会はあまりありません。
RI端子は、図ではオープンになっていますが、本来なら「ホスト→端末」という繋げ方をし、
「給電開始」信号を送るそうです。(端末起動開始の合図)



データのやり取り方は?

まず前述した通り「シリアル通信」というのは単線(送受信に各1本ずつ)でデータのやり取りをする事を言います。
たった一本の線で、どうやってデータを送るのか?
それにはクロック(周期)と呼ばれる、いわば時計の役割をしたものが必要です。
さらに例えるなら、小学校の音楽で使った「メトロノーム」みたいなものです。
カッチカッチってやつです。(覚えてます?)
そのメトロノーム・・・つまりクロックの周期にあわせ、データを1ビットずつ送信します。
つまり、このクロックの周期が速ければ速い程、高速なデータ通信が可能となる訳です。





この図が実際にRS-232Cで送るビットです。
これらのビットは内部のクロックに同期して送られます。
メトロノームで言えば、「カッチカッチ・・・」の音に合わせて「か・ぜ・に・と・ま・ど・う〜♪(by TSUNAMI)」と歌うわけです。
シリアル通信の速度の単位は「BPS(ビットパーセコンド)」で表し、つまり「1秒間に何ビット送れるか」というものです。
ADSLの「1.5Mbps」やアナログモデムの「54Kbps」も同じ意味です。(送るビットは違います)


それぞれのビットの意味

「うーん、なんだか次々と訳のわからない説明が続くなあ」

この辺、あいまいに説明してらっしゃるサイトさんも多いので、多少細かく説明しています。
もう少ししたらソフト作りの方に入りますんで・・・。

ちなみに、シリアル通信ではH(ハイレベル)の事をMark(マーク)、L(ローレベル)の事をSpace(スペース)という言い方をします。
しかし筆者、この言い方には慣れていないので、デジタル回路でも使うHとLで説明をします。
さらにRS-232Cでの信号レベルは±15Vで、スレッショルド電圧は±3V程度です。
H(ハイレベル)が−15V〜−3V。
L(ローレベル)が+3V〜+15V。
という規格になっています。
ハードに興味がない人は気にしないでください。


スタートビット

スタートビットとは、
「これからデータを送るよー」
という合図ビットです。
受信側ではこのビットをキャッチしたら、その次からのビットが「ほんまもんのデータやな!」と解釈します。
電圧レベルはL(ローレベル)です。

データ

データは、実際に送るデータビットのことです。
ビットについてはこの辺りでも説明しています。

このデータビット長(ビット数)は、普通は7〜8ビットが選べます。
8ビットも使わねーよ。というのであれば7ビットの方が1ビット分稼げます。
筆者は常に8ビットにしていますが。
送信する順序は下位ビットからですので、例えば
1011 0110(182)
というビットの並びだったら、送る順番は
0 1 1 0 1 1 0 1
になります。


パリティビット

さて、この辺は少しややこしいです。
パリティビットとは、簡単に言えば「正しいデータが送れたか?」の検出ビットのようなものです。
実際にどのように「間違い」だと判定するかというと、送信側(パソコン側)で以下のような計算をします。

Parity Bit = D0 + D1 + D2 + ... Dn

この結果、Parity Bitが「1」になれば奇数パリティ、「0」になれば偶数パリティとなります。(+というのは排他的論理和つまりXOR演算のことです)
そして受信側(制御機器側)では、同じ計算をし、受信したパリティと一致すれば「正しいデータが受信できた」と解釈をします。

しかし!
計算結果は1と0の2通りしかないわけですから、もしパリティビット自体が崩れたら?
もっと怖いのは、データが崩れ、さらにパリティも崩れ、その結果「正しいデータだ」と受信側が解釈してしまったら?
パリティチェックは元々は雑音対策として考えられたそうですが、上記に記した通りあまり信頼がおけないため、今では全くと言っていいほど使われません。
その代わりなのかなんなのか知りませんが、パリティチェックの方法は4通りもあったりします。

タイプ 説明
EVEN 結果が1なら偶数
MARK 常に1
ODD 結果が1なら奇数(上の計算はこれです)
SPACE 常に0

まぁ当たり前ですね。


ストップビット

スタートビットが「データの始まり」なのに対し、ストップビットは「データの終わり」を示します。
ストップビットは1ビットとは限らず、1、1.5、2と3種類あります。
これも雑音対策の1つで、
1なら1ビット。
1.5なら、1ビット+0.5ビット(つまり1周期半)
2なら2ビット。
となります。
信号レベルはH(ハイレベル)です。


クロックに同期して送るとは?

さて、ハードウェアな話ばかりで恐縮ですが、まだ続きます

先程から言っている、同期とはどういう事なんでしょうか?
クロック?メトロノーム?周期ってなに?

まずクロックというのは、パルスのことで、よく「俺のパソコン。1GHzだゼー!へへん!」なんて言いますよね。(今や1GHz程度じゃイバれませんが)
それの事です。
例えば「9,600bps」でしたら、1秒間で9600ビット送れます。
という事は、

1000ms / 9600bit

で約104.2us/ビットになります。
これを周波数に直すと、

1000ms / 104.2us ≠ 9600Hz

となり、実はbpsはHzと等価なんです。
つまり「通信レート 9,600bps」と言ったら、内部で9,600Hzの周波数を作り出し、1ビットを約104.2usで送る。という意味になります。
以上の説明で分かりにくい人は、以下もタイミングチャートと見比べてください。
また、受信側も9600Hzの周期に同期し、データの受信を行います。
つまりお互いに同じ周波数でデータのやり取りをするわけです。
(正しくは、制御側の通信速度に合わせて、ホスト側の通信速度を調整します)



縦軸が信号レベル。横軸が時間です。
なんとなく「同期」の意味、わかったでしょうか?



同期と非同期通信

「まだあんのかよー!」

これはオマケみたいなもんですので、もう少し辛抱を、、、

ここまで説明してきて、シリアル通信にはなんらかの「クロックが必要になる」という事がわかるはずです。
そしてRS-232Cは「同期通信」と「非同期通信」の2種類があります。

今まで説明してきたのは「非同期通信」の部類に入ります。
対して「同期通信」というのは、その名の通りクロックに同期することを言います。
非同期はクロックに同期しません

「えー!?非同期もクロックに同期するって言ったじゃん!この詐欺師!」

まあまあ(←あほ)
「非同期通信」とは内部クロックに同期するのであって、それ以外のものには一切同期をとりません。
「同期通信」とは内部クロックを持たず、外部のクロックに同期することから同期通信という名前になります。
この辺は予備知識程度に覚えておいてください。
また、
「同期通信」 → 「非調歩同期式通信」
「非同期通信」 → 「調歩同期式通信」
という呼び方もします。
「同期」が「非調歩同期式」なのは、外部クロックに同期して、内部クロックに同期(調歩)しないからです。



さっそくプログラムを書いてみよう!

やっとこさ本題に入ります。
VisualBasicには、MSCommという便利なコントロールがあり、これを使えば簡単にRS-232C通信ができます。
やったぜ!

そんなもん使いません。

ここでは全てAPI関数を使ってRS-232C通信を実現します。

それではまず、使うAPI関数の宣言です。
定数、構造体なども一緒に宣言しています。


Public Const GENERIC_READ           As Long = &H80000000    ' 読み取り
Public Const GENERIC_WRITE          As Long = &H40000000    ' 書き込み
Public Const OPEN_EXISTING          As Long = 3             ' 既存ファイルを開く
Public Const INVALID_HANDLE_VALUE   As Long = -1            ' 失敗

' タイムアウト構造体
Public Type COMMTIMEOUTS
    ReadIntervalTimeout         As Long ' 連続受信時の間隔
    ReadTotalTimeoutMultiplier  As Long ' 1文字受信の時間
    ReadTotalTimeoutConstant    As Long ' 受信時間の定数
    WriteTotalTimeoutMultiplier As Long ' 1文字送信の時間
    WriteTotalTimeoutConstant   As Long ' 送信時間の定数
End Type


' 通信リソース開く
Public Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" ( _
    ByVal lpFileName As String, _
    ByVal dwDesiredAccess As Long, _
    ByVal dwShareMode As Long, _
    ByVal lpSecurityAttributes As Long, _
    ByVal dwCreationDisposition As Long, _
    ByVal dwFlagsAndAttributes As Long, _
    ByVal hTemplateFile As Long _
) As Long

' ポートから入力
Public Declare Function ReadFile Lib "kernel32" ( _
    ByVal hFile As Long, _
    lpBuffer As Any, _
    ByVal nNumberOfBytesToRead As Long, _
    lpNumberOfBytesRead As Long, _
    ByVal lpOverlapped As Long _
) As Long

' ポートへ出力
Public Declare Function WriteFile Lib "kernel32" ( _
    ByVal hFile As Long, _
    lpBuffer As Any, _
    ByVal nNumberOfBytesToWrite As Long, _
    lpNumberOfBytesWritten As Long, _
    ByVal lpOverlapped As Long _
) As Long

' タイムアウトの設定
Public Declare Function SetCommTimeouts Lib "kernel32" ( _
    ByVal hFile As Long, _
    lpCommTimeouts As COMMTIMEOUTS _
) As Long

' 通信リソース閉じる
Public Declare Function CloseHandle Lib "kernel32" ( _
    ByVal hObject As Long _
) As Long


関数の説明は省略させて貰います。
関数の宣言はAPIビューアのものとは多少異なっています。
んで次にこれらのAPIを呼ぶ、自作関数群を作ります。

COMポートを開く


' COMポート開く
Public Function CommOpen(ByVal strPort As String) As Long
CommOpen = CreateFile( _
                strPort, _
                GENERIC_READ Or GENERIC_WRITE, _
                0, 0, _
                OPEN_EXISTING, _
                0, 0 _
            )
End Function

COMポートを開く関数です。
strPort には「COM1」などの文字列を渡します。
CreateFile 成功したら開いたポートのハンドルを返しますので、それをそのまま関数の戻り値とします。


' COMポートから受信
Public Function CommInput(ByVal hCom As Long, strBuffer As String, ByVal lngLen As Long) As Long
Dim lngRead As Long
Call ReadFile(hCom, ByVal strBuffer, lngLen, lngRead, 0)
CommInput = lngRead
End Function

COMポートからデータを受信する関数です。
hCom には開いているCOMポートのハンドルを指定します。
strBuffer にはいわゆる「受け皿」となるバッファを渡します。
lngLen にはバッファの領域をしていします。(つまり何バイト受信するかということです)
受信できたら、受信したバイト数を返します。


' COMポートへ送信
Public Function CommOutput(ByVal hCom As Long, ByVal strBuffer As String, ByVal lngLen As Long) As Long
Dim lngWrite As Long
Call WriteFile(hCom, ByVal strBuffer, lngLen, lngWrite, 0)
CommOutput = lngWrite
End Function

COMポートへデータを送信する関数です。
hCom には開いているCOMポートのハンドルを渡します。
strBuffer には送信したいデータを渡します。
lngLen には送信バッファのサイズを渡します。(つまり何バイト送信するかということです)
送信できたら、送信したバイト数を返します。


' COMポート閉じる
Public Function CommClose(ByVal hCom As Long) As Boolean
CommClose = CloseHandle(hCom)
End Function

COMポートを閉じます。
終了時には必ずこの関数を呼び、COMポートを閉じなくてはなりません。


基本的なものばかりですね。



タイムアウトを設定する

次にタイムアウト値の設定です。
タイムアウトとは、受信もしくは送信時にあまりに時間がかかるとアプリケーションはブロッキング状態になってしまいます。
特に受信の場合、受信データが送られてこなければ永遠に待ち続けます
「私、もう待ちきれないのっ!」
なんて言って男を見捨てる女とは違います。(かおり、待ってくれ!)
コンピュータは、ある意味ものすごく健気なのです。
そこで「それでは困る!」という場合に、ある一定時間応答がなければ制御を返す――つまりタイムアウトを設定します。

タイムアウト値の設定は、上のAPI宣言にもあるSetCommTimeouts関数を使います。


' タイムアウトの設定
Public Function SetCommTimeout(ByVal hCom As Long, typTimeout As COMMTIMEOUTS) As Boolean
SetCommTimeout = SetCommTimeouts(hCom, typTimeout)
End Function

タイムアウトを設定します。
hCom には開いているCOMポートのハンドル。
typTimeout にはCOMMTIMEOUTS構造体を渡します。

タイムアウト値は実際にはCOMポートドライバに通知をします。
構造体の各メンバの説明は、構造体宣言のコメントを見てください。(手抜きじゃないゾ!)
次にタイムアウト値の計算方法ですが、それぞれ、

ReadTimeOut = ReadTotalTimeoutMultiplier * ( 受信バイト数 ) + ReadTotalTimeoutConstant
WriteTimeOut = WriteTotalTimeoutMultiplier * ( 送信バイト数 ) + WriteTotalTimeoutConstant

という計算方法になります。
単純に「受信バイト数に限らずこの時間でタイムアウトさせたい」というのであれば、定数だけ指定すればOKです。
タイムアウトさせたくなければ、全てのメンバを0に設定します。
また、ReadIntervalTimeoutメンバには「連続受信時の間隔」とありますが、
これは2文字以上のデータを受信する場合、1文字目を受信してから2文字目を受信するまでの時間(ミリ秒)を設定します。

ちなみにAPI関数で扱うほとんどの時間系関数は、ミリ秒単位で設定・取得します。



そして本番へ・・・!

いよいよ、今回のメインプログラム作りに入ります。
今回は「その1」ですので、外部装置にデータを送って返ってきた文字を受信する。って所までの簡単なものを作ります。

まずメインとなるフォームを挿入します。
次に「送信データ」「受信データ」用エディットボックスと、「送信ボタン」「受信ボタン」を作ります。
特に特殊なプロパティは設定していませんので、各プロパティの説明は省きます。

外観はこんな感じ


フォームのデザインも終わったので、次に各プロージャ部分のコードを書いてみましょう。


その前に!

先に説明するのを忘れていました。
そういえば、プログラムが組めても外部装置がありませんでした。テヘッ。(気色悪っ)
どうしよう。困りました。
っとそこで、もしあなたがATモデムをお持ちならば、それをCOMポートにつなげてください。
ATモデムはパソコンとRS-232Cで通信しますので、カッコウの獲物です。
筆者の「ばいお」にもATモデムが内蔵されているので、それを外部装置として扱います。
持っていない方は、COMポートから出ている端子を以下のように接続してみてください。

TxD(3) − RxD(2)
RTS(7) − CTS(8)



これで「自分にデータを送る」という接続になります。
クリップとか通電するものなら何でも良いです。
但し、隣のピンとショート、これだけは絶対にしないでください。(15Vが印加されますんで。15VというのはICを壊すのには十分な電圧です)

「クリップなんかで大丈夫なの?」

この程度ならば全然大丈夫です。
多少ノイズ(雑音)が乗りますが、誤差の範囲です。
直に半田付けは止めましょう。

「パソコンが壊れたらどうしよう」

自己責任でお願いします。
責任は持ちませんが、ミドルレンジクラスのパソコンなら内部で保護回路が付いているでしょうから、例えショートしたとしてもいきなり壊れるという事はまずありえません。
もし回りにハードに詳しい人がいたら、その人にやってもらってください。
金を握らせて専用ケーブルを作ってもらいましょう。(冗談ですよ(^^;))

「つーかCOMポート自体ないんだけど」

USB - COM変換ケーブルを使うか、もしくは諦めてください。


それではプロージャのコードです。
フォームモジュールの全文です。


Private hCom As Long


Private Sub Command1_Click() Dim buf As String ' 送信する文字列を取得 ' 終端にキャリッジリターンを付ける buf = Text2.Text & vbCr ' 送信する If CommOutput(hCom, buf, Len(buf)) = 0 Then Call MsgBox("送信に失敗しました", vbOKOnly Or vbCritical) End If End Sub
Private Sub Command2_Click() Dim buf As String * 256 ' あらかじめ領域を確保しなければならない ' 200バイト受信する If CommInput(hCom, buf, 200) = 0 Then Call MsgBox("受信に失敗しました", vbOKOnly Or vbCritical) End If ' ターミナルへ表示 Text1.Text = Text1.Text & buf End Sub
Private Sub Form_Load() Dim cto As COMMTIMEOUTS ' COMポートを開く hCom = CommOpen("COM3") If hCom = INVALID_HANDLE_VALUE Then Call MsgBox("COMポートが開けません", vbOKOnly Or vbCritical) Unload Me Exit Sub End If ' タイムアウトを設定 cto.ReadTotalTimeoutConstant = 1000 Call SetCommTimeout(hCom, cto) End Sub
Private Sub Form_Unload(Cancel As Integer) ' COMポートを閉じる Call CommClose(hCom) End Sub

順序よく見てみましょう。

筆者の環境化では、ATモデムは「COM3」に繋がっているため、COM3ポートをオープンします。
その後、受信タイムアウトを1秒とします。
「開けません」というエラーメッセージが出なければ、めでたくCOMポートはオープンできました。
ただ、外部装置が接続されていなくても、COMポートの存在自体があればオープンは出来るので、次に何かデータを送ってみましょう。
筆者と同じく、ATモデムと接続している方は、

AT

コマンドを送信してみてください。
このコマンドはムデムが使用可能かどうか問い合わせるコマンドです。
一番上のエディットボックスに「AT」と入力し、「送信」ボタンを押します。
その後「受信」ボタンを押し、



のように、応答メッセージが返ってくれば大成功です。
また、電話番号をダイアルする場合は、

ATDT 0123-4567

もしくは

ATDP 0123-4567

というコマンドを送信します。(MSDNより抜粋)

「自分へ送信」の接続をしている方は、



となっていると思います。
これはさっき接続した配線をデータが通り抜けて、受信データとして返ってきたのです。
違うデータが返ってきたら、それはハードウェア的な受信ミス、もしくは送信ミスです。


どうでしょう?
ソフトでやると簡単ですが、この背景では、上で長ったらしく説明した処理が動いているのです。
つくづくパソコンってすごい「オモチャ」ですよね。



終わり

今回はこれで終わりです。
まだ説明不足なところもありますが、追って説明していければと思います。
今回は「月刊ソフト作り!」また「月刊プログラム!」を通して、一番長い章になってしまいました。
ソフトよりハードの説明で長くなってしまうとは・・・なんたること。
まぁハードあってのソフトですからね。
どんまいどんまい。(意味不明)

最後に、今回やったサンプルプロジェクトを下から落とせるようにしました。
参考になればと思います。
ではまた次回。

vb14.lzh (7.11KB)