; Rick's 6502 BIOS v0.01 for AMELIE (started 2007/08/06) ; 2007/09/06 ; ; To be compiled with 6502asm ; http://www.heyrick.co.uk/amelie/ ; ; ROM ; base & org = &E000, top = &FFFF, inserts vector links ; =============================================================== ; = = ; = P A G E Z E R O L O C A T I O N S = ; = = ; =============================================================== ; ; Page Zero is defined thus: ; ; &0000 - &0001 = Intentionally unallocated (2 bytes) ; (so errors causing writes to +0 do not muck things up) ; ; &0002 = Watchdog counter (1 byte) ; ; &0003 - &005F = BIOS locations (93 bytes) ; (all areas not specifically defined are "reserved") ; ; &0005 = Detected memory size (in 256 byte pages) ; ; &0008 = Junk byte used by delay routine ; ; &000A = 50Hz ticker value ; &000B = Seconds increment ; &000C = Minutes increment ; &000D = Hours increment ; &000E = Days increment ; ; &0010 = LED active (which LEDs are we controlling?) ; &0011 = LED on/off (current LED state) ; &0012 = LED actustate (current LED state copy) ; &0013 = LED #1 flash counter ; &0014 = LED #2 flash counter ; &0015 = LED #3 flash counter ; &0016 = LED #4 flash counter ; &0017 = LED #1 flash reload ; &0018 = LED #2 flash reload ; &0019 = LED #3 flash reload ; &001A = LED #4 flash reload ; ; &0020 = Serial INPUT buffer READ pointer ; &0021 = Serial INPUT buffer WRITE pointer ; &0022 = Serial OUTPUT buffer READ pointer ; &0023 = Serial OUTPUT buffer WRITE pointer ; &0024 = Serial output INHIBITED if non-zero ; (other serial status is in Page Two) ; ; &0060 - &007F = Vector area (32 bytes) ; &0060 = IRQ vector (IRQV) ; &0062 = Ticker vector (TICKV) ; &0064 = VIA IRQ vector (VIAV) ; &0066 = ; &0068 = Serial buffer full vector (SFULLV) ; &006A = Serial RXByte vector (SRXV) ; &006C = Serial NewLine vector (SNEWLV) ; &006E = Serial TXByte vector (STXV) ; &0070 = Serial Event vector (SEVEV) ; &0072 = WatchFail vector (SWAFV) ; &0074 = ; &0076 = ; &0078 = ; &007A = ; &007C = ; &007E = System Abort vector (ABORTV) ; ; &0080 - &00BF = Application area (64 bytes) ; (defined according to application code) ; ; &00C0 - &00FF = BIOS reserved (64 bytes) ; (not currently used) ; ; ; ---------------------------------------------------------------- ; WATCHDOG: ; Counts UP at 50Hz. If it ever reaches 250, BIOS will force ; an abort (reset hardware to default - should force motors off) ; then fast-flash all LEDs, and either enter a recursive loop ; or some form of debugger according to build of BIOS. ; Application code MUST periodically reset this to zero; the ; BIOS will do likewise *ONLY* in delay code. ORG &0002 .watchdog DCW 0 ; ---------------------------------------------------------------- ; DETECTED MEMORY SIZE: ; Not currently implemented. A later version of the BIOS will ; probe memory to count the number of 256 byte 'pages' that ; are available. For now, we will be hardwired to 2048 bytes ; which is 8 pages. ORG &0005 .memory_size DCB 0 ; ---------------------------------------------------------------- ; JUNK BYTE: ; This is used to waste time in the delay routine. ORG &0008 .delay_junk DCB 0 ; ---------------------------------------------------------------- ; TICKER / NEWSEC / NEWMIN / NEWHOUR / NEWDAY: ; This is incremented at 50Hz. When it reaches 50 (1 second), ; it is reset and NEWSEC is incremented. ; NEWSEC = 60 := NEWSEC = 0 AND NEWMIN ++ ; NEWMIN = 60 := NEWMIN = 0 AND NEWHOUR ++ ; NEWHOUR = 24 := NEWHOUR = 0 AND NEWDAY ++ ; NEWDAY = 255 := WRAPS TO NEWDAY = 0 ORG &000A .ticker DCB 0 .newsec DCB 0 .newmin DCB 0 .newhour DCB 0 .newday DCB 0 ; ---------------------------------------------------------------- ; LED STATUS: ; Normally, the state of the LEDs (including flashing) are under ; control of the BIOS. These locations specify which ; LED active - Bits 0 to 3 denote which LEDs we are controlling. ; If bit is zero, ignore the state of this LED ; LED onoff - The value to write to the latch, as the latch is ; a write-only device. If you have a need to set ; the LEDs other than that provided by the BIOS, ; then mask OUT the LED bit in "LED active" and ; mask IN the LED bit as applicable in "LED onoff" ; and call BIOS to write LED status to the latch. ; LED actustate - As soon as the "LED onoff" is written to the ; latch, the value of it is written to this byte. ; In the future we can, under the ticker event, ; set the status of the four LEDs according to ; their flash counter. Once performed, we can then ; compare "LED onoff" ('pending') with this the ; actual state to see if the LED status has in ; fact changed. DO NOT WRITE THIS YOURSELF. ; LED flash counter #1 to #4 - counts down at 50Hz. When it ; reaches zero, the state of the LED is changed. ; If it is already AT zero, the LED state is not ; altered. ; LED flash reload #1 to #4 - when the counter reaches zero ; it is automatically reloaded with the timeout ; value. This holds the value to reload into the ; counter. ORG &0010 .led_active DCB 0 .led_onoff .led_status ; alias DCB 0 .led_actustate DCB 0 .led_counter_1 DCB 0 .led_counter_2 DCB 0 .led_counter_3 DCB 0 .led_counter_4 DCB 0 .led_reload_1 ; The ticker is 50Hz, therefore: DCB 0 ; .led_reload_2 ; Reload = 50 = 1 second on, 1 second off DCB 0 ; 25 = 1/2 sec on, 1/2 sec off ("slow") .led_reload_3 ; 13 = ~1/4 sec on, ~1/4 sec off ("fast") DCB 0 ; 10 = 1/5 sec on, 1/5 sec off; used ONLY .led_reload_4 ; by the BIOS when abort failure. DCB 0 ; It isn't possible to have differing on/off times. ; ---------------------------------------------------------------- ; SERIAL STATUS: ; This holds the rapid-access serial status information. ; The serial buffers are a 96 byte OUTPUT ring buffer and a 128 ; byte INPUT ring buffer. ; Incoming data is written to the INPUT buffer WRITE pointer. ; Serial RXByte reads from the INPUT buffer READ pointer. ; If the two equal, the buffer is empty. ; Outgoing data is read from the OUTPUT buffer READ pointer. ; Serial TXByte writes to the OUTPUT buffer WRITE pointer. ; If the two equal, the buffer is empty. ; If INHIBIT is NON-ZERO, serial transmission is disabled. ; ; Note that the pointers start at ZERO, so the effective address ; is [ + ]. ; ORG &0020 .serial_input_read DCB 0 .serial_input_write DCB 0 .serial_output_read DCB 0 .serial_output_write DCB 0 .serial_output_inhibit DCB 0 ; ---------------------------------------------------------------- ; VECTORS: ORG &0060 .irqv DCW 0 .tickv DCW 0 .viav DCW 0 ; unused at &0066 DCW 0 .sfullv DCW 0 .srxv DCW 0 .snewlv DCW 0 .stxv DCW 0 .sevev DCW 0 .swafv DCW 0 ; reserved at &0074 DCW 0 ; reserved at &0076 DCW 0 ; reserved at &0078 DCW 0 ; unused at &007A DCW 0 ; unused at &007C DCW 0 .abortv DCW 0 ; All other page zero memory locations are reserved, unused, or ; available according to address. ; ---------------------------------------------------------------- ; E N D O F P A G E Z E R O A L L O C A T I O N S . . . ; ---------------------------------------------------------------- ; ============================================================ ; = = ; = H A R D W A R E L O C A T I O N S = ; = = ; ============================================================ ; ; ------------- ; VIA LOCATIONS ; ------------- ORG &A000 .via_iorb ; In/Out port B DCB 0 .via_iora_handshake ; In/Out port A *with* *handshake* DCB 0 .via_ddrb ; Port B data direction DCB 0 .via_ddra ; Port A data direction DCB 0 .via_t1cl ; Timer1 Low-order Counter/Latch DCB 0 .via_t1ch ; Timer1 High-order Counter DCB 0 .via_t1ll ; Timer1 Low-order Latch DCB 0 .via_t1lh ; Timer1 High-order Latch DCB 0 DCB 0 ; Timer2 (not supported in AmélieEm DCB 0 ; and not used in Amélie) DCB 0 ; Shift Register (not supported or used) .via_acr ; Auxiliary Control Register DCB 0 .via_pcr ; Peripheral Control Register DCB 0 .via_ifr ; Interrupt Flag Register DCB 0 .via_ier ; Interrupt Enable Register DCB 0 .via_iora ; In/Out port A (without handshake) DCB 0 ; -------------- ; ACIA LOCATIONS ; -------------- READ: WRITE: ORG &A100 .acia_txrx ; Receive Transmit DCB 0 .acia_status ; Status .acia_reset ; Reset DCB 0 .acia_command ; Command Command DCB 0 .acia_control ; Control Control DCB 0 ; ------------------------ ; OPTION SELECTOR SWITCHES ; ------------------------ ORG &A200 .optselector DCB 0 ; -------------- ; LATCH LOCATION ; -------------- ORG &A300 .latch DCB 0 ; ------------------------------------------------------------- ; E N D O F H A R D W A R E A L L O C A T I O N S . . . ; ------------------------------------------------------------- BAS &E000 FORCE; force low address to be &E000 ; The above is important! ; Because we are assembling ROM code, we want only &E000 to &FFFF to be ; saved, but we also want to set up locations in page zero so we can ; refer to it by label. ; No problems - we'll just FORCE the base address back to &E000. This is ; one of the more advanced (read 'sneaky') features of 6502asm! ; ========================================================= ; = = ; = D E B U G G E R S O F T W A R E = ; = = ; ========================================================= ; ORG &F000 ; In a 'full' build, the EPROM is expected to be allocated ; as follows: ; ; &E000 - &F000 4096 bytes for the application code ; ; &F000 - &F3FF 1024 bytes for the debugger ; &F400 - &FFFF 3096 bytes for the BIOS and hardware vectors ; ; Don't know if 1K will be enough for a debugger; may stash ; some generic support routines (get input, print hex, etc) in ; the BIOS. May shift allocations. See how it goes... ; ; Output an ID string into the EPROM DCS "\r\n\r\n" DCS "##############################\r\n" DCS "# Amélie DEBUGGER v0.01 #\r\n" DCS "# dev. version - 2007/09/06 #\r\n" DCS "# (c) 2007 Rick Murray #\r\n" DCS "# www.heyrick.co.uk/amelie/ #\r\n" DCS "##############################\r\n\r\n" ; However, there is NO debugger as yet. .debugger_entry RTS ; ------------------------------- ; E N D O F D E B U G G E R ; ------------------------------- ; ====================================================================== ; = = ; = T H E B I O S C O D E F O L L O W S = ; = = ; ====================================================================== ; ORG &F400 ; Output an ID string into the EPROM DCS "\r\n\r\n" DCS "##############################\r\n" DCS "# Amélie BIOS v0.01 #\r\n" DCS "# dev. version - 2007/09/06 #\r\n" DCS "# (c) 2007 Rick Murray #\r\n" DCS "# www.heyrick.co.uk/amelie/ #\r\n" DCS "##############################\r\n\r\n" ; R E S E T V E C T O R ; ----------------------- DCS "#RSTVEC#" ; marker .reset_vector ; label is required by 6502asm's "ROM" command NOP SEI ; should be already, but no guarantees CLD ; decimal mode undefined at NMOS 6502 init LDX #&FF ; reset stack pointer TXS ; *** MARKER POINT ZERO: RESET INVOKED ; -> Poke latch to switch *ALL* of the LEDs on: 0 0 0 0 LDX #%00001111 STX latch ; Blank the first kilobyte of memory, in parallel ; (second kilobyte is unallocated, it is user's responsibility to clear) ; == TO BE WRITTEN == ; Set up the vector table ; Also sets 'slamdunk' interrupt branch point LDX #0 .bios_vecloop LDA bios_vectable, X STA irqv, X INX CPX #32 BNE bios_vecloop ; *** MARKER POINT ONE: MEMORY INITIALISED OKAY ; -> Poke latch to switch *FIRST* LED *OFF*: x 0 0 0 LDX #%00000111 STX latch ; SET UP THE ACIA ; =============== ; Set up ACIA for 9600bps 8N1 comms LDX acia_status ; Read status to clear any IRQs LDX #%00000011 ; No parity, no echo, DTR low STX acia_command LDX #%00011110 ; 1 stop bit, 8 bit data, 9600bps STX acia_control ; *** MARKER POINT TWO: SERIAL INITIALISED OKAY ; -> Poke latch to switch first *TWO* LEDs *OFF*: x x 0 0 LDX #%00000011 STX latch ; SET UP THE VIA ; ============== ; Clear ALL interrupts and interrupt sources LDX #%01111111 STX via_ifr ; IRQ flags; clear any pending STX via_ier ; IRQ enable; clear all ; Set up the ports LDX #%00000000 ; all bits INPUT (bits 6+7 (IIC) write-thru) STX via_ddra ; write it to port A's DDR STX via_iorb ; ensure port B is inactive before we set ; the DDR - paranoid, YES! but remember ; that we may be connected to MOTORS, okay? LDX #%00111111 ; all bits OUTPUT except bits 6+7 STX via_ddrb ; write it to port B's DDR ; Set up Timer1 to create 20uS ticks (50Hz) ; If we are clocked at 2MHz, and we want to tick away 20uS, ; then each tick is 0.0000005th of a second, and we want a ; tick every 0.02th of a second. This means that we must ; set up the VIA to tick away 40,000 ticks per interrupt ; period, to give us our 50Hz! ; That's low-order = 64 ; high-order = 156 LDX #64 ; Timer1 continuous, no SR, PA/PB unlatched STX via_acr ; Aux. Control Register LDX #&40 STX via_t1cl ; Timer1 Low Order Latch first LDX #&9C ; then High Order latch STX via_t1ch ; SIDE EFFECT - timer has now been started ; Enable interrupts from the VIA. In AmélieEm, this will ; decide whether or not the Timer is fully 'active', so ; we'll be a few cycles off at the outset. LDX #%11000000 ; enable Timer1 to generate interrupts STX via_ier ; *** MARKER POINT THREE: VIA INITIALISED OKAY ; -> Poke latch to switch first three LEDs *OFF*: x x x 1 LDX #%00000001 STX latch ; NOTHING should be pending, but just in case... CLI ; slamdunker will handle it right now NOP NOP SEI ; Set the default interrupt branch point LDX #0 LDA bios_irqhdladdr, X ; first byte STA irqv, X INX LDA bios_irqhdladdr, X ; second byte STA irqv, X ; Re-enable interrupts CLI ; *** MARKER POINT FOUR: BIOS INITIALISED OKAY ; -> Poke latch to switch *ALL* LEDs *OFF*: x x x x LDX #%00000000 STX latch ; BIOS startup complete! Call application... JMP app_code_entry ; D E F A U L T V E C T O R T A B L E ; --------------------------------------- ; NO entry for IRQV, it is patched in separately .bios_vectable DCW bios_irqslamdunkaddr ; Initial state of IRQV DCW bios_tickv_handler ; TICKV DCW bios_viav_handler ; VIAV DCW bios_vecignore_handler ; unused 1 DCW bios_sfullv_handler ; SFULLV DCW bios_vecignore_handler ; SRXV is ignored DCW bios_vecignore_handler ; SNEWLV is ignored DCW bios_stxv_handler ; STXV DCW bios_vecignore_handler ; SEVEV is ignored DCW bios_vecignore_handler ; SWAFV is ignored (then calls ABORTV) DCW bios_vecignore_handler ; reserved 1 DCW bios_vecignore_handler ; reserved 2 DCW bios_vecignore_handler ; reserved 3 DCW bios_vecignore_handler ; unused 2 DCW bios_vecignore_handler ; unused 3 DCW bios_abortv_handler ; ABORTV ; I R Q H A N D L E R S ; ----------------------- ; Addresses of built-in handlers .bios_irqhdladdr DCW bios_irq_handler ; address of... .bios_irqslamdunkaddr DCW bios_irq_slamdunk ; address of... ; PRIMARY IRQ VECTOR ; ------------------ ; This is an absolute INDIRECT jump, so the user code can choose to patch ; in their own IRQ handler if they are so inclined, or otherwise do some ; sort of preprocess before calling the BIOS handler... DCS "#IRQVEC#" .irq_vector JMP (irqv) ; jump indirect ; SLAMDUNK IRQ HANDLER ; -------------------- ; This is an "ignore it" handler devised for the boot-up process. It'll ; simply slam EVERY device with whatever pokes are necessary to inform ; the device that the IRQ has been picked up. Of course, we couldn't care ; less WHY they're interrupting us. At startup, we simply don't wanna know. .bios_irq_slamdunk ; Preserve registers PHA ; don't need to preserve X or Y, we don't use 'em ; Tell the VIA to take a hike LDA #%11111111 STA &A00D ; Same for the ACIA LDX acia_status ; (other hardware later) ; Restore state and exit handler PLA RTI ; CENTRALISED IRQ HANDLER ; ----------------------- ; Unless the user code is willing to replace ALL of the IRQ handler (which ; they may do via vectoring), we'll (eventually) be called... .bios_irq_handler ; If patching into IRQs at base level... ; ; Intention: ; User patches in to IRQ vector ; ; IRQ happens. ; User code called. ; Checks for it's own device. ; If not found, THIS function called. ; ; User must handle checks QUICKLY, it'll be called every 2cs; 40,000 cycles. ; STATE ON ENTRY *MUST* BE AS HARDWARE IRQ VECTOR SETS IT. PHA ; Save ENTIRE state TXA ; we'll optimise this later PHA TYA PHA ; NOTICE - we do NOT check for BRK ; PROBE - is IRQ from VIA? ; If YES: handle it ; ** PRIORITY FOR 50Hz TICKER ** ; BIT via_irq_address ; transfer b7 of via status to N register ; BMI ... ; if VIA, handle it LDA via_ifr BMI bios_viaIRQentry ; PROBE - is IRQ from ACIA? ; If YES: handle it LDA acia_status BMI bios_aciaIRQ ; NOTHING ELSE GENERATES INTERRUPTS! .bios_irq_exit PLA ; Restore state TAY PLA TAX PLA RTI ; DONE HERE! .bios_tickerIRQ ; This is the regular 50Hz ticker. ; Here we manage the WatchDog and the LED blinking. ; We also check for outstanding serial data to be sent. LDX #33 ; '!' STX acia_txrx ; fall through to kill interrupts .bios_viaIRQentry ; We must determine if the VIA's IRQ source ; is the 50Hz ticker or some sort of input. ; Sod off VIA (for now, later we'll actually DO something!) LDX #%11000000 STX via_ifr ; DO NOT USE THE FOLLOWING IN A HARDWARE BUILD, IT (SHOULD) ; RESULT IN '!' BEING OUTPUT 50 TIMES A SECOND! LDX #84 ; 'T' to mark that VIA interrupt happened STX acia_txrx LDX #105 ; 'i' STX acia_txrx LDX #99 ; 'c' STX acia_txrx LDX #107 ; 'k' STX acia_txrx JMP bios_irq_exit .bios_viaIRQ ; Only one source - bump switches on CA1 LDX #66 ; 'B' STX acia_txrx JMP bios_irq_exit .bios_aciaIRQ ; Serial event occurred... ; For testing emulator, if byte RECEIVED then we ; simply echo it back. LDA acia_status AND #%1000 ; mask out all except RRF BEQ bios_irq_exit LDX acia_txrx ; Read the byte from serial port STX acia_txrx ; and write it back out again JMP bios_irq_exit ; Amelie doesn't use NMIs at ALL, so force a reset ; if this is ever called, but it should NOT be ; because of the vector patch. DCS "#NMIVEC#" .nmi_vector JMP reset_vector ; V E C T O R H A N D L E R S ; ----------------------------- .bios_vecignore_handler ; Called for vectors which have no specific 'action', ; but can be tapped in to for extensibility. JMP bios_irq_exit .bios_tickv_handler ; The 50Hz ticker event ; ; Here we: ; Increment the ticker value ; Check the LED flash status ; Decrement the watchdog byte ; == TO BE WRITTEN == JMP bios_irq_exit .bios_viav_handler ; Some other VIA event happened ; ; Here we: ; Set all outputs to zero (should 'stop' hardware) ; Clear the interrupt ; ; This is a generic handler, it is expected that the ; application code will replace this with something ; that is appropriate for a VIA interrupt. ; == TO BE WRITTEN == JMP bios_irq_exit .bios_sfullv_handler ; The serial buffer has filled up. ; ; Here we: ; Set the handshaking to disable any further reception. ; ; This is normally a default BIOS service. ; == TO BE WRITTEN == JMP bios_irq_exit .bios_stxv_handler ; The serial port has sent a byte. ; ; Here we: ; Send another byte, if there is one. ; ; This is normally a default BIOS service. ; == TO BE WRITTEN == JMP bios_irq_exit .bios_abortv_handler ; The WatchDog has kicked in. ; ; Here we: ; Set ALL LEDs to very-fast-blink ; ; == TO BE WRITTEN == JSR debugger_entry .bios_abort_nodebugger JMP bios_irq_exit ; U T I L I T Y C O D E . . . ; ----------------------------- DCS "#UTILIS#" .bios_delay ; This causes a delay of exactly 1ms (assuming no interrupts occur ; during call or return). ; The delay time INCLUDES call and return, so: ; JSR bios_delay ; would hold off for exactly a millisecond. ; Cycles ; JSR bios_delay 6 SEI ; 2 PHP ; 3 PHA ; 3 LDA #218 ; 218 loops required 2 STA delay_junk ; Page zero, wastes some time... 3 .bios_deloop NOP ; 2 \ = 9 cycles DEC delay_junk ; 5 } multiply by 218 BNE bios_deloop; 2 / = 1962 cycles (!) LDA delay_junk ; just wasting time 3 PLA ; 4 PLP ; 4 CLI ; 2 RTS ; 6 ; ==== ; = 2000 cycles (1ms) .bios_nidelay ; This causes a delay of exactly 1ms AND ASSUMES INTERRUPTS DISABLED. ; Cycles ; JSR bios_nidelay 6 PHP ; 3 PHA ; 3 LDA #218 ; 218 loops required 2 STA delay_junk ; Page zero, wastes some time... 3 .bios_nidelp NOP ; 2 \ = 9 cycles DEC delay_junk ; 5 } multiply by 218 BNE bios_nidelp; 2 / = 1962 cycles (!) ROR delay_junk ; just wasting time 5 NOP ; 2 PLA ; 4 PLP ; 4 RTS ; 6 ; ==== ; = 2000 cycles (1ms) DCS "####END-OF-BIOS-CODE####" ; LOOKING FOR THE HARDWARE VECTORS? ; The "ROM" instruction at the beginning auto-defined these for us... ; -> nmi_vector ; reset_vector ; irq_vector ; ; However we now want to PATCH the NMI vector to call the RST_VECTOR: ORG &FFFA DCW reset_vector ; NOW INCLUDE THE APPLICATION CODE! CNT "appcode.s65"