Notes on Turbo Modula-2 by Steven Hirsch Original release: 01/25/87 Intro: My company, a contractor in the Audio-visual and communications field, had undertaken a project for a large industrial client which would involve micro-computer based scheduling and control of video recorders. In late September we made the decision on a Z80 CP/M environment, and had chosen Modula-2 as the development language. We began with FTL Modula-2 from Workman & Assoc., as it was the only native-code compiler available at that time. By late October, panic was starting to set in. I had uncovered many bugs in the compiler/linker implementation and, despite Workman's polite concern, no fixes were ever brought to this programmer's attention. I was on the verge of re-writing the program (some 5,000 lines+ at this point..) in Pascal MT+ when the impending release of Borland's TM-2 was announced. Echelon, Inc., when they learned of my predicament, agreed to help out; shipping me a copy of the software, with a 3" stack of galley-proof sheets for documentation. I planned on moving into the computer room for an extended stay, figuring that the conversion process would be painful... It's nice to be wrong once in a while. The conversion to TM-2 was accomplished in one Saturday afternoon, with the largest effort being the implementation of 'SET OF CHAR'. FTL supports sets with up to 1024 members, while TM2 has the more traditional (for micro's) limitation of 16! To make a long story short, we delivered the controller on- schedule. The software has been absolutely bullet-proof (so far..), and the customer is delighted! In the course of this ordeal, I found myself being the first individual in the field to: a) Develop a complete product in TM-2. b) Use the assembler interface heavily. c) Use interrupt driven task-switching. d) Decipher the manual.. The assember interface, in particular, seems to be causing a great deal of confusion in the user's community. I therefore am writing this (somewhat rambling) tome to help those wrestling with the compiler as we speak! Assembly Language Modules As mentioned in the manual, TM-2 will allow one to create modules written in Z80 assembly language. I will limit this discussion to the technique of converting .REL files to .MCD, as the 'CODE' method (p.194 of the manual) has some fatal constraints (though it contains some valuable hints on parameter-passing!). The use of an assembler capable of generating a Microsoft-format REL file is essential (I use the SLR Systems Z80ASM tool, and have found it to be the best of the genre. Be sure to specify the /6 or /7 switch when using this program!). In certain sections of the low-level I/O code, I was able to cross-assemble 6502 code using the Microsoft ALDS assembler, which digests 8080, Z80, and 6502 mnemonics - producing a standard .REL file. The target environment is a dual-processor system, and some of the I/O primitives needed to be handled by the 6502 host... Intricacies of the control project will be dealt with in a future article, to be published (I hope..) by one of the hacker-oriented mags. Parameters are passed to the assembly routine on the stack, pushed on in left-to-right sequence (as they appear in the DEFINITION module). If the parameter is passed by value, (ie. the called procedure is not able to return a value or affect the parameter..) and is word-sized, it is placed on directly. Things are thus straight-forward for CARDINAL, INTEGER, ADDRESS, WORD, and CHAR types. In the case of BYTE or CHAR, the most signifigant byte of the word will be cleared to zero. Note that ARRAY[0..1] OF CHAR, although two bytes in length, will not be treated this way! It must be noted that I have not worked at all with floating point, or longs. A general technique for determination of the passing method will be described later. Any parameter passed by reference (called procedure able to affect it's value..) gets a one-word pointer to itself placed on the stack. This applies across the board to all types, although open array types are passed with additional information, as we'll see. Enumerated types are passed by their ORD value (word length!) within the enumeration, ie. if we have this situation: TYPE Junk = (foo, bar, other, things); And the procedure: PROCEDURE DoSomething( formalparm : Junk ); exists. DoSomething(bar); Will cause the word 01h to be placed on the stack! Following the above trend: DoSomething(foo); would generate the value 0h, etc. To reiterate, and clarify things, the procedure: PROCEDURE DoSomething( VAR formalparm: Junk ); Is called from this situation: VAR stuff : Junk; BEGIN stuff := foo; DoSomething(stuff); The DoSomething routine will find a one-word pointer to the location of the variable 'stuff' pushed on the stack. At that location will be the word-value '0000h'. Structured types such as ARRAY, RECORD, etc. are always passed via pointer. I believe that, when passed by value, the compiler creates a 'copy' of the variable prior to the call and passes a pointer to it; although I am not 100% sure of this fact. Responsibility for knowing the physical size and internal structure of the referenced object lies completely on the programmer, when dealing with assembler modules. The manual outlines the storage sizes of the various types, although the information is scattered across several sections. The exact method of declaration is extremely important when dealing with ARRAY types; a declaration of the form: PROCEDURE WorkWithString( VAR formalparm : ARRAY OF CHAR ); will be called with an extra parameter on the stack. First a pointer to the array is pushed on, followed by a word value representing the index (normal to zero) of the last element in the array. Note that the programmer is responsible for translating this index into a relative byte-offset. If one is dealing with ARRAY OF WORD, it is necessary to double the index. I believe that the same method is used for multi-dimensional open array's, although this programmer has not investigated it. If a declaration specifies the size of the array, the extra parameter is not present - only the address is passed. Be very careful when passing an odd-length text string (or ARRAY OF BYTE!) to a procedure expecting ARRAY OF WORD. The compiler will round up it's idea of the string-length to the next word boundry, which may cause your assembly code to step on an adjacent, and unrelated, variable! After all parameters are placed on the stack, the return address is pushed on over them. The actual 'call' to the routine is performed by a 'JP (HL)' instruction, which means that 'HL' contains a pointer to the start of your assembly routine upon entry. This correlates to the explanation of 'CODE'! Although your assembly code need not preserve any registers, the return address must be kept track of. My favorite method for dealing with parameters and return address is outlined in the accompanying file 'CLOCKIO.MAC'. Along with it's .DEF file, this is the low-level module used to communicate with the real-time clock in our controller. The RTC talks to us via a PIO chip, which is memory mapped into the Z80's address space. Without getting sidetracked into the hardware, this code should serve as an outline for interested programmers. It is called with two strings variables as parameters, returning with time and date loaded into them as ASCII strings (MM/DD/YR, and HH:MM). Note that we are not making use of the length index, as it is assumed to be a known constant - this does not represent the worlds greatest programming (Blush..), and the module ought to trap this situation. All procedure entry points are defined as public symbols. I prefer the '::' method, but 'PUBLIC symbol1, symbol2, etc.' may suit some folk's style better. One difficulty arises, in that there is no apparent way to cause the 'module body' intialization code (in our case the clock locator) to be executed before pas- sing control to the main program. I solved this by creating a 'dummy' module (in standard non-assembler Modula) containing only the call to 'INITCLK' in it's body, thus guaranteeing that 'INITCLK' is entered prior to any read-time calls. If the clock is not found, a message is displayed on the screen, and the program terminates to CP/M. If the assembler routine has been defined as a function procedure, it's value (which **must** be a non-structured type, of one-word length!) is pushed onto the stack before executing the return. The easiest way to get a feel for what is passed to a procedure, and how, is to write a dummy assembler module which pops a number of words off the stack, stores them in a known 'safe' area of RAM, and terminates with a RST 38H. The assembly code is linked together with an M-code module, which feeds it known parameters. The .COM file is executed under a debugger (I used the excellent Z8E tool, which is available in the public domain). When the RST break-point is hit, one may inspect the assigned storage area to find out what's what. DO NOT make the mistake of immediately terminating with a RST and inspecting the stack with the debugger, as it's stack becomes intertwined with the program being handled, causing endless confusion. No discussion of the assembler interface is complete without mentioning the fact that the REL.MCD utility requires GOBS of TPA to run, and will not successfully link to a .COM file either! A standard Z3 system does not have the TPA to convert even the smallest .REL file. I was forced to re-boot the computer under vanilla CP/M (what's that?), in order to perform this function. Borland has been notified of this anomoly, but I wouldn't hold one's breath for a solution. Some other miscellaneous points: - One must limit the name of any assembler routines to seven characters or less (public symbol limitation of the Microsoft assemblers!), or REL.MCD cannot figure out where to assign entry points. - Case does not seem to be an issue. I defined all entry points in upper case, in the assembly code, and in mixed case in the definition module. No problems occured, although if you have a procedure Foo and FOO in the same program it may get confused! Interrupt-driven Task Switching This was another venture into the great unknown of incomplete documentation. After digesting some half-dozen books on Z80 assembly language, and at least that many on Modula-2, I discovered an incredible dearth of hard information on interrupts. Most texts on assembly coding gloss over it, or ignore it completely - while the Modula texts sort of mention that it 'can be done but you really don't need to know about it'. After many days of frustration, during which I became intimately aquainted with the reset button on the target machine, I figured out how it 'can be done'. The Z80 has three modes of interrupt response, IM0, IM1, and IM2. On reset, the CPU comes up with non-maskable interrupts disabled, and in mode 0. Your BIOS may have other ideas on the mode, so be **careful** with any assumptions here! In the case of our target machine, the BIOS made no use of interrupts whatever, and executed a DI instruction (no interrupts) on warm-boot. Z80 mode 0, and mode 2, expect a portion of the interrupt vector to appear on the data buss, generally placed there by the interrupting device. Our clock had no means of doing so, and these methods were subsequently ruled out. Mode 1 seemed to fit the bill; it causes a transfer to the vector at 38h (RST 7), pushing the address of the next pending instruction on the stack before doing so (no information is needed from the data buss). The TM2 manual points out that the programmer is responsible for setting up the correct interrupt mode, and indicates that the IOTRANSFER procedure will initialize the interrupt vector. Unfortunately, it does not bother to mention what interrupt mode it is assuming! A bit of investigation disclosed that IOTRANSFER places a one- word vector to the interrupt code at the address specified in it's third calling parameter (see the manual!). That's all, just the address, no jump instruction. This seemed to imply that TM2 was designed to operate with Mode 2, using vectored interrupts. After some head-scratching, I realized that it would be possible to create a short section of code ORG'ed at 38h, then bias the vector passed to IOTRANSFER, causing it to patch the interrupt address into a 'JP' (or 'CALL') instruction in the stub. This would then enable the use of Mode 1 interrupts. The files INTHANDL (.def and .mod), and MAIN.MOD illustrate one method for doing this, along with a suggested method for setting up an interrupt-driven co-routine. Also, some handy primitives appear in the CLOCKIO.MAC listing. Once the IOTRANSFER vector has been established, clock interrupts cause immediate transfer of control to the instruction following the IOTRANSFER statement. TM2 saves all registers and, more critically, resets it's stack and heap to opposite ends of the 'workspace' established for it. Any references to static objects (at the module level) will be valid, and any Modula procedures may be re-entered at will if they rely upon local parameters (remember you will permanently affect statics!). The function DISPOSE will operate properly for dynamic variables in the non- interrupt heap, however NEW will grab it's space from the (probably limited!) interrupt heap. The previous fact will hasten a fatal stack collision, if not minded carefully! Although TM2 routines may be re-entered, under the defined conditions, CP/M cannot. As an interrupt may occur from within the operating system, one must learn to think of the o/s as a sleeping giant - not to be disturbed. It is assumed that any time-sensitive BIOS code would lock out interrupts completely. In our controller, we used our own handlers to 'talk' to the hardware peripherals, avoiding the o/s completely! Just remember that the foreground task needs to resume from the interrupt as if nothing had occured. Anything that does occur to static data (or peripheral devices) must be thought through carefully, or unexpected side-effects will manifest themselves. Delightfully, TM2 takes care of all housekeeping with regard to which modules may receive interrupts. Generally the main module will not be a 'monitor', ie. it will have interrupts enabled. This is the normal state for a TM2 module. If one desires to lock out interrupts, the IMPLEMENTATION module is declared like this: IMPLEMENTATION MODULE NoInterruptsAllowed[n]; Any postive integer between 1 and 9 may be used, all will have the effect of locking out interrupts within the module. Any non- monitor modules called from the above module inherit it's interrupt status. Unless your interrupt scheme is re-entrant, it is suggested that the interrupt handler loop be in a module which declares as a monitor. Naturally, one may use the Z80EI and Z80DI routines in my CLOCKIO code. Any mayhem created is then your responsibility! Other Subjects! Version control can easily become a headache in a large project. One is always able to remember the chronological sequence of compilation when only a few modules are in existance. My project grew to 30+ modules, and tracking the effects of definition module changes became a nightmare. This deserves a whole tirade to itself: For the past year and a half I have made extensive use of Plu- Perfect software's DateStamper program. It is one of those utilities that, once you use it, you can't believe you ever got along without it. Theoretically one could simply look for and re-compile all modules in a given project which post-dated the modified definition module (being sure to note it's date-stamp prior to changing it!). Wrong. For reasons which are still under investigation, TM2 breaks so many rules of civilized CP/M behavior that the DateStamper becomes absolutely unreliable. From what I have been able to determine, the editor (at least) will open a file with a specific FCB referenced, and then close it with a reference to an FCB in a different location. Whether it moves the FCB, copies it, or whatever, is unknown. This causes no problems with CP/M, however DateStamper is unable to track this sort of thing, and files will not reliably get their 'modify' stamp updated! This leads to files with blank modify fields, although they have a create date (for the most part, more later). A little digression is necessary here. I do most of my development programming on a one-meg ramdisk, moving all the work files (and the compiler) to it on startup. Periodically during a day's work, my usual habit is to run DateSweep, selecting for all files modified that day, and copy them to a back-up diskette. DateSweep will only copy a file if the target disk either doesn't contain that file or, if it does contain it with an earlier datestamp. If no modify stamp is present on source and target, it will not copy it at all. This seemed to prevent the use of DateSweep. A quick inspection led me to believe that the 'create' datestamp was being handled correctly, and I contacted Bridger Mitchell to see if a mod could be made to DateSweep, causing it to use the later of 'Create' or 'Modify'. Bridger responded promptly with a version which did this, asking me to Beta test it.. At first everything seemed to go well, files which had been updated were backed up according to their create dates before shutting off the computer (and killing the Ramdisk) - I thought. About one day into this method, I was rudely made aware that source and object code which had been de-bugged were exhibiting old problems again. Once all the moaning and groaning had subsided (and hardcopy of the correct code was typed in manually), I began a full-scale investigation of this phenomena. The bottom line is BE CAREFUL!!. TM2 is extremely erratic in it's interface with DateStamper. Sometimes all goes well, and the create date jives with when the source or object code originated. Sometimes the output file has no date at all, sometimes it inherits an invalid date from the previous incarnation of the file... phooey! This is definitely attributable to the compiler. I have not had one bit of such misbehavior through the entire course of previous development projects. ZRDOS's archive function does detect writes to files, so I am forced to resort to a kludgy method. The archive bit on all ramdisk files is set during setup. Before backup, I examine them manually to check for valid stamps. This requires keeping an accurate log of all changes, and the time which one performed them at! Why don't I just use the AC utility to copy any changed files? AC does not copy the stamp, and it will make it seem as if all modified files were copied at the same time, no use for tracking compilation order. If versions get out-of-sync on a many moduled project, it's a bear! The FTL compiler is supplied with a poorly documented and buggy utility called PRECEDEN.COM. After some modification, I have found it useful in creating the re-compilation sequence for any given definition file which becomes changed. Workman's manual states that 'the supplied utility programs may be freely distributed as linked programs'. This seems fairly generous, and I will be uploading the corrected object code for these files (with instructions) in the near future. I am not expecting any change in TM2's file protocol, but Bridger has a copy of it and if anyone can create a workaround, he can. If the reader has had the patience to stay with this so far, please advise me of any areas demanding additional explanation. If I have mis-represented any aspect of the above subjects, bring this to my attention also. I check in regularly with ZNODE Central, Downspout, Lilliput (System 1), and Sage's boards. New facts brought to light will help all of us! Good Modularity! Steve