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.