Implementación de I2C sin bloqueo en STM32

La mayoría de los artículos que puedo encontrar que son esencialmente "I2C para tontos en el microcontrolador XXX" sugieren bloquear la CPU mientras esperan los eventos de confirmación. Por ejemplo , (los comentarios y la descripción están en ruso, pero eso en realidad no importa). Aquí está el ejemplo de "bloqueo" del que estoy hablando:

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));

Estos whilebucles están ahí después de cada operación, esencialmente. Finalmente, mi pregunta es: ¿cómo implementar I2C sin "colgar" MCU con estos bucles while? En un nivel muy alto, entiendo que tengo que usar interrupciones en lugar de whiles, pero no puedo encontrar ningún ejemplo de código. ¿Puedes ayudarme con eso?

Tenga en cuenta que también puede usar DMA con I²C para obtener algún beneficio de rendimiento y reducir la cantidad de interrupciones a manejar.
@AlexeyMalev Aquí hay un ejemplo de una implementación I2C sin bloqueo para AVR (aunque se aplicarían principios similares para STM32): github.com/scttnlsn/avr-twi Esta implementación en particular usa interrupciones para administrar una máquina de estado y llamar a una función de devolución de llamada cuando la transmisión es finalizado.

Respuestas (5)

Seguro.

  1. Configure sus controladores en términos de un par superior e inferior , separados por un búfer compartido. La función inferior es un bit de código impulsado por interrupciones que responde a eventos de interrupción, se ocupa del trabajo inmediato necesario para servir al hardware y agrega datos al búfer compartido (si es un receptor) o extrae el siguiente bit de datos de el búfer compartido para continuar dando servicio al hardware (si es un transmisor) .El código normal llama a la función y acepta datos para agregarlos a un búfer saliente o busca y extrae datos del búfer entrante. Al emparejar cosas como esta y proporcionar el almacenamiento en búfer adecuado para sus necesidades, este arreglo puede funcionar todo el día sin problemas y desacopla su código principal de su servicio de hardware.
  2. Use una máquina de estado en su controlador de interrupción. Cada interrupción lee el estado actual, procesa un paso y luego pasa a un estado diferente o permanece en el mismo estado y luego sale. Esto puede ser tan complejo o tan simple como lo necesites. A menudo, esto es impulsado por un evento de temporizador. Pero no tiene por qué ser así.
  3. Crear un sistema operativo cooperativo. Esto puede ser tan fácil como configurar algunas pilas pequeñas asignadas mediante malloc() y permitir que las funciones llamen de manera cooperativa a una función switch() cuando hayan terminado con alguna tarea inmediata por ahora. Configura un subproceso separado para su función I2C, pero cuando decide que no hay nada que hacer en este momento, simplemente llama a switch() para provocar un cambio de pila a otro subproceso. Esto se puede hacer por turnos hasta que regrese y regrese de la llamada switch() que realizó. Luego regresa a su condición de tiempo, que verifica nuevamente. Si aún no hay nada que hacer, el while llama a switch() nuevamente. Etc. De esta manera, su código no se vuelve mucho más complejo de mantener y es simple insertar llamadas switch() en cualquier lugar que sienta la necesidad. Tampoco hay necesidad de preocuparse por la prioridad de las funciones de la biblioteca, ya que solo está haciendo la transición entre pilas en un límite de llamada de función, por lo que es imposible que se interrumpa una función de biblioteca. Esto hace que la implementación sea muy fácil.
  4. Hilos preventivos. Esto es muy parecido al #3 excepto que no hay necesidad de llamar a la función switch(). La preferencia se lleva a cabo en función de un temporizador, o un subproceso también puede elegir libremente liberar su tiempo. La dificultad aquí es lidiar con la preferencia de rutinas de biblioteca y/o hardware especializado donde puede haber secuencias de instrucciones específicas generadas por el compilador que no se pueden interrumpir (E/S consecutivas que deben recuperar un byte alto seguido de un byte bajo de la misma dirección asignada a la memoria, por ejemplo).

Sin embargo, creo que las dos primeras opciones son probablemente sus mejores apuestas. Sin embargo, mucho depende de cuánto depende del código de la biblioteca escrito por otros. Es muy posible que su código de biblioteca no esté diseñado para dividirse en componentes de nivel superior e inferior. Y también es muy posible que no pueda llamarlos según los eventos del temporizador u otros eventos basados ​​​​en la máquina de estado.

Tiendo a suponer que si no me gusta la forma en que la biblioteca hace el trabajo (solo bucles de espera ocupados), entonces estoy atascado escribiendo mi propio código para hacer el trabajo. Lo que significa que soy libre de usar cualquiera de los métodos anteriores.

Pero debe observar de cerca el código de la biblioteca y ver si ya tiene funciones que le permitan evitar el comportamiento de espera ocupada. Es posible que la biblioteca tenga soporte para algo diferente. Así que ese es el primer lugar para verificar, por si acaso. De lo contrario, juegue con la idea de usar las funciones existentes como parte de una división de controlador superior / inferior o como un proceso impulsado por una máquina de estado. Es posible que puedas resolver eso también.

No tengo ninguna otra sugerencia que me venga rápidamente a la mente, ahora mismo.

Corríjame si me equivoqué con la idea del n. ° 1: supongamos que tengo un código que lee algunos datos del bus I2C y hace algunos cálculos matemáticos pesados, por ejemplo, calcula el factorial de algún número. Como no tengo hilos aquí, mi código principal que calcula el factorial debería "pausarse" a veces (en lugar de ceder al hilo de manejo de interrupciones en algún lenguaje de alto nivel) y verificar si hay algún dato en el búfer que mencionó, agregado allí por controlador de interrupción ?
@AlexeyMalev Ese código factorial probablemente debería estar en su función principal, que llama al código de nivel superior . El código de nivel inferior no hace cosas que necesitan un descanso en el medio. Si suceden más cosas mientras su código principal está llamando al código superior o procesando después de llamarlo, la interrupción seguirá siendo atendida y los valores de relleno en el búfer por usted. Eso no se detiene incluso si está haciendo cálculos. No es necesario buscar más datos almacenados en el búfer, si no ha completado el otro trabajo. Si no eres capaz de mantener el ritmo, tienes un problema de todos modos.
@AlexeyMalev El trabajo de cálculo más largo debe estar en su código principal. Si tiene bits más cortos que deben realizarse entre tiempos, puede configurarlos en eventos de temporizador que interrumpen su código de cálculo largo para realizar sus tareas cortas. Estas tareas breves cronometradas también pueden llamar a la mitad del nivel superior de cualquier controlador para obtener datos almacenados en el búfer. Es solo una cuestión de diseñar un diagrama de tiempo. Deje suficiente tiempo entre los pasos para que se realicen los cálculos más largos, además de todos los tiempos de interrupción. Ese es su proceso principal. Sin embargo, puede usar mi opción n. ° 3 y saltear su código principal con llamadas switch ().
Lo siento, pero tengo que votar negativo. Esta larga respuesta no parece proporcionar ninguna información útil sobre cómo implementar una máquina de estado I²C controlada por interrupciones en un STM32.
La pregunta era: cómo implementar la interacción i2c sin bloqueo, mencioné las interrupciones como una opción. Basado en interrupciones en solo uno de los enfoques posibles.
@AlexeyMalev No conozco el entorno STM32 específicamente, pero traté de proporcionar respuestas amplias que generalmente se aplican, no bloquean y lo hice de inmediato.
@jonk Nono, me refería al comentario de JimmyB, no importa :)

Hay ejemplos en las STM32Cubebibliotecas. Obtenga el apropiado para su familia de controladores (por ejemplo, STM32CubeF4o STM32CubeL1) y busque Examples/I2C/I2C_TwoBoards_ComDMAen el Projectssubdirectorio.

Razón

Bueno, la razón es simple: el bloqueo es fácil y, a primera vista, parece funcionar. ¡Ay de ti si quieres hacer otra cosa, mientras tanto!

Entonces, sin entrar en muchos detalles, como no conozco el STM32, generalmente puede salir de este problema de dos maneras, según sus necesidades.

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));

Convertir a no bloqueante

O implementa un tiempo de espera para todos sus whilebucles. Esto significa:

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
static unsigned long start = now();
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT) && now()-start < TIMEOUT);  
if (now()-start >= TIMEOUT) { return ERROR_TIMEOUT; }

(Esto es pseudocódigo, por supuesto, entiende la idea. Siéntase libre de optimizar o ajustar sus preferencias de codificación según sea necesario).

Debe verificar los códigos de retorno cuando viaje hacia arriba en la pila y elija el lugar correcto donde realiza el manejo del tiempo de espera. Tenga en cuenta que también ayuda establecer una variable global i2c_timeout_occured=1o lo que sea para que pueda cancelar rápidamente más llamadas I2C sin tener que pasar demasiados argumentos.

Este cambio es bastante indoloro, con suerte.

De adentro hacia afuera

Si, en cambio, realmente necesita hacer otro procesamiento mientras espera ese evento, entonces necesita deshacerse del ciclo while interno por completo. Lo haces así:

void main_loop() {
   do_i2c_stuff();    // must never block
   do_other_stuff();
   ...
}

// Must never block. Assuming all I2C_... functions do not block either.
void do_i2c_stuff() {
  static int state=...;

  if (state==0) {
    I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
    state=1;
  } else if (state==1) {
    if (I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 
      state=2;
  } else ...
}

No es necesariamente muy complicado, dependiendo de tu otra lógica. Puede hacer mucho con la sangría, los comentarios y el formato adecuados para no perder la noción de lo que está programando.

La forma en que esto funciona es mediante la creación de una máquina de estado . Si miras tu código original, se ve así:

non-blocking code
while (!nonblocking_function_call1());
non-blocking code
while (!nonblocking_function_call2());

Para transformar eso en una máquina de estado, tiene un estado para cada uno:

state 0: non-blocking code
state 1: nonblocking_function_call1()
state 2: non-blocking code
state 3: nonblocking_function_call2()

Luego, como se muestra en el ejemplo anterior, llama a este código en un ciclo sin fin (su ciclo principal) y solo ejecuta el código que coincide con su estado actual (seguido en una variable estática state). El código de no bloqueo es trivial, no ha cambiado desde antes. El código de bloqueo se reemplaza por una variación que no bloquea, sino que solo se actualiza statecuando finaliza.

Tenga en cuenta que los whilebucles individuales se han ido; los ha reemplazado por el hecho de que tiene su bucle principal de nivel superior de todos modos, que llama a su máquina de estado repetidamente.

Esta solución puede ser dolorosa cuando tiene mucho código heredado, ya que no puede simplemente adaptar la función de bloqueo más interna, como en la primera solución. Brilla cuando comienzas a escribir código nuevo y sigues este camino desde el principio. Combínelo con muchas otras cosas que podría hacer un µC (por ejemplo, esperar a que se presione un botón, etc.); si te acostumbras a hacerlo de esta manera todo el tiempo, obtienes habilidades multitarea arbitrarias de forma gratuita.

Interrupciones

Francamente, para algo como esto (es decir, simplemente deshacerse del bloqueo infinito) me esforzaría por mantenerme alejado de las interrupciones a menos que tenga necesidades de tiempo extremas. Las interrupciones lo hacen complicado, rápido, es posible que no tenga suficientes de todos modos, y de todos modos se reducirá a un código bastante similar, ya que no desea hacer mucho más dentro de la interrupción, excepto establecer algunas banderas.

buena solución (subóptima) aquí: pensé que siempre sería necesario algún tipo de interrupción.
La idea era deshacerse de whilelos bucles. Tengo una pregunta sobre su último ejemplo de código; no estoy seguro de qué propone exactamente. El problema es que I2C_CheckEventregresa truedespués de un tiempo y no se bloquea, lo que significa que, en general, no es suficiente llamarlo una vez, lo cual (como lo obtuve) sugiere. ¿Puedes por favor explicar un poco sobre eso?
@AlexeyMalev, mis soluciones eliminan whilelos bucles infinitos. El primero todavía los tiene pero agrega un tiempo de espera. El segundo se deshace de ellos por completo (suponiendo que tiene un bucle de nivel superior que se ejecuta en un bucle perpetuo, de todos modos). He agregado alguna explicación, según lo solicitado. i2C_CheckEvent se llama a menudo, pero su método regresa inmediatamente, sin importar el resultado, por lo que también le da tiempo a las otras partes de su programa para que se ejecuten.
Esto no es "sin bloqueo". Sin bloqueo significa que su CPU entra en modo de suspensión cuando no se debe realizar ningún trabajo. Esto generalmente se hace usando un programador que permite que la tarea duerma en un semáforo en lugar de bloquear la CPU en un ciclo while. Cuando ocurre i2c, la rutina de interrupción da el semáforo y la tarea que estaba esperando en el semáforo se despierta y se ejecuta de nuevo. ESTO es sin bloqueo. Las soluciones descritas en esta publicación no lo son.
@Martin, hay diferentes aspectos del bloqueo. Mi respuesta refleja mi comprensión de la pregunta, que pregunta cómo evitar bloquear el flujo del programa al ingresar una llamada i2c. OP describe una variante de bloqueo, y mi solución esbozada es la correspondiente sin bloqueo. Mi solución resuelve el problema inmediato y es fácil de generalizar para interrumpir o manejar semáforos si es necesario.
Excepto que su primera solución no es sin bloqueo y su segunda solución REQUIERE un uso de CPU del 100% mientras una operación i2c está en progreso. Por lo tanto, ambas soluciones no son adecuadas para ningún código de nivel de producción serio.
@Martin, siento que mi respuesta es lo suficientemente extensa como para que el lector vea a lo que me refiero. No quiero decir que mi solución resuelva todos los problemas del mundo. No veo nada sobre el ahorro de energía de la CPU en la pregunta. Mi enfoque resuelve un problema específico (como se indicó): cómo intercalar este tipo de comunicación con otro procesamiento. Si prefiere una solución diferente, no dude en escribir una. Si tiene mejoras concretas sobre mi enfoque que no equivalen a una reescritura completa, siéntase libre de comentar y lo tomaré en cuenta.

Aquí hay una lista de I 2 C solicitud de interrupción disponible en un STM32. Como no sé el chip exacto que está utilizando, se recomienda consultar el manual de referencia del suyo si hay alguna diferencia, pero dudo que la haya.

ingrese la descripción de la imagen aquí

Para permanecer en su código de ejemplo, si necesita una versión sin bloqueo, debe habilitar el evento Bit de inicio enviado (maestro)ITEVFEN con el bit de control y verificar el SBindicador de evento dentro del ISR.

Teniendo en cuenta el extracto de @BenceKaulics de una hoja de datos, el pseudocódigo para una rutina de servicio de interrupción (ISR) podría verse así:

i2c_event_isr() {
  switch( i2c_event ) {

    case master_start_bit_sent: 

      send_address(...);
      break;

    case master_address_sent:
    case data_byte_finished:

      if ( has_more_data() ) {
        send_next_data_byte(); 
      } else {
        send_stop_condition();
      }

      break;
    ...
  }
}