home *** CD-ROM | disk | FTP | other *** search
/ Oakland CPM Archive / oakcpm.iso / cpm / asmutl / meyertut.ark / MEYER08.TXT < prev    next >
Text File  |  1987-12-04  |  17KB  |  425 lines

  1.                      CP/M Assembly Language
  2.                    Part VIII: Numerical Output
  3.                           by Eric Meyer
  4.  
  5.      We already know how to write 8080 code to print or input
  6. ASCII (character) strings. Now we need to learn how to handle
  7. numbers.
  8.      For example, you might want to use the generic FILTER
  9. program of Part VII as the basis for a word-counting utility --
  10. but how are you going to get it to print its results?
  11.      First a little vocabulary.
  12.      A "bit" is a binary digit, either 0 or 1 (on/off).
  13.      A "byte" is 8 bits, representing a number from 0 to 255 (FF
  14. hex).
  15.      A "nibble" (or nybble) is half a byte, or 4 bits, and
  16. corresponds to one hex digit.
  17.      Example: suppose the Accumulator holds the value 121
  18. decimal, or 79 hex. This is:
  19.  
  20.         7     9       hex
  21.      0111  1001       binary
  22.  
  23. (Practice with a good ASCII table or conversion chart will help
  24. you a lot here.)
  25.  
  26.  
  27. 1. Binary versus BCD
  28.      As you can see above, hexadecimal (being a power of 2) is a
  29. relatively easy way to represent the binary values with which the
  30. 8080 CPU deals.
  31.      Decimal isn't.
  32.      If you want to deal with decimal values, there are two
  33. alternatives:
  34.  
  35. 1)  you can write code to calculate decimal values from binary
  36.      for I/O purposes, as we will do here. Or,
  37. 2)  you can switch to a different internal format.
  38.  
  39.      "BCD" (Binary Coded Decimal) is a different way of
  40. representing values in the CPU itself.
  41.      In our earlier example, the value 79 hex in the Accumulator
  42. naturally meant 121 decimal.
  43.      However, you might also interpret it as 79 decimal!
  44.      This is BCD: each nibble is not just a hex digit, but a
  45. decimal digit.
  46.  
  47. Binary:  79 hex = 7*16 + 9 = 121 decimal
  48.    BCD:  79 hex = 7*10 + 9 =  79 decimal
  49.  
  50.      Of course, you need to make special arrangements for
  51. arithmetic to work out right, but the 8080 does this. An
  52. instruction "DAA", or Decimal Adjust Accumulator, is provided to
  53. use after each arithmetic operation.
  54.      BCD has two advantages over straight binary:
  55.  
  56. 1)  it makes decimal I/O as straightforward as hex, and
  57. 2)  most of all, it makes calculations such as division involving
  58.      decimal quantities (like dollars and cents) exact.
  59.  
  60.      But we're not accountants, so we will stick to common binary
  61. arithmetic.
  62.      You should just be aware that BCD is there.
  63.  
  64.  
  65. 2. Multiplication and Division
  66.      There are no multiply or divide instructions on the 8080,
  67. but the operations can be performed by other means.
  68.      The easiest way to multiply is by repeated addition.
  69.      Suppose you want to multiply the value in A by 2.
  70.      All you need to do is add it to itself: "ADD A".
  71.      Then, if you need to multiply by another quantity, say 10,
  72. you can combine additions in ingenious ways:
  73.  
  74. MOV  C,A      ;copy original value into C
  75. ADD  A        ;double it
  76. ADD  A        ;  twice (*4)
  77. ADD  A        ;  3 times (*8)
  78. ADD  C        ;add original (*9)
  79. ADD  C        ;  twice (*10, voila)
  80.  
  81. (If it bothers you that multiplying an integer by 10 takes some
  82. thought, 8080 is the wrong language for you!)
  83.      If you're worried about overflow (the value eventually
  84. exceeding 255) you need to be testing the Carry flag along the
  85. way.
  86.      Similar tricks can be performed with 2-byte values using the
  87. HL register and DAD H.
  88.      This kind of shortcut doesn't work with division, which will
  89. require learning about rotations.
  90.      Our new instruction is "RAR", which Rotates the Accumulator
  91. Right one bit (binary digit). The lowest bit moves off into the
  92. Carry flag, and the old Carry moves into the high bit place.
  93.      That is, labeling the bits 0-7,
  94.  
  95. Before:   A = 7 6 5 4 3 2 1 0    Carry = C
  96. After:    A = C 7 6 5 4 3 2 1    Carry = 0
  97.  
  98.      In numerical terms, this divides the value in A by 2. If
  99. there was any remainder (A wasn't even), it is held in the Carry
  100. flag. Here's a concrete example:
  101.  
  102. Before: A = 01100100 (100 dec, 64 hex) Carry = 0
  103. After:  A = 00110010  (50 dec, 32 hex) Carry = 0
  104.  
  105.      For going the other way, there's also an RAL (Rotate Left)
  106. instruction that works just the same, but ADD A is easier to use.
  107.      Be careful of the effects of the Carry bit when using
  108. rotations -- the safe thing to do is to mask your result with an
  109. ANI operation afterward, to ensure that the "new" bits introduced
  110. are all 0s.
  111.      One rotation divides by two; two, by four; four, by eight;
  112. and so on. Four rotations in a row divide by 16, bringing the
  113. high nibble into the place of the low one, all you need for
  114. hexadecimal I/O.
  115.      Division of 2-byte values has to be done one byte at a time
  116. in the Accumulator using RAR.
  117.      Of course, all this is just multiplying or dividing a
  118. register by a constant!
  119.      If you need to work with two register values, you'll have to
  120. decrement one while manipulating the other as above (it gets
  121. complicated).
  122.  
  123.  
  124. 3. Hexadecimal output
  125.      Handling numbers in hex is easiest because the 8080 CPU is
  126. designed around binary arithmetic: decimal requires translation.
  127.      It's not a bad idea to begin with some simple routines to
  128. handle hex numbers.
  129.      The following set of routines allow you to print out either
  130. one- or two-byte hex values:
  131.  
  132. ;subroutine to print out one hex word from HL
  133. ;register
  134. HEXWRD:  PUSH H         ;save original value
  135.          MOV  A,H       ;get high (first) byte
  136.          CALL HEXBYT    ;show it
  137.          POP  H         ;recover original value
  138.          MOV  A,L       ;get low (second) byte,
  139.                         ;fall thru
  140. ;subroutine to print out one hex byte from A
  141. ;register
  142. HEXBYT:  PUSH PSW       ;save the value in A
  143.          RAR
  144.          RAR            ;rotate right four times
  145.          RAR            ;(brings high nibble to
  146.          RAR            ; right)
  147.          CALL HEXNIB    ;display high nibble
  148.          POP  PSW       ;get original low
  149.                         ;nibble, fall thru
  150. ;subroutine to print out the low hex nibble from
  151. ;A register
  152. HEXNIB:  ANI  0FH       ;mask off anything in
  153.                         ;high nibble
  154.          MVI  C,30H     ;set up amount to add
  155.          CPI  0AH       ;is it less than 10?
  156.          JC   HEXNB1    ;if so OK, go ahead
  157.          MVI  C,37H     ;if not, compensate for
  158.                         ;digits A-F
  159. HEXNB1:  ADD  C         ;add offset, result is
  160.                         ;chr '0'..'F'
  161.          MOV  E,A       ;put it to the screen
  162.          MVI  C,2       ;with BDOS 2
  163.          JMP  BDOS
  164.  
  165.      The basic subroutine HEXNIB is based on the fact that the
  166. ASCII codes for the digits '0' . . '9' are 30 . . 39 (hex), so
  167. all you have to do is add 30 hex to turn a hex nibble 0 . . 9
  168. into the corresponding digit (character).
  169.      Hex digits 'A' . . 'F' are a little more work because
  170. they're not consecutive with '0' . . '9' in the ASCII table.
  171. There are seven other characters in between to skip over.
  172.      Notice how each level builds on the one below (HEXWRD on
  173. HEXBYT, etc), and how each subroutine simply falls through on the
  174. end instead of doing a separate CALL HEX . . ., RET.
  175.      (Remember that such equates as BDOS and routines as SPMSG
  176. can be found in earlier parts of this series.)
  177.  
  178.  
  179. 4. Subroutine style and the stack
  180.      If you're starting to nest subroutines deeply (HEXWRD calls
  181. HEXBYT calls HEXNIB calls BDOS . . .) you need to worry about
  182. running out of stack space.
  183.      Ideally every subroutine would PUSH most or all registers on
  184. entry and POP them on exit, to preserve their contents; and each
  185. would use CALLs ad infinitum.
  186.      In practice this can cause stack overflow, if not properly
  187. managed. This is why the routines above are written as they are:
  188. each preserves only the registers it needs itself.
  189.      And each ends by either falling through or JMPing someplace,
  190. for example
  191.  
  192.      JMP  BDOS  instead of   CALL BDOS
  193.                              RET
  194.  
  195.      Not only is this shorter, it also uses one less PUSH onto
  196. the stack. (Convince yourself that these really are equivalent,
  197. referring to our earlier discussion of the stack. Why remember to
  198. return, when all that's there is a RET?)
  199.      Of course you can avoid stack overflow by setting up a stack
  200. of your own as big as you like; but that's a complication we
  201. haven't ventured into yet.
  202.      For now, we use the default stack handed to us by CP/M, and
  203. we'd better not count on it being more than about 16 PUSHes deep.
  204.  
  205.  
  206. 5. Displaying free memory
  207.      Here is a simple question you can now answer: how much
  208. memory (of the total 64K in your system) is free for user
  209. programs?
  210.      This routine will tell you:
  211.  
  212. FREMEM: CALL SPMSG      ;announce what we're
  213.                         ;doing
  214.         DB   'Bytes free: ',0
  215.         LHLD BDOS+1     ;get the BDOS address
  216.                         ;into HL
  217.         MVI  L,0        ;round down to even page
  218.         DCR  H          ;subtract 100H at bottom
  219.         JMP  HEXWRD     ;say it
  220.  
  221.      The trick is that the BDOS address located at BDOS+1 (0006H)
  222. points close to the beginning of the BDOS in high memory.
  223.      For example, if the BDOS address is DF06, then memory from
  224. DF00 up is filled by the BDOS, but the rest (except from 0000 to
  225. 0100) is free. You will then see a message like
  226.  
  227.          Bytes free: DE00
  228.  
  229.      Admittedly, this is a bit cryptic; you might want an answer
  230. more like "55K", but for that we need decimal output.
  231.  
  232.  
  233. 6. Decimal output
  234.      Of course, most of the time you will want to see results in
  235. decimal form.
  236.      There is no direct way to convert binary (base 2) to decimal
  237. (base 10); you just have to calculate each decimal digit one at a
  238. time, by subtracting powers of 10!
  239.      Here is a basic routine DECOUT that can print a two-byte
  240. value in decimal form.
  241.      For each decimal digit, the routine has to see how many
  242. times 10^n can be subtracted from the value given.
  243.      All digits print; for example 63 shows as "00063".
  244.  
  245. ;subroutine to print decimal value 0-65535 from
  246. ;HL register
  247. DECOUT:  LXI  D,10000  ;figure each of 5 digits
  248.          CALL DECSUB
  249.          LXI  D,1000
  250.          CALL DECSUB
  251.          LXI  D,100
  252.          CALL DECSUB
  253.          LXI  D,10
  254.          CALL DECSUB
  255.          LXI  D,1     ;just fall thru for the
  256.                       ;last
  257. DECSUB:  MVI  C,0     ;initialize count
  258. DSLOOP:  MOV  A,H     ;get high byte of value
  259.          CMP  D       ;compare to 10^n
  260.          JC   DECL    ;if less go here
  261.          JNZ  DECG    ;if greater go here
  262.          MOV  A,L     ;same, have to look at
  263.                       ;low byte too
  264.          CMP  E
  265.          JC   DECL
  266. DECG:    INR  C       ;greater, increment
  267.                       ;count
  268.          MOV  A,L
  269.          SUB  E       ;and subtract 10^n
  270.          MOV  L,A
  271.          MOV  A,H     ;(D,E is subtracted
  272.                       ;from H,L)
  273.          SBB  D       ;note the Borrow
  274.                       ;(carry) here
  275.          MOV  H,A
  276.          JMP  DSLOOP
  277. DECL:    PUSH H       ;less, count is finished
  278.          MVI  A,30H
  279.          ADD  C       ;convert to ASCII
  280.                       ;digit 0..9
  281.          MOV  E,A
  282.          MVI  C,2
  283.          CALL BDOS    ;show it
  284.          POP  H       ;restore remaining value
  285.          RET
  286.  
  287.      A well written program is easy to alter according to the
  288. task at hand. As an exercise, you should be able to modify this
  289. subroutine to:
  290.  
  291. (1)  print spaces instead of lead zeros ("   63")
  292. (2)  ignore lead zeros entirely ("63")
  293. (3)  print 3 digits only (quantities from 0 to 999)
  294. (4)  print out in Octal (base 8) instead of Decimal and about
  295.      anything else you want.
  296.  
  297.      Now we can rewrite our little free memory program above to
  298. give a more intelligible result.
  299.      Just substitute DECOUT for HEXWRD, and you'll see something
  300. like
  301.  
  302.                Bytes free: 56832
  303. or about 55K. (Remember that 1K is 1024 (400H) bytes.)
  304.      If you want to see the result print out as "55K", try
  305. dividing the value in HL by 1024 (that's by 2, ten times) before
  306. printing it.
  307.  
  308.  
  309. 7. The WORDCNT program
  310.      And now what you've all been waiting for: a word counting
  311. program simply reads through a file and counts words as they go
  312. by.
  313.  
  314.      I have begun with our old FILTER.ASM, removed all the output
  315. file code, and changed the "FILTER" subroutine so that it tries
  316. to count words.
  317.  
  318.  
  319. ;*** WORDCNT.ASM word count program
  320. ;
  321. BDOS   EQU  0005H     ;basic equates
  322. FCB1   EQU  005CH
  323. ;
  324.        ORG  0100H     ;programs start here
  325. ;
  326. START: LXI  D,FCB1    ;point to 1st FCB (source
  327.                       ;file)
  328.        CALL GCOPEN    ;open it for reading
  329.        JC   IOERR     ;complain if error
  330. ;
  331. LOOP:  CALL FGETCH    ;get a character
  332.        JC   IOERR     ;complain if error
  333.        CPI  1AH       ;EOF?
  334.        JZ   DONE      ;quit if at end of file
  335.        CALL FILTER    ;process it in some way
  336.        JMP  LOOP      ;keep going
  337. ;
  338. DONE:  CALL SPMSG     ;give result
  339.        DB   'Words: ',0
  340.        LHLD COUNT
  341.        CALL DECOUT
  342. EXIT:  RET            ;all finished
  343. ;
  344. IOERR: CALL SPMSG     ;error? say so
  345.        DB   'IO ERROR',0
  346.        JMP  EXIT      ;and quit
  347. ;
  348. ;HERE IS THE CHARACTER PROCESSING ROUTINE
  349. ;
  350. FILTER: CPI  ' '      ;is it a space?
  351.         JNZ  FILT1    ;if not, do nothing
  352.         LXI  H,LSTCHR ;YES, check last chr
  353.         CMP  M        ;was also a space?
  354.         JZ   FILT1    ;if so, do nothing
  355.         LHLD COUNT    ;if not, END OF WORD
  356.         INX  H        ;increment count
  357.         SHLD COUNT    ;and save it again
  358. FILT1:  STA  LSTCHR   ;save char for reference
  359.         RET
  360. LSTCHR: DB   0        ;1 byte to save last char
  361. COUNT:  DW   0        ;2 byte count starts at
  362.                       ;zero
  363. ;
  364. ;Now add DECOUT from above, and GCOPEN, FGETCH,
  365. ;SPMSG from our original FILTER.ASM
  366.  
  367.      When I first wrote a word count program, I discovered that I
  368. had to decide what a "word" was!
  369.      You will discover that this is not trivial.
  370.      The "FILTER" routine shown above thinks that a "word" is any
  371. series of nonspaces followed by a space. (When it sees a space
  372. after a nonspace, it counts a word.)
  373.      That's not bad; but what about tabs? Carriage returns?
  374. Hyphens? End of file?
  375.      How many "words" does the following paragraph contain:
  376.  
  377.                At exactly 8:00 - not a min-
  378.           ute late -- he collected his mother-
  379.           in-law et cetera at the airport.
  380.  
  381.      (I think the answer is 16; you might disagree. Whatever you
  382. decide, you'll have some work to do on FILTER before it agrees
  383. with you!)
  384.  
  385.  
  386. 8. The Future. . . ?
  387.      We've now successfully covered what I consider the basics of
  388. assembly language programming.
  389.      There are many directions to go from here:
  390.  
  391. *  The rest of the 8080 instruction set
  392. *  The additional features of the Z80; and
  393. *  Almost limitless information about the CP/M operating system's
  394.      use of memory, disk files, and i/o devices that can easily
  395.      be exploited by the assembly programmer.
  396.  
  397.      Your next step (aside from a book or two, if only as a
  398. complete language reference) should be to pick up the source code
  399. to your favorite public domain utility program (XDIR, SD, CRCK,
  400. BYE, MODEM7, etc) and start learning.
  401.      Assembly language has a big drawback (aside from being hard
  402. to write): it is limited to a given family of CPU hardware.
  403.      Assembler code for one chip can't be easily transported to
  404. another (except for relatives like the 8080 and 8086/8), while
  405. well written code in high level languages like C or Pascal can be
  406. used almost without modification.
  407.      The advantage of assembler is efficiency: it produces the
  408. smallest, fastest code.
  409.      Of course this isn't always very important; and it becomes
  410. less so, as faster processors and bigger memory become available.
  411.      Many of the tasks we've used as examples here, such as
  412. filtering and word counting, could have been done far more easily
  413. in a higher level language like C, Pascal, or BASIC, with a very
  414. usable result.
  415.      But there are times when this isn't true: a good modem
  416. program has to be written in assembler.
  417.      Complex graphics, full screen editing, and database sorting
  418. can put you to sleep if the programs aren't written in assembler.
  419.      Understanding and modifying these programs, and of course
  420. the CP/M operating system itself, require work in the language
  421. they were written in: assembler.
  422.      Today ever fewer programmers work in assembly language.
  423.      None the less, or perhaps all the more, you will find it
  424. useful to be literate in it.
  425.