4
\$\begingroup\$

I'm trying to pick a bus for an expandable system. Requirements would ideally include:

  • Unknown number of modules (potentially up to a couple dozen).
  • Multiple identical modules are allowed (giving modules unique IDs ahead of time might be difficult)
  • Small and cheap to implement (each module can contain a cheap MCU)
  • Does not need to be long distance, a couple inches at most, but they not necessarily on the same board.
  • > 1Mbps would be nice.
  • Shared bus. No per-module select lines, etc. Ideally needs to handle branches and cycles in the bus.

Leading candidate seems to be I2C, but having multiple identical modules is tricky.

I was thinking I could come up with some scheme layered on top of I2C. For example: the master polls for slaves that aren't assigned an address by sending a message to a predetermined "broadcast" address, and modules that don't have an address randomly respond after a certain number of polls to avoid bus contention. The module will respond with a random number + CRC, so if there's contention the master can tell them to retry later. After some number of polls with no responses the master assumes all slaves have responded and can continue with initialization. I think this scheme could be compatibile with standard I2C devices. I don't know whether I2C support built into MCUs is compatible though.

Is this unnecessarily complex? Are there other simple bus protocols that can handle this scenario?

\$\endgroup\$
  • \$\begingroup\$ If contention occurs, what would your retry message look like? Another special broadcast address? \$\endgroup\$ – Jon L Dec 25 '11 at 4:06
  • 2
    \$\begingroup\$ Why not use a DIP Switch on each module as an address for the I2C bus? \$\endgroup\$ – Saad Dec 25 '11 at 5:21
  • \$\begingroup\$ @JonL: the master would keep broadcasting, and the slave would try again after some random number of broadcasts \$\endgroup\$ – tlrobinson Dec 25 '11 at 8:01
  • \$\begingroup\$ @Saad: that's my fallback plan, but there are two potential problems: 1) the modules are added by users, I'd rather not ask them to configure them, 2) the modules need to be small and cheap, and I'd probably need at least 4 bits configurable. \$\endgroup\$ – tlrobinson Dec 25 '11 at 8:04
  • \$\begingroup\$ Is there a guarantee of some 'factory configured' unique-address (not necessarily same address that would be used for inter-module comm) in these modules ? \$\endgroup\$ – icarus74 Dec 25 '11 at 11:38
3
\$\begingroup\$

I designed[1] something like this using I2C once. (Since I did it for work, I can't post the code.) As long as you have control over all the nodes (they're all MCUs programmed by you), this should work.

Basically, the devices are arranged in a daisy-chain using I2C as normal. In addition to the I2C, you have a point-to-point logic line, using two PIO pins per node. One pin ("upstream sense") is input-only and pulled up, while the other pin ("downstream sense") is output-only, but initially tri-stated (high-Z out) and optionally pulled up. Each node's upstream sense pin is connected to the downstream sense pin of the next chip upstream. The farthest-upstream and farthest-downstream pins are left unconnected. Optionally, each node can have an external FET which connects pull-up resistors to the I2C bus.

On power up, all nodes have their I2C ports as slaves with address 0 or some such (doesn't really matter), drive their downstream sense pins to 0, and wait for a fixed time (depends on how long it takes for all your nodes to power up and initialize). What they're looking to receive is an "all call" (broadcast) message.

Whichever node is farthest upstream will not see its upstream sense pulled low in this time. So it goes first (if pull-ups are FET-controlled, it turns its pull-up on), sets its port as a master, and broadcasts an all-call message identifying itself to the other nodes, including its address (whatever you want to use for the first one) and any other information identifying what it is to the other nodes. Then it waits for a fixed amount of time for another node (should be none, but who knows) to send an all-call message saying that they are in fact at the first address. If it gets such a message, it then repeats its identification, but with the next address. This cycle repeats until it finds an available address. (This pattern allows a node to reset and get its address back without confusing the bus.)

Once it is sure of its address, it sets it in the I2C peripheral and goes to slave mode, to listen for other nodes, and drives its downstream sense line high, which tells the next node downstream to go through the same process to get its address. At this point, it just listens for people trying to claim its address, and records the identification information of the other nodes. (Nodes also listen for other nodes' identification prior to getting a rising edge on upstream sense, building a network table, but they don't have a claimed address yet, so they don't check for collisions. When it comes time to claim an address, it can use the table data to pick a likely unclaimed address.)

After all this, everyone should have unique I2C addresses and be ready to go. Then you just use I2C as normal. (Needless to say, whatever initial address all nodes had could not be used post-configuration.) In our setup, all-call was only used for configuration, and direct addressing was only used for real work. If you want to use all-call after configuration, you'll need to design your all-call message to flag which mode it's in.

There's probably plenty that can be optimized here, but it should give you a start. We used this on a piggyback board for a half-brick power supply, so you could just snap together whatever bricks you needed (we added edge-mating connectors to our boards to carry I2C and the other lines) and then plug into a serial port on any one of the bricks to get voltage, current, and temperature information on all of them. It was pretty sweet and got our student (who did the heavy lifting) an A in senior lab. (Then he ran as fast as he could to grad school across the country...)

[1] By "designed" I mean I wrote up something similar to the text above, the 1% inspiration per Edison. The 99% perspiration was provided by my undergrad student.

\$\endgroup\$
1
\$\begingroup\$

I once designed something like this for a guy who needed to do measuremenst on an IIRC 30 x 30 grid in a greenhouse. I used a short pulse / long pulse encoding to allow for the inaccuracy of the PIC built-in clock. Each chain of 30 nodes used a 74HCT chip as buffer/regenerator. Without the node's PIC intervention, every node received the signal from the 'head master'. But each node could block the signal to the next node, which was used during startup (enumeration phase). The PICs were 16F819, at that time the cheapest PICs that could self-write. (The whole chain could be firmware-updated in parallel). The heads of each chain were in turn chained in a similar fashion, so the whole grid of ~ 1k nodes could be accessed from one PC. IIRC the customer once had this setup somehwere in Spain, a few 1000's km from where he lived, and he could do both firmware updates and measuerents from home.

IIRC the baudrate was not that high, make 19k2 or so. But node cost was very minimal: PIC, 74HCT chip, a few resistors and the always-present 100nF adn 22uF.

\$\endgroup\$
1
\$\begingroup\$

I use SPI a lot and it seems to work very well. There are a lot of devices out there that support SPI and I have used it in a number of microcontroller-to-microcontroller control applications.

\$\endgroup\$
  • 2
    \$\begingroup\$ SPI is out because of his "No per-module select lines" requirement. \$\endgroup\$ – Mike DeSimone Dec 25 '11 at 5:29
  • \$\begingroup\$ @Mike DeSimone - you can use a common select line and soft addressing, with the same assignment scheme proposed in the question, provided some means of making bus collisions detectable (open collector MISO drivers with pullup?) is used. It really becomes a question of which electrical scheme is preferable. \$\endgroup\$ – Chris Stratton Dec 25 '11 at 19:50
  • \$\begingroup\$ @Chris Stratton: What you mean by "soft addressing" is unclear. Collision detection will require MISO to be tri-statable. When you're done, you'll be halfway to I2C with more wires. \$\endgroup\$ – Mike DeSimone Dec 25 '11 at 20:22
  • \$\begingroup\$ @MikeDeSimone - I mean putting an address in each data packet. Ordinary tri-state would actually not quite work for collision detection, it would require something override-able, such as open collector & pullup. Of course you can simulate open-collector with tristate by driving the enable or not based on the data. Each of I2C and SPI has its advantages, and adherents. \$\endgroup\$ – Chris Stratton Dec 25 '11 at 20:40
0
\$\begingroup\$

I know you wanted > 1Mb, but if you're willing to settle for 1Mb exactly, then CAN bus is a pretty decent, low cost bus. If it's a short bus, you might even get away without transceivers, or with just simple transistors. Here's how you would automatically number all of the identical nodes on the bus. Each node would wait a random number of milliseconds, then send a simple CAN message. The number of can messages it sees before it sends it own tells it which ID it should be. If more than one node sends a message at the same time, both messages are ignored, and each node waits another random time.

Each one would have the following code:

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;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.