Main

I²C on a 6502 Single Board Computer - The 65uino #i2c #6502

The breadboard turns into a PCB and you can learn how to do software bitbang I2C in 6502 assembly code using my new project - the 65uino! Update: Next video is already out, where make the display show text! https://youtu.be/x6xsTXY7OtI You can find the full project description on https://hackaday.io/project/190260-65uino If you haven't seen it yet, I suggest you check out the video about my "Single Breadboard Computer" https://youtu.be/s3t2QMukBRs You might also want to check out my other video on the 6532 RIOT https://youtu.be/Fo5bwoBWVhU Source code and hardware files: https://github.com/AndersBNielsen/65uino (Includes spoiler code for next video!) #hackadayprize #arduino Credits: Video by cottonbro studio: https://www.pexels.com/video/man-people-office-emotions-4101234/ Video by Francesco Ungaro: https://www.pexels.com/video/close-up-video-of-a-resting-cat-8813894/ Video by Tom Fisk: https://www.pexels.com/da-dk/video/fugleperspektiv-folk-beskidt-abstrakt-5424850/

Anders Nielsen

9 months ago

Hello everyone! This is the 6502 based Single Breadboard Computer that we built in a previous video of mine a few months ago and in this video we'll be taking a look at how we can get 6502 based computers like these to play along with I2C devices like screens, accelerometers, temperature sensors, and thousands of other gadgets. While playing with breadboards is a lot of fun, it's not exactly meant to be a permanent solution, and as you might have noticed from the thumbnail of this video it's gon
e through a bit of a transition. A printed circuit board is way more convenient, so I thought I might as well go ahead and make one for it. I'm a huge fan of making things using recycled parts and by making something that's useful in 2023 out of these old old chips, and sharing an easy to make design, it's a great way to keep the old chips out of the landfills and create innovative solutions without buying new chips for hobby projects. Basically we take something old out of the waste stream and
at the same time avoid buying and eventually scrapping something into new waste - a complete win/win. If you're following along at home on a breadboard, don't worry, you can still do that since we're still using the same integrated circuits. Of course the project is also 100% open source so can easily send the gerber files off to a board house and make your own! Links to the project in the description. The PCB I designed is pin compatible with the popular Arduino Uno that hobbyists and many-a-ha
cker will already have a strong familiarity with. Of course it's not in any way associated with the Arduino company, and I think that'll be clear if I call it a 65uino! Even though there's a big difference between a 6502 computer and an Arduino, this does mean that many - if not most - shields and addons for Arduino will also be compatible with our little computer here. Of course the main IC on the Arduino Uno, the ATMega328P, has quite a few more features(datasheet) than the 6507+6532 combinati
on we used on the breadboard, but even though it doesn't have analog pins, needs an external clock and the ATMega328p is generally better in every way, it's also over 30 years newer and hides everything away in a single package - and that makes it harder to visualize what's actually going on, and why would we want that. So on the plus side we still get to play with integrated circuits that were developed close to 50 years ago, and we can still use it to play with many of the modern gadgets. Best
of both worlds in my book! And that brings us to the topic of this video. Back in the 70's most additions to a 6502, 6800 or 8080 based system would interface straight to the 8 bit data bus, along with a few address pins, but as you can see the none of gadgets I have here seem to have more than a few pins. "But wait, that 16 by 2 LCD screen has way more pins than the other things!?" you might say. If you've seen Ben Eater's 6502 videos that's a reasonable thing to assume, since he goes into gre
at detail using it directly on the 8 bit bus with a couple of control lines, but instead of doing it that way, on the back of this one I have a little 50 cent module that takes the pin count all the way down to two signal lines like the rest. So how does that work? Well, back in 1982 Phillips (who btw spun off to NXP semiconductors in 2006) got pretty fed up with having to make huge expensive dip40 packages for even the simplest of features and decided it was time for a better way for chips to t
alk to chips without needing 8 data lines, read/write lines and tons of address decoding just to read a single register. What they came up with was the Inter Integrated Circuit - or I2C - Bus. A two-wire interface that does it all. The I2C Bus has tons of features and many have been added over the years but the main selling points are that it only needs two wires besides power and GND and unlike SPI we can put a lot of stuff on the same bus without a Chip Select line. It even lets multiple contr
ollers use the same bus, and even at different supply voltages. So, if we take a closer look at this little monochrome OLED screen we'll see it only has power, GND, a pin called SDA and a pin called SCL. In other words a data pin and a clock pin. Oh, it looks like I forgot to solder the PCB, so while I do that we can talk a bit more about how i2c achieves its magic. If you've seen the video where I make an adapter to convert the PS/2 keyboard protocol to the PC/XT protocol, you'll see the simila
rity that the I2C bus is also "open collector" and takes advantage of the fact that anything on the bus can pull both lines down to ground, without the risk of damaging anything, but the only way to make a line go high is to release it an let the pullup resistors on the lines do their job. This is usually also the speed limiting factor for i2c and if you run it at high speed you might have to add extra pullup resistors to let the lines go high faster. I'm not going to talk about the more complic
ated features like bus arbitration, clock stretching, 10 bit addresses, fast modes and such but I can promise that by the end of this video you will have a pretty good idea of how I2C works at the most basic level - and if you want to learn the slightly more complicated stuff, the specification is actually only 60 pages long. In a few moments we'll start writing some code, but first let's make a plan for what we're trying to accomplish, while I finish soldering the PCB. An I2C transaction consis
ts of a minimum of a [S]tart condition, including an address along with a Read/Write bit, an ACK or NACK from the target and a Sto[P] condition. A start condition is any time the data line (SDA) goes low while the clock line (SCL) is high and a stop condition is any time the data line goes high while the clock line is high. This has the consequence that the data is only allowed to change, during a transaction, while the clock line is low - otherwise we introduce false start or stop conditions. T
echnically the whole data byte is optional since we can send a stop condition after the first ACK bit. Why? Let's say we want to quickly scan all the addresses to see what's listening, so we don't want to waste time sending or receiving data we don't need. In that case we can just send a stop condition after we get the ACK or NACK. Since an address with no response would leave the ACK bit in the HIGH state, a NACK is a 1 and an ACK is a 0 bit or a LOW. Usually we would start with a Write transac
tion, indicating the register inside the IC we want to read from or write to. Some IC's will let us keep writing bytes after we send the register we want to write to, while others will require a repeated start condition without a stop condition first. It all comes down to the datasheet of the specific device we're trying to talk to, but even if we can't program one routine that fits everything, we can write a few routines that we should be able to combine into transactions that make sense for th
e specific IC we're trying to control. After assembly it looks like the 65uino has no problem running the exact same code as the Single Breadboard Computer, so it looks like it's ready to talk to some i2c devices. You'll notice the LED blinks a lot slower on the 65uino and that's simply because I moved the LED to the highest bit on the port - DRB pin 7. Even though I did move a few pins around for the PCB design to make logical sense for an Arduino-like pinout it still blinks the LED on PortB pi
n 7 a.k.a. Arduino pin D13, since the code increments the whole port on every clock tick. I put the user button on PortB pin 6 because it's easy to check with the BIT instruction. The pinout you see here on screen is basically where I routed the power signals and the I/O ports of the 6532 RIOT and since I had plenty of space leftover I decided to add a new header that's usually not found on an Arduino. We already have two other places on a typical pinout to access the I2C bus but I thought it wo
uld be nice to have a dedicated header for the cheap and common monochrome OLED screen with a builtin SSD1306 screen controller, so why don't we start out by writing some 6502 assembly code for a basic i2c transaction and then move on to write a little minimal library to control the screen. This is the code the way we left it after we finished getting the 6502 Single Breadboard Computer to blink an LED using the 6532 timer and change the frequency using a button press. A pretty good 6502 "Hello
world" example. This time we'll dig a bit deeper and I'll assume you know at least a tiny bit about 6502 assembly code syntax, so if you don't you might want to check out my previous video first - or if you're a complete beginner, maybe Ben Eater's 6502 videos. They're awesome! Since we can assume we want to use the same I2C address several times and probably have a few different devices connected at the same time, we'll need a RAM location for the address - and maybe a few other related things
like the byte we're sending and receiving, so let's assign them a spot in RAM but also them easy to relocate. With the amount of bit juggling we'll be doing here, our code will be a lot more readable if we assign the pins we'll be using to a constant, as well as their inverted bitmask for when we want to clear a bit. Of course I could also write macros to do the bit flips, but since macro syntax can vary between assemblers and also makes it hard for beginners to see where the line goes between w
hat's a 6502 instruction and what's a made up macro instruction, I will spell out all the instructions. The code will be a little less compact, but also a bit less complicated. The i2c address notation is one of those things that haven't been specifically spelled out in the specification manual, so on some devices you'll find the 7 bit address noted as the byte you actually send, including the read/write bit, while others note the address before it's been shifted left. Of course the latter is th
e most logical way to do it, since that shows us the literal value of the address bits themselves independent of other things - even if we have to shift it one bit to the left before we send the address byte. It does mean, the notation on the back of this SSD1306 module is incorrect for our code and the address we'll need to use is half of that, 0x3c. In practice though, both ways are easy to handle as long as we're consistent. If we just assume we have the R/W bit in the carry flag, we can comb
ine it with the address using a simple rotate left - shifting the 7 address bits to the top and the R/W bit into bit 0, all ready to send. Remember, to set a specific bit in either the output register[DRB] or the data direction register [DDRB] to a 1 without affecting the rest of the register, we OR[A] the register with the bitmask of the bit we want to set and if we want to clear a bit without affecting the rest of the register we AND the register with the inverted bitmask of the bit we want to
clear. Like I mentioned earlier i2c is an "open collector" protocol, which requires that all devices can set their outputs to high impedance - essentially disconnecting them from the lines, letting them get pulled high if nothing else is driving them low. This means we can't just set the line HIGH when we need it to go HIGH. Some modern microcontrollers have high impedance capable outputs, which means they can either drive the line high, low or essentially disconnect it from the bus. The Rockwe
ll 6532 we're using for our GPIOs does not. When the line is set to an output it either drives the line HIGH or pulls the line LOW. If we drive a line high, while something else is trying to pull it low - we're not only going to lose, we'll most likely lose the ability to ever pull that pin high again.. if not the whole chip. We're not completely out of luck though, since - as far as the bus is concerned - the line is high impedance and essentially disconnected, if we just set it to an input by
setting the pin number to a 0 in Data Direction Register B (DDRB). To get a low or a high voltage on the line, we either set the data direction register as an output to pull it low, or set it as an input to let it get pulled high. Since the data register keeps its value when switching from input to output, we can just leave it at 0 and just toggle the direction register bits. Since the clock pin is conveniently located on pin 0 of DRB we can even use the increment and decrement instructions to f
lip the clock pin in a single instructions, as long as we keep track of what state it's supposed to be in. For the SDA pin we still need to OR or AND the direction register to change states. Slightly confusing when setting it to a 1 means low and a 0 means HIGH, but I hope the reasoning came across, if not, feel free to ask in the comments. Now that we have our start condition we can move on to sending the first byte, containing the address and R/W bit. Since sending a byte is a function we'll a
lso be using separately, and we're doing exactly the same thing when sending address bytes and data bytes, we can start by making a new entry point and ensuring SDA is low and get ready to loop 8 times, once for each bit. For the loop we simple shift the next bit into the carry flag and if it's set we set the SDA pin as an input, letting it get pulled high, and if it's clear we skip that part and set the SDA pin to output, forcing it low. You might have noticed I incremented DDRB twice in a row
when coming from the start condition, which would break things, but I put it there since we get a slightly more even loop, so we don't make a too short clock spike. The fix is to skip the first clock change. After that we get ready to clock in the ACK bit, set the SDA line to input and clock in the ACK bit. If we get an ACK we set the carry bit, if not we leave it cleared and that's all we need to send an i2c byte. Eventually we also need to read bytes, so to do that we need a separate function.
It has exactly the same structure as sending a byte, except we rotate the bit in and for now we just send a NACK after receiving a byte. You might think a Not Acknowledge is weird, when we receive a byte just fine but in this case it just means "don't send any more data", since we're only reading a single byte at a time with this routine. The last thing we need for a complete transaction is the final stop condition, which is simply setting SCL high before setting SDA high. Oh, and it looks like
I mistyped a label. That should be it for our basic I2C routines but if we plugged in a module at this point, of course it wouldn't do anything, but also we might actually fry it and release the magic smoke. Like I mentioned earlier it has it's own header it plugs into rather nicely but at the moment I'm not going to try that with the power on. Since DRB is still initially set as output and cycles between 5V and GND, like the breadboard version, now is probably a good time to fix it so both SDA
and SCL are initialized as inputs before we plug in the screen with the power on. Of course we also have to write some registers in the display to make it do anything at all, but many I2C modules have 5V tolerant power pins and some protection but they still might not be able to handle 5V straight to the data and clock pins. If they're set as inputs we limit the current and improve the chance of survival, even if the chip in question technically isn't 5V tolerant. It's always more than a good i
dea to check the data sheet if you're not 100% sure. But if we look at the datasheet for the SSD1306 IC in our module you might be surprised to find that it isn't actually 5V tolerant like it's advertised on most markets. Thankfully that's not just exaggerated marketing but down to the fact that the modules usually come with this little XC6206 SOT-23 regulator that takes the input voltage down to a reasonable level - but since there's no such thing on the other lines, 5V with enough current on t
he data and clock lines might damage the IC if we're unlucky. After that little side quest we better get back to writing some code that'll actually make the display do something. First of all let's fix it so bit 0, 1 and 6 are initialized as inputs so we don't risk frying anything any longer and our I2C code will actually work. Just so we don't forget later let's change our button reading code to use DRB pin 6 - instead of the top bit of DRA. Since the bit instruction puts bit 6 in the overflow
flag we use the branch o[V]erflow instruction instead of branch plus. This also inverts the logic - when we press the button it blinks faster instead of slower. Since we're using the other bits of register B now we can't use the inc instruction to toggle the bit anymore so what we need to do is to read the LED bit and check if we're toggling on or off. Again we can read the LED on bit 7 with the bit instruction, since that'll put the bit in the sign flag. A thing to note is that the 65uino has t
he LED hooked up with the cathode connected to the pin since the 6532 RIOT can sink way more current than it can source. Now that our blinky code is fixed we can get back to what we came to do. Let's get that display running. To initialize the SSD1306 IC over I2C we have to tell it a bit about the module it's part of. It doesn't even know the resolution of the display itself. Kinda like a person who doesn't know anything about themselves when they wake up in the morning until someone tells them.
Don't we all know a movie where that happens? So, what we have to do is send a series of commands, one byte at a time. Since we don't want to clutter the top of our code too much let's get the display address ready and throw the initialization into a subroutine a bit out of the way. Fortunately we can do all of this in a single i2c transaction and to make it easy to change in the future we should put the configuration data in a table and load it from there. We could make the initialization a bi
t faster and get rid of the cmp #$ff instruction by keeping track of how many bytes we're sending and write the table in reverse but we don't have to and it's probably just a few milliseconds. I'm not going to explain the initialization commands, since they're basically the same as the example in the appnote that's included with the datasheet, except I do try to increase the refresh rate of the display a bit to reduce flicker on camera. With the initialization ready that should actually be enoug
h to make the screen do something, so let's grab the ROM and burn our code. The charge pump and voltages in the display take a bit longer to settle after a cold start, so we need to press the reset button to resend our initialization code before we'll see something.. We can fix that by putting in a delay before we initialize the screen. And voila! A screen full of random dots! If that's not what you expected, I don't blame you. Since the RAM in the display doesn't initialize to any particular va
lue by itself and we're not writing anything to it yet, the display simply shows the random data in the framebuffer RAM. If we disconnected power long enough and turned it on again we'd get a different pattern. The display is a bit dimmer and flickers on camera because the refresh rate doesn't match my camera. It doesn't do that in real life, so I'll try to adjust the camera to give you a better view... of the random dots. So at this point you should have a good basic understanding of how the i2
c protocol works and even how to use it with a 48 year old CPU design.. to display random dots. But of course displaying random dots isn't all we're trying to accomplish here, so in the next video we'll take a look at how to make a font so we can display some text on the display. Eventually I also want to show how we can use a software serial protocol on the 65uino to display text and load software from our computer without needing a programmer to burn the ROM every time. I hope you enjoyed the
video, if that's the case I'm sure you know what to do, and either way I'd love to hear what you think in a comment. Thanks for watching!

Comments