Hola Habr!
Con respecto al
aflojamiento del régimen el otro día, la indignación en los comentarios de una publicación vecina de que los artículos sobre microcontroladores están parpadeando por el LED, así como la muerte prematura de mi blog estándar, que soy demasiado vago para restaurar, transferiré aquí material útil sobre una pequeña y lamentable luz. Un truco de prensa para trabajar con núcleos Cortex-M: verificar la validez de las direcciones aleatorias.
Una de las funciones muy útiles y al mismo tiempo, por alguna razón, listas para usar que no se describen en ningún lugar en los microcontroladores Cortex-M (todo) es la capacidad de verificar la exactitud de la dirección en la memoria. Con él, puede determinar el tamaño de flash, RAM y EEPROM, determinar la presencia en un procesador particular de periféricos y registros específicos, vencer los procesos caídos mientras se mantiene la salud general del sistema operativo, etc.
En el modo normal, cuando llega a una dirección inexistente en Cortex-M3 / M4 / M7, se emite una excepción BusFault y, en ausencia de su controlador, se intensifica a HardFault. En Cortex-M0 no hay excepciones "detalladas" (MemFault, BusFault, UsageFault), y cualquier falla se escala inmediatamente a HardFault.
En general, no puede ignorar HardFault: puede ser el resultado de una falla de hardware, por ejemplo, y el comportamiento posterior del dispositivo se volverá impredecible. Pero en el caso particular, esto puede y debe hacerse.
Cortex-M3 y Cortex-M4: el BusFault no cumplido
En Cortex-M3 y superior, verificar la validez de la dirección es bastante simple: debe prohibir todas las excepciones (excepto, obviamente, no enmascarables) a través del registro FAULTMASK, deshabilitar el procesamiento de BusFault específicamente y luego meter la dirección que se está verificando y ver si la bandera BFARVALID en el registro BFAR , es decir, registro de dirección de falla del bus. Si lo ha tomado, acaba de tener un BusFault, es decir La dirección es incorrecta.
El código se ve así, todas las definiciones y funciones provienen del CMSIS estándar (no de proveedor), por lo que debería funcionar en cualquier M3, M4 o 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 y Cortex-M0 +
Con Cortex-M0 y Cortex-M0 + todo es más complicado, como dije anteriormente, no tienen BusFault y todos los registros correspondientes, y las excepciones se escalan de inmediato a HardFault. Por lo tanto, solo hay una salida: hacer que el controlador HardFault pueda comprender que la excepción fue causada deliberadamente y volver a la función que la llamó, pasando una bandera que indica que HardFault estaba allí.
Esto se hace puramente en ensamblador. En el siguiente ejemplo, el registro R5 se establece en 1 y se escriben dos "números mágicos" en los registros R1 y R2. Si HardFault ocurre después de intentar cargar el valor en la dirección que se está verificando, entonces debe verificar los valores de R1 y R2, y si encuentran los números necesarios, establezca R5 en cero. El valor de R5 se transfiere al código syshech a través de una variable especial que está rígidamente vinculada a este registro, la dirección que se ensamblará en el ensamblador está implícita, solo sabemos que en arm-none-eabi el primer parámetro de la función se coloca en 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; }
El código del controlador HardFault en su forma más simple se ve así:
__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" )
En el momento de abandonar el controlador de excepciones, Cortex arroja los registros, que se garantiza que están dañados por el controlador (R0-R3, R12, LR, PC ...), en la pila. El primer fragmento, que ya se encuentra en la mayoría de los controladores HardFault listos para usar, a excepción de aquellos escritos bajo metal puro, determina qué pila: cuando se trabaja en el sistema operativo, puede ser MSP o PSP, y tienen direcciones diferentes. En proyectos de metal desnudo, la pila MSP (puntero de pila principal) generalmente se instala a priori, sin verificar, porque el PSP (puntero de pila de proceso) no puede estar allí debido a la falta de procesos.
Después de determinar la pila deseada y poner su dirección en R0, leemos los valores R1 (desplazamiento 0x04) y R2 (desplazamiento 0x08), la comparamos con palabras mágicas, si ambas coinciden: leemos el valor de PC (desplazamiento 0x18) de la pila, sumamos 2 (2 bytes: el tamaño de la instrucción en Cortex-M *) y guárdelo de nuevo en la pila. Si esto no se hace, cuando regresemos del controlador, nos encontraremos con la misma instrucción que realmente causó la excepción, y siempre correremos en círculos. El apéndice 2 nos lleva a la siguiente instrucción en el momento del regreso.
* Upd. En los comentarios, surgió la pregunta sobre el tamaño de las instrucciones en el Cortex-M, haré la respuesta correcta aquí: en este caso, el bloqueo provoca la instrucción LDRB, que está disponible en la arquitectura ARMv7-M en dos versiones: 16 bits y 32 bits. La segunda opción se seleccionará si se cumple al menos una de las condiciones:
- el autor indicó explícitamente la instrucción LDRB.W en lugar de LDRB (nosotros no)
- se utilizan registros por encima de R7 (para nosotros - R0 y R3)
- Se especifica un desplazamiento mayor de 31 bytes (no tenemos un desplazamiento)
En todos los demás casos (es decir, cuando los operandos se ajustan al formato de la versión de 16 bits de la instrucción), el ensamblador debe seleccionar la versión de 16 bits.
Por lo tanto, en nuestro caso, siempre habrá una instrucción de 2 bytes que debe ser superada, pero si edita el código con fuerza, las opciones son posibles.Luego, escriba 0 en R5, que sirve como indicador de entrar en HardFault. Los registros después de R3 no se guardan en la pila antes de los registros especiales, y al salir del controlador, no se restauran de ninguna manera, por lo que es nuestra conciencia estropearlos o no estropearlos. En este caso, cambiamos R5 de 1 a 0 a propósito.
El regreso del manejador de interrupciones se realiza exactamente de una manera. Al ingresar al controlador, se escribe un valor especial en el registro LR llamado EXC_RETURN, que es necesario escribir en la PC para salir del controlador, y no solo escribirlo, sino hacerlo con un comando POP o BX (es decir, "mov pc, lr, por ejemplo, no funciona , aunque la primera vez te parezca que funciona). BX LR parece un intento de ir a una dirección sin sentido (en LR habrá algo como 0xFFFFFFF1 que no tiene nada que ver con la dirección real del procedimiento al que debemos regresar), pero en realidad el procesador ve este valor en la PC (donde irá automáticamente), restaurará los registros de la pila y continuará ejecutando nuestro procedimiento, con el siguiente procedimiento después de llamar a HardFault debido al hecho de que aumentamos manualmente la PC en esta pila en 2.
Puede leer sobre todas las compensaciones y comandos
claramente dónde , por supuesto.
Bueno, o si los números mágicos no son visibles, entonces todo irá a regular_handler, después de lo cual sigue el procedimiento habitual de procesamiento de HardFault: como regla, esta es una función que imprime valores de registro en la consola, decide qué hacer a continuación con el procesador, etc.
Determinación del tamaño de RAM
Usar todo esto es simple y directo. ¿Queremos escribir un firmware que funcione en varios microcontroladores con diferentes cantidades de RAM, mientras que cada vez use RAM en un programa completo?
Sí 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); }
Entonces, se necesita maxsize aquí, que a la máxima cantidad posible de RAM entre él y el siguiente bloque de direcciones puede que no haya espacio en el que cpu_check_address se rompa. En este ejemplo, es 80 KB. Tampoco tiene sentido sondear todas las direcciones: solo mire la hoja de datos para ver cuál es el paso mínimo posible entre los dos modelos de controlador y configúrelo como bloque.
Transición programática al gestor de arranque ubicado en el medio de la nada
A veces puedes hacer trucos aún más complicados; por ejemplo, imagina que quieres saltar programáticamente al cargador de arranque de fábrica STM32 para cambiar al modo de actualización de firmware a través de UART o USB, sin molestarte en escribir tu cargador de arranque.
El gestor de arranque STM32 se encuentra en el área llamada Memoria del sistema, a la que debe ir, pero hay un problema: esta área tiene diferentes direcciones no solo en diferentes series de procesadores, sino en diferentes modelos de la misma serie (se puede encontrar una placa épica en
AN2606 en páginas 22 a 26). Cuando agrega la funcionalidad correspondiente a la plataforma en general, y no solo a un producto específico, desea versatilidad.
En los archivos CMSIS, también falta la dirección de inicio de la Memoria del sistema. No es posible determinarlo por ID de cargador de arranque, porque Este es un problema de huevo y gallina: la identificación del cargador de arranque se encuentra en el último byte de la memoria del sistema, lo que nos lleva de nuevo a la cuestión de la dirección.
Sin embargo, si miramos la tarjeta de memoria STM32, veremos algo como esto:

En este caso, estamos interesados en el entorno de Memoria del sistema; por ejemplo, en la parte superior hay un área que alguna vez fue programable (no en todos los STM32) y bytes de Opción (en total). Esta estructura se observa no solo en diferentes modelos, sino en diferentes líneas STM32, con la única diferencia en la presencia de OTP y la presencia de una brecha en las direcciones entre la memoria del sistema y las opciones.
Pero para nosotros en este caso, lo más importante es que la dirección de inicio de los Bytes de opción está en los encabezados regulares de CMSIS; allí se llama OB_BASE.
Más simple. Escribimos la función para buscar la primera dirección válida o no válida desde la especificada:
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; }
Y mire hacia abajo desde los bytes de Opción, primero el final de la memoria del sistema o la OTP adyacente, y luego el comienzo de la memoria del sistema en dos pasadas:
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;
Y sin mucha dificultad, organizamos esto en una función que encuentra el comienzo de la memoria del sistema y salta sobre ella, es decir, inicia el gestor de arranque:
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 del modelo de procesador específico ... nada depende. La lógica no funcionará en los modelos que tienen un agujero entre OTP y la memoria del sistema, pero no he verificado si hay alguno. Trabajará activamente con OTP - verificar.
Otros trucos se aplican solo al procedimiento habitual para llamar al gestor de arranque desde su código; no olvide restablecer el puntero de la pila y llame al procedimiento para abandonar el gestor de arranque antes de inicializar periféricos en el procesador, velocidades de reloj, etc .: debido a su minimalismo, el gestor de arranque puede obstruirse Inicialice la periferia y espere que esté en el estado predeterminado. Una buena opción para llamar al gestor de arranque desde un lugar arbitrario en su programa es escribir en el RTC Backup Register o simplemente en una dirección conocida en la memoria de un número mágico, reiniciar el programa y verificar en las primeras etapas de inicialización de este número.
PD Dado que todas las direcciones en la tarjeta de memoria del procesador están alineadas en el peor de los casos por 4, el procedimiento anterior se acelerará en gran medida por la idea de recorrerlas en incrementos de 4 bytes en lugar de uno.
Aviso importante
NB: tenga en cuenta que en un controlador específico la validez de una dirección específica no indica necesariamente la presencia real de la funcionalidad que se puede encontrar en esa dirección. Por ejemplo, la dirección del registro que controla algún bloque periférico opcional puede ser válida, aunque el bloque en sí está ausente en este modelo. Desde el punto de vista del fabricante, los trucos más interesantes son posibles, generalmente enraizados en el uso de los mismos cristales para diferentes modelos de procesadores. Sin embargo, en la mayoría de los casos, estos procedimientos funcionan y resultan muy útiles.