; 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 = 600 ; 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 ; TODO add a state variable that can be monitored by the BASIC WAIT command ; this is for the load address, so we can generate PRG files. ; works as long as the code segment comes right after these two bytes, ; i.e. as long as the LOADADDR segment resides at $load_address - 2 .segment "LOADADDR" .export LOADADDR LOADADDR: .word *+2 ; 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 .bss ; 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: ; FIXME this doesn't work if we're loading from ROM lda #1 sta initialized ; 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 sei ; 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+1 ; VIA timer 2 cannot be enabled and disabled on the fly, so we'll ; configure it here and arm it later ; one-shot mode lda VIA_CR and #%11011111 sta VIA_CR ; enable interrupt ; interrupt enable is done by setting bit7 and the desired interrupt ; bit in IER to 1 (no need to read-modify-write) lda #%10100000 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 cli ; 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 sei ; 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 cli ; 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 sei ; 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 cli ; 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 sei ; 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 cli ; return rts ; start the timer starttimer: ; arm VIA timer 2 by latching the counter period = PHI2_CLOCK/BAUD_RATE lda #period sta VIA_T1CH ; 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 sei ; 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 CRA 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 ; check if we're already in the middle of a reception lda inshift ; yes, don't do anything bne @handletimer ; nope, initiate reception lda #10 sta inshift @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 ; mark or blank? beq @shiftoutblank ; set output high lda #%00001000 ora VIA_PB bvc @shiftoutstore @shiftoutblank: ; set output low 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: ; FIXME we should check the start and stop bits, to synchronize RS232 transmissions ; check first if a reception is in progress lda inshift ; nope, skip input processing beq @irqreturn ; for unconditional branch clv ; pick up the current CA1 state lda ca1state and #%00000100 ; mark or blank? beq @shiftinblank ; shift in high bit sec bvc @shiftinstore @shiftinblank: ; shift in low bit clc @shiftinstore: ; rotate carry bit into buffer rol inbuf rol inbuf+1 ; decrement counter dec inshift @storein: ; did we complete a transmission? bne @irqreturn ; check if we have space available in the buffer lda inqlen cmp #16 ; nope, drop this byte bcc @irqreturn ; yes, store it tax lda inbuf+1 lsr lda inbuf ror sta inq, x ; and update the buffer length dex stx inqlen @irqreturn: ; restore registers pla tay pla tax pla ; we don't need to re-enable interrupts here, ; this will happen automatically when flags are restored later ;cli ; jump to previous handler ; we don't need to return or restore flags, this will be done by the chained interrupt handler jmp (oldvector)