Ticking away the moments that make up a dull day
You fritter and waste the hours in an off-hand way
I chose the lyrics from the excellent Pink Floyd’s Time song to show what my ISR was doing so far: just ticking the PCLK (and the FSYNC) away, without yet doing any useful work. There was plenty to do besides
Kicking around on a piece of ground in your home town
– namely, do PCM I/O, arrange data traffic between userland and the ISR, that was already enough to do. My ISR was waiting for me to write all the relevant code:
Waiting for someone or something to show you the way
So I plunged into firmware coding. Timed ISR code is one of the most demanding things (for me, at least), because one needs to take literally every instruction into account and struggle to optimize everything that can be optimized. To begin with, enough cycles were eaten for just invoking the ISR: the PIC needs 3 cycles to jump to a default address (0008h) and another 2 to execute the jump instruction usually found there. As things were, this jump was to an address inside the bootloader, which then jumped to a preconfigured address in “user space” (meaning the address space outside the bootloader), 0808h. From there, another jump, a test if this was a TMR1 interrupt, and still another jump to my ISR. All this summed up into wasting 9 or 10 clock cycles in order to just have the processor frogleap around its flash address space like mad. Not optimal, definitely.
I decided to dispose off the test for TMR1, since TMR1 interrupts were the only ones I would use. Then, I decided to tweak the firmware of the bootloader, so that it would jump directly to my ISR address. To do that, I had to fix an address in the EEPROM for my ISR, so I could use this as a constant in the bootloader. Doing all the relevant work brought the initial waste of time down to 5 cycles. This already felt better.
I soon discovered that the PIC assembly is not very powerful. In order to access a byte in RAM, one needs two clock cycles, one to load the “bank” (four topmost bits) of the RAM address, and another one to access the data. I made sure to ask the linker to place all my data in one “bank”, so all my ISR would have just a single “bank-load” instruction. Good. Just incrementing and testing my two counters, cnt16 (that was its initial name) and cnt4, required another 4 to 6 cycles. Re-loading TMR1 with the right value to fire within 23 or 24 cycles later required another 3; pulsing PCLK required another one; resetting the TMR1 interrupt flag still another one; and, returning from the interrupt, required another two cycles. All this summed up to 16 or so cycles. I was left with only seven cycles or even less (in some execution paths I had to also pulse FSYNC) to do all the useful work.
It was not doable.
There was nothing useful that could be done in seven cycles. For example, take PCM I/O: the ISR would have to test if it was the right moment to do PCM I/O (2-3 cycles), rotate left the output data byte (1 cycle), test for carry (1-2 cycles) and set accordingly the TXD signal — already seven or more cycles gone, and still nothing yet accomplished for the RXD signal…
At that point, I despaired. It seemed I had been caught in the exact situation that Pink Floyd describe by their lyrics:
And you run and you run to catch up with the sun but it’s sinking
Racing around to come up behind you again
The song’s lyrics were clearly a metaphore for my PIC’s ISR day: I was running and running to catch up with the 23 machine cycles that my “day” (the ISR invocation) had, but I was running short of time and the sun was setting (I had to return from the ISR) without having done all that had to be done; then, time raced around and the ISR had to be called again.
…Every year is getting shorter, never seem to find the time…
(…yes, this was exactly what was happening in my ISR code…)
…Plans that either come to naught…
(…my plans for this project, quickly sinking…)
…Or half a page of scribbled lines…
…Hanging on in quiet desperation…
(this was me — all this struggling for nothing after all… Ahh!… Why hadn’t I thought about all this before starting to mess with PCBs and soldering irons?).
Until, after a couple of days in deep despair, I had another moment of enlightment: my ISR was wasting too much time to perform the bookkeeping of interrupt handling. If I could perhaps call the ISR less often, it would naturally waste less time in useless tasks!
The solution was looking simple: instead of invoking the ISR at every half-cycle of PCLK, I would call it at every full-cycle. Then, I would have 46 to 47 machine cycles available! I would do all there was to do within 20 or so cycles, plus another 10 cycles wasted in bookkeeping, and this would leave me with still some good 10 to 20 free cycles! How come I hadn’t thought of this before?!?
In my human day metaphore, this meant that, instead of waking up at 7:00 am, running like mad to do a nine-to-five daytime job (plus the overhead of e.g., shaving, bathing, dressing, and driving to my workplace and back home), then getting a half-hour sleep and waking up again at 7:00 pm do quite the same stuff (plus the same overhead) for a nighttime job, I would work two daytime shifts in a row, seven-to-eleven (as in Led Zeppelin’s Since I ’ve been loving you, to quote another song): thus I ’d have the overhead of driving etc. just once for both shifts, and at least I ’d have the opportunity to get a few hours’ worth of sleep!
I converted quickly the stub ISR I had coded so far into this form, and it turned out that I even had to insert NOP instructions in order to wait until the right time to pulse PCLK down to logical zero! There was plenty of time — or, as the song puts it:
…there is time to kill today…
Now, with enough time available, my next job was to spread the various ISR tasks around the four “periods” spanned by my outer counter, cnt4 (in the meantime, I had changed the inner counter to count only up to 8, so this was no longer called cnt16, but cnt8). I designed the following schedule:
If you enlarge the image (by clicking on it), you ‘ll see that the first cnt8 round (with cnt4 equal to 0) is devoted to doing PCM I/O on the 3210 PCM bus; along with the first PCLK pulse in each of the next two rounds (with cnt4 equal to 1 and 2, respectively), PCM I/O data are moved around. You may have noticed that there is a mention of an input and an output ring. I made this to serve as buffers between the ISR (which is an isochronous procedure), and USB I/O (which is by definition an asynchronous one). Each of the two rings had a capacity of 63 bytes (because of the simple algorithm that I use, I had to spare one byte out of the 64 available in each ring), or ~8 milliseconds of audio.
An interesting little detail is that in a certain situation I did run out of time after all. In the case of “write byte to output ring” work, I had to code successive test-and-jump sequences for the value of my cnt4: is cnt4 equal to zero? No? Jump elsewhere, then test, is it equal to one? No? Jump and test again, and so on. Thus, the real work of writing the byte was starting late and I was running out of time again. So, just for that case, I figured that, instead of setting the timer and returning, I had to start with the next PCLK pulse from within the ISR without returning at all, and using a GOTO instruction instead. Expressed in my human working metaphore, once in a while my PIC would have to work three shifts in a row and start over right away with the first shift of the next day, but this was only one day out of sixteen, and my PIC would bravely cope with it!
Here is the full ISR code (beware: this is untested code and it will most certainly contain hundreds of bugs — I am just making this available here for scrutiny, without any guarantee that it is correct or even close to that). As you will see, I have painted the various sections of the code with different colors, so the reader’s eye can group together relevant pieces of code. Nevertheless, proof-reading this is a tough exercise, so if you attempt it, be prepared (for example have nearby enough cofee, aspirins, and the like).
So by now, I had gotten past two major obstacles, SPI and PCM I/O (or at least so it looked). My next step would be to complete my PCB with the DC-DC converter and the rest of the PCB components. And my next milestone had quite an ambitious description, to transfer audio from the PC to a handset using PCM I/O! Wow! But these I save for the next posts!