Welcome back. If you were surprised by the last
video where instead of our Intel CPU I was talking a lot about the Raspberry Pi Pico microcontroller,
then now it is time for me to explain my reasons. But, before I start, let me first thank PCBWay
for helping me create this video. PCBWay is a PCB prototyping manufacturer. Multiple PCBs are
their star products, ranging from standard PCB, Flex PCB, Advanced PCB, PCB assembly service and
others. They also offer one-stop solution service: apart from the inner PCBs, the outer enclosure
could be finished as well, such as 3D printing, CNC machining, injection moulding and others,
covering different kinds of materials. With PCBWay makers can realise their ideas into real projects.
Apart from that, their open-source community is pretty popular among the creators, especially
the "Shared Projects" section. Many PCBWayers have shared interesting electronic projects
there. If you are interested in any of them, you can get the boards there directly. Check
them out at pcbway.com. Thank you PCBWay for making this PCB for us. And... before we
start putting chips into the empty sockets, let me tell you a few words about my future plans.
I decided to separate this series of building this computer using 80C188 on the PCB into a separate
playlist and I'm gonna just carry on building and programming this computer, which doesn't mean that
I forgot about the old computer - the 80C88 - here on the breadboard. It's still here waiting for
more chips to be added to it, so in the original series I will carry on with the breadboard
computer using 80C88 and I will add more peripherals, more chips and explain how they work
and how to connect them to the computer. So OK, now let's get back to the PCB. If you saw my
last video where I was talking about the PIO block in Raspberry Pi Pico, it wouldn't come as
a surprise if I told you that that's exactly what we're gonna put into this socket here. So let
me do this right now. That's Raspberry Pi Pico and it's going here. There are two more sockets
here, which I will populate in a second. Just need to mention what the Pico is doing here. I decided
to use it in the computer because the PIO block is great for quickly reacting to what's happening
on the GPIO pins and sending data via those pins, so I thought it would be perfect for using this
chip as a peripheral in our computer talking to the main CPU using the data bus and the usual
signals: RD, WR and so on, but the problem is that GPIO pins in Pico use 3.3V logic. They
are supplied with a 3.3V, not 5V like the whole rest of the system and even worse the
GPIOs do not tolerate 5V on their inputs. So, if I just put straight data bus to the GPIO
pins, they could be damaged - they are not tolerant of 5V levels and that's exactly what
these two chips will do. These are usual 74245s bi-directional transceivers, which can just
transfer 8 bits of data both ways and there is a pin switching the direction of the data,
but they are a special version. It's another, newer family of the 7400s called "LVC" and this
chips work in 3.3V logic level, but they are tolerant of 5V. So you can put 5V to their inputs
and they will work correctly and recognise the logic levels and at their outputs you will see
the usual, well usual, the newer 3.3 level, which is acceptable for our Pico. So one of the chips
converts the data bus - 8 bits of the data bus - and the chip here, which is identical,
is used only really in one direction towards the Pico and it just converts a few signals like
Chip Select, WR and RD and ALE, so... Address Latch Enable. So let me explain why do I need the
ALE signal in the Pico. I'm gonna use the Pico as a few different peripherals really, so I need
some address pins. And if I wanted to use the separate GPIO pins for addresses and 8 additional
pins for data, I would run quickly out of the GPIOs available. The standard Pico has 26 GPIOs
available. I want to use some of them for the peripherals, which I'll talk through in a minute.
I just simply couldn't do do this - I couldn't use separate GPIO pins for address lines and 8
GPIO pins for the data bus and then three pins for the CS, WR and RD and also I need to talk to
that CPU the other way so I need 2 more pins for the READY signal, because obviously, sometimes
we can't just serve the data instantly on the data bus without Wait States. So I need the
READY signal and the Interrupt Request signal. I will need to - like any other device really,
like the timer or any other device - sometimes I need to request an interrupt in the CPU
to let it know, that it needs to read some data coming in from our peripheral. So I would
run out of the GPIOs and therefore I thought: well, we already have multiplexed data and address
signals in the data bus from the CPU, so even if I've got those 573s here to latch the address and
using it in addressing all the other chips here, I decided to use the multiplexed data and
latch the address again inside the Pico. And that's why I need the ALE signal. So, in the
PIO programme which handles all the transmission between the CPU and our Pico, which I will
discuss in a minute, you'll see how it's don.e OK, so we know why we need to use the LVCs -
they convert the 5V logic from the CPU to 3.3V logic in the Pico. But what about the data bus,
when it's being read from the Pico and what about the READY and Interrupt Request signals, which are
at 3.3V level and they are directly connected to the CPU and they need to drive the CPUs inputs?
So, if you look at the datasheet of the 80C188, you will see here in the 2nd row the V-ih symbol
which is a voltage allowed for the HIGH level on any input and as you can see, the minimum voltage
is 2V, which is far less than 3.3, so I decided to drive the inputs of the CPU straight from the
Pico, or - in case of the data bus - straight from the LVC, which produces, of course, 3.3V
logic levels. OK, so what the Pico will be doing here in our computer? You can see some pin headers
here and there. So, apart from obvious like serial port, which will be implemented in the Pico and
then the CPU could talk to the Pico using the I/O ports. There is also a pin header for an SD card
module - something like that - so we can slide it in and you can use this SD card as a hard drive.
That's my plan. Also, the other pin header is to be used with the RTC module, which works using I2C
bus, but I decided later on to not using this and that's because I just realised that the original
RTC chip - the DS12885 - is still easily available to buy. Also the 1287 version. Inside this big
case it contains the battery, which would keep the memory and the RTC feature working even if
it's completely off the device. As you can see, this one is produced in the 1988, so I'm sure the
battery is now completely dead after 35 years. But, I will use this RTC in the new version
of this board instead of the I2C here, but just for tinkering, there is still
I2C available in this pin header. So, most importantly, hard drive using SD card. I'm gonna
implement a hard drive controller in this Pico, a serial port - or UART. Also, because the Pico
may work as USB host, so you can connect devices to the Pico and it can talk to them, I decided
to use it as the keyboard controller as well. But more about these I will talk in the future
videos in this playlist, in this project and I'm already designing a new version of this PCB,
because I unfortunately made a few mistakes on it. The one which I already told you about the
Timer0In input, which was grounded - the pin 20 - I told you about this in the previous video,
but there are more. For example: I'm using the S2 signal from the CPU to decode the Chip Select
for RAM and I used it straight away from the CPU, but it's... it needs to be latched like the
address lines, because it's being removed too early and may cause problems reading or writing
to memory. So I had to latch it. Here's the fix: I just took the S2 and latched it using unused bits
of this 573. More fixes are visible on the back side of this PCB. Unfortunately, I found that I
made a mistake in connecting the Pico and so I had to just solder one "OR" gate - on the back of this
PCB. I added some decoupling capacitors as well. So there are few things which need to be fixed
and I decided to change a few things as well, but that's all is going to be done in the second
version of this PCB. There are some breadboards on the top currently they are holding only our
text LCD, but I'm planning to add something to this computer in one of the future videos. We'll
talk about it later. Here is the current schematic of this PCB including all the fixes. I've already
told you roughly how our Pico is gonna communicate with the main CPU, but now let's look at the
schematics. Here are the two LVC245s. One of them, as I mentioned, is the transceiver for our data
bus - well, in fact it's not only data bus - it's the multiplexed address and data 8-bit bus, that
is gonna go to ports 8-15 on the Pico. The PCS0 is the chip select for the I/O straight from the CPU
and it enables the transceiver, so it talks on the data bus only when the Pico is chosen by the CPU.
It's a chip select like any other: if it's HIGH - if it's inactive - then the transceiver is in high
impedance state, so it doesn't talk: doesn't read, doesn't write any data on the data bus. The
other LVC: I'm using only 4 bits out of 8, so as you can see, the rest is just grounded
and those are the control signals, so: ALE, the Address Latch Enable to demultiplex the address
from the data inside the Pico. Chip Select again, because we still need to be monitoring it in our
PIO block and you'll see the programme doing that in a second, and the WR and RD signals - they are
all going to the GPIO pins number 2, 3 and then 6 and 7. I didn't start from 0 and 1, because
these - as you can see - are reserved for UART, or serial port and the pins 4 and 5 are reserved
for I2C controller - you've seen that 4-pin header on the PCB. That's for future expansion. We
can add some I2C devices, some sensors to our computer. I just decided to save it for later.
Pin 6 and 7, as I mentioned: WR , RD, then 8 bits of the data bus or multiplexed address and
data bus. Then some other pins of the GPIOs on the Pico are reserved for SPI controller, which
will talk to our micro SD card and the other 2 are another I2C talking to the RTC - that's what's
on this schematic, but as I mentioned, I decided to use the old original DS12885 RTC directly on
the CPU's data bus. I will talk about it in the other series. We will pop it in our 80C88 data bus
on the breadboard the other time. OK, the other LVC - the one which translates the logic levels of
the control signals - is kind of hardwired to talk from the left hand side to the right hand side
only because these are input signals for our Pico, always. The data bus though needs to be - of
course - switched between reading and writing and I added this "OR" gate - which you saw on the back
of the PCB - because I forgot about it. Obviously, the data bus needs to be read by the Pico in
two cases: whether the CPU wants to write to it or whether the ALE signal is active and the Pico
needs to read the address... part of the address: the 8 bits of the address put on the multiplexed
lines. So that's what this chip does. The output pins from our Pico to the main CPU: pin 20
on the Pico - READY - and 21 - the Interrupt Request - are output signals, which go
straight to our main CPU directly. OK, let's finally have a look at our PIO programme for
our MultiIO peripheral. I called it "bus_ctrl" and if you have seen my previous video about the PIO
you already can tell that in this programme I'm using optional site-set to change the state of one
of the GPIOs. If you have a look here you can tell that the site-set bit I'm setting is the READY
signal to the main CPU - the 80188. I also defined other GPIO pins I use in this programme and I'm
doing this in the PIO section, not the C section for a simple reason. Because those definitions I
can use both in the PIO, like here for example, as well as in the C part of the programme.
The definitions, the macros are prefixed with the name of the programme and the underscore
and are defined for the C code as well, so it's a good way to keep everything tidy and
you can easily change those definitions in one place which is the PIO programme here. OK,
these are comments helping me to remember the pin mappings. We already looked
at the side-set - that's for READY signal. The bits which are set to OUT,
sometimes - not always, is the data bus the 8 bits of the data bus and the rest of the pins we
read, so they need to be mapped as the input pins. So let me quickly scroll to the initialisation
function - C function, which looks very similarly to the functions which I showed you in
the examples from the previous video, but we have more pin mappings, which is
quite interesting to look at, because in the previous video I used only site-set
and OUT, because we only used really 1 bit, 1GPIO pin to drive the LED modules, while in this
programme we're using 14 GPIO pins? I think it's 14 in total. We've got other mappings as well: the
IN mapping, which really tells the PIO which pin is the starting pin to read the data from and
as you may notice in contrast to the OUT pins, when you need to define how many pins will
be outputs - in our case it's only data bus, so it's 8 pins - the IN pins function doesn't have
this parameter - the third parameter - telling how many bits and that's because you can read all of
the pins in one go. The registers and the Input Shift Register is 32-bit long, so it can read
all of the pins in one go. The function... only thing the function does is to define which pin
is the least significant bit to read and if we, for example, define it as 26, for example, and
read more than available pins on the RP2040 the numbers will wrap up after 31. So it's... it will
go for example: from 26 next next bit would be 27, 28, 29, 30, 31 and then 0, 1, 2 and so on. So the
numbering wraps up around the 5-bit value: after 31 goes back to 0. In our case the lowest bit
we're gonna use as an input is the WR pin, which you saw the mapping and the all the connections on
the schematic just a minute ago. So you may ask: "oh, the ALE and CS pins have lower numbers, so
why don't you define the lower number for the ALE: number 2?" And that will become obvious in a
second. The other interesting point I'll try to make in this programme is that you may use Shift
Registers in different ways, really. Usually you use them to transfer the data between the CPU -
the main CPU - and the GPIO pins, but I also used Output Shift Register in a different way, which
will get to in a second. OK, let's go back to the C initialisation code, because there is an other
interesting mapping which is not used that often, which is the "jump pin". You can designate one
single pin to be checked and then conditionally jumped, based on its state. So... and that's
how we really read the ALE pin - not via the input functions, but just simply checking and
conditionally jumping if it's active or not. As you can see, in this case, we jump if the pin is
active, which means if it has the state of 1. OK, so let's analyse the programme line by line. This
is also an interesting line the "WAIT 0 gpio" and the GPIO number. What it does is to wait until the
specified pin goes to the state which is defined here, so effectively, at the beginning of our
programme, the first thing which we want to do is to wait until the CS goes LOW - goes active - so
we're not doing anything really in this programme until our peripheral device is selected by the
active Chip Select. Until then the PIO programme stays here and waits doing effectively nothing,
waiting... just waiting for the Chip Select to go LOW. If it does, which is the first kind of stage
of the bus cycle which involves our Pico as the peripheral, it goes the line below which checks
and conditionally jump if the ALE is active, it jumps down to the label called "ale_active" and
if not it just passes to the next instruction, but we're gonna get to that in a second. First let's
analyse what happens in the programme when the ALE is active, which means that the CPU is serving us
the current address on the address and data bus. Effectively, we're reading the 8 bits,
which are multiplexed with the data bus, Which is more than enough because that really,
if you remember, the 80188 allows for each CS to be mapped to 128 ports, I/O ports, which is
7 bits. We have the 8 bits, but we're kind of gonna ignore the eighth bit of the address anyway.
But what happens here? This instruction: "MOV y, pins" does two things, as you can see, it also
side-set the READY signal. If it does set it to "1" it means that we are signalling "we're not
ready yet". We'll get the READY signal back to "0" when we're ready reading or writing the
data. It's a signal for the 80188 to wait for us until we're ready with the whole bus cycle.
"MOV y, pins" reads all of the pins as inputs starting with our defined pin, which is the WR
- the lowest pin number 6 to the Y register and then we are reading the Y register into the Input
Shift Register - specifically 10 bits of that. I just need to explain two things now: why 10 bits?
That's because we read from the WR. We need to read these 2 bits: WR, RD and then 8 bits of the
data bus which come straight after these pins. So it's in total 8 + these 2: 10 bits. So that's why
we read 10 bits from the Y register into the ISR: Input Shift Register. Now the question you may
ask is: "why not doing this in one step ? Why do we need the Y register on the way?" Here is a
similar line which does the same thing in one go, which reads pins straight into the Input
Shift Register. And that will become obvious in a second, but we're using the Y register as a
temporary marker that the ALE was active and we've got the address part from the bus already read.
So the only thing it does... the PIO programme does in the "ale_active" is to read the 10 bits
from our inputs: WR, RD and the 8 bits of data, storing in the Y register and Input Shift
Register and then jump up back to the "idle" loop. And obviously, because the ALE is active for
around one whole clock of the 188, this programme may then work in a loop here: between the "idle"
and "ale_active", but that's fine, because - so it may read those bits many, multiple times -
but that's fine, because the only thing we care about is that the 10 least significant bits
of the Input Shift Register contain our input signals... most current input signals - let's
say... So this goes in a loop until the ALE becomes not active and therefore the jump wouldn't
be taken and the next step is to, as you can see, check WR. So this part of the programme starts
looking into our input bits knowing that we already passed the first stage of reading the
address from the address and data multiplexed lines. So now we're looking whether the CPU, 80188
wants to read data from our peripheral or write into it. First line explains why we had to use
the Y register. The only purpose of it is just to mark that we've already passed the ALE signal
stage. "How?" - you may ask. It checks whether the Y is 0 - "!y" means: (if it's) "jump if it's zero
- back to the idle". The Y register would contain anything but 0 only when the ALE signal was
active and we read the input pins. "Why?" - you may ask - "We could address the I/O port of 0 and
then Y would be 0?". Well, no, because we read not only 8 bits of data in here, but we read 10 bits
together with the WR and RD. When the CPU reads then the RD is active LOW - when it writes, then
the WR is active LOW, but they can't be LOW at the same time. So one of those bits must be "1"
at any point in time. That's why we read into Y all the pins and we know for a fact that if we've
gone through this part of the programme then the Y register would not contain 0 for sure. And that
allows us to decide whether to carry on with the programme or just go back to the "idle" state.
All right. So, first - as the label says - check the WR state. How we do that... now this time we
read our pins into the Output Shift Register and since the lowest number of the pin is WR, the
first bit in Output Shift Register - the least significant bit - would be the WR. We are shifting
out from the Output Shift Register this one bit to the X register. Then we're checking the state
of the X and conditionally jump to "check_rd" if it's not active, because the "JMP x--" means:
"check if the X is not 0 and jump only if it's not 0 and post-decrement". You may ask: "why do I need
to post-decrement the X register?" - Well, I don't have to, but that's the limited encoding of the
PIO instruction gets in the way. You can check if the X or Y is 0 and then conditionally jump, like
we did here, but for the check... the opposite check, where... "jump if the register is not 0",
the only encoding is with the post-decrement, so we don't care about post-decrement - it's
just the way the PIO instructions are encoded. It effectively means "jump if X is not 0". If the
WR is not active that means we need to check RD. Maybe the CPU wants to read from us? And the label
is here. The next bit to shift out is the RD, so we're shifting out to the X the register our RD
bit and doing the same check again so "if it's not active ,whoops - something's wrong - we're going
back to the idle, because the CPU neither reads or writes". But if it is active - the RD is active -
it just falls down here and since we already have the address to be read from in our Input Shift
Register, then we simply push it to the FIFO to be read by the the main CPU - the ARM core on
our RP2040. If I'm saying the CPU in the context of PIO programmes I usually mean the ARM core
inside the RP2040 - not our, kind of, main CPU in our computer: the Intel 80188, but I'll try to
explicitly tell which CPU I'm talking about. So, we're pushing our address read previously from the
input pins here to the FIFO and then we raise an interrupt - again, interrupt to the ARM core
within our Pico. That's interrupt number 1 - we are also using the other interrupt number 0 -
that's for writing. We're gonna get to that in a second, and straight after raising this interrupt,
we are changing the directions on the data bus, because - by default - we're reading from
the perspective of the Pico - we are reading: waiting and reading the address
bus and then the data bus, if the CPU - the 80188 CPU - wants to write
to us, but if it wants to read from us, that's the only case when we need
to change the direction of our GPIO pins as outputs to serve the data to the
80188 CPU. How do we do that? Well you could do this straight away by "MOV pindirs",
so instead of doing this in 2 steps we could do "MOV pindirs" and then immediate value,
but that value could be only maximum 5 bits. Unfortunately, we need to
change the direction of 8 bits: the whole data bus, so we need to do
this via Output Shift Register and that's the case where we use Output Shift Register in
a different way - not to transfer any data via GPIOs to or from the processor, but only to set
"pindirs" - pin direction - as outputs. And that's because of the feature of the "!null", which, as
you may remember from the previous video, sets all the bits to "1s", so effectively this function...
this instruction sets all 32 bits of the Output Shift Register to "1". Well we need only 8 of them
but we can just shift 8 of them to the "pindirs", which effectively changes the direction of the
data bus to outputs. Why does it not change the WR and RD? Well, if you remember... here we've got
this comment and you can also see it here we are setting the OUT pins with the lowest number... 8
pins... with the lowest number of the data bus, which is pin number 8 - not 6 - like in other
cases. This changes the direction of our data bus to outputs and gets it ready for the ARM core
and our PIO programme to serve the data on the data bus to the 80188. In this moment while we...
after we have raised the interrupt, we hope that ARM core will do its work in the interrupt handler
and at some point it will push through the FIFO the data to be served to the 80188 via the
data bus. And we're waiting for the data from the FIFO here in the "PULL block". If
you remember, again from the previous video, a "PULL block" waits - because it's a
blocking call - it waits until we've got some data from the ARM core CPU by our FIFO sent to our
PIO programme. If the interrupt handler sends this data, we can simply set the data bus pins to 8-bit
received from the ARM core and, as you can see, in the same instruction, we side-set the READY
signal for the main CPU the 80188, letting it know that the data is ready to be read and it can
finish its bus cycle. Then we simply wait in this loop until the 80188 deactivates the RD signal. So
in this simple loop we again read the input pins to the Output Shift Register. We need to throw the
WR bit which is in front of the Shift Register to "null" - so it means just throw it away nowhere
and the next bit is RD, so we check its state and if it's not zero... Sorry - if it's zero -
"!x" means "equal to 0 - jump if it is 0". So, if it's still active - loop around and then we
fall down to the "reset_bus" which sets the Y register to 0 - this is important exactly for
recognising the next bus cycle and whether the ALE was active or not - like before - and
we're just for sure side-setting here to 0, because the "reset_bus" is jumped from the
WR part of the code, which you will see in a second. So it signals the CPU, the 80188 that we
already after writing to us. And also - again - in the similar way to setting the direction of the
data bus to outputs, here we're doing the same, but now this time we're populating the whole
Output Shift Register with "0s" - no "!null" like here - "null" means set the
whole register to 0 and then "OUT" changes the pin directions of the data bus back
to the inputs and it wraps around to the "idle" state. So the only part of the code we haven't
really discussed yet is the WR part of the code. We've already mentioned this, so if the WR is
active it falls under this "JMP" and this "NOP" and the wait... delay I used it just to make sure
that the data bus is settled after initiating the bus cycle and the WR goes active and then again
we read 10 bits from the pins. 10 - because, again, we read the WR, RD and then 8 bits
of the data bus. So we read 10 pins of them into the Input Shift Register remembering that
this part of the code already stored the address from the CPU bus, so effectively we will have
in our Input Shift Register at this point 20 bits of data: the 10 bits - the address the WR
and RD which we're going to ignore in software and then the data the CPU, the 80188 wants to
write to us together with, again, WR and RD, but we're gonna ignore these as well. And what
we do here is: we push the Input Shift Register, so the kind of address and the value being
written to us via the FIFO to the ARM core and then we raise the interrupt. this time just
to easily tell between reading and writing we're using the interrupt number 0 and we wait until
the interrupt status bit is 0 and that happens when the ARM core acknowledges the interrupt
in software - we'll see the code in a second. So the code waits until the ARM core receives the
interrupt and the data being pushed here. And then we just go down to the "reset_bus" label, which
we've already seen. I just realised that I could use the site-set and signal the readiness... the
end of the bus cycle for the 80188 here... "side 0", but since I've tested this programme in the
state which I'm showing you... and showed you originally, I'm not gonna do this change now. The
only comment I need to make is that we cannot do the "side 0" here. You may say: "oh, OK, we raised
the interrupt and then we wait so we can 'side 0' marking that we we've done our thing on our side
and we're ready". Well, the thing is - and that's quite important information. Despite the fact
that the "wait" and the PIO programme waits on this instruction until the interrupt line
goes to 0, the side-set - and it's a comment on any instruction in the PIO which may wait for
anything - the site-set is always executed at the beginning, so even if the "wait" waits
here, the side-set would have been executed at the beginning, when the PIO goes into executing
this wait instruction. So that would be premature and we shouldn't do this here, but we might do
this here in the "JMP reset_bus" - it doesn't really matter. We're doing this here, I tested
this programme and it looks like it works. OK, let's finally see the C code of this Pico
programme and I will start from initialising the PIO. That is the function which is called at
the very beginning and you already may recognise some of the calls, like these two - this just
loads the programme and initialise the PIO to use the programme. Instead of using the definitions
from the SDK straight into here, like "pio0", for example and "state machine" could be 0, because
that's the only state machine we use on this PIO, I decided to separate all the definitions like
that into a separate file... header file here. The other lines, which we haven't seen in the
previous video is to initialise the interrupt requests. That line links the PIO interrupt line
number 0 and then we're just setting the function, which will handle the interrupt, the priority and
enables the bit for the interrupt to be active at all on the CPU side. That's the PIO side -
that's the CPU side. And that happens for the interrupt line number 0, which is for writing,
hence the function name: "bus_write_handler" and exactly similar thing for reading and the
interrupt number 1 - the PIO interrupt number 1 for reading... And that just runs... starts the
state machine running our programme. Let's go now to the handlers: both write and read. These
just effectively format the data being sent from the PIO programme, so that's the function which
reads from the FIFO and really... I use the loop, but effectively, we should be getting only one
data, one 32-bit data from the FIFO. It's just, again, safe coding - just in case
something got lost on the way. So, we read the data and then we just extract the bits
from the data, because in case of writing - remember? - we've got the address in this 32-bit
data as well as the data from the 80188 sent via the data bus. So we are extracting those
by just shifting out and, as you can see, this limits the address to 7 bits - to "7F"
in hex, because we ignore the eighth bit of the address line. And then the value and then we
call our "cpu_bus_write_io" function which just gets the kind of extracted data ready to be acted
on. We'll see this function in a minute and then we clear the flag - the interrupt flag - which
means that we are done in the handler. We've noticed the interrupt in first place, we've done
our job and you can go on PIO and do its thing. That's what the "WAIT 0 irq 0" does here. So once
we've called this "pio_interrupt_clear" function this "WAIT" will just pass on to the next
instruction. The "bus_read_handler" is very similar, but in this case we have only address
to be read from in the data sent from the PIO and we then call a twin function to the
"cpu_bus_write_io", the "cpu_bus_read_io" with the extracted address and its result... because,
well CPU expects - the 80188 - expects some data from us - it's reading from us - so this function
returns the data to be sent back to the 80188 during the bus cycle and the data returned from
this function we put to the FIFO, which then is pulled here from the FIFO and sent to the
data bus of the 80188. And then we clear the interrupt flag as well. Oh, yeah... One
more thing the "time_critical_func" is quite... critical. It's a definition to tell
the compiler to store the function in the RAM, so it will make the initialisation code to copy
the function with this definition from the flash to the RAM. That's because the Pico module
contains a serial flash - the RP2040 doesn't contain any flash for storing programmes
inside itself - it uses the serial flash, which takes some time to read from. So
we just want to make sure that these critical functions are being already
stored in the RAM and executed from RAM for the obvious reasons, because
we want to do our job as quickly as possible without any delays waiting for
the flash. So that's the "cpu_write_io" function which is called from the handler...
sorry... here... and the "cpu_bus_read_io", which we'll start from is here. I - for the
test we're gonna make in a second - I defined two separate I/O "devices". They are not very
useful really they just test our programme and if the whole idea works at all. So, I defined two
16-bits port I/Os. One starts with the address of "00" and "01" and that's because when you
read or write 16-bit I/O ports in the 80188, let's say: you address the port number X - the
lowest, least significant byte is read or written from the port number X, but the most significant
byte is read or written from the port number X+1. So, if you use 16-bit "IN" or "OUT", it will
effectively read or write two I/O ports: the X and X+1. So if we address port number 0
and use 16-bit transfer, it will effectively read or write - in our case we're reading
here - port 0 and then 1. And that is, as you might guess from the names of the variables,
it generates Fibonacci sequence, which we, remember, hopefully... from my first or second?
Second video on this channel, where we generated Fibonacci sequence in the top 16 bytes of the
address space of the 8088, so I decided to repeat this idea in this test. So if we address
port number 0 and you use 16-bit transfer, we can get - on each read from that port - next Fibonacci
sequence number. And the other kind of I/O port at address of 4 is even more boring - it just returns
constant data, so the lowest byte is "45" and the highest byte is "23". So theoretically, if we
run a programme on 80188 and we do "IN AX, 4", we should get number - 16-bit number - in hex:
"2345" - if everything works - we'll see it in a second. And all the other ports, which
we didn't define here, just return "FF", because we need to return something. But I...
just to test writing to the port - I also defined that if you write to the port of address of
0, it will reset the Fibonacci generator, because without it every time we read from the
port, it would generate consecutive numbers of the Fibonacci sequence, but at some point we may
decide to read again from the first number: "2", so if you want to do that, you just need
to "output" any value to address of 0, because we ignore the value being written
to this. It's just to test writing to the peripheral. And that's pretty much it in this
C code. So just to wrap it up before the test: we have two I/O ports... 16-bit I/O ports.
At offset 4 we just return constant data, writing doesn't do anything to this port and
the port at index 0, which reads consecutive numbers from the Fibonacci sequence. If you
want to reset the generator you just write to the port any value and that's it. Now, let's try
to execute everything and check if it works. OK, let's finally test the whole idea and the PIO
programme in our Pico, using the step-by-step mode. Before we actually turn on the the computer
let me just explain those wires to the... to the text LCD. I decided to not using the direct
connection, because it was too slow. The text LCD is too slow to talk to the CPU, so I decided to
use the text LCD only via the 8255 GPIO pins and I also updated the code, the "driver" for our LCD.
You can find it on my website on GitHub as always. I decided to use only 4 bits, because the text
LCDs can be driven using all 8 bits or 4 bits, which obviously requires twice as many
transactions, kind of, via the pins, but I still decided to do this. The code
is configurable, so you can switch it to 4 or 8 bits and you can also switch and use
different ports A, B or C and different pins to drive all the signals to the text LCD. So
that's updated - you can check it on my GitHub and I will be using for this test the new code
here. OK, I think it's time to test our idea of the Pico as an I/O device. So, as you saw in
the code, I defined a few ports within the PIO and from the listing you can see that I'm stopping
our CPU using the "INT3", which is the debug interrupt, to switch it into the step-by-step
mode. So after the whole initialisation it'll stop at address... I guess... "6C". OK,
let's run it. Oh! Look at that... "006C"... so, as I said, we are now
paused before "XOR AX, AX", which zeros the AX. So let's step through this.
Yeah... the AX now contains 0 and the whole idea is to check the... if the reading from an I/O in
Pico works at all, so I defined, as you remember, two ports with the offset 4 and offset
5: one returns... I think... "45" hex, the other one returns "23" hex, so we are reading
now to the AL value from the port number 4. If we step through, we can see that the
"45" is returned from the Pico. Again, this instruction just read a value using I/O
address space from the Pico. The Pico returned value "45". If we step through to read the, kind
of, older byte you - may say - if we treat these as 16-bit value it would be "23", but still
in AL. Now the next test from the listing is to read the whole value as a 16-bit integer.
So first, we zero out the AX again and the next instruction, as you can see, is not "IN AL"
- it's "IN AX", so it's a 16-bit read. It will read port 4 and 5 - it will return the whole
16-bit value into the AX, hopefully. Let's read that... And yes! We have the whole value: "2345"
in the AX, currently. Next test is to use the port at the offset 0, which is the test I kind of
borrowed from the one of the first videos on this channel, which was to return consequent values
from the Fibonacci sequence. So we zero out AX, but before we do that - sorry - the first
instruction loads 20 or "14" hex to the CX, which is the counter register in the loop. We've
got "14", so we're gonna see 20 Fibonacci numbers in the AX. All of them are 16-bit wide,
so we improved from the one of the first videos - from 8-bit to 16 bit. Let's see the
first value in the AX... and there you go: the first value is 2. Then we decrement CX and if
it's not 0, we go back in our loop to offset "79" and then we read another value
of the Fibonacci sequence which is 3 and that's how the loop works 20
times. We were reading only in our test, so I also defined, that if you
write anything to the same port 0, it will reset the Fibonacci generator. So after
you've written anything to the I/O port at offset 0, it would reset and the subsequent reads will
read Fibonacci sequence from the beginning - from the number 2. So, we're in the last part of our
loop. So we just zeroed out the AX and in the next step, at offset "80", we're going to write it
to the same I/O port, to our Pico to let it know, that we want to reset the Fibonacci sequence. No,
it's not very useful - I know - it's just a test of whether the reading and writing, both work when
we talk to our Pico. So, we are now writing the Pico should now see: "oh, they wrote something
to my port so I will now reset the sequence". And now we're getting into the next, very similar,
loop with just 10 iterations this time, so "0A" hex and we read the first - again - Fibonacci
number which is 2, then 3, then 5 and so on . So that was just a test that the whole hardware
and our PIO programme inside the Pico works and can effectively talk to our 80C188 like any
other peripheral. In the next video in this series we will start maybe using - MAYBE -
using SD card - we'll see - as the hard drive. We'll make some tests as well, so I hope to
see you there. Take care and see you next time.