¿Bus de bajo costo/complejidad para módulos expandibles?

Estoy tratando de elegir un bus para un sistema expandible. Los requisitos idealmente incluirían:

  • Número desconocido de módulos (potencialmente hasta un par de docenas).
  • Se permiten múltiples módulos idénticos (dar a los módulos identificaciones únicas por adelantado puede ser difícil)
  • Pequeño y económico de implementar (cada módulo puede contener un MCU económico)
  • No es necesario que sea de larga distancia, un par de pulgadas como máximo, pero no necesariamente en el mismo tablero.
  • > 1Mbps estaría bien.
  • Autobús compartido. No hay líneas seleccionadas por módulo, etc. Idealmente necesita manejar ramas y ciclos en el bus.

El principal candidato parece ser I2C, pero tener varios módulos idénticos es complicado.

Estaba pensando que podría idear algún esquema en capas sobre I2C. Por ejemplo: el maestro sondea a los esclavos que no tienen una dirección asignada mediante el envío de un mensaje a una dirección de "transmisión" predeterminada, y los módulos que no tienen una dirección responden aleatoriamente después de una cierta cantidad de sondeos para evitar la contención del bus. El módulo responderá con un número aleatorio + CRC, por lo que si hay una disputa, el maestro puede indicarles que vuelvan a intentarlo más tarde. Después de una cierta cantidad de encuestas sin respuestas, el maestro asume que todos los esclavos han respondido y puede continuar con la inicialización. Creo que este esquema podría ser compatible con dispositivos I2C estándar. Sin embargo, no sé si el soporte I2C integrado en MCU es compatible.

¿Es esto innecesariamente complejo? ¿Hay otros protocolos de bus simples que puedan manejar este escenario?

Si se produce una contención, ¿cómo sería el mensaje de reintento? ¿Otra dirección de transmisión especial?
¿Por qué no usar un interruptor DIP en cada módulo como dirección para el bus I2C?
@JonL: el maestro seguiría transmitiendo y el esclavo volvería a intentarlo después de un número aleatorio de transmisiones
@Saad: ese es mi plan alternativo, pero hay dos problemas potenciales: 1) los usuarios agregan los módulos, prefiero no pedirles que los configuren, 2) los módulos deben ser pequeños y baratos, y yo probablemente necesite al menos 4 bits configurables.
¿Existe una garantía de alguna dirección única 'configurada de fábrica' (no necesariamente la misma dirección que se usaría para la comunicación entre módulos) en estos módulos?

Respuestas (4)

Diseñé [1] algo como esto usando I2C una vez. (Como lo hice por trabajo, no puedo publicar el código). Siempre que tenga control sobre todos los nodos (todos son MCU programados por usted), esto debería funcionar.

Básicamente, los dispositivos están dispuestos en cadena utilizando I2C como de costumbre. Además del I2C, tiene una línea lógica punto a punto, utilizando dos pines PIO por nodo. Un pin ("sentido ascendente") es solo de entrada y se levanta, mientras que el otro pin ("sentido descendente") es solo de salida, pero inicialmente triestablecido (salida Z alta) y opcionalmente levantado. El pin de detección aguas arriba de cada nodo está conectado al pin de detección aguas abajo del siguiente chip aguas arriba. Los pines más alejados aguas arriba y más alejados aguas abajo se dejan desconectados. Opcionalmente, cada nodo puede tener un FET externo que conecta resistencias pull-up al bus I2C.

Al encender, todos los nodos tienen sus puertos I2C como esclavos con la dirección 0 o algo similar (realmente no importa), conducen sus pines de sentido descendente a 0 y esperan un tiempo fijo (depende de cuánto tiempo tome para todos sus nodos para encender e inicializar). Lo que buscan recibir es un mensaje de "llamada general" (difusión).

Cualquiera que sea el nodo que esté más lejos aguas arriba no verá su sentido aguas arriba reducido en este tiempo. Entonces, va primero (si los pull-ups están controlados por FET, enciende su pull-up), establece su puerto como maestro y transmite un mensaje de llamada general que se identifica a sí mismo a los otros nodos, incluida su dirección (lo que usted quiera). desea usar para el primero) y cualquier otra información que identifique qué es para los otros nodos. Luego espera una cantidad de tiempo fija para que otro nodo (no debería ser ninguno, pero quién sabe) envíe un mensaje de llamada general que diga que, de hecho, están en la primera dirección. Si recibe dicho mensaje, repite su identificación, pero con la siguiente dirección. Este ciclo se repite hasta que encuentra una dirección disponible. (Este patrón permite que un nodo se reinicie y recupere su dirección sin confundir el bus).

Una vez que está seguro de su dirección, lo configura en el periférico I2C y pasa al modo esclavo, para escuchar otros nodos, y eleva su línea de detección aguas abajo, lo que le dice al siguiente nodo aguas abajo que realice el mismo proceso para obtener su DIRECCIÓN. En este punto, solo escucha a las personas que intentan reclamar su dirección y registra la información de identificación de los otros nodos. (Los nodos también escuchan la identificación de otros nodos antes de obtener un flanco ascendente en el sentido ascendente, creando una tabla de red, pero aún no tienen una dirección reclamada, por lo que no verifican las colisiones. Cuando llega el momento de reclamar una dirección, puede usar los datos de la tabla para elegir una dirección probablemente no reclamada).

Después de todo esto, todos deberían tener direcciones I2C únicas y estar listos para comenzar. Entonces solo usas I2C como de costumbre. (No hace falta decir que cualquier dirección inicial que tuvieran todos los nodos no se podía usar después de la configuración). En nuestra configuración, la llamada general solo se usaba para la configuración, y el direccionamiento directo solo se usaba para el trabajo real. Si desea usar la llamada general después de la configuración, deberá diseñar su mensaje de llamada general para marcar en qué modo se encuentra.

Probablemente hay mucho que se puede optimizar aquí, pero debería servirle de punto de partida. Usamos esto en una placa piggyback para una fuente de alimentación de medio bloque, por lo que podría unir los ladrillos que necesitara (agregamos conectores de acoplamiento de borde a nuestras placas para llevar I2C y las otras líneas) y luego conectarlo a un puerto serie en cualquiera de los ladrillos para obtener información de voltaje, corriente y temperatura en todos ellos. Fue bastante agradable y consiguió que nuestro estudiante (que hizo el trabajo pesado) obtuviera una A en el laboratorio de último año. (Luego corrió lo más rápido que pudo a la escuela de posgrado en todo el país...)

[1] Por "diseñado" quiero decir que escribí algo similar al texto anterior, el 1% de inspiración según Edison. El 99% de la transpiración fue proporcionado por mi estudiante de pregrado.

Una vez diseñé algo como esto para un tipo que necesitaba hacer mediciones en una cuadrícula IIRC de 30 x 30 en un invernadero. Utilicé una codificación de pulso corto/pulso largo para permitir la inexactitud del reloj integrado del PIC. Cada cadena de 30 nodos usaba un chip 74HCT como buffer/regenerador. Sin la intervención del PIC del nodo, cada nodo recibió la señal del 'maestro principal'. Pero cada nodo podría bloquear la señal al siguiente nodo, que se utilizó durante el inicio (fase de enumeración). Los PIC eran 16F819, en ese momento los PIC más baratos que podían autoescribirse. (Toda la cadena podría actualizarse con firmware en paralelo). Los jefes de cada cadena se encadenaron a su vez de manera similar, por lo que se podía acceder a la cuadrícula completa de ~ 1k nodos desde una PC. IIRC, el cliente una vez tuvo esta configuración en algún lugar de España, a unos 1000 km de donde vivía,

IIRC, la velocidad en baudios no era tan alta, 19k2 más o menos. Pero el costo del nodo fue mínimo: PIC, chip 74HCT, algunas resistencias y los siempre presentes 100nF y 22uF.

Uso mucho SPI y parece funcionar muy bien. Hay muchos dispositivos que admiten SPI y lo he usado en varias aplicaciones de control de microcontrolador a microcontrolador.

SPI está fuera debido a su requisito de "Sin líneas seleccionadas por módulo".
@Mike DeSimone: puede usar una línea de selección común y un direccionamiento suave, con el mismo esquema de asignación propuesto en la pregunta, siempre que se use algún medio para hacer que las colisiones de autobuses sean detectables (¿controladores MISO de colector abierto con pullup?). Realmente se convierte en una cuestión de qué esquema eléctrico es preferible.
@Chris Stratton: Lo que quiere decir con "direccionamiento suave" no está claro. La detección de colisiones requerirá que MISO sea triestable. Cuando haya terminado, estará a medio camino de I2C con más cables.
@MikeDeSimone: me refiero a poner una dirección en cada paquete de datos. El tri-estado ordinario en realidad no funcionaría del todo para la detección de colisiones, requeriría algo que se pueda anular, como un colector abierto y un pullup. Por supuesto, puede simular un colector abierto con tres estados activando o no la habilitación en función de los datos. Cada uno de I2C y SPI tiene sus ventajas y adeptos.

Sé que quería > 1 Mb, pero si está dispuesto a conformarse con 1 Mb exactamente, entonces el bus CAN es un bus bastante decente y de bajo costo. Si se trata de un bus corto, es posible que incluso se escape sin transceptores o simplemente con transistores simples. Así es como numeraría automáticamente todos los nodos idénticos en el bus. Cada nodo esperaría un número aleatorio de milisegundos y luego enviaría un mensaje CAN simple. El número de mensajes de latas que ve antes de enviar el suyo propio le indica qué ID debe ser. Si más de un nodo envía un mensaje al mismo tiempo, ambos mensajes se ignoran y cada nodo espera otro tiempo aleatorio.

Cada uno tendría el siguiente código:

int negotiate_my_id(void)
{
    int pause_time = random number between 2 .. 100
    int my_id = 0;
    int num_messages_this_ms = 0;

    initialise_100kHz_timer();


    while(pause_time)
    {
        if (pause_time == 1)
            Send_CAN_message ( ID = 1, length = 0);


        timer_value = 100;                 // timer_value is decremented by the timer
        while(timer_value)
        {
            if (CAN_message_seen())        // Count num CAN messages seen
            {                              // during this millisecond.
                num_messages_this_ms++;    
                timer_value = 100;         // All nodes synchronise on message.
            }
        }

        if (num_messages_this_ms == 1)
        {
            my_id++;
        }

        if (num_messages_this_ms > 1)      // Saw a collision?
        {
            if (pause_time == 1)           // with my message?
                pause_time = random number between 2 .. 100
        }

        num_messages_this_ms = 0;
        pause_time--;
    }

   return my_id;
}