关于C语言连续强制类型转换,有符号数据位移,以及温度传感器int16有符号数据重组处理问题
关于C语言连续强制类型转换,是从温度传感器中数据处理拓展讨论的一个问题。
一. 温度传感器datasheet
1. 图一温度结果和限制
2. 图二 温度寄存器,IIC从这里读取。
二. IIC接收数据到温度处理计算代码怎么写?
问题的重点在于通过IIC传输数据时,是一个一个Bytes传输的,但温度数据是int16_t,如何把两个byts重组处理成int16_t?
1. 方式一
这是来自朋友的开源代码,基本写法和逻辑没有太大问题,但是比较绕哈。
uint8_t TEMP_ARRAY[2];//通过IIC从温度寄存器读取的两个BYTE温度数据。
if(TEMP_ARRAY[0]&0x80)
{
zero_flag=1;
temp=TEMP_ARRAY[0];
temp<<=8;
temp+=TEMP_ARRAY[1];
temp=~temp_ct7117;
temp+=1;
high_byte=(temp&0xff00)>>8;
low_byte=(temp&0x00ff);
dot_value=high_byte;
dot_value+=(low_byte>1)*0.0078125;
dot_value=-dot_value;
}
else
{
dot_value=TEMP_ARRAY[0];
dot_value+=((TEMP_ARRAY[1])>>1)*0.0078125;
TEMP_Display_Vlaue=dot_value*10;
zero_flag=0;
}
2. 方式二,涉及C语言连续强制类型转换。
写法看起来会简单非常多。但是要理解C语言连续强制转换逻辑。
uint8_t rxBuf[2];//数组用于存储温度寄存器数据。
int32_t raw_temperature;//用于存储寄存器16bit的值,这个值是rxBuf合并计算出来的。
double temperature;//温度数据。
raw_temperature = (((int32_t)(int8_t)rxBuf[0]) << 8) | rxBuf[1];
temperature = raw_temperature * (78125e-7);//计算摄氏度温度,保留小数。
2.1. 解析:
接收到的数据被存储在无符号的数组中,首先高八位在rxBuf[0]中,通过datasheet以及C语言int16_t的数据表示可以知道,当rxBuf[0]大于等于0x80时(也可以把0x80当作无符号数据看待),温度是负数。
int8_t的十六进制0x80~0xFF表示的都是负数。int16_t数据的正负数由最高位决定。
所以第一个高八位,直接先转换成int8_t数据,接着再转换成int32_t。
2.1.1. 有符号数据的位移
操作 | 符号位是否可能变化 | 条件说明 | 备注 |
左移<< | 是 | 符号位被移除时(如负数左移) | 若原符号位(最高位)为1(负数),左移后可能变为0(正数) |
右移>> | 否 | 算术右移保留原符号位 | 高位补原符号位(负数补1,正数补0),符号位本身不变 |
PS:如果位移位数超过数据位数,将可能发生未知行为,依赖编译器和CPU。
接着进行左移8位的操作。左移不是会移除符号位吗?注意上面说的是可能,因为数据是补码的方式存储,并且被转换成了int32_t,一个int8_t的负数被转换成了int32_t,它的bit8~bit31会全为1,所以这时候左移不会有任何影响,总结来说当原有符号数据宽度扩展到更大的数据类型宽度时,左移不会改变数据,也不会影响符号位。
2.2. 连续强制转换逻辑:从右到左依次转换,编译器并不会直接忽略中间的转换类型。
比如:如果温度是负数时,下面第二行代码将输出错误的数值。⚠️⚠️⚠️⚠️
raw_temperature = (((int32_t)(int8_t)rxBuf[0]) << 8) | rxBuf[1];
raw_temperature = (((int32_t)rxBuf[0]) << 8) | rxBuf[1];
ok,这个方式比较适合老道的程序员,新手要谨慎,千万别依葫芦画瓢。
3. 方式三,类型只是告诉编译器、CPU应该怎么去解释处理这段内存。
0x81赋值给有符号和无符号数据是怎么识别的,甚至给double,会怎么样?
只要了解数据类型其数据的内存布局或者说表示方法,我们就可以利用这个方法直接处理。
uint8_t rxBuf[2];//从温度传感器寄存器读取的数值。
int16_t raw_temp = 0;//rxBuf温度数据重组重组成int16_t
uint16_t raw = (rxBuf[0]<<8)|rxBuf[1];//rxBuf温度数据重组重组成uint16_t
raw_temp |= raw;//按位操作。
int32_t temp = raw_temp * 0.0078125*10;//乘10是把数据放大10倍,以保留一位小数。
cout << temp<< endl;
重点看第四行,按位操作。为什么可以这么做。
类型只是告诉编译器、CPU应该怎么去解释这段内存。
利用这个规则,直接按位操作,而不是进行强制类型。
数据的整个处理过程都是按位处理和uint8_t左移处理(左移时会被隐式转换成更高的类型,所以数据无影响),所以重组前后的数据在内存中没有发生任何变化。
如果处理器支持,你甚至可以直接raw_temp = (*(int16_t * )rxBuf)。处理器需要两个满足:①内存对齐,②字节序匹配(IIC是低地址存高八位,也即大端。MCU一般是小段,低地址低八位。所以rxBuf[0]存储IIC低八位时,则满足条件。)。
3.1. 方式三的简化效率版本。
// IIC大端序传感器 → 小端序处理器。
// 以下是简化版本,执行效率最高。//方式三简化版本1
raw_temp = (*(int16_t * )rxBuf)//方式三简化版本2
int16_t raw_temp = (int16_t)( (rxBuf[0] << 8) | rxBuf[1] );
4. 方式四,方式三的安全版本。
// 使用显式字节序的联合体
int32_t decode_temperature(const uint8_t* buf)
{union {int16_t val;struct {uint8_t lsb; //低八位,无符号。int8_t msb; // 高八位,有符号存储 };} raw;raw.lsb = buf[1];raw.msb = (int8_t)buf[0]; // 强制符号转换 // 定点运算(保留3位小数)return (raw.val * 78125LL) / 10000000;
}
这个方案是最优解,①字节大小端序的更加安全。②符号位安全无隐患。③可移植性和可读性高。
方案三的简化版本也可以使用,执行效率是最高的。