428 lines
8.2 KiB
Text
428 lines
8.2 KiB
Text
|
|
; 6502 mode
|
|
.p02
|
|
|
|
; load useful register/memory locations
|
|
.include "pet.inc"
|
|
|
|
; for some reason, the PIA registers are missing in pet.inc...
|
|
PIA1 := $E810
|
|
PIA1_PA := PIA1+$0 ; PORT A or DDR A: Data Direction Register A
|
|
PIA1_CRA := PIA1+$1 ; CRA: Control Register A
|
|
PIA1_PB := PIA1+$2 ; PORT B or DDR B: Data Direction Register B
|
|
PIA1_CRB := PIA1+$3 ; CRB: Control Register B
|
|
PIA2 := $E820
|
|
PIA2_PA := PIA2+$0
|
|
PIA2_CRA := PIA2+$1
|
|
PIA2_PB := PIA2+$2
|
|
PIA2_CRB := PIA2+$3
|
|
|
|
; 1MHz phase2 clock
|
|
PHI2_CLOCK = 1000000
|
|
; don't set the baud rate too high, or other operations will be starved
|
|
BAUD_RATE = 300
|
|
|
|
; we expect to be loaded somewhere in the middle of user ram
|
|
; TODO verify if this is safe with the Editor/Basic 2.0 ROM
|
|
.org $7000
|
|
|
|
; entry points
|
|
.code
|
|
; these are convenience entry points right at the beginning of the page,
|
|
; to reduce dependency on code size.
|
|
; relocatable code would be perfect, but that's a lot more work.
|
|
.export rs_install
|
|
rs_install:
|
|
jmp install
|
|
.export rs_uninstall
|
|
rs_uninstall:
|
|
jmp uninstall
|
|
.export rs_read
|
|
rs_read:
|
|
jmp read
|
|
.export rs_write
|
|
rs_write:
|
|
jmp write
|
|
|
|
; parameters and return values
|
|
.data
|
|
; available bytes to read or write
|
|
.export rs_available
|
|
rs_available: .byte 0
|
|
; single byte transfer from input / to output fifo
|
|
.export rs_data
|
|
rs_data: .byte 0
|
|
; status of operation
|
|
rs_status_ok = 0
|
|
rs_status_read_buffer_empty = 1
|
|
rs_status_write_buffer_full = 2
|
|
rs_status_device_not_initialized = 3
|
|
rs_status_device_already_initialized = 4
|
|
.export rs_status
|
|
rs_status: .byte 0
|
|
|
|
; driver state data
|
|
|
|
; 1=driver initialized
|
|
initialized: .byte 0
|
|
; saved IRQ vector
|
|
oldvector: .word 0
|
|
; current CRA value on CA1 change (bit contains the state of CA1)
|
|
ca1state: .byte 0
|
|
; output buffer
|
|
; this is 16bit, because we need to process start and stop bits as well
|
|
outbuf: .word 0
|
|
; current shifted bit (if 0, no data is being transferred)
|
|
outshift: .byte 0
|
|
; output queue (16 bytes)
|
|
; this is a push from the front - pop from the tail fifo
|
|
outqlen: .byte 0
|
|
outq: .res 16, 0
|
|
; input buffer
|
|
inbuf: .word 0
|
|
; current shifted bit (if 0, no data is being transferred)
|
|
inshift: .byte 0
|
|
; input queue (16 bytes)
|
|
; this is a push from the tail - pop from the front fifo
|
|
inqlen: .byte 0
|
|
inq: .res 16, 0
|
|
|
|
; main code follows
|
|
.code
|
|
|
|
; driver installation, must be called once to set up IRQs, etc.
|
|
install:
|
|
; check if we're already initialized
|
|
lda initialized
|
|
beq @hwinit
|
|
; signal error
|
|
lda #rs_status_device_already_initialized
|
|
sta rs_status
|
|
; and return
|
|
rts
|
|
|
|
@hwinit:
|
|
; disable interrupts, so we're not disturbed
|
|
cli
|
|
|
|
; save the previous handler
|
|
lda IRQVec
|
|
sta oldvector
|
|
lda IRQVec+1
|
|
sta oldvector+1
|
|
; assign our custom interrupt handler to the IRQ vector
|
|
lda #>irqhandler
|
|
sta IRQVec
|
|
lda #<irqhandler
|
|
sta IRQVec+1
|
|
|
|
; the timer is only started when a transmission is actually initiated
|
|
; but we'll enable VIA timer 1 interrupt
|
|
lda VIA_IER
|
|
ora #%01000000
|
|
sta VIA_IER
|
|
|
|
|
|
; set up the PIA1 and VIA to process I/O
|
|
; TXD: VIA PB3, output
|
|
lda VIA_DDRB
|
|
ora #%00001000
|
|
sta VIA_DDRB
|
|
; RXD: PIA1 CA1, input, interrupt
|
|
lda PIA1_CRA
|
|
; we start with the negative transition (RS232 start bit: H->L)
|
|
and #%11111101
|
|
; IRQ enable
|
|
ora #%00000001
|
|
sta PIA1_CRA
|
|
; CTS: PIA1 CB2, output (optional)
|
|
; RTS: PIA1 PA4, input (optional)
|
|
|
|
; clear some of the state variables
|
|
; the buffers are controlled by other variables anyway and don't need clearing
|
|
lda #0
|
|
sta ca1state
|
|
sta outshift
|
|
sta outqlen
|
|
sta inshift
|
|
sta inqlen
|
|
|
|
; we're ready
|
|
lda #1
|
|
sta initialized
|
|
; and ok
|
|
lda #rs_status_ok
|
|
sta rs_status
|
|
; fire away
|
|
sei
|
|
; return
|
|
rts
|
|
|
|
; driver uninstallation, must be called once to set up IRQs, etc.
|
|
.export uninstall
|
|
uninstall:
|
|
; check if we're already initialized
|
|
lda initialized
|
|
bne @hwuninit
|
|
; signal error
|
|
lda #rs_status_device_not_initialized
|
|
sta rs_status
|
|
; and return
|
|
rts
|
|
|
|
@hwuninit:
|
|
; disable interrupts, so we're not disturbed
|
|
cli
|
|
|
|
; restore the previous handler
|
|
lda oldvector
|
|
ldx oldvector+1
|
|
sta IRQVec
|
|
stx IRQVec+1
|
|
|
|
; disable all interrupts
|
|
; RXD: PIA1 CA1
|
|
lda PIA1_CRA
|
|
; IRQ disable
|
|
and #%11111110
|
|
sta PIA1_CRA
|
|
; and the VIA timer 1
|
|
lda VIA_IER
|
|
and #%10111111
|
|
sta VIA_IER
|
|
|
|
; we leave the I/O pins and the state alone
|
|
|
|
; but we'll go back to uninitalized state
|
|
lda #0
|
|
sta initialized
|
|
|
|
; and ok
|
|
lda #rs_status_ok
|
|
sta rs_status
|
|
; the rest of the system will probably want interrupts enabled
|
|
sei
|
|
; return
|
|
rts
|
|
|
|
|
|
; write one byte to FIFO
|
|
.export write
|
|
write:
|
|
; check if we're already initialized
|
|
lda initialized
|
|
bne @dowrite
|
|
|
|
; signal error
|
|
lda #rs_status_device_not_initialized
|
|
sta rs_status
|
|
; and return
|
|
rts
|
|
|
|
@dowrite:
|
|
; disable interrupts while we write into the buffer
|
|
cli
|
|
|
|
; test if there's space in the buffer first
|
|
lda #16
|
|
cmp outqlen
|
|
beq @wrnodata
|
|
|
|
; move data up first
|
|
ldy outqlen
|
|
; check if we need to move data at all
|
|
bne @wrbufend
|
|
|
|
; move all existing elements up, starting from the top
|
|
@wrbufloop:
|
|
lda outq-1,y
|
|
sta outq,y
|
|
dey
|
|
bne @wrbufloop
|
|
@wrbufend:
|
|
|
|
; write data to head
|
|
lda rs_data
|
|
sta outq
|
|
; update queue length
|
|
ldy outqlen
|
|
iny
|
|
sty outqlen
|
|
; also store to return value
|
|
sty rs_available
|
|
|
|
; all right
|
|
lda #rs_status_ok
|
|
sta rs_status
|
|
|
|
; an unconditional branch would be useful...
|
|
clc
|
|
bcc @wrdone
|
|
|
|
@wrnodata:
|
|
; return error
|
|
lda #rs_status_write_buffer_full
|
|
sta rs_status
|
|
|
|
@wrdone:
|
|
; and re-enable
|
|
sei
|
|
|
|
; return
|
|
rts
|
|
|
|
; read one byte from FIFO
|
|
.export read
|
|
read:
|
|
; check if we're already initialized
|
|
lda initialized
|
|
bne @doread
|
|
|
|
; signal error
|
|
lda #rs_status_device_not_initialized
|
|
sta rs_status
|
|
; and return
|
|
rts
|
|
|
|
@doread:
|
|
; disable interrupts while we read the buffer
|
|
cli
|
|
|
|
; test if we have any data first
|
|
lda inqlen
|
|
beq @rdnodata
|
|
|
|
; return data from head
|
|
lda inq
|
|
sta rs_data
|
|
|
|
; update the queue length
|
|
ldy inqlen
|
|
dey
|
|
sty inqlen
|
|
; also store to return value
|
|
sty rs_available
|
|
; check if we need to move any data
|
|
bne @rdbufend
|
|
|
|
; move all other data elements down
|
|
ldx #0
|
|
@rdbufloop:
|
|
lda inq+1,x
|
|
sta inq,x
|
|
inx
|
|
dey
|
|
bne @rdbufloop
|
|
@rdbufend:
|
|
|
|
; all right
|
|
lda #rs_status_ok
|
|
sta rs_status
|
|
|
|
; an unconditional branch would be useful...
|
|
clc
|
|
bcc @rddone
|
|
|
|
@rdnodata:
|
|
; return error
|
|
lda #rs_status_read_buffer_empty
|
|
sta rs_status
|
|
|
|
@rddone:
|
|
; and re-enable
|
|
sei
|
|
|
|
; return
|
|
rts
|
|
|
|
; start the timer
|
|
@starttimer:
|
|
; set up the VIA timer 1 to fire periodically
|
|
period = PHI2_CLOCK/BAUD_RATE
|
|
lda #>period
|
|
sta VIA_T1CL
|
|
lda #<period
|
|
sta VIA_T1CH
|
|
; VIA timer 1 free-run mode
|
|
lda VIA_CR
|
|
ora #%01000000
|
|
sta VIA_CR
|
|
; return
|
|
rts
|
|
|
|
; stop the timer
|
|
@stoptimer:
|
|
; VIA timer 1 disabled
|
|
lda VIA_CR
|
|
and #%10111111
|
|
sta VIA_CR
|
|
; return
|
|
rts
|
|
|
|
; IRQ handler
|
|
.interruptor irqhandler
|
|
irqhandler:
|
|
; disable interrupts
|
|
cli
|
|
; clear decimal flag to avoid unexpected behavior
|
|
cld
|
|
; save registers
|
|
pha
|
|
txa
|
|
pha
|
|
tya
|
|
pha
|
|
|
|
; test for the interrupts we're expecting
|
|
|
|
@handleflip:
|
|
; read PIA1 CRA, interrupt flag is automatically cleared
|
|
lda PIA1_CRA
|
|
; save for later, so we don't need to fetch it again
|
|
tax
|
|
; bit 7 is the CA1 interrupt flag - fired?
|
|
and #%10000000
|
|
; nope, skip flip handling
|
|
beq @handletimer
|
|
|
|
; handle CA1 interrupt
|
|
; fetch saved CRA
|
|
txa
|
|
; save current value for later use:
|
|
; bit 2 is contains the active value of CA1, which is what we want to know
|
|
sta ca1state
|
|
; invert transition (H->L <-> L->H)
|
|
eor #%00000010
|
|
; mask out interrupt flag - we don't want another IRQ right away
|
|
and #%01111111
|
|
sta PIA1_CRA
|
|
|
|
@handletimer:
|
|
; VIA timer 1 fired?
|
|
lda VIA_IFR
|
|
and #%01000000
|
|
; zero, no interrupt
|
|
beq @irqreturn
|
|
|
|
; handle timer interrupt
|
|
; reset timer 1 interrupt
|
|
; FIXME should we do this? perhaps someone else is also expecting a VIA interrupt
|
|
; if not, we *must* reset it
|
|
lda VIA_T1CL
|
|
|
|
@processfifo:
|
|
; TODO process input and output
|
|
|
|
@irqreturn:
|
|
; restore registers
|
|
pla
|
|
tay
|
|
pla
|
|
tax
|
|
pla
|
|
; enable interrupts
|
|
; FIXME should we do this here? what's common practice? is this compatible with other interrupt handlers
|
|
sei
|
|
; jump to previous handler
|
|
; we don't need to return or restore flags, this will be done by the chained interrupt handler
|
|
jmp (oldvector)
|