WindowsでSerialポートを使う

C++/CLI又はC#を使い、Windows上でSerialポートの制御をしてみます。



index


USB-シリアル変換モジュールを使う

秋月で販売しているUSB-シリアル変換モジュールを使ってみます。

先日、FT234Xの接続を行ったところ、最新のドライバ(2.12.0.6)では動作しませんでした。

デバイスマネージャーでドライバの削除をするか

FTDI Utilities

から入手したCDM Uninstallerを使ってドライバを削除してから

D2XX Direct Drivers

このサイトの表の二段目の「2.12.00 WHQL Certified」をインストールすると、使えるようになりました。

C++/CLIでSerialポートを羅列する

private: System::Void comboBox1_DropDown(System::Object^  sender, System::EventArgs^  e)
{
    comboBox1->Items->Clear();
    comboBox1->Items->AddRange(System::IO::Ports::SerialPort::GetPortNames());
}

C#でもほぼ同じです。

これで得られるSerialポート名は、Serialポートを開くのに最低限必要な文字列COMn(nは自然数)です。さらに詳細なポート名を取得するには、次項の方法を使ってください。

C#で詳細なSerialポート名を取得する

C#で詳細なSerialポート名、例えばUSB Serial Port (COM3)といったのを取得するにはWin32_SerialPortクラスかWin32_PnPEntityクラスを用いる必要があります。 FTDI製品は前者Win32_SerialPortクラスでは取得できないので、今回はWin32_PnPEntityクラスを使います。

private void comboBoxComPortName_DropDownOpened(object sender, EventArgs e)
{
    comboBoxComPortName.Items.Clear();

    var CheckComNum = new System.Text.RegularExpressions.Regex("COM[1-9][0-9]?[0-9]?");
            
    System.Management.ManagementClass mcPnPEntity = new System.Management.ManagementClass("Win32_PnPEntity");
    System.Management.ManagementObjectCollection manageObjCol = mcPnPEntity.GetInstances();

    foreach (System.Management.ManagementObject manageObj in manageObjCol)
    {
        var namePropertyValue = manageObj.GetPropertyValue("Name");
        if (namePropertyValue == null)
        {
            continue;
        }
        string name = namePropertyValue.ToString();

        if (CheckComNum.IsMatch(name))
        {
            comboBoxComPortName.Items.Add(name);
        }
    }
}

このプログラムでは、WPFアプリケーションのComboBoxに接続されているSerialポート名を列挙しています。

Serialポート名からSerialポートを開くのに必要な文字列COMn(nは自然数)を得るには、以下のようになります。

private string getPortName(void){
    var ExtractPortNum = new System.Text.RegularExpressions.Regex(".*(COM[1-9][0-9]?[0-9]?).*");
    if (comboBoxComPortName.SelectedItem == null)
    {
        textBoxTextArea.Text += "No Port Selected\n";
        return System.String.Empty;
    }
    string name = (string)comboBoxComPortName.SelectedItem;
    string portName = ExtractPortNum.Replace(name, "$1");
    return portName;
}

C++/CLIでもほぼ同様にできます。

WPFアプリケーションでSerialポートを使う

WindowsフォームではSerialポートがコントロールとして提供されていましたが、WPFではコントロールとしては提供されていません。しかし、DataReceivedEventHandlerでの処理を除けば、Windowsフォームの時とほぼ同様にSerialポートを利用できます。

public partial class MainWindow : Window
{

    public System.IO.Ports.SerialPort serialPort;

    public MainWindow()
    {
        InitializeComponent();
        
        //取り敢えずCOM3に設定
        serialPort = new System.IO.Ports.SerialPort("COM3" , 9600, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One);
        serialPort.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(serialPort_DataReceivedHandler);
    }

serialPort_DataReceivedHandlerは後で宣言します。

private void buttonComConnect_Click(object sender, RoutedEventArgs e)
{
    string portName = getPortName();
    if( portName != System.String.Empty )
    {
        serialPort.PortName = portName;
        try
        {
            serialPort.Open();
        }
        catch
        {
            textBoxTextArea.Text = "!Error! " + serialPort.PortName + "をOpenできませんでした.";
        }
        if (serialPort.IsOpen)
        {
            textBoxTextArea.Text = serialPort.PortName + "をOpenしました.";
        }
    }
    else
    {
        textBoxTextArea.Text = "!Error! Port nameを設定してください.";
    }
}

buttonComConnectを押した時の処理です。getPortName()は前項で用意した関数です。

DataReceivedEventHandlerでの処理の注意点

以下のコードは動きません。

private void serialPort_DataReceivedHandler(
            object sender,
            System.IO.Ports.SerialDataReceivedEventArgs e)
{
    
    try
    {
        textBoxTextArea.Text = serialPort.ReadLine();
    }
    catch
    {
        textBoxTextArea.Text = "!Error! " + serialPort.PortName + "に接続できません.";
    }
}

DataReceivedHandlerはUIスレッドとは別のスレッドで実行されているため、DataReceivedHandlerからUIを直接操作することはできません。WindowsフォームアプリケーションではSerialポートはコントロールとして組み込まれていたので問題なくできていました事ですが、WPFでは一捻り必要です。

この問題を解決するには、Dispatcherを利用します。コードを示したほうが早いと思うので示します。

private void serialPort_DataReceivedHandler(
            object sender,
            System.IO.Ports.SerialDataReceivedEventArgs e)
{

    try
    {
        textBoxTextArea.Dispatcher.Invoke(
            new Action(() =>
            {
                textBoxTextArea.Text = serialPort.ReadLine();
            })
        );//beginInbokeだと呼び出し直後に元のコントロールに制御が戻る
    }
    catch
    {
        textBoxTextArea.Dispatcher.Invoke(
            new Action(() =>
            {
                textBoxTextArea.Text = "!Error! " + serialPort.PortName + "に接続できません.";
            })
        );
    }
}

(UIコントロール名).Dispatcher.Invoke( new Action(() => { 行いたい処理; }) );で、別スレッドからUIコントロールの操作ができます。

このInvokeメソッドは、処理を同期的に行います。要するに、処理が終わるまで、プログラムの次の行には進まないということです。

同種の物にBeginInvokeメソッドがありますが、こちらは処理を非同期的に行います。要するに、UIスレッドに「この処理をせよ!」という命令を出して、すぐさまプログラムの次の行に進みます。

己が試した限りでは、BeginInvokeを使った場合、偶にフリーズ(所謂デッドロックか?)が起こりました。特に、他のUI(ボタンとか)を操作している時にDataReceivedHandlerが呼び出された時です。Invokeしている時に条件が悪いとデッドロックが起こるという話は聞きますが、寧ろInvokeを使うと安定しました。少々謎が残りますが、まぁいいでしょう。