通过C#语言进行硬件串口通信与物联网设备控制编程实例插图

通过C#语言进行硬件串口通信与物联网设备控制编程实例

你好,我是源码库的博主。今天我们来聊聊一个在物联网和嵌入式开发中非常经典的话题:如何使用C#通过串口与硬件设备通信。无论是控制一个Arduino、读取一个传感器,还是与一个工业PLC对话,串口通信都是最直接、最可靠的方式之一。我曾在多个物联网项目中与各种“脾气古怪”的串口设备打过交道,积累了不少实战经验,也踩过不少坑。这篇教程,我将带你从零开始,构建一个稳定、实用的C#串口通信与控制程序。

一、 准备工作与环境搭建

首先,你需要一个硬件设备。为了演示,我们可以用一个最简单的Arduino Uno,烧录一个让LED灯闪烁并回传数据的示例程序。当然,任何支持串口指令控制的设备(如温湿度传感器、步进电机驱动器)原理都相通。

硬件侧(Arduino示例代码)

// Arduino 代码
void setup() {
  Serial.begin(9600); // 初始化串口,波特率9600
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  if (Serial.available() > 0) {
    char command = Serial.read();
    if (command == '1') {
      digitalWrite(LED_BUILTIN, HIGH);
      Serial.println("LED ON"); // 向C#程序发送状态
    } else if (command == '0') {
      digitalWrite(LED_BUILTIN, LOW);
      Serial.println("LED OFF");
    } else if (command == '?') {
      Serial.println("READY"); // 查询设备状态
    }
    // 清空缓冲区
    while(Serial.available() > 0) { Serial.read(); }
  }
}

软件侧:在Visual Studio中创建一个新的C# Windows窗体应用(.NET Framework 或 .NET Core/.NET 6+ 均可,我们使用经典的System.IO.Ports)。项目创建好后,从工具箱拖拽必要的控件:ComboBox(用于选择串口)、Buttons(打开、关闭、发送)、TextBox(发送指令和显示接收数据)、RichTextBox(用于显示接收日志,比TextBox更适合多行文本)。

二、 核心代码实现:串口初始化和数据收发

这是整个程序的心脏。我们首先要在窗体类中声明一个SerialPort对象。

using System.IO.Ports;

public partial class MainForm : Form
{
    private SerialPort mySerialPort = new SerialPort();

    public MainForm()
    {
        InitializeComponent();
        // 初始化串口参数(默认,实际参数会在打开时设置)
        mySerialPort.BaudRate = 9600;
        mySerialPort.Parity = Parity.None;
        mySerialPort.DataBits = 8;
        mySerialPort.StopBits = StopBits.One;
        mySerialPort.Handshake = Handshake.None;

        // 关键!绑定数据接收事件处理函数
        mySerialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPort_DataReceived);
    }
}

踩坑提示1DataReceived事件是在一个非UI线程中触发的。如果你直接在事件处理函数里更新UI控件(比如把收到的文本写到RichTextBox),会引发跨线程异常。这是新手最容易掉进去的坑。我们必须通过InvokeBeginInvoke来安全地更新UI。

private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    // 读取所有可用数据
    string receivedData = mySerialPort.ReadExisting();

    // 使用UI线程委托更新接收显示框
    this.BeginInvoke(new Action(() =>
    {
        // 追加数据,并自动滚动到底部
        rtbReceived.AppendText(receivedData);
        rtbReceived.ScrollToCaret();
    }));
}

接下来,我们实现扫描可用串口、打开和关闭串口的功能。

// 窗体加载时或点击“刷新”按钮时扫描串口
private void RefreshPortList()
{
    string[] ports = SerialPort.GetPortNames();
    cmbPortName.Items.Clear();
    cmbPortName.Items.AddRange(ports);
    if (ports.Length > 0)
        cmbPortName.SelectedIndex = 0;
}

// 打开串口按钮点击事件
private void btnOpen_Click(object sender, EventArgs e)
{
    if (!mySerialPort.IsOpen)
    {
        try
        {
            mySerialPort.PortName = cmbPortName.SelectedItem.ToString();
            mySerialPort.Open();
            btnOpen.Enabled = false;
            btnClose.Enabled = true;
            AppendLog($"串口 {mySerialPort.PortName} 已打开。");
        }
        catch (Exception ex)
        {
            MessageBox.Show($"打开串口失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}

// 关闭串口
private void btnClose_Click(object sender, EventArgs e)
{
    if (mySerialPort.IsOpen)
    {
        mySerialPort.Close();
        btnOpen.Enabled = true;
        btnClose.Enabled = false;
        AppendLog("串口已关闭。");
    }
}

// 一个简单的日志方法
private void AppendLog(string message)
{
    rtbReceived.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}n");
    rtbReceived.ScrollToCaret();
}

三、 发送指令与控制设备

发送指令相对简单,但要注意异常处理和发送时机。

// 发送按钮点击事件
private void btnSend_Click(object sender, EventArgs e)
{
    if (mySerialPort.IsOpen && !string.IsNullOrWhiteSpace(txtSend.Text))
    {
        try
        {
            // 注意:WriteLine 会发送字符串并附加换行符(如n),具体取决于设备的协议要求
            // 有些设备需要rn,有些只要n,有些什么换行都不要。这需要根据你的设备手册调整。
            // 这里我们使用Write,发送原始字符串,由用户在输入时自己控制格式。
            mySerialPort.Write(txtSend.Text);
            AppendLog($"发送: {txtSend.Text}");
            txtSend.Clear();
        }
        catch (Exception ex)
        {
            AppendLog($"发送失败: {ex.Message}");
        }
    }
    else
    {
        MessageBox.Show("请先打开串口并输入指令。");
    }
}

踩坑提示2:协议与编码。串口通信是字节流。C#的Write(string)方法会使用SerialPort.Encoding属性(默认是ASCII)将字符串转换为字节发送。如果你的设备使用非ASCII字符(比如中文或特殊二进制协议),务必设置正确的编码(如UTF-8、GB2312),或者直接使用Write(byte[] buffer, ...)方法发送字节数组。同样,在接收侧,ReadExisting()返回的是根据编码转换的字符串。对于二进制数据,应使用Read(byte[], ...)ReadByte()

四、 实战优化与稳定性增强

一个健壮的工业级串口程序远不止于此。以下是几个提升稳定性的要点:

1. 超时与重试机制:设置SerialPort.ReadTimeoutWriteTimeout,避免程序在设备无响应时死锁。对于关键指令,可以实现一个简单的“发送-确认-重试”循环。

public bool SendCommandWithAck(string command, string expectedAck, int retries = 3)
{
    for (int i = 0; i < retries; i++)
    {
        mySerialPort.DiscardInBuffer(); // 清空输入缓冲区,避免旧数据干扰
        mySerialPort.Write(command);
        Thread.Sleep(100); // 给设备一点响应时间,根据实际情况调整

        string response = mySerialPort.ReadExisting();
        if (response.Contains(expectedAck))
        {
            AppendLog($"指令 {command} 执行成功,响应: {response}");
            return true;
        }
        AppendLog($"第{i+1}次尝试失败,响应: {response}");
    }
    AppendLog($"指令 {command} 重试{retries}次后失败。");
    return false;
}

2. 连接状态监控:可以通过定时发送心跳包(如?指令)并检查回复来判断设备是否在线。

3. 数据解析:对于复杂的返回数据(如“TEMP:25.6,HUM:60”),需要在接收事件中实现一个状态机或使用正则表达式进行解析,并将解析后的数据更新到UI的特定控件(如仪表盘、标签)。

4. 资源释放:务必在窗体关闭事件中关闭并释放串口对象。

private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (mySerialPort.IsOpen)
    {
        mySerialPort.Close();
    }
    mySerialPort.Dispose();
}

五、 总结与扩展

通过以上步骤,我们已经完成了一个具备基本收发、状态显示和简单控制功能的C#串口通信程序。它已经可以应对很多物联网原型开发和设备调试场景。

你可以在此基础上进行扩展:

  • 界面美化:将接收到的传感器数据用图表(如ScottPlot或LiveCharts)实时绘制出来。
  • 协议封装:为你的特定设备编写一个专门的通信类库,封装指令集,让业务逻辑代码更清晰。
  • 多线程与队列:对于高速或并发的数据收发,可以考虑使用生产者-消费者模型,将接收到的原始数据放入队列,由后台线程专门处理,避免阻塞UI或数据丢失。
  • 跨平台:如果你需要跨平台(如Linux),可以关注 .NET 的 System.IO.Ports 在非Windows平台上的支持,或者使用第三方库如 SerialPortStream。

串口通信就像与设备说一门古老而直接的语言,虽然底层,却无比强大和可靠。希望这篇教程能帮你顺利打开硬件世界的大门。编程愉快,如果在实践中遇到问题,欢迎来源码库交流讨论!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。