First an apology: the code is sloppy. It reflects its slow organic growth and part-time attention. And, I'm still surprised at how such an outwardly simple gadget can have disproportionately complex software. Still, the firmware is mostly conventional in structure so I'll only give a brief overview and focus on the secret sauce.
There are three basic sections: boot, menu, and run. The boot section initializes the MCU's peripherals, calibrates the ADC, and gets the real-time clock going. The ADC is calibrated because Vdd is used for +Vref for the widest dynamic range. Parameter checking is done throughout and out-of-bound values will halt the boot process. This includes a review of the power control & status registers: anything other than a power-up will halt the boot process on the premise that a serious fault was encountered.
Once boot is complete the menu is entered which gives the user the opportunity to set operating mode & options. This is done by gesturing over the light sensor. The menu exits to the run section automatically when the user stops gesturing.
The run section is the typical infinite loop that controls timing cycles. It can only be exited thru a reboot. The design philosophy of the run (and menu) section is that all data required to control operation be available by polling variables thru each loop. Anything that is timing or safety critical is handled by the interrupt vector (ISR). The ISR is also expected to handle data acquisition and the hardware interface. It can be thought of as a primitive hardware abstraction layer. This allows the run loop to focus on functionality while the ISR does the heavy lift and keeps things safe.
The ISR is responsible for:
- Calculating AC RMS current
- Disabling the AC output when the current limit is exceeded
- Switch from SSR to mechanical relay when SSR current limit is exceeded
- Disabling the AC output when the temperature limit is exceeded
- Sensing light level and interpreting a gesture
- Maintaining chronological time
- Maintains various timers used in the menu & run sections
- Monitors AC continuity logic signal, handles de-bounce, sets status
The tricky piece in the ISR is calculating RMS current. The square root sum-of-squares method is used. This requires many samples and is computationally intensive. It's really a job for a DSP or at least a 16-bit processor but that was part of the fun and required some creativity. (it also kept costs & complexity down) What really made it possible was a low bar for accuracy: Timer needs to know approximate current values and the hardware can tolerate several hundred milli-amps of error.
So the Nyquist criteria is violated and only ninety (90) samples are taken during a 60Hz sine cycle (16.66666mS). At 90 samples / cycle there is ~ 185uS between them. To make this work the PIC is run at its maximum instruction speed of 8MHz. Timer four generates an interrupt every ~ 46uS. The service block for timer4 works in four sequential steps, each being performed on a given interrupt and incrementing to the next step on the next interrupt:
- AD conversion of current value
- Read & square obtained current value, start light acquisition
- AD conversion on light, sum squared current value
- Read & evaluate obtained light value, start current acquisition
This is the most time sensitive work in the overall ISR and is placed at the top. The remaining sections of the ISR complete the data processing and set the data objects used by the foreground code.
Timer0 runs at 20mS intervals and controls AC RMS data acquisition cycles. It also completes the RMS calculation by taking the square root of the summed values. It's interval is deliberately longer than a 60Hz cycle. When a full set of samples is ready for RMS calculation the timer4 AC calculations are bypassed until the sample counter is reset. This maintains sufficient execution headroom for other parts of the ISR as well as foreground code.
This section of the ISR also handles connectivity state changes in the AC circuit. It uses the Interrupt On Change (IOC) flags but not the interrupt. This, in combination with timer0's interval, ensures that a state change will be detected without having to constantly monitor the logic state. Last, it also checks for high temperature. Since we're only interested in a threshold a comparator is used along with a fixed voltage supplied by the FVR & DAC. The output of the comparator is polled.
Timer0 code does a lot, but its total execution time is balanced with timer4 to ensure all code in the interrupt vector receives timely attention.
Timer1 is used for chronological timekeeping and is the last code block to be serviced in the ISR. This is possible because the timer interrupt only occurs every two (2) seconds.
You can glean additional details by reading thru the code's in-line documentation.