Classic Computer Magazine Archive COMPUTE! ISSUE 78 / NOVEMBER 1986 / PAGE 78

Commodore 128 Machine Language

Part 4

Jim Butterfield, Associate Editor

Previous installments of this series of articles have explained some fundamentals of machine language programming on the Commodore 128. In this session, we'll look at ways that a program can get information from various parts of the 128's memory architecture.

Banks Or Configurations

Figure 1 shows the memory configuration called bank 15. As you can see, it's a varied assortment of memory elements: RAM, ROM, and I/O chip registers. The bank 15 configuration is usually the most comfortable setup for machine language programming.

Figure 1. Bank 15 Configuration

Figure 2. Bank 0 Configuration

Figure 3. Bank 1 Configuration

Sometimes a program needs to get information from an area that's not visible in the current configuration. When this happens, the configuration must be switched to allow access to the desired data. The switch may be very brief indeed—just long enough to allow the data to be read or stored—or it may be a semipermanent reconfiguration.

Types Of Bank Switch

Data may be read from or written to any standard configuration (bank) by using one of a set of Kernal subroutines. The routine named INDFET ($FF74) gets a byte, INDSTA ($FF77) stores a byte, and INDCMP ($FF7A) compares a byte (more on these later). First note that these three routines are a little slow (measured on the rapid scale of machine language operations). They switch in the appropriate configuration, do their business with one byte, then switch everything back. To read, compare, or store a hundred bytes, these routines would perform two hundred bank switches.

As an alternative, you can manipulate the configuration directly. The configuration can remain for as long as you need. However, you must be careful. If you switch out the Kernal ROM, you must take care not to try to use Kernal routines until it has been switched back in. The same goes for the I/O registers: You can't use them when they are not there (when the processor has no means to address them). Worst of all, a clumsy program could make itself vanish by switching out the bank in which it resides.

The Kernal Switches

For most purposes, only four standard configurations are necessary:

Bank 15. Very convenient for ML programming. It has RAM from block 0, BASIC and Kernal ROMs, and the I/O chip registers (Figure 1).

Bank 14. Very similar to bank 15, but this configuration contains the character generator ROM at locations $D000–$DFFF instead of the I/O chip registers.

Bank 0. Almost purely RAM from block 0. The exceptions are addresses $0000–$0001, which are the 8502 microprocessor's on-chip I/O port registers and addresses $FFOO–$FF04, where MMU (memory management unit) chip registers are always seen, regardless of the bank configuration. Your machine language program will usually reside in this bank, and BASIC program text will also be stored here. As long as you don't try to do I/O or call Kernal routines, it's also a convenient bank for ML programming (Figure 2).

Bank 1. RAM from block 0 in addresses $0002–$0400. Above that, the bank consists of RAM from block 1 (except for the MMU chip registers at $FF00–$FF04). Use this configuration to read or change BASIC variables, arrays, and strings (Figure 3).

Your program may reside in one place, but may need access to information from an area that isn't visible in the current configuration. To do this, you may use one of the following Kernal routines:

INDSTA INDirect STore $FF77

Note that these routines are in Kernal ROM. If you call them, the Kernal must be visible, and that usually means that you're in bank 15. Before calling the routine, you must set up an indirect address somewhere in the zero page of memory to be a pointer to the address you wish to access. Then you must tell the routine where this indirect address is located, and set the processor's Y register with the offset from the address in the pointer to the one you actually wish to access. (Load Y with $00 if you wish to access the exact address in the pointer.)

Here's an example. Suppose you wish to read the contents of address $2468 within bank 1 using the Kernal INDFET routine. The first job is to pick an indirect address somewhere in page zero to serve as a pointer. Locations $FB–$FC are free, so the desired address can go there (LDA #$68: STA $FB: LDA #$24: STA $FC). In this case we set Y to zero (LDY #$00). The bank number goes into the X register (LDX #$01 for bank 1). Finally, we must tell the INDFET routine where to find the indirect address pointer we have set up. This is done by loading the accumulator (A register) with the pointer address:LDA #$FB. Now we can call INDFET with JSR $FF74. Upon return from the ROM routine, the accumulator will hold the value read from address $2468 in bank 1.

The procedure for using INDSTA or INDCMP to store or compare a value in another bank configuration is similar, except that it takes a bit more work to indicate the direct address location. Suppose you want to store the value 7 into location $CDEF in bank 0. It could be done this way: Begin by storing the target address in $FB–$FC (LDA #$EF: STA $FB: LDA #$CD: STA $FC). Next, tell the system where the indirect address pointer is located by storing the pointer address directly in the INDSTA routine, at address $02B9 (LDA #$FB: STA $02B9). To use INDCMP comparison rather than INDSTA for a store, you should store the indirect pointer address in $02C8. Set up the Y index (LDY #$0) and put the bank number in X (LDA #$00 for bank 0). Now you can load the byte value to be stored into the accumulator (LDA #$07) and complete the store operation with JSR $FF77.

After having done the selected task, these ROM routines return you to the same configuration that was set up when the routine was called. By the way, if you're wondering if there is a proper bank for addresses such as $FA or $02B9, don't worry. Addresses below $0400 are always seen in block 0 RAM in normal operation.

If you're using the bank 15 configuration, a shortcut is available for storing data in bank 0. Remember that bank 0 and bank 15 see the same RAM (block 0) in all addresses below $4000. In the bank 15 configuration, reading the contents of a ROM address ($4000–$CFFF or $E000–$FFFF) always returns the value from the corresponding ROM location, but writing to the address actually causes the value to be stored in the corresponding location in the underlying block 0 RAM. Thus, when you are programming in bank 15 (or bank 14), it's not necessary to use INDSTA to place values in bank 0 unless you need access to a RAM address under the I/O block ($D000–$DFFF). For instance, the example above could have placed a value in location $CDEF of bank 0 simply using STA $CDEF. However, the INDFET and INDCMP routines are still required for reading or comparing bank 0 locations from the bank 15 configuration.

Example Program

Here's a program to illustrate these techniques. First, a word to explain what it does. INPUT# is a problem command in BASIC. It often works well and efficiently, but it misbehaves when it encounters certain characters in a file. The characters that cause the most trouble are the comma, the colon, and sometimes quotation marks. If any of these are found in a file, INPUT# may not perform a clean input. One more thing: On the 128, INPUT# causes trouble if it brings in more than 160 characters before it finds a carriage return (character 13).

Let's create a problem file that INPUT# won't be able to handle. Enter this BASIC program:

120 PRINT#1, "THIS IS " ; CHR$(34) ; "WEIRD" ; CHR$(34)
150 FOR J = l TO 25
160 PRINT#1, "BORING, " ;
170 NEXT J
180 PRINT#1, "AND DULL. "
190 DCLOSE#1

Be sure to put a semicolon at the end of line 160. Do not put a space between the number signs (#) and the preceding characters or the program won't work. When you run the program, it writes a sequential file named WEIRD FILE to disk.

The file contains valid data, but INPUT# will have problems with it. The first line contains quotation marks, the second has a comma, the third has a colon, and the last line is longer than INPUT# can handle. To see how INPUT# fails, type NEW and enter this program:

120 INPUT#1, A$
130 PRINT A$
140 IF ST = 0 THEN 120
150 DCLOSE#1

When you run the program, the first line comes in without problems; however, it would have created trouble if the quotation marks had begun the line. The second and third lines are missing everything after the comma and colon, and the fourth line of data doesn't come in at all (it causes an error).

String Thing

The "String Thing" program provides a substitute for the flawed INPUT# statement. It reads from a file and puts the data directly into a string. It isn't bothered by commas, colons, or quotation marks—they are just ASCII characters like any others. A carriage return or end-of-file marker terminates the input. If the string's too long to fit the space provided, String Thing brings in as much as it can.

The toughest part is interfacing with BASIC. How can a machine language routine pass a string to a BASIC program? The answer is, "only with great care." Strings can be difficult to manipulate. Like numeric variables, each string variable has an entry in the variable table at the bottom of free memory in block 1. However, the table entry for a string variable doesn't contain the actual characters that make up the string. Rather, the characters are stored in an area at the top of free memory in block 1, known as the string pool. The table entry contains a three-byte descriptor for the variable. The first byte of the descriptor holds the length of the string. The remaining two bytes contain the address (in standard low-byte/high-byte format) of the location within the string pool where the characters for the string begin.

To simplify the task of passing strings, we'll create the string in BASIC, change its contents from machine language, then let BASIC use the changed results. We'll avoid moving the string or trying to change its length—both rather complicated operations. The string can be created with a BASIC statement something like A$="XXXXX". We'll also use BASIC to open the file, since that will help keep the machine language routine short and simple. Then we call the ML program to read from that file and store the results in A$. When a line of input from the file has been read, we return to BASIC and use A$, which now contains the input from the file.

Bank Considerations

Our program will be located in block 0 RAM, and the string we want will be in block 1. To store values in the string, we need to cross banks with INDSTA. But first we must find where the string is located. That information is stored in the string descriptor in the variable table, also in block 1, so the block 0 ML program must retrieve it from there using INDFET.

To make the string easy to find, we'll make sure it's the very first variable defined in the BASIC program. This eliminates the need to search for it by name.

Here we go. Since we know the string is the very first variable, we can use the system's start-of-variables pointer in locations $2F–$30 as an indirect address to the descriptor in bank 1. We don't need the variable name, contained in the first two bytes of the table entry, so we start our index Y at a value of 2:

B00 LDY #$02

To call INDFET correctly, we need to set the accumulator to the indirect address ($2F in this case) and set the X register to the bank number (in this case, 1 for bank 1).

B02 LDA #$2F
B04 LDX #$01
B06 JSR $FF74

The JSR $FF74 calls the INDFET routine. When that routine returns, the accumulator contains one byte of the string descriptor. We save that byte in a zero-page area (starting at location $FB) and go back for more.

B09 STA $00F9, Y
B0D CPY #$05
B0F BNE $0B02

Since Y starts at 2 and goes up to 4, we'll store data in locations $FB–$FD. Location $FB contains the length of the string and $FC–$FD contain its location. These three bytes—length and location—constitute the string's descriptor.

Now we know where the target string is located in bank 1; that's where we'll put the input from the file. Let's connect to the file:

B11 LDX #$01

The ROM routine at $FFC6 is CHKIN, which selects logical file 1 for input. (The BASIC program which calls this routine must open logical file 1 to the desired disk file before the routine is called.)

B16 LDY #$00

The Y register will be used as an index into the string, so we initialize it for the first character position. Now let's get some characters:

B1B CMP #$0D
B1D BEQ $0B32

The BASIN routine ($FFCF) brings in a character. If it's a carriage return, we've reached the end of the current string, so we branch ahead to the exit. Otherwise, our task is to store the character in block 1 using the INDSTA routine. INDSTA requires the address of an indirect pointer—in this case the address of our string at $FC–$FD— in location $02B9.

B21 STX $02B9

Now we must put the bank number into the X register. By the way, upon return from INDSTA the accumulator still contains the character from the file that we are going to put away.

B24 LDX #$01
B26 JSR $FF77

The character is stored in the string using INDSTA ($FF77). Now we can move the pointer along to the next position within the string, so that the next character goes in the proper place:

B2C BEQ $B32

We've also tested to see if the string is full—if so, we'll skip ahead to the exit. One last test and the main job is done. We want to stop if we have reached the end of file or if an error occurs while the disk is being read. We check this by testing the serial status byte located at $90. (The value here is the one returned when you use the reserved variable ST in BASIC programming.) If it's zero, we're not at end of file.

B2E LDA $90
B30 BEQ $0B18

If we're at the end of a string or the end of the file, we don't branch back. Instead, we use the Kernal CLRCH routine ($FFCC) to disconnect the input file. Also, we transfer the number of characters read into the string into the accumulator.


There it is: a useful program that demonstrates loading and storing data across banks. Let's gather it together as a BASIC program.

String Thing Demo

110 A$ = A$ + A$ + A$ + A$ + A$
120 A$ = A$ + A$ + A$
200 FOR J = 2816 TO 2870 : READ X : POKE J, X : T = T + X : NEXT J
220 DATA 160, 2, 169, 47, 162, 1, 32, 116, 255, 153
230 DATA 249, 0, 200, 192, 5, 208, 241, 162, 1, 32
240 DATA 198, 255, 160, 0, 32, 207, 255, 201, 13, 240
250 DATA 19, 162, 252, 142, 185, 2, 162, 1, 32, 119
260 DATA 255, 200, 196, 251, 240, 4, 165, 144, 240, 230
270 DATA 32, 204, 255, 152, 96
420 RREG L
430 PRINT LEFT$(A$, L)
440 IF ST = 0 THEN 410
450 DOLOSE#1

We take care to make A$ the first variable and make it a string of the maximum allowable length (255 characters). Lines 200–270 read the machine language routine from DATA statements and POKE it into memory. The program also demonstrates some BASIC 7.0 statements. DOPEN opens a disk file and DCLOSE closes it. DS and DS$ check disk status. RREG retrieves the values held in the microprocessor registers upon return from the SYS. In line 420, it is used to transfer the accumulator contents into the variable L.

Note the use of LEFT$ in line 430. If you used just PRINT A$ you'd get garbage characters following the input string. The String Thing routine takes a shortcut when writing characters into the string—it doesn't try to change the length of the string, since that's rather complicated in machine language. If you want to manipulate the string read from disk, use a statement like B$ = LEFT$(A$, L) to create a string of the true length. (Don't change A$ in BASIC; you want A$ to remain 255 characters long so that you can read large strings from disk.)

We've still seen only part of the story. There are more techniques that you may need to work within the 128's unique and powerful architecture. Stay tuned.