Gottchas

Olin Lathrop [olin at cognivis.com] of cognivis.com says:

The 16F87x manual (section 4.5, page 44) shows two NOP instructions after the BSF EECON1, RD instruction to start a program memory read. The comment reads "memory is read in the next two cycles after BSF EDCON1, RD". The description text reads "The data is available in the EEDATA and EEDATH registers after the second NOP instruction". After reading this and the rest of the description, I figured I would use the two cycles after the read start and before the data is available to do something else unrelated. THIS DOES NOT WORK. It appears the two instructions following the read start are not just to wait for the data to be available, but are actually ingnored by the processor. In other words, they need to be NOPs, not any arbitrary code that doesn't use EEDATA or EEDATH.

I'm guessing that the processor keeps the PC chugging along as it uses the cycles to read program memory at an address other than that indicated by the PC. Probably NOP instructions are forced into the opcode decoder, or it is somehow disabled by other means. It appears that the two words following the read start are never fetched. I don't have a problem with this mode of operation, but Microchip should have made this more clear. I did notice afterwards that the same NOPs following a program memory write are commented "Instructions here are ignored by the microcontroller".

Bob Ammerman of RAm Systems asks:

Given the 'canonical' way to read program memory:
    bsf    eecon1,RD
    nop
    nop
    <data available>

what happens if an interrupt occurs during the 'nops' that are being ignored?

For writes the datasheet explicitly says they are 'queued' (ie: held off until after the write completes), but for reads it isn't so clear.

Olin Lathrop [olin at cognivis.com] of cognivis.com says:

...do an experiment with a timer interrupt and a tight foreground loop doing a program memory read, keeping track of the min/max timer value found at the same point in the interrupt routine.

This "little" project ended up taking more time than I had intended. There are gotchas that might not be apparent at first thought.

My first idea was to write a simple program that did program memory reads in a tight loop in foreground. There would be a timer 0 interrupt routine, and it would maintain the min/max values it found for timer 0 at a fixed offset from the start of the interrupt.

Then I started thinking, what if my loop ended up being a nice sub multiple of the timer 0 interrupt period? This might be nearly impossible to calculate by counting cycles. Even if the cycles don't seem like a sub multiple, there is no way to be sure. What if the loop self-adjusts so that once it gets to a certain point that causes a specific delay it gets locked into that loop phase because of the delay, or perhaps a repeating pattern for each of the loop cycles per interrupt? If any of this happened, then there is no way to guarantee that an interrupt occurred at least once at every possible point in the loop, thereby perhaps missing the one important point.

The next plan was to randomize the time between interrupts a bit. I already had a random number routine lying around in the same project that I hacked up the main routine of to do this test. So I added a random 0 - 7 value to timer 0 once each loop iteration. I ran tests both with and without the instruction that starts the program memory read. The min/max timer 0 values in the interrupt routine were 7,13 for the emulator and the real chip, and 6,13 for the simulator without a program memory read. With a program memory read, the values were 6,13 on a real chip and 6,13 on the simulator. The emulator is useless for testing program memory access at full speed.

I was expecting some jitter, but not this much, especially not for the case without program memory reads. It's probably obvious to you what's going on as I explain this, but it was a bit confusing at first last night. It was late and I was trying to make it home in time to see the presidential debate. I missed it, so you guys will have to tell me who to vote for <g>.

It sounds reasonable enough when you think about it, but apparently the act of adding a value to timer 0 will set T0IF if the add results in a carry. My little (obviously wrong) mental picture of the PIC had timer 0 implemented as a separate counter whose carry would set T0IF. After all, it increments all by itself, and sets T0IF all by itself regardless of what the main ALU is doing. I hadn't realized that a carry from the main ALU could set T0IF on a ADDWF TMR0 instruction. It's an interesting tidbit to remember. It may be documented, I haven't looked.

So much for that idea. The next idea was to randomize the loop time by jumping randomly into a string of NOPs. That seems to have worked fine. With no program memory reads (the control), the real chip did 9,9, the emulator 9,9, and the simulator 8,9. With program memory reads the real chip and the simulator showed the same results as before (9,9 and 8,9), and again the emulator couldn't be reasonably tested.

All tests were run on a 16F876 at 8MHz or simulation/emulation thereof. I had LEDs connected to the output pins on the real hardware. These always stabilized in a very "short" time in human terms. Sometimes I could see a little bit of flashing before the number would stabilize, but this was always way to fast to read the value. I'm sure the LEDs never changed after the first 100mS, probably a lot sooner. I ran all simulator test to at least 1 million cycles, which was 1/2 second of simulated time.

So here are the conclusions of my testing. Some of these are definitely not what I expected:

1 - The 16F876 has NO TIMER 0 INTERRUPT JITTER AT ALL, regardless of whether a two cycle instruction is executing, or whether program memory reads are going on.

2 - The simulator can take a timer 0 interrupt sometimes one cycle "early" so that timer 0 is one count less than what it would be in the real hardware.

3 - T0IF will be set by the carry of a deliberate add to TMR0, not just by overflow of timer 0 from normal incrementing.

So this gets us back to the question of what really goes on during the two dead cycles of a program memory read. The instructions seem to get ignored, but interrupts can still happen and don't seem to be affected. How can this be? How does the program memory read get done without affecting the instruction flow? Is the program memory really faster than the rest of the system and they can somehow sneak in an extra read without missing an instruction fetch? (I doubt it) Is there already a "dead" cycle in an interrupt so that the program memory read can hidden there? (doesn't seem likely either) Does the program memory read just screw up on an interrupt? I didn't test this, maybe I should have. This will have to wait as I've got paying customers to take care of.

I have attached the source code of the main module that embodies most of this logic should anyone wish to see exactly how these measurements were taken.


;   Program to test interrupt behaviour during program memory read.
;   The foreground loop does program memory read operations, and the
;   timer 0 overflow interrupt is enabled.  The interrupt routine
;   measures the worst case timer 0 jitter, and writes the result
;   to port B so that it can be seen externally.
;
;   This program is a "quick hack" modification of the halloween creepy
;   critter STRT module and is completely contained in this module.  Some
;   of the structure and comments may therefore look out of place for
;   what this program actually does.
;
;   This runs on a PIC 16F876.
;
         include "hal.inc"

         extern  regs        ;force general registers to be defined
         extern  rand_init   ;initialize random number generator
         extern  stack_init  ;initialize software stack
         extern  rand        ;set REG0 to a random byte value

;
;***********************************************************************
;
;   Set static processor configuration bits.
;
         __config b'11111101110010'
                 ;  11------11----  code protection disabled
                 ;  --1-----------  no in circuit debugging, RB6,RB7 general I/O
                 ;  ---X----------  unused
                 ;  ----1---------  flash memory is writeable by program
                 ;  -----1--------  EEPROM read protection disabled
                 ;  ------0-------  low volt in circ prog off, RB3 is general I/O
                 ;  -------1------  brown out reset enabled
                 ;  ----------0---  power up timer enabled
                 ;  -----------0--  watchdog timer disabled
                 ;  ------------10  high speed oscillator mode
;
;***********************************************************************
;
;   Configuration constants.
;
readadr  equ     h'1234'     ;program memory address to read
;
;***********************************************************************
;
;   Global state.  (Hack alert: hard coded to bank 0)
;
.bank#v(0) udata

mint0    res     1           ;min timer 0 value found in interrupt routine
maxt0    res     1           ;max timer 0 value found in interrupt routine
gflags   res     1           ;global flag bits

#define flag_iinit  gflags, 0 ;interrupt state has been initialized
;
;   Private state used by interrupt routine.
;
status_save res  1           ;saved copy of STATUS, nibbles swapped
pclath_save res  1           ;saved copy of PCLATH (if multiple code pages)

itmp1   res     1            ;temp storage for use within interrupt routine

         udata_shr
w_save   res     1           ;saved W during interrupt, mapped to all banks
;
;***********************************************************************
;
;   Executable code.
;
;   Reset vector.
;
.reset   code    0
         clrf    intcon      ;disable all maskable interrupts
         gjump   start       ;jump to relocatable startup code
;
;***********************************************************************
;
;   Interrupt service routine.
;
.intr_svc code   4           ;start at interrupt vector
         movwf   w_save      ;save W
         swapf   status, w   ;make copy of status without effecting status bits
         clrf    status      ;select direct and indirect register banks 0
         dbankis 0           ;tell the assembler about it
         ibankis 0
         movwf   status_save ;save old STATUS value with nibbles swapped
         movf    pclath, w   ;save PCLATH
         movwf   pclath_save
         clrf    pclath
;
;   W, STATUS, and PCLATH have been saved.  Now start doing the real work.
;
         dbankif tmr0
         movf    tmr0, w     ;grab the timer 0 value at fixed point after interrupt
         movwf   itmp1       ;save it

         bcf     intcon, t0if ;clear the interrupt condition
;
;   Init MINT0 and MAXT0 with this value if this is the first interrupt.
;   The timer 0 shapshot value is in W and ITMP1.
;
         btfsc   flag_iinit  ;interrupt state not initialized yet ?
         goto    not_first   ;previously initialized, go do the real processing

         dbankif 0
         movwf   mint0       ;init the min/max timer 0 value found
         movwf   maxt0
         bsf     flag_iinit  ;indicate interrupt state now initialized
         goto    intr_ret    ;exit the interrupt
;
;   This is not the first interrupt.  MINT0 and MAXT0 have been previously
;   initialized.  Update these values to include the new timer 0 value,
;   which is in ITMP1.
;
not_first unbank             ;not first interrupt, MINT0 and MAXT0 initialized
         dbankif 0
         movf    mint0, w    ;get previous min value
         subwf   itmp1, w    ;compare to new value
         movf    itmp1, w    ;get new value ready in W
         skip_wle            ;old value is already at or below new ?
         movwf   mint0       ;no, update min with new value

         movf    itmp1, w    ;get new value
         subwf   maxt0, w    ;compare to previous max
         movf    itmp1, w    ;get new value ready in W
         skip_wle            ;new value is at or below previous max ?
         movwf   maxt0       ;no, update max with new value
;
;   MINT0 and MAXT0 have been updated to include the new value from this
;   interrupt.
;
;   Show the min and max values externally.  The MINT0 value will be written
;   to the port B low nibble, and the MAXT0 value to the high nibble.
;   Note that LEDs are wired to port B such that bit values of 0 light them.
;   The complement of the actual values are therefore written to port B so
;   that 1 bits will be indicated by lit LEDs.
;
         dbankif 0
         movf    mint0, w    ;get min value
         andlw   h'0F'       ;mask in the low nibble
         movwf   itmp1       ;temp save in ITMP1
         swapf   maxt0, w    ;get max value in high nibble
         andlw   h'F0'       ;mask in only the high nibble
         iorwf   itmp1, w    ;merge with min value in low nibble
         xorlw   h'FF'       ;make complement due to LED inverse logic
         dbankif portb
         movwf   portb       ;show the value on the LEDs
;
;   Restore the saved state and return from the interrupt.
;
intr_ret unbank              ;common code to return from the interrupt
         clrf    status      ;register bank settings are now 0
         dbankis 0           ;tell the assembler about it
         ibankis 0
         movf    pclath_save, w ;restore PCLATH
         movwf   pclath
         swapf   status_save, w ;get old STATUS with nibble order restored
         movwf   status      ;restore STATUS, register banks now unknown
         swapf   w_save      ;swap nibbles in saved copy of W
         swapf   w_save, w   ;restore original W

         retfie              ;return from interrupt, re-enable global interrupts
;
;***********************************************************************
;
;   Relocatable part of main routine.
;
.start   code
start    unbank
;
;   Init the interrupt system to completely off.  INTCON has already been
;   cleared.
;
         dbankif pie1        ;separately disable all individual interrupts
         clrf    pie1
         dbankif pie2
         clrf    pie2
         dbankif pir1        ;clear any pending interrupt conditions
         clrf    pir1
         dbankif pir2
         clrf    pir2
;
;   Initialize the separate modules.
;
         gcall   stack_init  ;init the software stack
         gcall   rand_init   ;init random number generator
;
;   Initialize system state.
;
         dbankif 0
         clrf    gflags      ;init all flags to reset
;
;   Set up the periodic interrupt from timer 0 overflow.  The timer 0 source
;   will be the instruction clock.
;
         dbankif option_reg
         movlw   b'10001000'
                 ; 1-------  disable port B passive pullups
                 ; -X------  RB0 interrupt edge select, not used
                 ; --0-----  timer 0 source is instruction clock
                 ; ---X----  external timer 0 source edge select
                 ; ----1---  assign prescaler to watchdog timer, not timer 0
                 ; -----XXX  prescaler setting
         movwf   option_reg

         dbankif tmr0
         clrf    tmr0        ;allow max time before first interrupt

         bcf     intcon, t0if ;clear any previously occurring interrupt condition
         bsf     intcon, t0ie ;enable timer 0 overflow interrupts
         bsf     intcon, gie ;globally enable interrupts
;
;   Set up the static state for reading a program memory location.
;
         dbankif eeadr
         movlw   low readadr ;load read address low byte
         movwf   eeadr

         dbankif eeadrh
         movlw   high readadr ;load read address high byte
         movwf   eeadrh

         dbankif eecon1
         bsf     eecon1, eepgd ;select program memory, not data EEPROM address space
;
;   Set up port B.
;
         dbankif trisb
         clrf    trisb       ;set all port B pins as outputs

         dbankif portb
         clrf    portb       ;init all LEDs to on to verify write later
;
;   Inifinite loop.  The purpose of this loop is to spend a large fraction of
;   the time doing program memory reads so that the interrupt condition will
;   become true during such a read frequently.
;
loop     unbank
;
;   Do a program memory read.
;
         dbankif eecon1
         bsf     eecon1, rd  ;start the read
         nop                 ;unused instructions, skipped while read completes
         nop
;
;   Randomly jump into a chain of NOPs.  This is intended to randomize
;   where interrupts occur whithin this loop.
;
         gcall   rand        ;get a random byte value in REG0

         movlw   high nops8  ;init PCLATH to NOPs chain start
         movwf   pclath
         movf    reg0, w     ;get the random byte
         andlw   7           ;make 0-7 random number
         addlw   low nops8   ;make low byte of jump address
         skip_ncarr          ;no carry to high byte ?
         incf    pclath      ;propagate the carry
         movwf   pcl         ;jump randomly into NOPs chain

nops8                        ;start of 8 successive NOP instructions
         nop
         nop
         nop
         nop
         nop
         nop
         nop
         nop

         goto    loop        ;back to do it all again

         end

Code: