mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(examples): add cache access counters example
This commit is contained in:
committed by
Ivan Grokhotkov
parent
16078650c8
commit
744f11fd63
@@ -5,6 +5,22 @@ examples/system/base_mac_address:
|
||||
- *common_components
|
||||
- esp_hw_support
|
||||
|
||||
examples/system/cache_counters:
|
||||
enable:
|
||||
- if: SOC_CACHE_CNT_SUPPORTED == 1
|
||||
disable:
|
||||
- if: CONFIG_NAME == "psram" and SOC_SPIRAM_SUPPORTED != 1
|
||||
disable_test:
|
||||
- if: IDF_TARGET == "esp32h21"
|
||||
temporary: true
|
||||
reason: lack of runners
|
||||
- if: CONFIG_NAME == "default" and SOC_SPIRAM_SUPPORTED == 1
|
||||
reason: the psram config subsumes the default config
|
||||
depends_components:
|
||||
- esp_hw_support
|
||||
- hal
|
||||
- soc
|
||||
|
||||
examples/system/deep_sleep:
|
||||
disable:
|
||||
- if: SOC_DEEP_SLEEP_SUPPORTED != 1
|
||||
|
||||
8
examples/system/cache_counters/CMakeLists.txt
Normal file
8
examples/system/cache_counters/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(cache_counters)
|
||||
69
examples/system/cache_counters/README.md
Normal file
69
examples/system/cache_counters/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S31 |
|
||||
| ----------------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- |
|
||||
|
||||
# Cache Access Counters Example
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
This example shows how to measure cache hit/miss statistics using the cache access counters present on chips with `SOC_CACHE_CNT_SUPPORTED`, via the `esp_cache_cnt.h` API (`esp_hw_support` component).
|
||||
|
||||
The API is chip-agnostic. Each chip defines a list of counter "units" — one unit is the set of counters observing one traffic stream, for example instruction fetches from core 0 into the L1 cache. The typical flow, which this example follows, is:
|
||||
|
||||
1. `esp_cache_cnt_start()` — clear and enable all counters.
|
||||
2. Run the code to be measured.
|
||||
3. `esp_cache_cnt_stop()` — disable the counters, so that reading out and reporting the results is not counted as well.
|
||||
4. `esp_cache_cnt_dump(NULL)` — print a table of all counter values; or, for programmatic access, enumerate the units with `esp_cache_cnt_num_units()` / `esp_cache_cnt_get_unit_info()` and read them with `esp_cache_cnt_get()`.
|
||||
|
||||
For each unit, the hardware counts completed accesses, miss stall events, requester conflicts, and lines transferred to/from the next level of the memory hierarchy. The number of line fills is the true miss count, so the miss ratio of a unit is `line_fills / accesses` (available as `esp_cache_cnt_miss_ratio()`). See the `esp_cache_cnt.h` documentation for the exact semantics of each counter.
|
||||
|
||||
The example runs the same read loop over buffers with different sizes and placements, and prints the counter values after each run. The working set determines which level of the memory hierarchy serves the accesses, which is visible in the counter values. The set of workloads depends on the target.
|
||||
|
||||
## How to use example
|
||||
|
||||
### Hardware Required
|
||||
|
||||
Any development board with a supported chip. To run the PSRAM workloads, the board must have PSRAM.
|
||||
|
||||
### Build and Flash
|
||||
|
||||
```
|
||||
idf.py set-target <target>
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
## Example Output
|
||||
|
||||
Output for the ESP32-P4 (a chip with a two-level cache hierarchy):
|
||||
|
||||
```
|
||||
Workload: flash rodata, 128 KB x 10 passes
|
||||
unit accesses line fills writebacks conflicts miss rate
|
||||
l1-icache-core0 132724 45 - 0 0.03%
|
||||
l1-icache-core1 2792 13 - 0 0.47%
|
||||
l1-dcache-core0 20714 20481 307 7 98.88%
|
||||
l1-dcache-core1 374 25 0 24606 6.68%
|
||||
l2-cache-inst 8 1 - 0 12.50%
|
||||
l2-cache-data 40960 1025 0 0 2.50%
|
||||
|
||||
Workload: PSRAM, 192 KB x 20 passes (fits in L2)
|
||||
unit accesses line fills writebacks conflicts miss rate
|
||||
l1-icache-core0 379266 24 - 0 0.01%
|
||||
l1-icache-core1 0 0 - 0 0.00%
|
||||
l1-dcache-core0 61674 60747 263 0 98.50%
|
||||
l1-dcache-core1 0 0 0 0 0.00%
|
||||
l2-cache-inst 3 1 - 0 33.33%
|
||||
l2-cache-data 121880 1152 1152 0 0.95%
|
||||
|
||||
Workload: PSRAM, 2 MB x 5 passes (exceeds L2)
|
||||
unit accesses line fills writebacks conflicts miss rate
|
||||
l1-icache-core0 1107734 4 - 0 0.00%
|
||||
l1-icache-core1 9302 25 - 0 0.27%
|
||||
l1-dcache-core0 164746 163921 167 154 99.50%
|
||||
l1-dcache-core1 1870 125 0 10600 6.68%
|
||||
l2-cache-inst 2 0 - 0 0.00%
|
||||
l2-cache-data 327680 80384 2481 0 24.53%
|
||||
|
||||
Cache counters example done
|
||||
```
|
||||
3
examples/system/cache_counters/main/CMakeLists.txt
Normal file
3
examples/system/cache_counters/main/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "cache_counters_example_main.c"
|
||||
PRIV_REQUIRES esp_psram
|
||||
INCLUDE_DIRS "")
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
|
||||
/*
|
||||
* Cache access counters example.
|
||||
*
|
||||
* Runs several memory access workloads with a known cache footprint and uses
|
||||
* the esp_cache_cnt API to show how the cache responds. Which workloads run
|
||||
* depends on the target: reading flash goes through the cache on every chip,
|
||||
* internal SRAM only on chips with SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE, and
|
||||
* the PSRAM workloads need PSRAM. Where there are two cache levels, the PSRAM
|
||||
* working sets are sized against both of them; where there is one, against
|
||||
* that one cache.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_memory_utils.h"
|
||||
#include "esp_cache_cnt.h"
|
||||
#include "hal/cache_hal.h"
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
/* Read-only data, so it is placed in .rodata and read from flash through the
|
||||
* cache. Larger than the data cache of any current chip, so that every pass
|
||||
* has to fetch the lines from flash again. */
|
||||
#define FLASH_BUF_SIZE (128 * 1024)
|
||||
static const uint32_t s_flash_buf[FLASH_BUF_SIZE / sizeof(uint32_t)];
|
||||
|
||||
/* Read one word from every cache line of the buffer, 'passes' times */
|
||||
static uint32_t read_workload(const uint32_t *buf, size_t size_bytes, int passes)
|
||||
{
|
||||
const size_t line_size = cache_hal_get_cache_line_size(1, CACHE_TYPE_DATA);
|
||||
const size_t words_per_line = line_size / sizeof(uint32_t);
|
||||
const volatile uint32_t *p = buf;
|
||||
uint32_t acc = 0;
|
||||
for (int pass = 0; pass < passes; pass++) {
|
||||
for (size_t i = 0; i < size_bytes / sizeof(uint32_t); i += words_per_line) {
|
||||
acc += p[i];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/* Counting is stopped before the results are printed, so that the printing
|
||||
* itself does not show up in the counters. */
|
||||
static void run_phase(const char *name, const uint32_t *buf, size_t size, int passes)
|
||||
{
|
||||
ESP_ERROR_CHECK(esp_cache_cnt_start());
|
||||
read_workload(buf, size, passes);
|
||||
ESP_ERROR_CHECK(esp_cache_cnt_stop());
|
||||
|
||||
printf("Workload: %s\n", name);
|
||||
ESP_ERROR_CHECK(esp_cache_cnt_dump(NULL));
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/* Reading data out of flash always goes through the cache, on every chip. */
|
||||
assert(esp_ptr_in_drom(s_flash_buf));
|
||||
run_phase("flash rodata, 128 KB x 10 passes", s_flash_buf, FLASH_BUF_SIZE, 10);
|
||||
|
||||
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
|
||||
/* Only on chips which have a cache in front of internal memory. Elsewhere
|
||||
* the CPU reaches internal SRAM directly and this workload would produce
|
||||
* no data cache traffic at all. */
|
||||
const size_t internal_size = 8 * 1024;
|
||||
uint32_t *internal_buf = heap_caps_malloc(internal_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
assert(internal_buf != NULL);
|
||||
memset(internal_buf, 0xA5, internal_size);
|
||||
run_phase("internal SRAM, 8 KB x 1000 passes", internal_buf, internal_size, 1000);
|
||||
free(internal_buf);
|
||||
#endif
|
||||
|
||||
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE && CONFIG_SPIRAM
|
||||
/* One PSRAM allocation, used with three different working set sizes:
|
||||
* smaller than the L1 data cache (64 KB), between L1 and L2 size,
|
||||
* and larger than the L2 cache. */
|
||||
const size_t small_size = 48 * 1024;
|
||||
const size_t mid_size = CONFIG_CACHE_L2_CACHE_SIZE * 3 / 8;
|
||||
const size_t big_size = CONFIG_CACHE_L2_CACHE_SIZE * 4;
|
||||
|
||||
uint32_t *psram_buf = heap_caps_malloc(big_size, MALLOC_CAP_SPIRAM);
|
||||
assert(psram_buf != NULL);
|
||||
memset(psram_buf, 0x5A, big_size);
|
||||
|
||||
run_phase("PSRAM, 48 KB x 100 passes (fits in L1)", psram_buf, small_size, 100);
|
||||
run_phase("PSRAM, 192 KB x 20 passes (fits in L2)", psram_buf, mid_size, 20);
|
||||
run_phase("PSRAM, 2 MB x 5 passes (exceeds L2)", psram_buf, big_size, 5);
|
||||
free(psram_buf);
|
||||
#elif CONFIG_SPIRAM
|
||||
/* Single cache level: one working set smaller than the cache and one
|
||||
* larger than it. */
|
||||
const size_t small_size = 8 * 1024;
|
||||
const size_t big_size = 512 * 1024;
|
||||
|
||||
uint32_t *psram_buf = heap_caps_malloc(big_size, MALLOC_CAP_SPIRAM);
|
||||
assert(psram_buf != NULL);
|
||||
memset(psram_buf, 0x5A, big_size);
|
||||
|
||||
run_phase("PSRAM, 8 KB x 500 passes (fits in cache)", psram_buf, small_size, 500);
|
||||
run_phase("PSRAM, 512 KB x 5 passes (exceeds cache)", psram_buf, big_size, 5);
|
||||
free(psram_buf);
|
||||
#endif
|
||||
|
||||
printf("Cache counters example done\n");
|
||||
}
|
||||
147
examples/system/cache_counters/pytest_cache_counters.py
Normal file
147
examples/system/cache_counters/pytest_cache_counters.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
import pytest
|
||||
from pytest_embedded import Dut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
|
||||
# Names of the units observing core 0 instruction and data traffic, per target
|
||||
INST_UNIT = {
|
||||
'esp32c5': 'l1-cache-ibus',
|
||||
'esp32c6': 'l1-cache-ibus',
|
||||
'esp32c61': 'l1-cache-ibus',
|
||||
'esp32h2': 'l1-cache-ibus',
|
||||
'esp32h4': 'l1-cache-inst-core0',
|
||||
'esp32p4': 'l1-icache-core0',
|
||||
'esp32s31': 'l1-cache-inst-core0',
|
||||
}
|
||||
DATA_UNIT = {
|
||||
'esp32c5': 'l1-cache-dbus',
|
||||
'esp32c6': 'l1-cache-dbus',
|
||||
'esp32c61': 'l1-cache-dbus',
|
||||
'esp32h2': 'l1-cache-dbus',
|
||||
'esp32h4': 'l1-cache-data-core0',
|
||||
'esp32p4': 'l1-dcache-core0',
|
||||
'esp32s31': 'l1-cache-data-core0',
|
||||
}
|
||||
|
||||
# The one workload every target runs. Reading .rodata goes through the cache on
|
||||
# every chip, and 128 KB exceeds the data cache everywhere, so each pass has to
|
||||
# fetch the lines from flash again.
|
||||
FLASH_PHASE = 'flash rodata, 128 KB x 10 passes'
|
||||
|
||||
|
||||
def expect_counter_row(dut: Dut, unit: str) -> dict:
|
||||
"""Match one row of the esp_cache_cnt_dump() table, return the counter values.
|
||||
|
||||
Counters which the unit does not provide (printed as '-') are returned as None.
|
||||
"""
|
||||
num = r'(\d+|-)'
|
||||
match = dut.expect(rf'{unit} +{num} +{num} +{num} +{num} +[0-9.]+%')
|
||||
|
||||
def field(idx: int) -> int | None:
|
||||
value = match.group(idx).decode()
|
||||
return None if value == '-' else int(value)
|
||||
|
||||
return {
|
||||
'accesses': field(1),
|
||||
'line_fills': field(2),
|
||||
'writebacks': field(3),
|
||||
'conflicts': field(4),
|
||||
}
|
||||
|
||||
|
||||
def check_phase(dut: Dut, workload: str, nonzero: dict) -> None:
|
||||
"""Expect one 'Workload: ...' line followed by a counter table.
|
||||
|
||||
nonzero maps unit names (in dump order) to the list of counters which
|
||||
must read non-zero after the workload.
|
||||
"""
|
||||
dut.expect_exact(f'Workload: {workload}')
|
||||
for unit, counters in nonzero.items():
|
||||
row = expect_counter_row(dut, unit)
|
||||
for counter in counters:
|
||||
assert row[counter], f'{workload}: expected {unit} {counter} > 0, got {row[counter]}'
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('config', ['default'], indirect=['config'])
|
||||
@idf_parametrize('target', ['esp32c6', 'esp32h2'], indirect=['target'])
|
||||
def test_cache_counters(dut: Dut) -> None:
|
||||
# No PSRAM: only the flash workload runs. Its code is fetched from flash as
|
||||
# well, so the instruction unit counts accesses too.
|
||||
check_phase(
|
||||
dut,
|
||||
FLASH_PHASE,
|
||||
{
|
||||
INST_UNIT[dut.target]: ['accesses'],
|
||||
DATA_UNIT[dut.target]: ['accesses', 'line_fills'],
|
||||
},
|
||||
)
|
||||
dut.expect_exact('Cache counters example done')
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('config', ['psram'], indirect=['config'])
|
||||
@idf_parametrize('target', ['esp32c5', 'esp32c61', 'esp32h4', 'esp32s31'], indirect=['target'])
|
||||
def test_cache_counters_psram(dut: Dut) -> None:
|
||||
inst = INST_UNIT[dut.target]
|
||||
data = DATA_UNIT[dut.target]
|
||||
check_phase(dut, FLASH_PHASE, {inst: ['accesses'], data: ['accesses', 'line_fills']})
|
||||
check_phase(dut, 'PSRAM, 8 KB x 500 passes (fits in cache)', {inst: ['accesses'], data: ['accesses']})
|
||||
# A working set larger than the cache must be fetched from PSRAM on every pass
|
||||
check_phase(dut, 'PSRAM, 512 KB x 5 passes (exceeds cache)', {inst: ['accesses'], data: ['accesses', 'line_fills']})
|
||||
dut.expect_exact('Cache counters example done')
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('config', ['psram'], indirect=['config'])
|
||||
@idf_parametrize('target', ['esp32p4'], indirect=['target'])
|
||||
def test_cache_counters_psram_esp32p4(dut: Dut) -> None:
|
||||
# 128 KB is larger than the L1 data cache but fits in L2, so L1 refills on
|
||||
# every pass while L2 only has to fill once
|
||||
check_phase(
|
||||
dut,
|
||||
FLASH_PHASE,
|
||||
{
|
||||
'l1-icache-core0': ['accesses'],
|
||||
'l1-dcache-core0': ['accesses', 'line_fills'],
|
||||
'l2-cache-data': ['accesses'],
|
||||
},
|
||||
)
|
||||
# Internal SRAM is reached through the L1 cache on this chip, and the whole
|
||||
# working set stays in it, so the L1 data unit counts accesses but no fills
|
||||
check_phase(
|
||||
dut,
|
||||
'internal SRAM, 8 KB x 1000 passes',
|
||||
{
|
||||
'l1-icache-core0': ['accesses'],
|
||||
'l1-dcache-core0': ['accesses'],
|
||||
},
|
||||
)
|
||||
check_phase(
|
||||
dut,
|
||||
'PSRAM, 48 KB x 100 passes (fits in L1)',
|
||||
{
|
||||
'l1-icache-core0': ['accesses'],
|
||||
'l1-dcache-core0': ['accesses'],
|
||||
},
|
||||
)
|
||||
# Larger than the L1 data cache: every pass refills L1 from L2
|
||||
check_phase(
|
||||
dut,
|
||||
'PSRAM, 192 KB x 20 passes (fits in L2)',
|
||||
{
|
||||
'l1-dcache-core0': ['accesses', 'line_fills'],
|
||||
'l2-cache-data': ['accesses'],
|
||||
},
|
||||
)
|
||||
# Larger than the L2 cache: lines are continuously fetched from PSRAM
|
||||
check_phase(
|
||||
dut,
|
||||
'PSRAM, 2 MB x 5 passes (exceeds L2)',
|
||||
{
|
||||
'l1-dcache-core0': ['accesses', 'line_fills'],
|
||||
'l2-cache-data': ['accesses', 'line_fills'],
|
||||
},
|
||||
)
|
||||
dut.expect_exact('Cache counters example done')
|
||||
1
examples/system/cache_counters/sdkconfig.ci.default
Normal file
1
examples/system/cache_counters/sdkconfig.ci.default
Normal file
@@ -0,0 +1 @@
|
||||
# Build with sdkconfig.defaults only (no PSRAM)
|
||||
1
examples/system/cache_counters/sdkconfig.ci.psram
Normal file
1
examples/system/cache_counters/sdkconfig.ci.psram
Normal file
@@ -0,0 +1 @@
|
||||
CONFIG_SPIRAM=y
|
||||
2
examples/system/cache_counters/sdkconfig.defaults
Normal file
2
examples/system/cache_counters/sdkconfig.defaults
Normal file
@@ -0,0 +1,2 @@
|
||||
# No target-independent default options; per-target options are in
|
||||
# sdkconfig.defaults.<target>. This file must exist for those to be applied.
|
||||
@@ -0,0 +1,2 @@
|
||||
CONFIG_CACHE_L2_CACHE_512KB=y
|
||||
CONFIG_CACHE_L2_CACHE_LINE_128B=y
|
||||
Reference in New Issue
Block a user