
通过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);
}
}
踩坑提示1:DataReceived事件是在一个非UI线程中触发的。如果你直接在事件处理函数里更新UI控件(比如把收到的文本写到RichTextBox),会引发跨线程异常。这是新手最容易掉进去的坑。我们必须通过Invoke或BeginInvoke来安全地更新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.ReadTimeout和WriteTimeout,避免程序在设备无响应时死锁。对于关键指令,可以实现一个简单的“发送-确认-重试”循环。
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。
串口通信就像与设备说一门古老而直接的语言,虽然底层,却无比强大和可靠。希望这篇教程能帮你顺利打开硬件世界的大门。编程愉快,如果在实践中遇到问题,欢迎来源码库交流讨论!

评论(0)