Para código incorporado, por que devo usar os tipos "uint_t" em vez de "unsigned int"?

22

Estou escrevendo um aplicativo em c para um STM32F105, usando o gcc.

No passado (com projetos mais simples), eu sempre variáveis definidas como char, int, unsigned inte assim por diante.

Eu vejo que é comum usar os tipos definidos no stdint.h, tais como int8_t, uint8_t, uint32_t, etc. Isto é verdade em múltiplos API do que estou usando, e também na biblioteca ARM CMSIS de ST.

Eu acredito que entendo por que devemos fazê-lo; para permitir que o compilador otimize melhor o espaço na memória. Espero que possa haver razões adicionais.

No entanto, devido às regras de promoção de número inteiro de c, continuo correndo contra os avisos de conversão sempre que tento adicionar dois valores, fazer uma operação bit a bit etc. O aviso tem algo como conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. A questão é discutida aqui e aqui .

Isso não acontece ao usar variáveis ​​declaradas como intou unsigned int.

Para dar alguns exemplos, é necessário:

uint16_t value16;
uint8_t value8;

Eu teria que mudar isso:

value16 <<= 8;
value8 += 2;

para isso:

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

É feio, mas posso fazê-lo, se necessário. Aqui estão as minhas perguntas:

  1. Existe um caso em que a conversão de não assinado para assinado e de volta para não assinado tornará o resultado incorreto?

  2. Existem outros grandes motivos a favor / contra o uso dos tipos inteiros stdint.h?

Com base nas respostas que estou recebendo, parece que os tipos stdint.h geralmente são os preferidos, mesmo que c seja convertido uintpara inte para trás. Isso leva a uma pergunta maior:

  1. Eu posso evitar os avisos do compilador usando typecasting (por exemplo value16 = (uint16_t)(value16 << 8);). Estou apenas escondendo o problema? Existe uma maneira melhor de fazer isso?
bitsmack
fonte
Use literais não assinados: ie 8ue 2u.
Stop Harming Monica
Obrigado, @OrangeDog, acho que estou entendendo mal. Eu tentei os dois value8 += 2u;e value8 = value8 + 2u;, mas recebo os mesmos avisos.
bitsmack
Use-as de qualquer maneira, para evitar avisos assinados quando você ainda não tiver avisos de largura :)
Pare de prejudicar Monica

Respostas:

11

Um compilador que esteja em conformidade com padrões, intentre 17 e 32 bits, pode legitimamente fazer o que quiser com o seguinte código:

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

Uma implementação que quisesse fazer isso poderia legitimamente gerar um programa que não faria nada, exceto gerar a sequência "Fred" repetidamente em cada pino de porta usando todos os protocolos imagináveis. A probabilidade de um programa ser portado para uma implementação que faria tal coisa é excepcionalmente baixa, mas é teoricamente possível. Se quiser escrever o código acima para garantir que não se envolva em comportamento indefinido, seria necessário escrever a última expressão como (uint32_t)x*xou 1u*x*x. Em um compilador intentre 17 e 31 bits, a última expressão cortaria os bits superiores, mas não se envolveria em comportamento indefinido.

Acho que os avisos do gcc provavelmente estão tentando sugerir que o código escrito não é totalmente 100% portátil. Há momentos em que o código realmente deve ser escrito para evitar comportamentos que seriam indefinidos em algumas implementações, mas em muitos outros casos deve-se simplesmente imaginar que é improvável que o código seja usado em implementações que fariam coisas muito irritantes.

Observe que o uso de tipos como inte shortpode eliminar alguns avisos e corrigir alguns problemas, mas provavelmente criaria outros. A interação entre tipos como uint16_te as regras de promoção inteira de C é nojenta, mas esses tipos ainda são provavelmente melhores do que qualquer alternativa.

supercat
fonte
6

1) Se você converter de um inteiro não assinado para um assinado do mesmo comprimento, sem nenhuma operação intermediária, obterá o mesmo resultado todas as vezes, portanto não há problema aqui. Mas várias operações lógicas e aritméticas estão agindo de maneira diferente em operandos assinados e não assinados.
2) A principal razão para usar stdint.htipos é que o tamanho de tais tipos de bits são definidas e igual em todas as plataformas, o que não é verdade para int, longetc, bem como charnão tem signess padrão, que pode ser com ou sem sinal de padrão. Torna mais fácil manipular os dados sabendo o tamanho exato sem usar verificações e suposições extras.

Eugene Sh.
fonte
2
Os tamanhos int32_te uint32_tsão iguais em todas as plataformas em que estão definidos . Se o processador não tiver um tipo de hardware exatamente igual , esses tipos não serão definidos. Daí a vantagem de intetc. e, talvez, int_least32_tetc.
Pete Becker,
1
@PeteBecker - Essa é sem dúvida uma vantagem, porque os erros de compilação resultantes o tornam imediatamente ciente do problema. Eu prefiro isso do que meus tipos mudando de tamanho em mim.
sapi
@ sapi - em muitas situações, o tamanho subjacente é irrelevante; Programadores C se davam muito bem sem tamanhos fixos por muitos anos.
Pete Becker
6

Como o número 2 de Eugene é provavelmente o ponto mais importante, eu gostaria de acrescentar que é um aviso

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

Também Jack Ganssle parece ser um defensor dessa regra: http://www.ganssle.com/tem/tem265.html

Tom L.
fonte
2
Pena que não existem tipos para especificar "Número inteiro não assinado de N bits que pode ser multiplicado com segurança por qualquer outro número inteiro do mesmo tamanho para gerar o mesmo tamanho de resultado". Regras de promoção inteira interagem horrivelmente com os tipos existentes, como uint32_t.
Supercat
3

Uma maneira fácil de eliminar os avisos é evitar o uso de -Wconversion no GCC. Eu acho que você precisa habilitar essa opção manualmente, mas se não, você pode usar -Wno-conversion para desativá-la. Você pode ativar avisos para conversões de precisão de sinal e FP através de outras opções , se ainda assim desejar.

Os avisos -Wconversion são quase sempre falsos positivos, e é provavelmente por isso que nem o -Wextra o habilita por padrão. Uma pergunta de estouro de pilha tem muitas sugestões para bons conjuntos de opções. Com base na minha própria experiência, este é um bom lugar para começar:

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

Adicione mais se precisar, mas é provável que não.

Se você precisar manter -Wconversion, poderá reduzir um pouco o seu código digitando apenas o operando numérico:

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

Isso não é fácil de ler sem o destaque da sintaxe.

Adam Haun
fonte
2

em qualquer projeto de software, é muito importante usar definições de tipo portáteis. (até a próxima versão do mesmo compilador precisa dessa consideração.) Um bom exemplo, há vários anos, trabalhei em um projeto em que o compilador atual definia 'int' como 8 bits. A próxima versão do compilador definiu 'int' como 16 bits. Como não usamos definições portáteis para 'int', o ram (efetivamente) dobrou de tamanho e muitas sequências de código que dependiam de um int de 8 bits falharam. O uso de uma definição de tipo portátil teria evitado esse problema (centenas de horas-homem para corrigir).

Richard Williams
fonte
Nenhum código razoável deve ser usado intpara se referir a um tipo de 8 bits. Mesmo que um compilador não-C como o CCS faça isso, um código razoável deve usar charum tipo ou tipo de digitação por 8 bits e um tipo de digitação por tipo (não "longo") por 16 bits. Por outro lado, a transferência de código de algo como o CCS para um compilador real pode ser problemática, mesmo que use typedefs apropriados, pois esses compiladores geralmente são "incomuns" de outras maneiras.
Supercat 23/12
1
  1. Sim. Um número inteiro assinado de n bits pode representar aproximadamente metade do número de números não negativos como um número inteiro não assinado de n bits, e confiar nas características de estouro é um comportamento indefinido, para que tudo possa acontecer. A grande maioria dos processadores atuais e anteriores usa dois complementos, de modo que muitas operações fazem a mesma coisa em tipos integrais assinados e não assinados, mas mesmo assim nem todas as operações produzirão resultados idênticos em termos de bits. Você está realmente pedindo problemas extras mais tarde, quando não consegue entender por que seu código não está funcionando conforme o esperado.

  2. Embora int e unsigned tenham tamanhos de implementação definidos, eles geralmente são escolhidos "de maneira inteligente" pela implementação, por motivos de tamanho ou velocidade. Eu geralmente os mantenho, a menos que tenha uma boa razão para fazer o contrário. Da mesma forma, ao considerar se devo usar int ou não assinado, geralmente prefiro int, a menos que tenha um bom motivo para fazê-lo de outra forma.

Nos casos em que eu realmente preciso de um melhor controle sobre o tamanho ou a assinatura de um tipo, geralmente preferirei usar um typedef definido pelo sistema (size_t, intmax_t etc.) ou criar meu próprio typedef que indica a função de um determinado tipo (prng_int, adc_int, etc.).

helloworld922
fonte
0

Geralmente, o código é usado no ARM thumb e AVR (e x86, powerPC e outras arquiteturas), e 16 ou 32 bits podem ser mais eficientes (nos dois sentidos: flash e ciclos) no STM32 ARM, mesmo para uma variável que se encaixa em 8 bits ( 8 bits é mais eficiente no AVR) . No entanto, se a SRAM estiver quase cheia, a reversão para 8 bits para vars globais pode ser razoável (mas não para vars locais). Para portabilidade e manutenção (especialmente para vars de 8 bits), há vantagens (sem nenhuma desvantagem) em especificar o tamanho MÍNIMO adequado , em vez do tamanho exato e do typedef em um local .h (geralmente em ifdef) para ajustar (possivelmente uint_fast8_t / uint_least8_t) durante o tempo de transferência / compilação, por exemplo:

// 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)

A biblioteca GNU ajuda um pouco, mas normalmente os typedefs fazem sentido de qualquer maneira:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// mas uint_fast8_t para AMBOS quando SRAM não é uma preocupação.

Marcell
fonte