by Richard F. Drushel (drushel@apk.net)
A couple weeks ago, a general notice was posted by the administrators of the Cleveland Freenet, to the effect that they were going to clean up (i.e., remove) any areas and SIGs which they deemed to be dead or abandoned. Unfortunately, the Coleco ADAM Forum was one of the SIGs that was slated for removal. I must admit, other than reposting my TWWMCA articles to the general bulletin board, I hadn't done much with it; and various technical glitches have prevented us from getting B.A.S.I.C. members Pat Williams and Jean Davies on-line and participating. Fortunately, as I am one of the sysops, I was able to intervene and save the Coleco ADAM Forum from destruction. I updated some of the informational text files, redid the menu structure a little, and have at least planned how I want to revamp the newsgroup and bulletin board areas which we have. If any of you have accounts on the Cleveland Freenet, all you have to do is type "go adam" at any "Your Choice ==>" prompt. If you don't have an account on CFN, but have telnet access and want to check us out, then do the following:
telnet://freenet-in-a.cwru.edu
You'll get a nice ASCII art picture of the Cleveland skyline, and then the following prompt:
Are you:
1. A registered user
2. A visitor
Please enter 1 or 2:
You can enter "2". You will then get another prompt:
Would you like to:
1. Apply for an account
2. Explore the system
3. Exit the system
Please enter 1, 2 or 3:
You can enter "2" again, and you'll be able to look around the system. Once you get to a "Your Choice ==>" prompt, you should type "term" and set your terminal type (probably "vt100" would be best). Then you can "go adam" and look at the information files and newsgroups. Unfortunately, you won't be able to post anything if you are just a visitor. You can, however, apply for an account, as indicated on the menu above; accounts on CFN are free.
I welcome any suggestions about improving the look of the Coleco ADAM Forum. Let me know what you think! You can reach me and the other sysops (Herman Mason and George Koczwara) by sending mail to xx001@po.cwru.edu.
In last week's TWWMCA (9709.07), I promised to talk about the problem of the non-reentrancy of EOS VRAM routines, and a solution that I implemented as part of the EOS-8 project.
What do I mean by "non-reentrancy"? Basically it means that VDP (video display processor) is hardware which can only be accessed by one user (or subroutine or process) at a time, and while that user is accessing it, nobody else dare interrupt him, or the VDP will get messed up or confused. Think how hard it is to concentrate if you're talking to someone on the telephone and then your kids pick up the extension phones and all start talking at once. Unless you have remarkable powers of concentration, the phone is a non-reentrant resource--only one person can talk intelligibly at a time.
The Z80 microprocessor allows external conditions (such as a keyboard or button being pressed, a character arriving at a modem) to generated a signal called an interrupt. When an interrupt is received, the Z80 finishes the last machine code instruction it was executing, and then jumps to some special locations in memory and starts executing whatever program is there. (The programmer has to plan for this and provide the appropriate program at the appropriate location.) This program is called an interrupt service routine (ISR), because it is supposed to deal in some special way with whatever condition caused the interrupt. For example, in the case of a keyboard interrupt, the ISR might read the key that was pressed and save an ASCII code for it in a buffer somewhere. The ISR ends with a special form of the RET (return) instruction, which tells the Z80 to go back to the program that it had been working with and keep going, right where it left off. Interrupts are an efficient alternative to polling, which means sitting in a loop asking "Are you ready yet? Are you ready yet?" over and over until the answer is "Yes". With interrupts, when it's ready, it will come grab *you*, so you can do other things until you get grabbed.
How might the VDP get interrupted? It turns out that the VDP itself generates an interrupt 60 times per second (50 for European ADAMs running on 50 Hz line current instead of 60 Hz). What if the main program was in the middle of a write to the VDP (say to display a character on the video screen) when this VDP interrupt occurred, and the VDP ISR also tried to write to the screen? The result would be garbage on the screen and a confused VDP, because it hadn't finished the first write before starting the second.
The issue is not academic for the VDP, because of a quirk in the way the VDP responds to interrupts. Any VDP ISR must have, as its last action before returning from the interrupt, a read of VDP register 8 in order to acknowledge the interrupt and enable it for another cycle 1/60th (1/50th) of a second later. If *any* VDP read or write is already in progress, this acknowledgment read will disrupt it and leave the VDP in an unstable state.
There are two possible solutions to the problem: (1) keep the VDP interrupt disabled, so the main program can never get interrupted; or (2) somehow keep track of when the VDP is being used by a user program, so that an interrupt routine won't try to access the VDP if it's busy, but will instead wait until another time when the VDP is free).
SmartBASIC 1.0 uses both methods in different places.
In TEXT mode, the main program can PRINT to the screen, while the VDP interrupt routine manages blinking on and off if the FLASH command is active. Blinking can't safely occur, however, if a PRINT is in progress. SmartBASIC uses a status flag to indicate that PRINT is active, and the FLASH interrupt routine leaves the VDP alone; however, the FLASH interrupt routine also maintains a status flag for PRINT to tell it that an interrupt has occurred and that PRINT needs to do the acknowledgment read of VDP register 8, to restart the VDP interrupt for the next cycle. The result of this handshaking is that access to the VDP is regulated to one routine at a time. The FLASH rate during a PRINT is visibly slower, however, than if you are just sitting at the ] prompt. (Try this: TEXT, FLASH, then CATALOG some disk/tape that will cause the directory listing to scroll off the screen. Observe the difference in blinking rate.)
In the graphics modes (GR, HGR, and HGR2), the VDP interrupt is turned off completely. Thus, the main program can read/write the VDP with impunity. Returning to TEXT mode restarts the VDP interrupt. (In SmartBASIC 1.x, a software clock is installed as part of the VDP interrupt routine, ticking at 60 (50) times per second; the clock stops whenever you are in a graphics mode, because the VDP interrupt is disabled.)
At ADAMcon IV, whence the EOS-8 project was hatched, one of the topics of discussion was this very problem of non-reentrancy of the EOS VDP I/O routines. Based upon study of disassembled EOS code, Bruce Walters had suggested a particular code fix which he thought might solve the problem. In brief, the current EOS-5 code reads/writes the VDP one byte at a time, using IN/OUT instructions in a software loop. The Z80 also provides instructions which can read/write multiple bytes to/from a memory buffer without a software loop, namely INIR/OUTIR (in/out-increment-repeat). These commands use the HL register as a pointer to the buffer, the C register as the port to read/write, and the B register as a counter. Data is transferred via port C; HL is incremented and B is decremented; transfer stops when B is zero. Bruce had hoped that INIR/OUTIR, being single commands, were not interruptable (i.e., they would not respond to an interrupt until B=0); but unfortunately, they *are* interruptable.
I reproduce below a post to the Programmer's Forum on Mark Gordon's Micro Innovations BBS, dated 31 July 1992 (about 2 weeks after ADAMcon IV), in which Chris Braymen and I discuss the issue. It was in writing this post that I thought of a mechanism for VDP interrupt deferral for the EOS VRAM routines.
[Note: I am using Internet E-mail format to respond to the technical discussion, so I can quote relevant parts of previous posts in my response. The T-BBS message editor does not allow this. -- Rich Drushel (75) ]
In a previous article, Chris Braymen (6) says:
>Hi Folks: A discussion took place at ACIV that I want to make sure I >understand.. The PROBLEM: You will end up with scrambled graphics if you >read VDP register 8 (to restart the video interrupt) while you are in the >process of accessing the VDP.
According to The TMS 9118/28/29 Data Manual (Texas Instruments, 1984),
"In an interrupt-driven environment (CPUs accepting interrupts), it is possible for an interrupt to occur before any one of the [multi-byte VDP access] sequences is finished. For example, an interrupt may occur immediately after loading address byte 1 or 2 during a write to VRAM operation. In this case, the interrupt service routine does not know where the interrupt occurred within the sequence. Therefore, it is necessary to disable and enable interrupts before and after every setup sequence. This action sequence prevents loss of continuity between the CPU and the VDP" (p. 2-9).
This means that *ANY* VRAM access (read data, write data, write register, read register 8) will be corrupted if an interrupting routine also tries to access VRAM.
>Most Coleco software solves this problem by maintaining a couple of flags >that effectively delay the video interrupt restart until after the >application is done using the VDP.
ColecoVision games employ an OS-7 routine which defers the writing of graphical objects (as defined with certain data structures) until desired by the programmer. Usually, VDP writes are deferred *until* an NMI occurs. At that point, the VDP is uninterruptable, so all the deferred writes are done until the deferral queue (or stack) is empty. This routine works only with the special graphical objects, not low-level VDP reads/writes or register access. In SmartBASIC, NMIs are simply disabled in GR, HGR and HGR2, ducking the problem. In TEXT mode, NMIs are enabled, and the NMI routine changes the pointer between normal and inverse pattern name tables (if FLASH is enabled) after a defined number of cycles. This is fine except that PRINT accesses the VRAM, as well as the actual TEXT command. The solutions are: The NMI routine checks to see if PRINT is executing, and if so, just does RETN (with no register 8 read to restart the NMI). The PRINT routine itself maintains the FLASH counter and will switch the name table pointers, and then issue a register 8 read. (Check the FLASH frequency when sitting waiting at the prompt versus when text is PRINTing and scrolling on the screen; it is noticeably slower while PRINTing.) Other programs leave the NMI disabled throughout.
>A proposal was made that this problem could be solved in the EOS system >software by using OTIR instructions in place of the current OUT and OUTI >instructions.
Having re-examined my disassembly of EOS-5, I believe that this is no longer a viable solution. See below.
>QUESTIONS: My understanding of this problem holds that: > > A) The problem occurs as a direct result of reading VDP register 8 and >has absolutely nothing to do with the amount of time spent away from the VDP >operation at hand during interrupt processing.
*Any* VRAM register read/write, or VDP data read/write, is vulnerable if interrupted by a routine which also attempts to access VRAM. It is not restricted to the register 8 read to restart the NMI for another cycle. Remember that the register 8 read is *OBLIGATORY* to restart the NMI; RETN is not sufficient.
> B) The problem will occur if register 8 is read between sending the >2 bytes that make up a VDP command, OR if register 8 is read between >reading or writing consecutive data using the auto incrementing VRAM >address.
This is correct.
>Some people at the table indicated the problem only exists during VDP >command access because the EOS already uses OTIR and INIR for data reads >and writes. This is not true! The EOS functions Read_VRAM and >Write_VRAM use OUTI and INI enclosed in loops, they do NOT use the >non-interruptable commands OTIR and INIR.
You are correct. What's more, the register writes use separate OUT commands, the space between which is vulnerable to interrupts.
[note added 9709.14: OTIR and INIR *are* interruptable, so the point is moot]
>So my questions are: > > 1) Am I correct in assuming the VDP hardware will get messed up if >register 8 is read during Command sending OR during data read and write?
You are correct.
> 2) If so, will the EOS rewrite use OTIR and INIR in place of OUTI >and INI in the Read_VRAM and write_VRAM routines?
It can't, because of 3) below.
> 3) If so, how will you incorporate the 2 NOP delay for the "Slow" >VDP between each read/write?
You can't. So unless Bruce's code with OTIR and INIR actually does work, meaning the "slow" VDP on the design board wasn't really so "slow" in practice, I don't think you can use OTIR and INIR to make the write/read uninterruptable.
>And most importantly <grin>, if the OTIR and INIR opcodes are un-interruptable >(except by DMA), how can I make sure I'm not losing MIDI bytes during large >transfers to the VDP?
Only by not using OTIR and INIR :)
>RECOMMENDATIONS: I recommend leaving the system VDP access completely >interruptable and making it the application programmers responsibility to >handle the Video interrupt restart at a time when he/she is not doing anything >else with the VDP.
I agree 100% here. However, I would like to propose a mechanism whereby interrupt routines *WHICH ACCESS VRAM* could do so without fear of corrupting a main program VRAM access in progress.
[description omitted; see IV below]
>I can live with 2 byte interrupt disabling transfers, but I'll have trouble >with any interrupt disabling transfer that's bigger than about 80 bytes.
If there isn't a bug in what I just proposed, you should be safe. INTs are enabled throughout all EOS VRAM function calls, and as long as your INT routine does not access VRAM, you could ignore the above.
>CLOSING: Please take this into consideration when designing the new EOS.
I am trying :)
>Spinner applications may also be adversely affected by an OTIR in write_VDP.
Absolutely.
>Thanks, Chris Braymen (6)
Regards, Rich Drushel (75)
I reproduce here the design summary of the VRAM interrupt deferral routines for EOS-8, as posted to the Programmer's Forum on Mark Gordon's Micro innovations BBS on 6 October 1992. The algorithms are presented as pseudocode and as skeleton assembler. The complete assembler version as used in EOS-8 (and in a proof-of-concept version of SmartBASIC 1.x) is available for download.
vram_def.asm, EOS-8 VRAM interrupt deferral routines
designed by Richard F. Drushel 6 October 1992
Here is a mechanism whereby interrupt routines *WHICH ACCESS VRAM* could do so without fear of corrupting a main program VRAM access in progress. (If your interrupt routine does not access VRAM, there is no problem). It would work best with the NMI (because of its relatively low frequency; an INT can occur at any frequency) but might be useful for INTs as well. It would not improve existing interrupt routines, but could be used as a paradigm for future ones or rewrites. Anyway, code would be required both in the interrupt routine and in each EOS routine which accesses VRAM. EOS would define an interrupt status byte with 3 flag bits:
bit 0 ==> vram_access 1 if an EOS function call accessing
VRAM is in progress, 0 if not.
bit 1 ==> nmi_request 1 if an NMI routine needs
to be executed, 0 if not.
bit 2 ==> int_request 1 if an INT routine needs
to be executed, 0 if not.
The handshaking of interrupt deferral is such that it is impossible to defer *BOTH* NMIs and INTs simultaneously. Indeed, since a READ_REGISTER (read VDP register 8) is required to restart NMIs after each NMI cycle, this effectively means that INT routines which access VRAM *cannot* be deferred if the NMI is active. The logic of the implementation follows:
INT: DI ;disable further INTs
IF vram_access=1 ;if VRAM I/O is in progress...
THEN int_request=1 ;...then request a deferred INT
RET ;and return to VRAM I/O
;*NOTE* INTs *DISABLED*
ELSE int_request=0 ;else wipe the INT request flag
CALL do_int ;do the user INT routine
EI ;reenable INTs
RETI ;return from maskable interrupt
ENDIF
do_int: {user INT routine goes here}
RET
NMI: IF vram_access=1 ;if VRAM I/O is in progress...
THEN nmi_request=1 ;...then request a deferred NMI
RET ;and return to VRAM I/O
;*NOTE* NMIs *DISABLED*
ELSE nmi_request=0 ;else wipe the NMI request flag
CALL do_nmi ;do the user NMI routine
read VDP register 8 ;restart the NMIs
RETN ;return from non-maskable interrupt
ENDIF
do_nmi: {user NMI routine goes here}
RET
EOSVRAM: IF vram_access=1 ;if VRAM I/O is already in progress...
;i.e. this is a VRAM call made from
;inside another VRAM call, already
;flagged
THEN JR do_vram_routine ;...then just do it
;this allows the interrupt routine to
;use EOS VRAM functions, where it is
;safe from interruption
ELSE vram_access=1 ;else mark that we're doing VRAM I/O
;*NOTE* now we can't be interrupted;
;INTs and NMIs will be deferred
CALL do_vram_routine ;do the VRAM routine without fear
IF int_request=1 ;now if a deferred INT occurred
;while we were doing the VRAM I/O...
THEN int_request=0 ;...then clear the request
CALL do_int ;do the user INT routine
vram_access=0 ;clear the VRAM usage flag
EI ;*FINALLY* reenable INTs
RETI ;and return from the interrupt
;We have done our main VRAM I/O *and*
;our INT VRAM I/O.
ELSE
ENDIF
IF nmi_request=1 ;now if a deferred NMI occurred
;while we were doing the VRAM I/O...
THEN nmi_request=0 ;...then clear the request
CALL do_nmi ;do the user NMI routine
vram_access=0 ;clear the VRAM usage flag
read VDP reg8 ;*FINALLY* reenable NMIs
RETN ;and return from the interrupt
;We have done our main VRAM I/O *and*
;our NMI VRAM I/O.
ELSE
ENDIF
vram_access=0 ;all done using VRAM
RET ;we're done!
The actual implementations are:
INT:
A56:
PUSH AF ;save register
LD A,(INTERRUPT_FLAGS) ;point to interrupt flags
BIT 0,A ;is a VRAM I/O in progress?
JR Z,A71 ;NO, so just do the routine
SET 2,A ;YES, so request a deferred INT
LD (INTERRUPT_FLAGS),A ;save it back
POP AF ;restore AF
RET ;return with INTs *DISABLED*
A71:
RES 2,A ;clear request flag for safety
LD (INTERRUPT_FLAGS),A ;save it
CALL A83 ;do the user INT routine (doesn't need
;to PUSH AF)
POP AF ;restore AF
EI ;reenable INTs
RETI ;return from maskable interrupt
A83:
JP user_int_routine ;jump to the actual user code
;it must save AF, HL and any other used
;registers, and end in RET
NMI:
A102:
PUSH AF ;save register
LD A,(INTERRUPT_FLAGS) ;get interrupt flags
BIT 0,A ;is a VRAM I/O in progress?
JR Z,A117 ;NO, so just do the routine
SET 1,A ;YES, so request a deferred NMI
LD (INTERRUPT_FLAGS),A ;save it back
POP AF ;restore AF
RET ;return with NMIs *DISABLED*
A117:
RES 1,A ;clear request flag for safety
LD (INTERRUPT_FLAGS),A ;and save it back
CALL A133 ;do the user NMI routine (doesn't need
;to PUSH AF)
IN A,(191) ;restart the NMI by reading VDP register 8
LD (VDP_STATUS_BYTE),A ;save it for EOS usage
POP AF ;restore AF
RETN ;return from non-maskable interrupt
A133:
JP user_nmi_routine ;jump to the actual user code
;it must save AF, HL and any other used
;registers, and end in RET
;end code which must be installed by the user application
;begin code which is in EOS RAM
VRAM:
PUSH HL ;save register
LD HL,INTERRUPT_FLAGS ;point to interrupt flags
BIT 0,(HL) ;is a VRAM I/O in progress?
JR NZ,DO_VRAM2 ;YES, so just do it
;(interrupts are already deferred)
SET 0,(HL) ;NO, so mark it now in progress
POP HL ;restore HL
CALL DO_VRAM ;do the VRAM routine
VRAM_COMMON_EXIT:
PUSH HL ;save HL
LD HL,INTERRUPT_FLAGS ;point to interrupt flags
BIT 2,(HL) ;is there a deferred INT?
JR NZ,DO_DEF_INT ;YES, so do it
BIT 1,(HL) ;is there a deferred NMI?
JR NZ,DO_DEF_NMI ;YES, so do it
RES 0,(HL) ;NO, so no deferred interrupts; all done
POP HL ;restore HL
RET ;bye!
DO_DEF_INT:
RES 2,(HL) ;clear request flag
CALL A83 ;do user INT routine
RES 0,(HL) ;all done with VRAM routine
POP HL ;restore HL
EI ;finally reenable INTs
RETI ;return from maskable interrupt
DO_DEF_NMI:
RES 1,(HL) ;clear request flag
CALL A133 ;do user NMI routine
RES 0,(HL) ;clear VRAM flag
POP HL ;restore HL
PUSH AF ;save AF
IN A,(191) ;restart NMIs by reading VDP register 8
LD (VDP_STATUS_BYTE),A ;save it for EOS
;VDP_STATUS_BYTE is in EOS global RAM
POP AF ;restore AF
RETN ;return from non-maskable interrupt
DO_VRAM2:
POP HL
DO_VRAM:
{routine here}
RET
If I get any questions or other expressions of interest from the readers, I can talk about a few more details of the interrupt deferral code, which have been omitted above. For instance, there are some EOS VRAM routines which *cannot* be deferred; the technical reasons for this are perhaps of some interest.
Otherwise, I will start to write a technical description of ADAMserve (which I have been meaning to do for quite some time).
See you next week!
*Rich*
Next Article
Previous Article
TWWMCA Archive Main Page