There are a number of concepts that can help you realize your dream. I can't, in the space of this answer, tell you exactly how to implement what you want, but I can show you the concepts that will help you to implement it.
First off there's the concept of the bus master. This isn't necessarily the device that instantiates communication - instead this is the device that owns and controls the bus.
When a device that isn't the bus master wants to communicate on the bus it first asks permission from the bus master. The old Z80 (well, I say "old", but they are still in use in many forms today) used this concept to allow other chips to use the data and address buses. It consists of two signals - BUSRQ and BUSACK. A device first looks to see if either of BUSRQ or BUSACK are active, and if neither are it then activates BUSRQ. If the bus master is willing to give up the bus to the other device (it's not using it at that moment) it activates BUSACK and the other device then knows that it can use the bus. Nothing else can use it until BUSRQ and BUSACK have both been released. Nice and simple and elegant.
But not perfect. If two devices both decide to ask for the bus at the very same instant you get a collision. This is a common problem amongst shared bus systems like this, and causes untold problems unless you know how to handle it properly.
Enter the concept of listen-while-you-talk. This involves the device that is sending on the bus also listening to what is being sent on the bus through a separate receiver. It then can know if what it sent on the bus is what actually ended up on the bus. For instance if two devices talk at the same time and one sends 10011001
and the other sends 11001100
the result that appears on the bus might actually end up as something else, such as 11011101
or maybe 10001000
depending on how the bus signals are created. So if you know what you sent got corrupted you can now do something about it.
Next concept: backing off. This is where both the senders wait for a short period and try and send again. As long as they both delay for a different amount of time the first one to try will get the bus and be able to communicate. But how do you guarantee that they will both delay for different times? You may think the answer is simple: use a random number, like rand()
or random()
. But that is also problematic:
Another concept: The pseudo random number generator
The Arduino doesn't generate random numbers. It just uses a complex mathematical formula to create a sequence of numbers that, to us, look random. They aren't though. Write a small program to print 10 random numbers through serial and run it multiple times (press the reset button). You will find the same "random" numbers in the same order every time. Try it on another Arduino and you get the same numbers again. Always the same.
So what to do? The answer is called seeding the random number generator. The next number generated by rand()
et al depend on the number that had been generated last. So change the first number and all the rest of the numbers will change. However, you have a catch-22 situation. You need a random number to seed the random number generator to make it random to be able to generate a random number to seed the random number generator... ad infinitum. You see where that's going? You can't seed from rand()
since rand()
isn't random until you have seeded from a random source. So you need to find a random source.
And that's not an easy task. The best source of entropy as it's known is white noise. This can be generated in a number of ways with a number of different circuits - from the breakdown of a diode junction to very high gain amplification of the thermal fluctuations in a resistor.
All are quite complex for what you want really, but there is a simpler, if slightly less random, method - read an analog input that isn't connected to anything. It won't have as much of a range as a proper entropy generator, but it should provide enough randomness to give a reasonable chance of each device getting a different seed.
Another useful concept is the interrupt.
This is good in a situation where you don't want the complexity of a multi-master bus with all the collisions etc. You have a single master that does all the work on the bus, and when a slave device has something important to say it nudges the master with an interrupt. The master then goes "Yes? What do you want?" to which the slave replies "Someone pressed my button".
That way the master isn't constantly polling the slave to see if the button has been pressed. It is often used in bus arrangements like SPI and there are many chips, such as IO expander chips, that can assert an interrupt when one of their input pins changes state.
But if you have 20 devices does that mean you have 20 interrupt pins? Not necessarily. New concept: wired OR.
It's perfectly possible to have multiple different slaves all using the same interrupt pin. The pin is normally held HIGH with a resistor (it could be an internal pullup resistor) and each slave has an open drain output connected to that pin. An open drain output, when "off", is not connected to anything - it's like the pin is in input mode (in fact it can be emulated on chips that don't have open drain by switching between input and output mode). When the output is "on" it connects the pin to ground, pulling down the IO pin, just like a button would.
It's then up to the master to make its way round the slaves that it knows are attached to that interrupt to see who needs attention. You can of course implement a number of different interrupt pins with different groups of slaves on each one - maybe a high priority one with just one device, and lower priority ones with multiple devices on each, for example.
The same concept of wired OR and open drain can be used for allowing multiple devices to all share the same physical wires. That is exactly how I2C works - the two bus lines are pulled up by resistors and the devices on it use open drain outputs to pull the line low to release it back to high to create the different logic levels. If two devices both pull it low together it will just be low. Without the open drain method if you had one device outputting a 1 and another outputting a 0 you would basically get a short circuit between the two and you would end up damaging chips.
And then of course you have the concept of synchronous versus asynchronous communication, but that is an entirely different kettle of fish. Simply said though, protocols with a clock, like SPI and I2C, which have a master generating that clock, are synchronous. Protocols like the UART and RS-232, RS-485, etc are asynchronous - they rely on both ends agreeing on how fast data is being sent (baud rate) so they know how to interpret the signals as they arrive.