6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Sat Nov 23, 2024 8:58 pm

All times are UTC




Post new topic Reply to topic  [ 6 posts ] 
Author Message
PostPosted: Fri Oct 05, 2018 1:06 am 
Offline

Joined: Sun May 13, 2018 5:49 pm
Posts: 255
In trying to run Tali Forth 2's test suite on my real hardware, I was having trouble losing characters when the test printed a lot of output and didn't read further input for a while. I had previously been playing with delays at the end of the line ("Newline tx delay" under "Terminal settings" in minicom), but that makes the tests take longer to run, so I started down the path of implementing hardware handshaking. My FTDI cable seems to transmit 3 extra characters after it sees CTS be deasserted, so I realized I needed an interrupt driven receive routine.

I found Garth's interrupt driven serial receive code at http://wilsonminesco.com/6502interrupts/ starting in section 3.1, but I wanted to post some of the difficulties I had with it along with my solution.

Once I got the code working, I was still having problems with dropped characters. The important part in Garth's description is this part:
Quote:
For this discussion, we will also assume for the sake of simplicity that we're not transmitting, and that DCD and DSR are always "true," meaning they won't be causing any interrupts.

I didn't have any issues with DCD or DSR, but Tali Forth 2 does echo its input and was also printing results as well. After reading the ACIA (65C51 in my case) datasheet carefully several times and making some basic RX/TX tests I determined that I was not always getting an interrupt for every character. I finally narrowed it down to my transmit routine. Because the transmit routine reads the ACIA's status register, it's possible that it can clear an IRQ before it even happens (or at least that's what appeared to be happening on my hardware).

The fix is to have the transmit routine not only check the TDRE bit (Transmit Data Register Empty), but also check the RDRF bit (Receiver Data Register Full) every time it reads the STATUS register and then run the code to get a character if one showed up while we were waiting to transmit. Because my tx routine was handling incoming characters, I also turned off interrupts while it was running. I'm not sure if that was necessary or not, but it works for me. My final working code looks like:

Code:
    ;; Defines for hardware:
.alias ACIA_DATA    $7F80
.alias ACIA_STATUS  $7F81
.alias ACIA_COMMAND $7F82
.alias ACIA_CTRL    $7F83
    ;; Defines for the 256 byte circular buffer with two 8-bit pointers.
.alias ACIA_BUFFER acia_buff+2
.alias ACIA_RD_PTR acia_buff+0
.alias ACIA_WR_PTR acia_buff+1

    ;; Init ACIA to 19200 8,N,1
    ;; Uses: A (not restored)
Init_ACIA: 
        lda #$1F
        sta ACIA_CTRL
        lda #$09    ; RX interrupt on.  RTS low (asserted).
        sta ACIA_COMMAND
        ;; Initialize the buffer
        stz ACIA_RD_PTR
        stz ACIA_WR_PTR
        ; Turn on interrupts.
        cli
        rts

;; Helper routines for the ACIA buffer
;; from http://wilsonminesco.com/6502interrupts/index.html
WR_ACIA_BUF:
        ; Put value in increment pointer.
        ldx ACIA_WR_PTR
        sta ACIA_BUFFER,X
        inc ACIA_WR_PTR
        rts

RD_ACIA_BUF:
        ; Read value and increment pointer.
        ldx ACIA_RD_PTR
        lda ACIA_BUFFER,X
        inc ACIA_RD_PTR
        rts

ACIA_BUF_DIF:
        ; Subtract the buffer pointers (wrap around is fine)
        lda ACIA_WR_PTR
        sec
        sbc ACIA_RD_PTR
        rts

v_irq:                          ; IRQ handler (only handling ACIA RX)
SERVICE_ACIA:
        pha
        phx

        lda ACIA_STATUS
        ;and #$07                ; Check for errors.
        ;bne SERVICE_ACIA_END    ; Ignore errors.
        and #$08                 ; Check for RX byte available
        beq SERVICE_ACIA_END     ; No byte available.

        ; There is a byte to get.
        lda ACIA_DATA
        jsr WR_ACIA_BUF

        ; Check how many bytes in the buffer are used.
        jsr ACIA_BUF_DIF
        cmp #$F0
        bcc SERVICE_ACIA_END

        ; There are only 15 chars left - de-assert RTS
        lda #$01
        sta ACIA_COMMAND

SERVICE_ACIA_END:
        plx
        pla
        rti


        ;; Get_Char - get a character from the serial port into A.
        ;; Set the carry flag if char is valid.
        ;; Return immediately with carry flag clear if no char available.
        ;; Uses: A (return value)
Get_Char:
        ;;  Check to see if there is a character.
        jsr ACIA_BUF_DIF
        beq no_char_available
char_available:
        ;; See if RTS should be asserted (low)
        ;; buffer bytes in use in A from above.
        cmp #$E0
        bcs buf_full
        lda #$09
        sta ACIA_COMMAND
buf_full:
        phx                         ; Reading from buffer messes with X.
        jsr RD_ACIA_BUF             ; Get the character.
        plx
        ;; jsr Send_Char                ; Echo
        sec                         ; Indicate it's valid.
        rts
no_char_available:
        clc                         ; Indicate no char available.
        rts

   
kernel_getc:
        ; """Get a single character from the keyboard (waits for key).
        ; """
        ;; Get_Char_Wait - same as Get_Char only blocking.
        ;; Uses: A (return value)
Get_Char_Wait: 
        jsr Get_Char
        bcc Get_Char_Wait
        rts

 

kernel_putc:
        ; """Print a single character to the console. """
        ;; Send_Char - send character in A out serial port.
        ;; Uses: A (original value restored)
Send_Char:
        sei
        pha                     ;Save A (required for ehbasic)
wait_tx:                        ; Wait for the TX buffer to be free.   
        lda ACIA_STATUS
       
        ; A byte may come in while we are trying to transmit.
        ; Because we have disabled interrupts, and we've just read from
        ; the status register (which clears an interrupt),
        ; we might have to deal with it ourselves.
        pha             ; Save the status for checking the TRDE bit later.
        and #$08        ; Check for byte received
        beq check_tx    ; No bye received, continue to check TRDE bit.

        ; A byte was received while we are trying to transmit.
        ; Process it and then go back to checking for TX ready.
        phx             ; Save X as the buffer routines use it.
        lda ACIA_DATA
        jsr WR_ACIA_BUF
        ; Check how many bytes in the buffer are used.
        jsr ACIA_BUF_DIF
        cmp #$F0
        bcc tx_keep_rts_active

        ; There are only 15 chars left - de-assert RTS
        lda #$01
        sta ACIA_COMMAND
tx_keep_rts_active:   
        plx             ; Restore X

check_tx:
        ; Check to see if we can transmit yet.
        pla
        and #$10
        beq wait_tx                 ; TRDE is not set - byte still being sent.
        ; Send the byte.
        pla
        sta ACIA_DATA
        cli
        rts                       


Notes: acia_buf is defined elsewhere to be a spot near the end of ram, and I made the 256 byte circular buffer fit exactly into one page (I don't think that matters much). I added "ACIA" to all of the labels from Garth's example code. There are also some Tali Forth 2 specific routines that I did not include, so hopefully I got everything relevant.

This works and has been rock solid for me for a few days now. I can now paste a 52K file of tests into my terminal (running at 19,200bps to a 4MHz 65C02) and get back 67K of results with no dropped characters.


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 05, 2018 4:36 am 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8546
Location: Southern California
SamCoVT wrote:
Once I got the code working, I was still having problems with dropped characters. The important part in Garth's description is this part:
Quote:
For this discussion, we will also assume for the sake of simplicity that we're not transmitting, and that DCD and DSR are always "true," meaning they won't be causing any interrupts.

I didn't have any issues with DCD or DSR, but Tali Forth 2 does echo its input and was also printing results as well. After reading the ACIA (65C51 in my case) datasheet carefully several times and making some basic RX/TX tests I determined that I was not always getting an interrupt for every character. I finally narrowed it down to my transmit routine. Because the transmit routine reads the ACIA's status register, it's possible that it can clear an IRQ before it even happens (or at least that's what appeared to be happening on my hardware).

This kind of thing is addressed in the last paragraph of that section, right before the "3.2 INTERRUPT-DRIVEN ARBITRARY WAVEFORM GENERATOR" heading for the next section.

It's good to hear of your progress.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 05, 2018 2:34 pm 
Offline

Joined: Sun May 13, 2018 5:49 pm
Posts: 255
GARTHWILSON wrote:
This kind of thing is addressed in the last paragraph of that section, right before the "3.2 INTERRUPT-DRIVEN ARBITRARY WAVEFORM GENERATOR" heading for the next section.


I did see that mentioned, but I didn't think it applied to me at first, as I was only using the ISR for receiving characters and not for transmitting them. It certainly was a helpful comment, however, as it lead to my "ah-ha" moment when I realized the transmit routine was reading the status register and was somehow causing the ISR not to fire.

I also ended up disabling interrupts in my transmit routine because, if I didn't, I would sometimes get double characters. I believe the transmit routine and the ISR would detect an incoming character and both would read it. This works OK in my case because the tx routine is "taking over" all of the duties of the ISR, and the only thing the ISR does is to receive characters from the ACIA. I can see that if I had other interrupt sources, I would probably need to implement interrupt-driven transmit as well. As an alternative, I could also use the "send and wait" transmit method used with the WDC 65C51 as that method doesn't read the STATUS register.

Thank you, by the way, for creating that guide in the first place. I've used several of the sections as reference material when designing my SBC and it's made the process much easier.


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 05, 2018 4:24 pm 
Offline
User avatar

Joined: Tue Mar 05, 2013 4:31 am
Posts: 1385
I wrote a full-duplex ISR for the 6551 back in the 80's and did some updates to it a few years ago when I built up a new 65C02 system. The main ISR entry point was loosely based on an old 6551 datasheet. It might be helpful... as this has been working flawlessly with a FTDI UART/USB interface with a terminal program (ExtraPutty works very well) with RTS/CTS handshaking. I use a pair of 128 byte circular buffers, have receive IRQ active always and toggle the transmit IRQ on/off as required. Just the core 6551 ISR code is shown below, but you're more than welcome to a copy of the entire BIOS (6551/6522 support) is you like, which includes the routines that handle the character in/out routines to the respective buffers.

Code:
;new full duplex IRQ handler (54 clock cycles overhead to this point - includes return)
;
INTERUPT0   LDA   SIOSTAT   ;Get status register, xfer irq bit to n flag (4)
               BPL   REGEXT   ;if clear no 6551 irq, exit, else (2/3) (7 clock cycles to exit - take branch)
;
ASYNC         BIT #%00001000   ;check receive bit (2)
               BNE RCVCHR   ;get received character (2/3) (11 clock cycles to jump to RCV)
               BIT #%00010000   ;check xmit bit (2)
               BNE XMTCHR   ;send xmit character (2/3) (15 clock cycles to jump to XMIT)
;no bits on means CTS went high
               ORA #%00010000 ;add CTS high mask to current status (2)
IRQEXT      STA STTVAL ;update status value (3) (19 clock cycles to here for CTS fallout)
;
REGEXT      JMP   (IRQRTVEC0) ;handle next irq (5)
;
BUFFUL      LDA #%00001100 ;buffer overflow flag (2)
               BRA IRQEXT ;branch to exit (3)
;
RCVCHR      LDA SIODAT   ;get character from 6551 (4)
               BNE   RCV0   ;If not a null character, handle as usual and put into buffer   (2/3)
               BBR6   XMFLAG,BREAKEY   ;If Xmodem not active, handle BRK (5)
;
RCV0         LDY ICNT   ;get buffer counter (3)
               BMI   BUFFUL   ;check against limit, branch if full (2/3)
;
               LDY ITAIL ;room in buffer (3)
               STA IBUF,Y ;store into buffer (5)
               INY ;increment tail pointer (2)
               BPL   RCV1   ;check for wraparound ($80), branch if not (2/3)
               LDY #$00 ;else, reset pointer (2)
RCV1         STY ITAIL ;update buffer tail pointer (3)
               INC ICNT ;increment character count (5)
;   
               LDA SIOSTAT ;get 6551 status reg (4)
               AND #%00010000 ;check for xmit (2)
               BEQ REGEXT   ;exit (2/3) (40 if exit, else 39 and drop to XMT)
;
XMTCHR      LDA OCNT ;any characters to xmit? (3)
               BEQ NODATA ;no, turn off xmit (2/3)
;
OUTDAT      LDY OHEAD ;get pointer to buffer (3)
               LDA OBUF,Y ;get the next character (4)
               STA SIODAT ;send the data (4)
               INY ;increment index (2)
               BPL   OUTD1   ;check for wraparound ($80), branch if not (2/3)
               LDY #$00 ;else, reset pointer (2)
;
OUTD1         STY OHEAD ;save new head index (3)
               DEC OCNT ;decrement counter (5)
               BNE   REGEXT   ;If not zero, exit and continue normal stuff (2/3) (31 if branch, 30 if continue)
;
NODATA      LDY   #$09   ;get mask for xmit off / rcv on (2)
               STY SIOCOM ;turn off xmit irq bits (5)
               BRA REGEXT ;exit (3) (13 clock cycles added for turning off xmt)
;

_________________
Regards, KM
https://github.com/floobydust


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 05, 2018 8:07 pm 
Offline

Joined: Sun May 13, 2018 5:49 pm
Posts: 255
Thank you, floobydust. It's always nice to see someone else's code and see how they do things. I learned about the BBR and BBS instructions from reading your code, and it was nice to see BIT being used -- I have to read the description of that one every time I go to use it.

I may move to interrupt-driven TX as well in the future, and I'll certainly give your code a second reading when I get to that point. For the moment, I'm just happy that I can now paste large chunks of Forth code into my SBC without any errors.


Top
 Profile  
Reply with quote  
PostPosted: Fri Oct 05, 2018 8:17 pm 
Offline
User avatar

Joined: Tue Mar 05, 2013 4:31 am
Posts: 1385
Sounds good.... and yes, I tend to use quite a few of the newer CMOS instructions, after all, why not? I've no intentions of ever using the older NMOS parts ever. Also, my code has some odd support in the BIOS for a null character, which is used to break the monitor out of a macro loop... but the monitor also supports Xmodem/CRC downloads, so the need to receive a null character as it can be valid received data, hence the BBR instruction.

I have an updated BIOS/Monitor version for the 6551/6522, but that's on my main machine at the other house... a bit streamlined in the BIOS routines. Good luck with your SBC... always nice to see another project come to life.

_________________
Regards, KM
https://github.com/floobydust


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

All times are UTC


Who is online

Users browsing this forum: No registered users and 49 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: