关于如何进行串口通讯,可以查阅另一篇博客 Unity 串口通讯,本文介绍的是如何处理传的数据,这里提供一个十六进制数据的案例,供大家去学习。
数据案例 AA015500AA021407EEFF
数据说明
下标 | 完整数值 | 含义 | 规则 |
---|---|---|---|
0 | 0xAA | 固定数据头 1 | 固定为 0xAA(帧起始标识,区分数据帧与干扰信号) |
1 | 0x01 | 帧序号 | 从 0x01 开始自增 |
2 | 0x55 | 固定数据头 2 | 固定为 0x55(二次校验标识,与数据头 1 配合确认帧合法性) |
3 | 0x01 | 温度数据高位 | 高位 |
4 | 0xAA | 温度数据低位 | 低位 |
5 | 0x02 | 湿度数据高位 | 高位 |
6 | 0x14 | 湿度数据低位 | 低位 |
7 | 0x07 | 按键值 | 按键1为1,按键2为2,按键3为4等,按位标识按键 |
8 | 0xEE | 结束标识(高位) | 固定为 0xEE(帧结束高位,与低位配合确认帧完整性) |
9 | 0xFF | 结束标识(低位) | 固定为 0xFF(帧结束低位) |
先完成准备工作,串口通信中常用的 “高低位字节组合”,传2个8位16进制值,高位字节和低位字节,组合成一个16进制的数值,高位 0x01,低位0xAA
合并原理
高位字节左移8位:将 0x01 左移8位,得到 0x0100(即二进制 00000001 00000000)。
与低位字节相加:将移位后的结果与 0xAA 相加(或按位或 |),得到 0x01AA(二进制 00000001 10101010)。
转换为十进制:0x01AA = 1 × 256 + 10 × 16 + 10 × 1 = 426。
以此,用C#来实现
/// <summary>
/// 将高位和低位十六进制值组合成16位有符号整数
/// </summary>
public static float CombineHighLowBytes(string highByteHex, string lowByteHex)
{
byte highByte = Convert.ToByte(highByteHex, 16);
byte lowByte = Convert.ToByte(lowByteHex, 16);
int combinedValue = (highByte << 8) | lowByte;
return combinedValue;
}
拓展一下需要的静态方法,比如收到的字节数组转化,字符分割,字符校验
/// <summary>
/// 将字节数组转换为十六进制字符串
/// </summary>
public static string BytesToHexString(byte[] bytes)
{
if (bytes == null) return string.Empty;
StringBuilder sb = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
{
sb.AppendFormat("{0:X2}", b);
}
return sb.ToString();
}
/// <summary>
/// 将字符串分割为两个字符一组的数组
/// </summary>
public static string[] SplitIntoTwoCharParts(string input)
{
if (string.IsNullOrEmpty(input))
{
return Array.Empty<string>();
}
List<string> parts = new List<string>();
for (int i = 0; i < input.Length; i += 2)
{
int length = (i + 2 <= input.Length) ? 2 : input.Length - i;
parts.Add(input.Substring(i, length));
}
return parts.ToArray();
}
/// <summary>
/// 检查字符串是否是有效的十六进制值
/// </summary>
public static bool IsHexString(string str)
{
return str.All(c => char.IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));
}
根据提供的解析规则,处理数据包,解析除了按键之外的数据
private const string PACKET_HEAD1 = "AA"; // 固定数据头1
private const string PACKET_HEAD2 = "55"; // 固定数据头2
private const string PACKET_TAIL_HIGH = "EE"; // 结束标识高位
private const string PACKET_TAIL_LOW = "FF"; // 结束标识低位
private const int EXPECTED_PACKET_PARTS = 10; // 预期10个数据部分
private void ProcessPacket(string packet)
{
string[] parts = DataConversionUtility.SplitIntoTwoCharParts(packet);
if (parts.Length != EXPECTED_PACKET_PARTS)
{
Debug.LogError($"数据位数解析异常,期望{EXPECTED_PACKET_PARTS}个数据,实际{parts.Length}个");
return;
}
try
{
// 验证数据头
if (parts[0] != PACKET_HEAD1 || parts[2] != PACKET_HEAD2)
{
Debug.LogError($"数据头验证失败,预期 {PACKET_HEAD1} 和 {PACKET_HEAD2},实际 {parts[0]} 和 {parts[2]}");
return;
}
// 验证数据尾
if (parts[8] != PACKET_TAIL_HIGH || parts[9] != PACKET_TAIL_LOW)
{
Debug.LogError($"数据尾验证失败,预期 {PACKET_TAIL_HIGH} 和 {PACKET_TAIL_LOW},实际 {parts[8]} 和 {parts[9]}");
return;
}
// 验证所有部分都是有效的十六进制
if (!parts.All(DataConversionUtility.IsHexString))
{
Debug.LogError("数据包包含无效的十六进制值");
return;
}
// 解析帧序号 (索引1)
byte frameSequence = Convert.ToByte(parts[1], 16);
OnFrameSequenceUpdated?.Invoke(frameSequence);
// 解析温度数据 (索引3-4)
float temperature = DataConversionUtility.CombineHighLowBytes(parts[3], parts[4]);
// 解析湿度数据 (索引5-6)
float humidity = DataConversionUtility.CombineHighLowBytes(parts[5], parts[6]);
// 触发相应事件
OnTemperatureUpdated?.Invoke(temperature);
OnHumidityUpdated?.Invoke(humidity);
// 输出日志
Debug.Log($"帧序号:{frameSequence},温度:{temperature},湿度:{humidity}");
}
catch (Exception ex)
{
Debug.LogError($"处理数据包时发生错误: {ex.Message}");
}
}
按键单独处理,先设定按键的常用状态
public enum KEYButtonState
{
None,
OnButtonDown,
OnButtonUp,
OnButtonClick
}
需要制定一个按键状态管理类,负责追踪和管理按键的状态变化,需要注意的是按键抖动和超时问题。
/// <summary>
/// 按键状态管理器,负责跟踪和管理按键状态变化
/// </summary>
public class KeyStateManager
{
// 按键状态存储
private Dictionary<int, KEYButtonState> _keyStates = new Dictionary<int, KEYButtonState>();
// 上一次按下的按键记录
private HashSet<int> _lastPressedKeys = new HashSet<int>();
// 按键状态变化事件
public event Action<int, KEYButtonState> OnKeyStateChanged;
public KeyStateManager()
{
// 初始化1-8号按键状态为None
for (int i = 1; i <= 8; i++)
{
_keyStates[i] = KEYButtonState.None;
}
}
/// <summary>
/// 更新按键状态并触发相应事件
/// </summary>
public void UpdateKeyStates(List<int> currentPressedKeys)
{
HashSet<int> currentKeys = new HashSet<int>(currentPressedKeys);
// 检查所有按键的状态变化
for (int key = 1; key <= 8; key++)
{
bool wasPressed = _lastPressedKeys.Contains(key);
bool isPressed = currentKeys.Contains(key);
KEYButtonState newState = _keyStates[key];
// 状态转换逻辑
if (!wasPressed && isPressed)
{
newState = KEYButtonState.OnButtonDown;
}
else if (wasPressed && !isPressed)
{
newState = KEYButtonState.OnButtonUp;
}
// 触发状态变化事件
if (newState != _keyStates[key])
{
_keyStates[key] = newState;
OnKeyStateChanged?.Invoke(key, newState);
// 当按键松开时,触发点击事件
if (newState == KEYButtonState.OnButtonUp)
{
OnKeyStateChanged?.Invoke(key, KEYButtonState.OnButtonClick);
}
}
}
// 保存当前按键状态用于下次比较
_lastPressedKeys = new HashSet<int>(currentKeys);
// 重置瞬间状态
ResetTransientStates();
}
/// <summary>
/// 重置瞬间状态为None,避免持续触发
/// </summary>
private void ResetTransientStates()
{
foreach (int key in _keyStates.Keys.ToList())
{
if (_keyStates[key] != KEYButtonState.None)
{
_keyStates[key] = KEYButtonState.None;
}
}
}
/// <summary>
/// 处理超时情况,将所有按键视为松开
/// </summary>
public void HandleTimeout()
{
if (_lastPressedKeys.Count > 0)
{
List<int> emptyKeys = new List<int>();
UpdateKeyStates(emptyKeys);
}
}
}
根据位运算规则,设定按键掩码和解析
private const byte KEY_1_MASK = 0b00000001; // 1 按下1
private const byte KEY_2_MASK = 0b00000010; // 2 按下2
private const byte KEY_3_MASK = 0b00000100; // 4 按下3
private const byte KEY_4_MASK = 0b00001000; // 8 按下4
private const byte KEY_5_MASK = 0b00010000; // 16 按下5
private const byte KEY_6_MASK = 0b00100000; // 32 以此类推
private const byte KEY_7_MASK = 0b01000000;
private const byte KEY_8_MASK = 0b10000000;
/// <summary>
/// 解析十六进制值,判断哪些按键被按下
/// </summary>
public List<int> ParseKeyPress(byte hexValue)
{
List<int> pressedKeys = new List<int>();
if ((hexValue & KEY_1_MASK) != 0) pressedKeys.Add(1);
if ((hexValue & KEY_2_MASK) != 0) pressedKeys.Add(2);
if ((hexValue & KEY_3_MASK) != 0) pressedKeys.Add(3);
if ((hexValue & KEY_4_MASK) != 0) pressedKeys.Add(4);
if ((hexValue & KEY_5_MASK) != 0) pressedKeys.Add(5);
if ((hexValue & KEY_6_MASK) != 0) pressedKeys.Add(6);
if ((hexValue & KEY_7_MASK) != 0) pressedKeys.Add(7);
if ((hexValue & KEY_8_MASK) != 0) pressedKeys.Add(8);
return pressedKeys;
}
在数据包处理的方法中加入按键处理
private readonly KeyStateManager _keyStateManager = new KeyStateManager();
private float _lastMessageTime;
private const float KEY_TIMEOUT = 1f;
protected void Awake()
{
_keyStateManager.OnKeyStateChanged += LogKeyState;
_lastMessageTime = Time.time;
}
/// <summary>
/// 检查消息超时
/// </summary>
private void CheckMessageTimeout()
{
if (Time.time - _lastMessageTime > KEY_TIMEOUT)
{
_keyStateManager.HandleTimeout();
_lastMessageTime = Time.time;
}
}
private void ProcessPacket()
{
//-------------从已有的逻辑部分增加
byte keyValue = Convert.ToByte(parts[7], 16);
List<int> pressedKeys = ParseKeyPress(keyValue);
_keyStateManager.UpdateKeyStates(pressedKeys);
Debug.Log($"按键:{string.Join(", ", pressedKeys)}");
}
/// <summary>
/// 打印按键状态变化
/// </summary>
private void LogKeyState(int key, KEYButtonState state)
{
string color = state switch
{
KEYButtonState.OnButtonDown => "green",
KEYButtonState.OnButtonUp => "yellow",
KEYButtonState.OnButtonClick => "blue",
_ => "white"
};
Debug.Log($"<color={color}>[DataHandlerLog]按键[ {key} ]{GetKeyStateDescription(state)}</color>");
}
/// <summary>
/// 获取按键状态的描述文本
/// </summary>
private string GetKeyStateDescription(KEYButtonState state)
{
return state switch
{
KEYButtonState.OnButtonDown => "被按下",
KEYButtonState.OnButtonUp => "被松开",
KEYButtonState.OnButtonClick => "被点击",
_ => "状态未知"
};
}
按键事件监听
public event Action<float> OnTemperatureUpdated;
public event Action<float> OnHumidityUpdated;
public void SubscribeToKeyEvents(Action<int, KEYButtonState> listener)
{
_keyStateManager.OnKeyStateChanged += listener;
}
public void UnsubscribeFromKeyEvents(Action<int, KEYButtonState> listener)
{
_keyStateManager.OnKeyStateChanged -= listener;
}
到这里位置,功能基本完善,但是还需要有个非常重要的处理,串口数据传输有个不可避免的问题,数据粘包。常用的解决方法是,添加一个缓冲区,接收到的数据不立刻处理,添加到缓冲区,然后从缓冲区去拿数据,拿一段去掉一段。
private string _dataBuffer = "";
/// <summary>
/// 接收数据并加入缓冲区
/// </summary>
private void ReceiveData(string newData)
{
_dataBuffer += newData;
ParseBuffer();
_lastMessageTime = Time.time;
}
/// <summary>
/// 解析缓冲区中的数据,提取完整数据包
/// </summary>
private void ParseBuffer()
{
while (true)
{
// 查找第一个数据头
int head1Index = _dataBuffer.IndexOf(PACKET_HEAD1);
if (head1Index == -1)
{
_dataBuffer = "";
break;
}
// 检查是否有足够的长度容纳第二个数据头
if (head1Index + 2 >= _dataBuffer.Length)
{
_dataBuffer = _dataBuffer.Substring(head1Index);
break;
}
// 验证第二个数据头
string potentialHead2 = _dataBuffer.Substring(head1Index + 4, 2); // 头1占2字节,帧序号占2字节
if (potentialHead2 != PACKET_HEAD2)
{
_dataBuffer = _dataBuffer.Substring(head1Index + 2);
continue;
}
// 查找数据包尾部
string remaining = _dataBuffer.Substring(head1Index);
int tailIndex = remaining.IndexOf(PACKET_TAIL_HIGH + PACKET_TAIL_LOW);
if (tailIndex == -1)
{
_dataBuffer = _dataBuffer.Substring(head1Index);
break;
}
// 提取完整数据包
int packetLength = tailIndex + 4; // 尾部占4个字符(2字节)
string completePacket = remaining.Substring(0, packetLength);
ProcessPacket(completePacket);
_dataBuffer = _dataBuffer.Substring(head1Index + packetLength);
}
}
模拟一下收到的信号数据,输出内容
实际上现在很多单片机已经支持直接传输中文字符串,处理起来更加简单,十六进制的话功耗会更小?
最后附上完整的数据解析代码
public class DataHandler : MonoSingleton<DataHandler>
{
public bool isSimulation;
// 事件定义
public event Action<float> OnTemperatureUpdated;
public event Action<float> OnHumidityUpdated;
public event Action<byte> OnFrameSequenceUpdated;
// 按键处理相关
private readonly KeyStateManager _keyStateManager = new KeyStateManager();
private float _lastMessageTime;
private const float KEY_TIMEOUT = 1f;
// 数据处理相关 - 根据新规则定义
private string _dataBuffer = "";
private const string PACKET_HEAD1 = "AA"; // 固定数据头1
private const string PACKET_HEAD2 = "55"; // 固定数据头2
private const string PACKET_TAIL_HIGH = "EE"; // 结束标识高位
private const string PACKET_TAIL_LOW = "FF"; // 结束标识低位
private const int EXPECTED_PACKET_PARTS = 10; // 预期10个数据部分
protected void Awake()
{
_keyStateManager.OnKeyStateChanged += LogKeyState;
_lastMessageTime = Time.time;
}
private void Start()
{
//没有硬件的时候 unirx 模拟数据接收
Observable.Interval(TimeSpan.FromSeconds(0.1f))
.Subscribe(_ => { if (isSimulation) ReceiveData("AA015500AA021407EEFF"); }
)
.AddTo(this);
}
protected virtual void Update()
{
CheckMessageTimeout();
}
/// <summary>
/// 检查消息超时
/// </summary>
private void CheckMessageTimeout()
{
if (Time.time - _lastMessageTime > KEY_TIMEOUT)
{
_keyStateManager.HandleTimeout();
_lastMessageTime = Time.time;
}
}
/// <summary>
/// 处理接收到的完整数据
/// </summary>
public void ReadComplateString(object data)
{
try
{
string hexString = data switch
{
byte[] byteData => DataConversionUtility.BytesToHexString(byteData),
string stringData => stringData,
_ => throw new ArgumentException($"未知的数据类型: {data.GetType()}")
};
if (!string.IsNullOrEmpty(hexString))
{
Debug.Log($"接收到数据: {hexString}");
ReceiveData(hexString);
}
}
catch (Exception ex)
{
Debug.LogError($"处理接收数据时发生错误: {ex.Message}");
}
}
/// <summary>
/// 接收数据并加入缓冲区
/// </summary>
private void ReceiveData(string newData)
{
_dataBuffer += newData;
ParseBuffer();
_lastMessageTime = Time.time;
}
/// <summary>
/// 解析缓冲区中的数据,提取完整数据包
/// </summary>
private void ParseBuffer()
{
while (true)
{
// 查找第一个数据头
int head1Index = _dataBuffer.IndexOf(PACKET_HEAD1);
if (head1Index == -1)
{
_dataBuffer = "";
break;
}
// 检查是否有足够的长度容纳第二个数据头
if (head1Index + 2 >= _dataBuffer.Length)
{
_dataBuffer = _dataBuffer.Substring(head1Index);
break;
}
// 验证第二个数据头
string potentialHead2 = _dataBuffer.Substring(head1Index + 4, 2); // 头1占2字节,帧序号占2字节
if (potentialHead2 != PACKET_HEAD2)
{
_dataBuffer = _dataBuffer.Substring(head1Index + 2);
continue;
}
// 查找数据包尾部
string remaining = _dataBuffer.Substring(head1Index);
int tailIndex = remaining.IndexOf(PACKET_TAIL_HIGH + PACKET_TAIL_LOW);
if (tailIndex == -1)
{
_dataBuffer = _dataBuffer.Substring(head1Index);
break;
}
// 提取完整数据包
int packetLength = tailIndex + 4; // 尾部占4个字符(2字节)
string completePacket = remaining.Substring(0, packetLength);
ProcessPacket(completePacket);
_dataBuffer = _dataBuffer.Substring(head1Index + packetLength);
}
}
/// <summary>
/// 处理完整的数据包
/// </summary>
private void ProcessPacket(string packet)
{
string[] parts = DataConversionUtility.SplitIntoTwoCharParts(packet);
if (parts.Length != EXPECTED_PACKET_PARTS)
{
Debug.LogError($"数据位数解析异常,期望{EXPECTED_PACKET_PARTS}个数据,实际{parts.Length}个");
return;
}
try
{
// 验证数据头
if (parts[0] != PACKET_HEAD1 || parts[2] != PACKET_HEAD2)
{
Debug.LogError($"数据头验证失败,预期 {PACKET_HEAD1} 和 {PACKET_HEAD2},实际 {parts[0]} 和 {parts[2]}");
return;
}
// 验证数据尾
if (parts[8] != PACKET_TAIL_HIGH || parts[9] != PACKET_TAIL_LOW)
{
Debug.LogError($"数据尾验证失败,预期 {PACKET_TAIL_HIGH} 和 {PACKET_TAIL_LOW},实际 {parts[8]} 和 {parts[9]}");
return;
}
// 验证所有部分都是有效的十六进制
if (!parts.All(DataConversionUtility.IsHexString))
{
Debug.LogError("数据包包含无效的十六进制值");
return;
}
// 解析帧序号 (索引1)
byte frameSequence = Convert.ToByte(parts[1], 16);
OnFrameSequenceUpdated?.Invoke(frameSequence);
// 解析温度数据 (索引3-4)
float temperature = DataConversionUtility.CombineHighLowBytes(parts[3], parts[4]);
// 解析湿度数据 (索引5-6)
float humidity = DataConversionUtility.CombineHighLowBytes(parts[5], parts[6]);
// 处理按键数据 (索引7)
byte keyValue = Convert.ToByte(parts[7], 16);
List<int> pressedKeys = ParseKeyPress(keyValue);
_keyStateManager.UpdateKeyStates(pressedKeys);
// 触发相应事件
OnTemperatureUpdated?.Invoke(temperature);
OnHumidityUpdated?.Invoke(humidity);
// 输出日志
Debug.Log($"帧序号:{frameSequence},温度:{temperature},湿度:{humidity},按键:{string.Join(", ", pressedKeys)}");
}
catch (Exception ex)
{
Debug.LogError($"处理数据包时发生错误: {ex.Message}");
}
}
#region 按键解析
// 按键掩码定义
private const byte KEY_1_MASK = 0b00000001; // 1
private const byte KEY_2_MASK = 0b00000010; // 2
private const byte KEY_3_MASK = 0b00000100; // 4
private const byte KEY_4_MASK = 0b00001000; // 8
private const byte KEY_5_MASK = 0b00010000; // 16
private const byte KEY_6_MASK = 0b00100000; // 32
private const byte KEY_7_MASK = 0b01000000;
private const byte KEY_8_MASK = 0b10000000;
/// <summary>
/// 解析十六进制值,判断哪些按键被按下
/// </summary>
public List<int> ParseKeyPress(byte hexValue)
{
List<int> pressedKeys = new List<int>();
if ((hexValue & KEY_1_MASK) != 0) pressedKeys.Add(1);
if ((hexValue & KEY_2_MASK) != 0) pressedKeys.Add(2);
if ((hexValue & KEY_3_MASK) != 0) pressedKeys.Add(3);
if ((hexValue & KEY_4_MASK) != 0) pressedKeys.Add(4);
if ((hexValue & KEY_5_MASK) != 0) pressedKeys.Add(5);
if ((hexValue & KEY_6_MASK) != 0) pressedKeys.Add(6);
if ((hexValue & KEY_7_MASK) != 0) pressedKeys.Add(7);
if ((hexValue & KEY_8_MASK) != 0) pressedKeys.Add(8);
return pressedKeys;
}
#endregion
#region 事件订阅
public void SubscribeToKeyEvents(Action<int, KEYButtonState> listener)
{
_keyStateManager.OnKeyStateChanged += listener;
}
public void UnsubscribeFromKeyEvents(Action<int, KEYButtonState> listener)
{
_keyStateManager.OnKeyStateChanged -= listener;
}
#endregion
/// <summary>
/// 打印按键状态变化
/// </summary>
private void LogKeyState(int key, KEYButtonState state)
{
string color = state switch
{
KEYButtonState.OnButtonDown => "green",
KEYButtonState.OnButtonUp => "yellow",
KEYButtonState.OnButtonClick => "blue",
_ => "white"
};
Debug.Log($"<color={color}>[DataHandlerLog]按键[ {key} ]{GetKeyStateDescription(state)}</color>");
}
/// <summary>
/// 获取按键状态的描述文本
/// </summary>
private string GetKeyStateDescription(KEYButtonState state)
{
return state switch
{
KEYButtonState.OnButtonDown => "被按下",
KEYButtonState.OnButtonUp => "被松开",
KEYButtonState.OnButtonClick => "被点击",
_ => "状态未知"
};
}
}
/// <summary>
/// 数据转换工具类,封装通用的数据转换方法
/// </summary>
public static class DataConversionUtility
{
/// <summary>
/// 将字节数组转换为十六进制字符串
/// </summary>
public static string BytesToHexString(byte[] bytes)
{
if (bytes == null) return string.Empty;
StringBuilder sb = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
{
sb.AppendFormat("{0:X2}", b);
}
return sb.ToString();
}
/// <summary>
/// 将字符串分割为两个字符一组的数组
/// </summary>
public static string[] SplitIntoTwoCharParts(string input)
{
if (string.IsNullOrEmpty(input))
{
return Array.Empty<string>();
}
List<string> parts = new List<string>();
for (int i = 0; i < input.Length; i += 2)
{
int length = (i + 2 <= input.Length) ? 2 : input.Length - i;
parts.Add(input.Substring(i, length));
}
return parts.ToArray();
}
/// <summary>
/// 检查字符串是否是有效的十六进制值
/// </summary>
public static bool IsHexString(string str)
{
return str.All(c => char.IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));
}
/// <summary>
/// 将高位和低位十六进制值组合成16位有符号整数
/// </summary>
public static float CombineHighLowBytes(string highByteHex, string lowByteHex)
{
byte highByte = Convert.ToByte(highByteHex, 16);
byte lowByte = Convert.ToByte(lowByteHex, 16);
int combinedValue = (highByte << 8) | lowByte;
return combinedValue;
}
}