/*
 * flash.c: flashing user config and system option bytes
 *
 * by default, the pin used for PF2 is configured as a reset.
 * this code will check that the no-reset bit is set.
 * if it is not, this code will update the option bytes to
 * set the BOR voltage, enable BOR, and enable the no-reset bit.
 *
 * file creation: 20231019 0126
 */


#include "py32f0xx_conf.h"

#include "flash.h"



struct UConf conf = {0};



/*
 * loads flash timing values.
 * ensure flash is unlocked before calling this function,
 * otherwise the writes will silently fail.
 */
void flash_set_timing(uint32_t *timing_table)
{
    // load flash write timings from provided timing table
    // these are listed in the datasheet, but are also in ROM on the MCU

    // note: if you decide to operate at a different frequency other than
    //       a supported frequency for which timing tables do not exist
    //       in ROM, you'll have to figure out what these values actually mean,
    //       as the meaning of the values isn't documented in DS or RM

    // note: the PRETPE and SMERTPE values seem to differ in MCU ROM from
    //       the values listed in the reference manual. this is true of
    //       all versions I have access to - English v1.0 and Chinese v1.2.
    //       for example, @8MHz, RM says 0x5dc0 for PERTPE but ROM holds 0x6b60
    //       in testing, either ROM or RM value seems to work.

    // note: the datasheet doesn't say it anywhere, but if flash is not unlocked,
    //       then flash timing values aren't writeable

    // apply flash table timing values
    FLASH->TS0  = (timing_table[0] >>  0) &  0xff;
    FLASH->TS3  = (timing_table[0] >>  8) &  0xff;
    FLASH->TS1  = (timing_table[0] >> 16) & 0x1ff;
    
    FLASH->TS2P = (timing_table[1] >>  0) &  0xff;
    FLASH->TPS3 = (timing_table[1] >> 16) & 0x7ff;

    FLASH->PERTPE  = timing_table[2];   // & 0x1ffff;
    FLASH->SMERTPE = timing_table[3];   // & 0x1ffff;

    FLASH->PRGTPE = (timing_table[4] >>  0) & 0xffff;
    FLASH->PRETPE = (timing_table[4] >> 16) &  0x7ff;
}

/*
 * initializes flash timing values, and configures OPTR as necessary
 * if OPTR needs configured, the MCU will reset
 * call this function before interrupts are enabled.
 */
__attribute__ ((long_call, section(".ramfunc"))) void flash_init()
{
    // unlock flash (necessary for writing timing registers
    FLASH->KEYR = FLASH_KEY1;
    FLASH->KEYR = FLASH_KEY2;
    while (FLASH->CR & FLASH_CR_LOCK);      // still locked? then stall forever
        
    // configure flash timings
    flash_set_timing(FLASH_HSI_8MHz);

    // is the PF2/NRST pin configured as reset?
    if ((FLASH->OPTR & FLASH_OPTR_NRST_MODE) == OB_RESET_MODE_RESET) {
        // it is but shouldn't be, so let's configure it as GPIO.
        
        // per datasheet, we must unlock FLASH->CR (already done above)
        // and then unlock OPTLOCK
        FLASH->OPTKEYR = FLASH_OPTKEY1;
        FLASH->OPTKEYR = FLASH_OPTKEY2;
        while (FLASH->CR & FLASH_CR_OPTLOCK);   // somehow, OB is still locked
    
        // write new OPTR values
        FLASH->OPTR = OB_RESET_MODE_GPIO | OB_WWDG_SW | OB_IWDG_SW | OB_BOR_LEVEL_2p3_2p4 | FLASH_OPTR_RDP_LEVEL_0;
                        // OB_BOR_ENABLE

        // enable EOPIE (per DS, EOP isn't set unless EOPIE is set)
        // and signal that we want to write option bits,
        FLASH->CR |= FLASH_CR_EOPIE | FLASH_CR_OPTSTRT;
        // then trigger the write by writing to a random address shown in the DS
        *((__IO uint32_t *)(0x40022080)) = 0x55aa55aa;

        // wait for BSY to go low, then EOP to go high
        while (FLASH->SR & FLASH_SR_BSY);
        while (!(FLASH->SR & FLASH_SR_EOP));

        // do an OBL_LAUNCH reset to load the new option bits
        // this must be done while OPTLOCK is cleared
        FLASH->CR |= FLASH_CR_OBL_LAUNCH;

        // as the MCU should have reset, we should never get here.
        while(1);
    }

    // re-lock flash when done
    FLASH->CR = FLASH_CR_LOCK;
}

/*
 * loads latest configuration from flash to conf variable in RAM.
 */
void flash_load_conf()
{
    int8_t i, j;
    uint8_t loaded = 0;
    
    struct UConf *fconf;
    uint32_t *f32;
    uint32_t *u32;

    for (i = (UCONF_COUNT - 1); i >= 0; i--) {
        fconf = (struct UConf *)(UCONF_FLASH_START + (UCONF_SIZE * i));
        f32   = (uint32_t *)fconf;
        
        // empty flash = nothing here
        if (fconf->crc32 == 0xffffffff) continue;

        // invalid header = skip
        if (fconf->conf_key != UCONF_KEY) continue;
        
        // calculate crc32
        CRC->CR = CRC_CR_RESET;
        while (CRC->CR & CRC_CR_RESET);

        for (j = 0; j < ((UCONF_SIZE / 4) - 1); j++) {
            CRC->DR = f32[j];
        }

        // check crc32
        if (CRC->DR == f32[(UCONF_SIZE / 4) - 1]) {
            // it passes. copy to RAM and break out
            u32 = (uint32_t *)&conf;
            for (j = 0; j < (UCONF_SIZE / 4); j++) {
                *u32++ = *f32++;
            }
            loaded = 1;
            break;
        }
    }

    if (!loaded) {
        // default config of zeroes works
    }
}

/*
 * erases a flash page at the specified address.
 */
__attribute__ ((long_call, section(".ramfunc"))) void flash_erase_page(uint32_t addr)
{
    uint32_t primask;

    // ensure flash is not busy
    while (FLASH->SR & FLASH_SR_BSY);

    // unlock flash
    FLASH->KEYR = FLASH_KEY1;
    FLASH->KEYR = FLASH_KEY2;
    while (FLASH->CR & FLASH_CR_LOCK);      // still locked? then stall forever

    // enable EOPIE (per DS, EOP isn't set unless EOPIE is set)
    // and signal that we want to erase a page
    FLASH->CR |= FLASH_CR_EOPIE | FLASH_CR_PER;

    // disable interrupts
    primask = __get_PRIMASK();
    __disable_irq();

    // trigger the erase by writing to a random address shown in the DS
    *((__IO uint32_t *)(addr)) = 0x55aa55aa;

    // ensure flash is not busy, then wait for end of programming
    while (FLASH->SR & FLASH_SR_BSY);
    while (!(FLASH->SR & FLASH_SR_EOP));

    // re-enable interrupts
    __set_PRIMASK(primask);

    // clear EOP
    FLASH->SR |= FLASH_SR_EOP;

    // and re-lock flash
    FLASH->CR = FLASH_CR_LOCK;
}

/*
 * erases a flash page at the specified address.
 * data must be a pointer to a full page and
 * must contain 128 bytes (32 words) of data to write.
 */
__attribute__ ((long_call, section(".ramfunc"))) void flash_write_page(uint32_t addr, uint32_t *data)
{
    uint32_t primask;
    
    uint8_t i;

    uint32_t *src;
    volatile uint32_t *dst;

    // ensure flash is not busy
    while (FLASH->SR & FLASH_SR_BSY);

    // unlock flash
    FLASH->KEYR = FLASH_KEY1;
    FLASH->KEYR = FLASH_KEY2;
    while (FLASH->CR & FLASH_CR_LOCK);      // still locked? then stall forever

    // enable EOPIE (per DS, EOP isn't set unless EOPIE is set)
    // and signal that we want to write a page
    FLASH->CR |= FLASH_CR_EOPIE | FLASH_CR_PG;

    // disable interrupts
    primask = __get_PRIMASK();
    __disable_irq();

    // write data to flash
    src = data;
    dst = (volatile uint32_t *)addr;
    for (i = 0; i < (FLASH_PAGE_SIZE / 4); i++) {
        // on the last page, enable the page write start bit to commit to flash
        if (i == (FLASH_PAGE_SIZE / 4) - 1) {
            FLASH->CR |= FLASH_CR_PGSTRT;
        }
        
        *dst++ = *src++;
    }

    // ensure flash is not busy, then wait for end of programming
    while (FLASH->SR & FLASH_SR_BSY);
    while (!(FLASH->SR & FLASH_SR_EOP));

    // re-enable interrupts
    __set_PRIMASK(primask);
    
    // clear EOP
    FLASH->SR |= FLASH_SR_EOP;

    // and re-lock flash
    FLASH->CR = FLASH_CR_LOCK;
}

/*
 * stores configuration from conf variable in RAM to flash.
 * performs very basic wear leveling.
 */
void flash_commit_conf()
{
    uint32_t i;
    
    uint8_t flash_idx = 0;
    uint8_t flash_full;

    struct UConf *fconf;
    uint32_t *f32, *u32;
    uint32_t write[(FLASH_PAGE_SIZE / 4)];

    // find first free page index
    for (i = 0; i < UCONF_COUNT; i++) {
        fconf = (struct UConf *)(UCONF_FLASH_START + (UCONF_SIZE * i));
        if ((fconf->crc32 == 0xffffffff) || (fconf->conf_key != UCONF_KEY)) {
            flash_idx = i;
            break;
        }
    }

    // if all pages are occupied, erase the first page
    if (i == UCONF_COUNT) {
        flash_full = 1;
        flash_erase_page(UCONF_FLASH_START);
    }

    // copy flash page from ROM to the write buffer,
    // as our data (64 bytes) is smaller than the page write size (128 bytes)
    f32 = (uint32_t *)fconf;
    u32 = (uint32_t *)write;
    for (i = 0; i < (FLASH_PAGE_SIZE / 4); i++) {
        *u32++ = *f32++;
    }

    // ensure config header is correct, and calculate CRC
    conf.conf_key = UCONF_KEY;
    u32 = (uint32_t *)&conf;
    
    CRC->CR = CRC_CR_RESET;
    while (CRC->CR & CRC_CR_RESET);

    for (i = 0; i < (UCONF_SIZE / 4) - 1; i++) {
        CRC->DR = u32[i];
    }
    u32[(UCONF_SIZE / 4) - 1] = CRC->DR;

    // copy user config from RAM to write buffer at the appropriate offset
    f32 = (uint32_t *)&conf;
    u32 = (uint32_t *)write;
    for (i = (flash_idx ? UCONF_SIZE : 0); i < (UCONF_SIZE / 4); i++) {
        *u32++ = *f32++;
    }

    // if flash was full, erase other pages
    if (flash_full) {
        for (i = (UCONF_FLASH_START + FLASH_PAGE_SIZE); i < FLASH_END; i += FLASH_PAGE_SIZE) {
            flash_erase_page(i);
        }
    }
}