Close

STM32 Standby Mode, FLASH vs Backup SRAM

A project log for The Slowest Video Player with 7-Colors!

Watch your favorite films at 1 frame per minute

manuel-tosoneManuel Tosone 08/07/2021 at 07:220 Comments

FLASH vs Backup SRAM

STM32 microcontrollers have different power modes, each of which is a compromise between power consumption and wakeup time. Standby mode features the lowest power consumption. In this mode, the CPU and peripherals are stopped, clocks are switched off, SRAM and register content are lost. The only peripherals that keep running are the RTC with its 32.768KHz oscillator and the backup SRAM. The microcontroller exits standby mode after a physical reset or an interrupt from the RTC clock. In either case, the CPU has lost all of its state, all the variables are gone and code execution restarts from the beginning.

There are two areas of memory that don't lose data during standby mode: FLASH and backup SRAM. Backup SRAM is the simplest to use, it consists of 4 Kbytes of memory that remains powered during standby. You just write to it like normal RAM. The only problem it has is that if power is lost (while changing the batteries for example) the contents are lost.

FLASH memory is not so easy to use, it is slower and has limited write/erase cycles but it maintains its content without needing power. The limited number of cycles is a big deal for this project. Every 24 minutes a string containing the path to the currently displayed frame has to be stored. This means 60 writes per day, with only 10000 write/erase cycles available the FLASH will only last 166 days.

Overcoming the Write/Erase Cycles Limitation

The FLASH memory of the STM32F405 is divided into 12 sectors, 0 to 11. Although it is possible to write to any sector, care must be taken to not overwrite the firmware. The compiler puts the code in memory starting from the first sector so the last one should be safe to use. To erase one byte inside a sector, the whole sector must be erased. Erasing a sector writes 0xFF to every byte in the sector, programming a byte inside a sector writes zeros to the appropriate bits. Sector 11 is 128KB in size, instead of erasing it each time, it can be erased once, data can be written sequentially until it is full and then erased again. This fixes the original write/erase problem and creates a new one. If every write is to a different location, how do you know from where to read?

Implementation

To keep things simple I made a struct that is 32 bytes long. The first 22 bytes store the actual data, the magic number is used to optimize the search algorithms and is always written to 0x5A, the checksum is to detect data corruption in case power is lost during a write.

Writes are always aligned to 32 bytes. A 128KB sector can store 4096 of these structs. 

typedef struct
{
    char file_path[22];    //File path string
    uint8_t padding[8];    //Padding to make the struct 32 bytes
    uint8_t magic;         //Magic number (0x5A = entry contains data)
    uint8_t checksum;      //Checksum for the entry
} FlashEntry_t;

To write a new block of data, start from the beginning of the sector and search for the first block of 32 bytes that are not already in use. If there is no space erase the sector end start from the beginning. To optimize the search, the magic number is checked first, if it is 0xFF the other bytes are checked else the block is considered already in use.

/*
 * Finds the address of the first empty entry
 * returns null if no valid entry is found
 * */
static FlashEntry_t* FLASH_FindFirstEmptyEntry(void)
{
    FlashEntry_t* pt = (FlashEntry_t*)_FLASH_STRT_ADDR;

    while(pt <= (FlashEntry_t*)(_FLASH_STOP_ADDR - sizeof(FlashEntry_t)))
    {
        //Check if the entry is empty (check magic first)
        if(pt->magic == 0xff)
        {
            uint8_t t = 0xff;

            for(int i = 0; i < sizeof(FlashEntry_t); i++)
                t &= ((uint8_t*)pt)[i];

                if(t == 0xff)
                    return pt;
            }

            //Point to the next entry
            pt ++;
    }

    return NULL;
}

To read the last block of data, start from the end of the sector and search backward for the first valid block. To optimize the search the magic number is checked first, if it is 0x5A the checksum for the block is computed.

/*
 * Finds the last entry in the flash
 * returns null if no valid entry is found
 * */
static FlashEntry_t* FLASH_FindLastValidEntry(void)
{
    FlashEntry_t* pt = (FlashEntry_t*)(_FLASH_STOP_ADDR - sizeof(FlashEntry_t));

    while(pt >= (FlashEntry_t*)_FLASH_STRT_ADDR)
    {
        //Check if the entry is valid (checksum is not computed if magic is invalid)
        if(pt->magic == 0x5A && FLASH_ComputeChecksum(pt) == 0)
            return pt;

            //Point to the previous entry
            pt --;
    }

    return NULL;
}

 The checksum is used to detect data corruption. This may happen if power is lost in the middle of a write.

/*
 * Compute checksum for the entry
 * */
static uint8_t FLASH_ComputeChecksum(FlashEntry_t* entry)
{
    uint8_t sum = 0;

    for(int i = 0; i < sizeof(FlashEntry_t); i++)
        sum += ((uint8_t*)entry)[i];

    return ~sum+1;
}

Conclusion

With this algorithm, the FLASH is only erased every 4096 writes, thus increasing the life span from 166 days to 166*4096 = 1862 years.

I hope that this can be useful, you can find the complete implementation code on GitHub.

And with this, I think I'll stop working on this project for now and start doing some long-term testing on the battery life. If my calculations are correct, when this baby hits 88 miles p... I'll be back in 291 days :)

Discussions