Cómo implementar secciones críticas en ARM Cortex A9

Estoy transfiriendo un código heredado de un núcleo ARM926 a CortexA9. Este código es básico y no incluye un sistema operativo ni bibliotecas estándar, todas personalizadas. Tengo una falla que parece estar relacionada con una condición de carrera que debe evitarse mediante una sección crítica del código.

Quiero algunos comentarios sobre mi enfoque para ver si mis secciones críticas pueden no implementarse correctamente para esta CPU. Estoy usando GCC. Sospecho que hay algún error sutil.

Además, ¿hay una biblioteca de código abierto que tenga este tipo de primitivos para ARM (o incluso una buena biblioteca ligera de spinlock/seméforo)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

El código se utiliza de la siguiente manera:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

La idea de la "clave" es permitir secciones críticas anidadas, y estas se utilizan al principio y al final de las funciones para crear funciones reentrantes.

¡Gracias!

consulte infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… no lo haga en asm incrustado por cierto. que sea una función como lo hace el artículo.
No sé nada sobre ARM, pero esperaría que para mutex (o cualquier función de sincronización de subprocesos o procesos cruzados), debería usar el clobber de "memoria" para asegurarse de que a) todos los valores de memoria actualmente almacenados en caché en los registros se vacíen volver a la memoria antes de ejecutar el asm y b) cualquier valor en la memoria al que se acceda después de que el asm se vuelva a cargar. Tenga en cuenta que realizar una llamada (como recomienda HuStmpHrrr) debería realizar implícitamente este golpe por usted.
Además, aunque todavía no hablo ARM, sus restricciones para 'key_' no parecen correctas. Dado que dice que esto está destinado a ser utilizado para el reingreso, declararlo como "=r" en la cerradura parece sospechoso. '=' significa que tiene la intención de sobrescribirlo, y el valor existente no es importante. Parece más probable que tuviera la intención de usar '+' para indicar su intención de actualizar el valor existente. Y nuevamente para desbloquear, enumerarlo como una entrada le dice a gcc que no tiene la intención de cambiarlo, pero si no me equivoco, lo hace (cambiarlo). Supongo que esto también debería aparecer como una salida '+'.
+1 por codificar en ensamblaje para un núcleo de tan alta especificación. De todos modos, ¿podría esto estar relacionado con los modos de privilegio?
Estoy bastante seguro de que necesitará usar ldrexy strexhacerlo correctamente. Aquí hay una página web que le muestra cómo usar ldrexe streximplementar un spinlock.
¿Está en un solo núcleo y solo quiere protección para no adelantarse a sí mismo? De lo contrario, si está tratando de sincronizar entre varios núcleos o periféricos DMA, las interrupciones no funcionarán en absoluto: necesitará las exclusivas antes mencionadas con las barreras adecuadas y una reflexión cuidadosa sobre la coherencia de la memoria caché.
El código se ve bien. ¿Qué te hace pensar que, de los millones de código que has portado, esas 4 líneas tienen la culpa?
Parece que la pregunta es más adecuada para SO que para EE
¿Puede escribir casos de prueba, por ejemplo, ejecutar una tarea de baja prioridad que escribe en un búfer y luego iniciar una de mayor prioridad que la interrumpe? ¿Tirar un alfiler en algún lugar cuando se lleva a cabo la contienda? ¿Eliminar su sección crítica y ver cómo las cosas van mal, luego volver a colocarla para solucionar el problema?

Respuestas (5)

La parte más difícil de manejar una sección crítica sin un sistema operativo no es realmente crear el mutex, sino averiguar qué debería suceder si el código quiere usar un recurso que no está disponible actualmente. Las instrucciones exclusivas de carga y exclusivas de almacenamiento condicional facilitan la creación de una función de "intercambio" que, dado un puntero a un entero, almacenará atómicamente un nuevo valor pero devolverá lo que contenía el entero apuntado:

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

Dada una función como la anterior, uno puede ingresar fácilmente un mutex a través de algo como

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

En ausencia de un sistema operativo, la principal dificultad a menudo radica en el código "no se pudo obtener mutex". Si ocurre una interrupción cuando un recurso protegido por mutex está ocupado, puede ser necesario que el código de manejo de interrupciones establezca una bandera y guarde alguna información para indicar lo que quería hacer, y luego tener un código similar al principal que adquiera el mutex verifique siempre que vaya a liberar el mutex para ver si una interrupción quería hacer algo mientras se retuvo el mutex y, de ser así, realice la acción en nombre de la interrupción.

Aunque es posible evitar problemas con las interrupciones que desean usar recursos protegidos por exclusión mutua simplemente deshabilitando las interrupciones (y, de hecho, deshabilitar las interrupciones puede eliminar la necesidad de cualquier otro tipo de exclusión mutua), en general, es deseable evitar deshabilitar las interrupciones por más tiempo del necesario.

Un compromiso útil puede ser usar un indicador como se describe anteriormente, pero tener el código de línea principal que liberará las interrupciones de desactivación de mutex y verificar el indicador antes mencionado justo antes de hacerlo (volver a habilitar las interrupciones después de liberar el mutex). Tal enfoque no requiere dejar las interrupciones deshabilitadas por mucho tiempo, pero evitará la posibilidad de que si el código de la línea principal prueba el indicador de interrupción después de liberar el mutex, existe el peligro de que entre el momento en que ve el indicador y el momento en que actúa sobre él, podría ser reemplazado por otro código que adquiera y libere el mutex y actúe sobre el indicador de interrupción; si el código de la línea principal no prueba el indicador de interrupción después de liberar el mutex,

En cualquier caso, lo más importante será tener un medio por el cual el código que intente usar un recurso protegido por mutex cuando no esté disponible tendrá un medio para repetir su intento una vez que se libere el recurso.

Esta es una forma de mano dura para hacer secciones críticas; deshabilitar las interrupciones. Es posible que no funcione si su sistema tiene/maneja fallas de datos. También aumentará la latencia de interrupción. El irqflags.h de Linux tiene algunas macros que manejan esto. Las instrucciones cpsiey pueden ser útiles; cpsidSin embargo, no guardan el estado y no permitirán el anidamiento. cpsno utiliza un registro.

Para la serie Cortex-A , ldrex/strexson más eficientes y pueden funcionar para formar un mutex para la sección crítica o pueden usarse con algoritmos sin bloqueo para deshacerse de la sección crítica.

En cierto sentido, ldrex/strexparece un ARMv5 swp. Sin embargo, son mucho más complejos de implementar en la práctica. Necesita un caché de trabajo y la memoria de destino de las ldrex/strexnecesidades de estar en el caché. La documentación de ARM en el ldrex/strexes bastante nebulosa, ya que quieren que los mecanismos funcionen en CPU que no sean Cortex-A. Sin embargo, para Cortex-A, el mecanismo para mantener la caché de la CPU local sincronizada con otras CPU es el mismo que se usa para implementar las ldrex/strexinstrucciones. Para la serie Cortex-A, la reserva granual (tamaño de ldrex/strexla memoria reservada) es la misma que una línea de caché; también necesita alinear la memoria con la línea de caché si tiene la intención de modificar varios valores, como con una lista doblemente vinculada.

Sospecho que hay algún error sutil.

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

Debe asegurarse de que la secuencia nunca se pueda adelantar . De lo contrario, puede obtener dos variables clave con interrupciones habilitadas y la liberación de bloqueo será incorrecta. Puede usar la swpinstrucción con la memoria clave para garantizar la coherencia en ARMv5, pero esta instrucción está en desuso en Cortex-A, ldrex/strexya que funciona mejor para sistemas de varias CPU.

Todo esto depende del tipo de programación que tenga su sistema. Parece que solo tienes líneas principales e interrupciones. A menudo necesita las primitivas de la sección crítica para tener algunos enlaces con el planificador dependiendo de los niveles (sistema/espacio de usuario/etc) con los que desea que trabaje la sección crítica.

Además, ¿hay una biblioteca de código abierto que tenga este tipo de primitivos para ARM (o incluso una buena biblioteca ligera de spinlock/seméforo)?

Esto es difícil de escribir de una manera portátil. Es decir, dichas bibliotecas pueden existir para ciertas versiones de CPU ARM y para sistemas operativos específicos.

Veo varios problemas potenciales con esas secciones críticas. Hay advertencias y soluciones para todos estos, pero como resumen:

  • No hay nada que impida que el compilador mueva el código a través de estas macros, por optimización u otras razones aleatorias.
  • Guardan y restauran algunas partes del estado del procesador que el compilador espera que el ensamblaje en línea deje en paz (a menos que se indique lo contrario).
  • No hay nada que impida que ocurra una interrupción en medio de la secuencia y que cambie el estado entre el momento en que se lee y el momento en que se escribe.

En primer lugar, definitivamente necesita algunas barreras de memoria del compilador . GCC los implementa como clobbers . Básicamente, esta es una forma de decirle al compilador "No, no puede mover los accesos a la memoria a través de esta pieza de ensamblaje en línea porque podría afectar el resultado de los accesos a la memoria". Específicamente, necesita ambos "memory"y "cc"clobbers, tanto en la macro inicial como en la final. Esto evitará que otras cosas (como las llamadas a funciones) se reordenen también en relación con el ensamblado en línea, porque el compilador sabe que es posible que tengan acceso a la memoria. He visto GCC para registros de código de estado en condición de retención de ARM en ensamblaje en línea con "memory"clobbers, por lo que definitivamente necesita el "cc"clobber.

En segundo lugar, estas secciones críticas guardan y restauran mucho más que si las interrupciones están habilitadas. Específicamente, están guardando y restaurando la mayor parte del CPSR (Registro de estado actual del programa) (el enlace es para Cortex-R4 porque no pude encontrar un buen diagrama para un A9, pero debería ser idéntico). Hay restricciones sutiles sobre qué partes del estado pueden modificarse, pero aquí es más que necesario.

Entre otras cosas, esto incluye los códigos de condición (donde cmpse almacenan los resultados de instrucciones similares para que las instrucciones condicionales subsiguientes puedan actuar sobre el resultado). El compilador definitivamente se confundirá con esto. Esto se soluciona fácilmente usando el "cc"clobber como se mencionó anteriormente. Sin embargo, esto hará que el código falle cada vez, por lo que no parece que tenga problemas. Sin embargo, es algo así como una bomba de relojería, ya que la modificación aleatoria de otro código puede hacer que el compilador haga algo un poco diferente que se romperá con esto.

Esto también intentará guardar/restaurar los bits de TI, que se utilizan para implementar la ejecución condicional de Thumb . Tenga en cuenta que si nunca ejecuta el código Thumb, esto no importa. Nunca descubrí cómo el ensamblaje en línea de GCC trata los bits de TI, aparte de concluir que no lo hace, lo que significa que el compilador nunca debe colocar el ensamblaje en línea en un bloque de TI y siempre espera que el ensamblaje termine fuera de un bloque de TI. Nunca he visto a GCC generar código que viole estas suposiciones, y he realizado un ensamblaje en línea bastante complejo con una gran optimización, por lo que estoy razonablemente seguro de que se mantienen. Esto significa que probablemente no intentará cambiar los bits de TI, en cuyo caso todo está bien. Intentar modificar estos bits se clasifica como "arquitectónicamente impredecible", por lo que podría hacer todo tipo de cosas malas, pero probablemente no haga nada en absoluto.

La última categoría de bits que se guardarán/restaurarán (además de los que realmente deshabilitan las interrupciones) son los bits de modo. Estos probablemente no cambiarán, por lo que probablemente no importará, pero si tiene algún código que cambie de modo deliberadamente, estas secciones de interrupción podrían causar problemas. Cambiar entre el modo privilegiado y el de usuario es el único caso de hacer esto que esperaría.

En tercer lugar, no hay nada que impida que una interrupción cambie otras partes de CPSR entre MRSy MSRen ARM_INT_LOCK. Cualquier cambio de este tipo podría sobrescribirse. En la mayoría de los sistemas razonables, las interrupciones asíncronas no cambian el estado del código que interrumpen (incluido CPSR). Si lo hacen, se vuelve muy difícil razonar sobre lo que hará el código. Sin embargo, es posible (me parece más probable cambiar el bit de desactivación de FIQ), por lo que debe considerar si su sistema hace esto.

Así es como los implementaría de una manera que aborde todos los problemas potenciales que señalé:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

Asegúrese de compilar -mcpu=cortex-a9porque al menos algunas versiones de GCC (como la mía) tienen por defecto una CPU ARM más antigua que no admite cpsiey cpsid.

Utilicé en andslugar de solo anden ARM_INT_LOCK, por lo que es una instrucción de 16 bits si se usa en el código Thumb. El "cc"clobber es necesario de todos modos, por lo que es estrictamente un beneficio de rendimiento/tamaño de código.

0y 1son etiquetas locales , como referencia.

Estos deberían poder usarse de la misma manera que sus versiones. Es ARM_INT_LOCKtan rápido/pequeño como el original. Desafortunadamente, no pude encontrar una manera de hacerlo de ARM_INT_UNLOCKmanera segura con tan pocas instrucciones.

Si su sistema tiene restricciones sobre cuándo se deshabilitan las IRQ y FIQ, esto podría simplificarse. Por ejemplo, si siempre están deshabilitados juntos, podría combinarlos en uno cbz+ cpsie ifcomo este:

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

Alternativamente, si no le importan los FIQ en absoluto, es similar a dejar de habilitarlos/deshabilitarlos por completo.

Si sabe que nada más cambia ninguno de los otros bits de estado en CPSR entre el bloqueo y el desbloqueo, también podría usar continuar con algo muy similar a su código original, excepto con ambos "memory"y "cc"clobbers en ambos ARM_INT_LOCKyARM_INT_UNLOCK

Usted dijo,

Estoy transfiriendo un código heredado de un núcleo ARM926 a CortexA9.

El SP7021 es una solución interesante que debería considerar si es posible, porque contiene 4 núcleos Arm7, más un núcleo ARM926 y un núcleo 8051 también. No tener que portar ese código sería una gran victoria, y hay una razón por la cual este chip se creó con 4 núcleos para alojar Linux, pero un núcleo completamente separado para el aspecto en tiempo real, si descarga el núcleo en tiempo real. con un sistema operativo, ¿cómo puede asegurarse de que su código en tiempo real no se vea comprometido? Esto es tan difícil de hacer bien que creo que la verdadera solución es encontrar formas de evitarlo, como usar chips separados. También dijiste,

Sospecho que hay algún error sutil.

Como sugiere Jack Ganssle en su libro, "El arte de diseñar sistemas integrados" , capítulo 3, "Deje de escribir programas grandes", sección "Partición con CPU", pág. 41,

"Por lo general, es más barato agregar más CPU simplemente por simplificar el software".

así que piense en dedicar un núcleo al aspecto en tiempo real, como el SP7021 está diseñado para hacer. (Divulgación completa: no tengo ninguna relación ni interés en el producto ni en la empresa).

Aquí hay un pensamiento: solo porque su empresa lo llama "Legacy", el solo uso de esa palabra automáticamente hace que parezca viejo y crujiente, y luego, por supuesto, debemos reemplazarlo absolutamente, ¡no necesariamente! El código depurado vale su peso en oro: eso es lo que costó desarrollarlo, mantenerlo y actualizarlo. Y si un nuevo microcontrolador tiene el mismo núcleo "clásico" incorporado "solo para tiempo real", ¿por qué no debería usarse?

Lo que sigue son mis opiniones sobre cómo me gustaría ver escrito este tipo de código, aunque soy un desarrollador sénior que realmente no ha hecho muchas cosas en tiempo real (todavía).

Me gusta la multitarea cooperativa porque es predecible (lo que significa que es posible depurar).

Me gusta el lenguaje de coordinación llamado Linda , en el que se publica el trabajo por realizar y los procesos de trabajo "revisan" un elemento de trabajo para trabajar en él y luego publican la respuesta.

Me gusta la forma en que lo hace Erlang (y me gusta su alta disponibilidad):

  • Todo es un proceso.
  • Los procesos están fuertemente aislados.
  • La creación y destrucción de procesos es una operación ligera.
  • El paso de mensajes es la única manera de que los procesos interactúen.
  • Los procesos tienen nombres únicos.
  • Si conoce el nombre de un proceso, puede enviarle un mensaje.
  • Los procesos no comparten recursos.
  • El manejo de errores no es local.
  • Los procesos hacen lo que se supone que deben hacer o fallan.

En lugar de tratar de manejar bloqueos y semáforos y condiciones de carrera y puntos muertos, intente adoptar una solución en la que realmente no necesite actuar como si realmente pudiera hacer estas cosas, porque nadie puede, en realidad no. ¡Hay una razón por la que Linda y Erlang lo hacen de esa manera!

Un 8051?? Debe colocar una advertencia de activación en una publicación que sugiera usar un 8051 en este día y edad. Caramba.
@ElliotAlderson: supongo que a continuación nos pedirá que eliminemos todo el código Cobol, poniendo de rodillas a todo el sistema bancario. ;-) Tienes razón, pero dijo que tiene un código heredado y que podría tener algún 8051 que también tiene que reescribir.