This weekly debrief is for paying supporters of my work. Please only read if you’ve paid. Thanks!
→ Click here if you've paid ←
This week’s debrief builds on the one from last week about working on the designs of the device bus interfaces, you might want to go read that one first if you haven’t already. I’ve been working through some issues with the current plans, and I wanted to take the opportunity to talk about some interesting design aims that have been lurking in the background.
Handling missing devices
For a while there’s been a massive unresolved question lying beneath all of this device interface work that I’ve only just managed to figure out an answer to: what should happen if a device is unavailable?
On one hand, if a program tries to reach out to, for example, the screen device to draw up a nice user interface, and no screen device is available on this system, the architecture spec says that the call should return immediately with a default value so that the program can continue without stuttering. On the other hand, we’ve got a bit more of a mess to deal with than the architecture spec ever did — our devices are having to communicate back and forth over real, physical wires, with the devices themselves implemented as messy piles of cords and boards all running asynchronously. Is the device really unavailable, or is it just taking a while to respond? How long should we have to wait?
Human in the loop
On top of this, there’s another piece of functionality that I really wanted to work in: users should be able to mock up devices by poking at a row of switches, tapping responses to commands directly into the bus with their fingers. Imagine you’re building a physical Bedrock system, and you haven’t rigged up a clock device yet, but a program needs to retrieve the current year from the clock device anyway. If the bus interface was designed to keep going if it hasn’t received a response within a 10 millisecond window, then the only way you’d be able to inject a fake response fast enough would be to build some circuit or other around a microcontroller (which is just way too complicated). If the bus was designed to wait indefinitely, however, then all you’d need is a shift register and a couple of switches, and you’ll be able to inject a response to any command at your own pace.
This is a big reason why the hardware device interface is built around SPI (which needs four signalling wires), and not something snazzy and slick like I2C (which would only need two, but doesn’t wait around).
Take it slow
So what do we do if a command is sent out to a device but doesn’t return right away? The Bedrock architecture spec says that devices can take as long as they need before they get back to you, but we don’t know if the device is working and busy, or if it was never connected to begin with.
We’ve got two choices: we can wait indefinitely, hoping for a response, or we can give up after a short timeout, returning a default value and continuing onwards. The former is simpler but would lead to the system locking up if some optional device isn’t available, which isn’t great for user experience. The latter is cleaner, the user won’t notice if a device is missing, but now we have to determine a good timeout duration that is long enough to allow slow devices to respond, but short enough that repeated commands to an unavailable device don’t slow down the system. Tricky tricky.
The solution I’ve decided on is to wait indefinitely. Let’s imagine we’ve got a stream device that communicates over a telegraph line using morse code, of all things, and you write to the transmission control port, causing the device to start tapping out a message over the next three seconds. This is the kind of weird and wonderful functionality that I want Bedrock to support, but the only way for this to work is if we give the device as much time as it needs to get the job done, waiting around and hoping for a response in the meantime. With a timeout-based approach, the system will either cut the device off because it’s taken too long to come back with a response, or otherwise every command sent to an unavailable port will freeze the system for the full three second timeout.
We could add an ‘acknowledged’ signal to the protocol so that we know if the device has started working, but then we’ve just circled back to figuring out how long a timeout should be, and regardless, it doesn’t work for device behaviours that are more complicated than just ‘start processing, stop processing’ (such as the sleep port on the system device).
Scanning and stubbing
So the plan is to freeze the system forever if a device is unavailable. This sounds bad, but we can add a couple of extra details to fix it up.
First up, we can stub out devices that we know are unavailable. In the most basic case, we could make a small device board that plugs into the bus and sends back a null response to every command sent to a particular slot ID. We don’t even need a microcontroller for this, we just need a four-bit shift register and some bitwise logic. Add a tiny row of switches, one per slot, and it’ll be able to auto-respond to commands addressed to any device in a list. You’d use this by flicking on the switches that match the unavailable devices, so that each command goes either to a connected device or is handled by the stub device.
A second approach we could take is to have the system scan over the list of devices once at boot, gathering a list of which devices are available and which are unavailable. We know that the check connection command doesn’t need extra processing time, so we could use a short timeout here to deal with missing responses, adding only a few milliseconds to the system boot time. Now that the system knows up-front which devices are connected, it can avoid sending out commands for the missing devices in the first place, speeding things up significantly.
Finally, if you’re doing some debugging work, you could boot the system without this scanning behaviour, leaving any devices that you want to test unstubbed. When a command comes in, the system will freeze, you can look at the command, and then you can type a response by hand at your own pace.
Broadcasting vs routing
Now that we’ve got that all sorted out, we can dig into another decision that’s been hanging around in the background: do we broadcast each command to all devices on the device bus, or do we route them directly to the addressed device?
Routed
With a routed design, a device bus would look like a telephone switchboard, or to give a more modern example, like a home internet router. One cable connects to the Bedrock system (the internet or fibre box, in this metaphor), and on the other side is a row of sixteen sockets for plugging in the device boards, one socket per device.
The benefits of a routed design are that each command can be sent directly to the addressed slot (meaning we don’t need to send a slot ID with each command), and that the address of each device is determined by the slot it’s connected to (meaning we don’t need to find a way to give slot IDs to custom devices). There’s also no risk of two devices ever sharing the same address, and there’s an opportunity for a more sophisticated bus board to auto-configure a pool of custom devices on boot, allocating slot IDs according to the preferences of the current program (as per this idea).
The big downside of a routed design is that a full socket board would take up a lot of space on small systems, especially when we’re talking about systems the size of a credit card.
Broadcasted
With a broadcasted design, the device bus would be a single socket shared simultaneously between any number of devices. If routed mode looks like modern switched ethernet, broadcast mode looks more like old coaxial ethernet, with new devices stabbing into the bus with a metal spike. To connect multiple devices to the bus, you’d either connect a sort of passive hub board that splits one socket out into multiple, or you could daisy-chain your devices together end-to-end — the important point is recognising that every device is connected electrically to a single shared socket up top.
To send a command to a device, the command is broadcast from that one socket to every connected device, meaning that now you’ve got a dozen different devices that can choose to respond. It’s up to the devices to act in an orderly fashion here — at most one device should respond (to avoid a train crash), and that device should be the one with slot ID matching that of the command. That should all be fine though, we can trust that the devices are all playing fair, and if they’re not then we can yank them and dealt with them, no harm done. Additionally, a broadcasted design allows us to create device boards that can provide functionality for multiple devices at the same time (like a combination input+screen device).
The downsides are similar to before, in that you’ll need some way to tell custom devices which slot IDs to listen out for (perhaps by adding a four-bit switch array to each board).
Command format
A very appealing consequence of choosing a routed design for the interface is that the command format can be just two bytes long: the first byte can be the 4-bit command ID followed by the 4-bit port number, and the second byte can be the data byte used by the write command. With a broadcasted design we’d have to fit a 4-bit slot ID in there as well, so the format rounds up to three bytes (8-bit slot+port, 4-bit command with 4-bit padding, and 8-bit data).
In reality, the extra 8 bits probably don’t make a massive difference, but it’s worth pointing out. This all still needs to be tested anyway.
Smaller is better
Wait, that heading doesn’t make sense. I mean physically smaller, prioritising flexibility over structure and all that.
The only design that makes sense for what I need it to do is the broadcasted design. Routed is fine if we’re only building traditional desktop computers, but a constant refrain around Bedrock is that I want it to be flexible enough to be able to do anything. Imagine if we could connect a device to a system wirelessly, but using audio, not radio. Have a speaker scream out a command as a sequence of piercing tones, and a device board halfway across the room can scream back a response. What whimsy, what joy! This, I suppose, is why we need to choose the broadcasted design.
In all seriousness, the ability to use a single small connector for every device is another reason why the broadcasted design is so important. I want to make tiny little boards that work as fully functional desktop computers, and they just won’t have the space for anything larger than a single socket.
Thanks
I’m doing a bad job of writing these posts any shorter, but at least there’s plenty of interesting stuff to read. Thanks for supporting my work and my writing and my hopes and my dreams. See you next time!