diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..023a6d5 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ + +MACHINE := pet + +.PHONY: all + +all: rs232.bin + +rs232.bin: driver.o + cl65 -t ${MACHINE} -o $@ $^ + +%.o: %.a65 + ca65 -t ${MACHINE} -o $@ $< diff --git a/driver.a65 b/driver.a65 new file mode 100644 index 0000000..ea548bf --- /dev/null +++ b/driver.a65 @@ -0,0 +1,428 @@ + +; 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 #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 #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)