3.26. El bus CAN en código

machine.CAN envuelve un controlador CAN por hardware. Inicialízalo con el id del bus y una tasa de bits:

from machine import CAN

can = CAN(1, 500_000)
can.set_filters(None)        # accept all incoming IDs

La tasa de bits debe coincidir exactamente con la de todos los demás nodos del bus: 125_000, 250_000, 500_000 y 1_000_000 son los valores comunes. set_filters() configura qué ID dejará pasar el controlador; None significa aceptar todo (útil al poner en marcha un enlace, menos útil una vez que el bus está ocupado).

3.26.1. Dentro del controlador

Tres elementos de hardware se sitúan entre el código Python de la cámara y el bus: los buzones de TX, un filtro de aceptación y la FIFO de RX. Cada uno se corresponde con una parte de la API de CAN usada a continuación.

Diagrama de bloques del controlador CAN. En el lado TX (arriba), can.send() deja caer las tramas en uno de los tres buzones de TX, y un bloque de arbitraje elige qué buzón pasa al bus a continuación. En el lado RX (abajo), las tramas entrantes pasan por un filtro de aceptación; las tramas aceptadas aterrizan en la FIFO de RX y can.recv() lee la FIFO desde su extremo más antiguo.

Los buzones de TX, el filtro de aceptación y la FIFO de RX del controlador entre el software y el bus.

  • Buzones de TX. Un pequeño conjunto de ranuras de hardware (normalmente tres) que retienen las tramas salientes que la cámara ha entregado mediante send() pero que aún no han llegado al bus. Cuando el bus está inactivo, el controlador elige el buzón con el ID de número menor (mayor prioridad) y arbitra por el bus en su nombre. La cámara no elige el buzón; el controlador asigna uno y devuelve su índice desde send().

  • Filtro de aceptación. Hardware configurable que compara el ID de cada trama entrante con una lista de patrones y descarta todo lo que no coincide. Las tramas que pasan continúan hacia la FIFO de RX; las tramas rechazadas las desecha el controlador sin que jamás lleguen a Python. set_filters() configura estos patrones.

  • FIFO de RX. Una cola primero en entrar, primero en salir: la primera trama que entra es también la primera en salir, como una fila en una taquilla. El controlador añade las tramas recibidas al final de la cola a medida que llegan, y recv() las extrae por el frente en el mismo orden. La cola importa porque el bus captura tramas en segundo plano mientras Python está ocupado en otra cosa; la cámara las va vaciando entonces una a una sin perder ninguna, siempre que la FIFO no se haya desbordado.

3.26.2. Envío de una trama

send() pone en cola una trama para su transmisión:

can.send(0x123, b"\x01\x02\x03\x04")

El primer argumento es el ID (un entero de 11 bits para una trama estándar); el segundo es la carga útil (de 0 a 8 bytes para CAN Classic). La llamada devuelve un pequeño índice entero que identifica el buzón de hardware al que fue la trama. El controlador arbitra con cualquier otro transmisor del bus y retransmite según sea necesario sin más ayuda del software.

Para ID extendidos (de 29 bits), aplica un OR con la bandera CAN.FLAG_EXT_ID en el tercer argumento:

can.send(0x18FF1234, b"hello", CAN.FLAG_EXT_ID)

3.26.3. Recepción de tramas

El controlador arranca sin ningún filtro instalado y descarta todas las tramas entrantes. Antes de que recv() devuelva nada, llama una vez a set_filters(); la forma más sencilla es None, que acepta todos los ID:

can.set_filters(None)   # accept every frame

recv() devuelve entonces la siguiente trama de la FIFO de recepción, o None si no hay nada esperando:

msg = can.recv()
if msg is not None:
    can_id, data, flags, errs = msg
    print("got", hex(can_id), bytes(data))

El bus llena la FIFO de RX en segundo plano, de modo que el bucle principal simplemente la vacía tan rápido como itera. Mientras la FIFO sea más profunda que el mayor intervalo entre vaciados, no se pierde ninguna trama.

3.26.4. Filtros

Un bus real suele estar ocupado con tramas que a la cámara no le importan. Los filtros por hardware permiten al controlador descartar los ID no deseados antes de que lleguen a la FIFO. set_filters() toma una lista de tuplas (id, mask, flags); una trama pasa el filtro si su ID, enmascarado por mask, coincide con el id configurado:

# Accept only IDs 0x100 - 0x10F (mask off the bottom 4 bits)
can.set_filters(((0x100, 0x7F0, 0),))

# Accept IDs 0x300 and 0x700 exactly
can.set_filters(((0x300, 0x7FF, 0),
                 (0x700, 0x7FF, 0)))

Las tramas no coincidentes las descarta el controlador y nunca aparecen en recv(), lo que ahorra tanto espacio de búfer como tiempo de CPU.

3.26.5. Estados de error y recuperación

Un bus CAN real acumula errores de transmisión: cortocircuitos a tierra, nodos ausentes, ruido eléctrico que corrompe bits. El controlador mantiene dos contadores que rastrean este comportamiento: un Contador de Errores de Transmisión (TEC) y un Contador de Errores de Recepción (REC), cada uno incrementado cuando el controlador detecta un error y decrementado tras una transferencia exitosa. Los valores de los contadores ponen al controlador en uno de cuatro estados:

  • Error Active (TEC y REC ambos por debajo de 96). Funcionamiento normal. Cuando el nodo detecta un error de bus, transmite una trama de error activa dominante, que obliga a todos los demás nodos a descartar la trama en curso para que el emisor pueda reintentar.

  • Error Warning (cualquiera de los contadores alcanza 96). Sigue plenamente activo en el bus: el estado de advertencia es una señal de software de que los errores se están acumulando, no un cambio de comportamiento.

  • Error Passive (cualquiera de los contadores alcanza 128). El nodo sigue en el bus pero deja de enviar tramas de error dominantes; los errores ahora se señalizan con tramas de error pasivas (recesivas) para que un nodo defectuoso no pueda seguir destrozando el bus para todos los demás.

  • Bus Off (TEC alcanza 256). El controlador ha decidido que este nodo es demasiado poco fiable para participar. Se desconecta del bus, deja de transmitir y de reconocer, y se mantiene al margen hasta que el software lo reinicie explícitamente.

Las tres primeras transiciones son totalmente automáticas: a medida que los contadores se decrementan tras tramas exitosas, el controlador se mueve de vuelta hacia Error Active sin intervención.

Bus Off es el único estado que requiere acción del software. restart() reinicia el controlador y lo devuelve a Error Active. Un patrón típico es comprobar el estado desde el bucle principal y reiniciar tras un breve retardo para dar tiempo al bus a estabilizarse:

import time
from machine import CAN

can = CAN(1, 500_000)
can.set_filters(None)

while True:
    if can.state() == CAN.STATE_BUS_OFF:
        time.sleep_ms(100)
        can.restart()
    # ... rest of the loop

Los valores actuales de los contadores están disponibles mediante get_counters() para diagnóstico: un TEC que sube de forma constante en un bus por lo demás tranquilo suele apuntar al cableado, la terminación o una tasa de bits mal configurada.