Back

Design

 

Timing considerations

 Avoiding interrupt conflicts

The only critical aspect is the 1ppS interrupt cannot be delayed once the OXCO and 1ppS are close to synchronised.

There are four interrupts: TMR2, RB0 (1ppS), Serial transmit, Serial receive. Most of the time, the TMR2 interrupt routine takes about 50 instruction times every 250 instruction times. It is the only other interrupt being serviced when the 1ppS signal is expected. Initially the software adjusts the TMR2 timer directly so that the 1ppS arrives between two TMR2 interrupts. The target is half way, i.e. about 100 instruction times after completing a TMR2 interrupt. This caters for deviations of +- 400Hz from 10MHz, which for the sort of oscillators used should be adequate. The program has been written to cater for larger deviations but not tested. When the OCXO and 1ppS are close to synchronised, TMR2 is not adjusted, and the OCXO frequency is finely adjusted so the 1ppS (on average) arrives at the target time.

The 1ppS interrupt service routine may delay the immediately following TMR2 interrupt, this should not be a problem. The 1ppS routine is less than 50 instructions, the possible issue would be if it were long enough to cause the loss of a TMR2 interrupt. One TMR2 interrupt can be delayed for over 200 instruction times without losing the next. The PWM module is double buffered, so it only needs to be serviced before the next TMR2 interrupt and is not time critical.

The TMR2 interrupt routine runs longer on a few occasions - the significant one is the 'end of second' where it produces an output 1ppS. This TMR2 interrupt for this should not be delayed, if it is the 1ppS output will jitter. This is avoided by having the end of second interrupt several TMR2 interrupts after the expected 1ppS interrupt. The other longer TMR2 interrupts are servicing the charge/discharge of the delay circuit capacitors. There should be no other interrupts at the time. If there is a conflicting interrupt, the timing of start of charge is not critical, and start of discharge can tolerate small delays.

The serial transmit and receive interrupts have to interleave with the TMR2 interrupts. So that the transmission interrupt does not interfere with either the 1ppS or TMR2 end of second, transmission is started by the end of second routine after producing the 1ppS output. The output is limited to the number of bytes in the transmission buffer at that time. This ensures the output completes early in the second (the maximum 48 bytes at 4800 baud takes less than a tenth of a second). The receive interrupts are currently always enabled, the GPS unit used starts transmitting after the 1ppS, which means the first character is received well after the TMR2 end of second (at 4800 baud a character takes over 2mS to send, 20 TMR2 interrupts). It is assumed (not tested) that the GPS messages for the second are completed well before the next 1ppS. If there was a chance of conflict, the receive interrupt could be inhibited just before expected 1ppS and enabled by the end of second routine without losing data, but this does not seem necessary.

Timing of the charge/discharge operation

The circuit to delay detection of the 1ppS signal is not adjusted initially but gives a fixed delay. When the pulling range of the OCXO has been determined, the circuit is exercised every second. The software aims to adjust the delay so that the 1ppS is detected about the target time described above. The timing is determined by the desired delay, held in the variable DisVal. DisVal can take a value 0-255. This determines how many TMR2 interrupts before the expected arrival of the 1ppS that discharge should start. The actual value used is DisVal+DisOff, DisOff being a constant (currently 2) so there is at least one intervening short TMR2 interrupt before the 1ppS. Before the capacitors are discharged, they are charged to a fixed voltage (around 4V). The time of charge is not fixed and charging must complete before discharge is required. This is achieved by setting the ports used for charge/discharge as inputs for a short time between charge completion and start of discharge. The ports are then in a high impedence state so the charge is retained. This retention period is set by the constant ChgOff (currently 50 or 5mS). This is the nominal time for retaining the charge but the actual time depends on how long it takes to charge. Charge time is captured by TMR1, so the time of previous charges is known. So it is used in the calculation of the time to start the current charge/discharge cycle.

In summary: time to start charge is <expected arrival of 1ppS>-<DisVal+DisOff>-<ChgOff>-<value from known previous charge times>

The order of events is:
1) - Charge/discharge times calculated in the routine FlagWait, the start charge time is set up for comparison by the TMR2 interrupt
2) - TMR2 interrupt tests for a match, when matched charging is started, TMR1 is started, start discharge time is set up for comparison
3) - The charge reaches the desired value and causes an RB0 interrupt. The interrupt stops TMR1 and sets ports as input
4) - TMR2 interrupt tests for a match, when matched starts discharging
5) - if there is a 1ppS, it is detected by an RB0 interrupt which captures the time
6) - TMR2 reaches end of second, alerts FlagWait, FlagWait returns to the mainline.

It is likely that TMR2 fires while FlagWait is calculating. To prevent false operation, when TMR2 starts discharge it clears the high byte of the match value. This can never match because the sofware loop counts up and the only time the high byte is zero is at end of second. There is no match test at end of second. FlagWait puts a value in the high byte as the last operation of the calculation when everything else is set up. This ensures the TMR2 match only occurs when there is valid data.

Designing for the PIC

Data banks

A major hassle with the PIC is the 7 bit data address with usable memory reduced by the lower 32 bytes allocated to hardware registers and the upper 16 bytes shared between banks. This leaves 80 bytes in banks 0 and 1 and the PIC16F628A has another 48 bytes in bank 2. Addressing all the data areas requires manipulating RP0,RP1 and IRP. Most bugs in the program were due to these being incorrect and addressing the wrong bank. An attempt is made in the program to allocate banks in a sensible way to mitigate this problem. To this end, the program is segmented into 3 separately assembled source modules - Main, Maths and TXdata.

TXdata - all of bank 2 is used as a data buffer, the buffer data is always addressed indirectly via FSR. This means it is never necessary to change RP1 (required if bank 2 were directly addressed) so RP1 is assumed to be 0 always. Further, the only time IRP is set other than 0 is in TXdata routines and it is always set back on exit. So the only bank switch affecting other modules is RP0.

Maths - uses only bank 1 directly. It is necessary for the calling code to set RP0 before calling any Maths module. User data is 'pushed' and 'popped' via FSR so can be in either bank 0 or bank 1.

Main - almost all variables used by Main are in bank 0 or common (accessed regardless of the selected bank). Where a switch to bank 1 is required, the return to bank 0 should occur as soon as is reasonable so the two switches are close and it is easier to check the code between them.

Use of Exlusive Or (XOR)

Two drawbacks of the PIC are: no compare instruction, and only one indirect pointer. XOR is used to address this to some extent. To test for greater or less, the only method available is to use an arithmetic operation. This will also set the zero flag in status. If the comparison is only for equality XOR can be used instead, and has the advantage it does not disturb the C flag, so it is used in preference to an arithmetic operation.

In the Maths module, data is held in a stack so arithmetic operations are similar to RPN (reverse Polish notation). Pushing and Popping the stack requires a block move of data. This would be easy with two indirect pointers but messy with only one. The solution (documented in the module) is to use only a ripple move - the data block is moved up or down one byte. This is achieved by an interesting use of XOR - to swap two values without needing an extra memory store. Given two variables A and B they can be swapped by XOR A,B; XOR B,A; XOR A,B. Applying this over successive bytes in a block can shift the data up or down one byte. The technique is also used elsewhere in the program.

Interrupt latency

An advantage of the PIC architecture is that interrupt latency (the time between an event causing an interrupt and entering the service routine) is not dependant on what instruction was being executed at the time. This means an interrupt routine can sample a timer and deduce the timer value at the time of the event. This is not the case with ATmega processors. If this program is rewritten for another processor then this has to be addressed. The ATmega has the ability to latch a counter value when an event occurs, this would be used instead.