Meu firmware gerou Hardfault! E agora?

Hardfault

Olá caro leitor, saindo um pouco da minha tradicional linha de artigos voltados a processamento de sinais, neste pequeno texto vou ilustrar a vocês um problema que ocorre muito quando estamos em processo de desenvolvimento de firmware, as exceções! Aquele famoso loop infinito que ocorre quando algo deu errado durante um acesso de memória, instrução ou operação matemática indefinida. Tenho visto nas minhas andanças com cada vez mais frequência dúvidas de colegas de como proceder quando o microcontrolador apronta uma dessas, uma vez que o código do vetor de exceção quase nunca está implementado. Vamos ilustrar nosso caso especifico assumindo que o processador alvo seja um popular ARM Cortex-M3/M4.

Hardfault ou exceção de hardware

Bem comum em microcontroladores ARM Cortex, o hardfault é aquela exceção não mascarável (não pode ser desabilitada) que vai ocorrer quando o processador realizar ações como:

  • Acesso desalinhado de memória;
  • Acesso à instrução não definida ou ilegal;
  • Execução de instrução ARM em modo Thumb (ou vice-versa);
  • Operação matemática ilegal (divisão por 0).

A grande verdade é que pra cada um desses eventos existem vetores de exceção próprios, porém mascaráveis, ou seja, podem ser desligados caso aquele programador mais despreocupado não queira uma exceção “enchendo o saco”. O hardfault propriamente dito só vai ocorrer quando o processador verificar no NVIC que uma das demais exceções ocorreu e ao mesmo tempo que essas estão mascaradas para somente então  desviar para o vetor de hardfault, que contém aquele triste loop infinito, e em várias situações deixando o desenvolvedor frustrado.

Como conseguir pistas de “crash” do Firmware pelo Hardfault?

Agora que já sabemos da existência da exceção e como funciona nos processadores derivados do ARMv7M (ARM Cortex), fica a questão de como iniciar a busca por um eventual bug que esteja causando o disparo da exceção (ao leitor mais curioso, o modelo de exceção apresentado é muito similar a outras arquiteturas de 16/32bits). O primeiro ponto é escrever um código de tratamento dentro do vetor de hardfault, de modo que em modo debug o processador faça algumas ações e pare a execução (sim, como se fosse um breakpoint) para permitir que o programador comece a buscar o problema.

O primeiro passo para escrever um bom código de notificação de exceção consiste primeiro em entender o Stack Frame gerado do processador cada vez que uma interrupção ou exceção é gerada. O Stack Frame é um conjunto de registradores que imediatamente após a ocorrência de uma exceção é acessado e salvo na pilha corrente (estando apontado pelo registrador MSP em sistemas bare-metal ou PSP em sistemas com uso de sistema operacional). No caso dos ARM Cortex-M, o Stack Frame gerado pode ser verificado na figura abaixo:

Hardfault - Stack Frame para processadores derivados do ARMv7M
Figura 1 : Stack Frame para processadores derivados do ARMv7M

Os registradores em questão são:

  • xPSR : Registrador de status e controle da aplicação;
  • PC : Contador de programa, aponta para o endereço de ocorrência da interrupção + 1;
  • LR : Chamado de link register, utilizado também para salvamento de endereço de retorno;
  • R12~R0: Registradores de uso geral, frequentemente usados como área de trabalho de um compilador C.

Basicamente, temos acima um snapshot completo do contexto corrente da nossa aplicação. Agora imagine, caro leitor, que seu programa escrito com todo esmero inesperadamente trava e cai dentro do vetor de Hardfault, concorda que podemos acessar o último stack frame gerado e rastrear de que lugar a falha veio?

Para isso, vamos pegar alguns trechos do tradicional startup.S. Esse arquivo é na verdade um “early code” que prepara toda a aplicação C para rodar (basicamente inicializa todas as memórias do microcontrolador e chama a função main). Vejam como ele é originalmente:

/******************************************************************************
*
* The minimal vector table for a Cortex M3. Note that the proper constructs
* must be placed on this to ensure that it ends up at physical address
* 0x0000.0000.
* 
*******************************************************************************/
   .section  .isr_vector,"a",%progbits
  .type  g_pfnVectors, %object
  .size  g_pfnVectors, .-g_pfnVectors
    
    
g_pfnVectors:
  .word  _estack
  .word  Reset_Handler
  .word  NMI_Handler
  .word  HardFault_Handler
  .word  MemManage_Handler
  .word  BusFault_Handler
  .word  UsageFault_Handler
  .word  0
  .word  0
  .word  0
  .word  0
  .word  SVC_Handler
  .word  DebugMon_Handler
  .word  0
  .word  PendSV_Handler
  .word  SysTick_Handler
  
  /* External Interrupts */
  .word     WWDG_IRQHandler                   /* Window WatchDog              */                                        
  .word     PVD_IRQHandler                    /* PVD through EXTI Line detection */                        
  .word     TAMP_STAMP_IRQHandler             /* Tamper and TimeStamps through the EXTI line */            
  .word     RTC_WKUP_IRQHandler               /* RTC Wakeup through the EXTI line */                      
  .word     FLASH_IRQHandler                  /* FLASH                        */                                          
  .word     RCC_IRQHandler                    /* RCC                          */                                            
  .word     EXTI0_IRQHandler                  /* EXTI Line0                   */                        
  .word     EXTI1_IRQHandler                  /* EXTI Line1                   */                          
  .word     EXTI2_IRQHandler                  /* EXTI Line2                   */                          
  .word     EXTI3_IRQHandler                  /* EXTI Line3                   */                          
  .word     EXTI4_IRQHandler                  /* EXTI Line4                   */                          
  .word     DMA1_Stream0_IRQHandler           /* DMA1 Stream 0                */                  
  .word     DMA1_Stream1_IRQHandler           /* DMA1 Stream 1                */                   
  .word     DMA1_Stream2_IRQHandler           /* DMA1 Stream 2                */                   
  .word     DMA1_Stream3_IRQHandler           /* DMA1 Stream 3                */                   
  .word     DMA1_Stream4_IRQHandler           /* DMA1 Stream 4                */                   
  .word     DMA1_Stream5_IRQHandler           /* DMA1 Stream 5                */                   
  .word     DMA1_Stream6_IRQHandler           /* DMA1 Stream 6                */                   
  .word     ADC_IRQHandler                    /* ADC1, ADC2 and ADC3s         */                   
  .word     CAN1_TX_IRQHandler                /* CAN1 TX                      */                         
  .word     CAN1_RX0_IRQHandler               /* CAN1 RX0                     */                          
  .word     CAN1_RX1_IRQHandler               /* CAN1 RX1                     */                          
  .word     CAN1_SCE_IRQHandler               /* CAN1 SCE                     */                          
  .word     EXTI9_5_IRQHandler                /* External Line[9:5]s          */                          
  .word     TIM1_BRK_TIM9_IRQHandler          /* TIM1 Break and TIM9          */         
  .word     TIM1_UP_TIM10_IRQHandler          /* TIM1 Update and TIM10        */         
  .word     TIM1_TRG_COM_TIM11_IRQHandler     /* TIM1 Trigger and Commutation and TIM11 */
  .word     TIM1_CC_IRQHandler                /* TIM1 Capture Compare         */                          
  .word     TIM2_IRQHandler                   /* TIM2                         */                   
  .word     TIM3_IRQHandler                   /* TIM3                         */                   
  .word     TIM4_IRQHandler                   /* TIM4                         */                   
  .word     I2C1_EV_IRQHandler                /* I2C1 Event                   */                          
  .word     I2C1_ER_IRQHandler                /* I2C1 Error                   */                          
  .word     I2C2_EV_IRQHandler                /* I2C2 Event                   */                          
  .word     I2C2_ER_IRQHandler                /* I2C2 Error                   */                            
  .word     SPI1_IRQHandler                   /* SPI1                         */                   
  .word     SPI2_IRQHandler                   /* SPI2                         */                   
  .word     USART1_IRQHandler                 /* USART1                       */                   
  .word     USART2_IRQHandler                 /* USART2                       */                   
  .word     USART3_IRQHandler                 /* USART3                       */                   
  .word     EXTI15_10_IRQHandler              /* External Line[15:10]s        */                          
  .word     RTC_Alarm_IRQHandler              /* RTC Alarm (A and B) through EXTI Line */                 
  .word     OTG_FS_WKUP_IRQHandler            /* USB OTG FS Wakeup through EXTI line */                       
  .word     TIM8_BRK_TIM12_IRQHandler         /* TIM8 Break and TIM12         */         
  .word     TIM8_UP_TIM13_IRQHandler          /* TIM8 Update and TIM13        */         
  .word     TIM8_TRG_COM_TIM14_IRQHandler     /* TIM8 Trigger and Commutation and TIM14 */
  .word     TIM8_CC_IRQHandler                /* TIM8 Capture Compare         */                          
  .word     DMA1_Stream7_IRQHandler           /* DMA1 Stream7                 */                          
  .word     FSMC_IRQHandler                   /* FSMC                         */                   
  .word     SDIO_IRQHandler                   /* SDIO                         */                   
  .word     TIM5_IRQHandler                   /* TIM5                         */                   
  .word     SPI3_IRQHandler                   /* SPI3                         */                   
  .word     UART4_IRQHandler                  /* UART4                        */                   
  .word     UART5_IRQHandler                  /* UART5                        */                   
  .word     TIM6_DAC_IRQHandler               /* TIM6 and DAC1&2 underrun errors */                   
  .word     TIM7_IRQHandler                   /* TIM7                         */
  .word     DMA2_Stream0_IRQHandler           /* DMA2 Stream 0                */                   
  .word     DMA2_Stream1_IRQHandler           /* DMA2 Stream 1                */                   
  .word     DMA2_Stream2_IRQHandler           /* DMA2 Stream 2                */                   
  .word     DMA2_Stream3_IRQHandler           /* DMA2 Stream 3                */                   
  .word     DMA2_Stream4_IRQHandler           /* DMA2 Stream 4                */                   
  .word     ETH_IRQHandler                    /* Ethernet                     */                   
  .word     ETH_WKUP_IRQHandler               /* Ethernet Wakeup through EXTI line */                     
  .word     CAN2_TX_IRQHandler                /* CAN2 TX                      */                          
  .word     CAN2_RX0_IRQHandler               /* CAN2 RX0                     */                          
  .word     CAN2_RX1_IRQHandler               /* CAN2 RX1                     */                          
  .word     CAN2_SCE_IRQHandler               /* CAN2 SCE                     */                          
  .word     OTG_FS_IRQHandler                 /* USB OTG FS                   */                   
  .word     DMA2_Stream5_IRQHandler           /* DMA2 Stream 5                */                   
  .word     DMA2_Stream6_IRQHandler           /* DMA2 Stream 6                */                   
  .word     DMA2_Stream7_IRQHandler           /* DMA2 Stream 7                */                   
  .word     USART6_IRQHandler                 /* USART6                       */                    
  .word     I2C3_EV_IRQHandler                /* I2C3 event                   */                          
  .word     I2C3_ER_IRQHandler                /* I2C3 error                   */                          
  .word     OTG_HS_EP1_OUT_IRQHandler         /* USB OTG HS End Point 1 Out   */                   
  .word     OTG_HS_EP1_IN_IRQHandler          /* USB OTG HS End Point 1 In    */                   
  .word     OTG_HS_WKUP_IRQHandler            /* USB OTG HS Wakeup through EXTI */                         
  .word     OTG_HS_IRQHandler                 /* USB OTG HS                   */                   
  .word     DCMI_IRQHandler                   /* DCMI                         */                   
  .word     CRYP_IRQHandler                   /* CRYP crypto                  */                   
  .word     HASH_RNG_IRQHandler               /* Hash and Rng                 */
  .word     F2DUMMY                           /* Era FPU no F4                */                         

Acima  temos o trecho que implementa a estrutura da tabela de vetores dos microcontroladores Cortex, reparem que essa possui as primeiras 16 entradas separadas das demais. São essas as entradas das exceções donde tem-se a quarta entrada, o vetor que nos interessa, o de hardfault. Sabendo agora onde ele fica, vamos implementar uma função que consiga extrair o último stack frame gerado antes do firmware entrar nesse ponto, vejam como está:

/**
 * @brief  This is the code that gets called when the processor receives an 
 *         unexpected interrupt.  This simply enters an infinite loop, preserving
 *         the system state for examination by a debugger.
 * @param  None     
 * @retval None       
*/
    .section  .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
  b  Infinite_Loop
  .size  Default_Handler, .-Default_Handler

Como o foco aqui é simplificar, iremos implementar nosso código de debug dentro do próprio Default_Handler, vejam como fica:

/**
 * @brief  This is the code that gets called when the processor receives an 
 *         unexpected interrupt.  This simply enters an infinite loop, preserving
 *         the system state for examination by a debugger.
 * @param  None     
 * @retval None       
*/
    .section  .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:

  cpsid  				;shutdown interrupts
  movs  r0, 0x02		;
  msr   control, r0 	;enters in privilegied mode
  isb  					;
  dsb					;flushes pipeline, ensring the next instruction
  						;will be executed in thread mode
  						;
  mrs   r0, psp			;uses msp for bare-metal systems
  ldmia	r0!,{r1 - r7}	;pops the last stack frame on our
  						;register workspace.
  bpkt  #0				;halts the execution (proceed the analysis)
  						;
  b 	.				;traps the code here

  .size  Default_Handler, .-Default_Handler

Eu sei, eu sei, Assembly, apesar do susto inicial veja que o código de startup é geralmente implementado em baixo nível tornando muito mais fácil embarcar código extra nesse módulo do que criar um arquivo separado e usar Assembly inline. Veja também que tal solução pode ser implementada em C com o auxílio das funções implementadas na CMSIS. Sobre o nosso módulo de rastreio de “crash”, iniciamos, ao entrar no vetor de exceção, o desligamento de toda e qualquer interrupção futura com o cpsid e inibimos qualquer tipo de preempção (até mesmo de interrupções de altíssima prioridade). Em seguida garantimos que o processador fique no chamado thread mode, nesse modo o acesso a qualquer registrador e memória é permitido, para isso modificamos o registrador especial control. Com o uso das instruções isb e dsb fazemos o que se chama de esvaziamento do pipeline do microcontrolador, isso é necessário pois garantimos que a próxima instrução a ser executada seja em thread mode, evitando por exemplo que o acesso ao ponteiro de pilha principal MSP, seja inibido pelo hardware, e falando em pilha, esse é o passo seguinte. Com o uso da instrução de carga multipla, acessamos o stack frame composto de 7 registradores e os colocamos na área de trabalho. Agora eis que o leitor me pergunta: Por que na área de trabalho e não em uma estrutura com acesso pelo watch de variáveis? Simples, o acesso aos registradores do microcontrolador é o mínimo invasivo possível, não existe nenhum pós processamento que poderia mascarar os dados lidos, além disso, no work podemos acessar de todo tipo de ferramenta de debug, de um completíssimo ARM DS Studio a um terminal conectado via OpenOCD. Ao final a instrução bkpt é chamada fazendo com que o programa pare sem qualquer necessidade de ação do desenvolvedor, ou seja, toda a ação é transparente e, se uma falha for gerada, o programa automaticamente tomará todas as providências necessárias para análise e através do evento de parada irá notificar o usuário.

Tenho os registradores, e agora?

Uma vez de posse do stack frame, precisamos saber o que fazer, é nesse ponto que deixo a cargo de cada desenvolvedor tomar as suas providências de análise, porém alguns procedimentos que podem ajudar e muito:

  • Sempre cheque o contador de programa (PC), ele incialmente contém o endereço do exato local onde a exceção foi chamada;
  • Se no endereço apontado pelo PC você encontrar words de 0x00000000 ou 0xFFFFFFFF, desconfie, esses são tratados por undefined instruction pelo ARM;
  • Seguindo o raciocínio do tópico anterior, caso essa suspeita se confirme, cheque também o conteúdo do link register LR, e subtraia 1 do valor. O endereço resultante é o ponto de chamada de uma subrotina que contém entre suas instruções a que gerou a falha;
  • Um hardfault muito comum é o associado ao modo de operação do microcontrolador, assim no lugar do LR extraído da pilha, checar o LR que já está no work é muito útil, pois algo que pode ter acontecido é o microcontrolador Cortex, que só executa instruções thumb, ter feito algum acesso em modo ARM. Assim, cheque o modo de operação através desses códigos disponibilizados no guia de usuário da ARM.

Um exemplo prático, a movimentação de memória defeituosa

Vamos a um exemplo bem comum de hardfault, overflow de memória, considere o seguinte trecho de código:

#define SIMPLE_ARRAY_SIZE 512


int simpleArray[SIMPLE_ARRAY_SIZE];
int complexArray[SIMPLE_ARRAY_SIZE];


memcpy(&simpleArray, &complexArray, (SIMPLE_ARRAY_SIZE + 1));

Se executarmos esse código será quase certeza (salvo alguma providência tomada na linkagem) que veremos o código travado lá naquele breakpoint. Vamos seguir os passos básicos, vamos checar o PC, é provável que encontremos algo como: 0x2AC00000, 0x0, ou 0xFFFFFFFF, o que não diz muita coisa. Então vamos ao LR. Se pegarmos seu valor, subtrairmos 1, e jogar no disassembly, estaremos provavelmente no ponto onde o memcpy() foi chamado. Ainda com o disassembly aberto, poderemos observar as instruções até chegar em algo parecido com isso:

ldr r0, [r1], #4
str r0, [r2], #4

Um processo de load e store, e aqui entra a personalidade de quem esta com a tarefa de busca do bug. Alguns logo de cara ja irão desconfiar do overflow, esse que vos escrever, vai olhar provavelmente no conteúdo dos registradores r0~r12 onde aparecerá alguma operação suspeita, porém relacionada ao ciclo de load e store, concluindo que está havendo estouro de um ou dois dos vetores. Mas vejam que o bug propriamente vem desse ponto, mas a causa é outra, levando-nos a procurar na chamada do memcpy() por erros nos argumentos, corrigindo assim o erro.

Conclusão

Debug é uma tarefa que pode consumir minutos ou dias, porém um fator bem determinante do tempo gasto na procura por um problema de firmware está na forma que o desenvolvedor aproveita as ferramentas que seu ambiente de desenvolvimento oferece, seja linha de comando ou uma IDE completa. Assim, o uso de um código de trace no vetor de hardfault se torna útil pois atende aos dois extremos do desenvolvedor, reduzindo o tempo de busca de problemas de firmware fornecendo no mínimo uma rota incial de onde buscar a causa de falhas.

Referências

Manual de usuário dos processadores ARM Cortex

YIU, Joseph – The Definitive Guide for ARM Cortex M3 and M4 Processors

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
3 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Adan Kvitschal
Adan Kvitschal
12/02/2021 13:24

Parabéns pelo conteúdo, material de qualidade em português. Trabalho nessa área na moduhub, startup que fundei e passou em alguns edtais, estamos entrando mais fundo na área de serviços pra terceiros. Vou continuar monitorando teu trabalho.

Ronaldo Lins
Ronaldo Lins
07/12/2015 23:14

Muito bom o material. HardFault é um terror, porém pode ser muito útil quando acontece no momento do desenvolvimento…

Simao Berkof
Simao Berkof
07/12/2015 16:38

Parabéns pelo material, o artigo é muito bom.

Home » Software » Firmware » Meu firmware gerou Hardfault! E agora?

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: