/*------------------------------------------------------------------------------------------*\
 *   
 *   Copyright (C) 2006 AVM GmbH <fritzbox_info@avm.de>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation version 2 of the License.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
\*------------------------------------------------------------------------------------------*/
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/skbuff.h>
#include <linux/netdevice.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <linux/init.h>
#include <linux/ioctl.h>
#include <asm/atomic.h>
#include <asm/mach_avm.h>

#if !defined(CONFIG_NETCHIP_ADM69961)
#define CONFIG_NETCHIP_ADM69961
#endif
#include <asm/mips-boards/prom.h>
#include <linux/avm_event.h>
#include <linux/avm_cpmac.h>
#include "cpmac_if.h"
#include "cpmac_const.h"
#include "cpmac_types.h"
#include "cpmac_debug.h"
#include "cpmac_eth.h"
#include "cpphy_const.h"
#include "cpphy_types.h"
#include "cpphy_mdio.h"
#include "adm6996.h"
#include "cpphy_adm6996.h"

/*------------------------------------------------------------------------------------------*\
 * Register cache
\*------------------------------------------------------------------------------------------*/
#define PRINT_REGISTER(x) DEB_TRC("[avm cpmac] Register 0x%x = 0x%x\n", x, ADM_GET_EEPROM_REG(mdio, (x)));
#define NUMBER_OF_SERIAL_REGISTERS (0x3c + 1)
/* alle 200ms Register aktualisieren */
#define MAX_SERIAL_REGISTER_AGE (HZ / 5)
struct adm_struct adm_serial_register;
static unsigned int *serial_register = (unsigned int *) &adm_serial_register;
static unsigned long serial_register_timestamp[NUMBER_OF_SERIAL_REGISTERS];
static int adm_serial_registers[] = {
    REG_ADM_LC_ID, REG_ADM_LC_STATUS0, REG_ADM_LC_STATUS2,
    REG_ADM_LC_RXPKTCNT0, REG_ADM_LC_RXPKTCNT1, REG_ADM_LC_RXPKTCNT2, REG_ADM_LC_RXPKTCNT3, REG_ADM_LC_RXPKTCNT4, REG_ADM_LC_RXPKTCNT5,
    REG_ADM_LC_RXPKTBYTECNT0, REG_ADM_LC_RXPKTBYTECNT1, REG_ADM_LC_RXPKTBYTECNT2, REG_ADM_LC_RXPKTBYTECNT3, REG_ADM_LC_RXPKTBYTECNT4, REG_ADM_LC_RXPKTBYTECNT5,
    REG_ADM_LC_TXPKTCNT0, REG_ADM_LC_TXPKTCNT1, REG_ADM_LC_TXPKTCNT2, REG_ADM_LC_TXPKTCNT3, REG_ADM_LC_TXPKTCNT4, REG_ADM_LC_TXPKTCNT5,
    REG_ADM_LC_TXPKTBYTECNT0, REG_ADM_LC_TXPKTBYTECNT1, REG_ADM_LC_TXPKTBYTECNT2, REG_ADM_LC_TXPKTBYTECNT3, REG_ADM_LC_TXPKTBYTECNT4, REG_ADM_LC_TXPKTBYTECNT5,
    REG_ADM_LC_COLLISIONCNT0, REG_ADM_LC_COLLISIONCNT1, REG_ADM_LC_COLLISIONCNT2, REG_ADM_LC_COLLISIONCNT3, REG_ADM_LC_COLLISIONCNT4, REG_ADM_LC_COLLISIONCNT5,
    REG_ADM_LC_ERRCNT0, REG_ADM_LC_ERRCNT1, REG_ADM_LC_ERRCNT2, REG_ADM_LC_ERRCNT3, REG_ADM_LC_ERRCNT4, REG_ADM_LC_ERRCNT5,
    REG_ADM_LC_OVERFLOWFLAG0, REG_ADM_LC_OVERFLOWFLAG2, REG_ADM_LC_OVERFLOWFLAG4,
    -1
};

static const unsigned int adm_port_registers[] = {
    REG_ADM_LC_PORT0_CONF, REG_ADM_LC_PORT1_CONF, REG_ADM_LC_PORT2_CONF,
    REG_ADM_LC_PORT3_CONF, REG_ADM_LC_PORT4_CONF, REG_ADM_LC_PORT5_CONF
};

static const unsigned int adm_vlan_port_map[] = {
    (1 << 0), (1 << 2), (1 << 4), (1 << 6), (1 << 7), (1 << 8)
};

#define AVM_CPMAC_7170 0
#define AVM_CPMAC_VDSL 1
#define AVM_CPMAC_PROFIVOIP2 2
#define AVM_CPMAC_PROFIVOIP1 3
#define AVM_CPMAC_MAX_PRODUCTS 4


cpphy_switch_configuration_t switch_configuration[AVM_CPMAC_MAX_PRODUCTS][CPMAC_MODE_MAX_NO] = {
    { /* 7170 */
        /* No legal mode        */ { 0, 0xff,
                                        { {"", 0x0}
                                        } 
                                   },
        /* CPMAC_MODE_NORMAL    */ { 1, 0xff,
                                        { {"eth0", 0x2f}  
                                        } 
                                   },
        /* CPMAC_MODE_ATA       */ { 2, 0,
                                        { {"eth0", 0x2e},
                                          {"wan",  0x21}   
                                        } 
                                   },
        /* CPMAC_MODE_SPLIT     */ { 4, 0xff,
                                        { {"eth0", 0x21},
                                          {"eth1", 0x22},
                                          {"eth2", 0x24},
                                          {"eth3", 0x28} 
                                        }
                                   },
        /* CPMAC_MODE_SPLIT_ATA */ { 4, 0,
                                        { {"eth1", 0x22},
                                          {"eth2", 0x24},
                                          {"eth3", 0x28},
                                          {"wan",  0x21} 
                                        }
                                   },
        /* CPMAC_MODE_ALL_PORTS */ { 1, 0xff,
                                        { {"eth0", 0x3f}  
                                        } 
                                   }
    }, { /* VDSL */
        /* No legal mode        */ { 0, 0xff,
                                        { {"", 0x0}
                                        } 
                                   },
        /* CPMAC_MODE_NORMAL    */ { 2, 4,
                                        { {"eth0", 0x2f},
                                          {"wan",  0x30}   
                                        } 
                                   },
        /* CPMAC_MODE_ATA       */ { 2, 0,
                                        { {"eth0", 0x2e},
                                          {"wan",  0x21}   
                                        } 
                                   },
        /* CPMAC_MODE_SPLIT     */ { 5, 4,
                                        { {"eth0", 0x21},
                                          {"eth1", 0x22},
                                          {"eth2", 0x24},
                                          {"eth3", 0x28},
                                          {"wan",  0x30}   
                                        }
                                   },
        /* CPMAC_MODE_SPLIT_ATA */ { 4, 0,
                                        { {"eth1", 0x22},
                                          {"eth2", 0x24},
                                          {"eth3", 0x28},
                                          {"wan",  0x21} 
                                        }
                                   },
        /* CPMAC_MODE_ALL_PORTS */ { 1, 0xff,
                                        { {"eth0", 0x3f}  
                                        } 
                                   }
    }, { /* ProfiVoIP 2 CPUs */
        /* No legal mode        */ { 0, 0xff,
                                        { {"", 0x0}
                                        } 
                                   },
        /* CPMAC_MODE_NORMAL    */ { 1, 0xff,
                                        { {"eth0", 0x3f}  
                                        } 
                                   },
        /* CPMAC_MODE_ATA       */ { 2, 0,
                                        { {"eth0", 0x3e},
                                          {"wan",  0x31}   
                                        } 
                                   },
        /* CPMAC_MODE_SPLIT     */ { 4, 0xff,
                                        { {"eth0", 0x31},
                                          {"eth1", 0x32},
                                          {"eth2", 0x34},
                                          {"eth3", 0x38} 
                                        }
                                   },
        /* CPMAC_MODE_SPLIT_ATA */ { 4, 0,
                                        { {"eth1", 0x32},
                                          {"eth2", 0x34},
                                          {"eth3", 0x38},
                                          {"wan",  0x31} 
                                        }
                                   },
        /* CPMAC_MODE_ALL_PORTS */ { 1, 0xff,
                                        { {"eth0", 0x3f}  
                                        } 
                                   }
    }, { /* ProfiVoIP 1 CPU */
        /* No legal mode        */ { 0, 0xff,
                                        { {"", 0x0}
                                        } 
                                   },
        /* CPMAC_MODE_NORMAL    */ { 1, 0xff,
                                        { {"eth0", 0x1f}  
                                        } 
                                   },
        /* CPMAC_MODE_ATA       */ { 2, 0,
                                        { {"eth0", 0x1e},
                                          {"wan",  0x11}   
                                        } 
                                   },
        /* CPMAC_MODE_SPLIT     */ { 4, 0xff,
                                        { {"eth0", 0x11},
                                          {"eth1", 0x12},
                                          {"eth2", 0x14},
                                          {"eth3", 0x18} 
                                        }
                                   },
        /* CPMAC_MODE_SPLIT_ATA */ { 4, 0,
                                        { {"eth1", 0x12},
                                          {"eth2", 0x14},
                                          {"eth3", 0x18},
                                          {"wan",  0x11} 
                                        }
                                   },
        /* CPMAC_MODE_ALL_PORTS */ { 1, 0xff,
                                        { {"eth0", 0x3f}  
                                        } 
                                   }
    }
};



/*------------------------------------------------------------------------------------------*\
 * initialize GPIO pins.  output mode, low 
\*------------------------------------------------------------------------------------------*/
static void adm_gpio_init(void) {
    /*--- set pins GPIO, OUTPUT, LOW ---*/
    avm_gpio_ctrl(GPIO_BIT_FSER_CLK, GPIO_PIN, GPIO_OUTPUT_PIN);
    avm_gpio_ctrl(GPIO_BIT_MII_DIO, GPIO_PIN, GPIO_OUTPUT_PIN);
    avm_gpio_ctrl(GPIO_BIT_MII_DCLK, GPIO_PIN, GPIO_OUTPUT_PIN);
    avm_gpio_set_bitmask(GPIO_MASK_MII_DCLK | GPIO_MASK_MII_DIO | GPIO_MASK_FSER_CLK, 0);
}

static void adm_gpio_finit(void) {
    avm_gpio_ctrl(GPIO_BIT_FSER_CLK, FUNCTION_PIN, GPIO_OUTPUT_PIN);
    avm_gpio_ctrl(GPIO_BIT_MII_DIO, FUNCTION_PIN, GPIO_OUTPUT_PIN);
    avm_gpio_ctrl(GPIO_BIT_MII_DCLK, FUNCTION_PIN, GPIO_OUTPUT_PIN);
}

/*------------------------------------------------------------------------------------------*\
 * read one bit from mdio port 
\*------------------------------------------------------------------------------------------*/
static int adm_mdio_readbit(void) {
    return avm_gpio_in_bit(GPIO_BIT_MII_DIO);
}

/*------------------------------------------------------------------------------------------*\
  MDIO mode selection
  0 -> output
  1 -> input
  switch input/output mode of GPIO 0
\*------------------------------------------------------------------------------------------*/
static void adm_mdio_mode(int mode) {
    avm_gpio_ctrl(GPIO_BIT_MII_DIO, GPIO_PIN, (GPIO_DIR)mode);
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
static void adm_mdc_hi(void) {
    avm_gpio_out_bit(GPIO_BIT_MII_DCLK, 1);
}

static void adm_mdio_hi(void) {
    avm_gpio_out_bit(GPIO_BIT_MII_DIO, 1);
}

static void adm_mdcs_hi(void) {
    avm_gpio_out_bit(GPIO_BIT_FSER_CLK, 1);
}

static void adm_mdc_lo(void) {
    avm_gpio_out_bit(GPIO_BIT_MII_DCLK, 0);
}

static void adm_mdio_lo(void) {
    avm_gpio_out_bit(GPIO_BIT_MII_DIO, 0);
}

static void adm_mdcs_lo(void) {
    avm_gpio_out_bit(GPIO_BIT_FSER_CLK, 0);
}

/*------------------------------------------------------------------------------------------*\
 * mdc pulse
 * 0 -> 1 -> 0
\*------------------------------------------------------------------------------------------*/
static void adm_mdc_pulse(void) {
    adm_mdc_lo();
    udelay(ADM_SW_MDC_DOWN_DELAY);
    adm_mdc_hi();
    udelay(ADM_SW_MDC_UP_DELAY);
    adm_mdc_lo();
}

/*------------------------------------------------------------------------------------------*\
 * mdc toggle
 * 1 -> 0
\*------------------------------------------------------------------------------------------*/
static void adm_mdc_toggle(void) {
    adm_mdc_hi();
    udelay(ADM_SW_MDC_UP_DELAY);
    adm_mdc_lo();
    udelay(ADM_SW_MDC_DOWN_DELAY);
}

/*------------------------------------------------------------------------------------------*\
 * enable eeprom write
 * For ATC 93C66 type EEPROM; accessing ADM6996 internal EEPROM type registers
\*------------------------------------------------------------------------------------------*/
static void adm_eeprom_write_enable(void) {
    unsigned int op;

    adm_mdcs_lo();
    adm_mdc_lo();
    adm_mdio_hi();
    udelay(ADM_SW_CS_DELAY);
    /* enable chip select */
    adm_mdcs_hi();
    udelay(ADM_SW_CS_DELAY);
    /* start bit */
    adm_mdio_hi();
    adm_mdc_pulse();

    /* eeprom write enable */
    op = ADM_SW_BIT_MASK_4;
    while(op) {
        if(op & ADM_SW_EEPROM_WRITE_ENABLE)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }

    op = ADM_SW_BIT_MASK_1 << (EEPROM_TYPE - 3);
    while(op) {
        adm_mdio_lo();
        adm_mdc_pulse();
        op >>= 1;
    }
    /* disable chip select */
    adm_mdcs_lo();
    udelay(ADM_SW_CS_DELAY);
    adm_mdc_pulse();
}

/*------------------------------------------------------------------------------------------*\
 * disable eeprom write
\*------------------------------------------------------------------------------------------*/
static void adm_eeprom_write_disable(void) {
    unsigned int op;

    adm_mdcs_lo();
    adm_mdc_lo();
    adm_mdio_hi();
    udelay(ADM_SW_CS_DELAY);
    /* enable chip select */
    adm_mdcs_hi();
    udelay(ADM_SW_CS_DELAY);

    /* start bit */
    adm_mdio_hi();
    adm_mdc_pulse();
    /* eeprom write disable */
    op = ADM_SW_BIT_MASK_4;
    while(op) {
        if(op & ADM_SW_EEPROM_WRITE_DISABLE)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }

    op = ADM_SW_BIT_MASK_1 << (EEPROM_TYPE - 3);
    while(op) {
        adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }
    /* disable chip select */
    adm_mdcs_lo();
    udelay(ADM_SW_CS_DELAY);
    adm_mdc_pulse();
}

/*------------------------------------------------------------------------------------------*\
  read registers from ADM6996
  32 Bit Mode:
    serial registers start at 0x200 (addr bit 9 = 1b)
    EEPROM registers -> shifted to 16bits; Serial registers -> 32bits
  Autodetect with addr == 0x0: (checks for EEPROM Signature)
\*------------------------------------------------------------------------------------------*/
static unsigned int adm_read_adml(unsigned int addr) {
    unsigned int op, dat;

    adm_gpio_init();

    adm_mdcs_hi();
    udelay(ADM_SW_CS_DELAY);

    adm_mdcs_lo();
    adm_mdc_lo();
    adm_mdio_lo();

    udelay(ADM_SW_CS_DELAY);

    /* preamble, 32 bit 1 */
    adm_mdio_hi();
    op = ADM_SW_BIT_MASK_32;
    while(op) {
        adm_mdc_pulse();
        op >>= 1;
    }

    /* command start (01b) */
    /*--- adm_write_bits_pulsed(ADM_SW_SMI_START, 2); ---*/
    op = ADM_SW_BIT_MASK_2;
    while(op) {
        if(op & ADM_SW_SMI_START)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }

    /* read command (10b) */
    /*--- adm_write_bits_pulsed(ADM_SW_SMI_READ, 2); ---*/
    op = ADM_SW_BIT_MASK_2;
    while(op) {
        if(op & ADM_SW_SMI_READ)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }

    /* send address A9 ~ A0 */
    /*--- adm_write_bits_pulsed(addr, 10); ---*/
    op = ADM_SW_BIT_MASK_10;
    while(op) {
        if(op & addr)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }

    /* set MDIO pin to input mode */
    adm_mdio_mode(ADM_SW_MDIO_INPUT);

    /* turnaround bits */
    op = ADM_SW_BIT_MASK_2;
    adm_mdio_hi();
    while(op) {
        adm_mdc_pulse();
        op >>= 1;
    }

    udelay(ADM_SW_MDC_DOWN_DELAY);

    /* start read data */
    dat = 0;
    if(!addr) {
        /* Autodetect: erstmal nur 16 Bit lesen, weil Mode unbekannt ist */
        op = ADM_SW_BIT_MASK_16;
    } else {
        op = ADM_SW_BIT_MASK_32;
    }
    while(op) {
        dat <<= 1;
        if(adm_mdio_readbit()) 
            dat |= 1;
        adm_mdc_toggle();
        op >>= 1;
    }
    if(!addr && (dat != 0x4154)) {
        /* Autodetect: im 32 Bit Mode die restlichen 16 Bit lesen */
        op = ADM_SW_BIT_MASK_16;
        while(op) {
            dat <<= 1;
            if(adm_mdio_readbit())
                dat |= 1;
            adm_mdc_toggle();
            op >>= 1;
        }
    }

    /* set MDIO to output mode */
    adm_mdio_mode(ADM_SW_MDIO_OUTPUT);

    /* dummy clock */
    op = ADM_SW_BIT_MASK_4;
    adm_mdio_lo();
    while(op) {
        adm_mdc_pulse();
        op >>= 1;
    }

    adm_mdc_lo();
    adm_mdio_lo();
    adm_mdcs_hi();

    /* EEPROM registers */
    if(!(addr & ADM_SERIAL_OFFSET))
    {
        if(addr & 1)
            dat >>= 16;
        else
        dat &= 0xffff;
    }
    return dat;
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
unsigned int adm_read_32Bit_cached(cpphy_mdio_t *mdio, unsigned int addr) {
    unsigned int address, data;

    /* Counter Register lesen */
    address = (addr - 0xa0) >> 1;
    if((jiffies - serial_register_timestamp[address]) > MAX_SERIAL_REGISTER_AGE) {
        /* Register-Cache ungueltig */
        serial_register_timestamp[address] = jiffies;
        if(mdio->Mode & CPPHY_SWITCH_MODE_16BIT) {
            data = cpphy_mdio_user_access_read(mdio, addr & 0x1f, addr >> 5) +
                  (cpphy_mdio_user_access_read(mdio, (addr + 1) & 0x1f, (addr + 1) >> 5) << 16);
        } else {
            data = adm_read_adml(address + ADM_DEVICE_ADDRESS + ADM_SERIAL_OFFSET);
        }
        serial_register[address] = data;
        return data;
    }
    return serial_register[address];
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
unsigned int adm_read(cpphy_mdio_t *mdio, unsigned int addr) {
    if(mdio->Mode & CPPHY_SWITCH_MODE_16BIT) {
        return cpphy_mdio_user_access_read(mdio, addr & 0x1f, addr >> 5);
    } else {
        return adm_read_adml(addr + ADM_DEVICE_ADDRESS);
    }
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
void adm_update_error_status(cpphy_mdio_t *mdio, unsigned int port) {
    switch(port) {
        case 0:
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_COLLISIONCNT0);
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ERRCNT0);
            break;
        case 1:
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_COLLISIONCNT1);
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ERRCNT1);
            break;
        case 2:
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_COLLISIONCNT2);
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ERRCNT2);
            break;
        case 3:
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_COLLISIONCNT3);
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ERRCNT3);
            break;
        case 4:
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_COLLISIONCNT4);
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ERRCNT4);
            break;
        case 5:
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_COLLISIONCNT5);
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ERRCNT5);
            break;
        default:
            DEB_ERR("adm_update_error_status, unhandled port %u\n", port);
            break;
    }
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
void adm_update_cache(cpphy_mdio_t *mdio) {
    unsigned int i = 0;

    for(i = 0; adm_serial_registers[i] != -1; i++) {
        ADM_GET_SERIAL_REG(mdio, adm_serial_registers[i]);
        PRINT_REGISTER(adm_serial_registers[i]);
    }
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
static int adm_write_adml(unsigned int addr, unsigned int dat) {
    unsigned int op;

    adm_gpio_init();

    /* enable write */
    adm_eeprom_write_enable();

    /* chip select */
    adm_mdcs_hi();
    udelay(ADM_SW_CS_DELAY);

    /* issue write command */
    /* start bit */
    adm_mdio_hi();
    adm_mdc_pulse();

    /* EEPROM write command */
    op = ADM_SW_BIT_MASK_2;
    while(op) {
        if(op & ADM_SW_EEPROM_WRITE)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_pulse();
        op >>= 1;
    }

    /* send address A7 ~ A0 */
    op = ADM_SW_BIT_MASK_1 << (EEPROM_TYPE - 1);

    while(op) {
        if(op & addr)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_toggle();
        op >>= 1;
    }

    /* start write data */
    op = ADM_SW_BIT_MASK_16;
    while(op) {
        if(op & dat)
            adm_mdio_hi();
        else
            adm_mdio_lo();

        adm_mdc_toggle();
        op >>= 1;
    }

    /* disable cs & wait 1 clock */
    adm_mdcs_lo();
    udelay(ADM_SW_CS_DELAY);
    adm_mdc_toggle();

    adm_eeprom_write_disable();

    return 0;
}


/*------------------------------------------------------------------------------------------*\
 * write register to ADM6996 eeprom registers
\*------------------------------------------------------------------------------------------*/
int adm_write(cpphy_mdio_t *mdio, unsigned int addr, unsigned int dat) {
    if(mdio->Mode & CPPHY_SWITCH_MODE_16BIT) {
        cpphy_mdio_user_access_write(mdio, addr & 0x1f, addr >> 5, dat);
    } else {
        adm_write_adml(addr + 0x80, dat);
    }
    return 0;
}


/*------------------------------------------------------------------------------------------*\
 * default VLAN setting
 * port 0~3 as untag port and PVID = 1
 * VLAN1: port 0~3 and port 5 (MII)
\*------------------------------------------------------------------------------------------*/
void adm_init(cpphy_mdio_t *mdio) {
    unsigned int i;
    char *hwrev, *cpu_nr;

    for(i = 0; i < NUMBER_OF_SERIAL_REGISTERS; i++) {
        serial_register[i] = 0;
        serial_register_timestamp[i] = 0;
    }

    mdio->Mode = 0 | CPPHY_SWITCH_MODE_WRITE | CPPHY_SWITCH_MODE_READ; /* 32 Bit, read, write */
    
    if(mdio->is_vdsl) {
        mdio->switch_config = &(switch_configuration[AVM_CPMAC_VDSL][0]);
    } else {
        mdio->switch_config = &(switch_configuration[AVM_CPMAC_7170][0]);
    }
    hwrev = prom_getenv("HWRevision");
    mdio->switch_status.cpu_port = 5;
    if(hwrev && strstr("104 ", hwrev)) {
        /* cpu_nr = prom_getenv("CPU_NR"); */
        char *cmdline = prom_getcmdline( );
        cpu_nr = strstr( cmdline, "CPU_NR=" );
        if(cpu_nr == NULL) {
            DEB_WARN("[avm cpmac] ProfiVoIP: running on first CPU\n");
            mdio->switch_config = switch_configuration[AVM_CPMAC_PROFIVOIP2];
            mdio->switch_status.cpu_port = 5;
        } else {
            DEB_WARN("[avm cpmac] ProfiVoIP: running on second CPU\n");
            mdio->Mode = 0; /* No access at all! */
            return;
        }
    }

    adm_gpio_init();

    if(ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_SIGNATURE) == 0x4154) {
        unsigned int reg;

#       ifdef undef
        DEB_ERR("[avm cpmac] adm_init, read test serial: 0x0: (%#x %#x), eeprom: 0x0: %#x\n",
            ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_ID),
            ADM_GET_EEPROM_REG(mdio, ADM_SERIAL_OFFSET),
            ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_SIGNATURE));
#       endif

        /* test if internal register is writeable */
        reg = ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_VLAN0_CONF);
        reg ^= 0x80; /* Port 4 VLAN Mapping */
        ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_VLAN0_CONF, reg);
        if(reg == ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_VLAN0_CONF)) {
            DEB_WARN("adm_init: adm6996 switch is in 32Bit Mode\n");
            reg ^= 0x80; /* Port 4 VLAN Mapping */
            ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_VLAN0_CONF, reg);
        } else {
            DEB_ERR("adm_init: adm6996 switch is in 32Bit Mode, NO WRITE ACCESS (%#x <-> %#x)\n",
                    reg, ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_VLAN0_CONF));
            mdio->Mode &= ~CPPHY_SWITCH_MODE_WRITE;
            return;
        }
    } else {
        adm_gpio_finit();
        mdio->Mode |= CPPHY_SWITCH_MODE_16BIT;
    }
};


/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
unsigned int adm_check_link(cpphy_mdio_t *mdio) {
    unsigned int status, linked;
   
    if(!(mdio->Mode & CPPHY_SWITCH_MODE_READ))
        return 1;

    status = ADM_GET_SERIAL_REG(mdio, REG_ADM_LC_STATUS0);
    linked = status & (  ADM_STATUS0_PORT0_LINKUP | ADM_STATUS0_PORT1_LINKUP 
                       | ADM_STATUS0_PORT2_LINKUP | ADM_STATUS0_PORT3_LINKUP 
                       | ADM_STATUS0_PORT4_LINKUP);
    
    return linked;
}


/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
static void adm_vlan_reset(cpphy_mdio_t *mdio) {
    unsigned int group, port;

    for(group = 0; group < AVM_CPMAC_MAX_VLAN_GROUPS; group++) {
        mdio->switch_status.vlan[group].written = 0;
        mdio->switch_status.vlan[group].vid = group + 16;
        mdio->switch_status.vlan[group].target_mask = 0x3F;
        mdio->cpmac_priv->map_vid_in[group] = group;
    }
    for(port = 0; port <= 5; port++) {
        mdio->switch_status.port[port].written = 0;
        mdio->switch_status.port[port].vid = port + 16;
        mdio->switch_status.port[port].keep_tag_outgoing = 0;
        mdio->cpmac_priv->map_port_out[port] = port + 16;
    }
};


/*------------------------------------------------------------------------------------------*\
 * Configure VLAN on the switch                                                             *
\*------------------------------------------------------------------------------------------*/
static void adm_new_vlan_config(cpphy_mdio_t *mdio) {
    unsigned int vlan_mapping, group, port, RegData;

    if(!(mdio->Mode & CPPHY_SWITCH_MODE_WRITE))
        return; /*--- CPMAC_ERR_NO_VLAN_POSSIBLE; ---*/

    for(port = 0; port < AVM_CPMAC_MAX_PORTS; port++) {
        DEB_TRC("[avm cpmac] Config port %u with VLAN group %u %stagged %s\n", 
                port,
                mdio->switch_status.port[port].vid,
                mdio->switch_status.port[port].keep_tag_outgoing ? "" : "un",
                mdio->switch_status.port[port].written ? "" : "(new)");

        if(mdio->switch_status.port[port].written)
            continue;
        mdio->switch_status.port[port].written = 1;
        RegData = ADM_GET_EEPROM_REG(mdio, adm_port_registers[port]);
        RegData &= ~(ADM_CONF_OUTPUT_PKT_TAG | ADM_CONF_PORT_VLAN_ID);
        if(mdio->switch_status.port[port].keep_tag_outgoing)
            RegData |= ADM_SW_PORT_TAG;
        RegData |= (mdio->switch_status.port[port].vid & 0xf) << ADM_SW_PORT_PVID_SHIFT;
        ADM_PUT_EEPROM_REG(mdio, adm_port_registers[port], RegData);
        if(port == 5) {
            RegData  = ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_PORT5_PVID) & 0xff00;
            RegData |= (mdio->switch_status.port[port].vid & 0xff0) >> 4;
            ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_PORT5_PVID, RegData);
        } else if(port == 4) {
            RegData  = ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_PORT34_PVID) & 0x00ff;
            RegData |= (mdio->switch_status.port[port].vid & 0xff0) << 4;
            ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_PORT34_PVID, RegData);
        } else {
            RegData  = ADM_GET_EEPROM_REG(mdio, REG_ADM_LC_PORT0_PVID + port) & 0xff00;
            RegData |= (mdio->switch_status.port[port].vid & 0xff0) >> 4;
            ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_PORT0_PVID + port, RegData);
        }
    }

    for(group = 0; group < AVM_CPMAC_MAX_VLAN_GROUPS; group++) {
        DEB_TRC("[avm cpmac] Config VLAN group %u: target 0x%x %s\n", 
                mdio->switch_status.vlan[group].vid,
                mdio->switch_status.vlan[group].target_mask,
                mdio->switch_status.vlan[group].written ? "" : "(new)");
              
        if(mdio->switch_status.vlan[group].written)
            continue;
        mdio->switch_status.vlan[group].written = 1;

        vlan_mapping  = mdio->switch_status.vlan[group].target_mask;
        vlan_mapping |= (mdio->switch_status.vlan[group].vid & 0xf) << 12;
        ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_VLAN_FILTER0_LOW + 2 * (mdio->switch_status.vlan[group].vid & 0xf), vlan_mapping);
        vlan_mapping = 0x8000 | mdio->switch_status.vlan[group].vid;
        ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_VLAN_FILTER0_HIGH + 2 * (mdio->switch_status.vlan[group].vid & 0xf), vlan_mapping);
    }
}


/*------------------------------------------------------------------------------------------*\
 * Reset the VLAN settings of the switch                                                    *
\*------------------------------------------------------------------------------------------*/
void adm_vlan_fbox_reset(cpphy_mdio_t *mdio) {
    /* switch off VLAN */
    adm_vlan_reset(mdio);
    adm_new_vlan_config(mdio);
}


/* Find unused VLAN ID group */
#define find_unused_vlan() \
    for(vid_group = 0; vid_group < AVM_CPMAC_MAX_VLAN_GROUPS; vid_group++) { \
        if(!(used_vid_mask & (1 << vid_group))) { \
            break; \
        } \
    } \
    if(vid_group == AVM_CPMAC_MAX_VLAN_GROUPS) { \
        DEB_ERR("[avm cpmac] Error! I need more VLAN groups!\n"); \
        return CPMAC_ERR_NO_VLAN_POSSIBLE; \
    } \
    used_vid_mask |= 1 << vid_group;



struct net_device *find_or_register_device(char *name,
                                           cpmac_priv_t *cpmac_priv,
                                           unsigned short VID) {
    struct net_device *dev;
    int i;

    for(i = 0; i < AVM_CPMAC_MAX_DEVS; i++) {
        dev = cpmac_priv->olddevs[i];
        if(dev && strcmp(dev->name, name) == 0) {
            DEB_TRC("[avm cpmac] Config: Update '%s' with VID %u\n", name, VID);
            cpmac_update_device(dev, VID);
            cpmac_priv->olddevs[i] = 0;
            return dev;
        }
    }
    if(strcmp(name, "wan") != 0) {
        DEB_TRC("[avm cpmac] Config: Connect '%s' with VID %u\n",
                        name, VID);
        return cpmac_register_eth_device(name, cpmac_priv->owner, VID);
    }
    DEB_TRC("[avm cpmac] Config: Connect '%s' with VID %u (WAN)\n",
                     name, VID);
    return cpmac_register_wan_device(name, cpmac_priv->owner, VID);
} 

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
cpmac_err_t adm_configure_cpmac(cpphy_mdio_t *mdio, struct avm_cpmac_config_struct *config) {
    unsigned short used_vid_mask = 0;
    unsigned char port, device, vid_group, used_ports = 0;
    unsigned int i;
    struct avm_new_switch_struct *status = &(mdio->switch_status);

    /* Check plausibility */
    if(config->cpmac_mode >= CPMAC_MODE_MAX_NO) {
        DEB_ERR("[avm cpmac] Unknown cpmac_configuration %u\n", config->cpmac_mode);
        return CPMAC_ERR_NOERR;
    }

    tasklet_disable(&mdio->cpmac_priv->tasklet);

    for(i = 0; i < AVM_CPMAC_MAX_DEVS; i++) {
        mdio->cpmac_priv->olddevs[i] = mdio->cpmac_priv->devs[i];
        mdio->cpmac_priv->devs[i] = 0;
    }

    DEB_INFO("[avm cpmac] Config cpmac to mode %u with %u devices:\n", 
            config->cpmac_mode, mdio->switch_config[config->cpmac_mode].number_of_devices);
    for(i = 0; i < mdio->switch_config[config->cpmac_mode].number_of_devices; i++) {
        DEB_INFO("[avm cpmac] Config cpmac device '%s' with target_mask 0x%x\n",
                mdio->switch_config[config->cpmac_mode].device[i].name,
                mdio->switch_config[config->cpmac_mode].device[i].target_mask);
    }

    memcpy(&mdio->cpmac_config, config, sizeof(struct avm_cpmac_config_struct));

    adm_vlan_reset(mdio);
    
    /* Reset VID->Port mapping */
    for(vid_group = 0; vid_group < AVM_CPMAC_MAX_VLAN_GROUPS; vid_group++) {
        mdio->cpmac_priv->map_port_in[vid_group] = status->cpu_port;
        mdio->cpmac_priv->map_devoffset_in[vid_group] = 0;
    }

    /* Do we have predefined VLAN IDs? */
    if(config->wan_vlan_number) {
        for(device = 0; device < mdio->switch_config[config->cpmac_mode].number_of_devices; device++) {
            if(strncmp(mdio->switch_config[config->cpmac_mode].device[device].name, "wan", 3) == 0) {
                for(i = 0; i < config->wan_vlan_number; i++) {
                    unsigned short vid = config->wan_vlan_id[i];

                    used_vid_mask |= 1 << (vid % 0xf);

                    /* Make all ports that belong to the same VLAN group receive the data of the port */
                    for(port = 0; port < AVM_CPMAC_MAX_PORTS; port++) {
                        if(mdio->switch_config[config->cpmac_mode].device[device].target_mask & (1 << port)) {
                            status->vlan[vid & 0xf].vid = vid;
                            status->vlan[vid & 0xf].target_mask = mdio->switch_config[config->cpmac_mode].device[device].target_mask;
                            status->vlan[vid & 0xf].written = 0;
                            if(port != status->cpu_port)
                                mdio->cpmac_priv->map_port_in[vid & 0xf] = port;
                        }
                    }
                }
            }
        }
    }

    DEB_TRC("[avm cpmac] Config: used_vid_mask = 0x%x\n", used_vid_mask); /* FIXME */

    /* Determine which ports are used and therefor need a VLAN ID */
    for(device = 0; device < mdio->switch_config[config->cpmac_mode].number_of_devices; device++) {
        used_ports |= mdio->switch_config[config->cpmac_mode].device[device].target_mask;
    }
    DEB_TRC("[avm cpmac] Config: used_ports = 0x%x\n", used_ports); /* FIXME */

    /* Tag incoming data with individual tags and deliver it to all interested ports */
    for(port = 0; port < AVM_CPMAC_MAX_PORTS; port++) {
        /* Do not tag incoming data from the CPU port */
        if(port == status->cpu_port)
            continue;

        /* If port is unused, it needs no VLAN tag */
        if(!(used_ports & (1 << port))) {
            continue;
        }

        /* Find unused VLAN ID group */
        find_unused_vlan();
        status->port[port].vid = 16 + vid_group;
        status->port[port].written = 0;
        mdio->cpmac_priv->map_port_in[vid_group] = port;
        DEB_TRC("[avm cpmac] Config: Setting VLAN tagging on port %u with tag %u\n", port, 16 + vid_group);
        /* Make all ports that belong to the same VLAN group receive the data of the port */
        for(device = 0; device < mdio->switch_config[config->cpmac_mode].number_of_devices; device++) {
            if(mdio->switch_config[config->cpmac_mode].device[device].target_mask & (1 << port)) {
                status->vlan[vid_group].vid = 16 + vid_group;
                status->vlan[vid_group].target_mask = mdio->switch_config[config->cpmac_mode].device[device].target_mask;
                status->vlan[vid_group].written = 0;
            }
        }

        /* Create VLANs for port specific outgoing traffic */
        find_unused_vlan();
        status->vlan[vid_group].vid = 16 + vid_group;
        status->vlan[vid_group].target_mask = (1 << status->cpu_port) | (1 << port);
        status->vlan[vid_group].written = 0;
        mdio->cpmac_priv->map_port_out[port] = status->vlan[vid_group].vid;
        DEB_TRC("[avm cpmac] Config: Setting outgoing VLAN ID for port %u to %u\n", port, 16 + vid_group);
    }
    status->port[status->cpu_port].keep_tag_outgoing = 1;
    status->port[status->cpu_port].written = 0;

    /* Map the ports for one device to one common VID on the system side */
    for(port = 0; port < AVM_CPMAC_MAX_PORTS; port++) {
        unsigned short mask = status->vlan[status->port[port].vid & 0xf].target_mask;

        /* Skip unused ports */
        if(!((1 << port) & used_ports))
            continue;
        for(i = 0; i < AVM_CPMAC_MAX_PORTS; i++) {
            if(mask & (1 << i)) { /* First port in this group is used as reference */
                mdio->cpmac_priv->map_vid_in[status->port[port].vid & 0xf] = status->port[i].vid;
                DEB_TRC("[avm cpmac] Config: Map incoming VID %u on port %u to VID %u\n",
                        status->port[port].vid, port, mdio->cpmac_priv->map_vid_in[status->port[port].vid & 0xf]);
                break;
            }
        }
    }

    mdio->cpmac_priv->wanport = mdio->switch_config[config->cpmac_mode].wanport;
    mdio->cpmac_priv->wanport_keeptags = 0;
    if(config->wan_vlan_number) {
        status->port[mdio->cpmac_priv->wanport].keep_tag_outgoing = 1;
        mdio->cpmac_priv->wanport_keeptags = 1;
        DEB_TRC("[avm cpmac] Config: Keep tags on wanport\n"); 
    }
#   ifdef CONFIG_IP_MULTICAST_FASTFORWARD
    for(vid_group = 0; vid_group < AVM_CPMAC_MAX_VLAN_GROUPS; vid_group++) {
        mcfw_portset *setp = &mdio->cpmac_priv->map_vid_portset[vid_group];
        mcfw_portset_reset(setp);
        for(port = 0; port < 4; port++) {
            if(port == mdio->cpmac_priv->wanport)
                continue;
            if((1 << port) & status->vlan[vid_group].target_mask)
                mcfw_portset_port_add(setp, port);
        }
    }
#   endif
    
    /* Now register the devices with the corresponding VIDs */
    for(device = 0; device < mdio->switch_config[config->cpmac_mode].number_of_devices; device++) {
        for(vid_group = 0; vid_group < AVM_CPMAC_MAX_VLAN_GROUPS; vid_group++) {
            if(mdio->switch_config[config->cpmac_mode].device[device].target_mask == status->vlan[vid_group].target_mask) {
                for(port = 0; port < AVM_CPMAC_MAX_PORTS; port++) {
                    if(port == status->cpu_port)
                        continue;
                    if((1 << port) & status->vlan[vid_group].target_mask)
                        mdio->cpmac_priv->map_devoffset_in[vid_group] = device;
                }
                if(mdio->cpmac_priv->devs[device])
                    continue;

                mdio->cpmac_priv->devs[device] = find_or_register_device(
                        mdio->switch_config[config->cpmac_mode].device[device].name,
                        mdio->cpmac_priv,
                        status->vlan[vid_group].vid);
            }
        }
    }
    for(i = 0; i < AVM_CPMAC_MAX_DEVS; i++) {
        struct net_device *dev = mdio->cpmac_priv->olddevs[i];
        if(dev) {
            DEB_INFO("[avm cpmac] Unregister %s\n", dev->name);
            mdio->cpmac_priv->olddevs[i] = 0;
            cpmac_unregister_device(dev);
        }
    }
    DEB_TRC("[avm cpmac] Possible unregister done\n");

    for(vid_group = 0; vid_group < AVM_CPMAC_MAX_VLAN_GROUPS; vid_group++) {
        int devoffset = mdio->cpmac_priv->map_devoffset_in[vid_group];
        struct net_device *dev = mdio->cpmac_priv->devs[devoffset];
        DEB_TRC("[avm cpmac] Config: VID group %u mapped to port %u attached to %s\n",
                vid_group,
                mdio->cpmac_priv->map_port_in[vid_group],
                dev ? dev->name : "????");
    }

    adm_new_vlan_config(mdio);
    /*--- { ---*/
        /*--- unsigned int i; ---*/
        /*--- for(i = 0x01; i < 0x01 + 9; i++) PRINT_REGISTER(i); ---*/
        /*--- for(i = 0x13; i < 0x13 + 16; i++) PRINT_REGISTER(i); ---*/
        /*--- for(i = 0x28; i <= 0x2c; i++) PRINT_REGISTER(i); ---*/
        /*--- for(i = 0x40; i < 0x40 + 32; i++) PRINT_REGISTER(i); ---*/
    /*--- } ---*/

    /* MAC clone, 802.1q based VLAN */
    ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_VLAN_MODE, 0xff30);
    /* MAC clone, 802.1q based VLAN, address aging timer 1 second */
    /*--- ADM_PUT_EEPROM_REG(mdio, REG_ADM_LC_VLAN_MODE, 0xff33); ---*/
    mdio->cpmac_priv->enable_vlan = 1;

    tasklet_enable(&mdio->cpmac_priv->tasklet);

    return CPMAC_ERR_NOERR;
}

/*------------------------------------------------------------------------------------------*\
\*------------------------------------------------------------------------------------------*/
cpmac_err_t adm_get_config_cpmac(cpphy_mdio_t *mdio, struct avm_cpmac_config_struct *config) {
    memcpy(config, &mdio->cpmac_config, sizeof(struct avm_cpmac_config_struct));
    return CPMAC_ERR_NOERR;
}

