Implementación de I2C sin bloqueo en STM32

3

La mayoría de los artículos que puedo encontrar que son esencialmente "I2C para dummies en el microcontrolador XXX" sugiere bloquear la CPU mientras se esperan los eventos de confirmación. Para ejemplo , (los comentarios y la descripción están en ruso, pero en realidad eso no importa). Este es 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 bucles while 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 silbidos, pero no puedo dar ningún ejemplo de código. ¿Puedes ayudarme con eso por favor?

    
pregunta Alexey Malev

5 respuestas

4

Claro.

  1. Configure sus controladores en términos de un par upper y lower , separados por un búfer compartido. La función lower es un bit de código impulsado por interrupciones que responde a eventos de interrupción, se ocupa del trabajo inmediato necesario para servir el hardware y agrega datos al búfer compartido (si es un receptor) o extrae el siguiente bit de datos del búfer compartido para continuar dando servicio al hardware (si es un transmisor). La función superior es llamada por su código regular y acepta datos para agregar a un búfer saliente o de lo contrario, verifica y extrae datos del búfer entrante. Al agrupar las cosas de esta manera, y proporcionar un almacenamiento en búfer adecuado para sus necesidades, esta disposición 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 hace una transición a un estado diferente o permanece en el mismo estado y luego sale. Esto puede ser tan complejo o tan simple como lo necesite. A menudo, esto se desactiva de un evento de temporizador. Pero no tiene que ser así.
  3. Crea un sistema operativo cooperativo. Esto puede ser tan fácil como configurar algunas pilas pequeñas asignadas usando malloc () y permitir que las funciones llamen de forma cooperativa a una función de cambio () cuando hayan terminado con alguna tarea inmediata por ahora. Configuró 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 en modo round-robbin hasta que regrese y regrese de la llamada switch () que realizó. Luego regresa a su condición de "while", que verifica nuevamente. Si todavía no hay nada que hacer, las llamadas while cambian () nuevamente. Etc. De esta manera, su código no es mucho más complejo de mantener y es sencillo insertar llamadas switch () en cualquier lugar que sienta la necesidad. Tampoco es necesario preocuparse por la prioridad de las funciones de 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 interrumpir una función de biblioteca. Esto hace que la implementación sea muy fácil.
  4. Subprocesos preventivos. Esto es muy parecido al # 3, excepto que no hay necesidad de llamar a la función switch (). La anticipación tiene lugar en función de un temporizador, o un hilo también puede elegir libremente liberar su tiempo. La dificultad aquí es lidiar con la preferencia de las rutinas de la biblioteca y / o el hardware especializado donde puede haber secuencias de instrucciones específicas generadas por el compilador que no pueden ser interrumpidas (E / S consecutivas que deben recuperar un byte alto seguido de un byte bajo de la misma dirección asignada de memoria, por ejemplo.)
Sin embargo, creo que las dos primeras opciones son probablemente tus mejores apuestas. Sin embargo, mucho depende de cuánto estés dependiendo del código de biblioteca escrito por otros. Es muy posible que el código de su biblioteca no esté diseñado para dividirse en componentes de nivel superior e inferior. Y también es muy posible que no pueda llamarlos en función de eventos de temporizador u otros eventos basados en máquinas de estado.

Tiendo a suponer que si no me gusta la forma en que la biblioteca hace el trabajo (solo en los bucles de espera ocupados), entonces me quedo 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 detenidamente el código de la biblioteca y ver si ya tiene funciones que le permitan evitar el comportamiento de espera ocupado. Es posible que la biblioteca tenga soporte para algo diferente. Entonces ese es el primer lugar para verificar, por si acaso. Si no, juegue con la idea de usar las funciones existentes como parte de una división del controlador upper / lower o bien como un proceso impulsado por una máquina de estado. Es posible que también puedas resolver eso.

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

    
respondido por el jonk
4

Motivo

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, ya que no conozco el STM32, generalmente puede solucionar este problema de dos maneras, dependiendo de sus necesidades.

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

Convertir a no bloqueo

O implementas un tiempo de espera para todos tus while loops. 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, se te ocurre la idea. Siéntete libre de optimizar o ajustar tus preferencias de codificación según sea necesario.)

Tienes que revisar los códigos de retorno cuando subes la pila y elegir el lugar correcto donde realizas tu tiempo de espera. Tenga en cuenta que también es útil establecer una variable global i2c_timeout_occured=1 o lo que sea para que pueda abortar 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 completamente del bucle interior while. 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. Puedes hacer mucho con el sangrado / comentario / formateo adecuado para que no pierdas de vista lo que estás programando.

La forma en que funciona es creando 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, tienes 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, usted llama a este código en un bucle sin fin (su bucle principal), y solo ejecuta el código que coincide con su estado actual (rastreado 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, pero solo actualiza state cuando finaliza.

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

Esta solución puede ser dolorosa cuando tienes muchos códigos heredados, ya que no puedes simplemente adaptar la función de bloqueo más interna, como en la primera solución. Brilla cuando empiezas a escribir código nuevo y sigues así desde el principio. Combínelo con muchas otras cosas que un µC podría hacer (por ejemplo, esperar a que se presione el 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 evitar interrupciones a menos que tenga necesidades extremas de tiempo. Las interrupciones hacen que sea complicado, rápido, es posible que no tengas suficientes de todas formas, y de todos modos se reducirá a un código bastante similar, ya que no quieres hacer mucho más dentro de la interrupción, excepto establecer algunas banderas.

    
respondido por el AnoE
3

Hay ejemplos en las bibliotecas STM32Cube . Obtenga el apropiado para su familia de controladores (por ejemplo, STM32CubeF4 o STM32CubeL1 ) y busque Examples/I2C/I2C_TwoBoards_ComDMA en el subdirectorio Projects .

    
respondido por el berendi
3

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

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

    
respondido por el Bence Kaulics
2

Considerando el extracto de @BenceKaulics de una hoja de datos, el pseudo-código para una rutina de servicio de interrupción (ISR) podría tener este aspecto:

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;
    ...
  }
}
    
respondido por el JimmyB

Lea otras preguntas en las etiquetas