View on Github
			
/**
 * @file sysclock_pic32mz_mk.h
 * @author Sebastien CAUX (sebcaux)
 * @copyright Robotips 2017
 * @copyright UniSwarm 2018-2023
 *
 * @date March 01, 2016, 22:10 PM
 *
 * @brief System clock support for udevkit for PIC32MZ family (DA, EC and EF)
 * and PIC32MK
 *
 * Implementation based on Microchip document DS60001250B :
 *  http://ww1.microchip.com/downloads/en/DeviceDoc/60001250B.pdf
 */

#include "sysclock.h"

#include <archi.h>
#include <board.h>

static uint32_t _sysclock_sysfreq = 0;
static uint32_t _sysclock_sosc = 0;
static uint32_t _sysclock_posc = 0;
static uint32_t _sysclock_pll = 0;
#if defined(ARCHI_pic32mk)
static uint32_t _sysclock_upll = 0;
#endif

/**
 * @brief Gets the actual frequency on a particular peripheral bus clock
 * @param busClock id of the bus clock (1 to 8 for PBCLK1 to PBCLK8), 0 for sysclock
 * @return bus frequency in Hz
 */
uint32_t sysclock_periphFreq(SYSCLOCK_CLOCK busClock)
{
    if (_sysclock_sysfreq == 0)
    {
        _sysclock_sysfreq = sysclock_sourceFreq(sysclock_source());
    }

    if (busClock == SYSCLOCK_CLOCK_SYSCLK)
    {
        return _sysclock_sysfreq;
    }

    if (busClock > SYSCLOCK_CLOCK_REFCLOCK4)
    {
        return 1;  // error, not return 0 to avoid divide by zero
    }

    if (busClock <= SYSCLOCK_CLOCK_PBCLK8)
    {
        // Peripheral bus divider
        volatile uint32_t *divisorAddr = &PB1DIV + (((uint8_t)busClock - SYSCLOCK_CLOCK_PBCLK1) << 2);
        uint8_t divisor = ((*divisorAddr) & 0x0000007F) + 1;
        return _sysclock_sysfreq / divisor;
    }

    // Ref clock divider
    volatile uint32_t *refClockConAddr = &REFO1CON + (((uint8_t)busClock - SYSCLOCK_CLOCK_REFCLOCK1) << 3);
    volatile uint32_t *refClockTrimAddr = &REFO1TRIM + (((uint8_t)busClock - SYSCLOCK_CLOCK_REFCLOCK1) << 3);
    uint32_t divisorInt = *refClockConAddr >> 16;
    uint32_t divisorTrim = *refClockTrimAddr >> 23;
    float divisor = divisorInt * 2 + ((float)divisorTrim * 2) / 512.0;
    if (divisorInt == 0)
    {
        divisor = 1;
    }
    uint32_t sourceFreq = 1;
    switch (*refClockConAddr & 0x0000000F)
    {
        case SYSCLOCK_REFCLK_SRC_REFCLKI:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_SPLL:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_UPLL:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_SOSC:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_LPRC:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_FRC:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_POSC:
            sourceFreq = 0;
            break;

        case SYSCLOCK_REFCLK_SRC_PBCLK1:
            sourceFreq = sysclock_periphFreq(SYSCLOCK_CLOCK_PBCLK1);
            break;

        case SYSCLOCK_REFCLK_SRC_SYSCLOCK:
            sourceFreq = _sysclock_sysfreq;
            break;
    }
    return (uint32_t)((float)sourceFreq / divisor);
}

/**
 * @brief Change the divisor of the busClock given as argument. This can take up to 60
 * CPU cycles.
 * @param busClock id of the bus clock (1 to 8 for PBCLK1 to PBCLK8)
 * @param div divisor to set
 * @return 0 if ok, -1 in case of error
 */
int sysclock_setClockDiv(SYSCLOCK_CLOCK busClock, uint16_t div)
{
    if (OSCCONbits.CLKLOCK == 1)
    {
        return -1;  // Clocks and PLL are locked, source cannot be changed
    }
    if (busClock == SYSCLOCK_CLOCK_SYSCLK)  // cannot change sysclock
    {
        return -1;
    }
    if (busClock > SYSCLOCK_CLOCK_PBCLK8)  // bad index
    {
        return -1;
    }
    if (div == 0 || div > 128)
    {
        return -1;  // bad divisor value
    }

    // get divisor bus value
    volatile uint32_t *divisorAddr = &PB1DIV + (((uint8_t)busClock - 1) << 2);

    // wait for divisor can be changed
    while ((*divisorAddr & _PB1DIV_PBDIVRDY_MASK) == 0)
    {
        nop();
    }

    // critical section, protected by lock on clock config
    unlockClockConfig();
    *divisorAddr = (*divisorAddr & 0xFFFFFF80) + (div - 1);
    lockClockConfig();

    // wait for divisor setted
    while ((*divisorAddr & _PB1DIV_PBDIVRDY_MASK) == 0)
    {
        nop();
    }

    return 0;
}

/**
 * @brief Return the actual frequency of the clock source
 * @param source clock id to request
 * @return frequency of 'source' clock, 0 in case of disabled clock, -1 in case of error
 */
int32_t sysclock_sourceFreq(SYSCLOCK_SOURCE source)
{
    int32_t freq = -1;
    uint16_t divisor;
    int32_t osctune;
    switch (source)
    {
#if defined(ARCHI_pic32mzda) || defined(ARCHI_pic32mzec) || defined(ARCHI_pic32mzef)
        case SYSCLOCK_SRC_BFRC:
            freq = 8000000;  // 8MHz BFRC hardware automatic selection
            break;

        case SYSCLOCK_SRC_FRC2:
#endif

        case SYSCLOCK_SRC_FRC:
            divisor = OSCCONbits.FRCDIV;
            if (divisor != 0b111)
            {
                divisor = 1 << divisor;
            }
            else
            {
                divisor = 256;
            }

            osctune = OSCTUN;
            if (osctune >= 32)
            {
                osctune = (osctune | 0xFFFFFFE0);
            }

            freq = (8000000 + osctune * 31250) / divisor;  // 8MHz typ FRC, tuned by OSCTUN (+/- 12.5%), divided by FRCDIV
            break;

        case SYSCLOCK_SRC_LPRC:
            freq = 32000;  // 32kHz LPRC
            break;

        case SYSCLOCK_SRC_SOSC:
            freq = _sysclock_sosc;  // external secondary oscillator
            break;

        case SYSCLOCK_SRC_POSC:
            freq = _sysclock_posc;  // external primary oscillator
            break;

        case SYSCLOCK_SRC_SPLL:
            if (_sysclock_pll == 0)
            {
                _sysclock_pll = sysclock_getPLLClock();
            }
            freq = _sysclock_pll;  // PLL out freq
            break;

#if defined(ARCHI_pic32mk)
        case SYSCLOCK_SRC_UPLL:
            freq = _sysclock_upll;  // USB PLL out freq
            break;
#endif
    }
    return freq;
}

/**
 * @brief Change a frequency of a source if it can be modified
 * @param source clock id to change
 * @return 0 in case of success, -1 in case of error
 */
int sysclock_setSourceFreq(SYSCLOCK_SOURCE source, uint32_t freq)
{
    if (source == SYSCLOCK_SRC_SOSC)
    {
        _sysclock_sosc = freq;
        return 0;
    }
    if (source == SYSCLOCK_SRC_POSC)
    {
        _sysclock_posc = freq;
        return 0;
    }
    return -1;
}

/**
 * @brief Return the actual clock source for system clock
 * @return SYSCLOCK_SOURCE enum corresponding to actual clock source
 */
SYSCLOCK_SOURCE sysclock_source(void)
{
    SYSCLOCK_SOURCE source = (SYSCLOCK_SOURCE)OSCCONbits.COSC;
#ifdef SYSCLOCK_SRC_FRC2
    if (source == SYSCLOCK_SRC_FRC2)
    {
        return SYSCLOCK_SRC_FRC;
    }
#endif
    return source;
}

/**
 * @brief Switch the source clock of sysclock to another one and wait for the change effective
 * @param source id to switch to
 * @return 0 if ok, -1 in case of error
 */
int sysclock_switchSourceTo(SYSCLOCK_SOURCE source)
{
    if (OSCCONbits.CLKLOCK == 1)
    {
        return -1;  // Clocks and PLL are locked, source cannot be changed
    }

#ifdef SYSCLOCK_SRC_BFRC
    if (source == SYSCLOCK_SRC_BFRC)
    {
        return -2;  // cannot switch to backup FRC
    }
#endif

    // disable interrupts
    uint32_t int_flag = disable_interrupt();

    // unlock clock config (OSCCON is write protected)
    unlockClockConfig();

    // select the new source
    OSCCONbits.NOSC = source;

    // trigger change
    OSCCONSET = _OSCCON_OSWEN_MASK;
    nop();
    nop();

    // relock clock config
    lockClockConfig();

    while (OSCCONbits.OSWEN == 1)
    {
        nop();
    }

    // restore interrupts
    if ((int_flag & 0x00000001) != 0)
    {
        enable_interrupt();
    }

    if (sysclock_source() != source)
    {
        return -3;  // Error when switch clock source
    }

    _sysclock_sysfreq = sysclock_sourceFreq(sysclock_source());

    return 0;
}

/**
 * @brief Internal function to set clock with PLL from XTAL or FRC
 * @param fosc desirate system frequency in Hz
 * @param src input source clock of PLL (SYSCLOCK_SRC_FRC or SYSCLOCK_SRC_POSC)
 * @return 0 if ok, -1 in case of error
 */
int sysclock_setPLLClock(uint32_t fosc, uint8_t src)
{
    if (sysclock_source() == SYSCLOCK_SRC_SPLL)
    {
        return -1;  // cannot change PLL when it is used
    }

    if (fosc > SYSCLOCK_FOSC_MAX)
    {
        return -1;  // cannot generate fosc > SYSCLOCK_FOSC_MAX
    }

    uint8_t inputBit;
    uint32_t fin;
#ifdef SYSCLOCK_SRC_FRC2
    if (src == SYSCLOCK_SRC_FRC2 || src == SYSCLOCK_SRC_FRC)
#else
    if (src == SYSCLOCK_SRC_FRC)
#endif
    {
        fin = sysclock_sourceFreq(SYSCLOCK_SRC_FRC);
        inputBit = 1;  // FRC as input
    }
    else if (src == SYSCLOCK_SRC_POSC)
    {
        fin = sysclock_sourceFreq(SYSCLOCK_SRC_POSC);
        inputBit = 0;  // POSC as input
    }
    else
    {
        return -1;  // source can be only FRC or POSC
    }

    // post div
    uint8_t postdiv = 32;
    uint8_t postdivBits = 0b101;
    while (fosc * postdiv > SYSCLOCK_FVCO_MAX && postdiv >= 2)
    {
        postdiv = postdiv >> 1;
        postdivBits--;
    }
    if (postdiv < 2)
    {
        return -1;
    }

    uint32_t fpllo = fosc * postdiv;

    // best pre divisor and multiplier computation
    uint16_t prediv = 1, multiplier = 1;
    int32_t error = 0x7FFFFFFF;
    for (uint16_t mprediv = 1; mprediv <= 8; mprediv++)
    {
        uint16_t mmultiplier = fpllo / (fin / mprediv);
        int32_t merror = fin / mprediv * mmultiplier / postdiv - fpllo;
        if (merror < 0)
        {
            merror = -merror;
        }
        if (merror < error && mmultiplier <= 128)
        {
            prediv = mprediv;
            multiplier = mmultiplier;
            error = merror;
        }
    }

    // disable interrupts
    uint32_t int_flag = disable_interrupt();

    unlockClockConfig();  // unlock clock config (OSCCON is write protected)
    // set prediv, post and multiplier in bits
    SPLLCONbits.PLLICLK = inputBit;
    SPLLCONbits.PLLODIV = postdivBits;
    SPLLCONbits.PLLMULT = multiplier - 1;
    SPLLCONbits.PLLIDIV = prediv - 1;
    lockClockConfig();

    // restore interrupts
    if ((int_flag & 0x00000001) != 0)
    {
        enable_interrupt();
    }

    _sysclock_pll = sysclock_getPLLClock();

    return 0;
}

/**
 * @brief Sets the system clock of the CPU, the system clock may be different of CPU
 * clock and peripheral clock
 * @param fosc desirate system frequency in Hz
 * @return 0 if ok, -1 in case of error
 */
int sysclock_setClock(uint32_t freq)
{
    uint8_t src = SYSCLOCK_SRC_FRC;
    if (_sysclock_posc != 0)
    {
        src = SYSCLOCK_SRC_POSC;
    }
    if (sysclock_source() == SYSCLOCK_SRC_SPLL)  // change sysclock to SRC (FRC or POSC) if source is already PLL
    {
        sysclock_switchSourceTo(src);
    }
    if (sysclock_setPLLClock(freq, src) != 0)
    {
        return -1;
    }
    return sysclock_switchSourceTo(SYSCLOCK_SRC_SPLL);
}

uint32_t sysclock_getPLLClock(void)
{
    uint32_t fin;
    if (SPLLCONbits.PLLICLK == 1)  // FRC as input
    {
        fin = sysclock_sourceFreq(SYSCLOCK_SRC_FRC);
    }
    else
    {
        fin = sysclock_sourceFreq(SYSCLOCK_SRC_POSC);
    }

    uint16_t prediv = SPLLCONbits.PLLIDIV + 1;
    uint16_t multiplier = SPLLCONbits.PLLMULT + 1;
    uint16_t postdiv = 1 << (SPLLCONbits.PLLODIV);

    uint32_t fpllo = fin / prediv * multiplier / postdiv;
    return fpllo;
}