/*
 * File:   power_switch.c
 * Author: etrauschke
 *
 * Created on November 3, 2017, 12:40 PM
 */

/*
 * MVC Power Switch firmware
 * 
 * This PIC firmware (FW) has support for 8 outputs which can be controlled by 
 * I2C commands. It also supports a "sleep" mode in which the FW turns off the
 * outputs for a given period of time and turns them back on after the time has
 * expired.
 * 
 * Whenever a new configuration of the outputs has been programmed over I2C it
 * will get stored in the internal ROM of the PIC so when the PIC gets turned
 * off and back on it will restore the last output configuration.
 * 
 * The I2C interface works as follows. The first byte which needs to be sent is
 * the register number. Then follow 2 or 4 bytes with the value to be written.
 * These bytes are ordered in big-endian (that way they are easier to read in
 * the client code or on a logic analyzer).
 * 
 * The following commands are supported:
 * 
 * Register | Function                      | Parameter size
 * ---------+-------------------------------+----------------------------
 *   0      |  set output bits to 'x'       | 2 bytes (1 bit per channel) 
 *   1      |  suspend outputs for 'x' sec. | 4 bytes (32 bit integer)
 * 
 * Example:
 *  - Turning on outputs 0,1,7:   0x00 0x00 0x83
 *  - Sleep for 3600 secs:        0x01 0x00 0x00 0x0E 0x10 
 * 
 * For values which get written to the config registers (currently that's just
 * reg 0) the current value can be read back by sending (writing) just the
 * register value and then invoking a I2C read. The last accessed register gets
 * stored in RAM so for reading back a register which just got written, the
 * value can be read back immediately. The first byte returned will indicate the
 * register which got read and the following bytes represent the value. To
 * read a 16 bit value 3 bytes need to be read by the client. The value is sent
 * back in big-endian byte order.
 * 
 * Note for sleep mode:
 * When the device is in sleep or suspend mode, sending another command will
 * overwrite the previously requested sleep cycle. If an update to the outputs
 * is requested (reg 0), the device will enable these outputs right away and not
 * go back into suspend. If a new sleep period is requested (reg 1) the outputs
 * will get suspended from the time it received the last command for the period
 * which has been requested. This allows to unsuspend the outputs by requesting
 * a sleep for 0 secs.
 * 
 * Note on power:
 * The device is permanently in sleep mode unless it either receives an
 * interrupt from the timer during the period when the outputs are suspended or
 * from receiving I2C commands which get processed immediately after reception.
 * 
 */

#include <xc.h>
#include <pic16lf1828.h>

// CONFIG
#pragma config FOSC=INTOSC, PLLEN=OFF, MCLRE = ON, WDTE = OFF
#pragma config CPD = OFF, BOREN = OFF, IESO = OFF, FCMEN = OFF, LVP=OFF
#define _XTAL_FREQ      500000

// definitions for I2C and cfg registers
#define I2C_REG_SWITCHES    0
#define I2C_REG_SLEEP       1

#define I2C_REG_COUNT       2 // number of registers

// Ports for outputs
#define SWITCH0     PORTCbits.RC5
#define SWITCH1     PORTCbits.RC4
#define SWITCH2     PORTCbits.RC3
#define SWITCH3     PORTCbits.RC6
#define SWITCH4     PORTCbits.RC7
#define SWITCH5     PORTBbits.RB7
#define SWITCH6     PORTCbits.RC2
#define SWITCH7     PORTBbits.RB5

void i2c_slave_init(unsigned char address);
unsigned char read_prom(unsigned char address);
void write_prom(unsigned char address, unsigned char value);
void set_outputs(unsigned char bits);
void set_register();
unsigned char read_cfg_reg(unsigned char byte);
void gosleep(unsigned long seconds);

short z;
// buffer for incoming I2C commands, it's 32bit so we can easily shift bytes
// around to assemble 16 and 32 bit values
unsigned long i2c_buf[8];
unsigned char i2c_reg;
unsigned char c;
unsigned short cfg_regs[I2C_REG_COUNT];
unsigned short t1_val;
unsigned long t1_intervals;


void interrupt isr()
{
    // timer interrupt
    if (TMR1IF == 1) {
        TMR1IF = 0;

        if (t1_intervals == 0) {
            // done
            set_outputs(cfg_regs[I2C_REG_SWITCHES] & 0xff);
            TMR1ON = 0;
        } else if (t1_intervals == 1) {
            TMR1L = t1_val & 0xff;
            TMR1H = (t1_val >> 8) & 0xff;        
        }
        t1_intervals--;
    }
  
    // I2c interrupt (triggered for every byte received/sent including address)
    if(SSP1IF == 1)
    {
        // clock stretch until we're done processing
        SSP1CONbits.CKP = 0;
        SSP1IF = 0;
        
        //byte is data
        if (SSP1STATbits.D_nA) {
            // write to pic
            if (!SSP1STATbits.R_nW) {
                // this is the first data byte => register number
                if (i2c_reg == 0xff) {
                    i2c_reg = SSP1BUF;
                    
                // register data
                } else {
                    i2c_buf[c++] = SSP1BUF;
                    if (c == 2 && i2c_reg == I2C_REG_SWITCHES) {
                        set_register();
                        c = 0;
                    } else if (c == 4 && i2c_reg == I2C_REG_SLEEP) {
                        gosleep((i2c_buf[0] << 24) | 
                                (i2c_buf[1] << 16) |
                                (i2c_buf[2] << 8) | 
                                i2c_buf[3]);
                        c = 0;
                    }
                }
            // read from pic
            } else {
                // read value from cfg_regs and send to I2C master,
                // but at most 2 bytes
                if (i2c_reg == 0xff || c > 1) {
                    SSP1BUF = 0;
                } else {
                    SSP1BUF = read_cfg_reg(c++);
                }
            }
        
        // byte is address
        } else {
            // address byte indicates new connection, clean out old status
            c = 0;
            BF = 0;
            // clear address out of buffer
            z = SSP1BUF;

            if (!SSP1STATbits.R_nW) {
                // for writes we expect a new register being sent as next byte
                i2c_reg = 0xff;
            } else {
                // for reads we return the current register number
                SSP1BUF = i2c_reg;
            }
            
        }
        // release clock
        SSP1CONbits.CKP = 1;
    }
}

/* 
 * Endless program loop. Up here for less scrolling during debugging
 */
void loop() {
    while (1) {
        SLEEP();
    };
}

/*
 * Read value from config register (in RAM). The 'byte' parameter specifies
 * which byte out of the a multi-byte value needs to be read. Byte order is
 * big-endian.
 */
unsigned char read_cfg_reg(unsigned char byte) {

    unsigned short val = 0;
    
    if (i2c_reg == I2C_REG_SWITCHES) {
        val = cfg_regs[I2C_REG_SWITCHES];
        return *((unsigned char *)&val + 1 - byte);    
    }
    return val;

}

/*
 * Write config registers to RAM and ROM when we receive them by I2C.
 * Also triggers additional actions based on I2C input, like sleep mode. 
 */
void set_register() {
    // we received a new cmd, disable timer if it's running
    TMR1ON = 0;
    
    switch (i2c_reg) {
    case I2C_REG_SLEEP:
        gosleep((i2c_buf[0] << 8) | i2c_buf[1]);
        return;

    case I2C_REG_SWITCHES:
        cfg_regs[i2c_reg] = ((i2c_buf[0] << 8) | i2c_buf[1]);
        write_prom(I2C_REG_SWITCHES, cfg_regs[I2C_REG_SWITCHES] & 0xff);
        set_outputs(cfg_regs[I2C_REG_SWITCHES] & 0xff);
        return;

    default:
        return;
    }
}

/*
 * Turn the outputs off and program timer to wake up after delay specified by
 * 'seconds'.
 * Note: function name is kind of a misnomer since the device is always sleeping
 * unless it receives interrupts from I2c or timer 1.
 */
void gosleep(unsigned long seconds) {
    
    unsigned long rem;
    
    if (seconds == 0) {
        set_outputs(cfg_regs[I2C_REG_SWITCHES] & 0xff);
        return;
    }
    
    // We have 16s intervals (max with 1:8 prescaler on 32.768kHz crystal).
    // Calculate number of required full intervals.
    t1_intervals = seconds >> 4;
    // calculate remaining seconds
    rem = (seconds - (t1_intervals << 4));
    // get counter value: 0x65536 - (rem / 16 * 65536)
    t1_val = 0 - (rem << 12);
    
    if (t1_intervals == 0) {
        TMR1L = t1_val & 0xff;
        TMR1H = (t1_val >> 8) & 0xff;
    } else {
        TMR1L = 0;
        TMR1H = 0;
    }
    set_outputs(0);
    TMR1ON = 1;
}

/*
 * Initialize I2C hardware. Set device address to 'address'.
 */
void i2c_slave_init(unsigned char address)
{
    SSP1STAT = 0x80;
    SSP1ADD = address;
    SSP1CON = 0x36;
    SSP1CON2 = 0x01;
    GIE = 1;
    PEIE = 1;
    SSP1IF = 0;
    SSP1IE = 1;
}

/*
 * Initialize hardware for timer 1.
 */
void timer1_init() {
    // external crystal
    // prescaler 1:8
    // no sync to internal osc
    T1CKPS0 = 1;
    T1CKPS1 = 1;
    TMR1CS1 = 1;
    TMR1CS0 = 0;
    nT1SYNC = 1;
    T1OSCEN = 1;
    __delay_ms(10);

    // timer is off until started by command
    TMR1ON = 0;
    TMR1GE = 0;
    
    // enable timer1 interrupts
    TMR1IF = 0;
    TMR1IE = 1;
}

/*
 * Read byte from internal ROM from address 'address'. 
 */
unsigned char read_prom(unsigned char address)
{
    EEADRL = address;
    EECON1bits.CFGS = 0;
    EECON1bits.EEPGD = 0;
    EECON1bits.RD = 1;
    return EEDATL;
}

/*
 * Write single byte with value 'value' to ROM address 'address'.
 */
void write_prom(unsigned char address, unsigned char value)
{
    EEADRL = address;
    EEDATL = value;
    EECON1bits.CFGS = 0;
    EECON1bits.EEPGD = 0;
    EECON1bits.WREN = 1;
    
    GIE = 0;
    
    EECON2 = 0x55;
    EECON2 = 0xaa;
    EECON1bits.WR = 1;
    
    GIE = 1;
    EECON1bits.WREN = 0;
    while (EECON1bits.WR) {
        
    }
}

/*
 * Set outputs. There are 8 outputs which correspond to the bits given in
 * the 'bits' parameter.
 */
void set_outputs(unsigned char bits) {

    if (bits & 0x01) {
        SWITCH0 = 1;
    } else {
        SWITCH0 = 0;        
    }
    if (bits & 0x02) {
        SWITCH1 = 1;
    } else {
        SWITCH1 = 0;        
    }
    if (bits & 0x04) {
        SWITCH2 = 1;
    } else {
        SWITCH2 = 0;        
    }
    if (bits & 0x08) {
        SWITCH3 = 1;
    } else {
        SWITCH3 = 0;        
    }
    if (bits & 0x10) {
        SWITCH4 = 1;
    } else {
        SWITCH4 = 0;        
    }
    if (bits & 0x20) {
        SWITCH5 = 1;
    } else {
        SWITCH5 = 0;        
    }
    if (bits & 0x40) {
        SWITCH6 = 1;
    } else {
        SWITCH6 = 0;        
    }
    if (bits & 0x80) {
        SWITCH7 = 1;
    } else {
        SWITCH7 = 0;        
    }
}

void main()
{
  WPUC = 0;            //Enables PORTB internal pull up resistors
  TRISB = 0x50;         //I2C on PORTB as input
  TRISC = 0x00;
  ANSELB = 0x00;
  ANSELC = 0x00;
  
  cfg_regs[I2C_REG_SWITCHES] = read_prom(I2C_REG_SWITCHES);
  set_outputs(cfg_regs[I2C_REG_SWITCHES]);

  i2c_slave_init(0x30); //Initialize as a I2C Slave with address 0x30
  timer1_init();
  loop();
}