Cómo lidiar con los desbordamientos de int firmados

Breve trasfondo primero; Tengo datos del bus CAN de un ángulo de dirección que obviamente está en hexadecimal. El ángulo de dirección cubre dos bytes de un mensaje. El documento de especificaciones que tengo dice que esos dos bytes forman un int firmado de 16 bits que son el ángulo de dirección con un preescalador de 1/1024, eso es todo lo que tengo (no tengo acceso a la fuente). Lo que estoy tratando de hacer es convertir esos valores hexadecimales en int firmados, sin embargo, no estoy seguro de cómo hacerlo correctamente.

Una pequeña sección del mensaje CAN dentro de un período de tiempo corto (nos estamos enfocando en el byte 2 y 3):

can0  700   [8]  00 00 99 93 55 0B EF BD
can0  700   [8]  00 00 95 95 10 0C 17 BE
can0  700   [8]  00 00 6F 97 FB 0A 17 BE
can0  700   [8]  00 00 39 99 5C 0A 40 BE
can0  700   [8]  00 00 AD 9A 62 08 EF BD
can0  700   [8]  00 00 EF 9B B5 08 40 BE
can0  700   [8]  00 00 CA 9D 9A 09 17 BE
can0  700   [8]  00 00 3E 9F 55 09 40 BE
can0  700   [8]  00 00 91 A0 ED 09 17 BE

Por lo que yo sé, normalmente, los datos en los mensajes CAN siguen este formato: un byte para los datos reales, un byte para la cantidad de desbordamientos.

Por ejemplo, tomemos un int sin signo de un valor de 2000. Suponiendo que el byte n.º 0 es para desbordamientos, el byte n.º 1 es para datos reales, obtenemos:

CAN message -> [07, D0, x, x, x, x, x, x]

07 indicando que ha habido 7 desbordamientos, D0 indicando que el resto es 208, por tanto:

7*255 + 208 = 2000

Entiendo cómo hacerlo con valores sin firmar. Pero esta vez en mi escenario estoy tratando con valores firmados. Supongo que un byte es para desbordamientos, un byte es para el resto, sin embargo, no estoy seguro.

  1. ¿Cómo se calculan los desbordamientos para valores con signo? ¿Es overflow += 1 cuando el valor > 127 y overflow -= cuando el valor < -128? ¿Tiene sentido que los desbordamientos tengan firma?

  2. ¿Cómo puedo convertir estos bytes en decimal con signo en C/C++? Digamos que mi valor de bytes en hexadecimal es 91. La última vez que intenté almacenarlo en int y lo imprimí, imprimió 145 (binario normal) y no -111 (complemento a 2). ¿Cómo puedo hacer cumplir el complemento de 2 (si tiene sentido) en mi código?

  3. ¿Podría estar interpretando mal el formato de bytes? Todo lo que dice es que estos dos bytes representan el ángulo de dirección y que el ángulo de dirección es int16_t. He monitoreado su cambio en tiempo real y uno de ellos cambia erráticamente, casi como saltando de 00 a FF, mientras que el otro aumenta/disminuye lentamente y casi linealmente con el tiempo. ¿Ideas?

Estoy realmente atascado aquí, ni siquiera sé si voy en la dirección correcta. ¡Cualquier sugerencia y ayuda son realmente apreciadas!

¿Están en binario o hexadecimal? Hex es una técnica para representar datos binarios utilizando caracteres imprimibles. ¿Está recibiendo un mensaje de texto que debe convertirse de hexadecimal a binario, o está recibiendo los datos binarios sin procesar?
Los "desbordamientos" solo se consideran en todo el rango de valores para el tamaño de palabra. Los bytes más altos dentro de la misma palabra son solo eso, bytes más altos.
@mkeith, si observa mi breve segmento de ejemplo, está claramente en hexadecimal. Los bytes 2 y 3 juntos representan un único valor int de 16 bits. Solo quiero saber cómo hacerlo correctamente.
@IgnacioVazquez-Abrams, no estoy 100% seguro de lo que está tratando de decir... ¿Quiere decir que en lugar de desbordamientos tenemos el byte 3, que son los 8 bits más altos del valor de 16 bits, mientras que el byte 2 son los 8 bits más bajos?
"Inferior" y "superior" depende de la endianidad de los datos, pero (por ejemplo) no tiene 0x12 0x34, tiene 0x1234.
@IgnacioVazquez-Abrams, soy consciente de eso. Por cierto, es un sistema little-endian. Entonces, ¿el byte 2 es LH, el byte 3 es UH? ¿No se suele hacer con desbordamientos? Por ejemplo, el byte 3 son sus datos, y cuando se desborda, ¿se incrementaría el valor en el byte 2?
Sin ofender, pero toda la pregunta no me inspira confianza de que sepas de lo que estás hablando. Mi sospecha sigue siendo que los valores que se le pasan son en realidad binarios y no hexadecimales. Los está mostrando en hexadecimal porque no se pueden mostrar en formato binario ya que el binario no se puede imprimir. Su pregunta es sobre cómo manipular datos binarios de un solo byte en un entero con signo correcto de dos bytes de ancho.
Nunca se hace con desbordamientos. Es un solo número .
@mkeith seguro, están en binario. Todo en la capa física se hace en binario. Los datos que tengo están en un archivo de Excel, que se extrajo de un script que representaba los datos en hexadecimal. Mi pregunta es más sobre la interpretación de los bytes en el mensaje que representa el valor físico. Como ha mencionado Ignacio, podrían ser dos bytes que representan las mitades superior e inferior en un valor de 16 bits, mi suposición inicial fue que un byte se usa para el conteo de desbordamiento mientras que el otro byte se usa para la mitad inferior. Además, disculpe por malinterpretar su pregunta.
@IgnacioVazquez-Abrams, gracias por eso, en realidad tiene mucho sentido. No sé de dónde saqué la idea de los desbordamientos, ¡debo haberlo confundido con otra cosa!
@hypomania Los desbordamientos son lo que obtienes cuando sumas dos números y obtienes un acarreo.
El desbordamiento es lo que se obtiene cuando la magnitud del resultado numérico es demasiado grande para el tipo de datos de la variable donde se supone que se almacena el resultado.
"Hasta donde yo sé, por lo general, los datos en los mensajes CAN siguen este formato: un byte para los datos reales, un byte para la cantidad de desbordamientos". ¿De qué diablos estás hablando? Desde una perspectiva general de CAN, esto es una completa tontería. Desde la perspectiva de un número de complemento a 2 estándar, esto también es una completa tontería. ¿Y qué es esta obsesión con los desbordamientos para empezar? ¿Qué te hace pensar que hay desbordamientos?
¿Qué protocolo CAN es este? ¿Cuál es el formato? ¿Qué significan esos bytes? A menos que establezca esto, la pregunta no puede responderse simplemente. No se parece a ningún estándar común.

Respuestas (6)

Aclarar algunos malentendidos probablemente ayudará.

Primero, sus datos son un valor de 16 bits. No hay "desbordamientos" ni "datos reales": los 16 bits simplemente se dividen en dos piezas de 8 bits (bytes). Para obtener el valor binario correcto, debe concatenar los bytes. En C, puede hacerlo comenzando con valores sin signo y usando operadores bit a bit, como este:

uint16_t highbyte, lowbyte, data; 
highbyte = get_can_byte();   //Do whatever you normally do to get the bytes
lowbyte = get_can_byte();

data = highbyte<<8 | lowbyte;

Ahora tiene el valor correcto de 16 bits. Si desea que el resultado esté firmado, simplemente puede convertir el valor en un tipo firmado:

int16_t signed_data;

signed_data = (int16_t)(highbyte<<8 | lowbyte);

Para responder a sus preguntas específicas:

  1. Los desbordamientos producen los mismos valores binarios independientemente de si su variable está firmada o no. Por ejemplo, 0x7fff + 1 == 0x8000. Si interpreta 0x8000 como 32768 o -32768 depende del tipo de datos. (Tenga en cuenta que el desbordamiento de enteros con signo no está técnicamente definido en el estándar C; esto es lo que hará su CPU).

  2. Como dije, todo depende del tipo de datos:

    uint16_t ui = 0xffff;    //65535
    int16_t   i = 0xffff;    //   -1
    
  3. Lo que estás describiendo suena razonable para una medición de alta precisión. Un cambio de +1 o -1 en su byte superior representa solo 1/256 de su rango de dirección total. El byte bajo será extremadamente sensible al ángulo exacto.

Tenga en cuenta que es mejor convertir a un valor con signo lo más tarde posible y hacerlo con una conversión explícita. Cosas como los operadores bit a bit pueden comportarse de manera diferente o producir un comportamiento indefinido cuando se usan en valores firmados. En general, siempre que esté manipulando bits, use valores sin firmar.

Vea el comentario de Jonk sobre mi respuesta. Creo que C no garantiza que el elenco de sin firmar a firmado hará lo correcto. Sin embargo, creo que realmente funcionará en la mayoría de las arquitecturas modernas con compiladores modernos. Si el OP quiere hacerlo de forma portátil, sugeriría preguntar en un foro de C, no aquí.
Pensé en agregar una versión pedante que cumpliera con los estándares, pero no quiero confundir al pobre interrogador. :-) Si logran encontrar una máquina donde falla, la descubrirán muy rápidamente. En mi opinión, si está jugando con los formatos de números y el código dependiente de endianness, de todos modos ha renunciado a la portabilidad universal.
Sí, Adán. Aceptar.
Gracias por una explicación detallada y clara. Me ha mostrado la dirección correcta y creo que tengo claro qué hacer a continuación, ¡gracias!

Estás complicando demasiado esto.

can0  700   [8]  00 00 *99 93* 55 0B EF BD -> 0x9399 -> -27751  
can0  700   [8]  00 00 *95 95* 10 0C 17 BE -> 0x9595 -> -27243  
can0  700   [8]  00 00 *6F 97* FB 0A 17 BE -> 0x976F -> -26769  
can0  700   [8]  00 00 *39 99* 5C 0A 40 BE -> 0x9939 -> -26311  

etc ...

Largo día de trabajo, mis pensamientos estaban desordenados. Gracias por esto, lo compliqué demasiado :)

La explicación más probable de por qué un byte parece aleatorio y otro cambia linealmente es que el byte lineal es el byte de orden superior (los desbordamientos). El byte de orden inferior parece aleatorio porque cambia rápidamente en comparación con la tasa de actualización.

Diría que el byte 3 es el byte de orden superior y el byte 2 es el byte de orden inferior. En enteros con signo que utilizan la representación de complemento a dos (que es casi universal hoy en día), los números negativos tendrán bits de orden superior establecidos en 1. Mirando el hexadecimal, si el byte de orden superior es mayor que 0x80, es un número negativo. Entonces, en tu ejemplo, todos los números son números negativos. 0xffff es -1, 0xfffe es -2, etc. Lo que tienes en tu fragmento es:

  • 0x9399
  • 0x9995
  • 0x976f
  • etc...

Dado que todos esos números son mayores que 0x8000, todos son negativos.

Lo mejor que puede hacer es estudiar la representación en complemento a dos de los enteros con signo. Parece que sabes cómo hacer la conversión uint16_t. Lo que puede funcionar para usted es simplemente hacer eso, luego asignar el valor a una variable de int16_t. El compilador podría convertirlo correctamente para usted (no está garantizado por la especificación C, pero muchos compiladores lo hacen de esa manera).

Espero que esto te ayude un poco.

Para almacenamiento del mismo tamaño, C garantiza la conversión de int con signo a int sin signo. (Lo que no suena como la dirección que quiere el OP). C NO garantiza la conversión de int sin firmar a int con signo; solo lo garantiza donde el valor de int sin signo tiene representación en el int con signo (positivo y encaja). Valores más grandes se puede convertir en basura aleatoria en la medida en que lo permita el estándar. Por supuesto, ningún compilador de C hace eso hasta donde yo sé. Pero según la especificación, podrían hacerlo si quisieran. Tal como dices, creo.
@jonk, Sí. Creo que está definido por la implementación. Podría ser indefinido. Pero funciona la mayor parte del tiempo en máquinas de complemento a dos. Hacerlo de tal manera que sea totalmente portátil es un dolor en el culo. Podría convertir a unit32_t, luego hacer algunas pruebas en el número sin firmar y convertirlo en el int16_t correcto sin ningún paso definido de implementación. Pero es un dolor.
Tiene toda la razón, el byte 2 es el byte inferior, el byte 3 es el superior (es un sistema little-endian). El tuyo y todos los demás comentarios me han aclarado mucho las cosas, es mucho más simple de lo que pensé que iba a ser. Para ser honesto, no tengo idea de dónde obtuve la idea de que los desbordamientos de números se almacenan en el byte cercano. Tiene sentido y da los mismos resultados, pero el concepto es totalmente diferente, los desbordamientos se realizan en todo el valor, en lugar del byte inferior. De todos modos, sé lo que estoy haciendo ahora, ¡gracias por su paciencia!
¡Me alegro de que mis comentarios ayudaran! Muchos de nosotros hemos estado haciendo esto durante mucho tiempo, por lo que ahora es fácil. Pero todos hemos tenido que resolverlo en el camino, generalmente con la ayuda de personas que lo han hecho antes. Nunca es fácil al principio. Tienes que averiguarlo.

No sé nada sobre sensores de ángulo de dirección reales en automóviles ...,

pero tenga en cuenta que un sensor de ángulo puede informar múltiples valores de giro. es decir, un giro puede ser 0-1024 y el sensor puede informar +/-32 giros en un número de 16 bits.

¿Por qué alguien haría esto?

Bueno, imagine que el volante está exactamente en el punto de 0/360 grados y cambiando entre 0 y 360 con la vibración de la carretera. ¿Qué sucede si promedia para filtrar el ruido? Obtienes 180 grados, lo cual es completamente incorrecto.

Este siempre es un problema que debe manejarse en alguna parte cuando se usan sensores de ángulo de rotación libre. Algunos sistemas devuelven dos valores de ángulo a 90 grados (sen+cos) para resolver esta ambigüedad. Otros extienden el valor del ángulo a 360+180 grados con histéresis, mientras que algunos hacen acumulación de ángulo de varias vueltas.

Gracias por el punto, lo tendré en cuenta una vez que haga la conversión real y mire la trama.

Las otras respuestas hacen un buen trabajo al explicar el formato del mensaje, pero probablemente lo tomaría así:

union
{
    struct
    {
        uint8_t hi;    //the order of these two depends on the endian-nesss of your specific micro
        uint8_t lo;    //swap if the data is garbled
    };
    uint16_t all;      //or sint16_t if you like: the only difference so far is at what value it wraps to the opposite end of its range
} reading;

uint16_t get_reading()
{
    reading.hi = get_hi_byte();
    reading.lo = get_lo_byte();
    return reading.all;
}

La ventaja aquí es que el cálculo aparente (shift + or) en realidad lo realiza completamente la estructura de la memoria sin ningún esfuerzo real, por lo que incluso un compilador estúpido aún generará un código eficiente. Y lo estás escribiendo directamente de la forma en que realmente sucede, lo que creo que es una gran ventaja para la legibilidad.


Si haces esto mucho, puedes:

typedef union
{
    struct
    {
        uint8_t hi;
        uint8_t lo;
    };
    uint16_t all;
} byte_int;

byte_int steering;
byte_int throttle;
//etc.
El problema con esta solución es que el código se vuelve dependiente de la endianess, y la endianess de la CPU no es necesariamente la misma que la endianess del protocolo. Por lo tanto, la mejor solución es usar cambios de bits. Es una mala idea escribir código dependiente de endianess cuando puede obtener código independiente de endianess en su lugar, por poca o ninguna sobrecarga de CPU adicional.
@Lundin Sí, se vuelve dependiente de endian y el protocolo podría ser diferente de la CPU. (esta última es la razón por la que copiamos un byte a la vez del protocolo) Pero he visto muchos proyectos que nunca dejan sus plataformas originales (la endianess no cambia) y muchos compiladores idiotas. Por ejemplo, esta misma operación - cambio de 16 bits por 8 y luego bit a bit o - es exactamente lo que he visto hacer en el código objeto: recorre 8 veces una función de biblioteca del compilador para cambiar 16 bits por 1 y luego es uno byte a la vez.
Si la plataforma cambia a la endianess opuesta, incluso mi versión tiene exactamente un lugar para arreglarlo si usa el typedef.
Es raro que proyectos completos abandonen la plataforma, pero los archivos fuente individuales lo hacen con frecuencia; de lo contrario, lo está haciendo mal, reinventando la rueda una y otra vez.
@Lundin Puse mi typedefs en un archivo de encabezado separado, que es uno de los dos que corresponden al chip específico. (el otro tiene un montón de definiciones específicas de chip para E/S y funcionalidad básica) Luego, el código fuente anterior se vuelve portátil.

Si tuviera un número decimal como 106, podría decir que tuvo un desbordamiento, donde pasó de 99 y "se desbordó" una vez hasta 100. Puede estar de acuerdo en que sería mejor llamarlo "carry". Entiendes esto, porque en tu publicación multiplicaste 7 * 256 antes de sumar 208 para obtener la respuesta de 2000. (Escribiste 255 pero creo que es un desliz ya que la respuesta fue correcta).

No dijiste si estabas trabajando en C o en cualquier otro lenguaje. Es posible que solo esté mirando un volcado de CAN y resolviendo las cosas en papel. Desde este punto de vista, la forma más sencilla de obtener una respuesta es que si el número es negativo (es decir, 2^15 (32768) o mayor), puede restar 2^16 (65536) de la lectura para obtener la respuesta. Es decir, si tuviera F8 33 y calculara F8 * 256 + 33, obtendría 63539. Entonces, 63539 - 65536 = -1997, que es el valor representado por F833 cuando se considera un número con signo.

La información presentada en las otras respuestas es válida y en su mayoría recomendada, pero esto puede ayudarlo a comenzar y podría ser apropiado para alguien que aún no ha abordado el final de la programación. También es una buena manera de verificar la respuesta que le da su programa, una vez que lo prueba.