Home >

Servo Lock Controller Module

Servo Lock Controller SMD/SMT PCB

This is a revised and improved version of the servo lock controller module used in the steel bondage cage project, redesigned using surface-mount components to make more efficient use of the limited space inside the electronics compartment. It's powered by a 6V 1.2Ah sealed lead-acid (SLA) battery, with a separate charger module that is enabled following a simple handshaking process with the servo lock controller.

Key features of this servo lock controller module

Original Design with Additional PCB MP3 Audio Player Module

In the original prototype-style design I added a sensory noise (MP3 audio) player module mounted on a separate PCB, which also included some circuitry for transmitting data via a pair of infrared LEDs; for sending timer data to a remote clock display unit. The new battery now occupies this space, with the audio player and infrared transmitter parts integrated into the new servo controller PCB design.

Yuasa NP1.2-6 Sealed Lead Acid Battery

You may think a SLA battery is perhaps an odd choice, as there are far more compact and energy-dense battery options these days. The primary reason for opting for the SLA battery is safety, especially considering the rather unique aspects of the intended application, whereby an energy source would be mounted in close proximity to a person, and they may not be able to move away from it should an electrical fault occur. For this reason I'm avoiding Li-Po and similar technologies, and it just so happens the YUASA NP1.2-6 fits snugly in the 26mm deep space inside the electronics compartment.

Servo Controller and Battery Charger Installed in Enclosure

The other advantage of the chosen power source is its favourable discharge curve, typically ranging from 6.45V to 5.25V, making it well suited to powering systems requiring a stable 5V. This voltage range provides just enough headroom to power the digital electronics via a Low Drop Out (LDO) voltage regulator, with the servos powered directly from the battery.

The original design used two packs of four AA batteries, wired in parallel to maximise the runtime of the system. These provided a voltage range of around 6.4V when new, which would drop right down to about 4V once discharged. This meant most of the digital electronics were being powered beyond their absolute maximum ratings, and the servos would eventually be operating well below their minimum voltage, which was less than ideal.

The new design uses a voltage divider across the battery to determine its state-of-charge, the output of the divider is connected to one of the microcontroller ADC pins, and a lookup table is used to convert the measured voltage to a percentage.

The state-of-charge of the battery is checked at regular intervals, and when nearing zero percent the user is prompted to connect the battery charger. If this happens while the timer's running and servos set to the locked position, the unlock routine will be called prior to prompting the user to charge the battery.

Enclosure Underside with Charger / 12V AC Adaptor

Seeing as the DY1703A MP3 player and LTK5128D audio amplifier chips are SMD, it seemed a good time to switch to a surface mount design. There's not much information available on the DY1703A chip, and it's not available to purchase on its own. The component search engine website didn't even have a symbol or footprint for it, so I had to make my own in KiCAD. Both of these chips were extracted from a DY-SV17F audio player module, purchased on eBay.

I searched for a similar MP3 player chip, but I was unable to find anything quite as straightforward to implement. The DY1703A is really quite versatile, as it can be operated via a UART serial interface, or through its eight I/O pins, it also provides a one-line control option too, but I've never used it.

The DY1703A can be configured to read audio files from either an SD card or from a SPI interfaced serial flash memory chip, such as a Winbond W25Q32 with 32Mbits (4 MegaBytes) of storage. The DY-SV17F module I received came with a RoyalMicro branded 25Q32, but its 4MB was insufficient for the thirteen one-hour long sensory noise recordings I wanted to use. So I configured the chip to work with an SD card instead. The maximum supported SD card size is 32GB, so accommodating the ~800MB of MP3 files was no longer an issue.

DY-SV5W and DY-SV17F MP3 Audio Player Modules

As there's so little information, I had to refer to another eBay purchased module, the DY-SV5W which also uses the DY1703A, but with an SD card socket instead of onboard flash memory. I had some of these to hand, as I'd already used one in my previous servo controller project. I was able to reverse-engineer both modules and figure out what had to be connected where. You can see in the schematic below pins 12,13,14 on the DY1703A (marked as no-connection) provide the CS,SDAT,SCLK pins required for interfacing a W25Q32 serial flash. Pins 17,18,19 are used to interface with the SD card. Pins 16,15,11 are used to set the configuration options, these determine which one of the three control interfaces are to be used.

SD cards require 3.3V, so I thought I was going to need a separate regulator, but thankfully the DY1703A takes care of this, providing a 3.3V-OUT on pin 3, especially for powering SD cards. The chip's footprint is a SSOP-24 package measuring 3.9 x 8.7mm with a 0.635mm pin pitch, or a SSOP-24_3.9x8.7mm_P0.635mm in KiCAD, and possibly the smallest chip I've had to solder. It was easy to remove from the module it arrived on using hot air.

Schematic for new surface mount Servo Lock Controller

I noticed on both audio modules that only the DY1703A's left audio channel is connected to the LTK5128D's single audio input, via a 20nF DC blocking capacitor and 100kΩ resistor. I decided to combine the output from both audio channels by adding an additional resistor and blocking capacitor for the right audio channel. This ensures a stereo audio recording will still be heard at the earphones without a loss of sound if the audio shifts between channels. The earphones have their left and right channels combined at the amplifier's single output.

The digital electronics are powered using three LDO voltage regulators, the first is the MCP1801T-5002I/OT which is used exclusively for powering the PIC18F46K22 microcontroller, and is always enabled by default. This regulator was selected due to its ultra-low quiescent current of approximately 25µA, so when the microcontroller is in its sleep state the total current drawn from the battery is about 40µA.

The two additional AP2210K-5.0TRG1 LDO regulators are used to power the hardware connected to the microcontroller, and are enabled by the microcontroller as required. Regulator PS2 is used to power the LCD display, the 74HC595 shift regulator driving its eight digital inputs, and the infrared receiver and sender sections. PS3 is used to power the audio player circuitry, consisting of the DY1703A and LTK5128D.

Power to the servos is controlled using the NTD25P03LT4G P-FET. Prior to entering its sleep state, the PIC18F46K22 will disable PS2 and PS3, and shut-off power to the servos by turning off the 2N7002 N-FET responsible for pulling the gate of the P-FET to ground.

Servo Lock Controller PCB Layout Diagram

The microcontroller is woken either via the tilt switch connected to the INT1 interrupt pin, or via the optional keypad using the chip's Interrupt On Change feature. When a keypress occurs, one of the pulled-high column inputs is connected to a row output, causing a transition to a low-state, resulting in an IOC interrupt.

Waking the system via remote control isn't supported, as the infrared sensor is only powered when the microcontroller is awake. The sensor draws 70uA of current, and having spurious infrared transmissions wake the microcontroller unnecessarily would only serve to deplete the battery faster. When the metal frame, forming the enclosure for the electronics, is removed - as would be necessary prior to use - the tilt switch will always power-up the system.

The tilt switch also serves as an emergency release. So if the tilt switch is triggered while the timer is in operation and the servos are in their locked position, the timer stops and the unlock routine is called. This way, if the remote control were to fail or was misplaced, escape from the frame can be achieved by a second person lifting the front wheels off the ground slightly while pivoting on the rear wheels.

Optional Keypads for Servo Lock Controller

The optional keypad connects to the 7-pin right-angle header at the centre of the board. This feature was added so the servo controller module could be used in an alternative BDSM frame design. The keypad provides the exact same functionality as the remote control, except the # key is used to toggle between numeric input and cursors for menu navigation - so buttons 2,4,6,8 would represent Up,Left,Right,Down with 5 representing 'OK'. Despite there being no indication on the keypad to this functionality, it's reasonably intuitive to use once you're aware of it.

In a design incorporating a keypad, the firmware would require entry of an access code before any menu options become available. This would be a randomly generated four-digit code, displayed to the operator prior to activating the timer and engaging the locking mechanism.

The 7-pin keypad connector was arranged to support a membrane keypad, as shown above. But the tactile feel of the other plastic keypad is far nicer to use. It's a shame that mounting it in an enclosure is a little more involved, whereas the membrane keypad has an adhesive backing and would simply stick down, having passed the flat-flex though a milled slot to the electronics below.

PCB Underside Showing TowerPro MG92B Servos

On the underside of the PCB are the connections for the two servos, and also the footprint for an unpopulated LED. This is for an optional downwards-facing infrared LED, so infrared data could be transmitted to receiving equipment placed above or below the electronics enclosure.

Servo Lock Controller with 1602 LCD Display mounted

The servo controller module uses the same 1602 LCD display as the original through-hole PCB design. This revised version uses a 74HC595 shift register to control it, so only three I/O pins are required to write data to the display using its 4-bit data mode. This frees up I/O pins for other hardware, such as the membrane keypad etc. The LCD display backlight is controlled by the microcontroller via a 2N7002 N-FET.

Sealed Lead Acid Battery Charger PCB

The separate battery charger module is a three-stage charger, allowing the battery to be left float charging indefinitely if required. Due to the slightly limited space inside the enclosure, it uses the popular LM2596 switching regulator and a 33uH inductor to provide the required charge current and voltages for charging the 6V SLA battery.

Some handshaking takes place between the two modules before charging is enabled. When the charger is powered by the 12V adaptor, it supplies 5V to the CHARGER_POWERED pin of the charger JST connector. This signals to the servo controller module that the charger is active, and it needs to prepare to shutdown; which may involve saving settings and returning the servos to their unlocked position.

Once this has taken place, and the servo controller is ready to be shutdown, it pulses the ENABLE_CHARGER pin. This latches two MOSFETs on the charger module, and enables the LM2596 regulator. The latched MOSFETs pull the SYSTEM_OFF pin to ground, which disables PS1 on the servo controller - the regulator powering the PIC microcontroller - which shuts down the servo controller module. When power to the battery charger is removed, the MOSFETs unlatch, and the servo controller resumes operation.

The charger was a separate project, the details of which can be found on the sealed lead acid battery charger article.

Full source code listing for this project

/* 
 * File:   tt_smd.c
 * Author: paul reynolds ( paul at v9tronics d0t c0m )
 *
 * compile and program using:-
 * xc8-cc -mcpu=PIC18F46K22 tt_smd.c lcd_1602_74hc595.c nec_ir_decode.c dy1703a_mp3.c eeprom_access.c
 *
 * bash ipecmd.sh -P18F46K22 -TPPK4 -M -I -OL -Ftt_smd.hex
 * 
 *
 * HARDWARE / FIRMWARE CHANGES
 *
 * - keypad columns now connected to RB4,RB5,RB6
 * - peripheral regulators and servo FET now connected to RA0,RA1,RA2
 * - PS1 replaced with MCP1801T-5002I/OT, reducing sleep power consumption to ~40uA
 * - wake on keypad button press added
 * - battery charger power-on acknowledgment, and system power-down when charger is enabled
 * 
 * 
 * CREATED: 14th January 2026
 * UPDATED: 31st March 2026
 * 
 * PCB Version 'FEB 2026'
 * 
 */

// PIC18F46K22 Configuration Bit Settings

// CONFIG1H
#pragma config FOSC = HSMP      // Oscillator Selection bits (HS oscillator (medium power 4-16 MHz))
#pragma config PLLCFG = ON      // 4X PLL ENDisable (Oscillator used directly)
#pragma config PRICLKEN = ON    // Primary clock enable bit (Primary clock enabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L
#pragma config PWRTEN = OFF     // Power-up Timer Enable bit (Power up timer disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable bits (Brown-out Reset disabled in hardware and software)
#pragma config BORV = 190       // Brown Out Reset Voltage bits (VBOR set to 1.90 V nominal)

// CONFIG2H
#pragma config WDTEN = NOSLP    // Watchdog Timer Enable bits (Watch dog timer disabled in sleep, SWDTEN has no effect)
#pragma config WDTPS = 8192     // Watchdog Timer Postscale Select bits (1:32768 = 2m,18s) (1:8192 = 32.7secs)

// CONFIG3H
#pragma config CCP2MX = PORTB3  // CCP2 MUX bit (CCP2 input/output is multiplexed with RB3)
#pragma config PBADEN = OFF     // PORTB A/D Enable bit (PORTB<5:0> pins are NOT configured as analog input channels on Reset)
#pragma config CCP3MX = PORTE0  // P3A/CCP3 Mux bit (P3A/CCP3 input/output is mulitplexed with RE0)
#pragma config HFOFST = ON      // HFINTOSC Fast Start-up (HFINTOSC output and ready status are not delayed by the oscillator stable status)
#pragma config T3CMX = PORTC0   // Timer3 Clock input mux bit (T3CKI is on RC0)
#pragma config P2BMX = PORTC0   // ECCP2 B output mux bit (P2B is on RB5)
#pragma config MCLRE = EXTMCLR  // MCLR Pin Enable bit (MCLR pin enabled, RE3 input pin disabled)

// CONFIG4L
#pragma config STVREN = OFF     // Stack Full/Underflow Reset Enable bit (Stack full/underflow will not cause Reset)
#pragma config LVP = ON         // Single-Supply ICSP Enable bit (Single-Supply ICSP enabled if MCLRE is also 1)
#pragma config XINST = OFF      // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L
#pragma config CP0 = OFF        // Code Protection Block 0 (Block 0 (000800-003FFFh) not code-protected)
#pragma config CP1 = OFF        // Code Protection Block 1 (Block 1 (004000-007FFFh) not code-protected)
#pragma config CP2 = OFF        // Code Protection Block 2 (Block 2 (008000-00BFFFh) not code-protected)
#pragma config CP3 = OFF        // Code Protection Block 3 (Block 3 (00C000-00FFFFh) not code-protected)

// CONFIG5H
#pragma config CPB = OFF        // Boot Block Code Protection bit (Boot block (000000-0007FFh) not code-protected)
#pragma config CPD = OFF        // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L
#pragma config WRT0 = OFF       // Write Protection Block 0 (Block 0 (000800-003FFFh) not write-protected)
#pragma config WRT1 = OFF       // Write Protection Block 1 (Block 1 (004000-007FFFh) not write-protected)
#pragma config WRT2 = OFF       // Write Protection Block 2 (Block 2 (008000-00BFFFh) not write-protected)
#pragma config WRT3 = OFF       // Write Protection Block 3 (Block 3 (00C000-00FFFFh) not write-protected)

// CONFIG6H
#pragma config WRTC = OFF       // Configuration Register Write Protection bit (Configuration registers (300000-3000FFh) not write-protected)
#pragma config WRTB = OFF       // Boot Block Write Protection bit (Boot Block (000000-0007FFh) not write-protected)
#pragma config WRTD = OFF       // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L
#pragma config EBTR0 = OFF      // Table Read Protection Block 0 (Block 0 (000800-003FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR1 = OFF      // Table Read Protection Block 1 (Block 1 (004000-007FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR2 = OFF      // Table Read Protection Block 2 (Block 2 (008000-00BFFFh) not protected from table reads executed in other blocks)
#pragma config EBTR3 = OFF      // Table Read Protection Block 3 (Block 3 (00C000-00FFFFh) not protected from table reads executed in other blocks)

// CONFIG7H
#pragma config EBTRB = OFF      // Boot Block Table Read Protection bit (Boot Block (000000-0007FFh) not protected from table reads executed in other blocks)


#define _XTAL_FREQ  32000000

#include <xc.h>
#include <pic18f46k22.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>


/*///////////////////////////////
//// PIC18F46K22 CONNECTIONS ////
/////////////////////////////////

  LEFT-TOP >> LEFT-BOTTOM
  01  RC7	RX1
  02  RD4	CHARGER_ENABLE
  03  RD5*	
  04  RD6	TX2>>DY1703RX
  05  RD7	RX2>>DY1703TX
  06  GND
  07  +5V
  08  RB0	IR_DATA_IN [INT0]
  09  RB1	TILT_SWITCH [INT1]
  10  RB2   CHARGER_POWERED
  11  RB3	BEEPER [CCP2]

  BOTTOM-LEFT >> BOTTOM-RIGHT
  12  NC1*
  13  NC2*
  14  RB4   KPAD-C1
  15  RB5   KPAD-C2
  16  RB6   KPAD-C3 (PGC)
  17  RB7   (PGD)
  18  MCLR
  19  RA0	PWR_AUDIO
  20  RA1	PWR_DEVICES
  21  RA2	PWR_SERVOS
  22  RA3	74HC595-11

  RIGHT-BOTTOM >> RIGHT-TOP
  23  RA4	74HC595-12
  24  RA5	74HC595-14
  25  RE0	SERVO1 [CCP3]
  26  RE1	BATT [AN6]
  27  RE2	SERVO2 [CCP5]
  28  +5V
  29  GND
  30  RA7	8MHZ
  31  RA6	8MHZ
  32  RC0	32KHZ
  33  NC3*

  TOP-RIGHT >> TOP-LEFT
  34  NC4*
  35  RC1	32KHZ
  36  RC2	IR_LED [CCP1]
  37  RC3*
  38  RD0	KPAD-R1
  39  RD1	KPAD-R2
  40  RD2	KPAD-R3
  41  RD3	KPAD-R4
  42  RC4*
  43  RC5*
  44  RC6	TX1
*/


/*
    8888888b.     d8888 8888888b.         d8888 888b     d888  .d8888b.  
    888   Y88b   d88888 888   Y88b       d88888 8888b   d8888 d88P  Y88b 
    888    888  d88P888 888    888      d88P888 88888b.d88888 Y88b.      
    888   d88P d88P 888 888   d88P     d88P 888 888Y88888P888  "Y888b.   
    8888888P" d88P  888 8888888P"     d88P  888 888 Y888P 888     "Y88b. 
    888      d88P   888 888 T88b     d88P   888 888  Y8P  888       "888 
    888     d8888888888 888  T88b   d8888888888 888   "   888 Y88b  d88P 
    888    d88P     888 888   T88b d88P     888 888       888  "Y8888P"  
*/

// INPUTS & OUTPUTS
#define TILT_SWITCH           PORTBbits.RB1
#define CCP3_PIN              LATEbits.LATE0  // SERVO1 on RE0
#define CCP5_PIN              LATEbits.LATE2  // SERVO2 on RE2

#define PWR_SERVOS            LATAbits.LATA2  // P-FET
#define PWR_DEVICES           LATAbits.LATA1  // 5V REG
#define PWR_AUDIO             LATAbits.LATA0  // 5V REG

#define CHARGER_POWERED       PORTBbits.RB2   // SET HI BY POWERED CHARGER
#define CHARGER_ENABLED       LATDbits.LATD4  // SET HI BY SYSTEM TO ENABLE CHARGER

// LOCK STATES
#define LOCKED                1
#define UNLOCKED              0

// LOCK ACTIONS
#define LOCK                  1
#define UNLOCK                2

// SERVO TIMER PRELOAD
#define TMR_PRELOAD           25535 // 65,535 - 40,000 = 25,535 = 20ms period

// INFRARED TRANSMISSION
#define IR_TX_ON              CCP1CONbits.CCP1M = 0b1100; // PWM mode
#define IR_TX_OFF             CCP1CONbits.CCP1M = 0; LATCbits.LATC2 = 0; // CCP off, output low                  

// MENU SCREENS - IN MENU ORDER
#define MENU_UNLOCK_BETWEEN   1
#define MENU_UNLOCK_AFTER     2     // we always start at (and return to) this screen
#define MENU_POWER_OFF        3
#define MENU_SENSORY_NOISE    4
#define MENU_NOISE_VOLUME     5
#define MENU_NOISE_TRACK      6
#define MENU_LCD_BACKLIGHT    7
#define MENU_BUZZER           8
#define MENU_BATTERY_STATUS   9
#define MENU_AUTO_POWER_OFF   10
#define MENU_SAVE_SETTINGS    11
#define MENU_LOAD_DEFAULTS    12
#define MENU_REMOTE_DISPLAY   13

// HARDWARE HEADERS
#include "lcd_1602_74hc595.h"
#include "nec_ir_decode.h"
#include "dy1703a_mp3.h"
#include "eeprom_access.h"


// system flags for controlling program flow

volatile uint8_t TIMER_UPDATE_FLAG    = false;    // a flag, to trigger a decrement of the countdown timer

volatile uint8_t LOCK_ACTION          = false;    // set lock position (do nothing at power on)

volatile uint8_t LOCK_STATE_FLAG      = UNLOCKED; // a flag, to keep track of the servo / lock state

volatile uint8_t POWER_OFF_FLAG       = false;    // a flag, to call power-off routine

volatile uint8_t POWER_LOW_FLAG       = false;    // a flag, to call low power warning, unlock & power-off routines

volatile uint8_t AUTO_OFF_FLAG        = false;    // a flag, to prevent repeated auto power off warnings

volatile uint8_t TILT_SWITCH_FLAG     = false;    // a flag, to prevent repeated tilt switch triggering


// counters for timeouts and display navigation

volatile uint8_t TICK_COUNTER_A       = 0;        // tick counter: return to timer screen, turn off servo timers

volatile uint16_t TICK_COUNTER_B      = 0;        // tick counter: auto power off

uint8_t DISPLAY_MENU                  = 0;        // keep track of menu position

uint8_t COMMAND_IS_NUMERIC            = 0;        // store value of a numeric button press

uint8_t TIMER_DIGIT_POSITION          = 0;        // keep track of digit position when entering time

uint8_t BATTERY_LEVEL_PC              = 0;        // keep track of battery level as percentage

uint8_t BATTERY_MONITOR[3]            = {0,0,0};  // interval count, low readings count, last low reading

uint8_t BATTERY_THRESHOLD             = 2;        // threshold (%) before low battery warning, unlock, power-off


// user adjustable parameters, stored in eeprom (except timer seconds)

uint8_t NOISE_ON                      = 0;        // sensory noise: (0)disabled / (1)when locked / (2)on now

uint8_t NOISE_VOLUME                  = 12;       // sensory noise volume

uint8_t NOISE_TRACK                   = 3;        // third track: pure noise 3

uint8_t BUZZER_ON                     = 0;        // buzzer: (0)disabled / (1)when locking / (2)always

volatile uint8_t AUTO_OFF             = 5;        // power off automatically after 5 minutes

uint8_t REMOTE_DISPLAY_ON             = 0;        // transmit time data to external clock display

uint8_t TIMER_MINUTES[3]              = {0,5,5};  // countdown timer, 55 mins default

uint8_t TIMER_SECONDS[2]              = {0,0};    // countdown timer, 00 secs default

uint8_t BETWEEN_MINUTES[6]            = {0,1,5,0,6,5};  // between timer, min 15 minutes, max 65 minutes



/*
    8888888888 888     888 888b    888  .d8888b. 88888888888 8888888 .d88888b.  888b    888  .d8888b.  
    888        888     888 8888b   888 d88P  Y88b    888       888  d88P" "Y88b 8888b   888 d88P  Y88b 
    888        888     888 88888b  888 888    888    888       888  888     888 88888b  888 Y88b.      
    8888888    888     888 888Y88b 888 888           888       888  888     888 888Y88b 888  "Y888b.   
    888        888     888 888 Y88b888 888           888       888  888     888 888 Y88b888     "Y88b. 
    888        888     888 888  Y88888 888    888    888       888  888     888 888  Y88888       "888 
    888        Y88b. .d88P 888   Y8888 Y88b  d88P    888       888  Y88b. .d88P 888   Y8888 Y88b  d88P 
    888         "Y88888P"  888    Y888  "Y8888P"     888     8888888 "Y88888P"  888    Y888  "Y8888P"  
*/

/////////////////////////////////
// SAVE & LOAD USER PARAMETERS //
/////////////////////////////////

static void save_user_parameters( void ){

        // write to this location, to indicate we have previously 
        // written values that are safe to restore
        write_to_eeprom( 0x05, 0xAA );

        // auto off could be 1800 seconds, that's larger than the limit 
        // of an unsigned char (255), so save as minutes instead
        write_to_eeprom( 0x11, NOISE_ON           );
        write_to_eeprom( 0x12, NOISE_VOLUME       );
        write_to_eeprom( 0x13, NOISE_TRACK        );

        write_to_eeprom( 0x14, BUZZER_ON          );    
        write_to_eeprom( 0x15, AUTO_OFF           );
        write_to_eeprom( 0x16, REMOTE_DISPLAY_ON  );

        write_to_eeprom( 0x21, TIMER_MINUTES[0]   );
        write_to_eeprom( 0x22, TIMER_MINUTES[1]   );
        write_to_eeprom( 0x23, TIMER_MINUTES[2]   );

        write_to_eeprom( 0x24, BETWEEN_MINUTES[0] );
        write_to_eeprom( 0x25, BETWEEN_MINUTES[1] );
        write_to_eeprom( 0x26, BETWEEN_MINUTES[2] );
        write_to_eeprom( 0x27, BETWEEN_MINUTES[3] );
        write_to_eeprom( 0x28, BETWEEN_MINUTES[4] );
        write_to_eeprom( 0x29, BETWEEN_MINUTES[5] );
}

static void load_user_parameters( void ){ 

    if( read_from_eeprom( 0x05 ) == 0xAA ){

        NOISE_ON           = read_from_eeprom( 0x11 );
        NOISE_VOLUME       = read_from_eeprom( 0x12 );
        NOISE_TRACK        = read_from_eeprom( 0x13 );

        BUZZER_ON          = read_from_eeprom( 0x14 );
        AUTO_OFF           = read_from_eeprom( 0x15 );
        REMOTE_DISPLAY_ON  = read_from_eeprom( 0x16 );

        TIMER_MINUTES[0]   = read_from_eeprom( 0x21 ); 
        TIMER_MINUTES[1]   = read_from_eeprom( 0x22 );
        TIMER_MINUTES[2]   = read_from_eeprom( 0x23 );

        BETWEEN_MINUTES[0] = read_from_eeprom( 0x24 );
        BETWEEN_MINUTES[1] = read_from_eeprom( 0x25 );
        BETWEEN_MINUTES[2] = read_from_eeprom( 0x26 );
        BETWEEN_MINUTES[3] = read_from_eeprom( 0x27 );
        BETWEEN_MINUTES[4] = read_from_eeprom( 0x28 );
        BETWEEN_MINUTES[5] = read_from_eeprom( 0x29 );
    }
}

static void load_default_parameters( void ){

    write_to_eeprom( 0x05, 0x00 );
}



//////////////////////////////////////////////////
// SET SERVO POSITIONS / TURN PWM TIMERS ON/OFF //
//////////////////////////////////////////////////

static void set_servo_position( uint8_t servo, uint16_t position_as_pc ){

    // 25535+4000 = 2ms pulse, 25535+2000 = 1ms pulse

    uint16_t min_pos = TMR_PRELOAD + 2000;

    if( servo == 1 ){ CCPR3 = min_pos + (position_as_pc*20); }
    if( servo == 2 ){ CCPR5 = min_pos + (position_as_pc*20); }        

}


static uint16_t get_servo_position( uint8_t servo ){

    uint16_t min_pos = TMR_PRELOAD + 2000;

    uint16_t set_pos = 0;

    if( servo == 1 && CCPR3 - min_pos >   0 ){ set_pos = 1; }
    if( servo == 1 && CCPR3 - min_pos >= 20 ){ set_pos = (CCPR3 - min_pos) / 20; }

    if( servo == 2 && CCPR5 - min_pos >   0 ){ set_pos = 1; }
    if( servo == 2 && CCPR5 - min_pos >= 20 ){ set_pos = (CCPR5 - min_pos) / 20; }

    return set_pos;
}


static void set_servo_pwm_timers( bool state ){

    if( state == true ){

        PIE2bits.TMR3IE = 1;
        PIE5bits.TMR5IE = 1;        
    }

    if( state == false ){

        // disable timer interrupts
        PIE2bits.TMR3IE    = 0;
        PIE5bits.TMR5IE    = 0;

        // clear CCPs
        CCP5CONbits.CCP5M  = 0;    
        CCP3CONbits.CCP3M  = 0; 

        // set outputs low - does this cause the servos to move to random positions?
        LATEbits.LATE0     = 0;
        LATEbits.LATE2     = 0;
    }
}



//////////////////////////////////////////////
// SET BUZZER ON/OFF FOR SPECIFIED DURATION //
//////////////////////////////////////////////

static void set_buzzer( uint16_t duration, uint8_t state ){

    if( state == true ){ 
        CCP2CONbits.CCP2M = 0b1100;        
    } 

    for( uint16_t i=0; i<duration; i++ ){ __delay_ms(1); }

    if( state == false ){ 

        CCP2CONbits.CCP2M = 0;       
        LATBbits.LATB3 = 0; // make sure RB3 (CCP2 pin) is low
    }
}



/////////////////////////////////////////////////////
// GET BATTERY LEVEL AS PERCENT USING LOOKUP TABLE //
/////////////////////////////////////////////////////

static const uint8_t battery_levels[64] = {
     0,  2,  3,  5,  6,  8, 10, 11,
    13, 14, 16, 17, 19, 21, 22, 24,
    25, 27, 29, 30, 32, 33, 35, 37,
    38, 40, 41, 43, 44, 46, 48, 49,
    51, 52, 54, 56, 57, 59, 60, 62,
    63, 65, 67, 68, 70, 71, 73, 75,
    76, 78, 79, 81, 83, 84, 86, 87,
    89, 90, 92, 94, 95, 97, 98, 100
};

static uint8_t get_battery_level_pc(){
    
    int16_t battery_adc_avg = 0;

    ADCON0bits.CHS  = 6;  // AN6/RE1 - battery voltage divider

    for( uint8_t j=0; j<10; j++ ){        

        PIR1bits.ADIF   = 0;  // clear interrupt flag

        ADCON0bits.GO   = 1;  // begin A/D conversion

        while( !PIR1bits.ADIF );

        battery_adc_avg += (ADRESH<<8)+ADRESL;

        PIR1bits.ADIF = 0;
    }

    battery_adc_avg /= 10;

    if( battery_adc_avg >= 655 ){ return 100; }

    if( battery_adc_avg <= 592 ){ return 0;   }

    return battery_levels[ battery_adc_avg - 592 ];
}



////////////////////////////////////////////////
// GENERATE RANDOM NUMBER BETWEEN MIN AND MAX //
////////////////////////////////////////////////

static uint16_t get_random_minutes( uint16_t min_minutes, uint16_t max_minutes ){

    // static, so future calls retain final value assigned by previous call
    static uint16_t random_n = 0xBEEF;

    // add entropy using value of timer1 - generating our 1sec tick
    random_n ^= TMR1L | ((uint16_t)TMR1H << 8);

    // xorshifts
    random_n ^= random_n << 7;
    random_n ^= random_n >> 9;
    random_n ^= random_n << 8;

    // return scaled into min and max
    return min_minutes + ( random_n % ( max_minutes - min_minutes + 1u ) );
}



///////////////////////////////////////////////////////
// TRANSMIT COUNTDOWN VIA INFRARED TO REMOTE DISPLAY //
///////////////////////////////////////////////////////

static void transmit_to_remote_display( uint8_t hh, uint8_t mm, uint8_t ss, uint8_t tx_times ){
        
        uint8_t cs = hh + mm + ss; // calculate checksum

        for( uint8_t x = 0; x < tx_times; x++ ){	

            IR_TX_ON;
            __delay_ms(9);     // a.g.c pulse
            IR_TX_OFF;
            __delay_us(4500);  // long pause
            IR_TX_ON;
            __delay_us(550);

            uint8_t command_tmp = 0;

            for( uint8_t yy = 1; yy <= 4; yy++ ){

                switch(yy){
                    case 1: command_tmp = hh; break; // hours
                    case 2: command_tmp = mm; break; // minutes
                    case 3: command_tmp = ss; break; // seconds
                    case 4: command_tmp = cs; break; // checksum
                }

                for( uint8_t y = 0; y < 8; y++ ){

                    IR_TX_OFF;
                        if( (command_tmp & 1) == 1 ){
                            __delay_us(1650);
                        }else{
                            __delay_us(550);
                        }
                    IR_TX_ON;
                        __delay_us(550);                                    

                  command_tmp >>= 1;    // shift one bit right                                   
                }
            }
            
            // pause between repeats - needs to be 42ms to be valid NEC IR
            IR_TX_OFF;
            
            __delay_ms(42);
        }

        IR_TX_OFF;
}


/////////////////////////////////////////////////
// KEYPAD SCANNING & MAPPING TO REMOTE BUTTONS //
/////////////////////////////////////////////////

/* here for reference...
static const char keypad_map[4][3] = {
    { '1', '2', '3' },
    { '4', '5', '6' },
    { '7', '8', '9' },
    { '*', '0', '#' }
};
*/

static const uint8_t keypad_to_remote_numeric[4][3] = {
    { 0x45, 0x46, 0x47 },
    { 0x44, 0x40, 0x43 },
    { 0x07, 0x15, 0x09 },
    { 0x16, 0x19, 0x0D }
};

static const uint8_t keypad_to_remote_cursor[4][3] = {
    { 0x00, 0x18, 0x00 },
    { 0x08, 0x1C, 0x5A },
    { 0x00, 0x52, 0x00 },
    { 0x16, 0x00, 0x0D }
};


static void keypad_init( void ){

// we're already doing everything here in peripherals_init()
// so let's comment this out and make sure it still works

    // rows, outputs
    TRISDbits.TRISD0 = 0;
    TRISDbits.TRISD1 = 0;
    TRISDbits.TRISD2 = 0;
    TRISDbits.TRISD3 = 0;

    // columns, inputs (pulled up externally)
    TRISBbits.TRISB4 = 1;
    TRISBbits.TRISB5 = 1;
    TRISBbits.TRISB6 = 1;

    // set rows low
    LATD &= 0xF0;   
}


static void keypad_scan( void ){

    static bool numeric_or_cursors = false;

    for (uint8_t row = 0; row < 4; row++) {

        LATD |= 0x0F;            // all rows HIGH
        LATD &= ~(1 << row);     // drive one row LOW

        __delay_us(5);           // settle

        uint8_t column_pins = PORTB & 0x70;

        // if ALL columns are high, no key on this row
        if( column_pins == 0x70 ){ continue; }

        for( uint8_t column = 0; column < 3; column++ ){

            // active-low column detect
            if( !(column_pins & ( 1 << (column+4) )) ){

                // # toggles between numbers and cursors
                if( row == 3 && column == 2 ) {   
                    numeric_or_cursors = !numeric_or_cursors;
                    __delay_ms(500);
                    return;
                }

                if( numeric_or_cursors ){
                    nec_ir_command = keypad_to_remote_numeric[row][column];
                }else{
                    nec_ir_command = keypad_to_remote_cursor[row][column];
                }

                __delay_ms(400);
                return;
            }
        }
    }
}




///////////////////////
// INTERRUPT HANDLER //
///////////////////////     
    
static void __interrupt() isr (void){
    
    // TIMER3 & CCP3 - PWM for Servo1
    if(TMR3IF){

        CCP3CON = 0;
        CCP3_PIN = 1; // set pin HIGH
        CCP3CONbits.CCP3M = 0b1001; // compare mode, clear ccp pin on match
        TMR3IF = 0; 
        TMR3 = TMR_PRELOAD;
    }

    // TIMER5 & CCP5 - PWM for Servo2
    if(TMR5IF){

        CCP5CON = 0;
        CCP5_PIN = 1; // set pin HIGH
        CCP5CONbits.CCP5M = 0b1001; // compare mode, clear ccp pin on match
        TMR5IF = 0; 
        TMR5 = TMR_PRELOAD;
    }

    // INT0 - infrared sensor
    if( INTCONbits.INT0IF ){ 
        
        INTCONbits.INT0IE = 0;
        
        nec_ir_gather_data();

        INTCONbits.INT0IE = 1;

        INTCONbits.INT0IF = 0;
    } 

    // INT1 - tilt switch
    if( INTCON3bits.INT1IF ){ 
        
        INTCON3bits.INT1IF = 0;
    }

    // INT2 - battery charger
    if( INTCON3bits.INT2IF ){ 
        
        INTCON3bits.INT2IF = 0;
    }

    // TIMER1 - the 32.768kHz crystal provides our one second clock pulse
    if( PIR1bits.TMR1IF ){ 
            
        // clear interrupt flag
        PIR1bits.TMR1IF = 0;                   

        // resetting the timer/counter to this value and counting up until 
        // an overflow occurs is what gives us an accurate 1sec interrupt
        // that updates the countdown timer, and counters a / b etc.
        TMR1H  = 0x80;
        TMR1L  = 0x00;

        // tick counter A: return to timer screen, turn off PWM timers, clear cursors
        TICK_COUNTER_A ++;  if( TICK_COUNTER_A > 20   ){ TICK_COUNTER_A = 0; }
        
        // tick counter B: auto power off
        TICK_COUNTER_B ++;  if( TICK_COUNTER_B > 2000 ){ TICK_COUNTER_B = 0; }

        // clear, so auto power off warning / countdown only occurs once per second
        AUTO_OFF_FLAG = false;

        // set timer update flag so a decrement occurs when locked
        if( LOCK_STATE_FLAG == LOCKED ){ 
            
            TIMER_UPDATE_FLAG = true;  
            
            TICK_COUNTER_B = 0; // so auto power off never occurs while locked!
        }
    }
    
    // IOC for PORTB
    if( INTCONbits.RBIF ){

        // read PORTB to update IOC latch
        volatile uint8_t qq = PORTB;

        INTCONbits.RBIF = 0;
    }    
}



/////////////////////////////////////////////////////////////////
// PERIPHERALS INITIALISED AT POWER ON AND RESUMING FROM SLEEP //
/////////////////////////////////////////////////////////////////

static void peripherals_init(){

    OSCTUNEbits.PLLEN   = 1;        // phase lock loop enable
    OSCCON2bits.SOSCGO  = 1;        // secondary oscillator 
    T1CONbits.T1SOSCEN  = 1;        // secondary oscillator
    OSCCON2bits.PRISD   = 1;        // primary oscillator drive circuit on

    // Peripheral Module Disable - power to all modules
    PMD0                = 0x00;
    PMD1                = 0x00;
    PMD2                = 0x00;

    // port configurations
    TRISA               = 0xC0;     // all out, except A7,A6 8MHz XTAL                 (0b11000000) 
    TRISB               = 0x77;     // all in, except B3,B7                            (0b01110111)
    TRISC               = 0x83;     // all out, except C7,C1,C0 (UART1_RX, 32kHz XTAL) (0b10000011)
    TRISD               = 0x40;     // all out, except RD6 (UART1_TX)                  (0b01000000)
    TRISE               = 0x02;     // all out, except E1 (BATT_AN6)                   (0b00000010)

    // interrupt configuration
    GIE                 = 1;        // global interrupt enable
    PEIE                = 1;        // peripheral interrupt enable bit  
    INT0IE              = 1;        // enable INT0 for: IR remote control data
    INTEDG0             = 1;        // rising edge
    INT1IE              = 1;        // enable INT1 for: tilt switch
    INTEDG1             = 1;        // rising edge
    INT2IE              = 1;        // enable INT1 for: battery charger
    INTEDG2             = 1;        // rising edge

    // set digital and analog pins
    ANSELA              = 0;
    ANSELB              = 0;
    ANSELC              = 0;
    ANSELD              = 0;                
    ANSELE              = 0x02;     // AN6

    // disable comparitor module
    CM1CON0bits.C1ON    = 0;
    CM2CON0bits.C2ON    = 0;

    // Analog to Digital Converter - for measuring battery voltage
    ADCON2bits.ADFM     = 1;        // ADC result is right justified
    ADCON2bits.ADCS     = 0b110;    // Fosc/32 is the conversion clock (2us)
    ADCON2bits.ACQT     = 0b101;    // acquisition time (12 TAD)
    ADCON1bits.TRIGSEL  = 0b0;
    ADCON1bits.PVCFG    = 0b00;     // V REF + source = VDD/+5V
    ADCON1bits.NVCFG    = 0b00;     // V REF - source = VSS/GND   
    ADCON0bits.ADON     = 1;        // turn on ADC

    // dummy ADC conversion
    PIR1bits.ADIF       = 0;        // clear interrupt flag
    ADCON0bits.GO       = 1;        // begin A/D conversion
    while( !PIR1bits.ADIF );

    INTCON2bits.RBPU    = 1;        // disable (1) weak pull-ups on port B
    
    // Slew Rate Control - always standard transitions
    SLRCONbits.SLRA     = 0;
    SLRCONbits.SLRB     = 0;
    SLRCONbits.SLRC     = 0;
    SLRCONbits.SLRD     = 0;
    SLRCONbits.SLRE     = 0;    

    // TIMER1 - 32.768KHz driven 1sec clock
    T1CONbits.RD16      = 0;
    T1CONbits.T1CKPS    = 0;
    T1CONbits.T1SYNC    = 1;
    T1CONbits.TMR1CS    = 1;
    T1CONbits.TMR1ON    = 1;  
    T1CONbits.TMR1CS0   = 0;        // external clocking on TxCKI Pin
    T1CONbits.TMR1CS1   = 1;
    T1CONbits.T1SOSCEN  = 1;        // secondary oscillator enable

    T1GCONbits.TMR1GE   = 0;        // gate enable - counts regardless of gate function
    PIE1bits.TMR1IE     = 1;        // timer 1 interrupt enable

    // TIMER2 & CCP2 - drive piezo buzzer when required (on RB3)
    T2CONbits.T2CKPS    = 0b10;     // prescaler is 16
    T2CONbits.TMR2ON    = 1;        // turn on timer2
    PR2                 = 250;      // set the period
    CCPR2L              = 65;       // set duty cycle - 65 originally
    CCP2CONbits.CCP2M   = 0;        // PWM mode: 0b1100, set elsewhere to turn on
    CCP2CONbits.DC2B    = 0b11;     // PWM duty cycle least significant bits

    // TIMER3 & CCP3 - 20ms PWM for servo control 1 (on RE0)
    T3CONbits.T3RD16    = 0;
    T3CONbits.T3CKPS    = 0b10;     // prescaler ratio (1:4)
    T3CONbits.TMR3CS    = 0;        // instruction clock (FOSC/4)
    T3CONbits.TMR3ON    = 1;        // turn on timer3
    PIE2bits.TMR3IE     = 1;        // timer3 Interrupt Enable bit    
    TMR3 = TMR_PRELOAD;             // preload timer

    CCPTMRS0bits.C3TSEL = 0b01;     // CCP3 capture/compare modes use timer3
    CCP3CONbits.CCP3M   = 0b1001;   // compare mode, clear CCP pin on match
    CCPR3 = TMR_PRELOAD;

    // TIMER5 & CCP5 - 20ms PWM for servo control 2 (on RE2)
    T5CONbits.T5RD16    = 0;
    T5CONbits.T5CKPS    = 0b10;     // prescaler ratio (1:4)
    T5CONbits.TMR5CS    = 0;        // instruction clock (FOSC/4)
    T5CONbits.TMR5ON    = 1;        // turn on timer5
    PIE5bits.TMR5IE     = 1;        // Timer3 Interrupt Enable bit    
    TMR5 = TMR_PRELOAD;             // preload timer

    CCPTMRS1bits.C5TSEL = 0b10;     // CCP5 capture/compare modes use timer5
    CCP5CONbits.CCP5M   = 0b1001;   // Compare mode, clear CCP pin on match
    CCPR5 = TMR_PRELOAD;

    // TIMER4 & CCP1 - IR Sender TX
    CCPTMRS0bits.C1TSEL = 0b01;     // CCP1 uses Timer4 for PWM
    PR4                 = 52;       // PWM period (~38 kHz)
    CCPR1L              = 28;       // PWM duty cycle 8 m.s.b (~33%) (70 >> 2)
    CCP1CONbits.DC1B    = 2;        // PWM duty cycle 2 l.s.b (70 & 0x03)
    CCP1CONbits.CCP1M   = 0;        // PWM mode: 0b1100, set elsewhere to turn on
    T4CONbits.T4CKPS    = 0b01;     // prescaler 4
    T4CONbits.TMR4ON    = 1;        // turn on timer4

    // SERIAL PORT #2 - for communicating with DY1703A MP3 player chip
    TXSTA2bits.CSRC     = 1;        // clock from BRG
    TXSTA2bits.SYNC     = 0;        // asynchronous EUART
    TXSTA2bits.TXEN     = 1;        // enables transmitter
    TXSTA2bits.TX9      = 0;        // 8bit transmit
    
    RCSTA2bits.CREN     = 0;        // enables receiver - disabled    
    RCSTA2bits.RX9      = 0;        // set to 8bit receive
    RCSTA2bits.SPEN     = 1;        // serial port enable
    
    TXSTA2bits.BRGH     = 0;        // baud rate generator - high baud rate
    TXSTA2bits.SENDB    = 0;        // send sync break on next transmission        
    BAUDCON2bits.BRG16  = 0;        // baud rate generator - 8 bit operation
    BAUDCON2bits.CKTXP  = 0;        // tx polarity
    SPBRG2              = 51;       // baud rate generator - 51=9600, 103=4800, 207=2400

    // power-on servos and enable regulators
    PWR_SERVOS          = true;
    PWR_DEVICES         = true;
    PWR_AUDIO           = true;

    // wake from keypad on button 
    IOCBbits.IOCB4      = 1;        // enable IOC on RB4    
    IOCBbits.IOCB5      = 1;        // enable IOC on RB5
    IOCBbits.IOCB6      = 1;        // enable IOC on RB6
    INTCONbits.RBIE     = 0;        // disable PORTB IOC interrupt  
    

    CHARGER_ENABLED     = false;
}



///////////////////////////////////////////////
// PERIPHERALS SHUT DOWN WHEN ENTERING SLEEP //
///////////////////////////////////////////////

static void peripherals_down(){

    OSCTUNEbits.PLLEN  = 0;         // phase lock loop
    OSCCON2bits.SOSCGO = 0;         // secondary oscillator
    T1CONbits.T1SOSCEN = 0;         // secondary oscillator
    OSCCON2bits.PRISD  = 0;         // primary oscillator drive circuit off

    T1CONbits.TMR1ON   = 0;
    T2CONbits.TMR2ON   = 0;
    T3CONbits.TMR3ON   = 0;
    T5CONbits.TMR5ON   = 0;

    ANSELE             = 0x00;      // all digital pins

    LATA               = 0x00;      // all ports outputs
    LATB               = 0x00;
    LATC               = 0x00;
    LATD               = 0x00;
    LATE               = 0x00;

    ADCON0bits.ADON    = 0;         // disable ADC

    // power-off servos and disable regulators
    PWR_SERVOS  = false;   __delay_ms(300);
    PWR_AUDIO   = false;   __delay_ms(300);
    PWR_DEVICES = false;   __delay_ms(300);  
    
    // Peripheral Module Disable - turn off power to all modules
    PMD0 = 0xFF;
    PMD1 = 0xFF;
    PMD2 = 0xFF;

    // wake from sleep on keypad button press
    volatile uint8_t qq = PORTB;    // read PORTB to update IOC latch

    INTCONbits.RBIF     = 0;        // clear PORTB IOC interrupt
    INTCONbits.RBIE     = 1;        // enable PORTB IOC interrupt 
}



///////////////////////////////////////////////////////
// SYSTEM INITIALISATION WHEN POWERING ON AND WAKING //
///////////////////////////////////////////////////////

static void sys_initialise(){

    lcd_backlight_on();

    // startup message
    strcpy( lcd_row_one, " BDSM TIME TRAP ");
    strcpy( lcd_row_two, "  BY V9TRONICS  "); 
    lcd_refresh();

    __delay_ms(2000);

    // show battery state
    BATTERY_LEVEL_PC = get_battery_level_pc();

    if( BATTERY_LEVEL_PC > 15 ){

        sprintf( lcd_row_two, "      %d %%      ", BATTERY_LEVEL_PC );

    }else{
    
        strcpy( lcd_row_two, "  RECHARGE NOW  ");
    }

    strcpy( lcd_row_one, " BATTERY STATUS ");

    lcd_refresh();

    __delay_ms(2000);

    // set the servos to their unlock position
    // if watchdog timer caused a reset, unlock occurs automatically
    set_servo_position(1,100);
    set_servo_position(2,0);

    // go straight to the timer screen... 
    DISPLAY_MENU   = MENU_UNLOCK_AFTER;
    nec_ir_command = 1;

    // set audio volume and stop playing
    dy1703a_set_volume( NOISE_VOLUME );
    dy1703a_stop();
}



/*
    888b     d888        d8888 8888888 888b    888 
    8888b   d8888       d88888   888   8888b   888 
    88888b.d88888      d88P888   888   88888b  888 
    888Y88888P888     d88P 888   888   888Y88b 888 
    888 Y888P 888    d88P  888   888   888 Y88b888 
    888  Y8P  888   d88P   888   888   888  Y88888 
    888   "   888  d8888888888   888   888   Y8888 
    888       888 d88P     888 8888888 888    Y888
*/                                                         
                                                               
void main(){

    OSCCONbits.IDLEN    = 0;        // enters sleep mode on SLEEP instruction
    OSCCONbits.OSTS     = 1;        // clock defined by FOSC<3:0> of CONFIG1H
    OSCCONbits.SCS      = 0;        // primary clock (determined by FOSC<3:0> CONFIG1H)
    OSCCON2bits.PLLRDY  = 1;        // only required when using INTOSC + 4xPLL
    OSCCON2bits.SOSCRUN = 1;

    /////////////////////////////
    // INITIALISE THE HARDWARE //
    /////////////////////////////

    peripherals_init();

    lcd_initialise();

    load_user_parameters();

    sys_initialise();

    keypad_init();

    while(1){ 

        //////////////////////////
        // CLEAR WATCHDOG TIMER //
        //////////////////////////

        // this must be cleared every 32 seconds or a reset will occur
        // resulting in the unlock routine being called
        asm("CLRWDT");


        //////////////////////////////////////////////
        // RESPOND TO BATTERY CHARGER BEING POWERED //
        //////////////////////////////////////////////

        while( CHARGER_POWERED ){

            // charging message
            strcpy( lcd_row_one, "    CHARGING    ");
            strcpy( lcd_row_two, "  POWERING OFF  ");

            lcd_refresh();

            __delay_ms(2000);

            // unlock, if locked

            if( LOCK_STATE_FLAG == LOCKED ){

                LOCK_ACTION = UNLOCK;

                break;
            }

            CHARGER_ENABLED = true;

            __delay_ms(2000);

            // charging message
            strcpy( lcd_row_one, "    CHARGING    ");
            strcpy( lcd_row_two, "     FAILED     ");

            lcd_refresh();

            CHARGER_ENABLED = false;

            __delay_ms(2000);
        }



        //////////////////////////////////////////////
        // TIMER DECREMENT, CALL UNLOCK WHEN ZEROED //
        //////////////////////////////////////////////

        if( TIMER_UPDATE_FLAG ){

            TIMER_UPDATE_FLAG = false;

            // decrement timer when locked

            if( LOCK_STATE_FLAG == LOCKED ){

                    if( TIMER_SECONDS[1] == 0 ){ 
                        
                        TIMER_SECONDS[1] = 9;  

                        if( TIMER_SECONDS[0] == 0 ){

                            TIMER_SECONDS[0] = 5;  

                            if( TIMER_MINUTES[2] == 0 ){ 
                                
                                TIMER_MINUTES[2] = 9;  

                                if( TIMER_MINUTES[1] == 0 ){

                                    TIMER_MINUTES[1] = 9;  

                                    if( TIMER_MINUTES[0] == 0 ){

                                        TIMER_SECONDS[1] = 0;
                                        TIMER_SECONDS[0] = 0;
                                        TIMER_MINUTES[2] = 0;
                                        TIMER_MINUTES[1] = 0;

                                        LOCK_ACTION = UNLOCK;

                                    }else{ TIMER_MINUTES[0]--; }

                                }else{ TIMER_MINUTES[1]--; }

                            }else{ TIMER_MINUTES[2]--; }

                        }else{ TIMER_SECONDS[0]--; }

                    }else{ TIMER_SECONDS[1]--; }
            }


            // show the timer screen
            if( DISPLAY_MENU == MENU_UNLOCK_AFTER ){

                // show 'in' while locked/counting down, 'after' while unlocked
                if( LOCK_STATE_FLAG == UNLOCKED ){
                    strcpy( lcd_row_one, "  UNLOCK AFTER  ");
                }else{
                    strcpy( lcd_row_one, "  UNLOCKING IN  ");                    
                }

                sprintf( lcd_row_two, "    %d%d%dm %d%ds   ",
                TIMER_MINUTES[0],TIMER_MINUTES[1],TIMER_MINUTES[2],
                TIMER_SECONDS[0],TIMER_SECONDS[1]); 

                lcd_refresh();
            }


            // return to the timer screen after 10 seconds - calls code above, later
            if( TICK_COUNTER_A == 10 ){ DISPLAY_MENU = MENU_UNLOCK_AFTER; }


            /////////////////////////////////////////////////
            // TRANSMIT TIMER TO REMOTE CLOCK DISPLAY UNIT //
            /////////////////////////////////////////////////

            // Convert timer digits to Binary Coded Decimal and transmit to a remote clock display unit, 
            // but only while counting down, to prevent conflict with the infrared remote control.

            if( REMOTE_DISPLAY_ON && DISPLAY_MENU == MENU_UNLOCK_AFTER && lcd_row_two_cursor == 0 ){

                uint16_t minutes = (TIMER_MINUTES[0]*100) + (TIMER_MINUTES[1]*10) + TIMER_MINUTES[2];

                uint8_t hh_byte=0, mm_byte=0, ss_byte=0;

                hh_byte  = (minutes/60)/10; // tens of hours
                hh_byte <<= 4;
                hh_byte |= (minutes/60)%10; // single hours

                mm_byte  = (minutes%60)/10; // tens of minutes
                mm_byte <<= 4;
                mm_byte |= (minutes%60)%10; // single minutes

                ss_byte  = TIMER_SECONDS[0];
                ss_byte <<= 4;
                ss_byte |= TIMER_SECONDS[1];

                // Our checksum can't be greater than 0xFF, we'll use 0x39 to turn off the hours segment, 
                // and 0x60 to turn off minutes and seconds on the remote clock unit. 0x39+0x60+0x60 = 0xF9
                // The maximum time is 999 minutes (16h:39m:59s) so this works fine. 

                if( REMOTE_DISPLAY_ON == 2){

                    // blank segments when they zero-out, so eventually we'll have a blank display,
                    // don't blank the minutes or seconds until their preceding digits have been zeroed

                    if( hh_byte == 0 ){  hh_byte = 0x39;  }

                    if( hh_byte == 0x39 && mm_byte == 0 ){  mm_byte = 0x60;  }

                    if( mm_byte == 0x60 && ss_byte == 0 ){  ss_byte = 0x60;  }
                }

                INTCONbits.INT0IE = 0; // disable, to prevent ir sensor causing an interrupt

                transmit_to_remote_display( hh_byte, mm_byte, ss_byte, 2 );

                INTCONbits.INT0IE = 1; // we're done, so enable again
            }

            ////////////////////////
            // BATTERY MONITORING //
            ////////////////////////

            BATTERY_MONITOR[0] ++;

            if( BATTERY_MONITOR[0] % 5 == 0 ){  // true at 5, 10, 15, 20, 25, 30 seconds

                BATTERY_MONITOR[2] = get_battery_level_pc();

                if( BATTERY_MONITOR[2] < BATTERY_THRESHOLD ){

                    BATTERY_MONITOR[1] ++;
                }

                // set power low flag once battery has fallen below threshold five times
                if( BATTERY_MONITOR[1] == 5 ){

                    POWER_LOW_FLAG = true;
                }

                // reset battery monitor
                if( BATTERY_MONITOR[0] > 25 ){ 

                    BATTERY_MONITOR[0] = 0; // count for 5sec interval
                    BATTERY_MONITOR[1] = 0; // low battery detections
                    BATTERY_MONITOR[2] = 0; // low battery level percentage
                }
            }
        }



        ///////////////////////////////////////////////
        // DISPLAY REFRESH AND REMOTE CONTROL DECODE //
        ///////////////////////////////////////////////

        if( lcd_display_refresh ){  lcd_refresh();  }  

        // if nec_ir_decode has been set, then raw data has been received,
        // so decode it and store the result in nec_ir_command for use below
        if( nec_ir_decode ){  nec_ir_decode_data();  }

        // clear the tilt switch flag whenever a button has been pressed
        if( nec_ir_command ){ TILT_SWITCH_FLAG = false; }



        ////////////////////////////////////////////
        // LOW BATTERY WARNING, UNLOCK, POWER-OFF //
        ////////////////////////////////////////////

        if( POWER_LOW_FLAG ){

            strcpy( lcd_row_one, "  LOW  BATTERY  ");
            sprintf( lcd_row_two, " %02d%%  REMAINING ", BATTERY_MONITOR[2] );
            lcd_refresh();

            __delay_ms(2000);

            if( LOCK_STATE_FLAG == LOCKED ){

                LOCK_ACTION = UNLOCK;
            }

            POWER_OFF_FLAG = true;
        }


        //////////////////////
        // LOCKING ROUTINES //
        //////////////////////

        // move servos to LOCKED position, but only if removable
        // frame section is level / has been re-inserted

        if( LOCK_ACTION == LOCK && TILT_SWITCH == false ){

            set_servo_pwm_timers(true);

            TICK_COUNTER_A = 0;

            LOCK_ACTION = 0;

            strcpy( lcd_row_one, "    LOCKING!    ");
            strcpy( lcd_row_two, "                "); 
            lcd_refresh();

            // move servo horns slowly 
            for( uint8_t a=0, b=100; a<=100; a++){
                
                set_servo_position(1,b);  //0%
                set_servo_position(2,a);  //100%

                __delay_ms(10);  b-=1;  // delay was 15ms previously
            }

            LOCK_STATE_FLAG = LOCKED;

            if( NOISE_ON ){

                dy1703a_set_volume( NOISE_VOLUME );

                dy1703a_play_track( NOISE_TRACK );
                dy1703a_play_mode(1); // repeat continuously
            }

            if( BUZZER_ON ){
                set_buzzer(150,true);
                set_buzzer(150,false);
            }

            lcd_clear_cursors = true; // clear flashing cursor
        }



        // move servos to UNLOCKED position, and set state flag
        if( LOCK_ACTION == UNLOCK ){

            set_servo_pwm_timers(true);

            TICK_COUNTER_A = 0;

            LOCK_ACTION = 0;

            // move servo horns slowly - if they're not in unlocked position already
            if( get_servo_position(1) == 0 || get_servo_position(2) == 100 ){

                strcpy( lcd_row_one, "   UNLOCKING!   ");
                strcpy( lcd_row_two, "                ");                
                lcd_refresh();

                for( uint8_t a=0, b=100; a<=100; a++ ){
                    
                    set_servo_position(1,a);  //100%
                    set_servo_position(2,b);  //0%

                    __delay_ms(10);  b-=1;                
                }
            }

            LOCK_STATE_FLAG = UNLOCKED;

            dy1703a_stop();

            if( BUZZER_ON ){
                set_buzzer(500,true);
                set_buzzer(500,false);
            }
        }


        //////////////////////////
        // POWER OFF/ON ROUTINE //
        //////////////////////////

        if( POWER_OFF_FLAG ){

            POWER_OFF_FLAG = false;

            strcpy( lcd_row_one, " GOING TO SLEEP ");
            strcpy( lcd_row_two, "    GOODBYE!    ");            
            lcd_refresh();

            __delay_ms(2000);

            peripherals_down();

            SLEEP();

            NOP();

            peripherals_init();

            lcd_initialise();

            strcpy( lcd_row_one, "     HELLO!     ");
            strcpy( lcd_row_two, "     HELLO!     ");            
            lcd_refresh();

            __delay_ms(1000);

            sys_initialise();

            keypad_init();
        }


        //////////////////////////////
        // AUTO POWER OFF & WARNING //
        //////////////////////////////

        if( AUTO_OFF_FLAG == false && (AUTO_OFF*60) >= 60 ){

            AUTO_OFF_FLAG = true;

            // give a 25 second countdown warning, prior to automatic power off
            if( TICK_COUNTER_B > ( (AUTO_OFF *60) - 25 ) ){

                set_buzzer(25,true);
                set_buzzer(25,false);

                strcpy( lcd_row_one, "  POWER OFF IN  ");

                uint16_t auto_off_countdown = (AUTO_OFF * 60) - TICK_COUNTER_B;

                if( auto_off_countdown > 9  ){            
                    sprintf( lcd_row_two, "   %d SECONDS   ",  auto_off_countdown );
                }
                
                if( auto_off_countdown < 10 ){

                    if( auto_off_countdown == 1 ){

                        strcpy( lcd_row_two, "    1 SECOND    ");
                    }else{
                        sprintf( lcd_row_two, "   %d  SECONDS   ", auto_off_countdown );
                    }
                }

                lcd_display_refresh = true;

                if( TICK_COUNTER_B >= (AUTO_OFF *60) ){

                    TICK_COUNTER_B = 0;

                    // call unlock and power off routines
                    LOCK_ACTION = UNLOCK;

                    POWER_OFF_FLAG = true;
                }
            }
        }



        //////////////////////////////////////////
        // MENU NAVIGATION & NUMERIC TIME ENTRY //
        //////////////////////////////////////////

        // move backwards through the menu, and clear any flashing cursor
        if( nec_ir_command == REMOTE_BTN_LT && DISPLAY_MENU > MENU_UNLOCK_BETWEEN ){

            DISPLAY_MENU --; 

            TIMER_DIGIT_POSITION = 0;

            lcd_clear_cursors = true;
        }

        // allow menu to roll around to the end, once we're at the beginning
        //if( nec_ir_command == REMOTE_BTN_LT && DISPLAY_MENU == MENU_UNLOCK_BETWEEN ){
        //    DISPLAY_MENU = MENU_REMOTE_DISPLAY;
        //}

        // move forwards through the menu, and clear any flashing cursor
        if( nec_ir_command == REMOTE_BTN_RT && DISPLAY_MENU < MENU_REMOTE_DISPLAY ){

            DISPLAY_MENU ++; 
            
            TIMER_DIGIT_POSITION = 0;

            lcd_clear_cursors = true;
        }

        // allow menu to roll around to the beginning, once we're at the end
        //if( nec_ir_command == REMOTE_BTN_RT && DISPLAY_MENU == MENU_REMOTE_DISPLAY ){
        //    DISPLAY_MENU = MENU_UNLOCK_BETWEEN;
        //}

        // set the minutes countdown using the numeric buttons
        switch( nec_ir_command ){
            case REMOTE_BTN_0: COMMAND_IS_NUMERIC = 99; break; // 99 represents 0
            case REMOTE_BTN_1: COMMAND_IS_NUMERIC = 1;  break;
            case REMOTE_BTN_2: COMMAND_IS_NUMERIC = 2;  break;
            case REMOTE_BTN_3: COMMAND_IS_NUMERIC = 3;  break;
            case REMOTE_BTN_4: COMMAND_IS_NUMERIC = 4;  break;
            case REMOTE_BTN_5: COMMAND_IS_NUMERIC = 5;  break;
            case REMOTE_BTN_6: COMMAND_IS_NUMERIC = 6;  break;
            case REMOTE_BTN_7: COMMAND_IS_NUMERIC = 7;  break;
            case REMOTE_BTN_8: COMMAND_IS_NUMERIC = 8;  break;
            case REMOTE_BTN_9: COMMAND_IS_NUMERIC = 9;  break;
            default: COMMAND_IS_NUMERIC = false;
        }



        if( DISPLAY_MENU == MENU_UNLOCK_BETWEEN && COMMAND_IS_NUMERIC ){

            if( COMMAND_IS_NUMERIC == 99 ){ COMMAND_IS_NUMERIC = 0; }

            BETWEEN_MINUTES[TIMER_DIGIT_POSITION] = COMMAND_IS_NUMERIC;

            TIMER_DIGIT_POSITION ++;

            if( TIMER_DIGIT_POSITION > 5 ){ TIMER_DIGIT_POSITION = 0; }

            // flash cursor at current digit
            // accounting for there being two sets of three digits, spaced five digits apart
            if( TIMER_DIGIT_POSITION < 3 ){ lcd_row_two_cursor = 3 + TIMER_DIGIT_POSITION; }
            if( TIMER_DIGIT_POSITION > 2 ){ lcd_row_two_cursor = 8 + TIMER_DIGIT_POSITION; }
        }


        if( DISPLAY_MENU == MENU_UNLOCK_AFTER && COMMAND_IS_NUMERIC ){

            if( COMMAND_IS_NUMERIC == 99 ){ COMMAND_IS_NUMERIC = 0; }

            TIMER_MINUTES[TIMER_DIGIT_POSITION] = COMMAND_IS_NUMERIC;

            TIMER_DIGIT_POSITION ++;

            if( TIMER_DIGIT_POSITION > 2 ){ TIMER_DIGIT_POSITION = 0; }

            lcd_row_two_cursor = 5 + TIMER_DIGIT_POSITION; // flash cursor at current digit

            // we're adjusting the timer, so reset the seconds, but not while the countdown is active
            if( LOCK_STATE_FLAG == UNLOCKED ){

                TIMER_SECONDS[0]=0;
                TIMER_SECONDS[1]=0;
            }
        }


        ////////////////////////////////
        // ALTER VALUES OF MENU ITEMS //
        ////////////////////////////////

        if( nec_ir_command == REMOTE_BTN_UP ){ 

            // adjust auto power off timer (30 minute maximum)
            if( DISPLAY_MENU == MENU_AUTO_POWER_OFF && AUTO_OFF < 30 ){ AUTO_OFF ++; }

            // adjust noise volume up
            if( DISPLAY_MENU == MENU_NOISE_VOLUME && NOISE_VOLUME < 30 ){

                NOISE_VOLUME ++;

                dy1703a_set_volume( NOISE_VOLUME );
            }

            // adjust noise track up
            if( DISPLAY_MENU == MENU_NOISE_TRACK && NOISE_TRACK < 13 ){

                NOISE_TRACK ++;

                if( NOISE_ON == 2 || (NOISE_ON == 1 && LOCK_STATE_FLAG == LOCKED) ){
                    dy1703a_play_track( NOISE_TRACK );
                }else{
                    dy1703a_select_track( NOISE_TRACK );
                    dy1703a_stop();
                }
                
            }            
        }

        if( nec_ir_command == REMOTE_BTN_DN ){ 

            // adjust auto power off timer (minimum is 0 / disabled)
            if( DISPLAY_MENU == MENU_AUTO_POWER_OFF && AUTO_OFF > 0 ){ AUTO_OFF --; }

            // adjust volume down
            if( DISPLAY_MENU == MENU_NOISE_VOLUME && NOISE_VOLUME > 0 ){ 

                NOISE_VOLUME --;

                dy1703a_set_volume( NOISE_VOLUME );
            }

            // adjust noise track down
            if( DISPLAY_MENU == MENU_NOISE_TRACK && NOISE_TRACK > 1 ){

                NOISE_TRACK --;

                if( NOISE_ON == 2 || (NOISE_ON == 1 && LOCK_STATE_FLAG == LOCKED) ){
                    dy1703a_play_track( NOISE_TRACK );
                }else{
                    dy1703a_select_track( NOISE_TRACK );
                    dy1703a_stop();
                }
            }
        }

        if( nec_ir_command == REMOTE_BTN_OK ){ 

            // reset cursor position
            TIMER_DIGIT_POSITION = 0;

            // toggle lock / timer
            if( DISPLAY_MENU == MENU_UNLOCK_AFTER ){

                lcd_backlight_on();

                lcd_clear_cursors = true; // clear any flashing cursor

                if( LOCK_STATE_FLAG == LOCKED   ){ LOCK_ACTION = UNLOCK; }

                if( LOCK_STATE_FLAG == UNLOCKED ){ LOCK_ACTION = LOCK;   }
            }

            // turn the power off, but always unlock first
            if( DISPLAY_MENU == MENU_POWER_OFF ){ 

                LOCK_ACTION = UNLOCK;

                POWER_OFF_FLAG = true;
            }

            // set the white noise: (0)disabled / (1)on when locked / (2)on-now
            if( DISPLAY_MENU == MENU_SENSORY_NOISE ){ 

                NOISE_ON ++;

                if( NOISE_ON > 2 ){ NOISE_ON = 0; }

                if( NOISE_ON == 0 ){

                    dy1703a_stop();
                }

                if( NOISE_ON == 1 && LOCK_STATE_FLAG == LOCKED ){

                    dy1703a_play_track( NOISE_TRACK );
                    dy1703a_play_mode(1);
                }

                if( NOISE_ON == 2 ){

                    dy1703a_play_track( NOISE_TRACK );
                    dy1703a_play_mode(1);
                }
            }

            // toggle the backlight on / off
            if( DISPLAY_MENU == MENU_LCD_BACKLIGHT ){ 
                lcd_backlight_toggle(); 
            }

            // set the buzzer: (0)disabled / (1)when locking / (2)always
            if( DISPLAY_MENU == MENU_BUZZER ){ 

                BUZZER_ON ++;
                if( BUZZER_ON > 2 ){ BUZZER_ON = 0; }
            }

            // battery status
            if( DISPLAY_MENU == MENU_BATTERY_STATUS ){ 

            }

            // save the user parameters to eeprom
            if( DISPLAY_MENU == MENU_SAVE_SETTINGS ){ 

                save_user_parameters();

                strcpy( lcd_row_two, " SETTINGS SAVED ");                
                lcd_refresh();  __delay_ms(2000);
            }

            // load the default user parameters
            if( DISPLAY_MENU == MENU_LOAD_DEFAULTS ){ 

                load_default_parameters();
                
                strcpy( lcd_row_two, "     LOADED     ");
                lcd_refresh();  __delay_ms(1500);
                strcpy( lcd_row_two, "   RESTARTING   ");                
                lcd_refresh();  __delay_ms(1500);

                asm("reset"); // restart to load defaults
            }

            // set the remote display: (0)disabled / (1)HH:MM:SS / (2)[HH?]:[MM?]:SS
            if( DISPLAY_MENU == MENU_REMOTE_DISPLAY ){ 

                REMOTE_DISPLAY_ON ++;
                if( REMOTE_DISPLAY_ON > 2 ){ REMOTE_DISPLAY_ON = 0; }
            } 
            
            // convert the min / max values to a random value
            if( DISPLAY_MENU == MENU_UNLOCK_BETWEEN ){

                lcd_clear_cursors = true; // clear any flashing cursor

                uint16_t random_minutes = get_random_minutes(
                    BETWEEN_MINUTES[0]*100 + 
                    BETWEEN_MINUTES[1]*10 + 
                    BETWEEN_MINUTES[2] , 
                    BETWEEN_MINUTES[3]*100 + 
                    BETWEEN_MINUTES[4]*10 + 
                    BETWEEN_MINUTES[5]
                );

                TIMER_MINUTES[0] = (uint8_t)   random_minutes / 100;
                TIMER_MINUTES[1] = (uint8_t) ( random_minutes / 10 ) % 10; 
                TIMER_MINUTES[2] = (uint8_t)   random_minutes % 10;

                // go straight to the timer screen
                DISPLAY_MENU = MENU_UNLOCK_AFTER;

                // call lock routine, if unlocked
                if( LOCK_STATE_FLAG == UNLOCKED ){ LOCK_ACTION = LOCK; }
            }
        }


        ///////////////////////////////////////
        // STAR (*) BUTTON TOGGLES BACKLIGHT //
        ///////////////////////////////////////

        if( nec_ir_command == REMOTE_BTN_ST ){

            // this must happen here so 'lcd backlight' menu screen stays in sync
            lcd_backlight_toggle();
        }  


        ///////////////////////////
        // POPULATE MENU SCREENS //
        ///////////////////////////

        if( nec_ir_command ){

            if( DISPLAY_MENU == MENU_UNLOCK_BETWEEN ){
                strcpy( lcd_row_one, " UNLOCK BETWEEN ");
                sprintf( lcd_row_two, "  %d%d%dm TO %d%d%dm  ",
                BETWEEN_MINUTES[0],BETWEEN_MINUTES[1],BETWEEN_MINUTES[2],
                BETWEEN_MINUTES[3],BETWEEN_MINUTES[4],BETWEEN_MINUTES[5]);
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_UNLOCK_AFTER ){

                if( LOCK_STATE_FLAG == UNLOCKED ){
                    strcpy( lcd_row_one, "  UNLOCK AFTER  ");
                }else{
                    strcpy( lcd_row_one, "  UNLOCKING IN  ");                    
                }

                sprintf( lcd_row_two, "    %d%d%dm %d%ds   ",
                TIMER_MINUTES[0],TIMER_MINUTES[1],TIMER_MINUTES[2],
                TIMER_SECONDS[0],TIMER_SECONDS[1]);
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_POWER_OFF ){
                strcpy( lcd_row_one, "   POWER  OFF   ");
                strcpy( lcd_row_two, "   PRESS [OK]   "); 
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_SENSORY_NOISE ){
                strcpy( lcd_row_one, " SENSORY  NOISE ");
                if(NOISE_ON == 0){ strcpy(lcd_row_two,"    DISABLED    "); }
                if(NOISE_ON == 1){ strcpy(lcd_row_two," ON WHEN LOCKED "); }
                if(NOISE_ON == 2){ strcpy(lcd_row_two,"     ON NOW     "); }
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_NOISE_VOLUME ){
                strcpy( lcd_row_one, "  NOISE VOLUME  ");
                if(NOISE_VOLUME > 9 ){ sprintf( lcd_row_two, "       %d       ", NOISE_VOLUME); }
                if(NOISE_VOLUME < 10){ sprintf( lcd_row_two, "       0%d      ", NOISE_VOLUME); }                 
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_NOISE_TRACK ){
                strcpy( lcd_row_one, "   NOISE TYPE   ");
                if(NOISE_TRACK == 1 ){ strcpy(lcd_row_two,"  PURE NOISE 1  "); }
                if(NOISE_TRACK == 2 ){ strcpy(lcd_row_two,"  PURE NOISE 2  "); }
                if(NOISE_TRACK == 3 ){ strcpy(lcd_row_two,"  PURE NOISE 3  "); } // default
                if(NOISE_TRACK == 4 ){ strcpy(lcd_row_two,"  PURE NOISE 4  "); }
                if(NOISE_TRACK == 5 ){ strcpy(lcd_row_two,"  PURE NOISE 5  "); }
                if(NOISE_TRACK == 6 ){ strcpy(lcd_row_two,"  PURE NOISE 6  "); }
                if(NOISE_TRACK == 7 ){ strcpy(lcd_row_two,"    DESK FAN    "); }
                if(NOISE_TRACK == 8 ){ strcpy(lcd_row_two,"   FAN HEATER   "); }
                if(NOISE_TRACK == 9 ){ strcpy(lcd_row_two,"  SPLASHY RAIN  "); }
                if(NOISE_TRACK == 10){ strcpy(lcd_row_two,"   HEAVY RAIN   "); }
                if(NOISE_TRACK == 11){ strcpy(lcd_row_two,"    OCEAN  1    "); }
                if(NOISE_TRACK == 12){ strcpy(lcd_row_two,"    OCEAN  2    "); }                
                if(NOISE_TRACK == 13){ strcpy(lcd_row_two,"    OCEAN  3    "); }                
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_LCD_BACKLIGHT ){
                strcpy( lcd_row_one, " LCD  BACKLIGHT ");
                if(lcd_back_light){  strcpy(lcd_row_two,"       ON       "); }
                if(!lcd_back_light){ strcpy(lcd_row_two,"    DISABLED    "); }
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_BUZZER ){
                strcpy( lcd_row_one, "     BUZZER     ");
                if(BUZZER_ON == 0){ strcpy(lcd_row_two,"    DISABLED    "); }
                if(BUZZER_ON == 1){ strcpy(lcd_row_two,"  WHEN LOCKING  "); }
                if(BUZZER_ON == 2){ strcpy(lcd_row_two,"     ALWAYS     "); }
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_BATTERY_STATUS ){
                strcpy( lcd_row_one, " BATTERY STATUS ");                
                sprintf( lcd_row_two, "      %d %%     ", get_battery_level_pc() );
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_AUTO_POWER_OFF ){
                strcpy( lcd_row_one, " AUTO POWER OFF ");
                if(AUTO_OFF >  9 ){ sprintf( lcd_row_two, "   %d MINUTES   ", AUTO_OFF ); }
                if(AUTO_OFF <= 9 ){ sprintf( lcd_row_two, "   %d  MINUTES   ", AUTO_OFF ); }
                if(AUTO_OFF == 1 ){ sprintf( lcd_row_two, "   %d  MINUTE    ", AUTO_OFF ); }
                if(AUTO_OFF <  1 ){ strcpy( lcd_row_two, "    DISABLED    "); }
                lcd_display_refresh = true;               
            }

            if( DISPLAY_MENU == MENU_SAVE_SETTINGS ){
                strcpy( lcd_row_one, " SAVE  SETTINGS ");
                strcpy( lcd_row_two, "   PRESS [OK]   ");
                lcd_display_refresh = true;
            }
            if( DISPLAY_MENU == MENU_LOAD_DEFAULTS ){
                strcpy( lcd_row_one, " LOAD  DEFAULTS ");
                strcpy( lcd_row_two, "   PRESS [OK]   ");
                lcd_display_refresh = true;
            }

            if( DISPLAY_MENU == MENU_REMOTE_DISPLAY ){
                strcpy( lcd_row_one, " REMOTE DISPLAY ");
                if(REMOTE_DISPLAY_ON == 0){ strcpy(lcd_row_two,"    DISABLED    "); }
                if(REMOTE_DISPLAY_ON == 1){ strcpy(lcd_row_two,"    HH:MM:SS    "); }
                if(REMOTE_DISPLAY_ON == 2){ strcpy(lcd_row_two,"    Hx:Mx:Sx    "); }                
                lcd_display_refresh = true;
            }
        }


        //////////////////////////////////////////
        // MISCELLANEOUS STUFF AND FINISHING UP //
        //////////////////////////////////////////

        // unlock / lock when the HAsh (#) button is pressed - its only function
        if( nec_ir_command == REMOTE_BTN_HA ){

            if( LOCK_STATE_FLAG == LOCKED   ){  LOCK_ACTION = UNLOCK;  }
            if( LOCK_STATE_FLAG == UNLOCKED ){  LOCK_ACTION = LOCK;    }
        } 

        // unlock if the frame is tilted - a method of escape if remote fails
        if( TILT_SWITCH == true && TILT_SWITCH_FLAG == false ){ 

            if( LOCK_STATE_FLAG == LOCKED ){

                LOCK_ACTION = UNLOCK;

                TILT_SWITCH_FLAG = true;
            }
        }

        // on every button press: zero tick counter, buzz if enabled
        if( nec_ir_command ){ 

            // zero, so we [return to timer screen] and [disable servo pwm timers]
            // some seconds after the last button press
            TICK_COUNTER_A = 0;

            // zero, to prevent auto power off, a button press is 'activity'
            TICK_COUNTER_B = 0;

            // buzz on button press, if buzzer option set to 'always'
            if( BUZZER_ON > 1 ){
                set_buzzer(50,true);
                set_buzzer(50,false);
            }
        } 

        // clear ir command - we're done with it now
        nec_ir_command = 0;


        // after 5 seconds: turn off servo pwm timers
        if( TICK_COUNTER_A == 5 ){

            set_servo_pwm_timers(false);
        }

        // after 5 seconds: clear flashing cursor
        if( lcd_row_two_cursor > 0 && TICK_COUNTER_A == 5 ){

            lcd_clear_cursors    = true;
            lcd_display_refresh  = true;

            TIMER_DIGIT_POSITION = 0;
        }


        ////////////////////////////////
        // SCAN THE (OPTIONAL) KEYPAD //
        ////////////////////////////////

        keypad_scan();

    }
}

Complete code listing for the Servo Lock Controller, compiled using XC8 V2.45

The above source code for the PIC18F46K22 complete with headers lcd_1602_74hc595.h, nec_ir_decode.h, dy_sv5w_mp3.h and eeprom_access.h can be downloaded here:-

tt_servo_lock_controller_mar_26.zip

This source code might be helpful to anyone working on an 8-bit PIC project requiring:

I've tried to write it in a well-commented and easy to follow way, without to much functionality hidden from view. Hopefully this makes it easy enough to understand, and allows chunks of it to be cut out and used in other projects. It's far nicer to look at in Visual Studio Code than on a webpage!

Precision servo control using a 16-bit timer on a PIC microcontroller

One of the technically interesting aspects of this project was configuring the microcontroller to provide precise, smooth and gradual control of the servos. This is achieved by preloading two of the 16-bit timers, then configuring two CCP (Counter Compare PWM) modules to clear their corresponding CCP pins when their CCPR registers matched specified timer values. Here is an explanation of the process using the CCP3 module configured to compare-match with the TMR3 timer to control just one servo:-

The 16-bit timer TMR3 counts up to 65,535 at which point an overflow occurs and the timer interrupt is triggered. The interrupt clears the CCP3CON register, sets the CCP3 (servo control signal) pin high, restores the register back to its previous configuration value, then preloads TMR3 with a value of 25,535.

The timer counting between 25,535 and 65,535 provides the 20ms period for the signal. The pulse controlling the servo position is formed by altering the CCPR3 register, which contains the value the CCP3 module uses to compare-match against the timer value, as it counts upwards.

When the timer matches the value in the CCPR3 register the CCP3 pin is cleared by the CCP3 module, as per its CCP3CON register configuration. Setting CCPR3 to 27,535 (25,535+2,000) clears the pin 2,000 increments after the timer overflowed and the pin was set high, resulting in a 1ms wide pulse. Setting CCPR3 to 29,535 (25,535+4,000) produces a 2ms wide control pulse.

System menu functions and configuration options