第三章:基础数据类型序列化——uint8/uint16/uint32/uint64、sint8/sint16/sint32/sint64的编码规则

好,咱们今天聊聊SOME/IP里最基础的东西——整数类型的序列化。

你可能觉得,整数嘛,不就是把数字塞进字节流里?其实没那么简单。不同的位宽、有无符号、字节序,这些细节一旦搞错,ECU之间通信直接就崩了。我在项目里见过太多次因为序列化不一致导致的“幽灵故障”,排查起来真是头大。

3.1 无符号整数:uint8/uint16/uint32/uint64

先看最简单的uint8。它占1个字节,取值范围0到255。序列化时,直接把这个字节写入数据流就行。反序列化时,读一个字节出来,完事。

核心规则:SOME/IP采用大端字节序(Big-Endian)。也就是说,高位字节在前,低位字节在后。

uint16占2个字节。比如数字0x1234,序列化后就是0x12 0x34。uint32占4个字节,0x12345678变成0x12 0x34 0x56 0x78。uint64同理,8个字节依次排开。

我刚开始做SOME/IP时,总觉得大端小端无所谓。直到有一次,我把uint32的0x01020304按小端发了出去,对面ECU死活解析不对。嗯,从那以后我再也不敢忽略字节序了。

3.1.1 对齐规则

这里有个坑——对齐。SOME/IP要求基础数据类型按自身长度对齐。什么意思?

  • uint8:1字节对齐,随便放
  • uint16:2字节对齐,起始偏移必须是2的倍数
  • uint32:4字节对齐,起始偏移必须是4的倍数
  • uint64:8字节对齐,起始偏移必须是8的倍数

举个例子。假设数据流里先放了一个uint8(占1字节),紧接着放一个uint16。uint8之后偏移是1,不是2的倍数,所以需要填充1个字节的0,然后才能放uint16。

注意:填充字节的值必须为0x00。我曾经见过有人填充0xFF,结果对面校验失败,查了半天才发现是填充值不对。

3.2 有符号整数:sint8/sint16/sint32/sint64

有符号整数和无符号整数的序列化规则基本一样,唯一的区别是——编码方式

SOME/IP规定,有符号整数使用二进制补码(Two's Complement)表示。这是计算机里最常用的有符号数编码方式。

说白了,正数的补码就是它本身的二进制。负数的补码是:先取绝对值的二进制,然后按位取反,再加1。

举个例子:sint8的-1,补码是0xFF。sint16的-1,补码是0xFF 0xFF。sint32的-1,补码是0xFF 0xFF 0xFF 0xFF。

你想想看,为什么用补码?因为补码的加减法可以直接用同一套硬件电路处理,不需要区分符号位。这在嵌入式系统里特别重要,省资源嘛。

3.2.1 序列化示例

咱们写个Python代码看看实际效果:

import struct

def serialize_sint32(value):
    """将sint32序列化为4字节大端"""
    # struct模块的'i'表示signed int,'>'表示大端
    return struct.pack('>i', value)

def deserialize_sint32(data):
    """从4字节大端数据反序列化出sint32"""
    return struct.unpack('>i', data)[0]

# 测试
val = -123456
packed = serialize_sint32(val)
print(f"序列化结果: {packed.hex()}")  # 输出: fffe1dc0

unpacked = deserialize_sint32(packed)
print(f"反序列化结果: {unpacked}")    # 输出: -123456

看到没?-123456的补码表示是0xFFFE1DC0。如果你用无符号整数去解析,会得到一个巨大的正数,那就错了。

3.3 常见错误与避坑指南

我在项目中遇到过不少序列化相关的bug,总结几个最常见的:

  1. 字节序搞反:发送端用大端,接收端用小端,或者反过来。结果就是数字完全不对。
  2. 对齐填充遗漏:结构体里字段之间忘了填充字节,导致后续字段偏移错误。
  3. 有符号无符号混用:发送端用sint32发了-1,接收端用uint32去解析,得到0xFFFFFFFF(即4294967295)。
  4. 位宽不匹配:发送端用uint64,接收端用uint32去读,数据截断,高位丢失。

我的建议:在项目初期就定义好所有接口的数据类型映射表。哪个字段用uint8,哪个用sint32,对齐方式是什么,白纸黑字写清楚。别靠口头约定,那玩意儿不靠谱。

3.4 性能考量

嵌入式系统资源有限,序列化反序列化的性能很重要。我个人习惯:

  • 能用uint8就别用uint16,省带宽
  • 能用uint16就别用uint32,省CPU
  • 尽量避免uint64,很多32位MCU处理64位整数很慢
  • 结构体字段按对齐要求重新排列,减少填充字节

举个例子,如果你有三个字段:uint8、uint32、uint16。按顺序排列的话,uint8之后要填充3个字节才能对齐uint32,uint32之后要填充2个字节才能对齐uint16。总共浪费5个字节。

如果改成:uint8、uint16、uint32。uint8之后填充1个字节对齐uint16,uint16之后不需要填充(因为2+2=4,已经是4的倍数),直接放uint32。只浪费1个字节。

你看,只是调整一下顺序,就省了4个字节。在CAN FD或者以太网帧里,这4个字节可能就是一条消息和两条消息的区别。

3.5 总结

好了,这一章的内容就这些。记住几个关键点:

  • 整数序列化用大端字节序
  • 有符号整数用补码表示
  • 按自身长度对齐,不足补0
  • 合理排列字段顺序,减少填充

下一章咱们聊字符串和字节数组的序列化,那个坑更多。到时候我给你们讲讲我踩过的一个关于UTF-8编码的雷,保证让你印象深刻。

公众号:蓝海资料掘金营,微信deep3321