/notes/mcu/can-dbc-cross-byte-endianness

Firmware//24 分钟

CAN DBC 跨字节信号:Intel / Motorola 字节序、位编号与 STM32 打包解析

从 DBC 的 start bit 和 bit numbering 出发,拆清跨字节 Intel(小端)与 Motorola(大端)信号如何落在 CAN payload 中;给出 STM32 上安全的打包、提取与边界校验方法。

CANDBCIntelMotorolaSTM32字节序

先把三个容易混淆的概念分开

处理 DBC 信号时,最常见的错误不是移位公式本身,而是把下面三件事混成一件事:

  1. CAN 报文 payload 的字节顺序: data[0]data[7] 是总线上定义的字节位置,和 MCU 的 CPU 字节序无关。
  2. DBC 的信号字节序: @1 是 Intel / little-endian;@0 是 Motorola / big-endian。它决定一个多位信号如何跨 payload 字节延伸。
  3. STM32 的内存字节序: 常用 STM32 Cortex-M 是 little-endian;这只影响一个 uint16_t / uint32_t 在 RAM 中的内部字节排列,不改变 CAN payload 的定义

工程规则:先把 CAN 数据当成 uint8_t data[8],按 DBC 逐位打包或提取;不要把 data 强制转换为 uint16_t * 或位域结构体来“图省事”。那会同时踩到字节序、对齐和编译器位域布局三类坑。

DBC 中一个典型信号写作:

SG_ VehicleSpeed : 12|12@1+ (0.01,0) [0|655.35] "km/h" ECU

它的含义是:

字段含义
12(起始位)DBC 的 start bit;含义受字节序影响
12(长度)信号共占 12 bit
@1Intel / little-endian
+unsigned;- 则代表最终 raw 值按二补码解释
(0.01, 0)physical = raw × factor + offset

本文统一约定:data[0] 是 payload 第 0 字节;每个字节内最低有效位记为 bit 0。所有数值示例都是 raw value,物理量换算在提取或打包后进行。

DBC 位编号:同一格子,Intel 与 Motorola 的“前进方向”不同

很多 DBC 工具把 64 bit payload 画成 8 行 × 8 列。表面的编号可能采用“锯齿编号”或“从左到右递增”显示,工具间视觉差异很大;可靠的做法是回到 DBC 语义:

  • Intel @1 start bit 是信号的 LSB;下一个更高权重位沿 payload 的自然内存位序向前走。
  • Motorola @0 start bit 是信号的 MSB;下一个更低权重位按 DBC 的 Motorola 位序向后走,跨字节时会跳到下一字节的 bit 7。

下面的图以每一格 data[byte].bit 表示一个真实 payload 位置,而不是依赖某个工具的图形编号。Intel 与 Motorola 的差异,核心就在于它们从 start bit 出发的位遍历方向不同:

同一个 raw 值,Intel 与 Motorola 的跨字节路径
raw = 0xABC / 12 bit
Intel @1 · start bit = LSB · 4|12data[0]b7b3b6b2b5b1b4b0b3b2b1b0data[1]b7b11b6b10b5b9b4b8b3b7b2b6b1b5b0b4低位先落在较低 payload 字节Motorola @0 · start bit = MSB · 11|12data[1]b7b6b5b4b3b11b2b10b1b9b0b8跨字节后跳到下一字节 bit7 → bit0

Intel / little-endian:起始位就是 LSB,跨字节连续增长

Intel 信号最容易用一个通用公式理解。若 DBC 定义为 start_bit = s、长度为 len,则其第 k 个信号位(k = 0 是 raw LSB)位于:

payload_byte = (s + k) / 8
payload_bit  = (s + k) % 8

因此,Intel 跨字节时低字节先出现。假设 12-bit unsigned 信号:

SG_ Intel12 : 4|12@1+ (1,0) [0|4095] "" Vector__XXX
raw = 0xABC = binary 1010 1011 1100

位级排列如下:

                 data[0]                                  data[1]
       bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 | bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
        ───  ───  ───  C    B    A    9    8    |  7    6    5    4    3    2    1    0
                      ↑ start_bit = 4,LSB = raw bit0

raw 位权:           bit3 bit2 bit1 bit0        bit11 bit10 bit9 bit8 bit7 bit6 bit5 bit4
数据值:               C    B    A    9            8     7     6    5    4    3    2    1

结果:data[0] 的 bit4..7 保存 raw 低 4 bit;data[1] 保存 raw 高 8 bit。

当其他字段先占用了 data[0] 的低 4 bit 时,打包后的具体字节为:

before: data[0] = 0x0D, data[1] = 0x00
write raw 0xABC at 4|12@1

after : data[0] = 0xCD, data[1] = 0xAB

这正是“低有效部分在较低 payload 字节”的含义;它不是“把整个 CAN 帧反序”。

STM32 上的 Intel 快速路径

对于 Intel,且 start_bit + len <= 64,可以安全地把 payload 组装成一个 逻辑 little-endian 的 64-bit 容器,再移位。这里的 load_le64 是显式字节组合,不依赖 CPU 是否是小端:

#include <stdint.h>
#include <stdbool.h>

static uint64_t can_load_le64(const uint8_t data[8])
{
    uint64_t value = 0U;

    for (uint32_t i = 0U; i < 8U; ++i) {
        value |= (uint64_t)data[i] << (8U * i);
    }
    return value;
}

static void can_store_le64(uint8_t data[8], uint64_t value)
{
    for (uint32_t i = 0U; i < 8U; ++i) {
        data[i] = (uint8_t)(value >> (8U * i));
    }
}

static bool dbc_get_intel_u64(
    const uint8_t data[8], uint8_t start_bit, uint8_t length, uint64_t *raw)
{
    if ((raw == NULL) || (length == 0U) || (length > 64U) ||
        ((uint16_t)start_bit + length > 64U)) {
        return false;
    }

    const uint64_t payload = can_load_le64(data);
    const uint64_t mask = (length == 64U) ? UINT64_MAX : ((UINT64_C(1) << length) - 1U);
    *raw = (payload >> start_bit) & mask;
    return true;
}

static bool dbc_set_intel_u64(
    uint8_t data[8], uint8_t start_bit, uint8_t length, uint64_t raw)
{
    if ((length == 0U) || (length > 64U) ||
        ((uint16_t)start_bit + length > 64U)) {
        return false;
    }

    const uint64_t mask = (length == 64U) ? UINT64_MAX : ((UINT64_C(1) << length) - 1U);
    if ((raw & ~mask) != 0U) {
        return false; // raw 超出 DBC 位宽,拒绝静默截断
    }

    uint64_t payload = can_load_le64(data);
    payload = (payload & ~(mask << start_bit)) | (raw << start_bit);
    can_store_le64(data, payload);
    return true;
}

对于上面的 4|12@1

uint8_t data[8] = {0x0DU, 0U};
(void)dbc_set_intel_u64(data, 4U, 12U, 0xABCU);
// data[0] == 0xCD, data[1] == 0xAB

uint64_t raw = 0U;
(void)dbc_get_intel_u64(data, 4U, 12U, &raw);
// raw == 0xABC

Motorola / big-endian:起始位是 MSB,跨字节跳到下一字节 bit7

Motorola 信号最容易误用 Intel 的 start + k 公式。它的 DBC start bit 指向 信号 MSB。在同一字节中,信号向 bit 0 方向延伸;一旦到达 bit 0,下一个信号位跳到下一个 payload 字节的 bit 7

假设:

SG_ Motor12 : 11|12@0+ (1,0) [0|4095] "" Vector__XXX
raw = 0xABC = binary 1010 1011 1100

start_bit = 11 对应 data[1].bit3。因为它是 MSB,12 个 bit 的路径是:

数据位: raw bit11 bit10 bit9 bit8 | bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
位置:    d[1].b3 d[1].b2 d[1].b1 d[1].b0 | d[2].b7 d[2].b6 d[2].b5 d[2].b4 d[2].b3 d[2].b2 d[2].b1 d[2].b0
值:         A        B       C       1   |    1       1       0      0      1      1      0      0

字节效果: data[1] 的低 4 bit = 0xA,data[2] = 0xBC

请注意,Motorola 并不意味着 data[1] 必然保存完整的“高字节”。信号是否字节对齐由 start bit 和 length 决定;只有刚好按字节对齐时,才呈现常见的高字节在前。

用“物理 payload 位置”实现 Motorola,避免图形编号歧义

以下通用实现不把 DBC 图上的锯齿编号直接当数组下标,而是把 start bit 先转成 (byte, bit)

static bool dbc_get_motorola_u64(
    const uint8_t data[8], uint8_t start_bit, uint8_t length, uint64_t *raw)
{
    if ((raw == NULL) || (length == 0U) || (length > 64U) || (start_bit >= 64U)) {
        return false;
    }

    uint8_t byte = start_bit / 8U;
    int8_t bit = (int8_t)(start_bit % 8U);
    uint64_t value = 0U;

    for (uint8_t i = 0U; i < length; ++i) {
        if (byte >= 8U) {
            return false; // 信号越过 DLC=8 的 payload
        }
        value = (value << 1U) | ((data[byte] >> bit) & 1U);

        if (bit == 0) {
            ++byte;
            bit = 7;
        } else {
            --bit;
        }
    }

    *raw = value;
    return true;
}

static bool dbc_set_motorola_u64(
    uint8_t data[8], uint8_t start_bit, uint8_t length, uint64_t raw)
{
    if ((length == 0U) || (length > 64U) || (start_bit >= 64U)) {
        return false;
    }
    if ((length < 64U) && ((raw >> length) != 0U)) {
        return false;
    }

    uint8_t byte = start_bit / 8U;
    int8_t bit = (int8_t)(start_bit % 8U);

    for (uint8_t i = 0U; i < length; ++i) {
        if (byte >= 8U) {
            return false;
        }

        const uint8_t source_bit = (uint8_t)((raw >> (length - 1U - i)) & 1U);
        const uint8_t bit_mask = (uint8_t)(1U << bit);
        data[byte] = source_bit ? (uint8_t)(data[byte] | bit_mask)
                                : (uint8_t)(data[byte] & (uint8_t)~bit_mask);

        if (bit == 0) {
            ++byte;
            bit = 7;
        } else {
            --bit;
        }
    }
    return true;
}

测试 11|12@0

uint8_t data[8] = {0U};
(void)dbc_set_motorola_u64(data, 11U, 12U, 0xABCU);
// data[1] == 0x0A, data[2] == 0xBC

uint64_t raw = 0U;
(void)dbc_get_motorola_u64(data, 11U, 12U, &raw);
// raw == 0xABC

两类跨字节信号的直观对照

同样是 12 bit、raw 0xABC,不同 DBC 定义会落到完全不同的位置:

Intel   SG_ intel  : 4|12@1+
Motorola SG_ motor : 11|12@0+

             data[0]        data[1]        data[2]
           7 ... 0         7 ... 0         7 ... 0
Intel:     C B A 9         8 7 6 5 4 3 2 1        -
Motorola:       -          A B C 1         1 1 0 0 1 1 0 0

Intel:raw LSB 从 start bit 开始,逐位向更高的 payload 位置走。
Motorola:raw MSB 从 start bit 开始,逐位向当前字节 bit0、再向下一个字节 bit7 走。

非字节对齐的 3-bit、17-bit 与有符号场景

  • 3-bit Intel: 6|3@1 会占用 data[0].bit6.bit7data[1].bit0;“很短”不代表不会跨字节。
  • 17-bit Motorola: 从某字节中间开始可能经过 3 个 payload 字节;不能假设长度大于 8 才会跨 3 字节。
  • signed: 先按 unsigned 规则提取 raw,再以信号长度做符号扩展;绝不要先把一个未对齐片段强转成 int16_t
static int64_t dbc_sign_extend(uint64_t raw, uint8_t length)
{
    // length 已由调用者校验为 1..64
    if ((length < 64U) && ((raw & (UINT64_C(1) << (length - 1U))) != 0U)) {
        raw |= ~((UINT64_C(1) << length) - 1U);
    }
    return (int64_t)raw;
}

例如 10-bit signed 的 raw 为 0x3F0,bit9 为 1;符号扩展后是 -16,不是 1008。最终物理量始终按:

physical = signed_or_unsigned_raw × factor + offset

STM32:小端 RAM 不等于 Intel DBC,也不能决定 CAN 报文字节序

STM32 Cortex-M 的小端 RAM 意味着:

uint16_t x = 0x1234;
RAM 中低地址 → 0x34, 0x12

但 DBC payload 的 data[0]data[1] 是协议字段,必须严格服从 DBC。下面两种写法都不推荐:

// 错误 1:假设 STM32 小端内存恰好就是 Intel 信号定义
uint16_t raw = *(const uint16_t *)&data[1];

// 错误 2:用位域描述 DBC(布局、顺序和对齐由编译器决定)
typedef struct {
    uint16_t flag : 4;
    uint16_t value : 12;
} BadDlcLayout;

原因包括:未对齐访问、严格别名、端序可移植性、结构体 padding,以及最关键的——Motorola 信号根本不是一个可以直接取出的“小端整数”。

在 STM32 HAL / LL / 寄存器级收发路径里,推荐保持以下边界:

STM32 协议边界:RAM 小端不等于 DBC 字节序
推荐的收发处理路径
CAN FIFOuint8_t data[8]协议字节DBC decoder@1 / @0提取 rawSignal layersigned + scale物理量Control taskstate / UI应用逻辑不要将 data 强转为 uint16_t* 或位域结构体显式按 DBC 位遍历 → 保留跨编译器、跨 MCU、CAN FD 的可移植性

反向打包则先把应用层物理值转换为 raw,检查范围,再清零或保留其他共享 bit,最后按 DBC 写入 data[8]

STM32 CAN 报文:从物理值打包、从报文还原

假设一帧同时含两个信号:

SG_ WheelSpeed : 4|12@1+ (0.01,0) [0|40.95] "m/s"
SG_ MotorTemp  : 27|10@0- (0.25,-40) [-168|87.75] "degC"

打包流程:

  1. 将物理量反变换为 raw:raw = round((physical - offset) / factor)
  2. 校验 raw 是否能放进规定长度;signed 信号要先限制到 [-2^(n-1), 2^(n-1)-1]
  3. 以全 0 或上一帧缓存初始化 data[8]。共享字节场景必须先明确其他信号是否需要保留。
  4. WheelSpeed 调用 Intel 写入;MotorTemp 转为 n-bit 二补码后调用 Motorola 写入。
  5. data 作为 CAN DLC=8 的 payload 交给 HAL。
#include "stm32f4xx_hal.h"
#include <math.h>

static bool pack_example(float speed_mps, float temp_c, uint8_t data[8])
{
    const int32_t speed_raw = (int32_t)lroundf(speed_mps / 0.01f);
    const int32_t temp_raw = (int32_t)lroundf((temp_c + 40.0f) / 0.25f);

    if ((speed_raw < 0) || (speed_raw > 0xFFF) ||
        (temp_raw < -512) || (temp_raw > 511)) {
        return false;
    }

    for (uint32_t i = 0U; i < 8U; ++i) data[i] = 0U;

    if (!dbc_set_intel_u64(data, 4U, 12U, (uint64_t)speed_raw)) return false;
    if (!dbc_set_motorola_u64(data, 27U, 10U, (uint64_t)(uint16_t)temp_raw & 0x3FFU)) return false;
    return true;
}

static HAL_StatusTypeDef send_example(CAN_HandleTypeDef *hcan)
{
    uint8_t data[8];
    CAN_TxHeaderTypeDef header = {0};
    uint32_t mailbox = 0U;

    if (!pack_example(12.34f, 25.0f, data)) return HAL_ERROR;

    header.StdId = 0x321U;
    header.IDE = CAN_ID_STD;
    header.RTR = CAN_RTR_DATA;
    header.DLC = 8U;

    return HAL_CAN_AddTxMessage(hcan, &header, data, &mailbox);
}

接收时反过来:先提取 raw、对 MotorTemp 做 10-bit 符号扩展,再计算物理量。接收回调只负责把原始帧投递给任务;DBC 解析在任务上下文完成,避免中断路径内进行浮点换算或复杂诊断。

static bool unpack_example(const uint8_t data[8], float *speed_mps, float *temp_c)
{
    uint64_t speed_raw = 0U;
    uint64_t temp_raw = 0U;

    if ((speed_mps == NULL) || (temp_c == NULL) ||
        !dbc_get_intel_u64(data, 4U, 12U, &speed_raw) ||
        !dbc_get_motorola_u64(data, 27U, 10U, &temp_raw)) {
        return false;
    }

    *speed_mps = (float)speed_raw * 0.01f;
    *temp_c = (float)dbc_sign_extend(temp_raw, 10U) * 0.25f - 40.0f;
    return true;
}

位级与字节级检查图:不要只看“数值读对了”

在台架排查 DBC 字节序问题时,建议把原始 payload、每个信号路径和最终物理值一起打印:

CAN 0x321 DLC=8
bytes : CD AB 00 1A 00 00 00 00

Intel WheelSpeed (4|12@1):
  d[0].b4..b7 + d[1].b0..b7 → raw 0xABC → 27.48 m/s

Motorola MotorTemp (27|10@0):
  d[3].b3..b0 + d[4].b7..b2 → raw 0x068 → -14.00 °C

如果来自 DBC 工具、Python 解码器和 STM32 三者的 raw / physical 值不一致,先比 raw 和 payload,不要先比最终浮点数。raw 不一致通常是 start bit 或字节序理解错误;raw 一致而 physical 不一致才检查 factor、offset 与 signed 属性。

最容易混淆的边界条件清单

  • @0 / @1 是 DBC 定义,不是 MCU 端序。 STM32 小端并不会自动处理 Intel,也不会让 Motorola 失效。
  • 不同工具的 bit 编号图可能看起来不同。 确认导出的 DBC 语法以及工具对 start bit 的定义;本文代码假定 start bit 可按 byte = start / 8bit = start % 8 映射到 payload 实体位置。若供应商工具使用反向显示编号,必须先转换。
  • Motorola start bit 是 MSB,Intel start bit 是 LSB。 两者的 start_bit 不能互换。
  • DLC 是边界。 Classic CAN 的有效 payload 是 0–8 字节;解析前应确认实际 DLC 足以覆盖信号末位,而不是固定假设 data[8] 总是有效。
  • 长度为 64 时禁止 1ULL << 64 掩码和符号扩展必须专门处理 64-bit 分支。
  • 带符号信号先提 raw,再按长度符号扩展。 不要把未对齐数据直接转换成有符号整型。
  • 共享字节要 read-modify-write。 写一个 3-bit 字段前不能整字节赋值,否则会清掉相邻信号。
  • CAN FD 仅改变 payload 长度,不改变这两类 DBC 信号的位序语义。 对超过 64 bit 的信号可用相同的逐位算法,并按实际 DLC 做边界检查。

结论

DBC 的 Intel / Motorola 不是“CPU 大小端选择题”,而是一个信号在 CAN payload 上的位遍历规则

Intel @1    :start bit = LSB,位权沿 payload 自然位序递增。
Motorola @0 :start bit = MSB,位权在字节内向 bit0 递减,跨字节跳到下一字节 bit7。

在 STM32 上最稳妥的工程实践是:payload 始终使用 uint8_t[] 表示;Intel 可用显式 little-endian 64-bit 容器快速处理,Motorola 使用逐位路径;所有打包和解包都执行长度、DLC、范围和 signed 校验。这样,跨字节、非字节对齐和不同信号长度都可以保持可解释、可测试和可移植。