Arduino、C# 和串行接口






4.97/5 (63投票s)
通过 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 板的通信:WeatherDataItem
和 WeatherDataContainer
。虽然 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
属性。
当 WeatherDataContainer
的 NewWeatherDataReceived
事件处理程序被触发时,网格将被填充,并且通过 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);
}
图表在 WeatherDataContainer
的 NewWeatherDataReceived
事件处理程序被触发时绘制,并且通过 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 圆柱形图来显示天气数据