As earlier stated, the purpose of POC V1.2 is to develop some hardware and programming concepts that can be applied to other hardware later on. Two hardware concepts embodied in V1.2 are the vQUART (virtual QUART) and interrupt "steering."
When I decided to implement the vQUART I realized I was going to have a lot of potential interrupt sources to service. Since there are four communications channels, there are four receivers and four transmitters that can interrupt—possibly all at the same time. Also, the DUARTs each have a timer that could generate an IRQ, which the timer in one of the DUARTs does at 10 millisecond intervals. Hence interrupt steering becomes important when a large number of IRQ sources exist in a system. Otherwise, significant MPU time may be consumed in figuring out which device is interrupting and why.
As the bulk of the IRQs that will be generated will be due to communications activities, I took advantage of a somewhat arcane DUART hardware feature to set up my interrupt steering. In a previous post, I wrote:
Quote:
Speaking of interrupts, the 28L92 can be configured so general purpose output pins 4, 5, 6 and 7 (OP4-7) act as open-drain IRQ signals. OP4 is wired to the RxD channel 1 IRQ, OP5 is wired to the RxD channel 2 IRQ, and OPs 6 and 7 are wired to the channel 1 and channel 2 transmitters, respectively. When the 28L92 interrupts with its main IRQ output, it will also pull one or more of these pins low if reception and/or transmission activity was the cause of the interrupt.
In order to take advantage of this feature, I have the OPs wired to the inputs of a 74ACT540 bus driver, with bits 0-3 wired to the receiver IRQs and bits 4-7 wired to transmitter IRQs—bits 0 and 4 are channel A, bits 1 and 5 are channel B, and so forth. The bus driver is assigned a location in the I/O map (designated SIOQ) which when read, will return a real-time IRQ status. If %00000000 is read, then no receive or transmit IRQs were generated. Otherwise, bits will be set according to what interrupted. These bits are real-time, meaning they stay set until the cause of the IRQ has been cleared.
This arrangement doesn't report the cause of other vQUART IRQs, such as timer underflow, which must be detected by reading the individual DUART interrupt status registers. However, the interrupt service handler's work will greatly reduced, since no polling of each receiver and transmitter will be required. This reduction becomes important when one considers that if all four channels are simultaneously receiving and transmitting at 115.2Kbps, as many as 46,080 IRQs due to SIO activity could occur in one second, assuming an IRQ occurs with each datum received or sent. In practice, the rate won't be quite as severe, due to the buffering provided by the DUARTs' FIFOs. In the best case, 2880 IRQs would occur each second of continuous bi-directional communication, as an interrupt will only come when a receiver FIFO is full or a transmitter FIFO is empty.
The basic circuit is pretty simple:
Attachment:
File comment: Serial I/O IRQ Steering Circuit
sio_irq_ckt.gif [ 98.34 KiB | Viewed 16072 times ]
Below is a closeup of the hardware that implements this circuit.
Attachment:
File comment: vQUART IRQ Steering Hardware
sio_irqs.gif [ 1.19 MiB | Viewed 16072 times ]
The 'ACT540 is wired so the individual bits read by the MPU are such that receiver IRQs are reported in the least significant nybble and the transmitter bits are reported in the most significant nybble. Bit 0 is channel A's receiver and bit 4 is channel A's transmitter. This arrangement makes it easy to process active IRQs in a succinct loop. Here's the code for processing receiver activity:
Code:
iirq0200 lda io_sioq ;get vQUART channel IRQ status
beq iirq0400 ;nothing to process
;
bit #m_irqrx ;receiver IRQ?
beq iirq0310 ;no
;
sta tiairqst ;save IRQ status
ldy #0 ;starting channel index
;
;
; channel processing loop...
;
.0000010 lsr tiairqst ;channel receiver interrupting?
bcc .0000030 ;no, skip it
;
tya ;yes, copy channel index &...
asl A ;make channel pointer...
tax ;offset
lda #nxpcresr ;clear any...
sta (tiacr,x) ;RxD overrun error
;
;
; RHR processing loop...
;
.0000020 lda (tiasr,x) ;get channel status
bit #nxprxdr ;RHR empty?
beq .0000030 ;yes, done with channel
;
lda (tiafif,x) ;get & hold datum from...
xba ;RHR
lda tiaputrx,x ;get queue 'put' pointer
inc A
cmp tiagetrx,x ;any room in queue?
beq .0000020 ;no, discard datum
;
xba ;store datum...
sta (tiaputrx,x) ;in queue
xba ;new 'put' pointer
sta tiaputrx,x ;store it &...
bra .0000020 ;loop
;
; ...end of RHR processing loop
;
;
.0000030 iny ;next channel
cpy #n_nxpchn ;all channels processed?
bcc .0000010 ;no
;
; ...end of channel processing loop
As can be seen, the body of this routine is an inner and outer loop. Prior to entering the loops, the value read from the interrupt steering hardware is tested to see if any receiver bits are set. If none is set the receiver processing code is completely skipped—there's nothing to process.
Upon determining at least one receiver has interrupted, the outer loop will start by right-shifting the value read from the interrupt steering hardware to determine which receivers require service. If the bit corresponding to any given receiver is cleared, the inner processing loop is skipped. Otherwise, the inner loop will repeatedly read the receiver holding register (RHR, aka FIFO) until the receiver status shows all datums have been gotten. If the circular queue into which the datums are to be stored is full the datum will be discarded. Once all datums have been gotten the IRQ for that receiver will be automatically cleared.
The code for processing transmitter IRQs is somewhat similar:
Code:
;vQUART TRANSMITTER IRQ PROCESSING
;
iirq0300 lda io_sioq ;IRQ status
;
iirq0310 bit #m_irqtx ;transmitter IRQ?
beq iirq0400 ;no, done with vQUART
;
sta tiairqst ;save IRQ status
ldy #n_nxpchn-1 ;starting channel
;
;
; channel processing loop...
;
.0000010 asl tiairqst ;transmitter interrupting?
bcc .0000040 ;no, skip it
;
tya ;copy channel index &...
asl A ;make channel pointer...
tax ;offset
;
;
; THR processing loop...
;
.0000020 lda tiagettx,x ;queue 'get' pointer
cmp tiaputtx,x ;queue 'put' pointer
beq .0000030 ;nothing to transmit
;
lda (tiasr,x) ;get channel status
bit #nxptxdr ;THR full?
beq .0000040 ;yes, done for now
;
lda (tiagettx,x) ;read queue &...
sta (tiafif,x) ;write to THR
inc tiagettx,x ;bump 'get' pointer &...
bra .0000020 ;loop
;
; ...end of THR processing loop
;
.0000030 lda #nxpcrtxd ;disable...
sta (tiacr,x) ;TxD
lda tiatstab,x ;tell foreground...
tsb tiatxst ;about it
;
.0000040 dey ;all channels serviced?
bpl .0000010 ;no
;
; ...end of channel processing loop
Here again, the always-handy
BIT instruction is used to determine if any transmitters are active—the entire code segment will be skipped if none is interrupting. However, for programming convenience, the outer loop works backwards through the channels as it services interrupting transmitters. The inner loop attempts to stuff as many datums into the transmitter's FIFO as possible, a procedure that assists in reducing IRQ servicing overhead.
During inner loop processing, there are three conditions that have to be accounted for:
- FIFO is full. If the FIFO is full, the inner loop is broken and further processing on the channel is deferred. The transmitter will not interrupt again until it has emptied its FIFO.
- There isn't enough data to fill the FIFO. If this happens the transmitter will be told to shut down after it has sent the final datum.
- There is no data to transmit. The transmitter will be immediately shut down.
Killing the transmitter when the data runs out is necessary to prevent it from continuously interrupting the MPU and causing deadlock. Due to the design of NXP UARTs, the transmitter can be told to shut down as soon as the data runs out, even if there are still datums in the FIFO waiting to be sent (and this can happen quite often with a serial hookup that uses hardware handshaking). The transmitter will continue to shift datums out on to the wire until the FIFO has emptied, at which time it will shut down. Shutting down the transmitter automatically disables the transmit IRQ. Note how a flag is set in the code to tell the foreground that the transmitter is dead and must be restarted when more data becomes available for transmission.
As can be seen, I've made extensive use of that useless
(<dp>,X) addressing mode in implementing this driver.
During reset, an array of pointers is copied from ROM to direct page, three sets of pointers for each channel's chip registers. The expense of calculating or looking up the register addresses each time an interrupt is serviced is avoided. Also, there is an array of pointers for the circular queues associated with each channel. Again, this eliminates the need to do table look-ups or pointer arithmetic on each interrupt.
Here is a snippet from the list file for that part of the assembled code:
Code:
02980 ; TIA-232 driver workspace...
02981 ;
02982 000E tiazpptr =tdsecct+s_tdcnt ;start of pointer array
02983 000E tiasr =tiazpptr ;vQUART channel status
02984 0016 tiacr =tiasr+s_nxpchp ;vQUART channel command
02985 001E tiafif =tiacr+s_nxpchp ;vQUART channel FIFO
02986 0026 tiagetrx =tiafif+s_nxpchp ;RxD queue 'get'
02987 002E tiaputrx =tiagetrx+s_nxpchp ;RxD queue 'put'
02988 0036 tiagettx =tiaputrx+s_nxpchp ;TxD queue 'get'
02989 003E tiaputtx =tiagettx+s_nxpchp ;TxD queue 'put'
02990 0046 tiatxst =tiaputtx+s_nxpchp ;TxD status (bit field)
02991 0047 tiairqst =tiatxst+s_byte ;vQUART IRQ status workspace
02992 ;
02993 0048 zeropage .= tiairqst+s_byte ;*** BIOS/kernel direct page end ***
Needless to say, if anything steps on that part of direct page there is going to be a computing train wreck. Incidentally, the TxD status bit field (
tiatxst) is used to keep track of which transmitters have been shut down—a bit is set if the corresponding transmitter is down. The handy
TRB and
TSB instructions are used to manipulate this field.
Earlier this evening, I implemented the above code and much to my amazement, it worked on the first try. So that's one item that can be checked off the to-do list.