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: