BURSTING YOUR 128: THE FASTLOAD BURST COMMAND

Started by Blacklord, June 24, 2007, 05:20 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Blacklord

(Reprinted from C= Hacking #3)

BURSTING YOUR 128: THE FASTLOAD BURST COMMAND
by Craig Bruce

1. INTRODUCTION

This article discusses the well-unknown Fastload command of the 1571 and 1581
disk drive Burst Command Instruction Set.  If you look in the back of your '71
(or '81 I presume) disk drive manual, you will find that the information given
about the Fastload utility is not exactly abundant.

The Fastload command was intended to load program files into memory for
execution, but it can be used just as well for reading through sequential
files that would be much too large to load into a single bank of internal
memory.

To make use of the Fastload burst command, I implement a word counting utility
that will count the number of lines, words, and characters in a text file on a
1571 or 1581 disk drive.  The advantage of using the Fastload command over
regular sequential file accessing through the kernel and DOS is that the
Fastload operates about 3.5 times faster on both drives.

2. WORD COUNTING UTILITY

To use the word counting program, LOAD and RUN it like a regular BASIC
program.  It will ask you for the name of a file.  Enter the name if it is on
device number 8, or put a one character prefix and a ":" if it is on another
device.  A "b" means device 9, "c" device 10, etc.  The following are examples
of valid names:

. filename          "filename" on device 8
. b:filename        "filename" on device 9
. a:filename        "filename" on device 8

The file must be on either a 1571 or 1581 disk drive; the program will not
work with non-burst devices.  The program will work with either PRG or SEQ
files, since the Fastload command can be told not to worry about the file
type.

I use the same definition of a word as the Unix "wc" command uses: a sequence
of characters delimited by whitespace, where whitespace is defined to be
SPACE, TAB, and NEWLINE (Carriage Return) characters.  To get the line count,
I simply count the number of NEWLINEs.  If the last line of the file does not
end with a NEWLINE character, then the count will be one line short.  This is
the same as the Unix wc command too.  A proper text file should have its last
line end with a NEWLINE character.

On my JiffyDOS-ified 1571 and 1581, I am able to achieve a word counting speed
of 5,400 chars/sec and 6,670 chars/sec, respectively.  I am not sure how much
of a difference JiffyDOS makes, but I am not willing to rip out the ROMs to
check.  I tested using a 318K file.

3. BURST READ LIBRARY

This section presents the burst reading library that you can incorporate into
your own programs and describes how the burst commands work.  The library has
three calls:

. burstOpen  ( .A=Device, .X=NameLen, burstBuf=Filename ) :
. burstRead  () : burstBuf, burstStatus, burstBufCount
. burstClose ()

I define three common storage variables for using this package: "burstBuf",
"burstStatus", and "burstBufCount".  "burstBuf" is a 256 byte area where the
data read in from the disk drive is stored before processing, and is located
at $0B00.  "burstStatus" is a zero-page location that keeps the status
returned from the burst command system.  This is needed by the user to detect
when the end of file has been encountered.  "burstBufCount" gives the number
of data bytes available in "burstBuf" after an open or read operation.  Its
value will be somewhere between 1 and 254.  A full sector contains 254 bytes
of data and two bytes of control information.

"burstStatus" and "burstBufCount" are defined to be at locations $FE and $FF,
respectively.  You are allowed to alter the values of the two variables and
the data buffer between calls, if you wish.  For reasons not completely
understood, interrupts must be disabled for the entire course of burst reading
a file.  I suspect this is because the IRQ service routine reads the interrupt
mask register of CIA#1, thus clearing the SerialDataReady flag that the burst
read routine waits for.  Anyway, the open routine does a SEI and the close
routine does a CLI, so you don't have to do this yourself.

If an error occurs during the exection of one of these routines, it will
return with the carry flag set and with the error code in the .A register
(same as the kernel (yes, I know that Commodore likes to call it the
"kernAl")).  Error codes 0 to 9 correspond to the standard kernel codes, error
code 10 means that the device is not a burst device, and error codes 16 to 31
correspond to the burst controller status codes 0-15.  If no error occurs, the
routines return with the carry flag clear, of course.

Only one file may be open at a time for Fastloading, since Fastload takes over
the disk drive and the entire serial bus.  Even regular files cannot be
accessed while a fastload is in progress.  Thus, Fastload is not suitable for
all file processing applications, but it works very well for reading a file
into memory (like for a text editor) and for summarization operations (like
word counting).  The burst library requires that the kernel and I/O space be
in context when it is called.

3.1. BURST OPEN

The way that a burst command is given is to give a magical incantation over
the command channel to the disk drive.  You can either use the low-level
serial bus calls (LISTN, SECND, CIOUT, and UNLSN) or use the OPEN and CHROUT
calls.  I used the low level calls for a little extra zip.

The burst command format for Fastload is given in the back of your drive
manual:

.  BYTE \ bit: 7     6     5     4     3     2     1     0  | Value
. -------+--------+-----+-----+-----+-----+-----+-----+-----+-------
.   0    |     0  |  1  |  0  |  1  |  0  |  1  |  0  |  1  |  "U"
.   1    |     0  |  0  |  1  |  1  |  0  |  0  |  0  |  0  |  "0"
.   2    |     P  |  X  |  X  |  1  |  1  |  1  |  1  |  1  |  159
. 3 - ?? |                                       |
. -------+--------------------------------------------------+-------

where "X" means "don't case" and "P" means "program".  If the P bit is '0'
then only program (PRG) files can be loaded, and if it is '1' then sequential
(SEQ) files can be loaded as well.  The package automatically sets this flag
for you.  Note that you don't have to do an Inquire Disk or Query Disk Format
in order to use this command like you have to do with the block reading and
writing commands.

If you want to try giving the incantation yourself, enter:

OPEN1,8,15,"U0"+CHR$(159)+"FILENAME"

(where "FILENAME" is the name of some file that exists on your disk) on your
128 and your disk drive will spring to life and wait for you to read the file
data.  You can't read the data from BASIC, so to cancel the command:

CLOSE1

The "burstOpen" call of this package accepts the name of the file to be loaded
at the start of the "burstBuf" buffer, the length of the filename in the .X
register, and the device number to load the file from in the .A register.  The
burst command header and the filename are sent to the disk drive as described
above.

The open command also reads the first sector of the file, for two reasons.
First, the status byte returned for the first sector has special meaning.
Status code $02 means "file not found".  The package translates this into the
kernel error code.  Second, and most important, there is a bizarre feature
(read: "bug") in the Fastload command.  If the file to be read is only one
block long, then the number of bytes reported for the block length is two
bytes too short.  The following table gives the number of bytes reported and
the number of actual bytes in the sector:

. Actual   |    4    |    3    |    2    |    1    |    0    |
. ---------+---------+---------+---------+---------+---------+
. Reported |    2    |    1    |    0    |   255   |   255   |

This is where I ran into problems with Zed-128; the logic of my program
screwed up on a zero length.  I have corrected the problem here, though.  This
bug is bizarre because it only happens if the first sector is the only sector
in the file.  The reported length for all subsequent sectors is correct.  Note
also that 255 is reported for lengths of both 1 and 0.  This is because there
is no actual zero length for Commodore files.  If you OPEN1,8,2,"EMPTY" and
then immediately CLOSE1 you get a file with one carriage return character in
it.

The open routine calls the read routine to read a sector and if it was the
only sector of the file, the two additional bytes are burst-read and put into
the data buffer.  Note that incrementing the reported count of 255 twice gives
the correct count of 1.  Also interesting in this case is that when the
1571/81 reports the 255, it actually transfers 255 bytes of data, all of which
are bogus.  It seems to me that they made the 1581 bug-compatible with the
1571 in this respect.

The open routine also executes a SEI for reasons discussed above.  The
information returned by the open call is the same as what is returned for the
"burstRead" call discussed next.

3.2. BURST READ

Once the Fastload command is started, the drive starts waiting to transfer the
data to you.  The transfer occurs sector by sector, with each sector preceeded
by a burst status byte.  The data is transferred using the shift register of
CIA#1 and the handshaking is done using the Slow Serial Clock line of CIA#2.
To receive a byte, you toggle the Slow Serial Clock line and wait for the
Shift Register Ready signal from CIA#1 and then read the data value from the
shift data register.

One of the clock registers in the CIA in the 1571/81 is used as the baud rate
generator for the serial line.  I think that it uses a delay of 4 microseconds
per bit, which gives a baud rate of 250,000 (31.25K/sec).  In my
experimentation, the maximum baud rate I have ever achieved in reality is
200,000 (25K/sec).  I read in my 1571 Internals book that the 250,000 baud
rate cannot actually be achieved because of electrical problems that I don't
understand.  This is an important difference because the data comes flying off
the surface of the disk at around 30K/sec and if the serial bus were fast
enough, it could be transferred to the computer as it is being read.  Some
things would be so much more convenient if whomever created the universe had
thought to make light go just a little bit faster.

The burst handshaking protocol slows the maximum transfer rate down to about
16K/sec.  Of course, the disk drive has more things to keep on top of than
just transferring data, so the actual burst throughput is lower than that:
about 5.4K/sec with my JiffyDOS-ified 1571 and about 7K/sec with a 1581.  Note
that you can probably increase your 1571's burst performance a bit by setting
it to use a sector interleave factor of 4, using the "U0>S"+CHR$(i) burst
command.  By default, a 1571 writes files with an interleave of 6.

All of the sectors before the last one will contain 254 bytes of data and the
last one will contain a specified number of bytes, from 1 to 254.  The status
code returned for the last sector is value $1F.  In this case, an additional
byte is sent before the data bytes that tells how many data bytes there will
be.  This is the value that is bugged for one-sector files as described in the
last section.  For those who like pictures, here are diagrams of the data
transferred for a sector:

.        REGULAR SECTOR                  LAST SECTOR OF FILE
.     +-------------------+             +--------------------+
.   0 | Burst Status Byte |           0 | Burst Status = $1F |
.     +-------------------+             +--------------------+
.   1 |                   |           1 |   Byte Count = N   |
. ... +  254 Data Bytes   |             +--------------------+
. 254 |                   |           2 |                    |
.     +-------------------+         ... |    N Data Bytes    |
.                                   N+1 |                    |
.                                       +--------------------+

If a sector returns a burst status code other than 0 (ok) or $1F (end), then
an error has occurred and the disk drive aborts the transfer, closes the burst
connection, and starts the drive light blinking.

The "burstRead" call of this package reads the data of the next sector of the
opened file into the "burstBuf" and returns the "burstStatus" and
"burstBufCount" (bytes read).  In the event of an error occuring, this routine
returns with the carry flag set and the translated burst error code in the .A
register (same as burstOpen).  When the last sector of the file has just been
read, this routine returns with a value of $1F in the "burstStatus" variable.

3.3. BURST CLOSE

After reading the last data byte of the last sector of the file, the burst
connection and is closed automatically by the disk drive.  The "burstClose"
routine is not necessary for communication with the disk drive, but is
provided for completeness and to clear the interrupt disable bit (CLI) that
the open routine set to prevent interrupts while burst reading.

3.4. PACKAGE USAGE

The following pseudo-code outlines how a user program is expected to use the
burst reading package:

.     jsr put_filename_into_burstBuf
.     ldx #filename_length
.     lda #device_number
.     jsr burstOpen
.     bcs reportError
. L1: jsr process_burstBuf_data
.     lda burstStatus
.     cmp #$1f
.     beq L2
.     jsr burstRead
.     bcc L1
.     jsr reportError
. L2: jsr burstClose

4. IMPLEMENTATION

This section discusses the code that implements the word counting program.  It
is here in a special form; each code line is preceeded by a few special
characters and the line number.  The special characters are there to allow you
to easily extract the assembler code from the rest of this magazine (and all
of my ugly comments).  On a Unix system, all you have to do is execute the
following command line (substitute filenames as appropriate):

grep '^\.%...\!' Hack3 | sed 's/^.%...\!..//' | sed 's/.%...\!//' >wc.asm

                       
.%001!  ;Word Count utility using the burst command set's Fastload facility
.%002!  ;written 92/06/25 by Craig Bruce for C= Hacking Net Magazine
.%003!

The code is written for the Buddy assembler and here are a few setup
directives.

.%004!  .mem
.%005!  .bank 15
.%006!  .org $1c01
.%007!
.%008!  ;*** BASIC startup code
.%009!

This is what the "10 sys 7200" in BASIC looks like.  It is here so this
program can be executed with BASIC RUN command.

.%010!  .word $1c1c
.%011!  .word 10
.%012!  .byte $9e
.%013!  .asc  " 7200 : "
.%014!  .byte $8f
.%015!  .asc  " 6502 power!"
.%016!  .byte 0
.%017!  .word 0
.%018!  .word 0
.%019!
.%020!  jmp main
.%021!
.%022!  ;========== burst read library ==========
.%023!
.%024!  burstStatus = $fe
.%025!  burstBufCount = $ff
.%026!  burstBuf = $b00

"serialFlag" is used to determine whether a device is Fast or not, and the
"ioStatus" (a.k.a. "ST") is to tell if a device is present or not.

.%027!  serialFlag = $a1c
.%028!  ioStatus = $90

"ciaIcr" is the interrupt control register of CIA#1.  It is polled to wait for
data becoming available in the shift register ("ciaData").  "ciaSerialClk" is
the Slow serial bus clock line that is used for handshaking on the Fast bus.

.%029!  ciaIcr = $dc0d
.%030!  ciaSerialClk = $dd00
.%031!  ciaData = $dc0c
.%032!
.%033!  kernelListen = $ffb1
.%034!  kernelSecond = $ff93
.%035!  kernelCiout  = $ffa8
.%036!  kernelUnlsn  = $ffae
.%037!  kernelSpinp  = $ff47
.%038!

This is the error code value this package returns if it detects that a device
is not Fast.

.%039!  errNotBurstDevice = 10
.%040!
.%041!  burstFilenameLen = burstBufCount
.%042!
.%043!  burstOpen = * ;(.A=Device, burstBuf=Filename, .X=NameLen):

Set up for a burst open: clear the Fast flag and the device not present flag.

.%044!     stx burstFilenameLen
.%045!     pha
.%046!     lda serialFlag
.%047!     and #%10111111
.%048!     sta serialFlag
.%049!     lda #0
.%050!     sta ioStatus
.%051!     pla

Command the disk device to Listen.  Then check if the device is present or not
(bit 7 of ioStatus).  If not present, return the kernel error code.

.%052!     jsr kernelListen
.%053!     bit ioStatus
.%054!     bpl +
.%055!
.%056!     devNotPresent = *
.%057!     jsr kernelUnlsn
.%058!     lda #5
.%059!     sec
.%060!     rts
.%061!

Tell disk device to listen on the command channel (channel #15).

.%062!  +  lda #$6f
.%063!     jsr kernelSecond

Send the "U0"+CHR$(159) burst command header.

.%064!     lda #"u"
.%065!     jsr kernelCiout
.%066!     bit ioStatus
.%067!     bmi devNotPresent
.%068!     lda #"0"
.%069!     jsr kernelCiout
.%070!     lda #$9f
.%071!     jsr kernelCiout

Send the filename.

.%072!     ldy #0
.%073!  -  lda burstBuf,y
.%074!     jsr kernelCiout
.%075!     iny
.%076!     cpy burstFilenameLen
.%077!     bcc -

Finish sending the burst command and make sure the device is Fast.

.%078!     jsr kernelUnlsn
.%079!     lda serialFlag
.%080!     and #$40
.%081!     bne +
.%082!     sec
.%083!     lda #errNotBurstDevice
.%084!     rts
.%085!

Disable interrupts.

.%086!  +  sei

Prepare to receive data and signal the disk drive to start sending (by
toggling the slow serial Clock line).

.%087!     clc
.%088!     jsr kernelSpinp
.%089!     bit ciaIcr
.%090!     lda ciaSerialClk
.%091!     eor #$10
.%092!     sta ciaSerialClk

Read the first sector of the file.

.%093!     jsr burstRead

Check for errors.  Burst error code 2 (file not found) is translated to its
kernel equivalent.

.%094!     lda burstStatus
.%095!     cmp #2
.%096!     bcc +
.%097!     bne shortFile
.%098!     sec
.%099!     lda #4
.%100!  +  rts
.%101!

Check if this is a one-block file.

.%102!     shortFile = *
.%103!     cmp #$1f
.%104!     bne openError
.%105!     ldy burstBufCount
.%106!     ldx #2
.%107!

If so, we have to read the two bytes that the disk drive forgot to tell us
about.  For each byte, we wait for for the Shift Register Ready signal, toggle
the clock, and read the shift data register.  I can get away with reading the
data register after sending the acknowledge signal to the disk drive because I
am running with interrupts disabled and it could not possibly send the next
byte before I pick up the current one.  We wouldn't want any NMIs happening
while doing this, though.

.%108!     shortFileByte = *
.%109!     lda #$08
.%110!  -  bit ciaIcr
.%111!     beq -
.%112!     lda ciaSerialClk
.%113!     eor #$10
.%114!     sta ciaSerialClk
.%115!     lda ciaData
.%116!     sta burstBuf,y
.%117!     iny
.%118!     dex
.%119!     bne shortFileByte

Store the updated byte count and exit.

.%120!     sty burstBufCount
.%121!     clc
.%122!     rts
.%123!

In the event of a burst error, re-enable the interrupts since the user might
not call the burstClose routine.  Return the translated error code.

.%124!     openError = *
.%125!     cli
.%126!     sec
.%127!     ora #$10
.%128!     rts
.%129!

Read the next sector of the file.

.%130!  burstRead = * ;( ) : burstBuf, burstBufCount, burstStatus

Wait for the status byte to arrive.

.%131!     lda #8
.%132!  -  bit ciaIcr
.%133!     beq -

Toggle clock line for acknowledge.

.%134!     lda ciaSerialClk
.%135!     eor #$10
.%136!     sta ciaSerialClk

Get status byte and check.  If 2 or more and not $1F, then an error has
occurred.  If 0, then prepare to read 254 data bytes.

.%137!     lda ciaData
.%138!     sta burstStatus
.%139!     ldx #254
.%140!     cmp #2
.%141!     bcc actualRead
.%142!     cmp #$1f
.%143!     bne openError

If status byte is $1F, then get the next byte, which tells how many data bytes
are to follow.

.%144!     lda #8
.%145!  -  bit ciaIcr
.%146!     beq -
.%147!     ldx ciaData
.%148!     lda ciaSerialClk
.%149!     eor #$10
.%150!     sta ciaSerialClk
.%151!
.%152!     actualRead = *
.%153!     stx burstBufCount
.%154!     ldy #0
.%155!

Read the data bytes and put them into the burst buffer.  The clock line toggle
value is computed before receiving the data for a little extra zip.  I haven't
experimented with this, but you might be able to toggle the clock line before
receiving the data (however, probably not for the first byte).

.%156!     readByte = *
.%157!     lda ciaSerialClk
.%158!     eor #$10
.%159!     tax
.%160!     lda #8
.%161!  -  bit ciaIcr
.%162!     beq -
.%163!     stx ciaSerialClk
.%164!     lda ciaData
.%165!     sta burstBuf,y
.%166!     iny
.%167!     cpy burstBufCount
.%168!     bne readByte
.%169!  +  clc
.%170!     rts
.%171!

Close the burst package: simply CLI.

.%172!  burstClose = *
.%173!     cli
.%174!     clc
.%175!     rts
.%176!
.%177!  ;========== main program ==========
.%178!

This is the word counting application code.

.%179!  bkWC = $0e
.%180!  bkSelect = $ff00
.%181!  kernelChrin  = $ffcf
.%182!  kernelChrout = $ffd2
.%183!

The "wcInWord" is a boolean variable that tells whether the file scanner is
currently in a word or not.  The Lines, Words, and Bytes are 24-bit counters.

.%184!  wcInWord = 2 ;(1)
.%185!  wcLines = 3  ;(3)
.%186!  wcWords = 6  ;(3)
.%187!  wcBytes = 9  ;(3)
.%188!
.%189!  main = *

Put the kernel ROM and I/O space into context then initialize the counting
variables.

.%190!     lda #bkWC
.%191!     sta bkSelect
.%192!     jsr wcInit

Follow the burst reading procedure outline.

.%193!     jsr wcGetFilename
.%194!     jsr burstOpen
.%195!     bcc +
.%196!     jsr reportError
.%197!     rts
.%198!  /  jsr wcScanBuffer
.%199!     lda burstStatus
.%200!     cmp #$1f
.%201!     beq +
.%202!     jsr burstRead
.%203!     bcc -
.%204!     jsr reportError
.%205!  +  jsr burstClose

Report the numbers of lines, words, and characters and then exit.

.%206!     jsr wcReport
.%207!     rts
.%208!

Initialize the variables.

.%209!  wcInit = *
.%210!     lda #0
.%211!     ldx #8
.%212!  -  sta wcLines,x
.%213!     dex
.%214!     bpl -
.%215!     sta wcInWord
.%216!     rts
.%217!

Get the device and filename from the user.  Returns parameters suitable for
passing to burstOpen.

.%218!  wcGetFilename = * ;() : burstBuf=Filename, .A=Device, .X=FilenameLen

Display the prompt.

.%219!     ldx #0
.%220!  -  lda promptMsg,x
.%221!     beq +
.%222!     jsr kernelChrout
.%223!     inx
.%224!     bne -

Get the input line from the user.

.%225!  +  ldx #0
.%226!  -  jsr kernelChrin
.%227!     sta burstBuf,x
.%228!     cmp #13
.%229!     beq +
.%230!     inx
.%231!     bne -
.%232!  +  jsr kernelChrout

Extract the device number from the start of the input line.  If it is not
there, assume device number 8.

.%233!     lda #8
.%234!     cpx #2
.%235!     bcc filenameExit
.%236!     ldy burstBuf+1
.%237!     cpy #":"
.%238!     bne filenameExit
.%239!     sec
.%240!     lda burstBuf
.%241!     sbc #"a"-8
.%242!     tay

If a device name was present, then we have to move the rest of the filename
back over it now that we've extracted it.

.%243!     ldx #0
.%244!  -  lda burstBuf+2,x
.%245!     sta burstBuf,x
.%246!     cmp #13
.%247!     beq +
.%248!     inx
.%249!     bne -
.%250!  +  tya
.%251!     filenameExit = *
.%252!     rts
.%253!
.%254!     promptMsg = *
.%255!     .asc "enter filename in form filename, or a:filename, "
.%256!     .asc "or b:filename, ..."
.%257!     .byte 13
.%258!     .asc "where 'a' is for device 8, 'b' is for device 9, ..."
.%259!     .byte 13,0
.%260!

Scan the burst buffer after reading a sector into it.

.%261!  wcScanBuffer = *
.%262!     ldy #0
.%263!     cpy burstBufCount
.%264!     bne +
.%265!     rts
.%266!  +  ldx wcInWord
.%267!  -  lda burstBuf,y
.%268!  ;   jsr kernelChrout  ;uncomment this line to echo the data read
.%269!     cmp #13
.%270!     bne +

If the current character is a carriage return, then increment the line count.

.%271!     inc wcLines
.%272!     bne +
.%273!     inc wcLines+1
.%274!     bne +
.%275!     inc wcLines+2

If the character is a TAB, SPACE, or a RETURN, then it is a Delimiter;
otherwise, it is considered a Letter.

.%276!  +  cmp #33
.%277!     bcs isLetter
.%278!     cmp #" "
.%279!     beq isDelimiter
.%280!     cmp #13
.%281!     beq isDelimiter
.%282!     cmp #9
.%283!     beq isDelimiter
.%284!
.%285!     isLetter = *

If the character is a Letter and the previous one was a Delimiter, then
increment the word count.

.%286!     cpx #1
.%287!     beq scanCont
.%288!     ldx #1
.%289!     inc wcWords
.%290!     bne scanCont
.%291!     inc wcWords+1
.%292!     bne scanCont
.%293!     inc wcWords+2
.%294!     jmp scanCont
.%295!
.%296!     isDelimiter = *
.%297!     ldx #0
.%298!
.%299!     scanCont = *
.%300!     iny
.%301!     cpy burstBufCount
.%302!     bcc -

Add the number of bytes in the burst buffer to the total byte count for the
file.

.%303!     clc
.%304!     lda wcBytes
.%305!     adc burstBufCount
.%306!     sta wcBytes
.%307!     bcc +
.%308!     inc wcBytes+1
.%309!     bne +
.%310!     inc wcBytes+2
.%311!  +  stx wcInWord
.%312!     rts
.%313!

Report the number of lines, words, and bytes read.  Uses a "printf" type of
scheme.

.%314!  wcReport = *
.%315!     ldx #0
.%316!  -  lda reportMsg,x
.%317!     beq reportExit
.%318!     cmp #13
.%319!     bcs +
.%320!     stx 14
.%321!     tax
.%322!     lda 2,x
.%323!     sta 15
.%324!     lda 0,x
.%325!     ldy 1,x
.%326!     ldx 15
.%327!     jsr putnum
.%328!     ldx 14
.%329!     jmp reportCont
.%330!  +  jsr kernelChrout
.%331!     reportCont = *
.%332!     inx
.%333!     bne -
.%334!     reportExit = *
.%335!     rts
.%336!
.%337!     reportMsg = *
.%338!     .byte 13
.%339!     .asc "lines="
.%340!     .byte wcLines
.%341!     .asc ", words="
.%342!     .byte wcWords
.%343!     .asc ", chars="
.%344!     .byte wcBytes,27
.%345!     .asc "q"
.%346!     .byte 13,0
.%347!

Reports the error number given in the .A register.  Called after an error is
returned from a burst routine.

.%348!  reportError = * ;( .A=errNum )
.%349!     pha
.%350!     ldx #0
.%351!  -  lda errorMsg,x
.%352!     beq +
.%353!     jsr kernelChrout
.%354!     inx
.%355!     bne -
.%356!  +  pla
.%357!     ldy #0
.%358!     ldx #0
.%359!     jsr putnum
.%360!     lda #13
.%361!     jsr kernelChrout
.%362!     rts
.%363!
.%364!     errorMsg = *
.%365!     .asc "*** i/o error #"
.%366!     .byte 0
.%367!
.%368!  ;==========library==========
.%369!

Routine to print out the 24-bit number given in .AYX.

.%370!  libwork = $60
.%371!  itoaBin = libwork
.%372!  itoaBcd = libwork+3
.%373!  itoaFlag = libwork+7
.%374!
.%375!  putnum = *

Initialize binary and BCD (Binary Coded Decimal) representations of number.

.%376!     sta itoaBin+0
.%377!     sty itoaBin+1
.%378!     stx itoaBin+2
.%379!     ldx #3
.%380!     lda #0
.%381!  -  sta itoaBcd,x
.%382!     dex
.%383!     bpl -
.%384!     sta itoaFlag
.%385!     ldy #24
.%386!     sed
.%387!

Rotate each bit out of the binary number and then multiply the BCD number by
two and add the bit in.  Effectively, we are shifting the bits out of the
binary number and into the BCD representation of the number.

.%388!     itoaNextBit = *
.%389!     asl itoaBin+0
.%390!     rol itoaBin+1
.%391!     rol itoaBin+2
.%392!     ldx #3
.%393!  -  lda itoaBcd,x
.%394!     adc itoaBcd,x
.%395!     sta itoaBcd,x
.%396!     dex
.%397!     bpl -
.%398!     dey
.%399!     bne itoaNextBit
.%400!     cld

Take the BCD bytes and spit out the two digits they contain.

.%401!     ldx #0
.%402!     ldy #0
.%403!  -  lda itoaBcd,x
.%404!     jsr itoaPutHex
.%405!     inx
.%406!     cpx #4
.%407!     bcc -
.%408!     rts
.%409!
.%410!     itoaPutHex = *
.%411!     pha
.%412!     lsr
.%413!     lsr
.%414!     lsr
.%415!     lsr
.%416!     jsr itoaPutDigit
.%417!     pla
.%418!     and #$0f
.%419!

Print out the individual digits of the number.  If the current digit is zero
and all digits so far have been zero, then don't output anything, unless it is
the last digit of the number.

.%420!     itoaPutDigit = *
.%421!     cmp itoaFlag
.%422!     bne +
.%423!     cpy #7
.%424!     bcc itoaPutDigitExit
.%425!  +  ora #$30
.%426!     sta itoaFlag
.%427!     jsr kernelChrout
.%428!     itoaPutDigitExit = *
.%429!     iny
.%430!     rts

5. UUENCODED PROGRAM

Here is the binary executable in uuencoded form.  The CRC32 of it is
3676144922.  LOAD and RUN it like a regular BASIC program.

begin 640 wc
M`1P<'`H`GB`W,C`P(#H@CR`V-3`R(%!/5T52(0``````3!$=AO](K1P**;^-
M'`JI`(60:""Q_R20$`<@KO^I!3A@J6\@D_^I52"H_R20,.NI,""H_ZF?(*C_
MH`"Y``L@J/_(Q/^0]2"N_ZT<"BE`T`0XJ0I@>!@@1_\L#=RM`-U)$(T`W2"]
M'*7^R0*0!=`$.*D$8,D?T"&D_Z("J0@L#=SP^ZT`W4D0C0#=K0SMYX3_&&!8.`D08*D(+`W<\/NM`-U)$(T`W:T,W(7^HO[)`I`6R1_0W:D(+`W<
M\/NN#-RM`-U)$(T`W8;_H`"M`-U)$*JI""P-W/#[C@#=K0SM8%@88*D.C0#_(#T=($D=(",I?[)'_`((+TM6QY@J0"B")4#RA#[A0)@H@"]C1WP!B#2_^C0]:(`(,__G0`+R0WP`^C0\R#2
MLZD(X`*0'JP!"\`ZT!M24Q%3D%-12!)3B!&3U)-($9)3$5.04U%+"!/4B!!.D9)3$5.04U%+"!/4B!"
M.D9)3$5.04U%+"`N+BX-5TA%4D4@)T$G($E3($9/4B!$159)0T4@."P@)T(G
M($E3($9/4B!$159)0T4@.2P@+BXN#0"@`,3_T`%@I@*Y``O)#=`*Y@/0!N8$
MT`+F!H@#(Q/^0
MQ1BE"67_A0F0!N8*T`+F"X8"8*(`O8(>\!_)#;`5A@ZJM0*%#[4`M`&F#R#,
M'J8.3'X>(-+_Z-#<8`U,24Y%4ST#+"!73U)$4ST&+"!#2$%24ST)&U$-`$BB
M`+V\'O`&(-+_Z-#U:*``H@`@S!ZI#2#2_V`J*BH@22]/($524D]2(",`A6"$
M889BH@.I`)5CRA#[A6>@&/@&8"9A)F*B`[5C=6.58\H0]XC0[-BB`*``M6,@
D!!_HX`20]F!(2DI*2B`/'V@I#\5GT`3`!Y`'"3"%9R#2_\A@````
`
end

6. REFERENCES

[1] Commodore Business Machines, _Commodore_1571_Disk_Drive_User's_Guide_,
    CBM, 1985.

[2] Rainer Ellinger, _1571_Internals_, Abacus Software, June 1986.