概要
USBシリアル変換IC CH340Eの動作確認のため、C#,XAMLを使用して、シリアル通信テスト用のWPFアプリ(.NET Framwork)を作成しました。
送信データをループバックして受信するテストですが、特定のバイト数(32byteや64byte)を送信すると、受信イベントが正常に終了できませんでした。
プログラムの作り方に問題があるのか、ドライバの問題か判定できませんが、同じプログラムを、FTDI社のFT232RQ で試したところ、正常に処理されました。
CH340Eは比較的最近のICなので、使い方に工夫が必要だと思いました。
・2022年7月2日 修正:
FT232RQでもCH340Eと同じ現象が確認されました。OSのスケジューリングや.NET側の問題のようです。テスト結果を追加しました。
外観
処理概要
入力した送信バイト数分のデータ(0x00より)を送信し、送信バイト数のデータを受信すると、受信データを表示します。
通信条件は、固定で、コードで記述しています。
プログラム
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バイト受信できました。(正常)
2) CH340E へ 32バイト送信すると、受信処理がうまくできませんでした。(受信終了後、再度、受信イベント発生)(異常)
3) FTDI FT232RQ へ32バイト送信すると、32バイト受信できました。(正常)
使用したドライバ
テスト機材
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側の問題だと思います。
・回避方法
プログラムを以下のように修正しました。
全てのデータを受信した事を示すフラグを作成します。全データ受信(表示処理の実行開始)でフラグを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) が更新されていました。