Introduction. 8080.CNV - Listing. Header. The First Pass. DB's, DC's, DW's. The Second Pass. Data on Second Pass. Consult Symbol Table. Registers and Instructions. File Writing Functions. Main Program: Patterns. Main Program: Skeletons. Main Program: Rule Set. HEXEH.CNV - Listing. HEXEH.CNV - Annotated. :Introduction. This HELP file presents a detailed analysis of 8080.CNV, an assembler for the Intel 8080 written in CNVRT. Partly it shows how to write an assembler for one of the 8-bit microcomputers, and partly it is intended to explain the workings of the CNVRT language through a practical example. Just how practical the example might be could be debated after seeing the final binary program, or how slowly it runs. All such things are relative, but in size and speed it will not soon replace ASM.COM or some similar assembler. That the program was written in two weeks and that it is rapidly modifiable to situations that were hardly forseen when Digital Research's ASM.COM was written, will cast a different light on the evaluation. For instance, it is no trouble to modify the program to transform a program written for the Intel 8080 into one suitable for the Intel 8086. The organization of the file is such that 8080.CNV is first reproduced exactly, touched up slightly to match it up with the 22 line panels of HELP.COM. Then, each subroutine or related group of subroutines is analyzed, both for its part in assembly process, and the way it illustrates CNVRT's programming style. 8080.CNV does not produce an Intel HEX file as output so an additional program, HEXEH.CNV, is shown which completes the required formatting. - The knowledgable student of assemblers will detect quite a few refinements required to make 8080.CNV fully compatible with, and interchangable with, ASM.COM or a similar assembler. These are quite literally left as exercises for the user; the purpose of 8080.CNV is to give dual insight into assemblers and into CNVRT. Making some extensions of 8080.CNV will allow one to measure whether this insight has been acquired. - :[8080.CNV] [Harold V. McIntosh, 5 April 1984] [Assembler for Intel 8080 instruction set] [Instructions must be lower case; symbols may be upper or lower case or mixed, but must be consistent. The source file FILE.ASM will produce an unformatted hex file FILE.UFH, and an annotated list file, FILE.PRN. The program HEXEH.REC can be used to transform the unformatted hex file to a normal hex file, FILE.HEX.] [Assembler directives recognized include: org, end, equ, db, dc, dw and ds. They should only be written in lower case.] [Sums, differences, and negatives may form part of symbolic fields, but references must be complete and may not be circular. Don't use , then later, ! Undefined symbols are taken as 0000H.] [Exclude CTR DIR RST BIO] [[Intel 8080 Assembler]] [-] [first pass] (()()(0)( (<>,); (<0>,(x,(R))): )) a (()()(1 2)( [quit at END] (end,); [quit at EOF] ((^Z),(, (%T,No END line) )); [skip comment] ((or,;,<>),(R)): [equ] (<:1:>equ<:2:>,(%W,MEM:ST,(%T,<1>:(r,<2>)::))(R)): [record labels] ((and,<:F:>:,<1>:)<2>,(%W,MEM:ST,(%T,<1>:<0>::))<2>): [left white] (<:H:><1>,<1>): [3-byte inst] (<:M:><:H:>,(#f,<0>+3)); [2-byte inst] (<:N:><:H:>,(#f,<0>+2)); [db] (db<:H:><1>,(#f,<0>+(y,<1>))); [dc] (dc<:H:><1>,(#f,<0>+(y,<1>))); [dw] (dw<:H:><1>,(#f,<0>+(z,<1>))); [ds] (ds<:2:>,(#+,<0>+(r,<2>))); [org] (org<:2:>,(r,<2>)); [1-byte inst] ((or,<:O:>,<:P:>,<:Q:>,<:U:>,<:V:>),(#f,<0>+1)); [other] (<1>,(, (%T,unknown <1>) )(#f,<0>+3)); )) x [-] [count db] (()()(0 1)( (<'><'><'><1>,(#+,(y,<'><1>)+1)); (<'><'><:Y:><1>,<1>): (<'><0><'><1>,(#+,(&!,<0>)+(y,<'><'><1>))); ((or,<:F:>,<:E:>,<:C:>)<:Y:><1>,(#+,(y,<1>)+1)); (<>,0); (<1>,(, (%T,syntax error in db: <1>) )0); )) y [count dw] (()()(0)( ((or,<:F:>,<:E:>,<:C:>)<:Y:><0>,(#+,(z,<0>)+2)): (<>,0); (<0>,(, (%T,syntax error in dw: <0>) )0); )) z [-] [second pass] (()()(0 4)( (<>,); (<0>,(if,(R),<4>,(u,<4>))): )) b (()()(1 2 3)( [END w/start] (end<:2:>,(if,%(&Dh,(c,<2>)),<3>,(X))); [quit at END] (end,(if,%0000,<3>,(X))); [quit at EOF] ((^Z),(, (%T,No END line) )); [skip comment] ((or,;,<>),(if, ,<3>,(X))<0>); [ignore equ] (<:1:>equ<:2:>,(if, ,<3>,(X))<0>); [discard label] (<:F:>:<2>,<2>): [left white] (<:H:><1>,<1>): [db] (db<:H:><1>,(if,(v,<1>),<3>,(X))(#f,<0>+(y,<1>))); [dc] (dc<:H:><1>,(if,(o,(v,<1>)),<3>,(X))(#f,<0>+(y,<1>))); [dw] (dw<:H:><1>,(if,(w,<1>),<3>,(X))(#f,<0>+(z,<1>))); [ds] (ds<:2:>,(if,(#+,<0>+(c,<2>)),<1>,(if,^(&Dh,<1>),<3>,(X))<1>)); [org] (org<:2:>,(if,(c,<2>),<1>,(if,^(&Dh,<1>),<3>,(X))<1>)); [lxi] (lxi<:H:><1><,><2><:Z:>,<< >>(if,(q,0+(i,<1>)+1)(s,<2>),<3>,(X))<< >>(#f,<0>+3)); [3-byte inst] ((and,<:M:>,<1>)<:H:><2><:Z:>,<< >>(if,(l,<1>)(s,<2>),<3>,(X))<< >>(#f,<0>+3)); [-] [mvi] (mvi<:H:><1><,><2><:Z:>,<< >>(if,(q,0+(e,<1>)+6)(t,<2>),<3>,(X))<< >>(#f,<0>+2)); [2-byte inst] ((and,<:N:>,<1>)<:H:><2><:Z:>,<< >>(if,(m,<1>)(t,<2>),<3>,(X))<< >>(#f,<0>+2)); [moves] (mov<:H:><1><,><2><:Z:>,<< >>(if,(q,1+(e,<1>)+(e,<2>)),<3>,(X))<< >>(#f,<0>+1)); [arit-logic] ((and,<:P:>,<1>)<:H:><2><:Z:>,<< >>(if,(q,2+(f,<1>)+(e,<2>)),<3>,(X))<< >>(#f,<0>+1)); [1-byte w/reg] ((and,<:O:>,<1>)<:H:><2><:Z:>,<< >>(if,(n,<1>),<3>,(X))<< >>(#f,<0>+1)); [cndl ret] (r(and,<:L:>,<1>)<:H:>,<< >>(if,(q,3+(h,<1>)+0),<3>,(X))<< >>(#f,<0>+1)); [accumulator] ((and,<:U:>,<1>)<:H:>,<< >>(if,(q,0+(j,<1>)+7),<3>,(X))<< >>(#f,<0>+1)); [-] [1-byte inst] ((and,<:Q:>,<1>)<:H:>,<< >>(if,(k,<1>),<3>,(X))<< >>(#f,<0>+1)); [other] (<1>,<< >>(if,000000,<3>,(X))<< >>(, (%T,unknown <1>) )<< >>(#f,<0>+3)); )) u [-] [compile db] (()()(1 2)( (<'><'><'><1>,(&n,<'>)(v,<'><1>)); (<'><'><:Y:><1>,<1>): (<'><1><'><2>,(&n,<1>)(v,<'><'><2>)); ((and,(or,<:F:>,<:E:>,<:C:>),<1>)<:Y:><2>,(t,<1>)(v,<2>)); (<>,); (<1>,(, (%T,syntax error in db: <1>) )); )) v [compile dw] (()()(1 2)(((and,(or,<:F:>,<:E:>,<:C:>),<1>)<:Y:><2>,(s,<1>)(w,<2>));(,);)) w [get symbol value] (()()(0 1 2)( ((and,(or,<:E:>,<:C:>)<>,<0>),(r,<0>)); ((and,<:F:>,<0>)<1>,(T)(d,(S))<1>): (<1>+(and,<:F:>,<0>)<2>,<1>+(T)(d,(S))<2>): (<1>-(and,<:F:>,<0>)<2>,<1>-(T)(d,(S))<2>): (<0>,(#f,<0>)); )) c [-] [search symbol table] (()()(1)(((^Z),(, (%T,Undefined <0>) )0);(<0>:<1>::,<1>);(,(S)):)) d [register equivalents] (()()()((b,0);(c,1);(d,2);(e,3);(h,4);(l,5);(m,6);(a,7);)) e [arith-logic equivalents] (()()()((add,0);(adc,1);(sub,2);(sbb,3);(ana,4);(xra,5);(ora,6);(cmp,7);)) f [immediate equivalents] (()()()((adi,0);(aci,1);(sui,2);(sbi,3);(ani,4);(xri,5);(ori,6);(cpi,7);)) g [condition equivalents] (()()()((nz,0);(z,1);(nc,2);(c,3);(po,4);(pe,5);(p,6);(m,7);)) h [register pair equivalents] (()()()((b,0);(d,2);(h,4);(sp,6);(psw,6);)) i [accumulator instructions] (()()()((rlc,0);(rrc,1);(ral,2);(rar,3);(daa,4);(cma,5);(stc,6);(cmc,7);)) j [sporadic instructions] (()()()((ret,C9);(xchg,EB);(xthl,E3);(pchl,E9); (sphl,F9);(di,F3);(ei,FB);(nop,00);(hlt,76);)) k [three-byte instructions except lxi] (()()(0)((jmp,C3);(call,CD);(shld,22);(lhld,2A);(sta,32);(lda,3A); (j(and,<:L:>,<0>),(q,3+(h,<0>)+2)); (c(and,<:L:>,<0>),(q,3+(h,<0>)+4));)) l [-] [two-byte instructions exceqt mvi] (()()(0)((in,DB);(out,D3);(<0>,(q,3+(g,<0>)+6));)) m [one-byte instructions with register] (()()()((dad,(q,0+(i,<2>)+9)); (ldax,(q,0+(i,<2>)+10)); (stax,(q,0+(i,<2>)+2)); (inx,(q,0+(i,<2>)+3)); (dcx,(q,0+(i,<2>)+11)); (inr,(q,0+(e,<2>)+4)); (dcr,(q,0+(e,<2>)+5)); (pop,(q,3+(i,<2>)+1)); (push,(q,3+(i,<2>)+5)); (rst,(q,3+<2>+7));)) n [mark last byte] (()()(0 1)((<0>(and,<[1]>,<1>)<>,<0>(&s,<1>));)) o [.PRN file] (()()(1 2)( ((^I)(and,<[8]>,<1>)<2>,(%W,(W),(%T, <1>(^MJ)))(^I)<2>): ((and,<[8]>,<1>)<2>,(%W,(W),(%T, (&Dh,<0>) <1>(^I)<4>(^MJ)))(^I)<2>): ((^I)<1>,(%W,(W),(%T, <1>)(^MJ))); (<1>,(%W,(W),(%T, (&Dh,<0>) <1>(^I)<4>)(^MJ))); )) p [join code fields] (()()(0 1 2 3)( (<0>+<1>+<2>,(if,(&Dh,(#f,<0>*64+<1>*8+<2>)),00<3>,<3>)); )) q [uniformly decimal] (()()(0)( (<0>H,(&Hd,<0>)); )) r [addr fld] (()()(0 1 2)((<0>,(if,(&Dh,(c,<0>)),(and,<[2]>,<1>)<2>,<2><1>));)) s [-] [one byte symbol] (()()(0 1)( (<'><'><'><'>,(&h,<'>)); (<'>(and,<[1]>,<0>)<'>,(&h,<0>)); ((and,(or,<:E:>,<:C:>),<0>),(if,(&Dh,(r,<0>)),00<1>,<1>)); ((and,<:F:>,<0>),(T)(d,(S))): (<0>,(, (%T,bad byte <0>) )00); )) t [-] [main program] (( [alfa] ((and,<[1]>,(or,(IVL,A,Z,),(IVL,a,z,),$,.))) A [num] ((and,<[1]>,(IVL,0,9,))) B [numeral] (<:B:>(ITR,<:B:>)) C [hex] ((and,<[1]>,(or,(IVL,0,9,),(IVL,A,F,),(IVL,a,f,)))) D [hexmal] (<:B:>(ITR,<:D:>)H) E [alfanum] (<:A:>(ITR,(or,<:A:>,<:B:>))) F [white] ((or, ,(^I))) G [whitespace] ((or,<:G:>(ITR,<:G:>),;<-->,<>)) H [registers] ((or,b,c,d,e,h,l,m,a)) I [pairs] ((or,b,d,h,sp)) J [pairs] ((or,b,d,h,psw)) K [condition] ((or,nz,z,nc,c,po,pe,p,m)) L [3-byte] ((or,lhld,shld,lxi,lda,sta,jmp,call,j<:L:>,c<:L:>)) M [2-byte] ((or,mvi,adi,aci,sui,sbi,ani,xri,ori,cpi,in,out)) N [1-byte] ((or,dad,ldax,stax,inx,dcx,inr,dcr,pop,push,rst)) O [ar-logic] ((or,add,adc,sub,sbb,ana,xra,ora,cmp)) P [sporadic] ((or,ret,xchg,xthl,pchl,sphl,di,ei,nop,hlt)) Q [accumulator] ((or,rlc,rrc,ral,rar,daa,cma,stc,cmc)) U [1-byte] ((or,mov,r<:L:>)) V [separator] ((or,<,>,(ITR,<:G:>)(or,;<-->,<>))) Y [-] [final] ((ITR,<:G:>)(or,;,<>)) Z [label] ((and,<:F:>,<1>)(or,:,<:G:>)(ITR,<:G:>)) 1 [constant] (<:H:>(and,(or,<:E:>,<:C:>),<2>)) 2 )( ((%R,<9>.ASM)) R ((%R,MEM:ST,<-->::)) S ((,(%Or,MEM:ST))) T (<9>.PRN) W ((%W,<9>.UFH,<3>)(p,<3>)) X [-] )(9)( ((PWS)(or),); (<9>(or, ,.,<>),<< >>(%Ow,MEM:ST)<< >>(%Or,<9>.ASM)<< >>(a,0)<< >>(, (%T,First Pass concluded) )<< >>(%C,<9>.ASM)<< >>(%Or,<9>.ASM)<< >>(%Ow,<9>.PRN)<< >>(%Ow,<9>.UFH)<< >>(%Or,MEM:ST)<< >>(b,0)<< >>(, (%T,Second Pass concluded) )<< >>(%E)); )) [end] - :Header. The header of 8080.CNV contains the required features plus a minimum of explanatory comments, namely: name of program author, date descriptive comments exclusion of unwanted runtime subroutines startup message three blank lines The startup message is quite terse because the program is not planned for interactive use and therefore will probably not be used by someone sitting at the console who does not know what it is for. Anyone could, of course, replace it by a message more to their liking. The introductory comments are also terse, but do warn the user about the most likely problems arising from differences between 8080.CNV and ASM.COM. The existence of a HELP file such as this one mitigates the absence of further details. - :The First Pass. The first pass of an assembler is required to size up the source code and thereby assign values to all the labels which have been marked in the program. For a CPU like the Intel 8080, for which instructions do not have alternative short and long forms [some jump or branch instructions in other machines are like that], the process is quite straightforward. Complications may arise if "org" statements or "ds" statements can have symbolic arguments, and depend on quantities not yet defined when the scan of the source code reaches them. It can be even worse when such arguments can refer back and forth to each other and some cycles of iteration are required to resolve their values. An assembler is greatly simplified if the restriction is made, that everything needed to evaluate a symbolic expression has been defined by the time it is reached while running through the first pass; and certainly no later than the first encounter during the second pass. An assembler such as 8080.CNV, which scans the source code line by line without retaining any of it in the memory, is limited in its capacity only by the size of the symbol table available to it. The memory area MEM:ST has been created to hold the symbol table, and is 1K bytes long unless another value is declared in the statement (%Ow,MEM:ST) found in the main program. - Since CNVRT is a symbol manipulation language, there is no limitation other than common sense and practicality on the length or nature of a label which can be entered in the symbol table; likewise almost any formula or expression may be associated with the label. That would be the consequence of a symbolic "equ" because labels would normally be given the value of the "program counter" as they were encountered. Even so the displacement relative to a symbolic value could be carried along - say an earlier "org" had had an unresolved value. The program that we are showing does none of these things; we merely want to make clear that it could. 8080.CNV does not recognize a symbol such as "." or "$" signifying the current value of the program counter. Such symbols find little favor in the assembly language of CPU's which have instructions of variable length. Although all the instructions of the Intel 8080 are rigorously one byte long, they may have one additional data byte or two additional address (or data) bytes, so that in practice there are of variable length ranging from one to three bytes. Rather than use the program counter as a symbol, it suffices to place a label at the current instruction or at one of the surrounding instructions. While occupying space in the symbol table, it eliminates all confusion about which location is specified. It also avoids the error of making a change within the scope of a program counter reference and not changing the displacement. - - [This panel repeats the first pass of the assembler 8080.CNV.] [first pass] (()()(0)( (<>,); (<0>,(x,(R))): )) a (()()(1 2)( [quit at END] (end,); [quit at EOF] ((^Z),(, (%T,No END line) )); [skip comment] ((or,;,<>),(R)): [equ] (<:1:>equ<:2:>,(%W,MEM:ST,(%T,<1>:(r,<2>)::))(R)): [record labels] ((and,<:F:>:,<1>:)<2>,(%W,MEM:ST,(%T,<1>:<0>::))<2>): [left white] (<:H:><1>,<1>): [3-byte inst] (<:M:><:H:>,(#f,<0>+3)); [2-byte inst] (<:N:><:H:>,(#f,<0>+2)); [db] (db<:H:><1>,(#f,<0>+(y,<1>))); [dc] (dc<:H:><1>,(#f,<0>+(y,<1>))); [dw] (dw<:H:><1>,(#f,<0>+(z,<1>))); [ds] (ds<:2:>,(#+,<0>+(r,<2>))); [org] (org<:2:>,(r,<2>)); [1-byte inst] ((or,<:O:>,<:P:>,<:Q:>,<:U:>,<:V:>),(#f,<0>+1)); [other] (<1>,(, (%T,unknown <1>) )(#f,<0>+3)); )) x - [This panel contains comments on the previous panel] [first pass] "a" binds the byte count to <0>, avoiding repetition (()()(1 2)( working variables <1> and <2>, <0> is implicit [quit at END] "end" is the legitimate program terminator [quit at EOF] picking up EOF is an error, but effective [skip comment] ignore null lines of full line comments [equ] equivalences go into symbol table. only decimal or hexadecimal constants allowed [record labels] record each label as found, remove from line [left white] ignore leading whitespace [3-byte inst] <:M:> recognizes all the 3-byte instructions [2-byte inst] <:N:> recognizes all the 2-byte instructions [db] "db" must be analyzed further [dc] "dc" is similar to "db" [dw] "dw" must be analyzed further [ds] "ds" with numerical argument increments counter [org] replace counter by numerical argument of org [1-byte inst] <:O:>,<:P:>,<:Q:>,<:U:>,<:V:> recognize 1-byte [other] anything else is an error )) x - The interposition of the function "a" which calls "x" avoids having to include the program counter on each line in the workspace and then parse it out again in each rule. Otherwise, "x" would not have been used and "a" would have had the argument 0:(R). Each rule would have had to have generated some variant of (#+,<0>+n):(R). There is no reason that the argument of an equ cannot be symbolic. The function "d" which searches the symbol table is set up so that it will continue searching until all symbolic references are satisfied. Final comments and whitespace would have to be stripped off. Since all symbol evaluation is postponed until the second pass, the only requirement is that the first pass would glean all the informaton which the second pass requires. The second pass makes no new definitions. The label parser requires that a label be alphanumerical, followed by a colon. Since equ's are recognized before labels, and equ can have a label which is not followed by a colon. This convention is compatible with M80 (Microsoft's relocating assembler), for which absolute constants must be defined without colons. Obviously, a label within a program would not be considered a constant, even if it were in an absolute segment. - :DB's, DC's, and DW's. [This panel contains the code to count the length of a DB or a DC] [count db] (()()(0 1)( [two quotes count as 1; we always keep the leading quote] (<'><'><'><1>,(#+,(y,<'><1>)+1)); [contents of a quote pair has been exhausted] (<'><'><:Y:><1>,<1>): [use &! to count quote-free ASCII string] (<'><0><'><1>,(#+,(&!,<0>)+(y,<'><'><1>))); [any other argument counts as one byte] ((or,<:F:>,<:E:>,<:C:>)<:Y:><1>,(#+,(y,<1>)+1)); [nothing left, put zero bytes to start the count, quit] (<>,0); [unrecognized parameter truncates paramenter list, reports zero] (<1>,(, (%T,syntax error in db: <1>) )0); )) y [-] [This panel contains the code to calculate the length of a DW] [count dw] (()()(0)( [only symbolic, hexadecimal, or decimal is allowed; each is worth 2 bytes] ((or,<:F:>,<:E:>,<:C:>)<:Y:><0>,(#+,(z,<0>)+2)): [terminate the recursion with a zero] (<>,0); [unrecognizable also terminates the sequence, terminal value is zero] (<0>,(, (%T,syntax error in dw: <0>) )0); )) z - The code of the previous panel, which counts out the space occupied by DB's, DC's, and DW's, originally used the argument <0>:(R) - a combination of the program counter and the line of code to which it applied. It was slower because the repeated joining and splitting of these two components was time consuming. For example, y begins with: (<'><'><'><1>,(#+,(y,<'><1>)+1)); while the former code was: ((<0>:<'><'><'><1>,(#+,<0>+1):<'><1>): The first of these rules uses one variable instead of two. It also consumes less workspace. Dropping one quote at a time while incrementing the program counter is tolerable because long strings of quotes are rarely encountered. Looking at the analogous rule for ordinary text we find: (<'><0><'><1>,(#+,(&!,<0>)+(y,<'><'><1>))); for which the alternative code was (<0>:<'>(and,<[1]>,(NOT,<'>))<1>,(#+,<0>+1):<1>): - The function &!, whose value is the length of its argument string, is a REC runtime function. Still slow compared to machine language, it is much faster than a CNVRT rule. The earlier splitting off of one letter at a time, as the first rule does with quotes, was rather slow. Using &! is much better. According to the general form of a db, common to dc and dw, several arguments can be placed on the same line, separated by commas; the pattern <:Y:> picks out such commas or else a terminal comment or end of line. Each argument, even though it is symbolic or involves a formula, counts for one byte. A series of ASCII bytes can be quoted, but if quotes themselves are included in the string, they are exchanged at the rate of two for one. Thus, 'x''''''y' means there are three quotes between x and y. Three rules account for quotes. Since we always regenerate the leading quote, finding three in succession means that the last two are a pair representing one single quote; we discard them, increase the count by one, and examine the rest of the line. Two quotes followed by a terminator means that the quoted string is exhausted; we go on to what is left. Anything else enclosed between two quotes can be sized with &!. - :The Second Pass. The second pass is more extensive than the first because it is necessary to generate the code for each instruction rather than simply ascertaining its length. Likewise, data must be expanded and not merely sized. The orderly layout of the Intel 8080 instruction set and the selection of a careful set of mnemonics allows the second pass to be subdivided into a relatively small number of categories. In each of them, the necessary registers, conditions, or operation specifications can be transformed into their numerical equivalent, and then the byte of code formed. In the case of data bytes or addresses, the correct number of bytes has to be generated. If necessary, this will require access to the symbol table which was formed on the first pass. Since the second pass will generate either a hexadecimal object code file or an annotated listing file or both, it is necessary to direct this information to the corresponding file. A lead-in is used here with the dual function of preserving the simulated program counter in a variable bound at this higher level, and conserving the entire assembler line so that it can be echoed in the annotated listing. The configuration (if,(R),<4>,(u,<4>)) is a typical way to do the latter. - The second pass is best understood relative to the output which it produces. Here we want to generate two separate files - FILE.UFH bearing the precursor of the Intel HEX file FILE.HEX - and an annotated reproduction of the source file indicating the code that was generated. This file is FILE.PRN. The .PRN file is most attractive if the generated code is placed on the same line as the source code, and if it does not take up too much space. Eleven columns are the minimum necessary, but if we choose sixteen we can insert a little more information at times, and not disrupt the original tab settings. The following format is used, taking AAAA as the current program counter: AAAA ;comment... comment line AAAA FB x: ei ;one-byte instruction AAAA FE42 x: cpi 'B' ;two-byte instruction AAAA CD0500 x: call BDOS ;three-byte instruction AAAA 414243 x: db 'ABC' ;byte data AAAA ^BBBB org BBBB ;origin AAAA ^BBBB ds N ;BBBB=AAAA+N AAAA %EEEE end EEEE ;end line The use of ^ and % facilitates the generation of the .UFH file by including an additional disk output while the .PRN file is being written. - The "unformatted hexadecimal file," FILE.UFH is simply a byte stream deposited on the disk as the second pass procedes. A loader can be constructed to change it into a .COM file, or it can be transformed to the traditional .HEX file. It is unformatted for simplicity; otherwise a buffer would have to be introduced to break up the stream into lines, insert origins, checksums, colons and the carriage return-line feed combinations that the .HEX file requires. Deferring all this activity to another pass by another program, 8080.CNV is simplified somewhat. Aside form the byte stream, FILE.UFH contains the symbols ^ to indicate an origin, blanks from comment lines, and % to indicate the start address found in an "end" line. The rules for all the different instruction formats use the function "q" which forms a byte from the three fields of the typical Intel 8080 instruction. The byte so formed is bound to the variable <3> where it can be sent to either FILE.PRN or FILE.UFH or both. Just as in the first pass, the value of the function "u" is the value of the program counter, which is to be bound to the variable <0>, from which it may be incorporated into FILE.PRN. The next 5 panels contain the second pass for the assembler 8080.CNV. Many explanatory comments have been interleaved. - [second pass] [the function "b" binds the program counter to <0>, the source line to <4>, using an "if" to keep their passages through the workspace distinct. If "u" returns a null rather than a new program counter, the pass is ended.] (()()(0 4)( (<>,); (<0>,(if,(R),<4>,(u,<4>))): )) b ["b" is the leadin, "u" does the work, of the second pass.] (()()(1 2 3)( [the end line may have a start address; mark the address with a "%"] [END w/start] (end<:2:>,(if,%(&Dh,(c,<2>)),<3>,(X))); [a simple "end" implies a start address 0000, marked with a %] [quit at END] (end,(if,%0000,<3>,(X))); [EOF should never be encountered by the program; if it is, quit] [quit at EOF] ((^Z),(, (%T,No END line) )); [-] [null lines or comments occupying a full line are ignored] [skip comment] ((or,;,<>),(if, ,<3>,(X))<0>); [equ's might be reevaluated; but here they are ignored, their work done] [ignore equ] (<:1:>equ<:2:>,(if, ,<3>,(X))<0>); [reevaluating a label might expose a phase, or pass, error. We ignore them] [discard label] (<:F:>:<2>,<2>): [left tabs, spaces are ignored, other things (even numbers) are not] [left white] (<:H:><1>,<1>): [recompute space for db for the listing, put bytes in the output] [db] (db<:H:><1>,(if,(v,<1>),<3>,(X))(#f,<0>+(y,<1>))); [dc is same as db, but the last byte has to be flagged] [dc] (dc<:H:><1>,(if,(o,(v,<1>)),<3>,(X))(#f,<0>+(y,<1>))); [dw is similar to db, dc, but each argument counts 2, we don't use quotes] [dw] (dw<:H:><1>,(if,(w,<1>),<3>,(X))(#f,<0>+(z,<1>))); [-] [ds increments the counter; we mark new counter with "^" for hexfile] [ds] (ds<:2:>,(if,(#+,<0>+(c,<2>)),<1>,(if,^(&Dh,<1>),<3>,(X))<1>)); [mark the origin with a "^" to give an origin to the hexfile] [org] (org<:2:>,(if,(c,<2>),<1>,(if,^(&Dh,<1>),<3>,(X))<1>)); [lxi needs a register pair, plus two data bytes] [lxi] (lxi<:H:><1><,><2><:Z:>,<< >>(if,(q,0+(i,<1>)+1)(s,<2>),<3>,(X))<< >>(#f,<0>+3)); [other three byte instructions also need a two byte address] [3-byte inst] ((and,<:M:>,<1>)<:H:><2><:Z:>,<< >>(if,(l,<1>)(s,<2>),<3>,(X))<< >>(#f,<0>+3)); [mvi requires a register, plus a data byte] [mvi] (mvi<:H:><1><,><2><:Z:>,<< >>(if,(q,0+(e,<1>)+6)(t,<2>),<3>,(X))<< >>(#f,<0>+2)); [-] [the other two-byte instructions require their data byte] [2-byte inst] ((and,<:N:>,<1>)<:H:><2><:Z:>,<< >>(if,(m,<1>)(t,<2>),<3>,(X))<< >>(#f,<0>+2)); [moves require two registers] [moves] (mov<:H:><1><,><2><:Z:>,<< >>(if,(q,1+(e,<1>)+(e,<2>)),<3>,(X))<< >>(#f,<0>+1)); [arithmetic-logic instructions require operation, register] [arit-logic] ((and,<:P:>,<1>)<:H:><2><:Z:>,<< >>(if,(q,2+(f,<1>)+(e,<2>)),<3>,(X))<< >>(#f,<0>+1)); [some one-byte instructions require a register] [1-byte w/reg] ((and,<:O:>,<1>)<:H:><2><:Z:>,<< >>(if,(n,<1>),<3>,(X))<< >>(#f,<0>+1)); [-] [conditional return requires the condition code] [cndl ret] (r(and,<:L:>,<1>)<:H:>,<< >>(if,(q,3+(h,<1>)+0),<3>,(X))<< >>(#f,<0>+1)); [there are eight "accumulator" instructions] [accumulator] ((and,<:U:>,<1>)<:H:>,<< >>(if,(q,0+(j,<1>)+7),<3>,(X))<< >>(#f,<0>+1)); [check for the remnants] [1-byte inst] ((and,<:Q:>,<1>)<:H:>,<< >>(if,(k,<1>),<3>,(X))<< >>(#f,<0>+1)); [insert 3 NOP's for unclassifiable lines] [other] (<1>,<< >>(if,000000,<3>,(X))<< >>(, (%T,unknown <1>) )<< >>(#f,<0>+3)); )) u - :Data on Second Pass. [This panel contains code to evaluate the arguments of a db, dc, or dw] [compile db] (()()(1 2)( (<'><'><'><1>,(&n,<'>)(v,<'><'><1>)); (<'><'><:Y:><1>,<1>): (<'><1><'><2>,(&n,<1>)(v,<'><'><2>)); ((and,(or,<:F:>,<:E:>,<:C:>),<1>)<:Y:><2>,(t,<1>)(v,<2>)); (<>,); (<1>,(, (%T,syntax error in db: <1>) )); )) v [compile dw] (()()(1 2)( ((and,(or,<:F:>,<:E:>,<:C:>),<1>)<:Y:><2>,(s,<1>)(w,<2>)); (<>,); (<1>,(, (%T,syntax error in dw: <1>) )); )) w - :Consult Symbol Table. Consultation of the symbol table is straightforward. If the argument is either decimal or hexidecimal, it is converted to decimal and returned. If it is symbolic, the symbol table is searched, the equivalent extracted, and the cycle repeated. Thus, there could be a chain of definitions, which would be acceptable provided that it were 1) not circular and 2) complete. Simple formulas are allowed - that is, a chain of sums and differences. Neither parentheses nor other operations are permitted, but a leading unary minus is recognized. Since the function #f will parse any algebraic argument, it would not be difficult to ensure the evaluation of all symbolic references, and thus derive a numerical value for any algebraic expression at all. Functions S and T, defined as skeletons in the main program, are responsible for extracting information from the symbol table. (T) opens the memory area for reading. It it checks to see whether the area was previously defined, and resets its pointers to zero. An area can be reopened as often as desired. (S) uses a pattern read according to which the memory area alternates the symbol name with its value. Either name or value can be arbitrarily long, within the constraint of fixed total area being available. - [This panel contains "c" and "d," which search the symbol table] [get symbol value] (()()(0 1 2)( ((and,(or,<:E:>,<:C:>)<>,<0>),(r,<0>)); ((and,<:F:>,<0>)<1>,(T)(d,(S))<1>): (<1>+(and,<:F:>,<0>)<2>,<1>+(T)(d,(S))<2>): (<1>-(and,<:F:>,<0>)<2>,<1>-(T)(d,(S))<2>): (<0>,(#f,<0>)); )) c [search symbol table] (()()(1)( ((^Z),(, (%T,Undefined <0>) )0); (<0>:<1>::,<1>); (,(S)): )) d - :Registers and Instructions. The part of an assembler concerned with translating the instructions of a given CPU is essentially a table of equivalences for these instructions. If, as in the case of the Intel 8080, the mnemonics are grouped into families with a simple syntax governing register assignments, condition codes, or other parameters, the size of the table can be considerably reduced. Rather than 256 distinct possibilities, we have to contend with about eleven. Each of these uses the same lists of equivalents, governed by its own requiremenus. Even the instructions which do not fit into a particular pattern, of which there are not many, can be gathered ioto their own short list and treated as special cases. The organization of CNVRT requires that most of this information be repeated twice, once in the form of the lists of alternatives required for pattern recognition, and once as a series of programs which translate the symbols into their numerical equivalents. The translation programs are shown it the following panels, the corresponding patterns are incorporated in the main program as a series of pattern definitions. - Index to register, condition, and instruction lists. list pattern equivalent ---- ------- ----------- registers <:I:> (e,...) pairs q0 <:J:> (i,...) pairs q3 <:K:> (i,...) condition <:L:> (h,...) 3-byte <:M:> (l,...) 2-byte <:N:> (g,...), (m,...) 1-byte <:O:> (n,...) ar-logic <:P:> (f,...) sporadic <:Q:> (k,...) accumulator <:U:> (j,...) mov, c-ret <:V:> - [The following panels show the register definitions] [register equivalents] (()()()((b,0);(c,1);(d,2);(e,3);(h,4);(l,5);(m,6);(a,7);)) e [arith-logic equivalents] (()()()((add,0);(adc,1);(sub,2);(sbb,3);(ana,4);(xra,5);(ora,6);(cmp,7);)) f [immediate equivalents] (()()()((adi,0);(aci,1);(sui,2);(sbi,3);(ani,4);(xri,5);(ori,6);(cpi,7);)) g [condition equivalents] (()()()((nz,0);(z,1);(nc,2);(c,3);(po,4);(pe,5);(p,6);(m,7);)) h [register pair equivalents] (()()()((b,0);(d,2);(h,4);(sp,6);(psw,6);)) i [accumulator instructions] (()()()((rlc,0);(rrc,1);(ral,2);(rar,3);(daa,4);(cma,5);(stc,6);(cmc,7);)) j [sporadic instructions] (()()()((ret,C9);(xchg,EB);(xthl,E3);(sphl,F9);(pchl,E9);(di,F3);(ei,FB);)) k - [three-byte instructions except lxi] (()()(0)( (jmp,C3);(call,CD);(shld,22);(lhld,2A);(sta,32);(lda,3A); (j(and,<:L:>,<0>),(q,3+(h,<0>)+2)); (c(and,<:L:>,<0>),(q,3+(h,<0>)+4)); )) l [two-byte instructions exceqt mvi] (()()(0)( (in,DB);(out,D3);(<0>,(q,3+(g,<0>)+6)); )) m [one-byte instructions with register] (()()()( (dad,(q,0+(i,<2>)+9)); (ldax,(q,0+(i,<2>)+10)); (stax,(q,0+(i,<2>)+2)); (inx,(q,0+(i,<2>)+3)); (dcx,(q,0+(i,<2>)+11)); (inr,(q,0+(e,<2>)+4)); (dcr,(q,0+(e,<2>)+5)); (pop,(q,3+(i,<2>)+1)); (push,(q,3+(i,<2>)+5)); (rst,(q,3+<2>+7)); )) n - :File Writing Functions. [dc requires flagging the sign in the last byte of its argument] [mark last byte] (()()(0 1)((<0>(and,<[1]>,<1>)<>,<0>(&s,<1>));)) o [Generate the line which will be sent to the print file. Insert program counter, hexadecimal value of the instruction, and echo the source line] [.PRN file] (()()(1 2)( ((^I)(and,<[8]>,<1>)<2>,(%W,(W),(%T, <1>(^MJ)))(^I)<2>): ((and,<[8]>,<1>)<2>,(%W,(W),(%T, (&Dh,<0>) <1>(^I)<4>(^MJ)))(^I)<2>): ((^I)<1>,(%W,(W),(%T, <1>)(^MJ))); (<1>,(%W,(W),(%T, (&Dh,<0>) <1>(^I)<4>)(^MJ))); )) p [Intel 8080 instructions follow the format 2+3+3, signifying "quadrant," destination, source for each instruction. "q" places them in a byte] [join code fields] (()()(0 1 2 3)( (<0>+<1>+<2>,(if,(&Dh,(#f,<0>*64+<1>*8+<2>)),00<3>,<3>)); )) q - [Make sure that all constants are decimal; convert hexidecimals] [uniformly decimal] (()()(0)( (<0>H,(&Hd,<0>)); )) r [reverse the bytes of an address field so that low order comes first] [addr fld] (()()(0 1 2)( (<0>,(if,(&Dh,(c,<0>)),(and,<[2]>,<1>)<2>,<2><1>)); )) s [make sure that a one-byte symbol is just one byte long. Translate quotes] [one byte symbol] (()()(0 1)((<'><'><'><'>,(&h,<'>)); (<'>(and,<[1]>,<0>)<'>,(&h,<0>)); ((and,(or,<:E:>,<:C:>),<0>),(if,(&Dh,(r,<0>)),00<1>,<1>)); ((and,<:F:>,<0>),(T)(d,(S))): (<0>,(, (%T,bad byte <0>) )00); )) t - :Main Program: Patterns. [The following shows the main program with syntactic definitions] [main program] (( [The ASM alphabet contains upper & lowercase letters, numerals, $ and .] [alfa] ((and,<[1]>,(or,(IVL,A,Z,),(IVL,a,z,),$,.))) A [digits are just 0,1,2,3,4,5,6,7,8,9,0] [num] ((and,<[1]>,(IVL,0,9,))) B [a numeral is a sequence with at least one digit] [numeral] (<:B:>(ITR,<:B:>)) C [hexadecimals are numerals or a,b,c,d,e,f of either case shift] [hex] ((and,<[1]>,(or,(IVL,0,9,),(IVL,A,F,),(IVL,a,f,)))) D [hexadecimal number must begin with a digit and end with an uppercase H] [hexmal] (<:B:>(ITR,<:D:>)H) E [alphanumeric means leading letter followed by letters or digits] [alfanum] (<:A:>(ITR,(or,<:A:>,<:B:>))) F [-] ["whitespace" is either a space or a tab] [white] ((or, ,(^I))) G [fields end with whitespace, comments, or at the end of the line] [whitespace] ((or,<:G:>(ITR,<:G:>),;<-->,<>)) H [the preceding was general syntax. The following is specific to the Intel 8080] [list of the Intel 8080's eight registers] [registers] ((or,b,c,d,e,h,l,m,a)) I [the four register pairs of quadrant 0] [pairs] ((or,b,d,h,sp)) J [the four register pairs of quadrant 3] [pairs] ((or,b,d,h,psw)) K [the eight conditions recognized by jumps, calls, returns] [condition] ((or,nz,z,nc,c,po,pe,p,m)) L [-] [list of the mnemonics for 3-byte instructions] [3-byte] ((or,lhld,shld,lxi,lda,sta,jmp,call,j<:L:>,c<:L:>)) M [list of the mnemonics for 2-byte instructions] [2-byte] ((or,mvi,adi,aci,sui,sbi,ani,xri,ori,cpi,in,out)) N [list of mnemonics for 1-byte instructions with register or data byte] [1-byte] ((or,dad,ldax,stax,inx,dcx,inr,dcr,pop,push,rst)) O [list of mnemonics for arithmetic-logical instructions, 1-byte w/register] [ar-logic] ((or,add,adc,sub,sbb,ana,xra,ora,cmp)) P [list of mnemonics for one-byte instructions without parameters] [sporadic] ((or,ret,xchg,xthl,pchl,sphl,di,ei,nop,hlt)) Q [list of mnemonics for "accumulator" instructions] [accumulator] ((or,rlc,rrc,ral,rar,daa,cma,stc,cmc)) U [mnemonics for moves, conditional returns] [1-byte] ((or,mov,r<:L:>)) V [-] [the preceding were lists of instructions. The following are specialized] [arguments of db, dc, dw are separated by commas, terminated at comments] [separator] ((or,<,>,(ITR,<:G:>)(or,;<-->,<>))) Y [a line of code may have trailing whitespace or comments, or end abruptly] [final] ((ITR,<:G:>)(or,;,<>)) Z [a label is alphanumeric, followed by : or whitespace; bind it to <2>] [label] ((and,<:F:>,<1>)(or,:,<:G:>)(ITR,<:G:>)) 1 [a constant is decimal or hexidecimal preceded by separation; bind it to <2>] [constant] (<:H:>(and,(or,<:E:>,<:C:>),<2>)) 2 ) [-] :Main Program: Skeletons. ( [read one line, consecutively, from the source file] ((%R,<9>.ASM)) R [read the next pair from the symbol table] ((%R,MEM:ST,<-->::)) S [position the read pointer at the beginning of the symbol table] ((,(%Or,MEM:ST))) T [send one line to the listing file, simultaneously type it at the console] ((%W,<9>.LST,(%T,<0>))) W ) [-] :Main Program: Rule Set. (9)( [echo name of the file to be assembled] ((PWS)(or),); [bind filename to variable <9>, run through program steps] (<9>(or, ,.,<>),<< [open symbol table - beware! only 1024 bytes unless more requested] >>(%Ow,MEM:ST)<< [open source file for first pass] >>(%Or,<9>.ASM)<< [execute first pass] >>(a,0)<< [announce end of first pass] >>(, (%T,First Pass concluded) )<< [close source file, recover overhead belonging to it] >>(%C,<9>.ASM)<< [-] [open source file again for the second pass] >>(%Or,<9>.ASM)<< [open file for the assembly listing] >>(%Ow,<9>.PRN)<< [open file for unformated hex] >>(%Ow,<9>.UFH)<< [%Or, conserves symbol table area, repositions pointer] >>(%Or,MEM:ST)<< [execute the second pass] >>(b,0)<< [announce end of second pass] >>(, (%T,Second Pass concluded) )<< [close all files] >>(%E)); )) [end] [-] :[HEXEH.CNV] [Harold V. McIntosh, 7 April 1984] [Make Intel HEX from UFH] [Exclude CTR DIR RST BIO] [[Intel HEX from UFH]] [lines for .HEX file] (()()(0 1 2)( [suppress spaces] (<0> <1>,<0><1>): [ever filled buffer] (<0>:(and,<1>,(NOT,<[32]>),(NOT,<-->(^Z))),<0>:<1>(R)): [update new origin] (<0>:^(and,<[4]>,<1>)<2>,(&Hd,<1>):<2>): [the final line] (<0>:%(and,<[4]>,<1>),(c,<1>)); [^ terminates line] (<0>:<1>^<2>,(b,<1>)^<2>): [% terminates line] (<0>:<1>%<2>,(b,<1>)%<2>): [quit at EOF] (<0>:(^Z),(c,0000)); [partial line at ^Z] (<0>:<1>(^Z),(b,<1>)(^Z)): [full line] (<0>:(and,<[32]>,<1>)<2>,(b,<1>)<2>): )) a [-] [punch a row] (()()(1 2 3)( ((and,<[32]>,<1>)<2>,<< >>(#+,<0>+16):<< >>(if,10(&Dh,<0>)00<1>,<3>,(W))<< >><2>); (<1>,(if,(&Dh,(#/,(&!,<1>)/2)),00<2>,<< >>(#+,<0>+(&Hd,<2>)):<< >>(if,<2>(&Dh,<0>)00<1>,<3>,(W))<< >>)); )) b [punch closing line] (()()(0 3)((<0>,(if,00<0>00,<3>,(W)));)) c [checksum] (()()(0 1 2)( (<0>:(and,<[2]>,<1>)<2>,(#+,<0>+(&Hd,<1>)):<2>): (<0>:,(if,(&Dh,-<0>),<[2]><1>,<1>)); )) d [-] [main program] (()( ((%R,<9>.UFH,<[32]>)) R ((%W,(X),(%T,:<3>(d,0:<3>))(^MJ))) W (<9>.HEX) X )(9)( ((PWS)(or),); ((or, ,?,<>),<< >>(, (%T,HEXEH.REC will generate FILE.HEX from FILE.UFH) )<< >>(, (%T,Type FILE to be used) )<< >>(&u,(%R,TTY:))): (<9>(or, ,.,<>),<< >>(%Or,<9>.UFH)<< >>(%Ow,<9>.HEX)<< >>(a,0:(R))<< >>(%E)); )) [end] [-] :HEXEH.CNV - Annotated. HEXEH.CNV is a program which will transform the unformatted hexadecimal file produced by 8080.CNV into the standard file with Intel's assembler and PROM programming format. It is not necessary to use this program unless the standard format is desired, or a program which produces a binary .COM file directly is not available. The format of FILE.UFH is that it consists of an unpunctuated stream of printable ASCII characters, as produced by 8080.REC. Interspersed in this stream are spaces resulting from comment lines ^AAAA resulting from org or ds directives %EEEE resulting from an end directive FILE.HEX, which is the output of HEXEH, conforms to an Intel format, :NNAAAA00DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDCC in which NN is the number of bytes generated by the line, AAAA is their initial address, 00 is zero for CP/M usage, DD are hexadecimal bytes in ASCII representation, and CC is a checksum modulo 256 giving zero linesum. - The layout of HEXEH is the following: a keep 32 characters on hand, pick prospective HEX lines out of them, maintain a program counter, and detect the end of the UFH file. b generate one line of HEX code, containing not more than 16 bytes of object code, represented in 32 ASCII nibbles. c punch the terminal line for the HEX file. d calculate a checksum for the line. main open files, generate HEX file, close files. These programs maintain the program counter as part of the workspace, rather than splitting the programs into two levels with the program counter bound into a variable at the top level. - Some programming techniques, peculiar to CNVRT, can be perceived in these subroutines. (if,00<0>00,<3>,(W)) is used for the sole purpose of binding a value to the variable <3>, which is used implicitly in W; for this reason <3> must not have been previously bound at this level, and likewise the "if" has no skeleton for a false alternative. In the skeletons (if,(&Dh,(#/,(&!,<1>/2)),00<2>,...) (if,(&Dh,-<0>),<[2]><1>,<1>) the first uses the pattern 00<2>, to pick out the least significant byte of the two byte number which is always the result of an arithmetic operation. In the second, <[2]> accomplishes the same separation, but is used instead of FF because overflow from one byte can encroach on the FF. The constant shifting between decimal and hexadecimal, represented by &Dh and &Hd, is a consequence of programming CNVRT for decimal arithmetic on ASCII character strings. - [The lines in this panel form the usual header for a CNVRT program, plus the header for the subroutine "a"] [HEXEH.CNV] [Harold V. McIntosh, 7 April 1984] [Make Intel HEX from UFH] [Exclude CTR DIR RST BIO] [[Intel HEX from UFH]] [lines for .HEX file] (()()(0 1 2)( [-] [spaces really shouldn't have been passed on, but get rid of them] [suppress spaces] (<0> <1>,<0><1>): [make sure that there is always enough data for a line of hexfile] [ever filled buffer] (<0>:(and,<1>,(NOT,<[32]>),(NOT,<-->(^Z))),<0>:<1>(R)): [^ requires a new origin] [update new origin] (<0>:^(and,<[4]>,<1>)<2>,(&Hd,<1>):<2>): [% is the normal file end marker] [the final line] (<0>:%(and,<[4]>,<1>),(c,<1>)); [get a line to process, but data cannot extend beyond ^, %, or ^Z] [^ terminates line] (<0>:<1>^<2>,(b,<1>)^<2>): [% terminates line] (<0>:<1>%<2>,(b,<1>)%<2>): [quit at EOF] (<0>:(^Z),(c,0000)); [process tag end of file] [partial line at ^Z] (<0>:<1>(^Z),(b,<1>)(^Z)): [pure data stream in the buffer, so take 16 bytes (32 nibbles) out of it] [full line] (<0>:(and,<[32]>,<1>)<2>,(b,<1>)<2>): )) a [-] [punch a row] (()()(1 2 3)( [if 16 bytes [32 nibbles] are available, generate a full line] ((and,<[32]>,<1>)<2>,<< [increment program counter by 16] >>(#+,<0>+16):<< [bind the line to <3> so W can use it] >>(if,10(&Dh,<0>)00<1>,<3>,(W))<< [repeat if anything is left] >><2>); [given less than a full row, count out its length] (<1>,(if,(&Dh,(#/,(&!,<1>)/2)),00<2>,<< [increment program counter] >>(#+,<0>+(&Hd,<2>)):<< [generate line with program counter and checksum] >>(if,<2>(&Dh,<0>)00<1>,<3>,(W))<< >>)); )) b [-] [punch closing line] (()()(0 3)( [The closing line has a special format, namely the byte count is zero] (<0>,(if,00<0>00,<3>,(W))); )) c [checksum] (()()(0 1 2)( [as long as nibble pairs remain, add them to the checksum] (<0>:(and,<[2]>,<1>)<2>,(#+,<0>+(&Hd,<1>)):<2>): [when they are all gone, report the negative of the accumulated sum] (<0>:,(if,(&Dh,-<0>),<[2]><1>,<1>)); )) d [-] [main program] (()( [read 32 characters at a time from the unformatted file] ((%R,<9>.UFH,<[32]>)) R [add checksum, CR-LF, to the line, send it to HEX file] ((%W,(X),(%T,:<3>(d,0:<3>))(^MJ))) W [avoid repeating file name] (<9>.HEX) X )(9)( [echo filename] ((PWS)(or),); [filename can be input from console, type "?"] ((or, ,?,<>),<< >>(, (%T,HEXEH.REC will generate FILE.HEX from FILE.UFH) )<< >>(, (%T,Type FILE to be used) )<< >>(&u,(%R,TTY:))): [record filename once established, open files, do the job, then quit] (<9>(or, ,.,<>),<< >>(%Or,<9>.UFH)<< >>(%Ow,<9>.HEX)<< >>(a,0:(R))<< >>(%E)); )) [end] - :[8080.HLP] [Harold V. McIntosh, 12 April 1984] [end]