Olá Habr!
Em relação ao
afrouxamento do regime outro dia, a indignação nos comentários de um post vizinho de que os artigos sobre microcontroladores estão todos piscando pelo LED e também a morte prematura do meu blog padrão, que tenho preguiça de restaurar, transferirei aqui material útil sobre uma lamentável pouca luz um truque de imprensa no trabalho com núcleos Cortex-M - verificação de endereços aleatórios quanto à validade.
Um dos recursos úteis e, ao mesmo tempo, por algum motivo, não descritos em nenhum lugar dos microcontroladores Cortex-M (todos), é a capacidade de verificar a correção do endereço na memória. Com ele, você pode determinar o tamanho do flash, RAM e EEPROM, determinar a presença em um processador específico de periféricos e registradores específicos, reprimir processos caídos, mantendo a integridade geral do sistema operacional, etc.
No modo normal, quando você chega a um endereço inexistente no Cortex-M3 / M4 / M7, uma exceção do BusFault é lançada e, na ausência de seu manipulador, ela é encaminhada para o HardFault. No Cortex-M0, não há exceções "detalhadas" (MemFault, BusFault, UsageFault), e quaisquer falhas passam imediatamente para o HardFault.
Em geral, você não pode ignorar o HardFault - pode ser devido a uma falha de hardware, por exemplo, e o comportamento adicional do dispositivo se tornará imprevisível. Mas, no caso específico, isso pode e deve ser feito.
Cortex-M3 e Cortex-M4: o BusFault não realizado
No Cortex-M3 e superior, a verificação da validade do endereço é bastante simples - você deve proibir todas as exceções (exceto, obviamente, não mascaráveis) através do registro FAULTMASK, desativar o processamento do BusFault especificamente e, em seguida, cutucar o endereço que está sendo verificado e ver se o sinalizador BFARVALID no registro BFAR , ou seja, registro de endereço de falha de barramento. Se você o tirou, acabou de ter um BusFault, ou seja, O endereço está incorreto.
O código se parece com isso, todas as definições e funções do CMSIS padrão (não fornecedor), portanto, ele deve funcionar em qualquer M3, M4 ou M7:
bool cpu_check_address(volatile const char *address) { static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; SCB->CFSR |= BFARVALID_MASK; uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; *address; if ((SCB->CFSR & BFARVALID_MASK) != 0) { is_valid = false; } SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; }
Cortex-M0 e Cortex-M0 +
Com o Cortex-M0 e o Cortex-M0 +, tudo fica mais complicado, como eu disse acima, eles não têm o BusFault e todos os registros correspondentes, e as exceções são imediatamente encaminhadas para o HardFault. Portanto, existe apenas uma saída: tornar o manipulador do HardFault capaz de entender que a exceção foi deliberadamente causada e retornar à função que a chamou, passando lá um sinalizador indicando que o HardFault estava lá.
Isso é feito exclusivamente em assembler. No exemplo abaixo, o registro R5 é definido como 1 e dois "números mágicos" são gravados nos registros R1 e R2. Se o HardFault ocorrer após tentar carregar o valor no endereço que está sendo verificado, ele deverá verificar os valores de R1 e R2 e, se encontrarem os números necessários, defina R5 como zero. O valor de R5 é transferido para o código syshech por meio de uma variável especial rigidamente vinculada a esse registro, o endereço a ser montado no assembler está implícito, apenas sabemos que em arm-none-eabi o primeiro parâmetro da função é colocado em R0.
bool cpu_check_address(volatile const char *address) { (void)address; register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" "ldr r1, =0xDEADF00D \n" "ldr r2, =0xCAFEBABE \n" "ldrb r3, [r0] \n" ); return result; }
O código do manipulador do HardFault em sua forma mais simples se parece com o seguinte:
__attribute__((naked)) void hard_fault_default(void) { __asm__ volatile ( "movs r0, #4 \n" "mov r2, lr \n" "tst r2, r0 \n" "bne use_psp \n" "mrs r0, msp \n" "b out \n" " use_psp: \n" "mrs r0, psp \n" " out: \n" "ldr r1, [r0, #0x04] \n" "ldr r2, =0xDEADF00D \n" "cmp r1, r2 \n" "bne regular_handler \n" "ldr r1, [r0, #0x08] \n" "ldr r2, =0xCAFEBABE \n" "cmp r1, r2 \n" "bne regular_handler \n" "ldr r1, [r0, #0x18] \n" "add r1, r1, #2 \n" "str r1, [r0, #0x18] \n" "ldr r5, =0 \n" "bx lr \n" " regular_handler: \n" )
No momento em que sai do manipulador de exceções, o Cortex lança os registradores, que são garantidamente corrompidos pelo manipulador (R0-R3, R12, LR, PC ...) na pilha. O primeiro fragmento - ele já está na maioria dos manipuladores HardFault prontos, exceto os gravados em puro metal puro - determina qual pilha: ao trabalhar no sistema operacional, ele pode ser MSP ou PSP, e eles têm endereços diferentes. Em projetos bare metal, a pilha MSP (Main Stack Pointer) geralmente é instalada a priori, sem verificação - porque o PSP (Process Stack Pointer) não pode estar lá devido à falta de processos.
Depois de determinar a pilha desejada e colocar seu endereço em R0, lemos os valores R1 (deslocamento 0x04) e R2 (deslocamento 0x08), comparamos com palavras mágicas, se ambos coincidem - lemos o valor do PC (deslocamento 0x18) da pilha, adicione 2 (2 bytes - o tamanho da instrução no Cortex-M *) e salve-o novamente na pilha. Se isso não for feito, quando retornarmos do manipulador, encontraremos a mesma instrução que realmente causou a exceção e sempre rodaremos em círculos. O Apêndice 2 nos move para a próxima instrução no momento do retorno.
* Upd. Nos comentários, surgiu a pergunta sobre o tamanho das instruções no Cortex-M, e darei a resposta correta aqui: neste caso, a falha causa a instrução LDRB, disponível na arquitetura ARMv7-M em duas versões - 16 bits e 32 bits. A segunda opção será selecionada se pelo menos uma das condições for atendida:
- o autor indicou explicitamente a instrução LDRB.W em vez de LDRB (não o fazemos)
- registros acima de R7 são usados (para nós - R0 e R3)
- Um deslocamento maior que 31 bytes é especificado (não temos um deslocamento)
Em todos os outros casos (ou seja, quando os operandos se ajustam ao formato da versão de 16 bits da instrução), o montador deve selecionar a versão de 16 bits.
Portanto, no nosso caso, sempre haverá uma instrução de 2 bytes que precisa ser revisada, mas se você editar o código fortemente, as opções serão possíveis.Em seguida, escreva 0 no R5, que serve como um indicador de entrada no HardFault. Registros após R3 não são salvos na pilha antes de registros especiais e, ao sair do manipulador, eles não são restaurados de forma alguma; portanto, é nossa consciência estragá-los ou não estragá-los. Nesse caso, alteramos R5 de 1 para 0 intencionalmente.
O retorno do manipulador de interrupção é feito exatamente de uma maneira. Ao inserir o manipulador, um valor especial é gravado no registro LR chamado EXC_RETURN, que é necessário gravar no PC para sair do manipulador - e não apenas gravá-lo, mas fazê-lo com um comando POP ou BX (ou seja, “mov pc, lr, por exemplo, não funciona , embora a primeira vez que lhe pareça funcionar). O BX LR parece uma tentativa de ir para um endereço sem sentido (no LR haverá algo como 0xFFFFFFF1 que não tem nada a ver com o endereço real do procedimento para o qual precisamos retornar), mas, na realidade, o processador vê esse valor no PC (para onde irá automaticamente), ele restaurará os registros da pilha e continuará executando nosso procedimento - com o próximo procedimento após chamar o HardFault devido ao fato de termos aumentado manualmente o PC nessa pilha em 2.
Você pode ler sobre todos os desvios e comandos
claramente onde , é claro.
Bem, ou se os números mágicos não estiverem visíveis, tudo será direcionado para o regular_handler, após o qual o procedimento usual de processamento de HardFault segue - como regra, esta é uma função que imprime valores de registro no console, decide o que fazer em seguida com o processador, etc.
Determinação do tamanho da RAM
Usar tudo isso é simples e direto. Queremos escrever um firmware que funcione em vários microcontroladores com diferentes quantidades de RAM, enquanto usa RAM em um programa completo?
Sim fácil:
static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) { char *address = base; do { address += block; if (!cpu_check_address(address)) { break; } } while ((uint32_t)(address - base) < maxsize); return (uint32_t)(address - base); } uint32_t get_cpu_ram_size(void) { return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024); }
É necessário maxsize aqui, para que, na quantidade máxima possível de RAM entre ela e o próximo bloco de endereços, não exista nenhuma lacuna na qual o cpu_check_address será quebrado. Neste exemplo, é 80 KB. Também não faz sentido investigar todos os endereços - basta olhar para a folha de dados para ver qual é o passo mínimo possível entre os dois modelos de controlador e configurá-lo como bloco.
Transição programática para o gerenciador de inicialização localizado no meio do nada
Às vezes, você pode fazer truques ainda mais complicados - por exemplo, imagine que você queira pular programaticamente para o carregador de inicialização de fábrica da fábrica STM32 para alternar para o modo de atualização de firmware via UART ou USB, sem se preocupar em escrever seu carregador de inicialização.
O carregador de inicialização STM32 fica na área chamada Memória do sistema, para a qual você precisa ir, mas há um problema - essa área tem endereços diferentes não apenas em diferentes séries de processadores, mas em diferentes modelos da mesma série (uma placa épica pode ser encontrada na
AN2606 em 22 a 26). Ao adicionar a funcionalidade correspondente à plataforma em geral, e não apenas a um produto específico, você deseja versatilidade.
Nos arquivos CMSIS, o endereço inicial da memória do sistema também está ausente. Não é possível determiná-lo pelo ID do carregador de inicialização, porque esse é um problema de galinha e ovo - o ID do carregador de inicialização está no último byte da memória do sistema, o que nos leva de volta à questão do endereço.
No entanto, se olharmos para o cartão de memória STM32, veremos algo como isto:

Nesse caso, estamos interessados no ambiente de memória do sistema - por exemplo, na parte superior há uma área programável uma vez (não no STM32) e bytes de opção (no total). Essa estrutura é observada não apenas em modelos diferentes, mas em diferentes linhas STM32, com a única diferença na presença de OTP e a presença de uma lacuna nos endereços entre a memória do sistema e as opções.
Mas para nós, neste caso, o mais importante é que o endereço inicial dos Option Bytes esteja nos cabeçalhos regulares do CMSIS - ele é chamado OB_BASE.
Mais simples. Escrevemos a função para procurar o primeiro endereço válido ou inválido, abaixo ou acima do endereço especificado:
char *cpu_find_next_valid_address(char *start, char *stop, bool valid) { char *address = start; while (true) { if (address == stop) { return NULL; } if (cpu_check_address(address) == valid) { return address; } if (stop > start) { address++; } else { address--; } }; return NULL; }
E observe os bytes de opção, primeiro o final da memória do sistema ou o OTP adjacente a ela e, em seguida, o início da memória do sistema em duas passagens:
char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; c = cpu_find_next_valid_address(a, b, true); c = cpu_find_next_valid_address(c, b, false) + 1;
E sem muita dificuldade, organizamos isso em uma função que encontra o início da memória do sistema e salta para ela, ou seja, lança o gerenciador de inicialização:
static void jump_to_bootloader(void) __attribute__ ((noreturn)); static void jump_to_bootloader(void) { char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; c = cpu_find_next_valid_address(a, b, true); c = cpu_find_next_valid_address(c, b, false) + 1; if (!c) { NVIC_SystemReset(); } uint32_t boot_addr = (uint32_t)c; uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr); uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4); void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr); __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); }
Depende do modelo específico do processador ... nada depende. A lógica não funcionará em modelos que tenham um furo entre o OTP e a memória do sistema - mas eu não verifiquei se há algum. Trabalhará ativamente com o OTP - cheque.
Outros truques aplicam-se apenas ao procedimento usual para chamar o gerenciador de inicialização a partir do seu código - não se esqueça de redefinir o ponteiro da pilha e chame o procedimento para deixar o gerenciador de inicialização antes de inicializar periféricos, velocidades de clock etc.: devido ao seu minimalismo, o gerenciador de inicialização pode entupir inicialize a periferia e espere que ela esteja no estado padrão. Uma boa opção para chamar o gerenciador de inicialização de um local arbitrário no seu programa é gravar no Registro de Backup do RTC ou simplesmente em um endereço conhecido na memória de um número mágico, reiniciar o programa e verificar os primeiros estágios de inicialização desse número.
PS Como todos os endereços no cartão de memória do processador estão alinhados, na pior das hipóteses, por 4, o procedimento acima será bastante acelerado pela idéia de percorrê-los em incrementos de 4 bytes em vez de um.
Aviso importante
Nota: observe que, em um controlador específico, a validade de um endereço específico não indica necessariamente a presença real de funcionalidade que pode estar localizada nesse endereço. Por exemplo, o endereço do registro que controla algum bloco periférico opcional pode ser válido, embora o próprio bloco esteja ausente neste modelo. Do lado do fabricante, os truques mais interessantes são possíveis, geralmente enraizados no uso dos mesmos cristais para diferentes modelos de processadores. No entanto, na maioria dos casos, esses procedimentos funcionam e provam ser muito úteis.