Programming guide - nealcrook/multicomp6809 GitHub Wiki

This page aims to be a one-stop programming guide to Multicomp6809. All of technical information here can also be found in various header-files but it is convenient to combine it in one place.

Memory Map

After reset, the low 56KBytes of the address space contains RAM and the upper 8Kbytes contains ROM. There is a 16-byte slot stolen from near the top of the ROM space, which is used for I/O. Because the ROM continues to the top of the address space, it provides the reset vector and other exception vectors after reset. The memory map looks like this:

     $0000---->|==================================|
               |                                  |
               |                                  |
               |          56KBytes RAM            |
               |                                  |
               |                                  |
     $E000---->|==================================|
               |                                  |
               |              ROM                 |
               |                                  |
     $FFD0---->|==================================|
               |              I/O                 |
     $FFE0---->|==================================|
               |              ROM                 |
               |==================================|
     $FFFF---->

Boot/Reset

After reset, the MMU (described below) is disabled and the ROM is enabled. The ROM usually contains an executable image for Camel FORTH and the reset vector holds the entry point for that image.

When you switch from CamelForth to a different environment (FLEX, FUZIX, CUBIX, etc) the CamelForth ROM is disabled and RAM mapped into its memory region using the MMU. Therefore, to pass control back to CamelForth the minimum code sequence is:

CAM1      CLRA
          STA    MMUADR     DISABLE MMU, ENABLE ROM
          JMP    [VRESET]   JUMP THROUGH THE ROM RESET VECTOR

It is not necessary to disable the MMU: clearing ROMDIS is enough; when the ROM is enabled, this overrides any MMU mapping for that address region.

I/O Space

The I/O devices are implemented in the following hardware blocks:

  • SBCTextDisplayRGB - an 80-column character-mapped display that accepts input from a PS/2 keyboard and drives a VGA monitor. This device appears like a virtual UART. It implements a sub-set of the ASCII terminal control sequences in hardware.
  • bufferedUART - behaves like an 6850. Baud rate is hard-wired at ??
  • gpio - General-purpose I/O.
  • sd_controller - hardware controller for SD card.
  • mem_mapper2 - provides ROM page-out, RAM memory-mapping/paging, RAM write-protection, timer interrupt and single-step interrupt.

The I/O devices occupy 16 locations, as follows:

$FFD0 PS2/VGA Display virtual UART VDUSTA
$FFD1 PS2/VGA Display virtual UART VDUDAT
$FFD2 Serial A (UART1) UARTSTA1
$FFD3 Serial A (UART1) UARTDAT1
$FFD4 Serial B (UART2) UARTSTA2
$FFD5 Serial B (UART2) UARTDAT2
$FFD6 GPIO          GPIOADR
$FFD7 GPIO          GPIODAT
$FFD8 SD controller SDDATA
$FFD9 SD controller SDCTL
$FFDA SD controller SDLBA0
$FFDB SD controller SDLBA1
$FFDC SD controller SDLBA2
$FFDD MMU           TIMER
$FFDE MMU           MMUADR
$FFDF MMU           MMUDAT

The 'recommended' equates are shown below (taken from mc09.defs in the NitrOS9 source tree)

********************************************************************
* Multicomp09 Interrupts:
*
* NMI - wired to single-step logic
* FIRQ - (currently) unused
* IRQ - wired to timer interrupt, VDU/KBD virtual ACIA and serial
*       ports 1 and 2.

********************************************************************
* Multicomp09 I/O space is $FFD0-$FFDF (16 locations). The equates
* below describe the I/O registers.

********************************************************************
* VDU/KBD (VIRTUAL ACIA)
VDUSTA         EQU $FFD0
VDUDAT         EQU $FFD1

* SERIAL PORT 1
UARTSTA1       EQU $FFD2
UARTDAT1       EQU $FFD3

* SERIAL PORT 2
UARTSTA2       EQU $FFD4
UARTDAT2       EQU $FFD5

********************************************************************
* GPIO device
* SEE VHDL HEADER FOR PROG GUIDE
GPIOADR        EQU $FFD6
GPIODAT        EQU $FFD7

* values supported by GPIOADR register
GPDAT0         EQU 0
GPDDR1         EQU 1
GPDAT2         EQU 2
GPDDR3         EQU 3


********************************************************************
* SDCARD CONTROL REGISTERS
* SEE VHDL HEADER FOR PROG GUIDE
SDDATA         EQU $FFD8
SDCTL          EQU $FFD9
SDLBA0         EQU $FFDA
SDLBA1         EQU $FFDB
SDLBA2         EQU $FFDC

********************************************************************
* 50Hz TIMER INTERRUPT
* TIMER (READ/WRITE)
*
* AT RESET, THE TIMER IS DISABLED AND THE INTERRUPT IS DEASSERTED. TIMER READS AS 0.
* BIT[1] IS READ/WRITE, TIMER ENABLE.
* BIT[7] IS READ/WRITE-1-TO-CLEAR, INTERRUPT.
*
* IN AN ISR THE TIMER CAN BE SERVICED BY PERFORMING AN INC ON ITS ADDRESS
*
* READ  WRITE  COMMENT
*  N/A   $02   ENABLE TIMER
*  $00   $01   TIMER WAS/REMAINS DISABLED. N=0.
*  $02   $03   TIMER WAS/REMAINS ENABLED, NO INTERRUPT. N=0.
*  $80   $81   TIMER WAS/REMAINS DISABLED, OLD PENDING INTERRUPT CLEARED. N=1.
*  $82   $83   TIMER WAS/REMAINS DISABLED, OLD PENDING INTERRUPT CLEARED. N=1.
*
TIMER          EQU $FFDD

********************************************************************
* MEM_MAPPER2 CONTROL REGISTERS
* MMUADR (WRITE-ONLY)
* 7   - ROMDIS (RESET TO 0)
* 6   - TR
* 5   - MMUEN
* 4   - RESERVED
* 3:0 - MAPSEL
* MMUDAT (WRITE-ONLY)
* 7   - WRPROT
* 6:0 - PHYSICAL BLOCK FOR CURRENT MAPSEL

MMUADR         EQU $FFDE
MMUDAT         EQU $FFDF

LEDs

Interrupts

IRQ is driven from the timer/uart interrupt

FIRQ is tied off (permanently negated)

NMI is driven from the single-step logic.

PS2/VGA Display

Provides minimum 6850 compatibility.

UART

Provides minimum 6850 compatibility and a fixed baud rate of 115200bd.

control reg
    7            6            5          4          3        2         1         0
Rx int en | Tx control | Tx control | ignored  | ignored | ignored | reset A | reset B
            (INT/RTS)    (RTS)
            [    0            1    ] = RTS LOW
                                                           RESET = [  1         1   ]
status reg
    7            6            5         4           3        2         1         0
   irq    | parity err | overrun    | frame err | n_cts  | n_dcd |  tx empty | rx full
            always 0       n/a          n/a
           (no parity)

GPIO

The GPIO unit uses an indirection mechanism so that its 2 read/write I/O addresses can be used to access a larger set of control registers:

$FFD6 GPIOADR - specifies the register to access
$FFD7 GPIODAT - provides data read/write to selected register

Using a 16-bit store you can generate an atomic register select/data write. There is no equivalent mechanism for reads. Therefore, if any ISR ever accesses a GPIO register, you must bracket any GPIO register operations with disable/enable of interrupts. It's probably safest simply to never access GPIO within an ISR.

When you have written a value to GPIOADR (either with an 8-bit or 16-bit store) you can perform multiple GPIODAT reads and writes to the selected register; there is no need to re-write GPIOADR until you wish to select a different register. Beware of interrupts though; see note above.

For each group of physical pins there are 2 registers in GPIO:

  • The odd register is the data direction register (DDRn).

  • The even register is the data register (DATn).

  • A 0 in the data direction register marks the bit as an output, and a 1 marks it as an input (mnemonic: 0utput, 1nput).

  • A write to the data register sends the write data to the pin for each bit that is an output

  • A read from the data register samples the pin for each bit that is an input, and returns the last value written for each bit that is an output.

  • When you switch a pin from input to output, it immediately assumes the value that was most recently written to it.

After reset, GPIOADR=0, all DDR=0 (output) all DAT=0 (output low).

Four registers are implemented (more can be added easily, FPGA resources permitting):

0 DAT0 bits [2:0]
1 DDR1 bits [2:0]
2 DAT2 bits [7:0]
3 DDR3 bits [7:0]

SD Controller

SD registers 0-4 are in address space $FFD8-$FFDC.

 Address Register
    0    SDDATA        read/write data
    1    SDSTATUS      read
    1    SDCONTROL     write
    2    SDLBA0        write-only
    3    SDLBA1        write-only
    4    SDLBA2        write-only (only bits 6:0 are valid)

For both SDSC and SDHC (high capacity) cards, the block size is 512bytes (9-bit value) and the SDLBA registers select the block number. SDLBA2 is most significant, SDLBA0 is least significant.

For SDSC, the read/write address parameter is a 512-byte aligned byte address. ie, it has 9 low address bits explicitly set to 0. 23 of the 24 programmable address bits select the 512-byte block. This gives an address capacity of 2^23 * 512 = 4GB .. BUT maximum SDSC capacity is 2GByte.

For SDSC, the SDLBA registers are used like this:

 31 30 29 28.27 26 25 24.23 22 21 20.19 18 17 16.15 14 13 12.11 10 09 08.07 06 05 04.03 02 01 00
+------- SDLBA2 -----+------- SDLBA1 --------+------- SDLBA0 --------+ 0  0  0  0  0  0  0  0  0

For SDHC cards, the read/write address parameter is the ordinal number of 512-byte block ie, the 9 low address bits are implicity 0. The 24 programmable address bits select the 512-byte block. This gives an address capacity of 2^24 * 512 = 8GByte. SDHC can be upto 32GByte but this design can only access the low 8GByte (could add SDLBA3 to get the extra address lines if required).

For SDHC, the SDLBA registers are used like this:

 31 30 29 28.27 26 25 24.23 22 21 20.19 18 17 16.15 14 13 12.11 10 09 08.07 06 05 04.03 02 01 00
  0  0  0  0  0  0  0  0+---------- SDLBA2 -----+------- SDLBA1 --------+------- SDLBA0 --------+

The end result of all this is that the addressing looks the same for SDSC and SDHC cards.

SDSTATUS (RO)
   b7     Write Data Byte can be accepted
   b6     Read Data Byte available
   b5     Block Busy
   b4     Init Busy
   b3     Unused. Read 0
   b2     Unused. Read 0
   b1     Unused. Read 0
   b0     Unused. Read 0

SDCONTROL (WO)
   b7:0   0x00 Read block
          0x01 Write block

To read a 512-byte block from the SDCARD:

  • Wait until SDSTATUS=0x80 (ensures previous cmd has completed)
  • Write SDLBA0, SDLBA1 SDLBA2 to select block index to read from
  • Write 0 to SDCONTROL to issue read command
  • Loop 512 times: Wait until SDSTATUS=0xE0 (read byte ready, block busy), Read byte from SDDATA

To write a 512-byte block to the SDCARD:

  • Wait until SDSTATUS=0x80 (ensures previous cmd has completed)
  • Write SDLBA0, SDLBA1 SDLBA2 to select block index to write to
  • Write 1 to SDCONTROL to issue write command
  • Loop 512 times: Wait until SDSTATUS=0xA0 (block busy), Write byte to SDDATA

At HW level each data transfer is 515 bytes: a start byte, 512 data bytes, 2 CRC bytes. CRC need not be valid in SPI mode, except for CMD0.

SDCARD specification can be downloaded from https://www.sdcard.org/downloads/pls/ -- all you need is the "Part 1 Physical Layer Simplified Specification"

MMU

Overview of MMU Operation

The 6809 can address 64KBytes of memory directly, through a 16-bit address bus. This will be referred to as the "logical address space". The MMU considers the logical address space as 8, 8KByte blocks. Address bits [15:13] identify a logical block number (0-7, 8 in total).

Up to 1MByte of RAM is supported, referred to as the "physical address space" and needing 20-bit address bus. Address bits [19:13] identify a physical block number (0-127, 128 in total).

Within the MMU, 6809 address lines [15:13] are used to index a programmable look-up table. Each entry in the table holds a physical block number, which is driven out as address lines/chip selects to RAM.

There are 16 entries in the table, arranged in two groups of 8. A register bit "TR" is used to select which group is used. This allows software to switch rapidly between two sets of mappings.

To program a table entry you first select the table entry using a write to one register then select the physical physical block number for that entry using a write to another register. These two operations can be combined into a single 16-bit write.

Each physical block can be write-protected so that it acts like ROM.

Logical block 7 ($E000-FFFF) acts differently in three ways:

  • The boot ROM sits in this block, overlaying any RAM that is mapped there. The ROM is enabled after reset but can be disabled by a register write.
  • The multicomp I/O is decoded in this block, in address range $FFD0-$FFDF. The I/O is always present. If you map ROM to this block, accesses to ROM are ignored and I/O is accessed instead. If you map RAM to this block, write accesses go to I/O and to RAM (ie, the RAM locations at $FFD0-$FFDF are corrupted).
  • When the "Fixed RAM Top" (FRT) is enabled, the address range $FE00-FFCF, $FFE0-$FFFF are always mapped to physical RAM block 7. This 256byte region is the "vector page" on the COCO (interrupted here by the I/O space). This special mapping is performed for both reads and writes. Furthermore, when this mapping is enabled, I/O writes will corrupt the associated locations in physical RAM block 7, regardless of what RAM block is mapped into logical block 7.

At reset, the MMU is disabled (giving a 1-1 mapping) and the ROM is (re-)enabled but the mapping registers themselves are NOT reset.

MMU Programming Interface

The software interface is through 2 write-only registers that occupy unused addresses in the SDCARD address space:

$FFDE MMUADR
$FFDF MMUDAT

The bit-assignment of these registers is shown below:

MMUADR
b7       ROMDIS Disable ROM. 0 after reset.
b6       TR     Select upper group of mapping registers.
b5       MMUEN  Enable MMU. 0 after reset.
b4       NMI bit.
b3       } MAPSEL Select mapping register to
b2       } write through MMUDAT. MAPSEL values 0-7 control
b1       } the address translation when TR=0, MAPSEL values
b0       } 8-15 control the address translation when TR=1.

MMUDAT
b7       WRPROT When 1 the physical block is read-only
b6       } Physical block number associated with the logical
b5       } block selected by the current value of MAPSEL.
b4       }
b3       }
b2       }
b1       }
b0       }

Magic: for NitrosL2, want a fixed 512byte region of r/w memory at the top of the address space. There is no space to provide an enable for this behaviour (which I call FRT for FixedRamTop) and so some special magic is used, as follows:

IF ROMDIS=1 & MMUEN=1 then a write with b4=0 (see NMI behaviour below) and b7=0 and b5=1 does NOT enable the ROM but actually sets FRT=1. Any write with MMUEN=0 sets FRT=0 again. In summary:

Current           Action        End State
-----------------+-------------+-----------------
ROMDIS MMUEn FRT  ROMDIS MMUEn  ROMDIS MMUEn FRT
x      x     x    RESET         0      0     0
x      x     x    0      1      0      1     x
x      x     x    1      1      1      1     x
x      x     x    x      0      x      0     0
1      1     x    0      1      1      1     1

If you select a physical block that is outside the actual size of your RAM, the behaviour is undefined (it will probably alias).

When MMUEN=0, logical blocks 0-7 are mapped to physical blocks 0-7.

You can write MMUDAT, MMUADR as separate 8-bit stores or as a 16-bit store.

The NMI bit should be set using an 8-bit store. On writes to MMUADR with bit4=1, the state of the other data bits is ignored (they do not change). The avoids the need to know the current state of any of the other bits. The NMI bit is self-clearing and generates an NMI edge after a specific delay. As part of a carefully-controlled code sequence it can be used to interrupt after execution of a single instruction (see Single Step, below)

Remember, these two registers are WRITE-ONLY!

Timer

The timer provides a regular interrupt 50 times a second by dividing down the 50MHz master input clock. The programming interface is a single r/w register:

$FFDD TIMER

The behaviour of this register is designed to allow simple and efficient software handling of the interrupt.

  • At reset, the timer is disabled and the interrupt is deasserted.
  • bit[1] is read/write, timer enable.
  • bit[7] is read/write-1-to-clear, interrupt.

In an ISR the timer can be serviced by performing an INC on its address.

Read  Write  Comment
 n/a   $02   Enable timer
 $00   $01   Timer was and remains disabled. N=0.
 $02   $03   Timer was and remains enabled, no interrupt. N=0.
 $80   $81   Timer was and remains disabled, old pending interrupt cleared.
             N=1.
 $82   $83   Timer was and remains enabled,  old pending interrupt cleared.
             N=1.

Single Step

Single-step is controlled by the NMI bit (bit4) in the MMUADR register (see MMU section above)

Start with the application context stored on the system stack. The stacked copy of PC points to the next instruction to be executed. The stacked CC has the E (entire) bit set. Now execute (exactly) this code sequence:

   LDA #$10      * set bit 4
   STA MMUADR    * trigger NMI
   RTI           * resume application

The RTI restores the application context from the system stack. The NMI is generated on the first instruction executed at the recovered PC, so that this instruction executes to completion and then the processor stacks the application context and continues execution at the address indicated by the NMI exception vector.

The NMI service routine can perform the same code sequence to do another single step.