Grayscale is what makes the good graphical games. Think of ZBlitz, Vertigo, Arkanoid, Boulder Dash, Bomberman, Diamonds, and the many other games that use grayscale to enhance their sprites. There are many different forms of grayscale out there. Grayscale has the basic idea of using two or more different video screens and switching them back and forth really fast. Each will stay visible for a different amount of time.

A graphical example might help. Screen 1 is visible a third (1/3) of the time while Screen 2 is visible the remaining two-thirds (2/3) of the time. The resultant shows the shade of gray viewed.

Screen 1 (1/3) Screen 2 (2/3) Result
 
 
 
 

If you still don't get this and you are really bored, get a black piece of paper and a white piece. Put one in each hand and move them back and forth really fast. You will get a blurred image that will seem sort of gray. That's the general idea of grayscale!

I've pasted Dux Gregis' Blitzgry Routine here. You can download this along with many other grayscale routines that I've come across in the Download Section. Each line in the routine is represented by a number which corresponds to an explanation at the end of the routine. Follow each line and try to picture in your head what it is doing. The easiest way to do this is to print out the Blitzgray Code and then refer to the explanations.

  1. int_addr equ $8f8f
  2. ;===============================
  3. ; Install IM 2 Grayscale Routine
  4. ;===============================
  5. OpenGray:
  6. ld hl,$ce00
  7. ld de, SixBytes
  8. ld bc, 6
  9. ldir
  10. ld hl,$8e00
  11. ld de,$8e01
  12. ld (hl),$8f
  13. ld bc,256
  14. ldir
  15. ld hl,int_copy
  16. ld de,int_addr
  17. ld bc,int_end-int_start
  18. ldir
  19. call _runindicoff
  20. ;================================================
  21. ; Set up parameters to pass the interrupt handler
  22. ; via the alternate register set
  23. ;================================================
  24. di
  25. exx
  26. ld b,$3c
  27. ld c,0
  28. ld d,5
  29. ld e,%110110
  30. ld hl,UserCounter
  31. exx
  32. xor a
  33. ld (UserCounter),a
  34. ld a,$8e
  35. ld i,a
  36. im 2
  37. ei
  38. ret
  39. ;==================================
  40. ; Close Grayscale Handler
  41. ;==================================
  42. CloseGray:
  43. im 1
  44. ld a,$3c
  45. out (0),a
  46. call _clrLCD
  47. ld hl,_plotSScreen
  48. ld de,_plotSScreen+1
  49. ld (hl), 0
  50. ld bc,1024
  51. ldir
  52. ld hl, SixBytes
  53. ld de, $ce00
  54. ld bc, 6
  55. ldir
  56. ret
  57. .org $8f8f
  58. int_copy:
  59. int_start:
  60. ex af,af'
  61. exx
  62. in a,(3)
  63. bit 1,a
  64. jr z,leave_int
  65. inc (hl)
  66. out (c),b
  67. dec d
  68. call z,reset_int_counter
  69. ld a,d
  70. cp 2
  71. call z,change_pages
  72. leave_int:
  73. in a,(3)
  74. rra
  75. ld a,c
  76. adc a,9
  77. out (3),a
  78. ld a,$0b
  79. out (3),a
  80. ex af,af'
  81. exx
  82. ei
  83. reti
  84. reset_int_counter:
  85. ld d,5
  86. change_pages:
  87. ld a,e
  88. xor b
  89. ld b,a
  90. ret
  91. int_end:
  92. UserCounter: .db 5
  93. SixBytes: .db 0,0,0,0,0,0
  94. .end

Line(s) Explanation Bytes
1 Our interrupt is going to need to be put at $8f8f. That's where all interrupts are put. This runs on an interrupt so that every 200th of a second it will be run (200 Mhz). That's fast to us; it's hard to imagine how fast the processor is running. That all depends on battery power too. Remember, this routine is only to set up grayscale, it's not to put sprites and stuff on the screen. If you put text, it will be light colored because it's only on one plane (one of the two or more video memories). none
7 Now we are going to switch to grayscale mode. We need to prep everything and copy the grayscale interrupt routine to where it needs to be. none
8-11 We are going to use the video memory ($fc00 to $ffff) for one plane and the graph screen (_plotScreen to $cdf9 1024 bytes) for the other plane. The graph screen needs the first 6 bytes to keep for system use. We are going to have to save those six bytes and put them back later. Our routine will use the entire area, including the six bytes we saved away. We are going to save them using a block copy to a part of our code. 11
13-17 The interrupt system uses a 256 byte long vector table for it's interrupt routines to know which addresses to call. We need to put $8f8f into all the slots in the vector table. We use another block copy to put $8f repeatedly which makes $8f8f8f8f8f8f8f8f which is the address we want over and over and over again. 13
19-22 Now we want to actually put our routine at $8f8f to be called every interrupt (approximately 200 times a second). Remember in line 1 where we made an alias to int_addr =$8f8f so we won't have to type $8f8f anymore. It's easier to remember int_addr being where the interrupt address is than the $8f8f stuff. Between lines 73 and 115 is our interrupt to be copied so we just use a block copy. 11
24 We don't want to see that little busy indicator at the top that TI doesn't do away with, so we call a routine that turns it off. The TI86 automatically turns it back on when the asm program is done. I just wish it would turn it off for us. That would be 3 bytes saved for every program made. 3
30 We are going to use the shadow registers so we need to disable interrupts for a second while we put some data in the shadow registers for use during the grayscale routine. The grayscale routine will exchange out the shadow registers with the regular ones and so it will have the contents of the shadow registers to use which we have prepared. 1
31 Change out bc, de, and hl with their shadow registers. We won't be needing to change out af with its shadow register because a changes so much and we don't need the flags. 1
32 $3c is used in conjunction with port 0 (which we will learn about later) to tell the TI86 which part of memory is to be used as the video memory. $3c would tell the processor it needs to use $fc00 as the video memory. $0a would mean $ca00 as the start of the video memory. You don't really have to understand this yet. 2
33 We are going to need to save which port (port 0) we are using so we can reference it faster during the routine. 2
34 We are going to use 5 as a counter. 2
35 When you xor %110110 with $3c you get $0a which is the other value we need for port 0. 2
36 This is the counter which we are going to need to time how long different planes are visible. 3
37 Put back all the shadow registers in their place for use during the grayscale routine. 1
38-39 We need to zero the UserCounter. Xor a is a one byte method of ld a,0 which is two bytes. It's faster and saves a byte. Like most counting, we start from zero and work our way up. 4
41-42 Remember that i used with r together tell where on the vector table the interrupt handler is supposed to be. We need to put the address of the vector table ($8e) into i to make up the most significant byte of the address. 4
45-46 Start up Interrupt Mode 2 on line 45 and then Enable Interrupts on line 46. 3
47 No need to explain. 1
53 Now that we've got the routine to turn on grayscale mode, we need a routine to turn it off. none
54 Start up interrupt Mode 1 again. 2
55-56 Remember port 0 is the port for the video memory. We are going to send $3c out it which tells the TI86's screen to show from $fc00 to $ffff as the video memory. 4
58 Clear the screen at $fc00. 3
60-64 Since _clrLCD only clears $fc00 to $ffff, we need to make our own routine that will clear the graph memory (our second plane). We are going to clear the first byte of the graph screen, then copy it to the second byte. Then we are going to copy the second byte to the third byte. Now each byte gets cleared by the previous loop of ldir. This is really handy to know. There are 1024 bytes to clear. Think about how this works because you can use it in a lot of things. 13
66-69 Remember in lines 8 through 11 when we saved those precious first six bytes of the graph screen? Well, we need to put them back now. 11
71 Done with it all! 1
73 This is an assembler directive which I will teach you about later. Don't pay attention to it now. none
75 Now we're going to start the actual routine to continuously switch between each of the two video planes. none
76-77 This is sometimes called while another code is being run in normal mode. We break for a micro-second and run this. Because of this we need to save the registers that the other code was using so it will have them when it is resumed. We need to save them before we do our routine and return them like they were after we're done switching video planes. Remember that we saved some data in the shadow registers before we started, we're going to exchange the registers so that the regular registers will be filled with that data. When we're done we'll put that data back in and return our parameters to the shadow registers. 2
79-81 Bit 1 of port 3 is to tell us if the video screen is in the middle of refreshing itself or not. If bit 1 is reset, the screen is refreshing and we shouldn't flip panels in the middle of a refresh. These refreshes take mini-micro-seconds. 6
83 Remember that hl was loaded with the address of the UserCounter which we had saved in our parameters. We want to increment that because it's our counter. 1
84 We saved c with 0, so we can save a byte by not putting in out (0),a. That one byte difference makes the routine faster. Our sending $3c or $0a through port 0 will change part of memory to be shown on the screen, which changes between $fc00 and $ca00. 2
86-87 To give 4 shades of gray (white, light gray, dark gray, black), we have to hold one of the planes up for a little longer than the other. Remember that we loaded d with 5. We use that as a counter to let one show 3 times when the other only shows 1. If the counter falls to zero after we decrease it, we call the routine to reset the counter and change planes. 4
88-90 We now need to check to see if we need to flip planes. We started at d being 5 and decreased it until it was 2. While we are decreasing it, one of the planes is on the screen. When it hits 2, we change to the other plane for the remaining two decreases. That's 3 decreases on one plane and 2 on the other. 6
92 We are going to have to leave now. We need to put the remains of the parameters back into the shadow registers and clean up to return from the interrupt. none
93-94 We need to check the status of the [ON] button. We get a byte from port 3 and rotate (We'll discuss that later) it once to the right. The bit rotated out is put in the carry. That means that the carry is either set or reset. 3
95-97 Remember that c was zero because we were saving that for referencing port 0 when changing video pages. We need to zero a, so we can either xor a, sub a, or, ld a,c (because c is zero at this time). Each of those are one byte long and so are faster than the two byte ld a,0. We use adc to add 9 to whatever is in a and then add the carry flag (either of value 1 or 0) to a on top of the 9 we are already loading. A will either be 9 or 10 ($09 or $0a; %00001001 or %00001010). That will turn the screen back on. We refreshed it when it was off I think. We now turn it on. 5
98-99 Now we need to send %00001011 through port 3 to do something. I don't know this junk. 4
101-102 We need to put back the parameters in the shadow registers and return the regular registers to what they were before we started our grayscale interrupt routine. 2
103-104 We need to enable interrupts (line 104) and return from the interrupts (reti). This not only returns from our routine but from the whole interrupt thing. Instead of just the regular return, this also checks the state of the interrupts for the system. 3
107-108 This is part of a call that is just added onto the front of the routine that changes pages. This part resets the counter and then leads right into the page changing routine. 2
109-114 Remember that e has a value that when xored with our byte to be sent to change the video planes, will yield what we need to use next time for switching video planes. We load our mask into a and xor it with b and put the result back into b. Then we're done with this call. 4
116 This is an easy way for us to determine in line 21 how much we need to copy and paste. We let the assembler figure out the length of the interrupt code by subtracting the beginning address from the end. This gives us the difference in the addresses which is the length of the code we need. none
118-119 This is where we store the data to use in our routine. This isn't copied cause we don't need to have it with the routine. It can be stored with the rest of this blitzgry.h code in your program. 7
121 We're done, and we need to tell the assembler that. .end does just that. none