65.9K
CodeProject 正在变化。 阅读更多。
Home

Arduino、C# 和串行接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (63投票s)

2014年4月22日

CPOL

11分钟阅读

viewsIcon

323862

downloadIcon

22795

通过 C# 和 System.IO.Ports 命名空间中的 SerialPort 类与 Arduino 板进行串行通信

Content

引言

Arduino 板提供了易于使用的环境来控制 LED 和电机等电子元件。我用 Arduino Mega 2560 微控制器板完成了这个项目,但您可以使用几乎任何官方 Arduino 板。

要下载并执行本文附加的源文件,您需要安装以下工具:

  • 一个 Arduino 板,通过 USB 连接到您的计算机
  • 官方 Arduino IDE(在此处下载 下载
  • 必须按照此处的说明安装 Arduino 板驱动程序。
  • Microsoft Visual Studio Express 2010、2012 或 2013

背景

我在 2013 年秋季对本文进行了全面重写,并添加了几个新示例、更好的解释和更多的背景信息。

什么是串行接口?

串行接口用于计算机和外围设备之间的信息交换。在使用串行通信时,信息会逐位(串行)通过电缆发送。现代串行接口包括以太网、Firewire、USB、CAN-Bus 或 RS-485,尽管它们不总是被称为“串行接口”。原理保持不变:信息在短距离内逐位传输。Glenn Patton 发布了一篇关于串行通信端口所用设计模式的精彩文章,它可以在 CodeProject 上找到:CodeProject

关于板

我使用 Arduino Mega 2560 板来测试和执行我的代码,尽管代码应该适用于大多数可用的 Arduino 设备 - 代码使用官方 Arduino IDE 编写,该 IDE 可以从 Arduino 项目页面免费下载,如本文开头所述。

工作原理

为了解释 C# 程序和 Arduino 板如何通信,我们将假设 Arduino 板有一个温度传感器,每天收集三次环境温度:一次在早上,一次在中午,一次在晚上。所有这些值都存储在 Arduino 板的 EEPROM 中。数据以以下格式存储:

11 06 2013 06:00=6.6
11 06 2013 12:00=11.78
11 06 2013 23:00=8.9
11 07 2013 06:00=4.54
11 07 2013 12:00=15.3
11 07 2013 23:00=8.6
11 21 2013 06:00=-2.33  

原始数据格式为 [日期时间]=[温度],温度值旨在以摄氏度(C)测量和存储。单个温度数据组由换行符分隔。

现在,为了专注于串行通信,我决定省略 EEPROM 的读写,而只是定义一组虚拟值,当收到请求温度数据的请求时发送这些值。我用 Arduino C++ 代码(我使用 Arduino 的 String 类)快速演示了单个数据集的外观。

String SingleSetOfData = "11 21 2013 06:00=-2.33"//Contains '11 21 2013 06:00=-2.33\0' 

鉴于日期时间(例如“11 21 2013 06:00”,代表 2013 年 11 月 21 日上午 06:00)最多可以有 15 个字符长,温度将涵盖从 -275.00 到 1999.999 度,以及分隔符('=')是另一个字符,加起来总长度为 23 个字符,而 `singleSetOfData` 需要占用 24 个字符,这是为什么?
简单问题,简单答案:编译器会在末尾添加一个终止字符 '\0',其空间也需要考虑在内 - `23 + 1 = 24`。


我不会详细介绍温度计的实现,因为互联网上有很多关于如何制作一个的说明集 - 我们将发送虚拟值。
但是,Arduino 板可能会接收到一条串行命令,告知它读取收集到的温度数据。我为每个命令分配了一个单独的命令代码,并将其定义为以 '#' 字符终止。因此,例如,告诉 Arduino 板发送已存储温度数据的命令将通过 Arduino 板的串行接口以这种方式传输,由将显示温度数据的 .NET 应用程序发出:

 1#

很简单。就是这样。Arduino 板收到此命令后,它将通过串行端口将我们之前定义的虚拟数据发送到 .NET 应用程序(当然,这需要 .NET 应用程序停止向 Arduino 板发送任何命令)。

[STX]
11 06 2013 12:00=11.78$
11 06 2013 23:00=8.9$
11 07 2013 06:00=4.54$
11 07 2013 12:00=15.3$
11 07 2013 23:00=8.6$
11 21 2013 06:00=-2.33[ETX]

每次传输都以 [STX] 代码开头,这是美国 ASCII 标准的开始传输文本的代码(代码“2”)。传输在发送 [ETX] 代码后结束,这是美国 ASCII 标准的结束传输文本的代码(代码“3”)。单个数据集用美元符号($)分隔。

使用代码

本章介绍的代码在 2013 年秋季被全面重写,以提供更有价值的学习资源,并匹配更少理论性的工作原理。

Arduino 源代码

错误、警告和状态代码

我在 Arduino 源代码中使用各种错误、警告和状态代码来指示方法调用是否成功,或导致了警告或错误。

 /*   WARNING, ERROR AND STATUS CODES                              */
//STATUS
#define MSG_METHOD_SUCCESS 0                      //Code which is used when an operation terminated  successfully
//WARNINGS
#define WRG_NO_SERIAL_DATA_AVAILABLE 250            //Code indicates that no new data is available at the serial input buffer
//ERRORS
#define ERR_SERIAL_IN_COMMAND_NOT_TERMINATED -1   //Code is used when a serial input commands' last char is not a '#' 

从串行输入读取命令

我创建了一个方法,该方法从 Arduino 板的串行输入缓冲区读取命令,并将其写入指向 Arduino String 的指针 - 返回一个整数值,表示操作成功或操作失败/警告。它返回以下三个代码之一:

  • MSG_METHOD_SUCCESS
    • 如果一切顺利,并且命令已写入 `command` 指针,则返回此代码。
  • WRG_NO_SERIAL_DATA_AVAILABLE
    • 如果在执行方法时串行输入缓冲区中没有数据,则返回此代码。
  • ERR_SERIAL_IN_COMMAND_NOT_TERMINATED
    • 如果在达到命令结束(即未以 '#' 字符终止)之前串行输入缓冲区中没有可用数据,则返回此代码。

实际上,与我在全面重写之前的文章和源代码相比,这是我唯一未作修改的方法。

此方法代码的构建如下:

默认返回代码 `operationStatus` 为 `MSG_METHOD_SUCCESS`。如果没有数据在串行输入缓冲区中(通过调用 Serial.Available() 检查,当串行输入缓冲区中有数据时返回 true),则将 `operationStatus` 指定为 `WRG_NO_SERIAL_DATA_AVAILABLE`。

int operationStatus = MSG_METHOD_SUCCESS;//Default return is MSG_METHOD_SUCCESS reading data from com buffer.
if (Serial.available()) {
     
}
else{//If not serial input buffer data is available, operationStatus becomes WRG_NO_SERIAL_DATA_AVAIBLE (= No data in the serial input buffer available)
    operationStatus = WRG_NO_SERIAL_DATA_AVAILABLE;
}   

如果串行输入缓冲区中有数据,程序将执行以下代码块:

char serialInByte;//temporary variable to hold the last serial input buffer character
     
do{//Read serial input buffer data byte by byte 
    serialInByte = Serial.read();
    *command = *command + serialInByte;//Add last read serial input buffer byte to *command pointer
}while(serialInByte != '#' && Serial.available());//until '#' comes up or no serial data is available anymore
     
if(serialInByte != '#') {
    operationStatus = ERR_SERIAL_IN_COMMAND_NOT_TERMINATED;
} 

现在发生的事情并不复杂,也很容易解释。我声明了变量 `serialInByte` 来存储 Serial.Read() 方法读取的单个字节(该方法从串行输入缓冲区读取一个字节)。我调用 Serial.Read() 方法,将返回的字节存储在 `serialInByte` 中,并将 `serialInByte` 追加到 `command` 指针 - 一直这样做,直到串行输入缓冲区中没有数据,或者出现 '#' 字符,表示命令结束。

`do`-`while` 循环之后的最后一个 if 语句检查最后一个读取的字符是否为 '#',如果不是,则将 `operationStatus` 设置为 `ERR_SERIAL_IN_COMMAND_NOT_TERMINATED`,因为这表示发生了传输错误。

响应串行命令

如您所知,Arduino 源代码有一个 `loop()` 方法,该方法以无限循环执行源代码。我将处理命令的代码直接放在该方法中:

 String command = "";  //Used to store the latest received command
 int serialResult = 0; //return value for reading operation method on serial in put buffer
  
 serialResult = readSerialInputCommand(&command); 

如您所见,我调用 `readSerialInputCommand` 从串行输入缓冲区读取命令字符串。方法的返回值存储在整数变量 `serialResult` 中,然后使用它来检测是否有一个命令可供读取。

if(serialResult == MSG_METHOD_SUCCESS){
    if(command == "1#"){//Request for sending weather data via Serial Interface
                        //For demonstration purposes this only writes dummy data
        WriteDummyWeatherData();
    }
} 

为了使此示例简单并专注于串行通信,我省略了任何数据存储/加载例程,而是通过调用 WriteDummyWeatherData() 将虚拟数据写回串行输出。

void WriteDummyWeatherData(){
  Serial.print(STX);
  Serial.print("11 06 2013 12:00=11.78");
  Serial.print(RS);
  Serial.print("11 06 2013 23:00=8.9");
  Serial.print(RS);
  Serial.print("11 07 2013 06:00=4.54");
  Serial.print(RS);
  Serial.print("11 07 2013 12:00=15.3");
  Serial.print(RS);
  Serial.print("11 07 2013 23:00=10.3");
  Serial.print(RS);
  Serial.print("11 21 2013 06:00=-2.33");
  Serial.print(ETX);
} 

当然,如果您确实想将实际的天气/任何其他数据发送到串行输出,则需要替换此方法调用,但对于演示目的来说,这已经足够了。

.NET 源代码

我实现了一个小型 WPF 演示应用程序,向您展示如何使用 .NET Framework 处理串行数据。

WeatherDataContainer 类

我有两个类帮助我组织与 Arduino 板的通信:WeatherDataItemWeatherDataContainer。虽然 WeatherDataItem 是一个单独的数据保存类(包含一个日期属性和一个温度属性),但 WeatherDataContainer(除了 WeatherDataItem 的列表外)还封装了与 Arduino 板的通信。

由于 GUI 只调用 WeatherDataContainer 类中可以找到的方法,因此我将重点放在它上面,而不是展开无聊的 GUI 代码。

WeatherDataContainer 有两个字段变量:

// Interface for the Serial Port at which an Arduino Board is connected.
SerialPort arduinoBoard = new SerialPort();

//Holds a List of WeatherData Items in order to store weather data received from an Arduino Board.
List<WeatherDataItem> weatherDataItems = new List<WeatherDataItem>(); 

在接下来的代码片段中,记住这些变量和事件非常重要,因为它们将被提及。

连接到 Arduino 板

来自 System.IO.Ports 命名空间的 SerialPort 类提供了 .NET 的串行通信功能。在发送或接收 SerialPort 实例的数据之前,您需要打开端口:

 public void OpenArduinoConnection()
 {
     if(!arduinoBoard.IsOpen)
     {
         arduinoBoard.DataReceived += arduinoBoard_DataReceived;
         arduinoBoard.PortName =  ConfigurationSettings.AppSettings["ArduinoPort"];
         arduinoBoard.Open();
      }
      else
      {
         throw new InvalidOperationException("The Serial Port is already open!");
      }
 } 

如您所见,我们添加了一个新的事件处理程序,该处理程序处理串行端口的 DataReceived 事件。当串行端口的输入缓冲区中有数据可用时,将触发此事件。

获取天气数据

您可以轻松地向 Arduino 板发送命令(我将命令封装起来以使代码更易读):

public void GetWeatherDataFromArduinoBoard()
{
   if (arduinoBoard.IsOpen)
   {
      arduinoBoard.Write("1#");
   }
   else
   {
      throw new InvalidOperationException("Can't get weather data if the serial Port is closed!");
   }
} 

在 Arduino 板收到此命令后,它将发送回天气数据并触发串行端口的 DataReceived 事件。

接收和解析串行数据

当串行端口的输入缓冲区中有数据可用时,将触发 DataReceived 事件,该事件由 WeatherDataContainer 类中的以下事件处理程序处理:

void arduinoBoard_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    string data = arduinoBoard.ReadTo("\x03");//Read until the EOT code
    //Split into 'date=temperature' formatted text
    string[] dataArray = data.Split(new string[] 
    {"\x02", "$" }, StringSplitOptions.RemoveEmptyEntries); 
    //Iterate through the split data and parse it into weather data items
    //and add them to the list of received weather data.
    foreach (string dataItem in dataArray.ToList())
    {
       WeatherDataItem weatherDataItem = new WeatherDataItem();
       weatherDataItem.FromString(dataItem);
       weatherDataItems.Add(weatherDataItem);
     }
     
     if(NewWeatherDataReceived != null)//If there is someone waiting for this event to be fired
     {
       NewWeatherDataReceived(this, new EventArgs()); //Fire the event, 
               // indicating that new WeatherData was added to the list.
     }
} 
关闭串行端口

强烈建议在不再使用 SerialPort 后关闭它。这可以通过在 SerialPort 对象上调用 Close() 方法来完成。

arduinoBoard.Close(); 

GUI

我选择了两种显示天气数据的方式:作为 DataGrid 和作为 3D 圆柱形图。两者都有其优点,尽管我更喜欢图表,因为它看起来更花哨。

在数据网格中显示数据

在数据网格中显示天气数据的一个优点是,如果您想以合理的方式显示大量数据。如果您只有十个或更少的数据项,以圆柱形图的形式显示天气数据是相当合理的。

上面网格的 XAML 代码看起来大致是这样的:

<DataGrid Name="weatherDataGrid" AutoGenerateColumns="False" Grid.Column="1">
  <DataGrid.Columns>
     <DataGridTextColumn Header="Date" 
     Width="1*" Binding="{Binding Date}">
     </DataGridTextColumn>
     <DataGridTextColumn Header="temperature (Celsius)" 
     Width="1*" Binding="{Binding temperatureCelsius}">
     </DataGridTextColumn>
  </DataGrid.Columns>
</DataGrid> 

该网格有两列,一列绑定到 WeatherDataItem 类的 Date 属性,另一列绑定到 temperatureCelsius 属性。

WeatherDataContainerNewWeatherDataReceived 事件处理程序被触发时,网格将被填充,并且通过 dispatcher 确保线程安全。

void weatherData_NewWeatherDataReceived(object sender, EventArgs e)
{
   Dispatcher.BeginInvoke(new ThreadStart(() => 
                          weatherDataGrid.ItemsSource = weatherData.WeatherDataItems)); 
在 3D 图表中显示数据

3D 图表位于应用程序窗口的不同选项卡上,并通过 DrawChart 方法直接绘制在 WPF Canvas 上。

private void DrawChart()
{
  List<double> temperatures = new List<double>();
  List<string> captions = new List<string>();
  foreach (WeatherDataItem item in weatherData.WeatherDataItems)
  {
    temperatures.Add(item.temperatureCelsius);
    captions.Add(string.Format("{0}\n{1} C", item.Date.ToShortDateString(), 
                 item.temperatureCelsius.ToString()));
  }
  ChartDataContainer container = new ChartDataContainer();
  container.Data = temperatures;
  container.DataCaptions = captions;
  container.MaxSize = new Point(800, 300);
  container.XAxisText = "Date / temperature (Celsius)";
  container.YAxisText = "";
  container.Offset = new Point(20, 20);
  container.ChartElementColor = Colors.BurlyWood;
  CylinderChartPainter.DrawChart(container, weatherChartCanvas);
} 

图表在 WeatherDataContainerNewWeatherDataReceived 事件处理程序被触发时绘制,并且通过 dispatcher 确保线程安全。

void weatherData_NewWeatherDataReceived(object sender, EventArgs e)
{
    Dispatcher.BeginInvoke(new ThreadStart(DrawChart));

Canvas 的 XAML 代码非常普通,它基本上只是一个填充 TabItem 的画布。

<TabItem Name="weatherChartTab" Header="3D Chart">
  <TabItem> 
    <Canvas Name="weatherChartCanvas">
                    
    </Canvas>
  </TabItem>
</TabControl> 

最后,会在 Canvas 上绘制一个看起来像下面图片中所示的图表的图表。

关注点

Arduino 在处理硬件和连接到计算机方面提供了巨大的可能性。最后但并非最不重要的是,在 官方 Arduino 主页上有 大量的社区支持。使用串行接口的可能性非常有趣:您可以发送大量数据,而且一点也不复杂。

对我来说,最好的部分是我能够做一些基本的 WPF 开发。在让 .NET 应用程序运行之前,我尝试了各种数据绑定和调用方法。

历史

  • 2012 年 10 月 10 日:创建文章
  • 2012 年 10 月 10 日:更新
  • 2012 年 10 月 12 日:增加了读取不同命令的功能
  • 2013 年 9 月 11 日:将许可证更改为 LGPL 3
  • 2013 年 11 月 23 日:全面重写 - 以新的、结构化的方法解决示例问题
  • 2013 年 11 月 26 日:更新为使用 3D 圆柱形图来显示天气数据
© . All rights reserved.