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