如何将两个寄存器中的二进制补码值读入整型变量

4

我正在尝试从STC3100电池监测芯片中读取数值,但我得到的数值不正确。根据数据手册所述:

The temperature value is coded in 2’s complement format, and the LSB value is 0.125° C.

REG_TEMPERATURE_LOW, address 10, temperature value, bits 0-7
REG_TEMPERATURE_HIGH, address 11, temperature value, bits 8-15

这是数据表格:http://www.st.com/internet/com/TECHNICAL_RESOURCES/TECHNICAL_LITERATURE/DATASHEET/CD00219947.pdf 我的代码里有以下内容:
__u8 regaddr = 0x0a; /* Device register to access */
__s32 res_l, res_h;

int temp_value;
float temperature;

res_l = i2c_smbus_read_word_data(myfile, regaddr);
regaddr++;
res_h = i2c_smbus_read_word_data(myfile, regaddr);
if (res_l < 0) {
  /* ERROR HANDLING: i2c transaction failed */
} else {
  temp_value = (res_h << 8)+res_l;
  temperature = (float)temp_value * 0.125;
  printf("Temperature: %4.2f C\n", temperature);
}

我做错了什么?我不应该这样将2的补码值复制到int中吗?

你能打印出reg_l和reg_h的单独字节,以确保它们按预期工作吗? - cnicutar
2
你能解释一下为什么你得到的值不正确吗?你得到了什么(寄存器读取的值,res_lres_h),你期望得到什么? - Michael Burr
@AlanCurry 没问题 :) - user529758
@Michael:我该如何通过单次访问读取这两个寄存器的值? - Reto
1
@Reto:抱歉,我在问题的代码中误读了。你已经使用单次访问读取了两个寄存器。但是,通过第二次调用i2c_smbus_read_word_data(),你正在读取另外两个寄存器。请参见我的答案以获取更多详细信息。 - Michael Burr
显示剩余2条评论
4个回答

6

i2c_smbus_read_word_data()函数将从您指定的设备寄存器开始读取16位,因此单个i2c_smbus_read_word_data()函数将使用单个i2c事务读取您感兴趣的两个寄存器。

i2c_smbus_read_word_data()函数以无符号方式返回从设备读取的16位数据 - 如果存在错误,则i2c_smbus_read_word_data()函数的返回值将为负数。您应该能够像这样读取温度传感器:

__u8 regaddr = 0x0a; /* Device register to access */
__s32 res;

int temp_value;
float temperature;

res = i2c_smbus_read_word_data(myfile, regaddr);

if (res < 0) {
  /* ERROR HANDLING: i2c transaction failed */
} else {
  temp_value = (__s16) res;
  temperature = (float)temp_value * 0.125;
  printf("Temperature: %4.2f C\n", temperature);
}

为了解决评论中的问题:
如果没有错误,i2c_smbus_read_word_data()函数将i2c总线获取的16位数据作为无符号16位值返回。 16位无符号值可以轻松地表示在函数返回的32位int中,因此根据定义,16位数据不能为负数。只有当出现错误时,res才会是负数。
(__s16)强制转换res将其解释为(可能为负数的)二进制补码值。这将使用res中的值并将其转换为有符号的16位int表示。严格来说,关于该转换如何处理负数,这是由实现定义的。我相信,在Linux实现中,这将始终简单地将res的较低16位视为二进制补码数字。
如果您担心(__s16)转换的实现定义方面,则可以避免它,而改用算术运算,就像caf的答案一样:
temp_value = (res > 0x7fff) ? res - (0xffff + 1) : res;

即使您在运行一种补码机器(Linux是否支持这样的机器?)上,也可以执行正确的转换为负值。请注意,上面发布的代码假定您正在运行小端机器 - 在将数据转换为负值之前,您需要在大端机器上适当地交换字节。但是,以下代码应该可以解决问题,无论目标CPU如何表示整数值(big / little,one' or two's):

__u16 data = __le16_to_cpu( (__u16) res);

// convert negative two's complement values to native negative value:
int temp_value = (data > 0x7fff) ? data - (0xffff + 1) : data;

哇,这个很好用,而且解决方案非常简单!在问完最后一个问题之后,我会将其标记为正确答案:如果值为负数会发生什么?请记住,这些值可以是负数,并以“二进制补码格式”存储。那么我的错误处理会失败吗?负值会出现在我的错误处理中吗?或者我应该如何处理负值? - Reto
@Reto:我已经更新了答案,试图直接回答这些问题。 - Michael Burr

2
在您的代码中,如果int恰好是32位类型,则表达式temp_value = (res_h << 8) + res_l;对于负值不会生成正确的结果,因为连接是16位的,符号位不会扩展。
您应该尽可能避免任何隐式转换,并明确指定您想要发生什么。 隐式转换规则和有符号和无符号之间的转换是神秘的,可能会产生意外的结果。 将表达式拆分成较小的部分也将有助于调试,因为您将能够准确地看到哪种类型转换或位操作不正确。
我还建议在算术和位运算中保持一致,更喜欢使用(a << 8) | b(a * 256) + b,而不是像您现在使用的(a << 8) + b
  __u8 tlow = (__u8)(res_l & 0xff) ;
  __u8 thigh = (__u8)(res_h & 0xff) << 8 ;
  __s16 temp_value = (__s16)((thigh << 8) | tlow);

  temperature = (float)temp_value * 0.125f ;
  printf("Temperature: %4.2hf C\n", temperature);

不需要在掩码和强制转换方面那么明确或者像我所做的那样分解成额外的变量,但这确实避免了必须知道混合类型表达式中发生的隐式转换的复杂细节,并且使读者和编译器非常清楚你想要发生什么。它还使调试变得更简单,因为您可以在调试器中查看这些中间值(您使用调试器吗?!)。如果您喜欢简洁,则可以通过将 "temp_value" 更正为 "__s16" 或将表达式强制转换为 "__s16" 来更正原始代码,但是由于这已经让您失误了,我不建议这样做,这也可能会让后来需要维护或重用此代码的人失误。无论如何,以下两种方法都可以:
__s16 temp_value = (res_h << 8) | res_l ;

或者

int temp_value = (__s16)((res_h << 8) | res_l);

最后一个至少将结果转换为int,这既是您要求的,而且可能在执行任何后续算术运算方面更安全。

如果您想表明您真正打算转换为__s16,然后分配给int,那么请明确说明:

int temp_value = (int)((__s16)((res_h << 8) | res_l));

因为一些不幸的维护人员可能会认为这是一个错误并尝试“纠正”它!

(因为某些可怜的维护者可能会认为这是一个错误并试图“修正”它!)

2

从你的帖子中并不清楚i2c_smbus_read_word_data的数据类型是什么,但如果可能返回负值,它不能仅仅是无符号字节。 我会使用res_l&0xff和res_h&0xff,只是出于谨慎而进行练习,因为它们不应该包含任何有用的信息。


1

您需要正确处理高位。最简单的方法是:

s32 temp_value = (res_h << 8) | res_l;
if (temp_value > 32767)
    temp_value -= 65536;

别忘了检查res_h事务是否也失败了。


就“最简单”的问题,我不太同意。在这个问题中,要求将其分配给一个int,但是您已将其更改为s32,这不一定是相同的。事实上,即使int为16位,您的代码也可以正常工作,但在这种情况下,测试将是多余的(即始终为false),因此如果要更改类型,为什么不使用__s16并完全省略测试呢?将__s16分配给任何大小的int都具有明确定义和正确的结果,无论是否进行显式转换。 - Clifford
@Clifford:如果int是16位的,那么如果res_h的最高位被设置,res_h << 8 | res_l的赋值将在技术上给出一个实现定义的结果。从代码中可以清楚地看出,temp_value变量是在转换为float之前的临时中间变量。 - caf
确实是实现定义的,但不是指位模式,而是指其解释。这是一个有效的观点,你的代码可以在非二进制补码架构中移植,但我没有看到很多这样的架构!;-)。我认为temp_value表示“温度值”,而不是“临时值”,这在上下文中似乎是合理的。我假设将其转换为浮点数仅用于显示调试文本。在变量名中使用“temp”表示临时总是一个坏主意,在温度传感器代码中更是如此! - Clifford
@Clifford:将超出范围的值分配给有符号整数类型不一定会保留位模式的任何部分(甚至允许引发“实现定义的信号”!)-例如,饱和有符号类型是合法的。 - caf
承认。也许“最安全”而不是“最容易”会更好地描述它。 - Clifford

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接