Para el código incrustado, ¿por qué debería usar los tipos "uint_t" en lugar de "unsigned int"?

22

Estoy escribiendo una aplicación en c para un STM32F105, usando gcc.

En el pasado (con proyectos más simples), siempre me he definido como variables char, int, unsigned int, y así sucesivamente.

Veo que es común el uso de los tipos definidos en stdint.h, como por ejemplo int8_t, uint8_t, uint32_t, etc. Esto es cierto en múltiples API de que estoy usando, y también en la biblioteca de ARM CMSIS de ST.

Creo que entiendo por qué deberíamos hacerlo; para permitir que el compilador optimice mejor el espacio de memoria. Espero que pueda haber razones adicionales.

Sin embargo, debido a las reglas de promoción de enteros de c, sigo encontrándome con advertencias de conversión cada vez que trato de agregar dos valores, hacer una operación bit a bit, etc. La advertencia dice algo así conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. El tema se discute aquí y aquí .

No sucede cuando se usan variables declaradas como into unsigned int.

Para dar un par de ejemplos, dado esto:

uint16_t value16;
uint8_t value8;

Tendría que cambiar esto:

value16 <<= 8;
value8 += 2;

a esto:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

Es feo, pero puedo hacerlo si es necesario. Aquí están mis preguntas:

  1. ¿Hay algún caso en el que la conversión de sin signo a firmado y de regreso a sin signo hará que el resultado sea incorrecto?

  2. ¿Hay alguna otra razón importante a favor / en contra del uso de los tipos enteros stdint.h?

Según las respuestas que recibo, parece que generalmente se prefieren los tipos stdint.h, aunque c se convierte de un lado uinta intotro. Esto lleva a una pregunta más grande:

  1. Puedo evitar las advertencias del compilador usando la conversión de texto (por ejemplo value16 = (uint16_t)(value16 << 8);). ¿Solo estoy ocultando el problema? ¿Hay una mejor manera de hacerlo?
bitsmack
fuente
Utilice literales sin signo: es decir, 8uy 2u.
Deja de dañar a Monica el
Gracias, @OrangeDog, creo que estoy malentendido. Intenté ambos value8 += 2u;y value8 = value8 + 2u;, pero recibo las mismas advertencias.
bitsmack
Úselos de todos modos, para evitar advertencias firmadas cuando aún no tiene advertencias de ancho :)
Deje de dañar a Monica el

Respuestas:

11

Un compilador que cumpla con los estándares y que tuviera intentre 17 y 32 bits puede legítimamente hacer lo que quiera con el siguiente código:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

Una implementación que quisiera hacerlo podría generar legítimamente un programa que no haría nada más que generar la cadena "Fred" repetidamente en cada pin de puerto utilizando todos los protocolos imaginables. La probabilidad de que un programa sea portado a una implementación que haría tal cosa es excepcionalmente baja, pero es teóricamente posible. Si desea escribir el código anterior para garantizar que no se involucre en Comportamiento indefinido, sería necesario escribir la última expresión como (uint32_t)x*xo 1u*x*x. En un compilador donde intestá entre 17 y 31 bits, la última expresión cortaría los bits superiores, pero no se involucraría en Comportamiento Indefinido.

Creo que las advertencias de gcc probablemente están tratando de sugerir que el código tal como está escrito no es completamente 100% portátil. Hay momentos en que el código realmente debería escribirse para evitar comportamientos que estarían indefinidos en algunas implementaciones, pero en muchos otros casos uno simplemente debería pensar que es poco probable que el código se use en implementaciones que harían cosas demasiado molestas.

Tenga en cuenta que el uso de tipos como inty shortpuede eliminar algunas advertencias y solucionar algunos problemas, pero probablemente crearía otros. La interacción entre tipos como uint16_ty las reglas de promoción de enteros de C son asquerosas, pero estos tipos probablemente sean mejores que cualquier otra alternativa.

Super gato
fuente
6

1) Si acaba de convertir de entero sin signo a signo de la misma longitud de un lado a otro, sin ninguna operación intermedia, obtendrá el mismo resultado cada vez, así que no hay problema aquí. Pero varias operaciones lógicas y aritméticas están actuando de manera diferente en operandos con y sin signo.
2) La razón principal para usar stdint.htipos es que el tamaño de bit de dichos tipos está definido e igual en todas las plataformas, lo cual no es cierto para int, longetc., además de charno tener firma estándar, puede ser firmado o no firmado por defecto. Hace más fácil manipular los datos conociendo el tamaño exacto sin usar suposiciones y comprobaciones adicionales.

Eugene Sh.
fuente
2
Los tamaños int32_ty uint32_tson iguales en todas las plataformas en las que se definen . Si el procesador no tiene un tipo de hardware que coincida exactamente , estos tipos no están definidos. De ahí la ventaja de intetc. y, quizás, int_least32_tetc.
Pete Becker
1
@PeteBecker: Sin embargo, eso podría decirse que es una ventaja, porque los errores de compilación resultantes lo hacen inmediatamente consciente del problema. Prefiero eso a que mis tipos cambien de tamaño en mí.
sapi
@sapi: en muchas situaciones, el tamaño subyacente es irrelevante; Los programadores de C se llevaron bien sin tamaños fijos durante muchos años.
Pete Becker
6

Dado que el # 2 de Eugene es probablemente el punto más importante, solo me gustaría agregar que es un aviso en

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

También Jack Ganssle parece ser partidario de esa regla: http://www.ganssle.com/tem/tem265.html

Tom L.
fuente
2
Es una lástima que no haya ningún tipo para especificar "Entero sin signo de N bits que puede multiplicarse de forma segura por cualquier otro entero del mismo tamaño para obtener el mismo resultado". Las reglas de promoción de enteros interactúan horriblemente con los tipos existentes como uint32_t.
supercat
3

Una manera fácil de eliminar las advertencias es evitar el uso de -Wconversion en GCC. Creo que debe habilitar esta opción manualmente, pero si no, puede usar -Wno-conversion para deshabilitarla. Puede habilitar advertencias para conversiones de precisión de signos y FP a través de otras opciones , si aún así las desea.

Las advertencias de -Wconversion son casi siempre falsos positivos, por lo que probablemente ni siquiera -Wextra lo habilita por defecto. Una pregunta de desbordamiento de pila tiene muchas sugerencias para buenos conjuntos de opciones. Según mi propia experiencia, este es un buen lugar para comenzar:

-std = c99 -pedantic -Wall -Wextra -Wshadow

Agregue más si los necesita, pero es probable que no lo haga.

Si debe mantener -Wconversion, puede acortar un poco su código simplemente escribiendo el operando numérico:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

Sin embargo, eso no es fácil de leer sin resaltar la sintaxis.

Adam Haun
fuente
2

En cualquier proyecto de software, es muy importante utilizar definiciones de tipo portátiles. (incluso la próxima versión del mismo compilador necesita esta consideración). Un buen ejemplo, hace varios años trabajé en un proyecto en el que el compilador actual definía 'int' como 8 bits. La siguiente versión del compilador definió 'int' como 16 bits. Como no habíamos usado ninguna definición portátil para 'int', el RAM (efectivamente) duplicó su tamaño y muchas secuencias de código que dependían de un int de 8 bits fallaron. El uso de una definición de tipo portátil habría evitado ese problema (cientos de horas hombre para arreglar).

Richard Williams
fuente
No debe usarse ningún código razonable intpara referirse a un tipo de 8 bits. Incluso si un compilador que no sea C como CCS lo hace, un código razonable debería usar uno charo un tipo de tipo dedefinido para 8 bits, y un tipo de tipo de definición (no "largo") para 16 bits. Por otro lado, portar código de algo como CCS a un compilador real puede ser problemático incluso si utiliza los debidos typedefs, ya que tales compiladores son a menudo "inusuales" de otras maneras.
supercat
1
  1. Sí. Un entero con signo de n bits puede representar aproximadamente la mitad del número de números no negativos como un entero sin signo de n bits, y confiar en las características de desbordamiento es un comportamiento indefinido, por lo que puede suceder cualquier cosa. La gran mayoría de los procesadores actuales y pasados ​​utilizan dos complementos, por lo que muchas operaciones hacen lo mismo en tipos integrales con signo y sin signo, pero aun así no todas las operaciones producirán resultados idénticos en términos de bits. Realmente está pidiendo problemas adicionales más adelante cuando no puede entender por qué su código no funciona como se esperaba.

  2. Si bien int y unsigned tienen tamaños definidos de implementación, a menudo estos son elegidos "inteligentemente" por la implementación ya sea por razones de tamaño o velocidad. Por lo general, me quedo con estos a menos que tenga una buena razón para hacer lo contrario. Del mismo modo, cuando considero si usar int o unsigned generalmente prefiero int a menos que tenga una buena razón para hacerlo de otra manera.

En los casos en que realmente necesito un mejor control sobre el tamaño o la firma de un tipo, generalmente preferiré usar un typedef definido por el sistema (size_t, intmax_t, etc.) o hacer mi propio typedef que indica la función de un determinado tipo (prng_int, adc_int, etc.).

helloworld922
fuente
0

A menudo, el código se usa en ARM thumb y AVR (y x86, powerPC y otras arquitecturas), y 16 o 32 bits pueden ser más eficientes (en ambos sentidos: flash y ciclos) en STM32 ARM incluso para una variable que cabe en 8 bits ( 8 bits es más eficiente en AVR) . Sin embargo, si la SRAM está casi llena, volver a 8 bits para los vars globales puede ser razonable (pero no para los vars locales). Para la portabilidad y el mantenimiento (especialmente para vars de 8 bits), tiene ventajas (sin inconvenientes) para especificar el tamaño MÍNIMO adecuado , en lugar del tamaño exacto y typedef en un lugar .h (generalmente bajo ifdef) para ajustar (posiblemente uint_fast8_t / uint_least8_t) durante la transferencia / tiempo de compilación, por ejemplo:

// apparently uint16_t is just as efficient as 32 bit on STM32, but 8 bit is punished (with more flash and cycles)
typedef uint16_t uintG8_t; // 8bit if SRAM is scarce (use fol global vars that fit in 8 bit)
typedef uint16_t uintL8_t; // 8bit on AVR (local var, 16 or 32 bit is more efficient on STM + less flash)
// might better reserve 32 bits on some arch, STM32 seems efficient with 16 bits:
typedef uint16_t uintG16_t; // 16bit if SRAM is scarce (use fol global vars that fit in 16 bit)
typedef uint16_t uintL16_t; // 16bit on AVR (local var, 16 or 32 bit whichever is more efficient on other arch)

La biblioteca GNU ayuda un poco, pero normalmente los typedefs tienen sentido de todos modos:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// pero uint_fast8_t para AMBOS cuando SRAM no es una preocupación.

Marcell
fuente