Por que printf () é ruim para depurar sistemas incorporados?

16

Eu acho que é uma coisa ruim tentar depurar um projeto baseado em microcontrolador usando printf().

Entendo que você não tem um local predefinido para saída e que isso pode consumir pinos valiosos. Ao mesmo tempo, vi pessoas consumindo um pino UART TX para enviar para o terminal IDE com uma DEBUG_PRINT()macro personalizada .

tarabyte
fonte
12
Quem te disse que isso é ruim? "Geralmente não é o melhor" não é o mesmo que um "ruim" não qualificado.
Spehro Pefhany
6
Toda essa conversa sobre o quanto de sobrecarga existe, se tudo o que você precisa fazer é enviar mensagens "Estou aqui", você não precisa de printf, apenas uma rotina para enviar uma string para um UART. Além disso, o código para inicializar o UART provavelmente possui menos de 100 bytes de código. Adicionar a capacidade de gerar alguns valores hexadecimais não aumentará tanto assim.
precisa saber é o seguinte
7
@ChetanBhargava - os arquivos de cabeçalho C geralmente não adicionam código ao executável. Eles contêm declarações; se o restante do código não usar as coisas declaradas, o código para essas coisas não será vinculado. Se você usar printf, é claro, todo o código necessário para implementar printfserá vinculado ao executável. Mas isso é porque o código o usou, não por causa do cabeçalho.
Pete Becker
2
@ChetanBhargava Você não precisa nem incluir <stdio.h> se rolar sua própria rotina simples para gerar uma string como eu descrevi (caracteres de saída para o UART até que você veja '\ 0') '
tcrosley
2
@ tcrosley Eu acho que esse conselho provavelmente é discutível se você tiver um bom compilador moderno, se você usar o printf no caso simples sem uma string de formato gcc e a maioria dos outros substituí-lo por uma chamada simples mais eficiente que faz o que você descreve.
Validade 04/04

Respostas:

24

Posso apresentar algumas desvantagens do uso de printf (). Lembre-se de que "sistema incorporado" pode variar de algo com algumas centenas de bytes de memória de programa a um sistema completo acionado por QNX RTOS montado em rack com gigabytes de RAM e terabytes de memória não volátil.

  • Requer algum lugar para enviar os dados. Talvez você já tenha uma porta de depuração ou programação no sistema, talvez não. Se você não o fizer (ou o que você possui não estiver funcionando), não será muito útil.

  • Não é uma função leve em todos os contextos. Isso pode ser um grande problema se você tiver um microcontrolador com apenas alguns K de memória, porque vincular no printf pode consumir 4K por si só. Se você tem um microcontrolador de 32K ou 256K, provavelmente não é um problema, muito menos se você tiver um grande sistema incorporado.

  • É pouco ou nenhum uso para encontrar certos tipos de problemas relacionados à alocação ou interrupções de memória e pode alterar o comportamento do programa quando as instruções são incluídas ou não.

  • É bastante inútil para lidar com coisas sensíveis ao tempo. Você se sairia melhor com um analisador lógico e um osciloscópio ou um analisador de protocolo ou mesmo um simulador.

  • Se você tem um programa grande e precisa recompilar muitas vezes, à medida que muda as instruções printf e as altera, você pode perder muito tempo.

Para que serve: é uma maneira rápida de gerar dados de forma pré-formatada, para que todo programador C saiba como usar a curva de aprendizado zero. Se você precisar cuspir uma matriz para o filtro Kalman que está depurando, pode ser bom cuspi-lo em um formato que o MATLAB possa ler. Certamente melhor do que procurar os locais de RAM, um de cada vez, em um depurador ou emulador .

Eu não acho que seja uma flecha inútil na aljava, mas deve ser usada com moderação, juntamente com o gdb ou outros depuradores, emuladores, analisadores lógicos, osciloscópios, ferramentas de análise de código estático, ferramentas de cobertura de código e assim por diante.

Spehro Pefhany
fonte
3
A maioria das printf()implementações não é segura para threads (ou seja, não re-participante), o que não é um bom negócio, mas é algo a ter em mente ao usá-la em um ambiente multithread.
JRobert #
11
O @JRobert traz um bom argumento ... e mesmo em um ambiente sem sistema operacional, é difícil fazer muita depuração direta de ISRs. Obviamente, se você estiver fazendo matemática printf () ou de ponto flutuante em um ISR, a abordagem provavelmente está desativada.
Spehro Pefhany
@JRobert Que ferramentas de depuração os desenvolvedores de software que trabalham em um ambiente multithread (em uma configuração de hardware onde o uso de analisadores lógicos e osciloscópios não é prático) possuem?
Minh Tran
11
No passado, eu rolei meu próprio printf () com segurança para threads; usou os pés descalços put () ou putchar () equivalentes para cuspir dados muito concisos em um terminal; dados binários armazenados em uma matriz que eu despejei e interpretei após a execução do teste; usou uma porta de E / S para piscar um LED ou gerar pulsos para fazer medições de tempo com um osciloscópio; cuspir um número para um D / A e medido com VOM ... A lista é tão longa quanto a sua imaginação e inversamente tão grande quanto o seu orçamento! :)
JRobert
19

Além de outras respostas, o ato de enviar dados para uma porta a taxas de transmissão serial pode ser simplesmente lento com relação ao tempo de loop e afetar a maneira como o restante do programa funciona (como QUALQUER depuração processo).

Como outras pessoas têm lhe dito, não há nada de "ruim" em usar essa técnica, mas, como muitas outras técnicas de depuração, tem suas limitações. Desde que você conheça e consiga lidar com essas limitações, pode ser um recurso extremamente conveniente para ajudá-lo a corrigir seu código.

Os sistemas embarcados têm uma certa opacidade que, em geral, torna a depuração um pouco problemática.

Scott Seidman
fonte
8
+1 para "sistemas embarcados têm uma certa opacidade". Embora eu tema que essa afirmação possa ser compreensível apenas para quem tem experiência decente em trabalhar com o incorporado, ela faz um resumo conciso e agradável da situação. É quase uma definição de "incorporado", na verdade.
N144 #
5

Existem dois problemas principais que você encontrará ao tentar usar printfem um microcontrolador.

Primeiro, pode ser difícil canalizar a saída para a porta correta. Nem sempre. Mas algumas plataformas são mais difíceis que outras. Alguns dos arquivos de configuração podem ser pouco documentados e muitas experiências podem ser necessárias.

O segundo é a memória. Uma printfbiblioteca completa pode ser GRANDE. Às vezes, você não precisa de todos os especificadores de formato, e versões especializadas podem estar disponíveis. Por exemplo, o stdio.h fornecido pelo AVR contém três diferentes printftamanhos e funcionalidades.

Como a implementação completa de todos os recursos mencionados se torna bastante grande, três tipos diferentes vfprintf()podem ser selecionados usando as opções do vinculador. O padrão vfprintf()implementa toda a funcionalidade mencionada, exceto conversões de ponto flutuante. vfprintf()Está disponível uma versão minimizada de que apenas implementa os recursos básicos de conversão de inteiros e cadeias, mas apenas a #opção adicional pode ser especificada usando sinalizadores de conversão (esses sinalizadores são analisados ​​corretamente na especificação de formato, mas simplesmente ignorados).

Eu tive um exemplo em que nenhuma biblioteca estava disponível e eu tinha memória mínima. Portanto, não tive escolha a não ser usar uma macro personalizada. Mas o uso deprintf ou não é realmente um dos que atenderá às suas necessidades.

Embedded.kyle
fonte
O downvoter poderia explicar o que está incorreto na minha resposta para que eu possa evitar meu erro em projetos futuros?
embedded.kyle
4

Para adicionar o que Spehro Pefhany estava dizendo sobre "coisas sensíveis ao tempo": vamos dar um exemplo. Digamos que você tenha um giroscópio a partir do qual o seu sistema embarcado está fazendo 1.000 medições por segundo. Você deseja depurar essas medidas, portanto, é necessário imprimi-las. Problema: imprimi-los faz com que o sistema fique muito ocupado para ler 1.000 medições por segundo, o que faz com que o buffer do giroscópio transborde, o que faz com que dados corrompidos sejam lidos (e impressos). E assim, ao imprimir os dados, você os corrompeu, fazendo pensar que há um erro na leitura dos dados, quando talvez não haja. Um chamado heisenbug.

njahnke
fonte
ri muito! "Heisenbug" é realmente um termo técnico? Eu acho que tem a ver com a medida do estado de partículas e de Heisenburg Princípio ...
Zeta.Investigator
3

A razão maior para não depurar com printf () é que geralmente é ineficiente, inadequado e desnecessário.

Ineficiente: printf () e parentes usam muito flash e RAM em relação ao que está disponível em um pequeno microcontrolador, mas a maior ineficiência está na depuração real. Alterar o que está sendo registrado requer recompilar e reprogramar o destino, o que atrasa o processo. Ele também usa um UART que você poderia estar usando para realizar um trabalho útil.

Inadequado: há tantos detalhes que você pode enviar através de um link serial. Se o programa travar, você não sabe exatamente onde, apenas a última saída concluída.

Desnecessário: Muitos microcontroladores podem ser depurados remotamente. JTAG ou protocolos proprietários podem ser usados ​​para pausar o processador, espiar registros e RAM e até alterar o estado do processador em execução sem precisar recompilar. É por isso que os depuradores são geralmente uma maneira melhor de depurar do que as instruções de impressão, mesmo em um PC com muito espaço e energia.

É lamentável que a plataforma de microcontrolador mais comum para iniciantes, o Arduino, não tenha um depurador. O AVR suporta depuração remota, mas o protocolo debugWIRE da Atmel é proprietário e não documentado. Você pode usar uma placa de desenvolvedor oficial para depurar com o GDB, mas se tiver, provavelmente não está mais preocupado com o Arduino.

Theran
fonte
Você não poderia usar ponteiros de função para brincar com o que está sendo registrado e adicionar um monte de flexibilidade?
Scott Seidman
3

printf () não funciona por si só. Ele chama muitas outras funções e, se você tiver pouco espaço na pilha, poderá não ser capaz de usá-lo para depurar problemas próximos ao seu limite de pilha. Dependendo do compilador e do microcontrolador, a string de formato também pode ser colocada na memória, em vez de referenciada no flash. Isso pode aumentar significativamente se você apimentar seu código com instruções printf. Esse é um grande problema no ambiente do Arduino - iniciantes que usam dezenas ou centenas de instruções printf repentinamente se deparam com problemas aparentemente aleatórios porque estão substituindo sua pilha pela pilha.

Adam Davis
fonte
2
Embora eu aprecie o feedback que o voto negativo fornece, seria mais útil para mim e para os outros se aqueles que discordarem explicassem os problemas com esta resposta. Estamos todos aqui para aprender e compartilhar conhecimento, considere compartilhar o seu.
Adam Davis
3

Mesmo se alguém quiser cuspir dados em alguma forma de console de registro, a printffunção geralmente não é uma maneira muito boa de fazer isso, pois ela precisa examinar a sequência de formato passada e analisá-la em tempo de execução; mesmo que o código nunca use outro especificador de formato %04X, o controlador geralmente precisará incluir todo o código necessário para analisar seqüências de formato arbitrárias. Dependendo do controlador exato em uso, pode ser muito mais eficiente usar código como:

void log_string(const char *st)
{
  int ch;
  do
  {
    ch = *st++;
    if (ch==0) break;
    log_char(ch);
  } while(1);
}
void log_hexdigit(unsigned char d)
{
  d&=15;
  if (d > 9) d+=7;
  log_char(d+'0');
}
void log_hexbyte(unsigned char b)
{ log_hexdigit(b >> 4); log_hexdigit(b); }
void log_hexi16(uint16_t s)
{ log_hexbyte(s >> 8); log_hexbyte(s); }
void log_hexi32(uint32_t i)
{ log_hexbyte(i >> 24); log_hexbyte(i >> 16); log_hexbyte(i >> 8); log_hexbyte(i); }
void log_hexi32p(uint32_t *p) // On a platform where pointers are less than 32 bits
{ log_hexi32(*p); }

Em alguns microcontroladores PIC, log_hexi32(l)provavelmente levaria 9 instruções e poderia levar 17 (se lestiver no segundo banco), enquanto log_hexi32p(&l)levaria 2. A log_hexi32pfunção em si poderia ser escrita para ter cerca de 14 instruções, de modo que se pagaria se chamada duas vezes .

supercat
fonte
2

Um ponto que nenhuma das outras respostas mencionou: Em um micro básico (IE, há apenas o loop main () e talvez alguns ISRs em execução a qualquer momento, não um sistema operacional multi-threaded) se ele travar / parar / ficar preso em um loop, sua função de impressão simplesmente não acontecerá .

Além disso, as pessoas disseram "não use printf" ou "stdio.h ocupa muito espaço", mas não tem muita alternativa - o embedded.kyle faz menção a alternativas simplificadas, e esse é exatamente o tipo de coisa que você provavelmente deveria ser normalmente, em um sistema embarcado básico. Uma rotina básica para extrair alguns caracteres do UART pode ser alguns bytes de código.

John U
fonte
Se a sua impressão não acontecer, você aprendeu bastante sobre onde o seu código é problemático.
21814 Scott Seidman
Supondo que você tenha apenas uma impressão que possa acontecer, sim. Mas interrupções pode disparar centenas de vezes no tempo que leva uma chamada printf () para obter qualquer coisa fora do UART
John L