//==========================================================================
//
//      sam9_powersave.c
//
//      HAL power saving support code for ARM9/SAM9
//
//==========================================================================
// ####ECOSGPLCOPYRIGHTBEGIN####                                            
// -------------------------------------------                              
// This file is part of eCos, the Embedded Configurable Operating System.   
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 Free Software Foundation, Inc.
// Copyright (C) 2003, 2004, 2005, 2006, 2007 eCosCentric Limited           
//
// eCos 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; either version 2 or (at your option) any later      
// version.                                                                 
//
// eCos 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 eCos; if not, write to the Free Software Foundation, Inc.,    
// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.            
//
// As a special exception, if other files instantiate templates or use      
// macros or inline functions from this file, or you compile this file      
// and link it with other works to produce a work based on this file,       
// this file does not by itself cause the resulting work to be covered by   
// the GNU General Public License. However the source code for this file    
// must still be made available in accordance with section (3) of the GNU   
// General Public License v2.                                               
//
// This exception does not invalidate any other reasons why a work based    
// on this file might be covered by the GNU General Public License.         
// -------------------------------------------                              
// ####ECOSGPLCOPYRIGHTEND####                                              
//==========================================================================
//#####DESCRIPTIONBEGIN####
//
// Author(s):    eCosCentric
// Contributors: 
// Date:         2007-09-19
// Purpose:      HAL board power saving support
// Description:  Implementations of HAL power saving.
//
//####DESCRIPTIONEND####
//
//========================================================================*/

#include <pkgconf/hal.h>
#include <pkgconf/system.h>
#include CYGBLD_HAL_PLATFORM_H

#include <cyg/infra/cyg_type.h>         // base types
#include <cyg/infra/cyg_trac.h>         // tracing macros
#include <cyg/infra/cyg_ass.h>          // assertion macros

#include <cyg/hal/hal_io.h>             // IO macros
#include <cyg/hal/hal_arch.h>           // Register state info
#include <cyg/hal/hal_diag.h>
#include <cyg/hal/hal_intr.h>           // Interrupt names
#include <cyg/hal/hal_cache.h>
#include <cyg/hal/sam9.h>         // Platform specifics

#include <cyg/infra/diag.h>             // diag_printf

//--------------------------------------------------------------------------
// Debug tracing. This is only useful if the clocks have not been
// changed since the serial line baud rate depends on the system
// clock.

#define DEBUG 0

#if DEBUG
#define ps_diag( __fmt, ... ) diag_printf("PS: %s[%d]: " __fmt, __FUNCTION__, __LINE__, __VA_ARGS__ );
#else
#define ps_diag( __fmt, ... ) 
#endif

//--------------------------------------------------------------------------
// In-memory tracing. With the clocks turned down, it is not possible
// to use diag_printf(), so the only way to debug this code is to use
// an in-memory trace buffer. The macros to do this have been left in
// place to help if further debugging is needed in the future. 

#if 0

#define         XSIZE 256       // Must be power of 2
cyg_uint32      X[XSIZE];
cyg_uint32      xpos = 0;

#define XX(x)                                   \
{                                               \
    X[xpos++ & (XSIZE-1)] = x;                  \
    X[xpos & (XSIZE-1)] = 0xFFFFFFFF;           \
}

#else

#define XX(xx)

#endif

//--------------------------------------------------------------------------
// SRAM base definition. Get virtual address of on-chip SRAM in each
// of the sam9 platforms by looking up the virtual address
// corresponding to the SRAM physical address, which we know for
// all SAM9.

#define SRAM_VIRT_BASE CYGARC_VIRTUAL_ADDRESS(0x200000)

//--------------------------------------------------------------------------
// Set up clock parameters

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)
# ifndef SAM9_PLF_PLLA_POWERSAVE_80MHZ
// This really needs to be set to correspond to 80MHz - the lowest
// value of PLLA on SAM9. But we cop out by setting a default
// on the basis of an 18.432MHz input clock. Clearly this
// should generally be adjusted per platform by the platform
// itself setting SAM9_PLF_PLLA_POWERSAVE_80MHZ.
//
// 80MHz ~= 18.432MHz/8 * 35
// DIVA      = 8
// PLLACOUNT = 63
// OUTA      = 0 == 80..160MHz
// MULA      = 34 == multiplier-1

#  define SAM9_PLF_PLLA_POWERSAVE_80MHZ 0x20223F08
# endif
#endif

//--------------------------------------------------------------------------
// Ethernet Receive Buffer Descriptor, borrowed from ethernet driver
// headers.

typedef struct bd {
    unsigned long address;    
    unsigned long status;
} bd_t;

// Address bits
#define BD_WRAP    0x00000002
#define BD_BUSY    0x00000001

//--------------------------------------------------------------------------
// Saved power management controller state.

static cyg_uint32 powersave_sysclk;     // Saved system clock status register
static cyg_uint32 powersave_pclock;     // Saved peripheral clock register
static cyg_uint32 powersave_mosc;       // Saved main oscillator register
static cyg_uint32 powersave_plla;       // Saved PLLA register
static cyg_uint32 powersave_pllb;       // Saved PLLB register
static cyg_uint32 powersave_mck;        // Saved MCK register
static cyg_uint32 powersave_pck[4];     // Saved programmable clocks

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)
//--------------------------------------------------------------------------
// My IP address, split into two 16 bit parts, both in network
// order. The _lo half contains the part that comes first in the
// packet, and is strictly the high order half of the network
// address. The _hi half contains the part that comes second in the
// packet and is strictly the low order half of the network address.
// i.e. for an IP address AA.BB.CC.DD, _lo contains AA.BB in network
// order and _hi contains CC.DD.

static cyg_uint16 powersave_ip_addr_lo;
static cyg_uint16 powersave_ip_addr_hi;

static cyg_bool powersave_ip_addr_set = false;

#endif

//--------------------------------------------------------------------------
// Select a given clock source and wait for it to switch.

static void cyg_hal_sam9_clock_select( cyg_uint32 clock,
                                             cyg_uint32 prescale,
                                             cyg_uint32 mdiv )
{
    cyg_uint32 mck, sr;

    HAL_READ_UINT32( _PMC_MCKR, mck );    

    XX(0x66661111);
    XX(mck);

    // If we need to change the prescaler, do it before changing
    // the clock source.
    if( (mck & _PMC_MCKR_PRES) != prescale )
    {
        XX(0x66662222);
        mck &= ~_PMC_MCKR_PRES;
        mck |= prescale;
        XX(mck);
        HAL_WRITE_UINT32( _PMC_MCKR, mck );
    }

    HAL_READ_UINT32( _PMC_MCKR, mck );    

    if( (mck & _PMC_MCKR_CSS) != clock )
    {
        XX(0x66663333);
        
        mck &= ~_PMC_MCKR_CSS;
        mck |= clock;
    
        XX(mck);
        HAL_WRITE_UINT32( _PMC_MCKR, mck );

        // Wait for clock to switch
        do
        {
            HAL_READ_UINT32( _PMC_SR, sr );
        } while( (sr & _PMC_SR_MCKRDY) != _PMC_SR_MCKRDY );

        XX(sr);
    }

    HAL_READ_UINT32( _PMC_MCKR, mck );    
    // If we need to change the MDIV value, do it here.
    if( (mck & _PMC_MCKR_MDIV) != mdiv )
    {
        XX(0x66664444);
        mck &= ~_PMC_MCKR_MDIV;
        mck |= mdiv;
        XX(mck);
        HAL_WRITE_UINT32( _PMC_MCKR, mck );
    }
    
    XX(0x6666FFFF);
}

//--------------------------------------------------------------------------
// Set up PLLA and wait for it to lock.

static void cyg_hal_sam9_clock_plla( cyg_uint32 plla )
{
    cyg_uint32 sr;

    XX(0x88881111);
    XX( plla );
    
    HAL_WRITE_UINT32( _CKGR_PLLAR, plla );

    if( plla == _CKGR_PLLAR_DIVA_0 )
        return;
    do
    {
        HAL_READ_UINT32( _PMC_SR, sr );
    } while( (sr & _PMC_IER_LOCKA) != _PMC_IER_LOCKA );
}

//--------------------------------------------------------------------------
// Set up PLLB and wait for it to lock.

static void cyg_hal_sam9_clock_pllb( cyg_uint32 pllb )
{
    cyg_uint32 sr;
    
    XX(0x99991111);
    XX( pllb );

    HAL_WRITE_UINT32( _CKGR_PLLBR, pllb );

    if( pllb == _CKGR_PLLBR_DIVB_0 )
        return;
    
    do
    {
        HAL_READ_UINT32( _PMC_SR, sr );
    } while( (sr & _PMC_IER_LOCKB) != _PMC_IER_LOCKB );
}


//--------------------------------------------------------------------------
// Switch to lower clock rate and turn off all unnecessary clock feeds
// to peripherals.

static void cyg_hal_sam9_powerdown_clocks( void )
{
    cyg_uint32 mck, pclock;

    // Save Power management system state

    HAL_READ_UINT32( _PMC_SCSR  , powersave_sysclk      );
    HAL_READ_UINT32( _PMC_PCSR  , powersave_pclock      );
    HAL_READ_UINT32( _CKGR_MOR  , powersave_mosc        );
    HAL_READ_UINT32( _CKGR_PLLAR, powersave_plla        );
    HAL_READ_UINT32( _CKGR_PLLBR, powersave_pllb        );
    HAL_READ_UINT32( _PMC_MCKR  , powersave_mck         );
    HAL_READ_UINT32( _PMC_PCKR0 , powersave_pck[0]      );
    HAL_READ_UINT32( _PMC_PCKR1 , powersave_pck[1]      );
    HAL_READ_UINT32( _PMC_PCKR2 , powersave_pck[2]      );
    HAL_READ_UINT32( _PMC_PCKR3 , powersave_pck[3]      );

    mck = powersave_mck;

    ps_diag("sysclk %08x pclock %08x mosc %08x plla %08x pllb %08x\n",
            powersave_sysclk, powersave_pclock, powersave_mosc,
            powersave_plla, powersave_pllb);

    ps_diag("mck %08x pck %08x %08x %08x %08x\n",
            mck, powersave_pck[0], powersave_pck[1],
            powersave_pck[2], powersave_pck[3] );

    XX(0x55551111);

    // Switch to 32kHz clock while we reprogram PLLA.
    cyg_hal_sam9_clock_select( _PMC_MCKR_CSS_SLOW_CLK,
                                     _PMC_MCKR_PRES_CLK,
                                     _PMC_MCKR_MDIV_1 );

    // Turn off PLLB
    cyg_hal_sam9_clock_pllb( _CKGR_PLLBR_DIVB_0 );

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)
    
    cyg_hal_sam9_clock_plla( SAM9_PLF_PLLA_POWERSAVE_80MHZ );

    // Switch back to PLLA, but set the prescaler to /2 to actually
    // run at 40MHz.

    cyg_hal_sam9_clock_select( _PMC_MCKR_CSS_PLLA_CLK,
                                     _PMC_MCKR_PRES_CLK_2,
                                     _PMC_MCKR_MDIV_1 );
#else

    // Default is to turn PLLA off and leave system running on 32kHz
    // clock.
   
    cyg_hal_sam9_clock_plla( _CKGR_PLLAR_DIVA_0 );
    
    
#endif
    
    // Disable all peripheral clocks, other than ethernet

    // First disable all devices
    HAL_WRITE_UINT32( _PMC_PCDR, powersave_pclock );

    // Now re-enable those that we want to keep running
    pclock = CYGVAR_HAL_ARM_ARM9_SAM9_POWERSAVE_ACTIVE_DEVICES;

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)
    // Keep the ethernet device running if we are polling it
    pclock |= (1<<24);
#endif
#if defined(CYG_HAL_STARTUP_RAM) && DEBUG
    // Keep timer 0 going if we are debugging via RedBoot.
    pclock |= (1<<17);
#endif
    
    HAL_WRITE_UINT32( _PMC_PCER, pclock );
    
    XX(0x5555FFFF);
}

//--------------------------------------------------------------------------

static void cyg_hal_sam9_powerup_clocks( void )
{    
    cyg_uint32 sr;

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)
    
    // Switch back to 32kHz clock while we restore the PLLs to their
    // original rates.

    cyg_hal_sam9_clock_select( _PMC_MCKR_CSS_SLOW_CLK,
                                     _PMC_MCKR_PRES_CLK,
                                     _PMC_MCKR_MDIV_1 );
#endif
    
    cyg_hal_sam9_clock_plla( powersave_plla );
    cyg_hal_sam9_clock_pllb( powersave_pllb );
    
    // Set MCK to saved value and wait for it to stabilize. This also
    // switches the CPU to running off the previous clock source,
    // usually PLLA.

    XX(0xaaaa1111);    
    HAL_WRITE_UINT32( _PMC_MCKR, powersave_mck );

    do
    {
        HAL_READ_UINT32( _PMC_SR, sr );
    } while( (sr & _PMC_SR_MCKRDY) != _PMC_SR_MCKRDY );

    XX(0xaaaa2222);

    // Restore programmable clocks
    
    HAL_WRITE_UINT32( _PMC_PCKR0 , powersave_pck[0]      );
    HAL_WRITE_UINT32( _PMC_PCKR1 , powersave_pck[1]      );
    HAL_WRITE_UINT32( _PMC_PCKR2 , powersave_pck[2]      );
    HAL_WRITE_UINT32( _PMC_PCKR3 , powersave_pck[3]      );
    
    // Turn on system clocks

    HAL_WRITE_UINT32( _PMC_SCER, powersave_sysclk );

    // Re-enable peripheral clocks

    HAL_WRITE_UINT32( _PMC_PCER, powersave_pclock );    

    XX(0xaaaaffff);    
}

//--------------------------------------------------------------------------
// Disable CPU, putting it into idle mode until an interrupt occurs.

static void cyg_hal_sam9_idle( void )
{
#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_IDLE)
    
    XX(0x77771111);
    
    HAL_WRITE_UINT32( _PMC_SCDR, _PMC_SCER_PCK );
    
    XX(0x7777FFFF);

#endif
}

//--------------------------------------------------------------------------
// Poll GPIO bits

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_GPIO)

static cyg_uint32 pio_old[3];
static cyg_uint32 pio_hi[3] = { CYGVAR_HAL_ARM_ARM9_SAM9_POWERSAVE_PIO_HI } ;
static cyg_uint32 pio_lo[3] = { CYGVAR_HAL_ARM_ARM9_SAM9_POWERSAVE_PIO_LO } ;
static cyg_uint32 pio_chg[3] = { CYGVAR_HAL_ARM_ARM9_SAM9_POWERSAVE_PIO_CHANGE } ;
static cyg_uint32 pio[3] = { SAM9_PIOA, SAM9_PIOB, SAM9_PIOC };

static cyg_bool cyg_hal_sam9_power_poll_gpio( void )
{
    int i;

    for( i = 0 ; i < 4; i++ )
    {
        cyg_uint32 pdsr, pdsr_old;
        HAL_READ_UINT32( pio[i]+_PIO_PDSR, pdsr );
        if( (pdsr & pio_hi[i]) != 0 ) return true;
        if( (pdsr & pio_lo[i]) != pio_lo[i] ) return true;
        pdsr_old = pio_old[i];
        pio_old[i] = pdsr;
        if( (pdsr & pio_chg[i]) != (pdsr_old & pio_chg[i]) ) return true;
    }

    return false;
}

#endif

//--------------------------------------------------------------------------

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)

//--------------------------------------------------------------------------
// Parse an ARP packet
//
// We only want to return true if we have an ARP request asking for
// our IP address.

static cyg_bool cyg_hal_sam9_power_arp( cyg_uint16 *pkt )
{
    ps_diag("sizes %04x opcode %04x IP %04x %04x\n",
            pkt[9], pkt[10], pkt[19], pkt[20] );

    XX(0x33331111);
    XX((pkt[20]<<16)|pkt[19]);
    
    if( (pkt[9] == 0x0406)                      &&      // Hw size == 6 && Sw size == 4
        (pkt[10] == 0x0100)                     &&      // Opcode == request
        (pkt[19] == powersave_ip_addr_lo)       &&      // Target IP.hi == me
        (pkt[20] == powersave_ip_addr_hi)       )       // Target IP.lo == me
        return true;
        
    return false;
}

//--------------------------------------------------------------------------
// Parse an IP packet
//
// We only want to return true if we have an IP packet that is
// directed to this machine. We don't want to respond to broadcasts.

static cyg_bool cyg_hal_sam9_power_ip( cyg_uint16 *pkt )
{
    ps_diag("IP %04x %04x\n", pkt[15], pkt[16] );

    XX(0x44441111);
    XX((pkt[16]<<16)|pkt[15]);
    
    if( (pkt[15] == powersave_ip_addr_lo) &&
        (pkt[16] == powersave_ip_addr_hi) )
        return true;
    return false;
}

//--------------------------------------------------------------------------
// Poll ethernet device
//
// If the ethernet MAC thinks it has received a packet then scan the
// packet buffers in on-chip SRAM and see if any match the criteria
// for us to wake up.

static cyg_bool cyg_hal_sam9_power_poll_ethernet( void )
{
    cyg_uint32 event = _ETH_RSR;

    if( event & (_ETH_RSR_OVR|_ETH_RSR_BNA) )
    {
        XX(0x22221111);
        XX(event);
        _ETH_RSR = 0x7;         // Clear event flags        
    }

    if( event & _ETH_RSR_REC )
    {
        cyg_uint32 base = SRAM_VIRT_BASE;
        volatile bd_t *rxbd = (volatile bd_t *)CYGARC_UNCACHED_ADDRESS(base);

        XX(0x22222222);
        XX(event);
        
        _ETH_RSR = 0x7;         // Clear event flags
        
        for(;;)
        {
            if( (rxbd->address & BD_BUSY) != 0 )
            {
                cyg_uint16 *pkt = (cyg_uint16 *)CYGARC_VIRTUAL_ADDRESS( rxbd->address & 0xFFFFFFFC );
                pkt = (cyg_uint16 *)CYGARC_UNCACHED_ADDRESS( pkt );

                XX(0x22223333);
                XX(rxbd->address);

#if 0
                ps_diag("rxbd %08x: %08x %08x\n", rxbd, rxbd->address, rxbd->status );
                
                ps_diag("pkt %08x: %04x%04x%04x %04x%04x%04x %04x\n", pkt,
                        pkt[0],pkt[1],pkt[2],
                        pkt[3],pkt[4],pkt[5],
                        pkt[6]);
#endif

                XX(0x22224444);
                XX(*(long *)&pkt[0]);
                XX(*(long *)&pkt[2]);
                XX(*(long *)&pkt[4]);
                XX(*(long *)&pkt[6]);
                
                if( (pkt[6] == 0x0608) && cyg_hal_sam9_power_arp( pkt ) )
                    return true;

                if( (pkt[6] == 0x0008) && cyg_hal_sam9_power_ip( pkt ) )
                    return true;

                // If the packet was not of interest, clear the buffer
                // busy bit and pass it back to the MAC.
                
                rxbd->address &= ~BD_BUSY;                    
            }

            if( rxbd->address & BD_WRAP )
                break;
            
            rxbd++;
        }
    }

    return false;
}

#endif

//--------------------------------------------------------------------------
// Main poll routine.
//
// Poll any devices that can wake us up and if there is no activity,
// put the CPU into idle mode to wait for an interrupt.

cyg_uint32 cyg_hal_sam9_power_loops;

static void cyg_hal_sam9_power_poll( void )
{
    ps_diag( "started\n", 0 );
    
    for(;;)
    {
        cyg_hal_sam9_power_loops++;

#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)        
        if( powersave_ip_addr_set && cyg_hal_sam9_power_poll_ethernet() )
            break;
#endif
#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_GPIO)
        if( cyg_hal_sam9_power_poll_gpio() )
            break;
#endif
      
        cyg_hal_sam9_idle();
    }
}

//--------------------------------------------------------------------------
// Main powerdown entry point.
//
// Switch unwanted clocks off and lower clock frequency. Poll devices
// for a wakeup condition, restore clocks and return.

void cyg_hal_sam9_powerdown( void )
{
    cyg_uint32 i, d;
    CYG_INTERRUPT_STATE oldints;

    ps_diag( "entering\n", 0 );
    XX(0x11111111);
    
    HAL_DISABLE_INTERRUPTS( oldints );
    HAL_FLASH_CACHES_OFF( d, i );
    {
        cyg_hal_sam9_powerdown_clocks();

        cyg_hal_sam9_power_poll();

        cyg_hal_sam9_powerup_clocks();
    }
    HAL_FLASH_CACHES_ON( d, i );
    HAL_RESTORE_INTERRUPTS( oldints );

    XX(0x1111FFFF);
    ps_diag( "exiting\n\n", 0 );
}

//--------------------------------------------------------------------------
// Initialization.
//
// Set up power saving system, mainly by supplying it with the IP
// address of this machine.

void cyg_hal_sam9_powersave_init( cyg_uint32 ip_addr )
{
#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_ETHERNET)
    {
        powersave_ip_addr_lo = ip_addr&0xFFFF;
        powersave_ip_addr_hi = (ip_addr>>16)&0xFFFF;

        if( ip_addr != 0 )
            powersave_ip_addr_set = true;

        ps_diag("ip_addr %08x addr_lo %04x addr_hi %04x\n",
                ip_addr, powersave_ip_addr_lo, powersave_ip_addr_hi );
    }
#endif
#if defined(CYGSEM_HAL_ARM_ARM9_SAM9_POWERSAVE_POLL_GPIO)
    {
        int i;

        for( i = 0 ; i < 4; i++ )
        {
            cyg_uint32 pdsr;
            HAL_READ_UINT32( pio[i]+_PIO_PDSR, pdsr );
            pio_old[i] = pdsr;
        }
    }
#endif
}

//--------------------------------------------------------------------------
// End sam9_powersave.c
