C#串口读取Arduino连续数据问题:每分钟读取时数据错位缺失
AGV Arduino与C#串口通信问题解决方案
问题根源
你用Timer定时读取4行数据的方式本质上是「主动轮询」,但Timer的触发时机和Arduino的发送节奏完全不匹配——Arduino连续发送数据时,Timer触发可能刚好卡在一组数据的中间,导致读取的4行不属于同一组,最终数据对应错误。正确的做法是用串口的DataReceived事件被动接收数据,再缓存处理完整的分组。
新手友好解决方案步骤
1. 准备全局变量
在Form类中定义全局变量,用于缓存未处理的数据和完整行:
private SerialPort sp; private string receivedBuffer = string.Empty; // 缓存所有未处理的串口数据 private List<string> dataLines = new List<string>(); // 存储拆分后的完整行
2. 替换Timer为DataReceived事件
在打开串口的代码中(比如按钮点击事件),绑定DataReceived事件,不再启动Timer:
private void btnOpenPort_Click(object sender, EventArgs e) { sp = new SerialPort("COM3", 9600); // 替换为你的串口名和波特率 sp.DataReceived += Sp_DataReceived; // 绑定数据接收事件 sp.Open(); // 注释掉原来的timer1.Start(); }
3. 实现DataReceived事件处理函数
这个函数会在串口有数据到达时自动触发,负责缓存、拆分完整行、分组处理:
private void Sp_DataReceived(object sender, SerialDataReceivedEventArgs e) { // 读取串口缓冲区所有数据 string newData = sp.ReadExisting(); // 追加到缓存 receivedBuffer += newData; // 拆分缓存为行(兼容Arduino的换行符) string[] lines = receivedBuffer.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); // 处理不完整的行:如果缓存末尾不是换行符,说明最后一行不完整,放回缓存 if (receivedBuffer.EndsWith(Environment.NewLine)) { receivedBuffer = string.Empty; dataLines.AddRange(lines); } else { // 只保留完整行,不完整的行放回缓存 for (int i = 0; i < lines.Length - 1; i++) { dataLines.Add(lines[i]); } receivedBuffer = lines[lines.Length - 1]; } // 凑够4行一组就处理 while (dataLines.Count >= 4) { // 取出一组4行数据 string s1 = dataLines[0]; string s2 = dataLines[1]; string s3 = dataLines[2]; string s4 = dataLines[3]; // 移除已处理的行,避免重复解析 dataLines.RemoveRange(0, 4); // 跨线程更新UI必须用Invoke(DataReceived在后台线程运行) this.Invoke(new Action(() => { try { string[] result_batt = s1.Split(','); string[] result_prox = s2.Split(','); string[] result_RPM = s3.Split(','); string[] result_speed = s4.Split(','); // 增加数组长度检查,避免索引越界 if (result_batt.Length >=4) get_Batt(result_batt[0], result_batt[1], result_batt[2], result_batt[3]); if (result_prox.Length >=4) get_prox(result_prox[0], result_prox[1], result_prox[2], result_prox[3]); if (result_RPM.Length >=2) get_RPM(result_RPM[0], result_RPM[1]); if (result_speed.Length >=2) get_speed(result_speed[0], result_speed[1]); } catch (Exception ex) { // 错误提示,避免程序崩溃 labelError.Text = $"解析错误:{ex.Message}"; } })); } }
4. 优化原有处理函数(增加稳定性)
电池信息处理(避免转换错误)
private void get_Batt(string s0, string s1, string s2, string s3) { string num = Regex.Replace(s1, "[%]", ""); if (int.TryParse(num, out int batt_val)) { // 确保进度条值在合法范围内 progressBar1.Value = Math.Clamp(batt_val, 0, progressBar1.Maximum); if (batt_val <= 0) { label2.Text = "Working time remaining: Unknown"; label3.Text = batt_val + "%"; } else { label2.Text = "Working time remaining: xx:xx:xx"; label3.Text = batt_val + "%"; } } }
接近传感器处理(去除空格干扰)
private void get_prox(string s0, string s1, string s2, string s3) { // 去除字符串前后空格,避免Arduino发送带空格的数据导致判断错误 s0 = s0.Trim(); s1 = s1.Trim(); s2 = s2.Trim(); s3 = s3.Trim(); label26.Text = s3; // 用else if避免重复判断 if (s3 == "0") { blackCircle4.Show(); greenCircle8.Hide(); } else if (s3 == "1") { blackCircle8.Show(); greenCircle4.Hide(); } if (s0 == "0") { blackCircle1.Show(); greenCircle5.Hide(); } else if (s0 == "1") { blackCircle5.Show(); greenCircle1.Hide(); } if (s1 == "0") { blackCircle2.Show(); greenCircle6.Hide(); } else if (s1 == "1") { blackCircle6.Show(); greenCircle2.Hide(); } if (s2 == "0") { blackCircle3.Show(); greenCircle7.Hide(); } else if (s2 == "1") { blackCircle7.Show(); greenCircle3.Hide(); } }
5. 清理Timer相关代码
删除原有的timer1_Tick函数,移除Form中的Timer控件即可。
关键注意事项
- 线程安全:DataReceived事件在后台线程运行,必须用
this.Invoke更新UI,否则会触发跨线程错误。 - 参数匹配:C#串口的波特率、数据位、停止位必须和Arduino代码完全一致(比如Arduino用
Serial.begin(9600);,C#也要设9600)。 - Arduino端优化(可选):可以给每组数据增加起始/结束标记,比如发送
START开头、END结尾,C#端可以先匹配标记再解析分组,进一步避免错位:Serial.println("START"); Serial.println("00.0V, 00.0%, 000, CL"); Serial.println("1,1,1,1"); Serial.println("140,140"); Serial.println("5,5"); Serial.println("END");
内容的提问来源于stack exchange,提问作者ZHLN
相关产品推荐
相关产品推荐

