Apple IIOther

Programming the Videx Videoterm with Assembly Language

Introduction

This article describes how to write assembler based software that utilises the Videx Videoterm 80 Column Display. The aim of the article is to provide some simple examples to describe some basic operations. It is expected that the reader become familiar with the ideas and develop them as is required for a particular application. The code shown is based on the code provided in the Videoterm ROM and in some cases is shown verbatim, albeit with expanded comments and explanations.

The Videoterm card has a couple of methods of displaying characters on the screen The first is to use the documented control sequences to update the cursor position and calling COUT to output the character. The second method, is to perform the task by directly manipulating the screen memory without affecting the cursor position. Typically, both approaches will be needed.

For the purposes of this article, it is assumed that the Videoterm Display card is located in Slot 3. The code was cross compiled using VS Code, Merlin 32 on MacOS. See the article 6502 Cross Assembly for the Apple II using MacOS.

Emulating the Videoterm Card

For development purposes it is convenient to emulate the Apple II with a Videoterm card fitted. The only emulator I have found that implements the Videx Videoterm hardware and firmware correctly is the MAME system. An example of how this can be achieved is shown in the more general article on MAME Using MAME to Emulate the Apple II+.

Peripheral Card Memory

Scratchpad RAM (Screen Holes)

The normal Apple text mode consists of two screens (pages). These screens are mapped to the following memory locations.

   Page 1 $400-$7FF
   Page 2 $800-$9FF

These two 1024 byte blocks support 2 screens of 40 columns x 24 rows of text (2 x 960) characters. The 64 memory locations not used in the first block (1024 – 960) are used as scratchpad RAM for peripheral cards. These locations are often refered to as Screen Holes. The Apple ][ Reference Manual (p82) and the Videterm Installation an Operation Manual (p.3-2) describes these addresses in more detail.

The addresses below show the screen hole addresses for slot 0, to determine the locations for other slots, simply add the slot number (see below). Note that the locations for slot 0 are a special case and are used as a system sratchpad rather than specifically for slot 0. They are used to store things such as the current slot in use, more on that later.

   $0478
   $04F8
   $0578
   $05F8 Holds the slot number of the disk controller card from which DOS was booted
   $0678
   $06F8
   $0778
   $07F8 Holds the slot number in the format Cn of the peripheral card that is currently active.

Adding the slot number (3) to above addresses gives the scratchpad ram locations for slot 3. These are shown below with a short description of what the Videoterm Display uses them for.

   $047B Screen Base Address (Low)
   $04FB Screen Base Address (High)
   $057B Cursor Horozontal Position
   $05FB Cursor Vertical Position
   $067B Pascal Character Write Location
   $06FB First Line on the Screen
   $077B Power Off/Lead In Counter
   $07FB Video Setup Flags

I/O RAM Locations

Each peripheral card is allocated 280 bytes of RAM in the system I/O space, there is also a 2KB common area that all cards can share.

Each slot is given 16 location for general purpose I/O starting at C080 these are shown below. The locations for slot 3 are $C0B0 – $C0BF.

   Slot 1    $C090 - $C09F
     "  2    $C0A0 - $C0AF
     "  3    $C0B0 - $C0BF
     "  4    $C0C0 - $C0CF
     "  5    $C0D0 - $C0DF
     "  6    $C0E0 - $C0EF
     "  7    $C0F0 - $C0FF

ROM Locations

Each slot is allocated memory space for 256 bytes of ROM space, the locations are shown below. The locations for slot 3 are $C300 – $C3FF.

   Slot 1    $C100 - $C1FF
     "  2    $C200 - $C2FF
     "  3    $C300 - $C3FF
     "  4    $C400 - $C4FF
     "  5    $C500 - $C5FF
     "  6    $C600 - $C6FF
     "  7    $C700 - $C7FF

Expansion ROM

Although called the Expansion ROM, the memory space $C800 – $CFFF can be used by peripheral cards to hold programs, drivers, RAM etc. The whole space is available to all cards but only one card can use it at any one time.

Initialising the Videoterm Card

This code initialises the Videoterm card by switching its ROM into $C800, setting up the appropriate system locations (slot zero Screen Holes), calling the ROM entry point and clearing the screen ready for action. From this point, anything sent to COUT will appear on the 80 column display.

If the system is fitted with a Soft Switch, this can be utilised by reading or writing location $C058 (Annunciator 0).

   VXSLOT      EQU $7F8                ; Holds the slot number in the form $Cn (AII Ref. P.83)
   
   VXINIT      LDX $C3                 ; represents the slot number in the form $Cn
               STX VXSLOT              ; store slot 3 for DOS
               STA $CFFF               ; turn off expansion ROMS (AII Ref. P.84)
               STA $C300               ; accessing the memory will enable slot 3 rom
   
               LDA #$8C
               JMP $C300               ; init card and clear the screen
   
               LDY $C058               ; See note above regarding the Soft Switch.
   
               RTS                     ; all done

Monitor Routines (COUT, RDKEY)

The Monitor routine COUT redirects through $36 and $37. When the Videoterm is initialised these addresses are shifted to the Videoterm’s IO routines ($C307). This means that COUT can be called from assembler as normal, output is handled by the Videoterm providing upper and lower case support. Similarly RDKEY uses $38 and $39 for redirection and again, these are set to point to the Videoterm IO routines ($C332). Therefore RDKEY can also be used as normal. The traditional ‘Shift Key Mod’ is supported by the Videoterm so upper and lower case can be used with the Shift key or Ctrl A.

The RDKEY routine waits for a keypress before continuing, however, many applications require that a keyboard be polled for a keypress without waiting. The traditional way to do this is through the use of the KEYDATA and KEYSTRB memory locations (see Apple II Reference Manual P.78-79).

Some consideration may be needed if you application uses this approach and lower case is required. Routines that do no redirect through $36/$37 and $38/$39 will need to be coded explicitly to handle lower case etc. as they do not use the Videoterms IO routines.

The following is an example of how the KEYDATA and KEYSTR addresses can be used with the ‘Shift Key Mod’ to implement lower case. The routine sets the Carry if there is a valid character in A. The output of this routine, in A, can be passed to COUT if required, in order to be displayed.

   KEYDATA         EQU $C000                       ; Page 79 Apple II Reference Manual
   KEYSTR          WEQ $C010
   VXSETUPFLAGS    EQU $07FB                       ; Videoterm Setup Flags (07F8 + Slot)
   
   TCKEYIN         LDA LKEYDATA
                   CLC
                   BPL :NOCHAR
                   STA LKEYSTRB
                   PHA                             ; save A
                   LDA VXSETUPFLAGS                ; check the Videoterm's shift flag
                   ASL
                   ASL
                   PLA                             ; carry now contains bit 6
                   BCC :DONE
                   CMP #$80                        ; dont convert special chars
                   BCC :DONE
                   BIT PB3
                   BMI :NOSHIFT
                   BPL :DONE 
   :NOSHIFT        ORA #$20    
   :DONE           AND #%01111111                  ; remove hi bit to get ASCII
                   SEC                             ; char available in A so set carry and exit
   :NOCHAR         RTS

Positioning the Cursor

This is the simplest approach and is simply a matter of sending an appropriate control code to COUT to move the cursor. For example the following sub-routine returns the cursor to the home position (0,0) without affecting the display.

   VXHOME      LDA #$19
               JMP COUT

This next sub-routine is similar but clears the display

   VXCLS       LDA #$0C
               JMP COUT

The following sub-routine moves the cursor forward one position and stops when column 80 is reached.

   VXWIDTH     EQU #80                 ; Number of columns (0-79)
   VXCHORZ     EQU $057B               ; Cursor Horizontal Position
   
   VXFORWARD   LDA #$1C                ; ctrl code for Forward
   :LOOP       LDY VXCHORZ             ; load the cursor horiz position
               CPY VXWIDTH-1           ; if at the last column then exit
               BEQ :END
               JSR COUT                ; send A to COUT to move right
               DEX                     ; Y contains the number of positions to move
               BNE :LOOP
   :END        RTS

The following sub-routine places the cursor at position (column and row) as defined by the X and Y registers respectively.

   * Cursor is positioned on the screen at row determined by Y and column determined by X
   * the function will not attempt to position the cursor beyond the margins
   
   VXHEIGHT    EQU #24
   
   VXGOTOXY    LDA #$1E
               JSR COUT
               TXA
               CMP VXWIDTH
               BCC :WIDTHOK
               LDA VXWIDTH-1
               CLC
   :WIDTHOK    ADC #$20                ; add 32 to X and limit it to the right margin
               JSR COUT
               TYA
               CMP VXHEIGHT
               BCC :BOTTOMOK
               LDA VXHEIGHT-1
               CLC
   :BOTTOMOK   ADC #$20                ; add 32 to y and limit it to the bottom margin
               JMP COUT

The following list describes the most popular control codes for cursor movement.

  • $1C Moves the cursor forward
  • $08 Moves the cursor back
  • $0A Moves the cursor down
  • $1F Moves the cursor up
  • $19 Home the cursor without affecting the display
  • $0C Clear the screen
  • $0D Carraige return
  • $0A Line reeed
  • $07 Bell
  • $01 Toggles the shift
  • $13 Stops and starts scrolling
  • $15 Moves the cursor forward and copies a charater
  • $0B Clear to end of screen
  • $1D Clear to end of line

Direct Manipluation of Screen Memory

Direct manipulation of the screen is useful for manipulating text on the screen. The ability to determine what exists at a certain screen position or display a character on the screen irrespective of the cursor position is very useful.

The 2K screen memory resides on the Videx Videoterm in 4 pages of 512 bytes. To write characters to the screen using screen memory, it is neccessary to use page relative addressing. The 512 byte page is located at address $CC00, this address is part of the Expansion ROM address space (see above).

The process of putting a character on the screen, requires the following steps

  • Determine the screen address for the target row and column within the 2K screen memory.
  • Determine the character set to use.
  • Determine the page (0-3) that the address falls into.
  • Activate the page.
  • Determine the address within the selected page.
  • Map the address within the apple memory map.

The good news is that this is not as hard as it may at first seem. The following sections describe the approach to be taken with examples in Merlin 32 based 6502 assembler.

Determine the Screen Address

The screen addresses are contiguous in that the first row on the screen is followed by the second row and so on. As each row contains 80 characters, it means that the address, within the 2K screen memory, of any character position can be found using the following formula (where X = Column and Y = Row).

   SCREEN ADDRESS = X + (Y * 80)

However, it is important to recognise that with a scrolling screen, the address of the first character is somewhat dynamic. Therefore to accurately determine the address for the position at X, Y it is necessary to modify the formula to include the contents of the START address, this is stored in address $6FB (see section Scratchpad RAM (Screen Holes)) and needs to be multiplied by 16 before being used.

   SCREEN ADDRESS = X + (Y * 80) + ($6FB * 16)

This can be achieved in assembler reasonably easily with the help of a multiply sub-routine, however, if we coded this as the equation is shown, it would not be too efficient. By rearranging the formula as shown below, it is possible to perform all multiplication with simple bit shifting and addition.

If we ignore X for the moment we can calculate a Base Address for the row determined by Y. Once this has been calculated we can simply add X (the column) to get the final address. So to calculate the base address the formula would be…

   (Y * 80) + ($6FB * 16)

…if, as mentioned above, we refactor this to the following we get the same result but with some benefits.

   (Y * 5 + $6FB) * 16

The benefit is that this is much easier to code and will run much faster. Below is an example sub-routine described in three parts.

   SCRADDR     TYA                     ; transfer Y into A
               STA LSTA                ; store at temporary location ideally (zero page)
               ASL                     ; multiply by 4 by shifting left twice
               ASL
               ADC LSTA                ; add the original value to make it Y * 5
               ADC $6FB                ; add the START address
               PHA                     ; save the result for later

Note that the highest value that Y should be is $17 (23d) therefore the two ASL instructions will clear any carry before the first ADC, so no need for CLC. Similarly, multiplying the maximum value ($17/23d) by 5 will not cause a carry so no need for CLC for the second ADC either. That is two bytes and four cycles saved.

All that is needed is to multiply the whole thing by 16. This can be done by shifting the value left 4 times with ASL. However, the result requires two bytes, therefore, each bit that is passed out from each shift will need to be shifted into the high byte from the right. An alternative approach is to shift the value calculated so far, four times to the right to remove the lower nibble and place what is left directly in the Base Address high byte. What we are effectively doing, is isolating the top four bits and then placing them in the Base Adress high byte. The effect is to multiply the top four bits by 16 e.g.

               LSR                     ; shift right to get the high nibble
               LSR
               LSR
               LSR
               STA LRESULT+1           ; zero page storage if possible

LRESULT now holds the ‘high’ byte of the base address we are calculating.

As the top four bits have already been multiplied by 16 and are now safely stored in the Base Address high byte we can safely multiply the value in A by 16, by shifting to the left 4 time without having to worry that the upper 4 bits will be shifted out e.g.

               PLA                     ; recover the value of Y * 5 + START
               ASL
               ASL
               ASL
               ASL
               STA LRESULT             ; zero page storage if possible
   
               RTS                     ; return from sub-routine

Once complete, LRESULT and LRESULT+1 will hold the base address of the row indicated by Y. By adding X we can get the correct screen address in the 2K range, more on that later.

This approach is exactly how the Videoterms firmware works. The Videoterm’s Screen Base Address located at $478 and $4F8 (see Scratchpad RAM (Screen Holes)) maintains the address of the current row (column 0), i.e. column 0 of the row that the cursor is on. This means that if the screen position of interest is on the current row, these addresses can be used as the base address instead of performing the calculations described above.

Now that we have a subroutine that calculate the base address for any column specified by Y we can start to look at some other elements of the process and put a character on the screen.

For the code that follows it is assumed that the character to be placed on the screen is in A and that the position on the screen is given by X (col) and Y (row).

Determine the Character Set to Use

At this point it is worth noting that the Videoterm has the option for installing an alternate character set ROM/EPROM. This can selected using bit 0 of the Flags memory location at $07FB (see above). When putting a character on the screen, this can be factored in quite easily by shifting bit 0 of the Flags memory location. Therefore, given that the character to be placed on the screen is in A, the screen position is identified by X and Y, and we have calculated the memory location of X, Y using the subroutine above, we can start as follows.

               ASL                     ; shift left and clear bit 7
               PHA                     ; save the shifted value
               LDA $07FB               ; load the flags
               LSR                     ; shift bit 0 into the Carry
               PLA                     ; recover the shifter character
               ROR                     ; rotate the Carry (flag) into bit 7 of the character
               PHA                     ; save A for later

The A register now holds the character but with the high bit set to 1 if the alternate character set is selected, and set to 0 if the normal character set is selected.

At this point we have to calculate the memory location of X, Y using the subroutine created earlier, in addition, the memory page needs identifying and selecting.

               CLC                     ; add X, the column
               TXA
               ADC LRESULT             ; base address low byte
               PHA                     ; save
               LDA #$00
               ADC LRESULT+1           ; base address high byte
               PHA                     ; save A for later

Determining the Page

At this point we have the screen address in the 2K range and need to determine the page, this is done by dividing the address by 512.

  PAGE = SCREEN ADDRESS / 512

However, when activating the correct page (see next section) the special page selection addresses are 4 bytes apart. With this in mind, it simplifies things if we obtain the value of the page, multiplied by 4.

The base address high byte (currently in the A register) can be used to determine the page number. The page can be determined from bits 1 and 2. Bits 1 and 2 of the high byte represent the number of 512s in the address. For example if the address was $05F0 e.g. 00000101 11110000 bits 1 and 2 would show that we should select page 2 (10) if we mask these bits and them multiply the byte by 2 the result we have is PAGE * 4.

               AND #%00000110          ; mask bits 1 and 2
               ASL                     ; multiply by 2

Reading the cards I/O page selection address (C0B0 + PAGE * 4, see above) is enough to activate the page.

               TAX                     ; X now contains PAGE * 4
               LDA C0B0,X              ; read the cards appropriate I/O page selection address

At this point the correct page has been selected, therefore, all we need to do is recover the lowest 9 bits of the final 16 bit address. This represents the page range address i.e. the address in the 512 byte range. A simple way to represent 9 bits is to use the low byte and the carry. The code below recovers the high byte from the stack and shifts bit 0 into the carry, before recovering the low byte.

               PLA                     ; recover the high byte
               LSR                     ; move bit 0 of high byte (bit 9 of address) into the carry
               PLA                     ; low byte is in A

Putting the Character on the Screen

All that remains is to map the 512 byte page address to the Apple II memory map. The display area starts at $CC00, this address is part of the Expansion ROM address space (see above). At this point, in code described here, the Carry effectively represents the high byte of the 512 byte page range address, therefore, by checking this, we can store the Character at the appropriate address within the Apple II memory map with a simple indexed store instuction.

               TAX                     ; move the low byte into index register X
               BCS :STORE              ; carry not set store A ($CC00 + address)
               STA $CC00,X
               BCC :DONE
   :STORE      STA $CC00 + $100,X      ; carry set store A ($CD00 + address)
   :DONE                               ; all done

Alternative Character Set

If an alternative character set ROM is fitted in U17, it can be selected with the following code

               LDA #26
               JSR COUT
               LDA #33
               JMP COUT

The standard characterset (U20) can be selected with the code

               LDA #26
               JSR COUT
               LDA #32
               JMP COUT

Each of the above are effectively sending Ctrl Z (26) followed by a 2 (32) or 3 (33) to switch the character set.

Graphics Characters

The following sub-routine sends a Videoterm graphic character (0-31) specified in A, to COUT. After checking for a valid character (<32) then the code sends a Ctrl Z followed by the graphic character. The Videx Videoterm manuals has a list of the characters that can be used, they include lines aand corners allowing boxes to be made.

               CMP #32                         
               BCS :END                        ; if A >= 32 then invalid
               PHA                             ; store A
               LDA #26                         ; Ctrl Z
               JSR COUT                        ; send Ctrl Z
               PLA                             ; recover A
               JMP COUT                        ; send A (Char Number 0 - 31)
   :END        RTS

The characters to create a simple box with title bar would be as follows;

           22 26 26 26 28
           21 00 00 00 21
           23 26 26 26 29
           21 00 00 00 21
           21 00 00 00 21
           19 26 26 26 25

Summary

Programming the Videoterm is relatively easy once a few basic concepts are understood. The control sequences provide a very simple method of moving the cursor around the screen and displaying alternate/graphic character sets. Depending upon the application, this may be sufficient.

When using direct access to the screen memory the process of calculating the paged address can seem a little complex. However, by creating some useful assembly language sub-routines perhaps based on the code above, this should be relatively easy to handle.