在开发汽车控制器的过程中,不可避免要使用CAN通信,而CAN通信又需要使用CAN矩阵和DBC来约束各个控制器之间的数据传输格式和协议。
DBC里面的signal有Intel和Motorola两种格式,下面用一个图来展示两种格式的排列区别
Intel格式
信号的高位在高字节的高位,信号的低位在低字节的低位,如图的INTEL1,箭头由低位指向高位。
Motorola格式
信号的高位在低字节的高位,信号的低位在高字节的高位
具体的理解要根据CAN矩阵的MAP图来理解,下面来分析下根据8字节原始报文解析对应signal的过程。
在写代码之前,我们还需要了解关于signal的一些基本知识
起始位:start_bit,这个范围一般是0-64,表示siganl在64bit里面最低有效位,有时候的起始位也表示在起始字节里面的最低有效位
起始字节:start_byte,signal的最低有效位所在的字节
长度:信号占用的长度,嵌入式系统里面一般是1-32
精度和偏移:由于CAN报文里面是无法发送浮点数和负数,所以在原始CAN报文的基础上增加一个精度的概念,发送方按照精度和偏移将浮点数或者负数进行计算后填充到CAN报文,接收方再按照精度和偏移计算后取出真实数据
第一种方法:这是公司里面同事不知道从哪搞来的代码,我的项目中直接沿用了
以Intel格式为例,8个字节数组里面共有64个bit,Intel格式的signal占用的bit永远是连续的,而且start_bit就是signal的最低位,那么理所当然我们可以想到,我们只需要把8个字节数组转成64bit,然后根据signal的起始位和信号长度从64bit里面拿对应的值就可以了。代码实现如下:
uint32_t get_data_from_char_array_intel(uint8_t *raw,
uint8_t raw_size,
uint8_t start_byte,
uint8_t start_bit,
uint8_t bit_length)
{
unsigned char data_bit[64] = {0};
int j, k=0;
for(j=0;j
这种方式简单粗暴,直接转成64长度的数组,然后按起始字节和长度从数组里面取数据出来即可。
这个代码在TBOX上面跑的时候,没有任何问题,所以想也没想直接移植到我的控制器里面,知道有一天,测试反馈我说,我们的控制器总是比别人慢半拍,因为控制器里面有很多CAN报文需要解析,signal也比较多,于是分析解析的代码,仔细看才知道,这个代码没执行一次,避免不了8*8的一个for循环,如果一个CAN报文里面有64个signal,那这个运算量就非常庞大,会导致整个程序运行速度变慢。
第二种方法:
于是开始优化代码,优化方向是先把8字节数组转64字节数组这个计算先去掉,既然是按bit从8字节数组里面取数据,那么就根据起始字节和长度,从起始字节里面的最低位开始取数据,然后bit自增,增加到7以后byte就自增,bit归0继续取数据,直到结束。
以上面图中的INTEL2信号为例,start_byte = 5; start_bit = 5; length = 16; 取值步骤如下:
取byte 5 的 bit 5 作为第 0 位
取byte 5 的 bit 6 作为第 1 位
取byte 5 的 bit 7 作为第 2 位
(byte++; bit=0;)
取byte 6 的 bit 0 作为第 3 位
...
取byte 7 的 bit 4 作为第 15位
根据以上取值步骤,只需要根据信号长度,从指定字节的指定位取值,然后向左偏移后跟结果进行或运算即可。
代码如下:
uint32_t get_data_from_char_array_intel(uint8_t *raw,
uint8_t raw_size,
uint8_t start_byte,
uint8_t start_bit,
uint8_t bit_length)
{
(void)raw_size;
uint32_t i = 0;
uint8_t byte_index = start_byte; /* start from 0 */
uint8_t bit_index = start_bit; /* range 0-7 */
uint32_t ret_val = 0;
for(i = 0; i < bit_length; i++)
{
ret_val |= ((raw[byte_index] >> bit_index)&0X01) << i;
if (bit_index++ >= 7)
{
bit_index = 0;
byte_index++;
}
}
return ret_val;
}
上面代码中,根据信号的长度,依次从指定字节获取对应的bit,然后进行左移后跟ret_val进行或运算。
Motorola格式的解析也是一样的,只是取值的顺序有变化,字节内的bit增长方式也有变化,按上述方式取值的话,先取起始字节的起始位作为返回值的最高位,然后bit减一,bit到0后byte++,bit从7开始取值,直到取值完成。
以上面图中的MOT2信号为例,start_byte = 1; start_bit = 1; length = 16; 取值步骤如下:
取byte 1 的 bit 1 作为第 15 位
取byte 1 的 bit 0 作为第 14 位
(byte++; bit=7;)
取byte 2 的 bit 7 作为第 13 位
...
取byte 3 的 bit 2 作为第 0 位
实现代码如下:
uint32_t get_data_from_char_array_motorola( uint8_t *raw,
uint8_t raw_size,
uint8_t start_byte,
uint8_t start_bit,
uint8_t bit_length)
{
(void)raw_size;
uint32_t i = 0;
uint8_t byte_index = start_byte; /* start from 0 */
uint8_t bit_index = start_bit; /* range 0-7 */
uint32_t ret_val = 0;
for(i = 0; i < bit_length; i++)
{
ret_val |= ((raw[byte_index] >> bit_index)&0X01) << (bit_length - i - 1);
if (bit_index == 0)
{
bit_index = 7;
byte_index++;
}
else
{
bit_index--;
}
}
return ret_val;
}
两种方式的区别就是第一次取值是返回值的最低位还是最高位,byte内bit的增长方式是正增长还是负增长。
经过以上优化以后,程序的实时性就满足要求了。
第三种方法:
第二种方式有一个缺点就是每次调用这个解析函数的时候,必不可少的是这个for循环次数必定跟信号的长度相等,如果信号都是32位的,那么这个for循环还是要调用32次。于是继续优化代码,思路如下,直接来看下上面图中INTEL2信号的解析方式:
uint16_t value = 0;
value |= buf[5] >> 5;
value |= (uint32_t)buf[6] << 3;
value |= (uint32_t)(buf[7]&0X1F) << 11;
采用这种方法,一个4字节的signal,最多执行5次或运算就行了,执行效率大大提高,具体代码就不贴了,根据这个思路就可以写出对应的代码。