Upcycling a Tandy Model 100, Part II: I2C Strikes Back

Surprises can lurk in even a simple comms protocol

5 min read

Two stacks of rectangular circuit boards sit under a notebook computer’s keyboard, joined by a handful of wires.

A microcontroller [directional?] ]capable of running CircuitPython and providing lots of storage now commands the Arduino Mega [directional?] that drives the Tandy’s LCD screen [top].

James Provost

Last year for Hands On, I gutted a defunct TRS-80 Model 100. The goal was to upgrade its 24 kilobytes of RAM and 2.4-megahertz, 8-bit CPU but keep the notebook computer’s lovely keyboard and LCD screen. That article was almost entirely about figuring out how to drive its squirrely 1980s-era LCD screen. I left the rest, as they say, as an exercise for the reader. After all, sending a stream of data from a new CPU to the Arduino Mega controlling the screen would be a trivial exercise, right?


No, folks, no it was not. IEEESpectrum’s Hands On articles provide necessarily linear versions of how projects come together. It can give the impression we’re terribly clever, which has about the same relationship to reality as an influencer’s curated social-media feed. So every now and then I like to present a tale steeped in failure, just as a reminder that this is what engineering’s like sometimes.

To send screen data to the Mega, I had a choice between several methods that are supported by CircuitPython’s display driver libraries. I wanted to use a CircuitPython-powered microcontroller as the Model 100’s new brain because there’s a lot of existing software I could port over. In particular, CircuitPython’s display libraries would greatly simplify creating graphics and text and would automatically update the display. My choices were between a parallel interface and two serial interfaces: SPI and I2C.

The parallel interface would require at least 12 wires. SPI was better, being a four-wire interface. But I2C was best of all, requiring only two wires! Additionally, there are many breakout boards that support I2C, including storage and sensors of all types. One I2C bus can, in theory, support over a hundred I2C peripherals. I2C is much slower than SPI, but the Model 100’s delightfully chunky 240-by-64-pixel display is slower still. And I’d used I2C-based peripherals many times before in previous projects. I2C was the obvious choice. But there’s a big difference between using a peripheral created by a vendor and building one yourself.

An illustration of the chips.  The Grand Central controller [bottom] provides the new brains of the Tandy. Although the controller has the same form factor as the Arduino Mega, it has vastly more compute power.. A custom-built shield holds a supporting voltage-level shifter [top left] that converts the 3.3- and 5-volt logic levels used by the controllers appropriately.James Provost

On the circuit level, I 2C is built around an “open drain” principle. When the bus is idle, or when a 1 is being transmitted, pull-up resistors hold the lines at the voltage level indicating a logical high. Connecting a line to ground pulls it low. One line transmits pulses from the central controller as a clock signal. The other line handles data, with one bit transmitted per clock cycle. Devices recognize when traffic on the bus is intended for them because each has a unique 7-bit address. This address is prepended to any block of data bytes being sent. In theory, any clock speed and or logic level voltage could be used, as long as both the controller and peripheral accept them.

And there was my first and, I thought, only problem: The microcontrollers that ran CircuitPython and were computationally hefty enough for my needs ran on 3.3 volts, while the Arduino Mega uses the 5 V required to drive the LCD. An easy solve though—I’d just use a US $4 off-the-shelf logical level shifter, albeit a special type that’s compatible with I2C’s open-drain setup.

Using a $40 Adafruit Grand Central board as my central controller, I connected it to the Mega via the level shifter, and put some test code on both microcontrollers. The most basic I2C transaction possible is for the controller to send a peripheral’s address over the bus and get an acknowledgement back.

No response. After checking my code and wiring, I hooked up a logic analyzer to the bus. Out popped a lovely pulse train, which the analyzer software decoded as a stream of correctly formed addresses being sent out by the Grand Central controller as it scanned for peripherals, but with no acknowledgement from the Mega.

A diagram showing the arrangement of the I2C bus and how logic levels are altered between high and low when the clock signal is high to transmit 1 and 0s.An I2C is a relatively low-speed bus that provides bidirectional communications between a controller and (in theory) over a hundred peripherals. A data and clock line are kept at a high voltage by pullup resistors The frequency of a clock line is controlled by the controller, while both the control and peripheral devices can affect the data line by connecting it to ground. A peripheral will take control of the data line only after it has been commanded to do so by the controller to avoid communication collisions. James Provost

I’ll skip over the next few hours of diagnostic failure, involving much gnashing of teeth and a dead end involving a quirk in how the SAMD chip at the heart of the Grand Central controller (and many others) has a hardware-accelerated I 2C interface that reportedly can’t go slower than a clock speed of 100 kilohertz. Eventually I hooked up the logic analyzer again, and scrolling up and down through the decoded pulses I finally noticed that the bus scan started not at address 0 or 1, but at 16. Now, when I’d picked an address for the Mega in my test code, I’d seen many descriptions of I2C in tutorials that said the possible range of addresses ran from 0 to 127. When I’d looked at what seemed like a pretty comprehensive description by Texas Instruments of how the I2C bus worked down to the electrical level, addresses were simply described as being 7-bit—that is, 0 to 127. So I’d picked 4, more or less at random.

But with the results of my logic scan in hand, I discovered that, oh, by the way, addresses 0 to 7 are actually unusable because they are reserved for various bus-management functions. So I went back to my original hardware setup, plugged in a nice two-digit address, and bingo! Everything worked just fine.

True, this headache was caused by my own lack of understanding of how I 2C works. The caveat that reserved addresses exist can be found in some tutorials, as well as more detailed documentation from folks like Texas Instruments. But in my defense, even in the best tutorials it’s usually pretty buried and easy to miss. (The vast majority of I2C instruction concerns the vastly more common situation where a grown-up has built the peripheral and hardwired it with a sensible address.) And even then, nothing would have told me that CircuitPython’s heartbeat scan would start at 16.

Oh well, time to press on with the upgrade. The rest should be pretty easy, though!

The Conversation (0)