Как читать значение дополнения 2 из двух регистров в int


Я пытаюсь считывать значения с монитора батареи STC3100 IC, но значения, которые я получаю, не являются правильными. Что написано в справочнике:

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 Cn", temperature);
}

Что я делаю не так? Разве не так я должен скопировать значение дополнения 2 в int?

4 3

4 ответа:

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() возвращает 16 бит данных, полученных из шины i2c в виде 16-битного значения без знака, если ошибки нет. 16-битное беззнаковое значение может быть легко представлено в 32-битном int, возвращаемом функцией, поэтому по определению 16-битные данные не могут быть отрицательными. res будет отрицательным тогда и только тогда, когда есть ошибка.

Интерпретация 16-битного значения как (возможно, отрицательного) значения дополнения двух обрабатывается (__s16) приведением res. Это принимает то значение, которое находится в res и преобразует его в знаковое 16-битное представление int. Строго говоря, это реализация, определенная относительно того, как отрицательные числа будут обработаны этим броском. Я считаю, что в реализациях Linux это всегда будет просто относиться к нижним 16 битам res как к дополнительному числу двойки.

Если вас беспокоит определенный аспект реализации приведения (__s16), вы можете избежать его, используя арифметику вместо приведения, как в ответе caf:

temp_value = (res > 0x7fff) ? res - (0xffff + 1) : res;

Который выполнит правильное преобразование к отрицательному значению, даже если вы работаете на своей машине дополнения (Linux вообще поддерживает запуск на такой вещи?).

Также обратите внимание, что приведенный выше код предполагает, что вы работаете на машине с малым концом - вам нужно будет соответствующим образом поменять байты на машине с большим концом, прежде чем преобразовывать данные в отрицательное значение, следующее должно сделать трюк, однако целевой процессор представляет целочисленные значения (big/little, one' или two):

__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;

Из Вашего сообщения не ясно, что такое тип данных i2c_smbus_read_word_data, но если возможно возвращать отрицательные значения, то это не могут быть просто неподписанные байты. Я хотел бы использовать res_l & значение 0xFF и res_h & 0xff просто как упражнение в паранойе, так как они не должны содержать ничего интересного.

В вашем коде, если 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 A __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));

Потому что какой-нибудь незадачливый сопровождающий может позже подумать, что это ошибка. и попытайтесь это "исправить"!

Вам нужно правильно обращаться с высоким битом. Самый простой способ сделать это:

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

Не забудьте проверить, не провалилась ли транзакция res_h.