Re: Handling SSC interrupts in Applesoft BASIC
- From: dempson@xxxxxxxxxxxxx (David Empson)
- Date: Wed, 23 Sep 2009 21:00:23 +1200
Jeremy Visser <jeremy@xxxxxxxxxxx> wrote:
I've been reading John B. Matthews article on 'Programming the Super
Serial Card': http://home.roadrunner.com/~jbmatthews/ssc.html
He has written an assembly-language program that reads and writes to the
Super Serial Card. I'm no assembly programmer, but from what I
understand, he installs an interrupt handler, which gets it to call his
program each time data is received on the serial card.
I'm looking to do the same in BASIC, as polling is simply horrible, and
assembly is way over my head. (This is the point where you can tell me
to jump off a cliff. ;) )
Hi Jeremy
For reference: in my day job, I am an embedded systems programmer,
dealing with serial communications and related issues like interrupts,
so I'm very familiar with the general principles.
I'm also a lapsed Apple II programmer - I started out on the Apple II,
originally in Pascal, then taught myself Applesoft BASIC and assembly
language. (That covers about 1981-1983.)
I haven't used the SSC much, but I am reasonably familiar with it.
A little background on the concept of an interrupt.
An interrupt is a signal from some peripheral device in the comptuer to
request attention from the CPU. Assuming the perhipheral has been
configured to generate interrupts, and the CPU is paying attention (you
can block interrupts from being processed), what typically happens is:
1. The peripheral activates the interrupt request signal.
2. The CPU finishes executing the current assembly language instruction.
3. The CPU notices that an interrupt is pending.
4. The CPU saves the location at which it was executing, and the
processor flags, then jumps to a fixed location to process the
interrupt.
5. In the Apple II, the initial interrupt handler (in ROM) does some
minimal additional setup, then jumps via a RAM-based vector, allowing a
user-defined interrupt handler (written in assembly language) to be
called.
6. The user-defined interrupt handler may need to save additional
resources, such as the rest of the CPU registers which it needs to use.
7. The interrupt handler works out which interrupt occurred, by checking
the status register of any peripherals which may have interrupts
enabled.
8. Once the appropriate interrupt source has been found, the interrupt
handler does whatever is required to deal with the interrupt, such as
reading a receive data byte and storing it somewhere else, such as in a
circular buffer.
9. The interrupt handler restores the registers it saved in step 6.
10. The interrupt handler ends with the "return from interrupt"
instruction, which causes the CPU to restore the processor flags and
execution address which were saved in step 4.
11. The CPU resumes execution from the instruction after the one it was
executing in step 2.
Some of the details vary depending on the operating system and Apple II
model. For example, the IIc, IIgs and enhanced IIe have a more complex
ROM-based interrupt handler which can deal with several built-in
interrupt sources, and ProDOS can also hook into the interrupt mechanism
and supports installation of multiple interrupt handlers (with different
entry/exit conditions).
The key point: all of this assumes the interrupt handler is written in
assembly language.
So from what I understand, in order to do the same in BASIC, I'll need
to do two things:
1) Find the memory location of a BASIC subroutine (is that even
possible?)
2) Register an interrupt handler
Ah, no. Don't even bother trying.
In theory, this could be achieved, but you would have to start with an
assembly language interrupt handler that did the following additional
steps (after step 6 above):
6.1: preserve all memory locations required by Applesoft BASIC to keep
track of what it is currently executing. This will typically require
saving a fairly large number of zero page locations somewhere else.
6.2: set up an appropriate context to execute an arbitrary piece of
Applesoft BASIC code, by setting the same zero page variables to the
right state to execute a new statement at an arbitrary line in the
program.
6.3: call something to invoke the Applesoft "interrupt handler", and
arrange for a method to allow regaining control after it finishes.
(The Applesoft interrupt handler would implement steps 7 and 8.)
In step 9, the assembly language interrupt handler would have to restore
all the Applesoft zero page variables it preserved in 6.1.
All of this would have to be repeated for every single byte transmitted
or received. The overhead would be worse than simply polling the serial
port from Applesoft.
AND you have to write a fair amount of assembly language to do it.
It makes vastly more sense to write the entire interrupt handler in
assembly language. It will be much faster to execute.
The problem then is how to get the Applesoft BASIC code to process the
data faster.
I'm struggling to understand John's code (I'm mainly a
PHP/HTML/CSS/Python programmer, so any credibility I once had was
destroyed in one fell swoop by this sentence), but he makes it look
quite easy to do in assembly.
Because BASIC is interpreted, not compiled, I'm guessing that jumping to
a subroutine without GOSUB would be impossible to do. (Somebody tell me
I'm wrong, quick!)
In theory, you can invoke Applesoft code from assembly language by
setting up several zero page variables and calling a suitable routine
inside Applesoft. Getting control back again could be an interesting
exercise. I don't know if I've ever tried.
At the moment, I'm having to poll the serial card as fast as possible,
but that means I miss characters, as BASIC can't poll fast enough. Not
only that, but $C098 (data location for slot 1) only contains one
character. No idea where the rest of the 256 char buffer is (John's
program talks about a 'circular buffer', but I have no idea what that is
or how I can access it).
The SSC doesn't have a hardware buffer.
It holds one completed byte in the "receive data register", while
assembling the next byte in the "receive shift register". If you are
polling the SSC and receiving continuous data at 9600 bps, with 8N1
encoding, you need to be able to read the holding register at least once
every 960th of a second (just over one millisecond between characters).
If you don't read the data register in time, the SSC will record an
overrun error and one of the bytes will be lost.
John's program has implemented its own "circular buffer" (256 bytes) by
using an interrupt handler (written in assembly language) to store bytes
into that buffer (somewhere in memory) as they arrive at the SSC.
I haven't looked at his specific algorithm, but the general principle is
to maintain at least two variables to keep track of the data in the
buffer: a "put" pointer and a "get" pointer. The interrupt handler
stores bytes at the "put" pointer and advances that pointer, while the
code reading data out of the buffer fetches it from the "get" pointer
and advances that pointer.
The "buffer empty" condition is indicated by the put and get pointers
being equal.
The "buffer full" condtion is indicated by the put pointer being one
byte behind the get pointer. You can only store 255 bytes in a 256 byte
circular buffer, unless you maintain a separate byte counter, or a
buffer full or empty flag.
If a byte is received while the buffer is full, it must be discarded by
the interrupt handler. It may be possible to avoid this situation by
using hardware handshake or XON/XOFF flow control, depending on the
other device and the protocol in use. Hardware handshake is difficult
with the SSC due to the built-in automatic behaviour of some flow
control signals
The main purpose of the buffer is to allow "smoothing" the rate at which
receive data must be processed. The code reading data out of the buffer
must be able to keep up with the rate at which data is arriving, on
average, but it can afford to spend a little longer doing other tasks
occasionally as long as it can "catch up" with several bytes of received
data.
If the code reading the data out of the buffer is way too slow, the
buffer won't achieve anything unless flow control can be used to stop
the other device from sending data as quickly, or the protocol only
involves short bursts of data which can fit entirely within the buffer
and be completely processed before the next burst starts.
So what I'm having to do is on the other side of the serial line (my
Linux laptop) is split up the message into Ctrl+K-separated chunks with
a simple Python program, and have a 0.05 second delay between each
character.
So this:
The quick brown fox
Become this:
^KT^Kh^Ke^K ^Kq^Ku^Ki^Kc^Kk^K ^Kb^Kr^Ko^Kw^Kn^K ^Kf^Ko^Kx^K^C
All of which is really slow and horrible to transmit (though readable by
BASIC through simple polling), and if I could install interrupts or at
least get some kind of hardware-based signalling from the serial card
(e.g. an "I'm ready" flag that would become 1 if I have data, then I
poke to 0 when I've read it), that would make my life blissful.
Why are you sending Ctrl-K characters? Is that just so you can detect a
change in data in the receive buffer?
There is a flag bit in the 6551 which indicates that receive data is
available. Unfortunately, reading a bit is cumbersome in Applesoft
BASIC. It is very easy in assembly language.
To detect an available byte in the receive data register, you need to
test the Receive Data Register Full (RDRF) flag in bit 3 of the status
register ($C099 for slot 1). At the same time, you have to ignore all
the higher and lower order bits in that register, because they could be
either 1 or 0 depending on what else is going on.
In a reasonable language, that could be done with a bitwise AND
operation, or a shift, at worst using integer divide and mod operations.
Unfortunately, Applesoft BASIC isn't a reasonable language. You have to
do a fair amount of floating point arithmetic to extract bit 3, and the
processing overhead is significant compared to the time between received
bytes.
A typical technique, assuming slot 1:
1000 ST = PEEK(49305)
1010 ST = ST - INT(ST / 16) * 16
1020 IF ST >= 8 THEN REM receive data is available
The expression in line 1010 eliminates bits 4 and higher, leaving a
value in the range 0-15, with values 8 or higher indicating that bit 3
is set.
At the point of the REM, you would PEEK(49304) to read the byte and do
whatever you want with it. No subsequent "cleanup" is required - the
RDRF flag is automatically reset to 0 when you read the receive buffer.
Using John's interrupt-driven circular buffer would allow your Linux
code to send up to 255 bytes in a single burst, then wait long enough
for the Applesoft BASIC program to process all the receive bytes before
sending the next burst of 255 bytes.
Assuming you can work out how to interface his code to your Applesoft
program.
But I don't understand much of what's written in the manuals and things
I've been reading. (W.F. Luebbert's "What's Where in the Apple" is
extraordinarily helpful (and completely nuts :) ), but unfortunately
doesn't go very far at all into interrupts.)
Interrupts are a somewhat advanced technique, really only in the realm
of assembly language.
Any documentation on programming the Super Serial Card around? I
couldn't for the life of me find where John got the following memory
locations from:
slot equ $20 ;slot 2
data equ $C088+slot
status equ $C089+slot
command equ $C08A+slot
control equ $C08B+slot
Those are locations within the DEVSEL space in the slot (16 bytes
reserved for each slot in $C0n0 through $C0nF, where n is the slot
number plus 8).
The Super Serial Card contains a 6551 ACAI chip which does the actual
serial data receive and transmit. It has four registers, and the design
of the card maps those registers to start at address 8 within the 16
bytes reserved for the slot.
They are briefly documented in the manual for the SSC, and well
documented in the data sheet for the Rockwell R6551 (or equivalent).
Any help would be much appreciated.
Jeremy.
(Apologies if this is a duplicate post. I sent this several hours ago,
and it still didn't propagate, so I'm presuming it didn't work.
Apologies if it did.)
I didn't see any earlier post, for what it's worth.
--
David Empson
dempson@xxxxxxxxxxxxx
.
- Follow-Ups:
- Re: Handling SSC interrupts in Applesoft BASIC
- From: Jeremy Visser
- Re: Handling SSC interrupts in Applesoft BASIC
- From: e p chandler
- Re: Handling SSC interrupts in Applesoft BASIC
- From: Jeremy Visser
- Re: Handling SSC interrupts in Applesoft BASIC
- References:
- Handling SSC interrupts in Applesoft BASIC
- From: Jeremy Visser
- Handling SSC interrupts in Applesoft BASIC
- Prev by Date: Handling SSC interrupts in Applesoft BASIC
- Next by Date: Re: Handling SSC interrupts in Applesoft BASIC
- Previous by thread: Handling SSC interrupts in Applesoft BASIC
- Next by thread: Re: Handling SSC interrupts in Applesoft BASIC
- Index(es):
Relevant Pages
|