C/C++中解析CAN总线信号:从原始字节提取比特位
车载网络通信中,经常需要将接收到的CAN报文字节数据拆解为具体的物理信号。本文介绍如何通过C/C++编程,根据定义的高位(MSB)和低位(LSB),从原始字节流中精准提取特定位域的数值。
CAN数据与信号结构
CAN总线标准帧通常包含8个字节(64位),而新一代的CAN FD协议支持最长64字节的数据载荷。CAN标识符分为标准帧(11位ID)和扩展帧(29位ID)。 所谓“CAN信号”,是指这串字节数据中,从第几位到第几位代表某个具体的车辆参数(如车速、转速)。例如,数据中的某8个比特位组合在一起可能表示速度值。要获取该信号,就必须知道其起始位(MSB,Most Significant Bit)和结束位(LSB,Least Significant Bit)在整个报文中的索引位置。
核心解析算法
解析的核心在于按位读取。由于C语言没有直接提供跨字节的位提取指令,我们需要手动处理字节偏移量以及每个字节内的位偏移量。 以下是完整的解析逻辑示例,包括数据结构定义、赋值及提取过程:
#include <iostream>
#include <array>
#include <cstring>
#include <cstdlib>
// 位操作辅助宏
#define BITSET(p,n) ((p) |= (1u <<(n)))
#define BITCLR(p,n) ((p) &= ~(1u <<(n)))
#define BITGET(i,n) ((i) & (1u << (n)))
// 定义用于截取高位和低位边缘部分的掩码
unsigned char msbmask[] = {
0xFF, 0xFE, 0xFC, 0xF8,
0xF0, 0xE0, 0xC0, 0x80
};
unsigned char lsbmask[] = {
0x01, 0x03, 0x07, 0x0F,
0x1F, 0x3F, 0x7F, 0xFF
};
// 用于存储待解析信号的位置信息
// MSBPos: 信号最高位在报文总比特流中的索引(0开始)
// LSBPos: 信号最低位在报文总比特流中的索引
typedef struct {
unsigned char* can_data_ptr;
int len;
int msb_pos;
int lsb_pos;
}can_signal;
static can_signal cansingal;
// 初始化信号数据
can_data_assignment(unsigned char* candata, int msbpos, int lsbpos, int lens)
{
cansingal.can_data_ptr = (unsigned char*)malloc(lens);
memcpy((void *)cansingal.can_data_ptr, (const void *)candata,lens);
cansingal.len = lens;
cansingal.msb_pos = msbpos;
cansingal.lsb_pos = lsbpos;
return 0;
}
// 执行信号提取
unsigned int can_data_transfer_signal()
{
// a,b 计算当前所在字节及字节内偏移
int a = 0;
int b = 0;
int c = 0;
int d = 0;
unsigned int singal = 0;
printf("%d %d\n", cansingal.lsb_pos, cansingal.msb_pos);
printf("%02x %02x %02x %02x\n", cansingal.can_data_ptr[0], cansingal.can_data_ptr[1], cansingal.can_data_ptr[2], cansingal.can_data_ptr[3]);
a = cansingal.lsb_pos / 8; // 所在字节
b = cansingal.lsb_pos % 8; // 字节内最低位偏移
cansingal.can_data_ptr[a] = cansingal.can_data_ptr[a] & msbmask[b];
c = cansingal.msb_pos / 8;
d = cansingal.msb_pos % 8;
cansingal.can_data_ptr[c] = cansingal.can_data_ptr[c] & lsbmask[d];
printf("%02x %02x %02x %02x\n", cansingal.can_data_ptr[0], cansingal.can_data_ptr[1], cansingal.can_data_ptr[2], cansingal.can_data_ptr[3]);
// 逐位提取并重组目标信号
for (int i = cansingal.lsb_pos, j = 0; i <= cansingal.msb_pos; ++i, ++j)
{
a = i / 8;
b = i % 8;
if ( BITGET(cansingal.can_data_ptr[a], b) )
{
BITSET(singal, j);
}
else
{
BITCLR(singal,j);
}
}
return singal;
}
// 释放资源
void can_data_free(void)
{
free(cansingal.can_data_ptr);
cansingal.len = 0;
cansingal.lsb_pos = 0;
cansingal.msb_pos = 0;
}
int main(int argc, char* argv[])
{
// 测试用例:4字节报文
unsigned char candata[4] = { 0x44, 0xFE, 0x23, 0x81};
// 模拟提取:从第14位到第31位的信号
can_data_assignment(candata, 31, 14, 4);
unsigned int c = can_data_transfer_signal();
can_data_free();
printf("Signal Value: %d\n", c);
return 0;
}原理详解与运行验证
在上述代码中,msbmask 用于清理比目标范围更低的那些位,lsbmask 用于清理比目标范围更高的位。通过整除8 (/8) 找到目标位所在的字节下标,通过取余8 (%8) 找到该字节内的具体比特位偏移。
以输入 { 0x44, 0xFE, 0x23, 0x81 } 为例,系统会将连续的比特流从高位到低位排列,然后从中截取指定位数进行还原。实际运行时效果如下:
C语言底层实现要点
实现此算法依赖三个核心知识点:数组与指针的关系、位运算逻辑以及地址索引计算。
1. 数组与指针的等价性
在内存中,数组名即为首地址。指针每加1,指向的地址增加其基类型的大小。若基类型为int(4字节),则加1跳过4个字节;若为字符数组,则加1跳过1个字节。
公式表达:p[i] 等价于 *(*(p+i))。
以下代码演示了指针遍历数组的直观结果:
#include <stdio.h>
int main(int argc, char *argv[]){
int a[] = {1, 3, 5, 7, 9};
int *p, i;
p = a;
printf("%p %p %p\n", a, a+1, a+2);
for(i = 0; i < 5; i++){
printf("%d %d %d %d\n", a[i], *(p+i), *(a+i), p[i]);
}
return 0;
}输出显示,虽然地址随类型步长跳变,但 dereference 后的数值顺序一致,这证明了在处理CAN报文连续字节数组时,可以直接使用整数索引偏移来跨越多个字节边界。
以上就是通过C/C++底层位操作实现CAN信号解析的详细内容,更多关于车载网络协议开发中数据处理的技术资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













