; 6502 mode .p02 ; define where we want to be loaded .global __LOADADDR__ __LOADADDR__ = $7000 ; and where data should reside .global __DATAADDR__ __DATAADDR__ = $7200 ; 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 ; parameters and return values can reside in an unused area of the zero page .zeropage ; 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 ; specify the load address, so code and data will be located there ;.org __LOADADDR__ ; 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 ; driver state data .data ; 1=driver initialized initialized: .byte 0 ; saved IRQ vector oldvector: .word 0 ; state of the CA1 input: bit2=0 low level, bit2=1 high level 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 2 ; clearing is done by setting bit 7 to 0 and the desired interrupt to 1 lda #%00100000 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: ; arm VIA timer 2 by latching the counter 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 2 fired? lda VIA_IFR and #%00100000 ; zero, no interrupt beq @irqreturn ; timer 2 fired: rearm (we're in one-shot mode!), and clear the interrupt flag ; FIXME this should be done earlier, for more accurate timing ; FIXME we could skip writing the low byte here, writing the high byte is enough jsr starttimer @loadout: ; do we have more bits to send? lda outshift ; yes, skip loading next byte bne @shiftout ; load next byte from FIFO ; do we have more bytes? lda outqlen ; nope, skip ahead to input processing beq @shiftin ; load length ldx outqlen ; decrement, also gives the index dex ; load next byte from fifo lda outq,x ; store new queue length stx outqlen ; prepare shift register ; layout: 000000E7 6543210S ; bit0 = start bit = 0 ; bit1..7 = data0..6 clc rol sta outbuf ; bit8 = data7 ; bit9 = stop bit = 1 lda #%00000001 rol sta outbuf+1 ; we're shifting 10 bits out lda #10 sta outshift @shiftout: ; make sure overflow flag is clear, so we can do unconditional branches clv ; load shift register bit 0 lda outbuf and #%00000001 ; set or clear? beq @shiftoutclear ; set output lda #%00001000 ora VIA_PB bvc @shiftoutstore @shiftoutclear: ; clear output lda #%11110111 and VIA_PB @shiftoutstore: sta VIA_PB ; shift the next bit in ; 0 -> high byte -> carry lsr outbuf+1 ; carry -> low byte ror outbuf ; decrement counter dec outshift @shiftin: ; TODO process input bit-bang @storein: ; TODO process input fifo @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)