TinyELF

Chapter 7 - Interrupts and DMA

Now that you understand all of the 1802 instructions (go back and review them if you have any doubts), you are ready to use them in a real program. You are going to build this program in pieces, and so learn about some of the particular hardware features of the ELF II one step at a time. In the process you should come to understand how to program for interrupts and DMA, and how to watch for timing problems. Incidentally, you will also learn a little about writing diagnostics for your computer (more about what those are later). This chapter is not as detailed about the individual programs as Chapters 3-6, so be prepared to do more of your own thinking.

First let's talk about DMA, or Direct Memory Access. All of the I/O instructions transfer data into or out of the computer at the time the program commands it; that is, when the I/O instruction is executed. DMA runs independently of the program (to a certain extent), and the data is transferred into or out of the computer when the outside world (as represented by the other circuits in the computer) decide to do it, and the program in the computer may not even be aware it is happening. This is because the program counter, accumulator, and the register pointed to by X are not involved in the DMA transfer. All that happens is that the hardware "steals a cycle" from the CPU to move a byte into or out of memory directly (thus the name "Direct Memory Access"). The designers of the 1802 made this particularly easy by setting up R0 to point to the memory location that the DMA will access. To an extent this is not true DMA but rather a "Data Channel"; but DMA is easier to pronounce and spell, so that's what we will call it.

Sequence memory, then enter Program 7.1:

        ..  PROGRAM 7.1 -- TEST DMA
        ..
0000 90   REST: GHI 0     .. INITIALIZE Rl, R2, R3
0001 B1         PHI 1
0002 B2         PHI 2
0003 B3         PHI 3
0004 F81B       LDI INTS  .. R1 = INTERRUPT PC
0006 A1         PLO 1
0007 F8FF       LDI #FF   .. R2 = STACK (TEMP DATA)
0009 A2         PLO 2
000A F80F       LDI MAIN  .. R3 = MAIN PC
000C A3         PLO 3
000D 71         DIS       .. X=0, R0=000E
000E 23         #23       .. SET X=2, P=3
000F 69   MAIN: INP 1     .. TURN ON TV
0010 90   LOOP: GHI 0     .. DISPLAY R0
0011 22         DEC 2
0012 52         STR 2     .. FROM MEM AT R2
0013 64         OUT 4     .. X=2!
0014 3010       BR  LOOP  .. DO NOTHING

To run this program, be sure your TV set is correctly connected to the ELF II video output. With the computer reset you should see a blank screen. As soon as you switch it into Run mode, the TV should start flickering at a rate of about once per second. If you look carefully, you should see a regular pattern flash by each time. What is happening is that the 1861 Video display chip in your computer is asking for bytes out of memory, and whatever R0 points to is transferred out and displayed on your TV set. R0 is incremented each time, so the video display slowly walks through memory. This program also outputs the high byte of R0 on the hex display, so you see it incrementing at the same rate the TV flickers.

If you change the INP 1 instruction in location 000F to a NOP and then run the program, the TV display will remain off and R0 will stay at 000F. The hex display shows 00 (the high byte). To see the low byte, change the GHI 0 in 0010 to GLO 0. Now change the NOP back to the INP 1 that was originally in 000F. What do you think you will see when you run the program? Obviously the TV should be no different. If you output all the hex digits in rapid succession, the hex display should look like "88", right? Run it.

Why do you see "8A"? Notice that the right vertical bars of the "A" are dimmer than the others. Flip it into Wait a few times. Notice that the right digit is most often "F" or "7". You may see other values like 80, 22, etc. but you can tell that these are glitches in the display caused by switching into Wait, because they have the lower bar on in the right digit, whereas that bar is always off when the program is running. It happens that these are the opcodes of your program at the point you stopped it. If you display "F" and "7" in rapid succession, it looks like an "A". If the display is more often "F", then that part will be brighter. So, we can conclude that R0 is being incremented by 8. Its initial value is 000F (as you saw when you NOPed the INP instruction), so the program displays 0F, 17, 1F, 27, etc. Assuming that R0 is 000F when the 1861 begins its scan, here are the values in R0 that correspond to each position on the TV screen (though what actually appears on the TV screen are the contents of the memory locations pointed to by R0):

           R0 gets incremented 8 times per line, so the
           contents of 8 memory locations are displayed
           ___________________________________________
          /                                           \
1st      |                                             | blank space
byte_____|___                                          | (1802 running)
         |   \                                         |
line 1   |   000F 0010 0011 0012 0013 0014 0015 0016   |
line 2   |   0017 0018 0019 001A 001B 001C 001D 001E   |
line 3   |   001F 0020 0021 0022 0023 0024 0022 0026   | DMA Active
...      |   XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX   | (1861 running)
line 127 |   03FF 0400 0401 0402 0403 0404 0405 0406   |
line 128 |   0407 0408 0409 040A 040B 040C 040D 040E   |
         |                                      /      |
1024th___|_____________________________________/       | blank space
byte     |                                             | (1802 running)
          \___________________________________________/
                ^-- the address in R0 ends in "F" or
                    "7" at the start of each line

             A computer's-eye view of your TV screen

On your TV set, only a small portion of the screen is used by the 1861. This represents about half of the available time for the TV display, so the rest of the time the 1861 is not asking the computer for data and R0 is not being incremented. This happens after 1024 bytes have been output, so most of the time R0 is at 000F, 040F, 080F, 0C0F or some other multiple of 400 hex (1024 decimal) plus "F". That is why "F" is brighter.

Well, this is not very interesting. However, we can also control R0 in the program. Enter the following code on top of Program 7.1 (i.e. use this code to replace the bytes at 0010-0014):

0010 F880 LOOP: LDI #80   .. FIX R0
0012 A0         PLO 0
0013 3010       BR  LOOP  .. CONTINUOUSLY

Now when you run the program, you will see a nice clean set of vertical bars of various widths across your TV screen. Do you know what they are? Try different values in location 0011, and see if you can find the relationship between what is in the memory location R0 points to and what appears on the TV screen.

Each horizontal line displayed on the TV screen is eight bytes of eight bits, or 64 bits wide. The leftmost byte is M(R0), i.e. the contents of the memory location pointed to by R0. Next comes M(R0+1), then M(R0+2) and so on until M(R0+7) at the right end of the line. The most significant bit in each byte (bit 7) is displayed on the left, and the least significant (bit 0) on the right. A "1" in a bit is displayed as a white spot, and a "0" as a black spot. Like this:

byte: M(R0)    M(R0+1)  M(R0+2)  M(R0+3)  M(R0+4)  M(R0+5)  M(R0+6)  M(R0+7) 
bits: 76543210 76543210 76543210 76543210 76543210 76543210 76543210 76543210

Let's be sure you understand what this program is doing. First it sets up R1, R2 and R3. We use R2 to point to temporary storage in memory. Don't worry about R1 yet; I only included it to save work later when you will expand Program 7.1 into Program 7.6. R3 will be the PC. How does it get that way? (Hint: look at the comments in the program). If you still do not understand what the 23 in location 000E is doing, go back to Chapter 6 and review the DIS instruction.

After executing the DIS instruction, R0 contains 000F (you saw that on the display) and the next instruction does an input from "Port 1". This happens to be recognized by the 1861 to mean "Turn on the TV display" so it starts up, requesting bytes from the computer and displaying them. The TV screen consists of a whole bunch of horizontal lines (you can see them if you look closely). The 1861 uses 128 of them in the middle of the raster (which my dictionary defines as "a pattern of scanning lines covering the area upon which the image is projected"). For each line, the 1861 asks for eight bytes of memory. While the computer is pumping these eight bytes out to the 1861 it has no time for executing the program (all the cycles have been "stolen"), but during the black spaces at the left and right ends of each line there is time for exactly three 2-cycle instructions. Finally, after the 1024th byte on the 128th line, the 1861 goes to sleep (the black space at the top and bottom of the raster) and the computer is able to do some 941 more 2-cycle instructions before the 1861 starts asking for bytes again.

When we modified the program to force R0 to be some particular value, then every time the 1861 asked for a block of eight bytes it got the same block, so every line was the same. Instead of 1024 bytes, we used just 8 bytes for the entire screen. We can take advantage of this trick to save memory, so instead of needing 1024 bytes to hold the picture being displayed we can make do with only 48. The result will be that instead of being only one raster line high, each picture element (also called a pixel) will be 25 lines high. That is, we will display the first 8-byte line 25 times, then the second 8-byte line 25 times, etc. so that by the time we reach the 128th line, we have only used 48 bytes.

The next program is added on top of Program 7.1. In other words, if Program 7.1 is still in memory, then just step over it (in Examine mode) to location 000F and then start entering Program 7.2. If not, key in the first fifteen bytes of Program 7.1, and then begin entering Program 7.2. Notice that three bytes are missing at 0021-0023. Don't worry about putting anything there. (Why?)

        ..  PROGRAM 7.2 -- TV DISPLAY DRIVER
        ..
000F 69   IN:   INP 1     .. TURN ON TV
0010 3410       B1  *     .. SYNCHRONIZE EF1
0012 F814 SYNC: LDI #14   .. =20 DECIMAL
0014 3C14       BN1 *     .. WAIT FOR NEW EF1
0016 FF02       SMI 02    .. MEASURE SIZE
0018 3416       B1  *-2   .. 12 OR 28 INSTRUCTIONS
001A FE         SHL       .. 20-28 IS NEGATIVE;
001B 3312       BDF SYNC  .. TOO LONG=NO DMA
001D 93         GHI 3     .. FIX R0
001E B0         PHI 0
001F 3024       BR  DONT
0021    ..
0022    ..  DISPLAY REFRESH
0023    ..
0024 3C24 DONT: BN1 *
0026 F8C8       LDI BUFF  .. SET UP R0
0028 3428       B1  *     .. WAIT FOR DISPLAY
002A A0   ROW:  PLO 0
002B            ...       .. DMA HERE
002B A0         PLO 0     .. RESET R0
002C B7         PHI 7
002D F80B       LDI 11    .. =(RASTER COUNT - 3)/2
002F            ...
002F A7         PLO 7     .. R7 LOW IS COUNTER
0030 97         GHI 7     .. KEEP FIXING R0
0031 A0         PLO 0
0032            ...
0032 27   REPT: DEC 7     .. COUNT RASTERS
0033 97         GHI 7     .. KEEP FIXING R0
0034 A0         PLO 0
0035            ...
0035 A0         PLO 0
0036 87         GLO 7     .. CHECK THE COUNT:
0037 3A32       BNZ REPT  .. TWO RASTERS PER LOOP
0039            ...
0039 80         GLO 0     .. IF LAST TIME,
003A 3C2A       BN1 ROW   .. (NO, DO IT 6 TIMES)
003C A0         PLO 0     .. JUST BLANK IT
003D            ...
003D 343C       B1  *-1   .. (3 RASTERS)
003F 3024       BR  DONT  .. NEXT FRAME

This program is a little tricky. First, you need to know that the 1861 sets EF1 during the last four lines of the display on the TV, and also for an equal time just before the first line of the display. So this program uses the B1 and BN1 instructions to keep track of where the 1861 thinks it is. Unfortunately, the "first 4 lines" and "last 4 lines" flags look the same to the 1802, so it is possible that the program might think the TV is in the display part of its cycle when it is actually in the vertical blanking (the black at the top and bottom), and vice versa. The display will not work right if this is so, so we cannot let it happen.

Since the EF1 flag is true during the last four lines of the display, that means that on that occasion the computer can execute only 12 instructions (three for each line). On the other hand, before the display starts, the same amount of time is good for 28 instructions (7 each, for four lines). So we (that is, the computer, at our command) will count the number of instructions that can be executed while EF1 is true. We start the counter at 20 (decimal) in location 0012. Then each time we look and see EF1 true we subtract two, for the two instructions in the loop. When EF1 goes false, we look to see if the counter in D went negative. If so, we go back and try again to get the other end of the display. To convince yourself that this works you might try the program a few times after putting a Branch Never (hex 38) in place of the BDF at 001B.

The main part of this program waits for the signal from EF1 telling that the display is about to start, then loads R0 with the address 00C8. The low byte of R0 is also saved in the high byte of R7. R7 also contains, in its low byte, a counter of the number of lines left to be repeated. The program continually copies the same address into R0, counting the number of times; when the count reaches zero, we let R0 advance to the next block of eight and repeat. After five of these, EF1 comes on again, so we hold the last line until EF1 goes away, then go back to the beginning.

Each place in the program with three dots ("...") marks where the 1861 is going to ask for a block of eight bytes. By carefully counting the number of instructions in the loops you can be sure that the program and the hardware play together in synchronism. We have to be sure that R0 gets reset (backed up by 8) once for each block except the last line in a group: You will notice a PLO 0 between every pair of dots, but after the last in the group is also a GLO 0 (so you put back what was already there). The timing is tricky; reading the 1861 data sheet may help you to understand what is required.

In between these PLOs are the instructions that set up the counter in R7, and that count the lines. You should have no trouble understanding this part of the program. We are breaking the 128 lines into five blocks of 25 lines each. The few instructions we have between the first three lines are used in putting an 11 in the counter, then every two lines following them we count down by one. This accounts for 125 of the 128 lines, so we know that we are in the last four (i.e. EF1 is on) when the counter hits zero.

When you run this program, you will see several tall blocks or vertical bars with notches. You can put blocks of black and white (like, say, a checkerboard pattern) on the display by storing patterns of "FF" and "00" in memory between 00C8 and 00F7. Try it to get a feel for what the next program needs to do.

As you can see, this is still not very interesting. Key in Program 7.3 on top of Program 7.2. Notice that it replaces the last branch in 003F, and continues from there.

        ..  PROGRAM 7.3 -- SECONDS CLOCK
        ..
003F 90         GHI 0
0040 B7         PHI 7
0041 F8C7       LDI FRCT  .. POINT TO FRAME COUNT
0043 A7         PLO 7     .. R7 IS AVAILABLE
0044 07         LDN 7
0045 FC01       ADI 1     .. BUMP COUNTER
0047 57         STR 7
0048 FF3D       SMI 61    .. MOD 61
004A 3B24       BNF DONT  .. NOT OVER
004C E7         SEX 7
004D 73         STXD      .. ROLL OVER
004E F0         LDX       .. TO SECONDS
004F FC03       ADI 3
0051 57         STR 7
0052 3B69       BNF UNIT  .. GO DISPLAY
0054 F8E2       LDI -30   .. ROLL OVER
0056 73         STXD
0057 F0         LDX       .. TO TENS
0058 FC03       ADI 3
005A 57         STR 7
005B FC0C       ADI 12    .. (OVERFLOW AT 60)
005D 3B62       BNF TENS
005F F8E2       LDI -30   .. ONE MINUTE!
0061 57         STR 7
0062            ...       .. COULD DO MINUTES, HOURS...
0062 F8C8 TENS: LDI BUFF  .. POINT TO LEFT DIGIT
0064 306B       BR UNIT+2
0066 F8C6       LDI SECS  .. (POINT TO COUNTER)
0068 A7         PLO 7
0069 F8CC UNIT: LDI BRIT  .. OR RIGHT DIGIT
006B A0         PLO 0     .. R0 POINTS INTO BUFFER
006C 47         LDA 7     .. POINT TO DIGITS
006D FCAC       ADI TABL  .. (TABLE OFFSET)
006F A7         PLO 7
0070 47   DOWN: LDA 7     .. GET DOTS
0071 52         STR 2     .. (SAVE)
0072 E2         SEX 2
0073 F0   HALF: LDX       .. CONVERT A DOT
0074 FE         SHL       .. FROM A BIT
0075 52         STR 2
0076 75         SDB       .. =00 IF DF=1, =FF IF DF=0
0077 50         STR 0     .. STORE INTO BUFFER
0078 10         INC 0
0079 80         GLO 0
007A FA03       ANI 3     .. DO THIS 4 TIMES
007C 3A73       BNZ HALF  .. (9*4 INSTRUCTIONS)
007E 10         INC 0     .. SKIP OVER THE OTHER SIDE
007F 10         INC 0     .. OF THE SCREEN
0080 10         INC 0     .. TO THE NEXT LINE
0081 10         INC 0
0082 F0         LDX       .. CHECK FOR SECOND 4 BITS
0083 3A73       BNZ HALF  .. ((36+6)*2)
0085 80         GLO 0     .. REPEAT IF THIS WAS LEFT
0086 FFF8       SMI BEND  .. CHECK AGAINST BUFFER END
0088 3B70       BNF DOWN  .. ((84+6)*3)
008A 3266       BZ UNIT-3 .. ((270+9)*2)
008C 3024       BR DONT   .. MAX TOTAL <600 INSTRUCTIONS
008E    ..
008E    ..  DOT TABLE FOR DIGITS
008E    ..
008E DAAADF     #DAAADF   .. 0
0091 D9DD8F     #D9DD8F   .. 1
0094 9EDB8F     #9EDB8F   .. 2
0097 9EDE9F     #9EDE9F   .. 3
009A EAA8EF     #EAA8EF   .. 4
009D 8B9E9F     #8B9E9F   .. 5
00A0 CB9ADF     #CB9ADF   .. 6
00A3 8EDBBF     #8EDBBF   .. 7
00A6 DADADF     #DADADF   .. 8
00A9 DACEDF     #DACEDF   .. 9
00AC    TABL=*
00C5    ..
00C5    ..  TIME COUNTERS AND DISPLAY BUFFER
00C5    ..
00C5 E2   STEN: #E2       .. MUST INITIALIZE
00C6 E2   SECS: #E2
00C7 00   FRCT: 00
00C8      BUFF=#C8        .. EMPTY BUFFER SPACE
00CC      BRIT=#CC
00F8      BEND=#F8

Notice that there is a large gap between the "Dot Table" and the last three bytes. Do not neglect those last three bytes at 00C5-00C7, or the program will blow up. Can you guess what this program will do before you run it? (Hint: look at the comments!) Try it, then after you get it started and can get unglued from watching it, we will take a walk through the code.

This program is quite complicated and I will not try to explain every detail to you. But if you can figure it out, you have gone a long way toward understanding the 1802! The important thing to remember is that a TV shows 60 pictures every second. These are normally arranged in an "interlaced" fashion to give more detail, but the 1861 does not bother with that. It also does not do it in exactly 1/60th of a second. Anyway, after each display frame the program increments a counter (in location 00C7). When the counter goes over 61 it is knocked back down to 0 (61 turns out to be slightly more accurate than 60). This happens almost exactly once every second, so the program builds a "digital clock" around that counter!

When I program a counter, I can count from any number to any other number that will fit in the memory or register(s) I am using. In the display driver we counted down from 11; the frame counter counts up from zero. When counting seconds we count up from -30, but count by threes. That way, after ten seconds the counter will hit zero, which is an easy condition to test in the program, and we can set it back to -30. I chose to count up by threes because it takes exactly three bytes to specify the dots for one digit on the display (four dots wide, six dots tall, total 24 dots = 24 bits = 3 bytes). Thus at each second I can take the actual counter, add to it the address of the dot table, then use the sum as an index into the table (an Index is a number or a register which points to a particular entry in a table of values) to fetch the dots for that digit. The minutes works the same way, except that if it reaches -12 (by which we mean "6") we bump it back down.

Each second (except the tenth) we set up some pointers, one to point to the rightmost four bytes of the top line of the display buffer, and one to point into the dot table. There are four loops nested together in this program. The inner loop, from 0073 to 007D, shifts one bit out of the byte fetched from the dot table, then stores a whole byte of "FF" or "00" in the buffer depending on whether that bit was zero or one, respectively (note: 0 stores FF, 1 stores 00). This happens four times, or until the address in R0 is divisible by four (i.e. the right two bits are zero).

Then R0 is incremented four times and the process repeated once. This is the next level of loop, and the program recognizes the end of this loop by the fact that all the bits were shifted out of the dots byte.

The third level of loop goes back for the other two bytes in the dots table. The program can tell it is at the end of that loop if the buffer pointer (R0) is past the end of the buffer.

The outside loop takes care of the fact that we use the same part of the program to set up the tens digit as the units, only that the buffer pointer is pointing to the leftmost four bytes of each line. On every tenth second, after resetting the units to zero and incrementing the tens, the program sets up the tens digit first. Then when it finishes, if R0 is pointing exactly at the end of the buffer (i.e. the first byte past it, 00F8), the program knows it just did the tens, so it goes back for the units. When it finishes the units R0 will point to 00FC (why?), so the display formatting is finished. Take a few minutes to think through each instruction. Write down on paper what should be in the registers (and memory) at each point. In other words, pretend you are the computer and "execute" the program. It is good practice.

Did you know your computer could do two things at once? Well, not at exactly the same instant, but it can switch back and forth fast enough so it seems to be doing both at the same time. Notice the numbers in parentheses in the comments of Program 7.3. These refer to how many instructions it takes to do that part of the loop. The outside loop is a little under 600 instructions once every ten seconds, about 300 instructions the other nine seconds, and is not executed at all in between. That means there over 50,000 instructions just going to waste every second! If you have nothing to use them for you have lost nothing, but sometimes you can do things in between. If you do, however, you must be careful to get back to the display routine in time for the next frame or the picture will be garbled. You could do this by continually testing EF1, but that is a real nuisance since you have to test it every ten or so instructions. Or you can let the 1861 interrupt you when it needs attention.

Suppose you are entertaining guests in your home and your two-year-old daughter says "I gotta go potty!" You drop everything and tend to her needs. Then, you come back and resume your conversation where you left off. (If you do not have a two-year-old daughter, I'm sure you can imagine what it is like). You have been interrupted. Actually, when you get back the conversation may have gone on without you; but when the computer is interrupted everything stops and waits for the CPU to come back and resume where it left off. If we program it carefully, the program that was running need not even know it was interrupted. An interrupt then, is some external event (like the 1861 getting ready to start its DMA) which triggers the computer to set aside what it is doing so it can tend to the device which caused the interrupt, then return at a later time and resume what it was doing.

Key in Program 7.4 on top of Program 7.3 (and 7.2 and 7.1). Notice that it is in two parts. You do not have memory sequenced to help you any more, so you will have to follow the program listing carefully while skipping over the parts already entered.

        ..  PROGRAM 7.4 -- TEST INTERRUPTS
        ..
000F C4   MAIN: NOP       .. DON'T TURN ON TV
0010 30AC       BR TEST   .. DO TIME TEST
0012    ..
0012    ..  DISPLAY REFRESH
0012    ..
0012 12   DONE: INC 2     .. X=2!
0013 42         LDA 2     .. RESTORE DF
0014 F6         SHR
0015 42         LDA 2     .. RESTORE R7
0016 B7         PHI 7
0017 42         LDA 2
0018 A7         PLO 7
0019 42         LDA 2     .. NOW D
001A 70         RET       .. RESTORE X AND P
001B C4   INTS: NOP       .. EVEN OUT CYCLES
001C 22         DEC 2     .. PUSH STACK, TO
001D 78         SAV       .. SAVE X AND P (IN T)
001E 22         DEC 2
001F 73         STXD      .. SAVE D
0020 87         GLO 7     .. SAVE R7
0021 73         STXD
0022 97         GHI 7
0023 73         STXD
0024 7E         SHLC      .. SAVE DF
0025 73         STXD
00AC    ..
00AC    ..  FLASH Q FOR TIME TEST
00AC    ..
00AC 17   TEST: INC 7     .. USE R7
00AD 97         GHI 7     .. AS COUNTER
00AE 7A         REQ       .. RESET Q, THEN
00AF 3AAC       BNZ TEST
00B1 7B         SEQ       .. IF ZERO, TURN ON Q
00B2 30AC       BR TEST

Notice that this program is approximately the same as Program 2.3 -- that is, it blinks the Q light once every couple seconds. Count how many you get in one minute and make a note of it. Now change the NOP in 000F back to an INP 1 instruction, so the DMA will run. Run the program again and count how many blinks in a minute. Why do you suppose you got fewer? You see, when the CPU takes time out to do DMA, it cannot be running its program. Cycles are being stolen.

Now you need to make three patches. The first is to change the DIS instruction in 000D to a RET (70); this will enable interrupts. The other two are the two branch instructions which branch to 0024; they must be changed to branch to 0012. In other words, at 004A there should be "3B12" and at 008C there should be "3012". When I was testing this I forgot to make those changes and wiped out half the program.

Now run it. Notice that you now get the clock back on the TV, and the Q continues to blink, although somewhat slower. Your computer is doing two things at once! You might want to time the Q blinks again, to see how much time out of the blink routine the clock routine is stealing.

Before we go any further, you should take a close look at the program your computer is running. In other words, write out on a piece of paper what is left of Programs 7.1, 7.2, and 7.3, and where Program 7.4 fits in. If it is any help, you might want to examine memory for reference. Program 7.6 at the end of this chapter is almost the same as what you have in memory (except for 00AC-00C4), and it is all in one place; you can use it for reference. I want you to particularly notice how the computer gets into the display driver and clock routine. Perhaps it would help here to draw a flowchart.

Let's follow the program sequence. The program starts with R0 as PC. It puts addresses in R1 and R3 (and R2, as you see, is just used to point to temporary data). The RET instruction in 000D sets P=3 so that now R3 is the PC; this frees up R0 for DMA. R3 points to the INP instruction in 000F, then the program branches around the display driver, to 00AC. There the computer is stuck in a tight loop, setting and resetting Q. Nowhere do we see a branch into the display driver, except those already within that part of the program. R1 points to 001B, but there is no SEP 1 instruction in the program anywhere. There is no way this program can, by executing instructions, get into the section from 0012 to 008D, and that's a fact. But it is getting there, because you know that this is the only part of the program that counts seconds on the TV screen!

It gets there without executing instructions. This is the essence of the interrupt. The Q blinking program is paying no attention to the 1861 or the TV or EF1 at all. It just sits there and counts and blinks. Bye and bye, the 1861 decides it is time to put a new picture on the TV screen, and it pulls down on the 1802's INTERRUPT pin. The 1802 finishes whatever instruction it was executing, copies X and P into that mysterious T register, and forces X=2 and P=1 just as if a DIS instruction had been executed with X pointing to a register pointing to a byte with hex 21 in it. But there is no such instruction or byte. It happens kind of like magic.

Now all of a sudden P=1, R1 is the PC, and the next instruction is in 001B. No matter that the last instruction was in 00AE and that the next instruction was supposed to be the BNZ in 00AF (maybe; the last instruction could equally well have been the GHI or any other instruction in that loop). There are exactly 14.5 instruction times before the first line of DMA comes crashing through, and the program had better be ready. Remember, the computer has no way of knowing where it came from, except for that saved X and P in T, so first off we want to save it in RAM; later we will be able to do a RET referencing that byte to return control to where we came from. We do not know if that other program was using R2 to point to temporary data (this one was not, but generally that is not the case), so we decrement R2 first. Notice that the SAV instruction uses the register pointed to by X, but the interrupt set X=2. The display driver uses D and so does the blinking routine. If the blinker was interrupted between the GHI and the BNZ (as we supposed), then if the display routine altered D without saving and restoring it, the blinker would malfunction. The STXD in 001F saves D.

Perhaps you noticed that both the blinker and the display routine use R7. Why, you might ask, when there are so many other registers? True; I could have used separate registers, but then you would not have needed to save and restore a register. When you write larger programs, you will run out of registers and this will become a necessity. Finally, the clock routine alters DF, so we shift it into the accumulator in 0024, then save the result.

Notice that all of these bytes are saved by pushing them onto a stack, which is an area in memory for saving data. The Stack Pointer points either to the next available (unused) byte or to the last stored byte; all the bytes above the "top" of the stack contain data, and all below it are available. You "push" data onto the stack by decrementing the stack pointer and storing; you "pop" data off (i.e. recover it) by incrementing and loading. In our case, R2 is the stack pointer. There is a lot more that could be said about stacks, but you will get the idea by looking at the program.

When the display is finished, or after formatting new digits in the display buffer, the program has to get back to the blinker program. It does this by branching to 0012. All the data we saved we now recover, in the reverse order. DF is retrieved by shifting the saved byte right, R7 is reloaded from the stack, D is retrieved, and finally the RET instruction reloads X and P from the byte saved, and enables the interrupts (which were disabled when the interrupt came). At this point in time X is still =2. RET loads X and P, then increments R2. Since R2 was decremented before saving X and P, R2 is now in the same condition it was before the interrupt. Now P=3 (because it was reloaded) so the next instruction is (you guessed it!) the BNZ (or whatever was going to have been executed before the interrupt). R1 now points to the entry point 001B, just where we found it. R7, D, and DF have also been carefully put back where they were. The system resumes running the blinker program, and is ready for the next interrupt.

What happens if you do not save everything and put it all back? I'm glad you asked! Key in Program 7.5 on top of everything else, and we shall see.

        ..  PROGRAM 7.5 -- MINI DIAGNOSTIC
        ..
00AC E7   TEST: SEX 7     .. SET UP R7
00AD A7         PLO 7     .. AS COUNTER
00AE 3BB1       BNF PLUS
00B0 17         INC 7
00B1 60   PLUS: IRX
00B2 7C01       ADCI 1    .. IN PARALLEL WITH D
00B4 3AB1       BNZ PLUS
00B6 E2         SEX 2     .. WHEN D=00,
00B7 87         GLO 7     .. COMPARE THEM:
00B8 22         DEC 2
00B9 52         STR 2
00BA 64         OUT 4     .. DISPLAY DIFFERENCE
00BB 32BF STOP: BZ  *+4   .. EQUAL CONTINUES
00BD 3FBD       BN4 *     .. UNEQUAL WAITS
00BF 37BF       B4  *     .. WAIT FOR RELEASE
00C1 6C         INP 4     .. GET NEW COUNT
00C2 FE         SHL       .. SET DF
00C3 30AC       BR TEST   .. REPEAT

As usual, try to understand what this program does before running it. There are several ways to count in the 1802, and this program starts with the same number (in D) and counts in two different ways, then compares the results. One way of counting is to increment a register; another is to add one to D. In this case, when D reaches zero, if R7 is not also zero, then something went wrong. The purpose of the program is to make it as easy as possible for something to go wrong, without actually forcing it. So in the loop we will increment R7 with the IRX instruction, and we will add one to D with the ADCI instruction. Since the initial contents of DF affect the operation of ADCI we also make a corresponding correction to the initial value of R7. At the end, the program outputs the difference (i.e. the low byte of R7), then waits for the "I" key if that was not zero. In any case a new value is input from the keyboard to become the new carry and accumulator values.

If you keyed in this program correctly, when you run it, it should display hex "00", and count time on the TV as before. You can push various keys, and while that will affect the program, you will not see it.

Now change the LDA 2 in 0019 to an INC 2. What have you done? What will our new program do if D is not the same after an interrupt as it was before? Try it. It immediately displays some non-zero value and waits for you to push the "I" key. Try entering different numbers. Does that make any difference? How about hex FF? Notice that with hex FF it may go several seconds before erroring off.

There are four LDA 2 instructions in the interrupt exit part of the program (0012-001A). When we changed one of them to an INC 2 it meant that the D register was no longer being restored at the end of the interrupt. Put that one back and change one of the others. Notice the various failures. I hope you are getting a feel for the kinds of errors that an improper interrupt service routine can cause. You should be especially aware that these kinds of errors are very intermittent: they only show up if the unrestored register is in the process of being used when the interrupt comes. You should notice that I carefully arranged that your testing did not affect R2. If the stack pointer does not get restored correctly, not only does the main program have errors, but everything in memory is likely to get wiped out! We call this condition a stack fault. You might try it if you feel like re-keying your program in. Notice that losing an Increment may be different than losing a Decrement.

The faults that you have been simulating resemble those that also show up when there is something wrong with the hardware. This does not occur very often, but if it does, it is nice to be able to devise a program which will isolate the fault. We call such a program a diagnostic because it helps us to "diagnose" the problem.

There is another characteristic of these 1861 programs that I have not yet discussed; timing. Why, for example, is there a NOP in 001B and nowhere else? The reason is that all of the instructions whose left hex digit is C take half again as long to execute as all other instructions. Specifically, all long branches, long skips, and NOP take three cycles, and all other instructions take only two cycles. The interrupt, on the other hand, takes only one cycle. The timing for the 1861 is somewhat critical; even a one cycle error causes a line of displayed data to shift or jitter to one side. So we balance out the 1-cycle interrupt with a 3-cycle NOP. You can see the consequences of a timing error for yourself by changing that NOP to something else like SEX 2 (X=2 already, so this is amounts to a two-cycle NOP instead of three). Do you see how your picture is skewed? Put the NOP back and try changing the BZ instruction in 00BB to a SEX 2 followed by LSZ. See what that does to your picture? (Try pushing various keys). When using the 1861, you should avoid all 3-cycle instructions except for those at certain points in the interrupt service routine.

The program you now have in memory should be the same as Program 7.6.


EXERCISES

  1. Program 7.6 counts up. There are several ways you might modify this to count down. One would be to subtract three from the units and tens counters instead of adding three. What else must be done to make this idea work? Is there room in the program to insert the additional add or subtract instruction to compare for wraparound in the units digit? How could you change the program so that it counts down from +30? Would this be longer or shorter? Try it.

  2. Another way to display seconds counting down is rename the digits so that when the counter has a value corresponding to "0" you display "9". Likewise, you'd interchange "1" and "8", "2" and "7", and so on. What does this do to the tens digit? How can you change the program so that the tens will count down from 5 to 0, and not 9 to 4? Try your suggestion.

  3. There are three dots at location 0062 representing what must be done to count minutes. Modify the program to do this, and to display minutes instead of seconds. Hours?

  4. What changes could you make to this program to make a "stopwatch" out of it? That is, when you push the "I" key it counts and when you release it it stops. When it is stopped it should continue to display the last count, but the internal registers should be set to some value which corresponds to zero. I would suggest forcing them to: 59 seconds, 60 frames each time the display routine ends with EF4 false, and counting normally if EF4 is true.

    Real stopwatches have a double-action button: The first time you push it the watch is started; when it is pushed a second time the watch stops. Can you think of a way to do this? Hint: Assign a memory location, one bit of which has the same significance as EF4 used to. Every time you push "I" the bit is complemented. Caution: You cannot just complement it every time you go by with EF4 true, because you may go by several times for a single button push. Do you need more memory to do this?

  5. Ideally we would like our digital clock to display hours, minutes, and seconds. 256 bytes may not be not enough for such a program. Also you will need to figure out how to display the numbers smaller, such as by making the dots only two bits wide instead of eight, and only eight lines high instead of 25. Note: Plan on quite a bit of time to do this one. It is not easy. Also watch out for trying to do too much in one interrupt time. If you take too long you will not be ready to respond to the next interrupt.

        ..  PROGRAM 7.6 -- TV DIGITAL CLOCK
        ..
0000 90   REST: GHI 0     .. INITIALIZE R1, R2, R3
0001 B1         PHI 1
0002 B2         PHI 2
0003 B3         PHI 3
0004 F81B       LDI INTS  .. R1 = INTERRUPT PC
0006 A1         PLO 1
0007 F8FF       LDI #FF   .. R2 = STACK
0009 A2         PLO 2
000A F80F       LDI MAIN  .. R3 = MAIN PC
000C A3         PLO 3
000D 70         RET       .. X=0!
000E 23         #23       .. SET X=2, P=3
000F 69   MAIN: INP 1     .. TURN ON TV
0010 30AC       BR TEST   .. DO DIAGNOSTIC
0012    ..
0012    ..  DISPLAY REFRESH
0012    ..
0012 12   DONE: INC 2     .. ASSUME X=2!
0013 42         LDA 2     .. RESTORE DF
0014 F6         SHR
0015 42         LDA 2     .. RESTORE R7
0016 B7         PHI 7
0017 42         LDA 2
0018 A7         PLO 7
0019 42         LDA 2     .. NOW D
001A 70         RET       .. RESTORE X AND P
001B C4   INTS: NOP       .. EVEN OUT CYCLES
001C 22         DEC 2     .. PUSH STACK, TO
001D 78         SAV       .. SAVE X AND P (IN T)
001E 22         DEC 2
001F 73         STXD      .. SAVE D
0020 87         GLO 7     .. SAVE R7
0021 73         STXD
0022 97         GHI 7
0023 73         STXD
0024 7E         SHLC      .. SAVE DF
0025 73         STXD
0026 F8C8       LDI BUFF  .. SET UP R0
0028 3428       B1  *     .. WAIT FOR DISPLAY
002A A0   ROW:  PLO 0
002B            ...       .. DMA HERE
002B A0         PLO 0     .. RESET R0
002C B7         PHI 7
002D F80B       LDI 11    .. (RASTER COUNT - 3)/2
002F            ...
002F A7         PLO 7
0030 97         GHI 7     .. KEEP FIXING R0
0031 A0         PLO 0
0032            ...
0032 27   REPT: DEC 7     .. COUNTER RASTERS
0033 97         GHI 7
0034 A0         PLO 0
0035            ...
0035 A0         PLO 0
0036 87         GLO 7     .. TWO LINES PER LOOP
0037 3A32       BNZ REPT
0039            ...
0039 80         GLO 0     .. IF LAST TIME,
003A 3C2A       BN1 ROW
003C A0         PLO 0     .. JUST BLANK IT
003D            ...
003D 343C       B1 *-1    .. (3 LINES)
003F    ..
003F    ..  SECONDS CLOCK
003F    ..
003F 90         GHI 0
0040 B7         PHI 7
0041 F8C7       LDI FRCT  .. POINT TO FRAME COUNT
0043 A7         PLO 7     .. R7 IS AVAILABLE
0044 07         LDN 7
0045 FC01       ADI 1     .. BUMP COUNTER
0047 57         STR 7
0048 FF3D       SMI 61    .. MOD 61
004A 3B12       BNF DONE  .. NOT OVER
004C E7         SEX 7
004D 73         STXD      .. ROLL OVER
004E F0         LDX       .. TO SECONDS
004F FC03       ADI 3
0051 57         STR 7
0052 3B69       BNF UNIT  .. GO DISPLAY
0054 F8E2       LDI -30   .. ROLL OVER
0056 73         STXD
0057 F0         LDX       .. TO TENS
0058 FC03       ADI 3
005A 57         STR 7
005B FC0C       ADI 12    .. (OVERFLOW AT 60)
005D 3B62       BNF TENS
005F F8E2       LDI -30   .. ONE MINUTE!
0061 57         STR 7
0062            ...       .. COULD DO MINUTES, HOURS...
0062 F8C8 TENS: LDI BUFF  .. POINT TO LEFT DIGIT
0064 306B       BR UNIT+2
0066 F8C6       LDI SECS  .. (POINT TO COUNTER)
0068 A7         PLO 7
0069 F8CC UNIT: LDI BRIT  .. OR RIGHT DIGIT
006B A0         PLO 0
006C 47         LDA 7     .. POINT TO DIGITS
006D FCAC       ADI TABL  .. (TABLE OFFSET)
006F A7         PLO 7
0070 47   DOWN: LDA 7     .. GET DOTS
0071 52         STR 2     .. (SAVE)
0072 E2         SEX 2
0073 F0   HALF: LDX       .. CONVERT A DOT
0074 FE         SHL       .. FROM A BIT
0075 52         STR 2
0076 75         SDB       .. =00 IF DF=1, =FF IF DF=0
0077 50         STR 0     .. STORE INTO BUFFER
0078 10         INC 0
0079 80         GLO 0
007A FA03       ANI 3     .. DO THIS 4 TIMES
007C 3A73       BNZ HALF  .. (9*4 INSTRUCTIONS)
007E 10         INC 0
007F 10         INC 0
0080 10         INC 0
0081 10         INC 0
0082 F0         LDX       .. CHECK FOR SECOND 4 BITS
0083 3A73       BNZ HALF  .. ((36+6)*2)
0085 80         GLO 0     .. REPEAT IF THIS WAS LEFT
0086 FFF8       SMI BEND
0088 3B70       BNF DOWN  .. ((84+6)*3)
008A 3266       BZ UNIT-3 .. ((270+9)*2)
008C 3012       BR DONE   .. MAX TOTAL <600 INSTRUCTIONS
008E    ..
008E    ..  DOT TABLE FOR DIGITS
008E    ..
008E DAAADF     #DAAADF   .. 0
0091 D9DD8F     #D9DD8F   .. 1
0094 9EDB8F     #9EDB8F   .. 2
0097 9EDE9F     #9EDE9F   .. 3
009A EAA8EF     #EAA8EF   .. 4
009D 8B9E9F     #8B9E9F   .. 5
00A0 CB9ADF     #CB9ADF   .. 6
00A3 8EDBBF     #8EDBBF   .. 7
00A6 DADADF     #DADADF   .. 8
00A9 DACEDF     #DACEDF   .. 9
00AC    TABL=*
00AC    ..
00AC    ..  MINI DIAGNOSTIC
00AC    ..
00AC E7   TEST: SEX 7     .. SET UP R7
00AD A7         PLO 7     .. AS COUNTER
00AE 3BB1       BNF PLUS
00B0 17         INC 7
00B1 60   PLUS: IRX
00B2 7C01       ADCI 1    .. IN PARALLEL WITH D
00B4 3AB1       BNZ PLUS
00B6 E2         SEX 2     .. WHEN D=00,
00B7 87         GLO 7     .. COMPARE THEM:
00B8 22         DEC 2
00B9 52         STR 2
00BA 64         OUT 4     .. DISPLAY DIFFERENCE
00BB 32BF       BZ *+4    .. EQUAL CONTINUES,
00BD 3FBD       BN4 *     .. UNEQUAL WAITS
00BF 37BF       B4  *     .. WAIT FOR RELEASE
00C1 6C         INP 4     .. GET NEW COUNT
00C2 FE         SHL       .. SET DF
00C3 30AC       BR TEST   .. REPEAT
00C5    ..
00C5    ..  TIME COUNTERS AND DISPLAY BUFFER
00C5    ..
00C5 E2   STEN: #E2       .. MUST INITIALIZE
00C6 E2   SECS: #E2
00C7 00   FRCT: 00
00C8      BUFF=#C8        .. EMPTY BUFFER
00CC      BRIT=#CC
00F8      BEND=#F8
                END

[ << Chapter 6 ] [ Index ] [ Appendix A >> ]



* (A Short Course In Programming is Copyright 1980 by Tom Pittman, and is reproduced in TinyELF's help book with the author's permission. Visit Tom's website.)