ArduinoとPCの間のデータ通信の方法

ArduinoとPCの間のデータ通信はシリアル通信で行っています。
シリアル通信というのは、1本の通信線でデータを1ビットずつ順番に送信する伝送方式のことを言います。
ArduinoとPCといったデバイス間の通信はもちろんのこと、基板上のIC間の通信としてもよく使用されています。
例えば、通信線の電圧が5Vなら“1”、0Vなら“0”のように取り決めれば、特定の周期で電圧を変えることで情報を伝えることができます。これが基本的なシリアル通信の仕組みです。
そして、ArduinoーPCの間の通信は、その中でも『USBデータ通信』という規格でデータ伝送を行っています。
USBデータ通信の特長として特筆すべきは、通信線がD+とD-の2つ用意されており、これらの差動電圧でデータを伝達する点です。

USBデータ通信ではD+とD-の電圧差に応じて論理を決定しています(下式参照)。
\begin{eqnarray}
\left\{
\begin{array}{l}
{\rm 論理が1}\ \ (D_{+}\ – \ D_{-} \ >\ 0.2\ {\rm V}の場合) \\
{\rm 論理が0}\ \ \ (D_{+}\ – \ D_{-} \ <\ -0.2\ {\rm V}の場合)
\end{array}
\right.
\end{eqnarray}
これであれば、もし外部からUSBケーブルにノイズが乗ったとしても、D+とD-の電圧の差分でノイズの影響を消し去ることができるのです。
したがって、ケーブル経由でデータを転送する場合でもデータ欠損が少なく済みます。

Arduinoにはマイコンが搭載されており、シリアル通信ではもちろんこのマイコンが送受信するわけなのですが、実は一部を除きArduinoボードに搭載されているマイコンのほとんどは直接USBデータ通信ができません。
マイコンに実装されている機能はUART通信ですので、USBデータ通信の規格に変換してあげる必要があります。そのため、ほとんどのArduinoボードにはUSBシリアル変換ICというものが搭載されています。このICでUART通信⇔USBデータ通信の相互変換を行っています。
(2024年現在、Arduino UNO R4のようにマイコンにUSBシリアル変換ICの機能が搭載されているものもあります。)

Arduinoにおいては、『ByteデータまたはASCII文字』でデータを転送します。
そのうち特によく使うのはASCII文字かと思います。
例えば、送信側は‘A’という文字を一度ASCII変換(エンコード)して1バイトの16進数にし、電気信号として受信側へ送ります。そして、受信側は受け取ったバイトデータをASCII文字に戻して(デコードして)‘A’と認識します。
ArduinoとPCのデータ通信プログラム
シリアル通信でArduinoからPCへ文字列を送信するプログラムを作成します。
Arduino側から1秒毎にシリアル通信で経過秒数を送ってくれています。

こちらがArduino側のプログラムです。
最初にsetup()でシリアル通信のボーレートを9600bpsに設定し、loop()でASCII文字の経過秒数を1秒毎に送信しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <Arduino.h> void setup(void) { // 通信ボーレートを9600bpsに設定 Serial.begin(9600); } void loop(void) { unsigned long second; String message; message = "It's passed "; second = millis() / 1000; // msec⇒sec変換 message.concat(second); // It's passed + (秒数) message.concat(" second."); // It's passed + (秒数) + second. Serial.println(message.c_str()); // string型⇒char型変換 delay(1000); // 1000ms待つ } |
一方のWPF側のプログラムです。なお、今回もPrismを利用してMVVMで作成しています。MVVMの詳細やPrismのインストール方法は以下をご参考に。
また、WPFで開発をするときに「.NET」か「.NET Framework」かを選択する必要があります。
今後の長期的サポートを考えると「.NET」で開発することになることがほとんどだと思いますが、「.NET」でSerialPortクラスを使用する際は以下のことにご留意ください。
.NET Frameworkではシリアル通信で使用する「SerialPort」クラスがいつでも使用できる状態にありましたが、後継の.NETではNuGetパッケージマネージャーでパッケージをインストールしないと使えません。
そのため、まずはSerialPortクラスが入っているSystem.IO.Portsパッケージをインストールしてください。
『参照』を押し、検索ボックスに「system.io.ports」と入力します。
検索結果にある『System.IO.Ports』を選択し、右側にある現在のプロジェクト名のチェックボックスにチェックを入れます。
そのうえで『インストール』を押します。
これでSystem.IO.Portsのインストールは完了です。
USBデータ通信で受信したArduinoからのメッセージ(経過秒数)をTextBoxに表示するだけです。
なお、よくある実装パターンとしてCOMポートをComboBoxから選択し、接続または切断できるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<Window x:Class="SerialComm_Sample.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True" Title="Arduinoシリアル通信テストアプリ" Height="350" Width="525" > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="3*" /> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="1*" /> </Grid.ColumnDefinitions> <!--行の定義--> <Grid.RowDefinitions> <RowDefinition Height="1*" /> <RowDefinition Height="3*" /> </Grid.RowDefinitions> <StackPanel Orientation="Vertical" HorizontalAlignment="Left" Margin="10,0,0,0"> <Label Grid.Column="0" Grid.Row="0" Content="Arduinoシリアル通信テストアプリ" FontSize="18" FontWeight="Bold" Margin="5" HorizontalAlignment="Left" VerticalAlignment="Center" /> <!-- PortListの内容をItemsSourceにバインディングし、選択中のアイテムはSelectedPortに反映 --> <ComboBox HorizontalAlignment="Left" Width="180" ItemsSource="{Binding PortList}" SelectedItem="{Binding SelectedPort}"/> </StackPanel> <!-- バインディングによって接続ボタンが押下されたらConnectCommandが発行される --> <Button Grid.Column="1" Grid.Row="0" Command="{Binding ConnectCommand}" Content="接続" FontSize="14" FontWeight="Bold" Margin="5" /> <!-- バインディングによって切断ボタンが押下されたらDisonnectCommandが発行される --> <Button Grid.Column="2" Grid.Row="0" Command="{Binding DisconnectCommand}" Content="切断" FontSize="14" FontWeight="Bold" Margin="5" /> <!-- バインディングによってRecvMessageの内容がTextBoxに反映される --> <TextBox Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="3" VerticalScrollBarVisibility="Visible" Background="LightGray" Margin="10" Text="{Binding RecvMessage}"/> </Grid> </Window> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
using Prism.Commands; using Prism.Mvvm; using SerialComm_Sample.Models; using System.Collections.ObjectModel; namespace SerialComm_Sample.ViewModels { public class MainWindowViewModel : BindableBase { private MessageProvider m_MessageProvider = null; private SerialPortWrapper m_SerialPortWrapper = null; // View側のCommandを受け取るためのDelegateCommand public DelegateCommand ConnectCommand { get; } public DelegateCommand DisconnectCommand { get; } // COMポートリスト public ObservableCollection<string> PortList { get { return m_SerialPortWrapper.PortList; } set { m_SerialPortWrapper.PortList = value; } } // 現在選択されているCOMポート名(文字列) public string SelectedPort { get { return m_SerialPortWrapper.SelectedPort; } set { m_SerialPortWrapper.SelectedPort = value; } } // 受信メッセージ(Model側のプロパティをパススルー) public string RecvMessage { get { return m_MessageProvider.RecvMessage; } set { m_MessageProvider.RecvMessage = value; } } // コンストラクタ public MainWindowViewModel( ) { // Modelのインスタンスを生成 m_MessageProvider = new MessageProvider( ); m_SerialPortWrapper = new SerialPortWrapper( m_MessageProvider ); // Model側でPropertyChangedイベントが発生したら、ViewModelでRaisePropertyChanged()を呼び出す // なお、Model側とViewModelでプロパティ名を一致させたため、イベント発生元のModelでのプロパティ名(e.PropertyName)を // そのままRaisePropertyChanged()に入れられる m_MessageProvider.PropertyChanged += ( sender, e ) => RaisePropertyChanged( e.PropertyName ); m_SerialPortWrapper.PropertyChanged += ( sender, e ) => RaisePropertyChanged( e.PropertyName ); // View側でCommandが発行されたら、ViewModelのprivateメソッドで処理する ConnectCommand = new DelegateCommand( Connect_Click ); DisconnectCommand = new DelegateCommand( Disconnect_Click ); } // ConnectCommand発行時の処理 private void Connect_Click( ) { // ModelのConnect()を呼び出す m_SerialPortWrapper.Connect( ); } // DisconnectCommand発行時の処理 private void Disconnect_Click( ) { // ModelのDisconnect()を呼び出す m_SerialPortWrapper.Disconnect( ); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
using Prism.Mvvm; using System; using System.Collections.ObjectModel; using System.IO.Ports; using System.Linq; using System.Text; using System.Windows; namespace SerialComm_Sample.Models { public class SerialPortWrapper : BindableBase { private SerialPort m_SerialPort = null; private ObservableCollection<string> m_PortList = new(); // USBデータ通信で使用するCOMポートのリスト public ObservableCollection<string> PortList { get { return m_PortList; } set { SetProperty( ref m_PortList, value ); } // set時にSetProperty()により変更通知イベントが発生する } // 現在選択しているCOMポート(文字列) private string m_SelectedPort; public string SelectedPort { get { return m_SelectedPort; } set { SetProperty( ref m_SelectedPort, value ); } // set時にSetProperty()により変更通知イベントが発生する } // コンストラクタ public SerialPortWrapper( MessageProvider p_MessageProvider ) { // シリアルポートの初期設定 m_SerialPort = new SerialPort( ); m_SerialPort.BaudRate = 9600; // 9600bpsに設定 m_SerialPort.DataBits = 8; // データビットは8bits m_SerialPort.Parity = Parity.None; // パリティなし m_SerialPort.Encoding = Encoding.UTF8; // 文字コードはUTF-8 m_SerialPort.WriteTimeout = 5000; // 書き込みタイムアウト:5000ms m_SerialPort.ReadTimeout = 5000; // 読み込みタイムアウト:5000ms // USBデータ通信の受信イベントの処理を記述(処理は別スレッドで行われる) m_SerialPort.DataReceived += ( sender, e ) => { try { // 受信メッセージがあれば取得する string message = m_SerialPort.ReadExisting(); // 別スレッドで処理した受信メッセージをメインスレッドに渡すためInvoke Application.Current.Dispatcher.Invoke( ( ) => { // MessageProviderにメッセージを渡して画面表示してもらう p_MessageProvider.SetMessage( message ); } ); } catch ( Exception ex ) // 例外処理 { // もし例外が発生したらメッセージボックスで表示する MessageBox.Show( ex.Message ); } }; // 現在有効なシリアル通信のCOMポートを取得してPortListに追加する foreach ( var port in SerialPort.GetPortNames( ) ) { PortList.Add( port ); } // PortListの最初の項目がComboBoxに表示されるよう設定する SelectedPort = PortList.FirstOrDefault( ); } // 接続コマンドが発行されたときの処理 public void Connect( ) { try { // SelectedPortをUSBデータ通信のポートに設定する m_SerialPort.PortName = SelectedPort; // ポートを開く m_SerialPort.Open( ); } catch ( Exception ex ) // 例外処理 { MessageBox.Show( ex.Message ); } } // 切断コマンドが発行されたときの処理 public void Disconnect( ) { try { // ポートを閉じる m_SerialPort.Close( ); } catch ( Exception ex ) // 例外処理 { MessageBox.Show( ex.Message ); } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using Prism.Mvvm; namespace SerialComm_Sample.Models { public class MessageProvider : BindableBase { // 受信メッセージを格納するもの private string m_RecvMessage = ""; public string RecvMessage { get { return m_RecvMessage; } set { SetProperty( ref m_RecvMessage, value ); } } // RecvMessageの末端に文字列を追加していく public void SetMessage( string p_Message ) { RecvMessage = string.Concat( m_RecvMessage, p_Message ); } } } |
ざっとこんな感じです。