6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Mon May 13, 2024 7:30 pm

All times are UTC




Post new topic Reply to topic  [ 2 posts ] 
Author Message
PostPosted: Mon Aug 14, 2023 11:00 am 
Offline

Joined: Fri Jul 09, 2021 10:12 pm
Posts: 741
At the weekend I wanted a way to receive serial data, and I don't have any UARTs, so I chose to do it through a 6522. I searched around but only found examples of bit-banging, which I didn't want to do - so I wrote some code to do it using the shift register, posted below. It uses CB2, CB1, the shift register, and Timer 2. Some benefits of this approach are low CPU cost and avoiding using up general purpose pins on the VIA; a drawback is that it is sensitive to interrupt response time.

The idea is quite simple - the TTL-level active-high serial RX line is connected to pin CB2, and we get an interrupt when it goes low for the start bit. We disable that interrupt temporarily, and trigger the shift register under T2 control to read in the bits, having set specific values on T2 to wait long enough for the first bit, and then the right amount between bits. The CPU can get on with other things for a while, and gets another interrupt when the shift register has read 8 bits. At this point we turn off the shift register, read out the byte, and re-enable the CB2 interrupt ready for the next byte.

My VIA is clocked at 16MHz, so for 115200 baud I need 139 ticks between bits. Higher baud rates would probably work but I haven't tried them; equally, slower VIA clocks would also work well, as 139 ticks is much more than is needed for stability I think.

This was fairly simple to do and has low CPU load - two interrupts per byte received. It is however sensitive to interrupt response time, especially for the CB2 interrupt, and this would get worse at higher baud rates. My system doesn't currently use interrupts for anything else, but if this was a problem then you could connect RXD straight to the CPU's /NMI pin and manually ignore those interrupts when they occur during the byte. Or just get a UART. :)

Note that the bytes arrive with their bits in the wrong order, and serial_getchar has to reverse them as it reads them out of the buffer. It would be possible to efficiently reverse two bytes at once, if there's another waiting in the buffer, but I didn't handle that yet.

Code:
; IRQ handling
;
; Currently the only IRQs used are for serial input.  VIA's CB2 is connected to our TTL-level
; active high serial receive line.  The start bit causes an interrupt, then the VIA's shift
; register is used to shift in the bits, causing another interrupt to read the byte and reset
; ready for the next one.
;
; The byte is placed in a circular input buffer.
;
;
; More low-level details:
;
; After CB2 goes low for the start of the start bit, we need the first shift register sample
; to be taken about 1.5 baud periods after the interrupt.  CB1 will be driven high at first,
; then toggled each time T2 expires.  After the first half-cycle, remaining half cycles will
; be equal to whatever we leave in T2's low latch, which needs to be half of BAUD_TICKS which
; itself is the baud period converted into I/O clocks.
;
; The time until the first sample is BAUD_TICKS plus whatever we initially load into T2.  To
; sample in the middle of each bit, we want to load BAUD_TICKS as the first period - this
; causes us to wait an entire baud period before CB1 goes low, and then as usual it will wait
; a further half baud period before it goes high, in the middle of the bit.
;
; Due to interrupt latency we may need to subtract some time from this first half-period
; though, and that's IRQLATENCY_TICKS.  We can also overcompensate here in order to sample
; the data stream earlier in the bit window.  This makes us less tolerant of drift, but gives
; a bit more time at the end of the byte to read the shift register, put everything back, and
; be ready for the next one.  It also makes us more resilient against a delayed CB2 interrupt
; for whatever reason.  Either of these might matter more at faster baud rates.


; Number of I/O clocks per baud bit
BAUD_TICKS = 16000000 / 115200

; Latency before T2 is loaded after start bit begins.  This depends on the amount of work
; the IRQ handler does and the ratio between the CPU clock and the I/O clock, and also whether
; IRQs were disabled or an IRQ was already being handled, etc
IRQLATENCY_TICKS = 66 * 16000 / 25175


irq_init:
    stz zp_serial_in_head
    stz zp_serial_in_tail
    stz zp_stray_interrupts

    ; Disable VIA interrupts and clear CB2 and SR flags
    lda #$7f : sta VIA_IER : sta VIA_IFR

    ; Disable shift register
    lda #VIA_DEF_ACR : sta VIA_ACR

    ; Define CB2 interrupt to occur on falling edge, other Cxx interrupts don't matter
    lda #$20 : sta VIA_PCR

    ; Enable CB2 interrupt and SR interrupt
    lda #$8c : sta VIA_IER

    cli

    rts


serial_getchar:
.(
    phy

    bit zp_serial_error      ; set overflow flag to bit 6

    ldy zp_serial_in_tail

    ; Wait for the head to be ahead of the tail
nodata:
    cpy zp_serial_in_head : beq nodata

    iny : sty zp_serial_in_tail

    lda serial_in_buffer,Y

    ; Swap the order of bits
    ldy #4
swaploop:
    asl : ror zp_scratch
    asl : ror zp_scratch
    dey : bne swaploop

    ply

    lda zp_scratch
    rts
.)


&irq:
.(
    ; It should be either a CB2 interrupt or a shift register interrupt
    pha
    lda VIA_IFR
    lsr : lsr : lsr : bcs serial_interrupt_sr    ; check for SR interrupt (bit 2)
    lsr : bcs serial_interrupt_cb2               ; check for CB2 interrupt (bit 3)

    ; otherwise it's a stray interrupt for some reason - increment a counter, signal an error and return
    inc zp_stray_interrupts
    lda #$f1 : sta DEBUGPORT
    pla : rti


serial_interrupt_cb2:
    lda #BAUD_TICKS-IRQLATENCY_TICKS-2 : sta VIA_T2CL   ; set T2 larger, to wait an extra half-bit before the first sample
    stz VIA_T2CH                                        ; start T2 right away as enabling the SR doesn't seem to do this consistently
    lda #VIA_DEF_ACR+$04 : sta VIA_ACR : bit VIA_SR     ; enable the shift register and issue a dummy read to start it
    lda #$08 : sta VIA_IER                              ; disable the CB2 interrupt while we read the data
    lda #(BAUD_TICKS/2)-2 : sta VIA_T2CL                ; set T2 to half the bit gap, minus two, for the rest of the byte

    pla : rti


serial_interrupt_sr:
.(
    lda #VIA_DEF_ACR : sta VIA_ACR  ; disable shift register
    lda #$08 : sta VIA_IFR          ; clear CB2 flag
    lda #$88 : sta VIA_IER          ; enable CB2 interrupt

    lda VIA_SR                      ; read the byte

    phy

    ; Increase the head pointer and store the data, unless it would conflict with the tail
    ldy zp_serial_in_head : iny
    cpy zp_serial_in_tail : beq full
    sty zp_serial_in_head

    sta serial_in_buffer,Y

    ply : pla : rti

full:
    ; The input buffer is full, discard the data and signal an error
    lda #$f6 : sta DEBUGPORT
    lda #$ff : sta zp_serial_error
    ply : pla : rti
.)

.)


Top
 Profile  
Reply with quote  
PostPosted: Tue Aug 15, 2023 6:50 am 
Offline

Joined: Tue Jul 05, 2005 7:08 pm
Posts: 993
Location: near Heidelberg, Germany
It looks like this is the same technique as the C64 9600 baud userport interface.

The C64 has the advantage that one of the CIAs is connected to NMI which helps with the interrupt response time.

André

Edit: kudos for the high baud rate you achieved

_________________
Author of the GeckOS multitasking operating system, the usb65 stack, designer of the Micro-PET and many more 6502 content: http://6502.org/users/andre/


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 2 posts ] 

All times are UTC


Who is online

Users browsing this forum: barrym95838 and 9 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: