60歳からの電子工作ノート

生涯学習として取り組んでいます。

シリアル通信プログラムによるCH340Eの動作確認

概要

USBシリアル変換IC CH340Eの動作確認のため、C#,XAMLを使用して、シリアル通信テスト用のWPFアプリ(.NET Framwork)を作成しました。
送信データをループバックして受信するテストですが、特定のバイト数(32byteや64byte)を送信すると、受信イベントが正常に終了できませんでした。
プログラムの作り方に問題があるのか、ドライバの問題か判定できませんが、同じプログラムを、FTDI社のFT232RQ で試したところ、正常に処理されました。
CH340Eは比較的最近のICなので、使い方に工夫が必要だと思いました。

・2022年7月2日 修正:
FT232RQでもCH340Eと同じ現象が確認されました。OSのスケジューリングや.NET側の問題のようです。テスト結果を追加しました。

外観
Fig. シリアル通信テストアプリ (WPF)
処理概要

入力した送信バイト数分のデータ(0x00より)を送信し、送信バイト数のデータを受信すると、受信データを表示します。
通信条件は、固定で、コードで記述しています。

Fig. 通信テストアプリのイメージ
プログラム

Microsoft Visual Studio Community 2019 Version 16.11.1 使用
WPFアプリ(.NET Framework)
.NET Framework 4.7.2
・表示のXAML部分

<Window x:Class="CommTest.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:CommTest"
        mc:Ignorable="d"
          ResizeMode="CanResizeWithGrip" 
        Title="CommTest" Height="600" Width="800">
    <Grid>
        <!-- Gridで使用するボタンの大きさ、色を定義-->
        <Grid.Resources>
            <Style TargetType="Button">
                <Setter Property="Height" Value="30" />
                <Setter Property="Width" Value="100"/>
                <Setter Property="Margin" Value="10" />
                <Setter Property="BorderBrush" Value="#a6a6a6" />
                <Setter Property="Foreground" Value="#333333" />
                <Setter Property="Background" Value="#fcfcfc"/>
            </Style>
        </Grid.Resources>

        <!-- カラム Grid 横方向の大きさ指定。 "AUTO"は、横幅を変更するGridSplitterの部分  -->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"  MinWidth="100"/>
            <ColumnDefinition Width="AUTO"/>
            <ColumnDefinition Width="2*" MinWidth="100" />
        </Grid.ColumnDefinitions>

        <!-- Grid 行方向の大きさ指定 "AUTO"は、高さを変更する GridSplitterの部分-->
        <Grid.RowDefinitions>
            <RowDefinition Height="3*"  MinHeight="100" />
            <RowDefinition Height="AUTO"  />
            <RowDefinition Height="1*" MinHeight="100" />
        </Grid.RowDefinitions>

        <!--横幅を変更する GridSplitter-->
        <GridSplitter Grid.Row="0" Grid.Column="1"   Grid.RowSpan="3" Width="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Gainsboro"/>
       
        <!--高さを変更する GridSplitter-->
        <GridSplitter Grid.Row="1" Grid.Column="0"    Grid.ColumnSpan="3" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Gainsboro"/>

        <!-- スタックパネル (row=0,col=0)  ComboBoxやコントロールを配置-->
        <StackPanel Grid.Row="0" Grid.Column="0"  Margin="10">

                    <ComboBox  x:Name = "ComPortComboBox"   TextSearch.TextPath="ComPortName" Height="30" Width="100" Margin="10" BorderBrush="#a6a6a6" Foreground="#333333" Background="#fcfcfc">
                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal"  >
                                    <TextBlock Text="{Binding ComPortName}"  />  <!--データバインド-->
                                </StackPanel>
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>

                    <Button x:Name= "ComPortOpenButton" Content="Open" Click="ComPortOpenButton_Click"/>
                    <Button x:Name= "ComPortSearchButton"  Content="Find"  Click="ComPortSearchButton_Click"/>
                    <TextBlock HorizontalAlignment="Center" Margin="10">
                        ( 76.8 Kbps,1-stop, no-parity)
                    </TextBlock>

                <TextBox x:Name="OpenInfoTextBox"   IsReadOnly="True" BorderThickness="0"  Margin ="10" Text ="Open/Close infomation."/>
               
        </StackPanel>

        <!-- スタックパネル (row=2,col=0) 送信ボタンを配置-->
        <!-- row= 1 には、Gridsplitterが配置されている -->
        <StackPanel Grid.Row="2" Grid.Column="0"  Margin="10">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Send Byte:"/>
                 <TextBox x:Name="SendByteTextBox"   IsReadOnly="False" BorderThickness="1"  Margin ="10,0,0,0" Text ="16"/>
            </StackPanel>
            <Button Content="Test Send"  Click="Test_Send_Button_Click"/>
            <Button Content="Clear" Click="Clear_Button_Click" />
        </StackPanel>

        <!-- スタックパネル (row=0,col=2) TextBoxを配置-->
        <!-- col= 1 には、Gridsplitterが配置されている -->
        <StackPanel Grid.Row="0" Grid.Column="2"  Margin="10">
            <TextBlock Text="Send Data:"/>
            <TextBox x:Name="SendTextBox"   IsReadOnly="True" BorderThickness="1"  Margin ="10" Text =""/>
            <TextBlock Text="Receive Data:"/>
            <TextBox x:Name="RcvTextBox"   IsReadOnly="True" BorderThickness="1"  Margin ="10" Text=""/>
        </StackPanel>
        <!-- スタックパネル (row=2,col=2) TextBoxを配置-->
        <StackPanel Grid.Row="2" Grid.Column="2"  Margin="10">
            <TextBlock Text="Thread Info."/>
            <TextBox x:Name="ThreadIdTextBox"   IsReadOnly="True" BorderThickness="1"  Margin ="10" Text =""/>
        </StackPanel>

    </Grid>
</Window>

C#によるコードの部分

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO.Ports;
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;

namespace CommTest
{

    // COMポートの コンボボックス用 
    public class ComPortNameClass
    {
        string _ComPortName;

        public string ComPortName
        {
            get { return _ComPortName; }
            set { _ComPortName = value; }
        }
    }



    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableCollection<ComPortNameClass> ComPortNames;    // 通信ポート(COM1,COM2等)のコレクション 
                                                                       // データバインドするため、ObservableCollection 
        public static SerialPort serialPort;        // シリアルポート

        byte[] sendBuf;          // 送信バッファ   
        int sendByteLen;         // 送信データのバイト数

        byte[] rcvBuf;           // 受信バッファ
        int srcv_pt;             // 受信データ格納位置

        int data_receive_thread_id;  // データ受信ハンドラのスレッドID
        int data_receive_thread_cnt;  // データ受信ハンドラの実施回数

        public MainWindow()
        {
            InitializeComponent();

            MainWindow.serialPort = new SerialPort();    // シリアルポートのインスタンス生成
            MainWindow.serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);  // データ受信時のイベント処理

            ComPortNames = new ObservableCollection<ComPortNameClass>();  // 通信ポートのコレクション インスタンス生成

            ComPortComboBox.ItemsSource = ComPortNames;       // 通信ポートコンボボックスのアイテムソース指定  

            SetComPortName();                // 通信ポート名をコンボボックスへ設定

            if (ComPortNames.Count > 0)     // 通信ポートがある場合
            {
                if (serialPort.IsOpen == true)    // new Confserial()実行時に、 既に Openしている場合
                {
                    OpenInfoTextBox.Text = "(" + serialPort.PortName + ") is opened.";
                    ComPortOpenButton.Content = "Close";      // ボタン表示 Close
                }
                else
                {
                    OpenInfoTextBox.Text = "(" + serialPort.PortName + ") is closed.";
                    ComPortOpenButton.Content = "Open";      // ボタン表示 Close
                }
            }
            else
            {
                OpenInfoTextBox.Text = "COM port is not found.";
            }


            sendBuf = new byte[2048];     // 送信バッファ領域  serialPortのWriteBufferSize =2048 byte(デフォルト)
            rcvBuf = new byte[4096];      // 受信バッファ領域   SerialPort.ReadBufferSize = 4096 byte (デフォルト



            int id = System.Threading.Thread.CurrentThread.ManagedThreadId; // 現在実行しているスレッドのID
            ThreadIdTextBox.Text += "Main WindowのスレッドID : " + id.ToString() + "\n";

        }



        // 通信ポート名をコンボボックスへ設定
        private void SetComPortName()
        {
            ComPortNames.Clear();           // 通信ポートのコレクション クリア


            string[] PortList = SerialPort.GetPortNames();              // 存在するシリアルポート名が配列の要素として得られる。

            foreach (string PortName in PortList)
            {
                ComPortNames.Add(new ComPortNameClass { ComPortName = PortName }); // シリアルポート名の配列を、コレクションへコピー
            }

            if (ComPortNames.Count > 0)
            {
                ComPortComboBox.SelectedIndex = 0;   // 最初のポートを選択

                ComPortOpenButton.IsEnabled = true;  // ポートOPENボタンを「有効」にする。
            }
            else
            {
                ComPortOpenButton.IsEnabled = false;  // ポートOPENボタンを「無効」にする。
            }

        }


        // Findボタンを押した時の処理
        // 通信ポートの検索ボタン
        //
        private void ComPortSearchButton_Click(object sender, RoutedEventArgs e)
        {
            SetComPortName();
        }


        // Openボタンを押した時の処理
        // 通信ポートのオープン
        //
        //  SerialPort.ReadBufferSize = 4096 byte (デフォルト)
        //             WriteBufferSize =2048 byte
        //
        private void ComPortOpenButton_Click(object sender, RoutedEventArgs e)
        {
            if (serialPort.IsOpen == true)    // 既に Openしている場合
            {
                try
                {
                    serialPort.Close();

                    OpenInfoTextBox.Text = "Close(" + serialPort.PortName + ")";

                    ComPortComboBox.IsEnabled = true;        // 通信条件等を選択できるようにする。
                    ComPortSearchButton.IsEnabled = true;    // 通信ポート検索ボタンを有効とする。
                    ComPortOpenButton.Content = "Open";    // ボタン表示を Closeから Openへ

                }
                catch (Exception ex)
                {
                    OpenInfoTextBox.Text = ex.Message;
                }

            }
            else                      // Close状態からOpenする場合
            {
                serialPort.PortName = ComPortComboBox.Text;    // 選択したシリアルポート

                serialPort.BaudRate = 76800;           // ボーレート 76.8[Kbps]
                serialPort.Parity = Parity.None;       // パリティ無し
                serialPort.StopBits = StopBits.One;    //  1 ストップビット

                serialPort.Open();             // シリアルポートをオープンする
                serialPort.DiscardInBuffer();  // 受信バッファのクリア


                ComPortComboBox.IsEnabled = false;        // 通信条件等を選択不可にする。

                ComPortSearchButton.IsEnabled = false;    // 通信ポート検索ボタンを無効とする。

                OpenInfoTextBox.Text = " Open (" + serialPort.PortName + ")";

                ComPortOpenButton.Content = "Close";      // ボタン表示を OpenからCloseへ

            }
        }


        // Test Sendボタンを押した時の処理
        // データの送信
        private void Test_Send_Button_Click(object sender, RoutedEventArgs e)
        {

            if (MainWindow.serialPort.IsOpen == true)
            {
                srcv_pt = 0;                   // 受信データ格納位置クリア
                data_receive_thread_cnt = 0;  // 

                int.TryParse(SendByteTextBox.Text, out sendByteLen); // 送信バイト数の入力

                for ( byte i = 0; i <sendByteLen; i++)
                {
                    sendBuf[i] = i;
                }

               
                MainWindow.serialPort.Write(sendBuf, 0, sendByteLen);     // データ送信

                for (int i = 0; i < sendByteLen; i++)   // // 送信データの表示
                {
                    if ((i > 0) && (i % 16 == 0))    // 16バイト毎に1行空ける
                    {
                        SendTextBox.Text += "\r\n";
                    }

                    SendTextBox.Text +=  sendBuf[i].ToString("X2") + " ";

                }
                SendTextBox.Text += "(" + DateTime.Now.ToString("HH:mm:ss.fff") + ")" + "\r\n";
            }

            else
            {
                OpenInfoTextBox.Text = "Comm port closed !";
                
            }

        }

        // デリゲート関数の宣言
        private delegate void DelegateFn();

        // データ受信時のイベント処理
        private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
        {
          
            int rd_num = MainWindow.serialPort.BytesToRead;       // 受信データ数

            MainWindow.serialPort.Read(rcvBuf, srcv_pt, rd_num);   // 受信データを読み出して、受信バッファに格納

            srcv_pt = srcv_pt + rd_num;     // 次回の保存位置
            data_receive_thread_cnt++;      // データ受信ハンドラの実施回数のインクリメント

            int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
            data_receive_thread_id = id;                          //データ受信スレッドのID格納


            if (srcv_pt == sendByteLen)  // 最終データ受信済み (受信データ数は、送信バイト数と同一とする) イベント処理の終了
            {
                Dispatcher.BeginInvoke(new DelegateFn(RcvProc)); // Delegateを生成して、RcvProcを開始   (表示は別スレッドのため)
            }

        }

        //
        // データ受信イベント終了時の処理
        // 受信データの表示
        //
        private void RcvProc()
        {
            string rcv_str = "";

            ThreadIdTextBox.Text += "データ受信ハンドラの実施回数:" + data_receive_thread_cnt.ToString() + "\n";

            ThreadIdTextBox.Text += "DataReceivedHandlerのスレッドID:" + data_receive_thread_id.ToString() + "\n";

            int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
            ThreadIdTextBox.Text += "RcvProcのスレッドID:" + id.ToString() + "\n";


            for (int i = 0; i < srcv_pt; i++)   // 表示用の文字列作成
            {
               if ((i > 0) && (i % 16 == 0))    // 16バイト毎に1行空ける
               {
                    rcv_str = rcv_str + "\r\n";
               }

               rcv_str = rcv_str + rcvBuf[i].ToString("X2") + " ";
            }

             RcvTextBox.Text = RcvTextBox.Text + rcv_str;  // 受信文と時刻の表示

             RcvTextBox.Text += "(" + DateTime.Now.ToString("HH:mm:ss.fff") + ")(" + srcv_pt.ToString() + " bytes )" + "\r\n";



        }

        
        // クリアボタンを押した時の処理
        private void Clear_Button_Click(object sender, RoutedEventArgs e)
        {
            SendTextBox.Text = "";
            RcvTextBox.Text = "";
            ThreadIdTextBox.Text = "";
        }
    }
}
テスト

TXDとRXDをつなげて、送信データをループバックするテストです。

1)  CH340E へ50バイト送信すると、50バイト受信できました。(正常)

Fig. CH340E 送受信テスト (50byte)

2)  CH340E  へ 32バイト送信すると、受信処理がうまくできませんでした。(受信終了後、再度、受信イベント発生)(異常)

Fig. CH340E 送受信テスト (32byte)

3) FTDI  FT232RQ へ32バイト送信すると、32バイト受信できました。(正常)

Fig. FTDI FT232RQ 送受信テスト (32byte)
使用したドライバ
Fig. ドライバのバージョン(CH340E)
Fig. ドライバのバージョン(FTDI FT232RQ)
テスト機材
Fig. テスト機材 CH340E(上) と FTDI FT232RQ(下)
2022年7月2日 追加修正分

「CH340Eの32バイト受信で、受信イベントが異常となる件」
 FTDIのドライバ設定で、BMオプション(待ち時間)を16[msec](デフォルト値)から、1[msec] に変更すると、FTDI FT232RQでも、CH340Eと同じ現象が発生しました。PCへのデータは、USBシリアル変換IC内のバッファがフルになると、すぐにPC側へ転送されますが、フルにならない場合は、待ち時間(latency time)後に転送されるそうです。待ち時間が最小で、特定のバイト数(32バイトまたは66バイト)を受信すると、1回多く受信イベントが発生してしまう原因は、OSのスケジューリングや.NET側の問題だと思います。

Fig. 32バイト受信で1回多く受信イベント発生 (FTDI FT232RQ使用)
Fig. FTDIドライバ BMオプション設定
Fig. FTDIドライババージョンとポートの設定(ポート設定→詳細設定でBMオプション変更)

・回避方法
プログラムを以下のように修正しました。
全てのデータを受信した事を示すフラグを作成します。全データ受信(表示処理の実行開始)でフラグをONします。
フラグがON状態で、再度受信イベントが発生したら、その受信イベントを処理せずにリターンします。
GitHubに C#XAMLを登録しました。
https://github.com/vABCWork/CommTest1

・環境:
Visual Studio Community 2022(64ビット)
.NET Framework Version 4.8
WPFアプリ(.NET Framework) の開発
Windows 11 Pro

・機材: Pmod USBUART ( FTDI FT232RQ)

・補足: CH340Eのドライバ( CH341SER.EXE) が更新されていました。

Fig. CH340Eの最新ドライバ