How the STM32 handles interrupts while writing to flash memory
The process I went through to learn how to safely write data to flash memory in the STM32 without causing interrupts to fail was long and lonely. So I wrote this entire page up because sometimes I feel like I might help someone to avoid the same process I had to go through. So here is my full write up on the subject.
For the STM32, code (firmware) is stored in flash memory - simple and straightforward, right? Well, there's a catch. When you want to write data into flash memory, read access to the flash memory is "blocked". During "blocking", the CPU cannot read any instructions or data from the flash memory. So the CPU is forced (by hardware logic and timers within the CPU) to wait for the blocking to end. By "wait", I mean the CPU is literally forced to "pause" until the flash write is finished. On top of that, if and when the CPU receives an interrupt (from the MIDI UART for example, but it could be any interrupt) it cannot jump to the corresponding interrupt service routine because the address of that routine is stored in the interrupt vector table which (by default) is also stored in flash memory (which is temporarily blocked!). The solution is to place the ISR functions, the ISR vector table, and all functions that are called by the ISR functions, into SRAM. Doing that requires some real work, as follows:
Stage 1 - Store the Required Functions in Static RAM
First, we need the CPU to be able call the functions that write to flash and functions (and all sub functions) that are called by interrupts (ISR's) while flash writing is underway. To do that, the called functions need to be stored in static RAM memory (because the flash memory is locked and any attempts to read from flash will result in the CPU being placed into a wait state until the blocking ends). In other words, since the essential functions are stored in static RAM (instead of flash memory), the CPU will be able to execute those functions even while the flash write is underway.
Step 1) Determine which interrupts are non essential (can be disabled temporarily). We will disable those before writing to flash and reenable them after the flash write completes. For the HA-832 project, we can get away without the sound generation interrupt (TIM2) and the LFO delay timer interrupt (TIM3).
Step 2) Determine which interrupts are essential (cannot be disabled at all). For the HA-832, the MIDI/sequencer interrupt cannot be disabled and neither can SysTick.
Step 3) Mark the essential ISR functions as being stored in static RAM. This is done by setting an attribute on the function that tells the compiler and linker to store the function in static RAM instead of flash memory.
Step 4) Make sure that
any and all sub functions that are called by the essential ISR functions are also marked as being stored in static RAM. In some cases, it might be possible to reduce the number of functions calls by collapsing code into the ISR's themselves.
Step 5) Mark the flash write functions so that they also will be stored in static RAM. Also ensure that any subroutines called by those flash write functions are also stored in static RAM.
Step 6) Modify the linker script to place the marked functions into static RAM during the linking process.
Stage 2 - Relocate the Interrupt Vector Table into Static RAM
Second, when the CPU receives an interrupt, it needs to be able to jump to the corresphonding interrupt service (ISR) routine. In other words when an interrupt is received, the CPU needs to look up the address of the ISR function and then jump to that function address. The ISR jump addresses are stored in the interrupt vector lookup table, which is (by default) stored in flash memory. So we need to relocate the interrupt vector table into static RAM too.
Step 1) Define an array that will be used to store the vector table in static RAM.
Step 2) At start up, copy the contents of the vector table into the new vector table.
Step 3) Also at start up, switch the STM32 vector table pointer to use the new vector table. Since the new vector table is stored in static RAM it can be accessed while flash writes are underway.
Step 4) Disable the non essential interrupts before carrying out flash write functions. And reenable them after the flash write functions have completed.
Step 5) Modify the linker script to place the new interrupt vector table array into static RAM during the linking process.
Where to Begin?
It's important to keep in mind that all of the above was new learning for me. I was not aware of any of the details before I started working on this problem. The starting point for this was that I noticed that MIDI data reception simply stopped when a flash write was performed. At that point, the only thing I suspected was that the MIDI hardware interrupt was being blocked somehow, for unknown reasons.
I had experienced flash write blocking before while working with the HAWK-800 kit and the AT28C256 flash ROM's. So I had some sense of what the basic problem could be but I had very little idea of how to connect that original challenge to any steps required to fix the issue on the STM32. So I did many, many, many google searches and found the following two articles to be the most useful:
Writing to flash blocks all interrupts This article outlines the basic steps involved in correcting the problem. It is not dealing with the STM32F3 series CPUs specifically but it turns out that the methods are all very similar across the ARM CPU variants. The article also mentions the SysTick timer interrupt which was helpful because I could have easily missed that during my remediation work. This article also shows how there is a define in the SystemInit function that allows for pointing the vector table at static RAM. So with that articile, I had some steps to begin to work through in order to solve the problem.
Executing code from RAM on STM32 CPUs This article shows how to instruct the C compiler to relocate functions into static RAM. It also explains how the linker script is used to change the compilation process to place 'marked' C functions into the right places in memory. The article also explains how the startup *.s file contains assembly that copies that different code and data sections into memory during start up including the way in which code is copied from flash into static RAM.
But we are just getting started! Let's get into the specifics.
Stage 1 Steps - Putting Functions into Static RAM Memory
S1 Steps 1 and 2 - Identify Interrupts that Can and Cannot be Disabled during Flash Writes
In the HAWK-832 code (so far), we have three ISR's in place. One ISR each, for timer2, timer3, and external hardware interrupts. Timer2 is for the envelope generator and timer3 is for the LFO delay timers. The brief moments when flash writes occur are not going to interfere with the sound generation or playability if we disable those two interrupts for a split second so both interrupts can be safely disabled while flash write operations are happening.
Disabling those two interrupts avoids the need to place their corresponding ISR functions into SRAM. However, the hardware interrupts (MIDI and the sequencer) must function at all times, so they cannot be disabled. First, if we disable MIDI interrupts then we could easily miss MIDI messages that are received, which would be disastrous. Second, we don't want to miss the sequencer interrupt because that controls the pace and syncing of other instruments. If we miss sequencer clocks then the sequencer will go out of sync with MIDI timing, which would also be disastrous.
So the following code will be needed inside of flash memory write functions to disable just the two timers (and since writes happen in a few tens of milliseconds, they will be re-enabled before any sound degradation is noticed).
NVIC_DisableIRQ(TIM2_IRQn);
NVIC_DisableIRQ(TIM3_IRQn);
And then, once the flash write functions are completed, we need to reenable the interrupts again.
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_EnableIRQ(TIM3_IRQn);
S1 Step 3 - Assign Critical ISR Functions to RAM
All functions that will be called during flash write operations must be placed into static RAM. This is done by declaring each function with a 'section' attribute that instructs the
linker to place the compiled function code into static RAM.
I used the label ".code_in_ram". The system tick timer, the harware interrupt service routing, and the flash write functions all need to be declared in this way.
__attribute__((section(".code_in_ram"))) void Time_Update(void)
{
LocalTime += SYSTEMTICK_PERIOD_MS;
}
__attribute__((section(".code_in_ram"))) void EXTI15_10_IRQHandler() {
// function code...
}
S1 Step 4 - Assign ISR Subfunctions to RAM
As can be seen above, the system tick ISR does not call any functions (it just increments a variable). And, I was careful to write the hardware ISR so that it doesn't call any functions either. So in our case, step 4 is not required. I mention it here to ensure that this isn't forgotten in future projects - or your own!
S1 Step 5 - Assign Flash Write Functions to RAM
In the HAWK-832 code (so far), we have two functions that write to flash. So both of those need to be defined so that they will be placed into static RAM. As follows:
__attribute__((section(".code_in_ram"))) uint8_t save_globals(void) {
// function code...
}
__attribute__((section(".code_in_ram"))) uint8_t save_patch(uint8_t patch, uint8_t bank) {
// function code...
}
S1 Step 5 - Continued...
Writing data to flash (in both of the functions above) requires calling three functions (as shown below). Each one of those need to go into static RAM too.
__attribute__((section(".code_in_ram"))) FLASH_Status FLASH_ErasePage(uint32_t Page_Address){
// function code...
}
__attribute__((section(".code_in_ram"))) FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data){
// function code...
}
__attribute__((section(".code_in_ram"))) FLASH_Status FLASH_GetStatus(void){
// function code...
}
S1 Step 6 - Tell the Linker where to Locate the Functions that are Allocated to RAM
The last step (6) of stage one is where we tell the linker (we told the compiler above already) where to place the functions into static RAM. In my configuration, the linker file is called "stm32_flash.ld".
The text below shows the ".data" section from within the stm32_flash.ld file. This section assigns most of the C program variables into space within the static RAM.
What we are going to do is insert a new section into this allocation that includes the functions that were assigned to a section we called ".code_in_ram".
Here is the linker file beforehand:
/* Initialized data sections goes into RAM, load LMA copy after code */
.data : AT ( _sidata )
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM
And here it is after we added our special section (note the
blue text):
/* Initialized data sections go into SRAM, load LMA copy after code */
.data : AT ( _sidata )
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
*(.code_in_ram*) /* added section for functions that must be in SRAM */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM
So what we have done is told the compiler to place the flash related functions into the very top of the static RAM (above the main data sections).
Stage 2 Steps - ISR Vector Table Relocation
The other major component of this effort is to place the ISR vector jump table into static RAM. To recap this stage, we have to place the ISR jump vector table into static RAM because when an interrupt occurs the CPU has to stop executing the function that it is currently processing at that moment and jump to the address of the ISR that generated the interrupt. The address of each ISR is stored in flash memory (by default). So, if a flash write is underway when an interrupt occurs, then the CPU is forced to wait for the flash write to finish before it can access the ISR jump table to get the address of the ISR that it needs to jump to to execute it. What a mounthful that was.
S2 Steps 1-3 - Create Our Own ISR Vector Table
Steps 1, 2, and 3, of stage two are handled within main.c. At step 1, we define the size of the ISR vector table that will be held in SRAM (it will be an exact copy of the original ISR table that is held in flash memory.
/* main.c */
// a new vector table is needed that resides in SRAM so that MIDI (and systick) interrupts can continue while flash is being written to
#define ISR_TABLE_SIZE 174
__attribute__((section(".isr_vector_ram"))) uint32_t vectorTable[ISR_TABLE_SIZE];
In step 2, we use memcpy to copy the contents of the interrupt vector table (in flash) over to the vectorTable array. Note that the vectorTable array was also marked as going into a special location labeled as ".isr_vector_ram". More on that in a moment (see step 5 below).
Then at step 3, we set the SCB->VTOR register to point to the vector table array in SRAM. Steps 2 and 3 are shown below. Note that I disable and re-enable interrupts, which is probably not necessary (to be confirmed).
int main(void){
// set up SRAM based ISR vector table (needed for ISR's to operate while writing to flash)
__disable_irq();
memcpy(vectorTable, (uint32_t*)SCB->VTOR, sizeof(uint32_t) * ISR_TABLE_SIZE);
SCB->VTOR = (uint32_t)vectorTable;
__enable_irq();
S2 Step 4 - Handling Interrupt State in Flash Functions
Any functions that write to flash need to disable the non essential interrupts and then reenable them after the flash write/s have completed.
Below are the two flash write functions (so far) that I have in the code base. I have reproduced them here in full in order to show the order of operations of disabling and re-enabling the non-essential interrupts (TIM2 and TIM3).
See the blue text below. As an aside, if you were to look into the FLASH_ErasePage() and FlashProgramWord() functions, you would find they call FLASH_GetStatus(). So that is why FLASH_GetStatus() also needs to be located in SRAM.
__attribute__((section(".code_in_ram"))) uint8_t save_globals(void)> {
NVIC_DisableIRQ(TIM2_IRQn);
NVIC_DisableIRQ(TIM3_IRQn);
FLASH_Unlock();
uint8_t status;
// NOTE: FLASH_ErasePage erases an entire 2K flash bank
if ((status=FLASH_ErasePage(FLASH_GLOBALS)) == FLASH_COMPLETE) {
uint32_t FlashAddress = FLASH_GLOBALS; // pointer to globals in flash
uint32_t *pdata = (uint32_t*) &globalsMem; // pointer to source data
uint32_t len = (sizeof(globalsMem) / 4) + 1; // number of words to program
while (len) {
if (FLASH_ProgramWord(FlashAddress, *pdata) == FLASH_COMPLETE) {
FlashAddress += 4;
pdata++;
len--;
} else break;
}
}
NVIC_EnableIRQ(TIM3_IRQn);
NVIC_EnableIRQ(TIM2_IRQn);
FLASH_Lock();
return status;
}
__attribute__((section(".code_in_ram"))) uint8_t save_patch(uint8_t patch, uint8_t bank)> {
NVIC_DisableIRQ(TIM2_IRQn);
NVIC_DisableIRQ(TIM3_IRQn);
uint8_t status;
FLASH_Unlock();
patch=patch&63;
bank=bank&3;
// make a copy of the target 2K bank of flash then push the operating patch into the correct position within
// first, find the base address of the 2K block wherein the patch resides
uint32_t fAddress = (FLASH_PATCHES+(PATCHSIZE*(patch+(bank*64))))&FLASH_2K_MASK;
// second, copy whole 2K block into FlashBank
FlashBank = *((flash_t *) fAddress);
// third, copy operating patch into correct position within FlashBank
FlashBank.data[patch&7] = operPatch;
// NOTE: FLASH_ErasePage erases an entire 2K flash bank
if ((status=FLASH_ErasePage(fAddress)) == FLASH_COMPLETE) {
uint32_t *pdata = (uint32_t*) &FlashBank; // pointer to source data
uint32_t len = (sizeof(FlashBank) / 4) + 1; // number of words to program
while (len) {
if ((status=FLASH_ProgramWord(fAddress, *pdata)) == FLASH_COMPLETE) {
fAddress += 4;
pdata++;
len--;
} else break;
}
}
NVIC_EnableIRQ(TIM3_IRQn);
NVIC_EnableIRQ(TIM2_IRQn);
FLASH_Lock();
return status;
}
S2 Step 5 - Tell the Linker to Locate the New ISR Vector Table in SRAM
Finally, one more task to complete the whole solution. We need to modify the linker script once more, to place the new interrupt vector table array into static RAM during the linking process.
Here is the linker script with the ".isr_vector_ram" location for the vector array variable placed just above the functions that we placed into SRAM.
/* Initialized data sections go into SRAM, load LMA copy after code */
.data : AT ( _sidata )
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
*(.code_in_ram*) /* added section for functions that must be in SRAM */
. = ALIGN(128);
*(.isr_vector_ram) /* add our own vector table (which will be a copy) here */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM
Finished - Job Well Done!
At this point, I was able to compile and link the code, write the binary to the STM32, boot up and... it all worked! I can celebrate a bit that it does actually work because the code can now write to flash memory without missing out on any precious interrupts!
Which means I can go back to writing actual useful code that actually does something "useful"- like make music!