Hello everyone, I am Pi Zi Heng, a serious technical person. Today, I will introduce to you the actual capture of Flash signal waveforms to observe the AHB read access situation under the i.MXRT FlexSPI peripheral.
In the previous article “Actual Capture of Flash Signal Waveforms to Observe AHB Read Access under i.MXRT FlexSPI Peripheral (With Prefetch)”, I captured the timing waveform of Flash end corresponding to AHB read access with Cache off but Prefetch on. We found that the Prefetch function of FlexSPI does improve Flash access efficiency to some extent. However, the maximum AHB RX Buffer is only 1KB (for i.MXRT1050), and it cannot be split into smaller granularity Buffers to cache data from different Flash addresses (for the same AHB master). Therefore, for repeated accesses to multiple different small data blocks in the Flash space in the code, the Prefetch mechanism does not significantly improve access efficiency.
To address this inefficient situation of frequently accessing non-contiguous Flash address space, the ARM Cortex-M7 core provides a solution, which is the L1 Cache technology. Today, I will continue to test the AHB read access situation of Flash with L1 Cache enabled (this article mainly focuses on D-Cache):
1. Cortex-M7 Cache Function
For the Cortex-M series family (M0+/M3/M4/M7/M23/M33/M35P/M55), L1 Cache only exists on the Cortex-M7 and Cortex-M55 cores. In simple terms, L1 Cache is configured specifically for high-performance cores, and the current i.MXRT1xxx series microcontrollers are all based on the Cortex-M7 core.
Below is the core system block diagram of i.MXRT1050, which shows that it integrates a 32KB D-Cache. The Cache is connected to the SIM_M7 and SIM_EMS modules via the AXI64 bus, which is ultimately connected to the FlexSPI module via the AHB bus. Therefore, the AHB read access to Flash can be accelerated by the D-Cache.

Details about the D-Cache working mechanism can be found in the ARM Cortex-M7 Processor Technical Reference Manual. In short, the 32KB D-Cache is divided into 1024 Cache Lines, each Cache Line is 32 bytes in size, and four Cache Lines form a set (i.e., the so-called 4-way set associative). Each set of Cache Lines has an address tag that records the target address information where the cached data resides.
When L1 D-Cache is enabled, there are two main types of AHB read accesses to the target memory: Hit (the data to be accessed is in the Cache) and Miss (the data to be accessed is not in the Cache). For Hit, there is nothing more to say; just get the data directly from the Cache. For Miss, the data will first be read from the target memory into the Cache, and then the data will be read from the Cache (this is called Read-Allocate; there is actually another term, Read-Through, which corresponds to it. Read-Through means reading data directly from the target memory, which is generally the behavior when Cache is not enabled).
The Cache strategy control for the target address space is mainly through attribute configuration (in the kernel MPU module) and switch control (in the kernel SCB module). The following BOARD_ConfigMPU() function is a typical Cache attribute configuration for the Flash region allocated to the FlexSPI address mapping space. In this code, the 64MB space starting from 0x60000000 is configured as Normal Memory, non-shareable, Cache enabled, and the write access behavior is Write-Back (there is another write access strategy called Write-Through). The read access behavior does not need configuration (fixed Read-Allocate).
/* MPU configuration. */
void BOARD_ConfigMPU(void)
{
/* Disable I cache and D cache */
SCB_DisableICache();
SCB_DisableDCache();
/* Disable MPU */
ARM_MPU_Disable();
/* Region 0 setting: Instruction access disabled, No data access permission. */
MPU->RBAR = ARM_MPU_RBAR(0, 0x00000000U);
MPU->RASR = ARM_MPU_RASR(1, ARM_MPU_AP_NONE, 2, 0, 0, 0, 0, ARM_MPU_REGION_SIZE_4GB);
/* Region 2 setting: Memory with Device type, not shareable, non-cacheable. */
MPU->RBAR = ARM_MPU_RBAR(2, 0x60000000U);
MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_FULL, 2, 0, 0, 0, 0, ARM_MPU_REGION_SIZE_512MB);
#if defined(XIP_EXTERNAL_FLASH) && (XIP_EXTERNAL_FLASH == 1)
/* Region 3 setting: Memory with Normal type, not shareable, cacheable, outer/inner write back. */
MPU->RBAR = ARM_MPU_RBAR(3, 0x60000000U);
MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_RO, 0, 0, 1, 1, 0, ARM_MPU_REGION_SIZE_64MB);
#endif
/* Enable MPU */
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
/* Enable I cache and D cache */
SCB_EnableDCache();
SCB_EnableICache();
}
Lastly, I would like to mention the write access behavior strategy under Cache enablement, which is not related to the theme of this article:
(In the case of Hit) Write-Through mode: Write directly to the target memory and also update in the Cache (no data consistency issues caused by multiple Masters accessing, but no improvement in write access performance). (In the case of Hit) Write-Back mode: The Cache line will be marked as dirty, and the actual write operation will only occur when this line is invalidated, writing the data in the Cache Line back to the target memory. (This improves write access performance, but there is a risk; if the Cache hits, at this time only the Cache is updated, and the target memory is not updated, the data read by other Masters from the target memory is incorrect). (In the case of Miss) Write-Allocate: Load the data to be written into the Cache first, and then flush it into the target memory. (In the case of Miss) no-Write-Allocate: Write directly to the target memory.
2. D-Cache Experiment Preparation
Refer to the article “Actual Capture of Flash Signal Waveforms to Observe AHB Read Access under i.MXRT FlexSPI Peripheral (No Cache)” for the first section Experiment Preparation. The same preparation work is required for this experiment.
3. D-Cache Experiment Code
Refer to the article “Actual Capture of Flash Signal Waveforms to Observe AHB Read Access under i.MXRT FlexSPI Peripheral (No Cache)” for the second section Experiment Code. The project and link file settings for this experiment code are the same, but the specific test function is changed to the following ramfunc type function test_cacheable_read(). There will be many different tests regarding D-Cache this time; the system configuration before the while(1) statement remains unchanged, while the statements inside while(1) can be adjusted according to the actual test situation:
#if (defined(__ICCARM__))
#pragma optimize = none
__ramfunc
#endif
void test_cacheable_read(void)
{
// System configuration
/* Disable L1 I-Cache*/
SCB_DisableICache();
/* Enable L1 D-Cache*/
SCB_EnableDCache();
SCB_CleanInvalidateDCache();
// Enable/disable FlexSPI's Prefetch feature as needed
while (1)
{
// Test case code, can be adjusted as needed
}
}
To help analyze the results of this series of test cases, we need to expand the special const data area .ahbRdBuffer as follows:
const uint8_t ahbRdBlock1[1024] @ ".ahbRdBuffer1" = {
// Sequential order
0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13,
0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33,
// Reverse order
0x33, 0x32, 0x31, 0x30, 0x23, 0x22, 0x21, 0x20,
0x13, 0x12, 0x11, 0x10, 0x03, 0x02, 0x01, 0x00,
// Sequential insert order
0x01, 0x00, 0x03, 0x02, 0x11, 0x10, 0x13, 0x12,
0x21, 0x20, 0x23, 0x22, 0x31, 0x30, 0x33, 0x32,
// Reverse insert order
0x32, 0x33, 0x30, 0x31, 0x22, 0x23, 0x20, 0x21,
0x12, 0x13, 0x10, 0x11, 0x02, 0x03, 0x00, 0x01,
};
const uint8_t ahbRdBlock2[1024] @ ".ahbRdBuffer2" = {
// Reverse insert order
0x32, 0x33, 0x30, 0x31, 0x22, 0x23, 0x20, 0x21,
0x12, 0x13, 0x10, 0x11, 0x02, 0x03, 0x00, 0x01,
// Sequential insert order
0x01, 0x00, 0x03, 0x02, 0x11, 0x10, 0x13, 0x12,
0x21, 0x20, 0x23, 0x22, 0x31, 0x30, 0x33, 0x32,
// Reverse order
0x33, 0x32, 0x31, 0x30, 0x23, 0x22, 0x21, 0x20,
0x13, 0x12, 0x11, 0x10, 0x03, 0x02, 0x01, 0x00,
// Sequential order
0x00, 0x01, 0x02, 0x03, 0x10, 0x11, 0x12, 0x13,
0x20, 0x21, 0x22, 0x23, 0x30, 0x31, 0x32, 0x33,
};
// In the project link file
keep{ section .ahbRdBuffer1, section .ahbRdBuffer2 };
place at address mem:0x60002400 { readonly section .ahbRdBuffer1 };
place at address mem:0x60002800 { readonly section .ahbRdBuffer2 };
4. D-Cache Experiment Results
4.1 Repeating Experiments from the No Cache Article
Now, let’s redo all the experiments in the article “Actual Capture of Flash Signal Waveforms to Observe AHB Read Access under i.MXRT FlexSPI Peripheral (No Cache)” with D-Cache enabled:
#define AHB_ADDR_START (0x60002400)
void test_cacheable_read(void)
{
// Omit system configuration (I-Cache, Prefetch off, D-Cache on)
while (1)
{
SDK_DelayAtLeastUs(10, SystemCoreClock);
for (uint32_t i = 1; i <= 8; i++)
{
SDK_DelayAtLeastUs(2, SystemCoreClock);
memcpy((void *)0x20200000, (void *)AHB_ADDR_START, i);
}
}
}
4.1.1 AHB_ADDR_START Value Range [0x60002400 – 0x60002418]
When the AHB_ADDR_START value is within the range [0x60002400 – 0x60002418], the timing waveform at the Flash end is the same as shown below. Because of the D-Cache, we no longer see periodic CS signals, indicating that aside from the fact that accessing new addresses in Flash must go through the FlexSPI peripheral, all subsequent repeated accesses to the same Flash address occur directly in the D-Cache.
Additionally, D-Cache always caches data at addresses that are 32-byte aligned and caches 32 bytes of data at a time (since the D-Cache Line size is 32 bytes), so in the waveform results, the starting address is always 0x60002400, and reading 32 bytes of data (which resides in one D-Cache Line) means that the waveform differences caused by the AHB Burst Read strategy when D-Cache and Prefetch are off do not exist here.

4.1.2 AHB_ADDR_START = 0x60002419
When the actual code reads Flash data that spans two adjacent 32-byte aligned data blocks (0x60002400 – 0x6000241f, 0x60002420 – 0x6000243f), two valid CS signals will appear at the Flash end, each transmitting 32 bytes of data. The D-Cache continues to function, and this time two D-Cache Lines are utilized (since the total size of D-Cache is 32KB, with 1024 Cache Lines). Therefore, we still do not see periodic CS signals at the Flash end.

4.1.3 Additional Experiment, Reading 1KB from 0x60002400
When the code continuously reads 1KB of data, the waveform shows 32 valid CS signals, each transmitting 32 bytes of data, totaling 1KB of data transmission. This time, the D-Cache dispatched 32 Cache Lines, and we still do not see periodic CS signals at the Flash end.

4.2 Repeating Experiments from the Prefetch Article
Now, let’s redo all the experiments in the article “Actual Capture of Flash Signal Waveforms to Observe AHB Read Access under i.MXRT FlexSPI Peripheral (With Prefetch)” with D-Cache enabled:
4.2.1 Loop Reading Any Length Data Block Within 1KB of 32-byte Aligned Starting Address, Starting Copy Address Located Within the First 31 Bytes
In this case, the actual waveform at the Flash end is similar to the test results in section 4.1 of the article “Actual Capture of Flash Signal Waveforms to Observe AHB Read Access under i.MXRT FlexSPI Peripheral (With Prefetch)”; therefore, I will not include the image here. The Prefetch mechanism serves as the first layer of caching, while the D-Cache retrieves results from the Prefetch Buffer as a secondary cache. The only difference is that due to the existence of the D-Cache, the starting cache address may change (from 8-byte alignment to 32-byte alignment):
#define PREFETCH_TEST_ALIGNMENT (7) // Can take values 0 - 31
#define PREFETCH_TEST_START (0x60002400 + PREFETCH_TEST_ALIGNMENT)
uint32_t testLen = 0x1; // Can take values 1 - (1KB-PREFETCH_TEST_ALIGNMENT)
void test_cacheable_read(void)
{
// Omit system configuration (I-Cache off, Prefetch on, D-Cache on)
while (1)
{
memcpy((void *)0x20200000, (void *)PREFETCH_TEST_START, testLen);
}
}
4.2.2 Loop Reading Data Blocks Greater Than 1KB or 1KB Data Blocks with Non-32-byte Aligned Starting Address
In this case, there will be two complete 1KB Prefetch operations at the Flash end: the first Prefetch operation reads 1KB from 0x60002400, and the second Prefetch operation reads 1KB from 0x60002800. With the presence of D-Cache, the second Prefetch operation has enough time to complete without needing to insert additional software delays to avoid being interrupted by the next access demand coming back from the while(1) loop:
void test_cacheable_read(void)
{
// Omit system configuration (I-Cache off, Prefetch on, D-Cache on)
while (1)
{
memcpy((void *)0x20200001, (void *)0x60002401, 0x400);
}
}

4.2.3 Loop Reading Two Different Data Blocks (Within Two Different 1KB Spaces with 32-byte Aligned Starting Address)
In this case, even with the presence of D-Cache, the Prefetch operation during the first CS period (i.e., memcpy((void *)0x20200000, (void *)0x60002400, 0x100);) is still interrupted by the second CS’s Prefetch operation (i.e., memcpy((void *)0x20200400, (void *)0x60002800, 0x100);). However, the Prefetch operation during the second CS period will not be interrupted again because the subsequent Flash data access demand coming back from the while(1) loop has already been cached in the D-Cache:
void test_cacheable_read(void)
{
// Omit system configuration (I-Cache off, Prefetch on, D-Cache on)
while (1)
{
memcpy((void *)0x20200000, (void *)0x60002400, 0x100);
memcpy((void *)0x20200400, (void *)0x60002800, 0x100);
}
}

4.3 How to See Periodic CS Signals with D-Cache Enabled
After testing so many situations, is it possible for us to see periodic CS signals at the Flash end, meaning that Flash is continuously being read? Of course, we can. We know that the total size of D-Cache is 32KB; as long as we continuously copy more than 32KB of data, the D-Cache will start to overflow. Below, this code will allow us to see the long-lost periodic timing waveform (be careful, continuous operation of Flash will consume more power, haha).
void test_cacheable_read(void)
{
// Omit system configuration (I-Cache off, Prefetch on, D-Cache on)
while (1)
{
memcpy((void *)0x20200000, (void *)0x60002400, (0x8000 + 1));
}
}

Thus, the actual capture of Flash signal waveforms to observe the AHB read access situation under the i.MXRT FlexSPI peripheral has been introduced by Pi Zi Heng. Where is the applause~~~