核心是避免write(string)将十六进制字符串误转为ascii字节,必须用write(byte[], int, int)发送经正确解析(去空格、校验、两位一转)的字节数组。

串口通信发 16 进制数据,核心不是“怎么转字符串”,而是“别让 Write(string) 悄悄给你转成 ASCII 字节”。只要走错这一步,接收端看到的永远是 0x30 0x31 0x20 0x30 0x33(即 `"01 03"` 的 ASCII 编码),而不是你想要的 0x01 0x03。
如何正确枚举可用串口(SerialPort.GetPortNames())
这是所有操作的前提,但容易被忽略权限或延迟问题:
-
SerialPort.GetPortNames()返回的是当前系统识别到的 COMx 名称数组,不检查物理连接状态,也不触发驱动加载 —— 插着没通电的 USB 转串口设备也可能出现在列表里 - 在 WinForms 中建议放在窗体
Load事件里调用,但首次调用可能因系统缓存返回空数组;可加一次Thread.Sleep(100)后重试,或绑定到SystemEvents.PowerModeChanged事件监听热插拔 - 某些虚拟串口(如 VSPE、com0com)或蓝牙串口可能需要管理员权限才能列出,普通用户账户下会静默跳过
怎么把 "01 03 00 FF" 这类字符串安全转成 byte[]
不能直接 Encoding.ASCII.GetBytes("01 03"),那是错的。必须剥离空格、校验合法性、按两位解析:
- 先用
.Replace(" ", "").Replace("0x", "").ToUpper()清理格式,得到纯十六进制字符序列 - 用正则
^[0-9A-F]{2,}$校验 —— 注意必须是偶数长度,否则Substring(x,2)会越界 - 推荐用
Convert.ToByte(str, 16)解析每两位,比int.Parse(..., NumberStyles.HexNumber)更轻量且明确只接受两位 - 示例片段:
string input = "01 03 00 FF";<br>string clean = input.Replace(" ", "").ToUpper();<br>if (clean.Length % 2 != 0 || !Regex.IsMatch(clean, "^[0-9A-F]{2,}$"))<br> throw new ArgumentException("非法十六进制字符串");<br>byte[] data = Enumerable.Range(0, clean.Length / 2)<br> .Select(i => Convert.ToByte(clean.Substring(i * 2, 2), 16))<br> .ToArray();
发送时为什么不能用 Write(string)?
因为 SerialPort.Write(string) 默认按当前 SerialPort.Encoding(通常是 UTF-8 或 ASCII)编码字符串,把每个字符当文本处理 —— 输入 "FF" 发出去的是 0x46 0x46('F' 的 ASCII 码),不是 0xFF。
- 必须用
Write(byte[], int, int)重载,传入真实字节数组 - 如果从 TextBox 读取,务必确认用户输入的是十六进制字符串(如
"A1B2"),而不是想当然地把"A1B2"当作要发的原始字节流 - 常见错误:用户输入
"0x01,0x03",代码没做预处理就直接切分,结果Convert.ToByte("0x01", 16)报异常 ——Convert.ToByte不支持0x前缀,得先.Replace("0x", "")
实际发包前最容易漏掉的三件事
硬件协议往往比代码逻辑更苛刻:
- CRC/校验位:很多工业设备要求帧尾带 CRC16,例如 Modbus RTU。光发
01 03 00 00 00 01不行,得补上计算出的两个字节,否则设备静默丢弃 - 最小间隔:RS-485 半双工场景下,发送完一帧后需延时(如 3.5 字符时间)再发下一帧,否则从机收不到完整命令 ——
Thread.Sleep不可靠,要用基于波特率的精确等待 - 接收缓冲区溢出:如果对方连续发数据,而你只靠
DataReceived事件被动读,BytesToRead可能瞬间飙高导致丢包;必须配合定时器或环形缓冲区做粘包处理,不能依赖单次Read
真正卡住人的从来不是“怎么发”,而是“发过去之后对方认不认” —— 协议细节、电气特性、时序约束,这些不会报编译错误,但会让数据看起来“发了,又像没发”。











