O rake mais comum ao usar printf em programas para microcontroladores

De tempos em tempos, em meus projetos, tenho que usar o printf em conjunto com uma porta serial (UART ou uma abstração via USB que imita uma porta serial). E, como sempre, passa muito tempo entre suas aplicações e consigo esquecer completamente todas as nuances que precisam ser levadas em consideração para que funcionem normalmente em um projeto grande.

Neste artigo, compilei minhas próprias nuances principais que surgem ao usar o printf em programas para microcontroladores, classificadas por evidências do mais óbvio ao completamente não-óbvio.

Breve introdução


De fato, para usar o printf em programas para microcontroladores, basta:
  • inclua o arquivo de cabeçalho no código do projeto;
  • redefina a função do sistema _write para enviar para a porta serial;
  • Descreva os stubs de chamadas do sistema que o vinculador exige (_fork, _wait e outros);
  • use printf chamada no projeto.

De fato, nem tudo é tão simples.

Descreva todos os stubs, não apenas os usados.


A presença de vários links vagos no layout do projeto é surpreendente, mas depois de ler um pouco, fica claro o que e o porquê. Em todos os meus projetos, estou conectando este submódulo . Assim, no projeto principal, redefino apenas os métodos que preciso (apenas _write neste caso) e o restante permanece inalterado.

É importante observar que todos os stubs devem ser funções C. Não é C ++ (ou está envolto em "C" externo). Caso contrário, o layout falhará (lembre-se da mudança de nome durante a montagem com o G ++).

Em _write vem 1 caractere


Apesar do protótipo do método _write ter um argumento que transmite o tamanho da mensagem exibida, ele tem um valor de 1 (na verdade, nós mesmos faremos com que seja sempre 1, mas mais sobre isso posteriormente).
int _write (int file, char *data, int len) { ... } 

Na Internet, muitas vezes você pode ver exatamente essa implementação desse método:
Implementação frequente da função _write
 int uart_putc( const char ch) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); {} USART_SendData(USART2, (uint8_t) ch); return 0; } int _write_r (struct _reent *r, int file, char * ptr, int len) { r = r; file = file; ptr = ptr; #if 0 int index; /* For example, output string by UART */ for(index=0; index<len; index++) { if (ptr[index] == '\n') { uart_putc('\r'); } uart_putc(ptr[index]); } #endif return len; } 


Essa implementação tem as seguintes desvantagens:
  • baixa produtividade;
  • insegurança de streaming;
  • incapacidade de usar a porta serial para outros fins;


Baixo desempenho


O desempenho lento ocorre devido ao envio de bytes usando os recursos do processador: você precisa monitorar o registro de status em vez de usar o mesmo DMA. Para resolver esse problema, você pode preparar o buffer para o envio antecipado e, ao receber o caractere do final da linha (ou preencher o buffer), envie. Este método requer uma memória buffer, mas melhora significativamente o desempenho com o envio frequente.
Exemplo de implementação de _write com um buffer
 #include "uart.h" #include <errno.h> #include <sys/unistd.h> extern mc::uart uart_1; extern "C" { //      uart. static const uint32_t buf_size = 254; static uint8_t tx_buf[buf_size] = {0}; static uint32_t buf_p = 0; static inline int _add_char (char data) { tx_buf[buf_p++] = data; if (buf_p >= buf_size) { if (uart_1.tx(tx_buf, buf_p, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } buf_p = 0; } return 0; } // Putty  \r\n    //    . static inline int _add_endl () { if (_add_char('\r') != 0) { return -1; } if (_add_char('\n') != 0) { return -1; } uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } int _write (int file, char *data, int len) { len = len; //   . if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) { errno = EBADF; return -1; } //     //   \n. if (*data != '\n') { if (_add_char(*data) != 0) { return -1; } } else { if (_add_endl() != 0) { return -1; } } return 1; } } 

Aqui, o objeto uart, uart_1, é responsável por enviar diretamente usando o dma. O objeto usa métodos FreeRTOS para bloquear o acesso de terceiros ao objeto no momento do envio de dados do buffer (recebendo e retornando mutex). Portanto, ninguém pode usar o objeto uart ao enviar de outro thread.
Alguns links:
  • _escreva o código de função como parte de um projeto real aqui
  • a interface da classe uart está aqui
  • implementação da interface da classe uart sob stm32f4 aqui e aqui
  • instanciação da classe uart como parte do projeto aqui


Insegurança de streaming


Essa implementação também permanece desprotegida, já que ninguém incomoda no fluxo FreeRTOS vizinho começar a enviar outra linha para printf e, assim, triturar o buffer que está sendo enviado no momento (o mutex dentro do uart protege o objeto de ser usado em fluxos diferentes, mas os dados não são transmitidos a eles ) Caso exista o risco de que printf de outro encadeamento seja chamado, é necessário implementar um objeto de camada que bloqueie totalmente o acesso ao printf. No meu caso particular, apenas um thread interage com printf, portanto complicações adicionais reduzirão apenas o desempenho (captura e liberação constantes de mutex dentro da camada).

Incapacidade de usar a porta serial para outros fins


Como enviamos somente depois que toda a cadeia foi recebida (ou o buffer está cheio), em vez do objeto uart, você pode chamar o método converter para alguma interface de nível superior para transferência de pacotes subsequente (por exemplo, entrega com garantia de acordo com o protocolo de transmissão semelhante aos pacotes modbus de transação). Isso permitirá que você use um uart para exibir informações de depuração e, por exemplo, para interação do usuário com o console de gerenciamento (se houver um disponível no dispositivo). Basta escrever um descompressor no lado do destinatário.

Por padrão, a saída flutuante não funciona


Se você usar newlib-nano, por padrão printf (assim como todos os seus derivados, como sprintf / snprintf ... e outros) não suportará a saída de valores flutuantes. Isso é facilmente resolvido adicionando os seguintes sinalizadores de vinculador ao projeto.
 SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_") 

Veja a lista completa de sinalizadores aqui .

O programa congela em algum lugar nas entranhas do printf


Essa é outra falha nos sinalizadores do vinculador. Para que o firmware seja configurado com a versão desejada da biblioteca, você deve especificar explicitamente os parâmetros do processador.
 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_") 

Veja a lista completa de sinalizadores aqui .

printf força o microcontrolador a entrar em uma falha grave


Pode haver pelo menos dois motivos:
  • problemas de pilha;
  • problemas com _sbrk;

Problemas de pilha


Esse problema realmente se manifesta ao usar o FreeRTOS ou qualquer outro sistema operacional. O problema está usando o buffer. O primeiro parágrafo dizia que em _write vem 1 byte cada. Para que isso ocorra, você deve proibir o uso de buffer no seu código antes de usar o printf pela primeira vez.
 setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); 

A partir da descrição da função, segue-se que um dos seguintes valores pode ser definido da mesma maneira:
 #define _IOFBF 0 /* setvbuf should set fully buffered */ #define _IOLBF 1 /* setvbuf should set line buffered */ #define _IONBF 2 /* setvbuf should set unbuffered */ 

No entanto, isso pode levar a um estouro da pilha de tarefas (ou interrupções, se de repente você for uma pessoa muito ruim que chama printf de interrupções).

Tecnicamente, é possível organizar pilhas com muito cuidado para cada fluxo, mas essa abordagem requer um planejamento cuidadoso e é difícil detectar os erros que ela carrega. Uma solução muito mais simples é receber um byte cada, armazená-lo em seu próprio buffer e enviá-lo no formato necessário, analisado anteriormente.

Problemas com _sbrk


Esse problema foi para mim pessoalmente o mais implícito. E então, o que sabemos sobre _sbrk?
  • Outro esboço que precisa ser implementado para suportar uma parte considerável das bibliotecas padrão;
  • necessário para alocar memória na pilha;
  • usado por todos os tipos de métodos de biblioteca como malloc, grátis.

Pessoalmente, em meus projetos, em 95% dos casos, eu uso o FreeRTOS com métodos redefinidos new / delete / malloc que usam um monte de FreeRTOS. Portanto, quando aloco memória, tenho certeza de que a alocação está no heap do FreeRTOS, que ocupa uma quantidade predeterminada de memória na área bss. Você pode olhar para a camada aqui . Portanto, puramente tecnicamente, não deve haver problema. Uma função simplesmente não deve ser chamada. Mas vamos pensar, se ela ligar, então onde ela tentará recuperar sua memória?

Lembre-se do layout da RAM do projeto "clássico" para microcontroladores:
  • .data;
  • .bss;
  • espaço vazio
  • pilha inicial.

Em dados, temos os dados iniciais de objetos globais (variáveis, estruturas e outros campos globais do projeto). No bss, campos globais que possuem um valor zero inicial e, com cuidado, um monte de FreeRTOS. É apenas uma matriz na memória. com o qual os métodos do arquivo heap_x.c funcionam. Em seguida é o espaço vazio, após o qual (ou melhor, do final) é a pilha. Porque O FreeRTOS é usado no meu projeto, então essa pilha é usada apenas até o agendador iniciar. E, portanto, seu uso, na maioria dos casos, é limitado ao colobyte (na verdade, geralmente um limite de 100 bytes).

Mas onde, então, a memória é alocada usando _sbrk? Veja quais variáveis ​​ela usa no script vinculador.
 void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ... 

Agora, nós os encontramos no script do vinculador (meu script é um pouco diferente do que o st fornece, no entanto, essa parte é praticamente a mesma):
 __stack = ORIGIN(SRAM) + LENGTH(SRAM); __main_stack_size = 1024; __main_stack_limit = __stack - __main_stack_size; ...  flash,    ... .bss (NOLOAD) : ALIGN(4) { ... . = ALIGN(4); __bss_end = .; } >SRAM __heap_start = __bss_end; __heap_end = __main_stack_limit; 

Ou seja, ele usa memória entre a pilha (1 kb de 0x20020000 para baixo com 128 kb de RAM) e bss.

Entendido. Mas ele tinha uma redefinição dos métodos malloc, free e outros. Use _sbrk depois de tudo não é necessário? Como se viu, uma obrigação. Além disso, esse método não usa printf, mas o método para definir o modo de buffer - setvbuf (ou melhor, _malloc_r, que não é declarado como uma função fraca na biblioteca. Ao contrário do malloc, que pode ser facilmente substituído).

Como eu tinha certeza de que o sbrk não era usado, coloquei um monte de FreeRTOS (seção bss) próximo à pilha (porque sabia com certeza que a pilha era usada 10 vezes menos que o necessário).

Soluções para o problema 3:
  • recuo entre bss e a pilha;
  • substitua _malloc_r para que _sbrk não seja chamado (separe um método da biblioteca);
  • reescreva o sbrk via malloc e gratuitamente.

Eu decidi pela primeira opção, pois não foi possível substituir o _malloc_r padrão (que está dentro da libg_nano.a (lib_a-nano-mallocr.o)) (o método não foi declarado como __attribute__ ((fraco)), mas para excluir apenas uma função da bi-biblioteca Não consegui vincular). Eu realmente não queria reescrever o sbrk para uma ligação.

A solução final foi alocar partições separadas na RAM para a pilha inicial e _sbrk. Isso garante que as seções não sejam empilhadas umas sobre as outras durante a fase de configuração. Dentro do sbrk, há também uma verificação para sair da seção. Eu tive que fazer uma pequena correção para que, ao detectar uma transição para o exterior, o fluxo travasse em um loop while (já que o uso do sbrk ocorre apenas no estágio inicial de inicialização e deve ser processado no estágio de depuração do dispositivo).
Mem.ld modificado
 MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1M CCM_SRAM (RW) : ORIGIN = 0x10000000, LENGTH = 64K SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 126K SBRK_HEAP (RW) : ORIGIN = 0x2001F800, LENGTH = 1K MAIN_STACK (RW) : ORIGIN = 0x2001FC00, LENGTH = 1K } 


Alterações para section.ld
 __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP); 

Você pode olhar para mem.ld e section.ld no meu projeto de sandbox neste commit .

UPD 07/12/2019: corrigida a lista de sinalizadores para trabalhar printf com valores flutuantes. Corrigi o link para as CMakeLists em funcionamento com sinalizadores de compilação e layout corrigidos (havia nuances com o fato de que os sinalizadores deveriam ser listados um por um e por meio de ";", enquanto em uma linha ou em linhas diferentes isso não importa).

Source: https://habr.com/ru/post/pt459420/


All Articles