BeeBMIDI (Part 5)
Jay Chapman's crusade to get E&MM readers writing their own BBC software continues, with a look at how interrupt-handling works out in practice.
The second of two articles in which we look in detail at how to receive data from MIDI In. Last month was the theory half - this month is the practical.
Last month I discussed in detail the sort of problems that can arise if data is lost during real-time input from MIDI In. The obvious solution to the problem would seem to be the use of interrupts, but as that first article explained, if interrupts are masked out for too long, our own interrupt may be ignored for such a length of time that data is still lost. So, how can we use interrupts and ensure that all the data arrives and is safely stored?
Perhaps the most obvious solution, and certainly the simplest, is to avoid the interrupt masking problem and the use of interrupts altogether. The reason we considered interrupts at all was that testing the ACIA status register from our MIDI program can be interrupted by all the background processing that the BBC Micro is doing on our behalf (eg. incrementing the TIME pseudo-function value). If you don't mind this processing not being done, you can stop it - and thereby ensure that your MIDI program has the micro's full attention - by 'masking out', or disabling, the interrupts that are responsible.
The author's own MIDI software makes use of interrupt facilities for several MIDI-related background functions. For example, a VIA timer is used to count through the MIDI clock time interval and then interrupt to say that another 'tick' has occurred. For this and other reasons, the normal interrupts should not be semipermanently masked out, so NMI interrupts are employed instead to guarantee response.
If you want to take the easy way out - and it does let you get away with writing very little assembly code - then you should disable interrupts (meaning IRQs, but not NMIs) at the start of your real-time input routine and enable them again at its end. You can do this with two routines that are CALL-able from BASIC, and these are shown in Table 1. You can assemble the two routines into a suitable area of memory (the user character buffer &C00 is assumed in the Table - if this is already in use you could always reserve space with a DIM statement) using the BBC's built-in assembler. If the routines are stored at location &C00, then CALL &C00 will 'mask out' IRQs and CALL &C02 will re-enable them. If you 'bracket' your real-time input code with a disable CALL at its start and an enable CALL at its end, your response problems will be dealt with at a stroke.
The use of interrupts may well be the most complex thing you've yet attempted on your BBC micro. In just a few pages, I can't possibly teach you all you need to know on this subject and seriously recommend that you beg, borrow or buy a copy of the Advanced User Guide, which will broaden your horizons considerably!
Anyway, what do we have to do to use interrupts to deal with MIDI In?
Well, first we have to write a routine that will be invoked when the relevant interrupt occurs: this is our 'interrupt handler'. This routine must check that the interrupt has been caused due to a byte arriving via MIDI In and put the byte somewhere safe so that the real-time input routine can pick it up at a convenient moment. The interrupt that spurred the routine into action must be turned off so that we don't attempt to deal with it twice, and the interrupt handler must then exit gracefully so that the code it interrupted is not disrupted in any way.
Second, we need somehow to connect the interrupt handler to the hardware interrupt mechanism so that the code will be invoked when the interrupt occurs. Since it's likely that other interrupt handlers are already connected, we've got to ensure we don't disconnect them when we connect our routine. Even worse, there's also a risk of our leaving nothing connected at all! Since the disk and econet systems both use NMI, we could end up with quite a mess if we're not careful.
Third, we need to arrange some foolproof means of communication between the interrupt handler and the main program. Without interrupts, all the programming we've done so far has been serial. Do this, then this and then that, in other words. With interrupts, we're dealing with (apparently) concurrent, asynchronous activities - which means that, in general, we have to be very careful where two pieces of code might be changing the same memory location 'at the same time'. This problem is unfortunately too complex for me to discuss in detail here, and in fact the nature of our specific problem deals with it quite naturally anyway.
Whether you use IRQ or NMI interrupts, you need to arrange for each incoming byte to be kept safe until your main program can pick it up. Now, it may well be that more than one byte arrives - and is safely gathered in by the interrupt handler before your main program can turn its attention to incoming bytes. It is not sufficient, therefore, to have one 'safe' location in which to leave an incoming byte: you may need to deal with a second byte, and perhaps a third, or even a fourth...
To deal with this problem we use a circular or ring buffer. Figure 1 shows what such a buffer looks like in theory, while Figure 2 shows how it's actually implemented in practice. There are two pointers associated with the buffer. The input pointer (In) points to the first empty location: this is where the next byte received will be put. The output pointer (Out) points to the buffer location containing the oldest buffered byte, ie. the byte that arrived first out of those currently buffered. We also keep track of how many bytes are currently stored in the butter in the 'used count' location.
Every time there is a Receive Register Full interrupt from the 6850, we pick up the byte received, store it where directed by In, and move In on to point at the next free location: we must also increment the count, of course. Whenever our realtime input routine is ready to deal with a new byte, we check that the count is greater than zero to make sure that at least one byte has been received. If one has been received, we pick up the byte pointed to by Out, move Out on to point at the next used location, and decrement the used count.
If the used count is zero, In and Out will actually point at the same ring buffer location, but that in itself causes no problems since we will not try to pick up an Out byte after checking the count. If too many bytes come in too quickly, we may lose bytes because the buffer 'overflows' - you try storing 11 bytes!
The solution to this problem is to make the buffer so large that the real-time input routine always has time to catch up. I make mine 256 bytes long, but if your MIDI input routine gets that far behind, you've probably switched the computer off anyway!
Seeing as we haven't got any suitably circular memory chips readily to hand, we've got to make do with ordinary RAM to make our ring buffer. Figure 2 shows that we simply take a section of RAM (locations 100 to 109 in the diagram) and create the circularity we require by manipulation of the buffer In and Out pointers. When we increment a pointer, we check whether it has gone off the end of the buffer, and if it has, reset it to the start.
My own software uses the 256-byte block of memory from &C00 to &CFF as the ring buffer. Using 256 bytes has the advantage that when I increment the counter location, which is one byte in memory, it automatically 'wraps round' from 255 (&FF) to 0 (&00), which gives the necessary circularity without any further effort on my part. If you use a different sized buffer, you'll have to do a little more work.
It's worth mentioning that if you intend writing a lot of assembler code, the BBC's own assembler can become a limiting factor in that both the source and the assembled code need to fit into memory at the same time. If you use a full 80-column line in order to give each assembler instruction a descriptive comment, you can run out of memory all too quickly. You're stuck with the BASIC editor rather than a word processor, though in fairness there are ways around this.
Now, the VASM disk-to-disk assembler allows you to prepare source files (with View in my own case: Wordwise and other editors would also work), and there are other facilities which could also prove very useful. If you can't afford the Acorn Assembler Development System (which is another solution to the same problem but needs a 6502 2nd processor) then VASM might not be a bad idea. For more details on the system, contact VIDA REBUS, (Contact Details).
Figure 3 shows the interrupt-handling routine. Ignore the first three instructions for the moment - we'll come back to them when we consider how to connect the routine to the hardware interrupt mechanism.
Before we do anything, we must be sure that the interrupt that invoked this piece of code is actually the one we're interested in! To do this we get and then check the contents of the 6850's status register. If bit 7, the IRQ bit, is set (ie. is 1) to say that the 6850 is interrupting, then the bmi (branch on minus) instruction's condition is true. This is so because the status register with bit 7 on is negative when considered as a 2's compliment binary number. The bmi therefore causes execution to continue at the label '1%' if the 6850 interrupted, and to exit via the jump &D03 otherwise (this will be explained later). '1%' is a VASM local label - if you're using the BBC's assembler, substitute an ordinary one.
If we arrive at the label 1% we know that the interrupt was for us, so we pick up the byte in the receive register (Ida Rxreg) and store it in the first free location in the ring buffer pointed to by rxbufIN (Idy rxbufIN; sta rxbuf,y). We then increment rxbufIN to point to the new 'first free' location (inc rxbufIN) and increment the count (inc rxbufCNT). Note that this is done last so that at no time does the count say there are more bytes in the buffer than we've actually stored.
Having got the bytes coming into the buffer, it's not difficult to see how the main program (in my case the real-time input routine) picks these bytes up. Figure 4 shows typical code to do the job.
We check the buffer count and branch to the pick up code if it is not zero (Ida rxbufCNT; bne 1%). If it is zero, we continue with the various 'housekeeping' chores (jmp loop). We point to and then pick up the first of the buffered bytes (1dx rxbufOUT; Ida rxbuf,x) and then move the Out pointer on (inc rxbufOUT) and decrement the used count (dec rxbufCNT). We now have the received byte in the accumulator and can deal with it at our leisure, happy in the knowledge that the interrupt routine will safely gather in any byte that might arrive at some inconvenient moment. Provided we don't take so long that the buffer overflows, that is.
How do we connect the interrupt handler routine to the hardware interrupt mechanism and BeeBMIDI? Well, whatever you do, don't forget to connect the NNMI link (x-z) on the BeeBMIDI board as shown last month.
When an interrupt occurs, the BBC micro automatically starts executing code held at a particular place in memory. To cut a long story very short, the address that execution starts at for the NMI interrupt is &D00. What we want to do is somehow insert a 'hook' into this code so that our NMI interrupt is also dealt with. There are official ways to claim the NMI interrupt facility, but these are only available from ROM-based software, so we have to improvise!
Figure 5 shows the code that needs to be run to insert our 'hook'. Before writing this code, I disassembled the first few bytes held from &DOO onwards. The job in hand was to insert the code for 'jmp rxint' at &D00 so that whenever an NMI occurred, the interrupt handler will be executed. Since 'jmp rxint' takes up 3 bytes, the first three bytes of the existing &D00 routine had to be replaced. And as the first three bytes (pha;tya;pha on my Watford DFS disk-based system) are all single-byte instructions, they could be moved without difficulty. If any multi-byte instructions had been involved, I would have had to be careful always to move complete instructions.
Looking back at Figure 3, you can now see the reason for the first three instructions which we ignored earlier. They are the ones we've replaced by 'jmp rxint'. The pha;tya;pha sequence saves the accumulator, while Y registers on the stack so that the NMI routine can use these registers for its own purposes without overwriting, and thereby losing, the values the interrupted program had in them.
When we exit from our part of the NMI routine we must, of course, restore these saved register values. This is the purpose of the pla;tay;pla sequence of instructions just before the rti instruction that causes the interrupted code to be executed again from where it was stopped.
If the NMI interrupt turns out not to be for us, we do a 'jmp &D03' which runs the original NMI code to deal with the disk or econet. You should remove the hook from the NMI code when you're not using a BeeBMIDI board, as well as resetting the 6850 by writing a 3 into the control register (or simply turning its power off). If you don't - and if the 6850 is left with an interrupt pending (because your MIDI software crashes, for example) - you'll find your disks won't work properly.
Having made the hardware/software connection, we must finally configure the 6850 ACIA on the BeeBMIDI board so that it uses interrupts. In the past, we've used the 'Receive Register Full' bit in the status register to check if a byte has arrived. We still use it (as a confirmation that we know what's going on - we still need to check the status register for errors anyway), but we also ask the 6850 to interrupt whenever the Receive Register Full event occurs. The configuration is accomplished by the (Ida #&95; sta ctrlreg) code shown in Figure 5.
Gear in this article:
Feature by Jay Chapman
mu:zines is the result of thousands of hours of effort, and will require many thousands more going forward to reach our goals of getting all this content online.
If you value this resource, you can support this project - it really helps!