Machine Code Made Easy - Part 27
Author
Description
Welcome to M/C Part 27. 27: Nice number, don't you think? Three
cubed, spookily enough, and sitting right smack bang in between
26 and 28.
Enough of such frivilous trivia, and on with the show. Oh, hang
on, before we get on with part four of the Driver special, a wee
personal note from me. I'll have started University by the time
you read this, and my life is sure to be disrupted for a while
at least. To complicate matters my home address will be changing
soon too, so don't send disks or anything else for a couple of
months at least. Don't worry, though, I'm not deserting the SAM
or Fred. I won't be here next month, but service will
(hopefully) be returned to "normality" A.S.A.P.
Right: Driver. This month I'm going to describe how to get round
the only real problems application programmers will find:
Firstly, memory paging.
TOTAL RECALL
Last time round I described how Driver allocates memory in 16k
pages, a practice which is pretty obvious given the physical
specifications of the Coupe. However, since the user can switch
between applications willy nilly, and some applications won't
know how much memory they need until it runs out (for example, a
word processor), the allocation inevitably leads to memory being
broken up and incongruent.
I also mentioned the application page table (APT): How you
initialise it, how it stores page numbers, and how to reserve/
free pages. I won't bother to repeat all that; you can just look
it up.
However, the incongruence of the pages can lead to problems -
you've got to be careful not to touch memory in section D
(paging in ROM1 could help), and shuffling data about can take
ages! This was precisely the nightmare I had with Notepad - the
word processor you get in the Driver package. I designed it so
that typing automatically inserted characters and deleting
automatically pulled all the following text backwards. In
addition I wanted to use the Driver clipboard, deleting and
inserting large blocks of text. This meant that either
A. I could restrict myself to files 16k long OR
B. I had to find a fast way of copying all the text.
Ugh! The first option was attractive - I didn't have to worry
about 19-bit address representation (page/offset), and I could
use LDIR and LDDR for simplicity. But it seemed a waste of
Driver's facilities, and I wanted a SAM word processor that
could handle large files.
Unfortunately, the second option wouldn't work - even in a file
30k long, inserting characters at the start caused noticable
delays.
So what did I do? What I always seem to do - I found a
compromise that was better than either alternative. And I'm
gonna tell you about it...
PAGE BUFFERING
I mentioned that I could handle files up to about 16k long
without problems. So, I treated the file in blocks about 16k
long - just the way it is represented by the paging mechanism.
The sneaky bit is that I left about 256 bytes free at the end of
each page, so that the default length of a file block was
actually 15.75k long.
So, when the user is typing, the longest block the program has
to shuffle (normally) is 15.75k. As this happens, the block size
increases until it reaches 16k, then the top 256 bytes are
copied into the bottom of the next block, and any overspill from
that is copied into the next one... and so on. Obviously, when
the user deletes the block size is reduced.
The method means that we need a table of pointers for the top of
each block, and some wee routines to adjust any address that
points to a non-existant character. We also need routines to
insert and delete a block of bytes.
ADJUSTING THE PAGING WHILST COUNTING
This is the first routine I'm gonna give you. We can represent
each address with a page number (0-127) in A, and an address
offset (8000h-BFFFh) in HL. Put the table of length pointers at
an address which is a multiple of 512.
Use the routine after something like INC HL when the paging
might need changed - it returns the altered address in AHL, with
CY if the physical paging needs to be changed.
page.up PUSH DE
PUSH HL
PUSH HL
LD L,A
LD H,length.tab/512
ADD HL,HL
LD E,(HL)
INC L
LD D,(HL)
POP HL
SBC HL,DE ; NC if we need to increase
; the paging.
POP HL
POP DE
CCF ; Toggle CY.
RET NC
LD HL,8000h
INC A
RET
Of course, this can slow things up quite considerably when
you're shuffling data about, so it can be a good idea to make a
note of the block length when you start, and only update it when
you change blocks.
Counting downwards is easier, because the bottom address is
always 8000h: Entry and exit as before.
page.down CP A
BIT 7,H
RET NZ
DEC A
PUSH DE
LD L,A
LD H,length.tab/512
ADD HL,HL
LD E,(HL)
INC L
LD D,(HL)
LD L,E
LD H,D
POP DE
DEC HL
SCF
RET
You might want to vary things a bit to compensate for empty
pages in the middle and so on, but it all depends on the
application.
INSERTING A BLOCK
This insert routine is horrendously complicated (heh, heh, heh),
considering that all it does it move some data around in the
same way that an LDDR command would. You need a buffer 256 bytes
long, at a 0-lsb address.
In addition, it can only cope with blocks up to 16383 bytes long
- for longer blocks use multiple calls. So why is it so long? (A
question I'm often asked. Ahem). Well, I don't really know. It
was by far the most difficult thing I had to do for Notepad, and
took well over a week to get working.
If the insert is going to cause a page overspill, the block
length is reset at 15.75k - of course, the routine has to cope
with the possibility that more than one overspill will result.
Extra memory is reserved from Driver if necessary, and an error
reported if there are any problems ("nomem"). The only other
routines needed are:
driver_in Page Driver into section C
data_in Page data page A into section C.
add_ix_a Adds A to IX: Result in IX, A unchanged.
Take a deep breath...
; ENTRY: AHL = address, DE = length of block.
insert.block LD (ib.page),A ; Store for later
LD (ib.address),HL
ADD A,A
LD IX,length.tab ; Page length table
CALL add_ix_a
LD L,(IX+0)
LD H,(IX+1) ; Page "top" in HL
ADD HL,DE ; Add on block length
BIT 6,H ; Page overspill?
LD A,(ib.page)
LD HL,(ib.address)
JP Z,small.insert ; If not, use a simpler
; routine.
ib1 CALL ib.check
; If page overspill: Returns CY, HL = 8000h, A incremented, DE =
space needed in next page.
; If no overspill: DE = new end of page, HL = old, A unchanged.
JR C,ib1 ; Loop until we've dealt
; with the overspill
LD (ib.countp1),A ; Store pointers for
; source/target addresses.
LD (ib.countp2),A
LD (ib.counta1),HL
LD (ib.counta2),DE
ADD A,A
LD IX,length.tab
CALL add_ix_a
LD (IX+0),E
LD (IX+1),D ; Store new page "top"
LD A,(ib.page)
LD C,A ; Page for start of block
ib2 LD A,(ib.countp1)
LD HL,(ib.counta1)
CALL data_in
EX AF,AF'
LD DE,buffer
ib3 EX AF,AF'
CP C ; Reached end page
; (start!)?
JR NZ,ib4 ; If not...
PUSH BC
PUSH HL
LD BC,(ib.address)
SBC HL,BC ; Compare addresses
POP HL
POP BC
JR Z,ib.end ; Finish main loop if end
ib4 DEC HL
BIT 7,H ; Page overlap?
JR NZ,ib7 ; If not, jump
DEC A
PUSH AF
PUSH IX
ADD A,A
LD IX,length.tab
CALL add_ix_a
LD L,(IX+0)
LD H,(IX+1) ; Get top address
LD (IX+0),0
LD (IX+1),BFh ; Store balance size
POP IX
POP AF
CALL data_in
EX AF,AF'
JR ib3 ; Loop back for empty page
ib7 EX AF,AF'
LD A,(HL) ; Copy byte to buffer
DEC E
LD (DE),A
JR NZ,ib3 ; Loop for 256 bytes
EX AF,AF'
LD (ib.countp1),A ; Store pointers
LD (ib.counta1),HL
LD A,(ib.countp2)
LD DE,(ib.counta2) ; Target pointers
LD HL,buffer
CALL data_in
EX AF,AF'
ib5 DEC DE
BIT 7,D ; Page overlap?
JR NZ,ib6 ; If not...
EX AF,AF'
DEC A ; Next page
LD DE,BF00h ; Balance address
DEC IX
DEC IX
LD (IX+0),E
LD (IX+1),D ; Store balance
CALL data_in
EX AF,AF'
JR ib5 ; Loop for empty page
ib6 DEC L
LD A,(HL) ; Copy byte from buffer
LD (DE),A
JR NZ,ib5 ; Loop for 256 bytes
EX AF,AF'
LD (ib.countp2),A ; Store pointers
LD (ib.counta2),DE
JP ib2 ; Loop until finished
ib.end XOR A ; E = bytes left to copy
SUB E
JR Z,ib.e3 ; If none, jump
LD B,A ; B = 256-E
LD A,(ib.countp2) ; Target pointers
LD DE,(ib.counta2)
LD HL,buffer
CALL data_in
EX AF,AF'
ib.e1 DEC DE
BIT 7,D
JR NZ,ib.e2 ; Page overlap?
EX AF,AF'
LD DE,BF00h
DEC IX
DEC IX
LD (IX+0),E
LD (IX+1),D ; Store balance
JR ib.e1
ib.e2 DEC L
LD A,(HL)
LD (DE),A ; Copy from buffer
DJNZ ib.e1 ; Loop.
EX AF,AF'
LD (ib.countp2),A
ib.e3 LD HL,(ib.address)
LD BC,BF00h
CP A
SBC HL,BC
RET C
RET Z ; Ret if start ad<BF01h
LD C,L
LD B,0
LD HL,BF00h
LD A,(ib.page)
CALL data_in
LD DE,buffer
PUSH BC
LDIR ; Copy to buffer
POP BC
LD A,(ib.countp2)
CALL data_in
LD DE,8000h
LD HL,buffer ; Copy from buffer
LDIR
RET
ib.check PUSH AF
ADD A,A
LD IX,length.tab
CALL add_ix_a
LD L,(IX+0)
LD H,(IX+1)
ADD HL,DE
BIT 6,H
JR NZ,ib.c1 ; JR if overspill.
EX DE,HL ; DE = new top
LD L,(IX+0)
LD H,(IX+1) ; HL = old top
POP AF
CP A ; Reset CY
RET
ib.c1 LD DE,BF00h
CP A
SBC HL,DE ; Subtract BF00h
EX DE,HL ; Put in DE
LD HL,8000h
LD A,(pagetab) ; Top page reserved
LD B,A
POP AF
INC A
CP B
RET C ; Return if page exists
PUSH DE
CALL driver_in
CALL jreserve_page ; Reserve new page
POP DE
JP C,nomem ; Error
LD A,(pagetab)
DEC A ; New top page
LD HL,8000h
PUSH AF
ADD A,A
LD IX,length.tab
CALL add_ix_a
LD (IX+0),L
LD (IX+1),H ; Reset length
POP AF
SCF ; Set CY
RET
small.insert LD (ib.page),A
LD (ib.address),HL
ADD A,A
LD IX,length.tab
CALL add_ix_a
LD L,(IX+0)
LD H,(IX+1) ; Top of page
ADD HL,DE ; New top
PUSH HL
LD BC,(ib.address)
LD L,(IX+0)
LD H,(IX+1) ; Old top
CP A
SBC HL,BC ; Subtract
JR Z,si1 ; No bytes to copy
LD C,L
LD B,H
LD L,(IX+0)
LD H,(IX+1) ; Old top
POP DE ; New top
PUSH DE
DEC HL
DEC DE
LD A,(ib.page)
CALL data_in
LDDR ; Make space
si1 POP HL
LD (IX+0),L
LD (IX+1),H ; Store new top
RET
DELETING A BLOCK
I was gonna give you the routine, but I've run out of time, and
it's pretty easy anyway. Don't bother about intra-page copying
- just an LDIR to get rid of stuff inside a page, and adjust the
page lengths for other bits.
Right, I'd better convert this file and bung it in the post.
I'll see you in a couple of months (I'll let you know how I'm
getting on!) with the final edition of the Driver special. Stay
tuned...