diff --git a/docs/PolarFire.md b/docs/PolarFire.md index a8ca7c3c6f..b0ac9a1aa9 100644 --- a/docs/PolarFire.md +++ b/docs/PolarFire.md @@ -1225,6 +1225,31 @@ The AES offload is worth about 4.2x on the decrypt. **The hash offload is about Athena also does not fit eNVM alongside the full feature set: with the manifest linked it needs both `GZIP` and `MPFS_SBI_SC_FLASH` dropped. Dropping `GZIP` costs nothing when the FIT's kernel is stored uncompressed. +The port also serves AES-256-ECB, which is how AES-256-XTS reaches the engine: wolfCrypt's XTS masks the whole buffer with the tweak chain and issues one bulk ECB call per XTS call (`HAVE_AES_ECB` required, distinct in/out buffers), while the tweak math stays in software. wolfBoot itself encrypts images with CTR and has no XTS use; the ECB path is for an application. Requests under `WC_MPFS_ATHENA_AES_MIN_SZ` (32 bytes) are declined to software, which keeps the XTS tweak and ciphertext-stealing blocks off the engine. See `wolfcrypt/src/port/microchip/README.md` in wolfSSL. + +#### Measuring the offload bare-metal + +The test application doubles as a bare-metal wolfCrypt test and benchmark harness for the engine, in the same M-mode environment wolfBoot uses, so its figures compare with the software build of the same app and not with a Linux userspace benchmark: + +```sh +cp config/examples/polarfire_mpfs250_m_qspi.config .config +make clean +make MPFS_ATHENA=1 MPFS_ATHENA_CAL_DIR=/path/to/hart-software-services/services/crypto \ + WOLFCRYPT_BENCHMARK=1 WOLFBOOT_L2SCRATCH_SIZE=512k +``` + +Load `test-app/image_v1_signed.bin` with `mpfs_qspi_prog.py` as for any M-mode test app. The benchmark app is about 240 KB and is loaded at `0x0A020200`, so the bootloader's stack has to sit at the top of all four scratchpad ways rather than the default 256 KB, which is what `WOLFBOOT_L2SCRATCH_SIZE=512k` does; `test-app/RISCV64-mpfs250.ld` reserves 64 KB for the app's own stack above that, sized for the benchmark's SP math frames. The `WOLFCRYPT_TEST=1` app is about 370 KB and does not fit the scratchpad beside the bootloader: build it on `polarfire_mpfs250_m_qspi_ddr.config` with its `-DWOLFBOOT_MMODE_SMODE_BOOT` line removed, which loads the signed app into DDR and enters it with the M-mode direct jump, and with `NO_ASM=1`, since the test-app's RISC-V SHA assembly does not assemble against the test configuration. `MPFS_ATHENA=1` makes the app bring the engine up itself, so the bootloader it runs under may be built with or without the offload. The app registers the port, runs its known-answer self-test and prints `Athena: self-test passed, engine served N call(s)`; the stock `wolfcrypt_test()` and `benchmark_test()` then drive the engine through `WC_USE_DEVID`, with the benchmark labelling offloaded results `HW`. The benchmark runs once per call size in `MPFS_BENCH_SIZES` (default 1 KiB, 512 B and 4 KiB; each needs twice that from `WOLFSSL_STATIC_MEMORY_TEST_SZ`), since one engine transfer per call amortises very differently at sector size and in bulk. The app ends with the total engine call count: a passing test or a plausible rate proves nothing about offload on its own, because every case the port declines runs in software. The same app built without `MPFS_ATHENA` is the software baseline. Object files are configuration-specific, so `make clean` between the two builds. + +Measured on the Video Kit with the DDR configuration (E51 at 600 MHz), the software column being wolfCrypt's default C AES for RISC-V: + +| Call size | AES-256-XTS software | AES-256-XTS Athena | AES-256-CTR Athena | SHA-384 software | SHA-384 Athena | +|---|---|---|---|---|---| +| 512 B | 48 KiB/s | 1.23 MiB/s | 12.9 MiB/s | 5.7 MiB/s | 11.4 MiB/s | +| 1 KiB | 49 KiB/s | 2.06 MiB/s | 14.6 MiB/s | 5.8 MiB/s | 13.2 MiB/s | +| 4 KiB | 49 KiB/s | 4.2 MiB/s | 16.1 MiB/s | 5.8 MiB/s | 14.8 MiB/s | + +AES-128-XTS stays in software at about 68 KiB/s. The app keeps the five MSS watchdogs refreshed from a machine timer interrupt every 5 s, since a test or benchmark pass runs far longer than their window, and its M-mode trap handler prints the cause and halts instead of resuming at the faulting instruction. The RISC-V harts trap on misaligned accesses, so `user_settings.h` defines `WOLFSSL_USE_ALIGN` for `ARCH_RISCV64`; the AES-GCM tests and benchmark also need the wolfSSL change that takes the byte-wise GCM table multiply under that define on 64-bit builds. + ## Source files `hal/mpfs250.c` - Hardware abstraction layer (UART, QSPI, SD/eMMC, multi-hart) diff --git a/docs/Targets.md b/docs/Targets.md index ac736baf21..112cdb1566 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -909,7 +909,7 @@ Make variables, on the command line or in the `.config`. See [PolarFire.md](Pola | Option | Purpose | |--------|---------| | `LIBERO_FPGA_CONFIG_DIR` | Path to the board's Libero/HSS `fpga_design_config` headers. Enables LPDDR4 init (`MPFS_DDR_INIT`); required for the M-Mode + DDR build. | -| `MPFS_ATHENA` (+ `MPFS_ATHENA_CAL_DIR`) | SHA-384 and AES-256-CTR offload to the Athena F5200 (TeraFire), with a mandatory self-check. | +| `MPFS_ATHENA` (+ `MPFS_ATHENA_CAL_DIR`) | SHA-384, AES-256-CTR and AES-256-ECB (AES-XTS) offload to the Athena F5200 (TeraFire), with a mandatory self-check. With `WOLFCRYPT_TEST=1` or `WOLFCRYPT_BENCHMARK=1` the test-app also drives the engine. | | `SNVM_KEYSTORE`, `SNVM_KEYSTORE_PROVISION`, `SNVM_KEYSTORE_REQUIRE_ROM` | Serve the verification keystore from secure NVM; the one-time writer; refuse to boot unless every keystore page is ROM-flagged. | | `SNVM_KEK`, `SNVM_ENCKEY_PROVISION` | PUF-derived KEK with RFC 3394 key-wrap for the sNVM-stored image-encryption key, and its one-time writer. | | `MPFS_SCB_SMODE_DENY` | Fence S-mode off the System Controller mailbox (PMP plus dtb node removal), so nothing after wolfBoot can reach the sNVM write service. | diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 545565f5c3..4464e3cd82 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -145,10 +145,6 @@ extern uint8_t _main_hart_hls; /* linker-provided address symbol; typed as uint8 /* CLINT MSIP register for IPI delivery */ #define CLINT_MSIP_REG(hart) (*(volatile uint32_t*)(CLINT_BASE + (hart) * 4)) -/* CLINT machine-timer comparator (per hart) and MTIME counter */ -#define CLINT_MTIMECMP_REG(hart) \ - (*(volatile uint64_t*)(CLINT_BASE + 0x4000UL + (hart) * 8UL)) -#define CLINT_MTIME_REG (*(volatile uint64_t*)(CLINT_BASE + 0xBFF8UL)) /* Signal secondary harts that E51 (main hart) is ready. */ static void mpfs_signal_main_hart_started(void) @@ -216,10 +212,10 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls) } } -#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#ifdef WOLFBOOT_RISCV_MMODE /* Enable the CLINT MTIME counter via the SYSREG RTC clock divider. Without it * MTIME never advances and an S-mode OS has no scheduler tick. */ -static void mpfs_enable_mtime(void) +void mpfs_enable_mtime(void) { volatile uint32_t *rtc_cr = (volatile uint32_t *)(SYSREG_BASE + 0x0CUL); uint32_t div; @@ -232,7 +228,9 @@ static void mpfs_enable_mtime(void) *rtc_cr |= (1UL << 16); /* enable RTC/time-base clock */ __asm__ volatile("fence iorw, iorw" ::: "memory"); } +#endif /* WOLFBOOT_RISCV_MMODE */ +#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) /* SBI HSM hart_start backend: stage the target hart's mailbox and ring its * MSIP; it enters S-mode at saddr with a0=hartid, a1=opaque. */ int sbi_hal_hart_start(unsigned long hartid, unsigned long saddr, diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 01dcdd8fe7..3e5961f542 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -388,6 +388,10 @@ int mpfs_nonce(uint8_t *nonce); #ifndef CLINT_BASE #define CLINT_BASE 0x02000000UL #endif +/* Machine-timer comparator (per hart) and the MTIME counter. */ +#define CLINT_MTIMECMP_REG(hart) \ + (*(volatile uint64_t*)(CLINT_BASE + 0x4000UL + (hart) * 8UL)) +#define CLINT_MTIME_REG (*(volatile uint64_t*)(CLINT_BASE + 0xBFF8UL)) /* RTC Clock Frequency (1 MHz after divisor) */ #define RTC_CLOCK_FREQ 1000000UL @@ -472,6 +476,7 @@ void uart_write(const char* buf, unsigned int sz); #endif #ifdef WOLFBOOT_RISCV_MMODE void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); +void mpfs_enable_mtime(void); #endif #endif /* __ASSEMBLER__ */ diff --git a/include/user_settings.h b/include/user_settings.h index 9b8d17b952..77d2f21c1f 100644 --- a/include/user_settings.h +++ b/include/user_settings.h @@ -596,6 +596,14 @@ extern int tolower(int c); # endif #endif +/* The RISC-V harts trap on a misaligned load or store with nothing to emulate + * it bare-metal, so make wolfCrypt's ALIGN* attributes real: without this the + * word-sized casts it makes on struct members (the AES-GCM table, for one) + * fault. */ +#ifdef ARCH_RISCV64 +# define WOLFSSL_USE_ALIGN +#endif + #if defined(EXT_ENCRYPTED) # define HAVE_PWDBASED #endif @@ -764,7 +772,9 @@ extern int tolower(int c); #define WC_NO_HASHDRBG #define CUSTOM_RAND_GENERATE_SEED my_rng_seed_gen #define CUSTOM_RAND_GENERATE_BLOCK my_rng_seed_gen + #ifndef __ASSEMBLER__ extern int my_rng_seed_gen(unsigned char* output, unsigned int sz); + #endif /* Full test/benchmark algo set: AES ECB/CBC/CTR/GCM/XTS, * SHA-256/384/512, SHA-3. (AES-CBC/GCM added below; SHA-384/512 @@ -919,7 +929,17 @@ extern int tolower(int c); #define WOLFSSL_STATIC_MEMORY #endif #ifndef WOLFSSL_STATIC_MEMORY_TEST_SZ - #define WOLFSSL_STATIC_MEMORY_TEST_SZ (10 * 1024) + #if defined(TARGET_mpfs250) && defined(MPFS_DDR_INIT) + /* Every default bucket, including the two above 4 KB that the + * benchmark's 4 KB buffers and the test's larger contexts need; + * a pool without them hands out something else. Only with DDR: + * the L2 scratchpad app has no room for it. */ + #define WOLFSSL_STATIC_MEMORY_TEST_SZ (128 * 1024) + #elif defined(TARGET_mpfs250) + #define WOLFSSL_STATIC_MEMORY_TEST_SZ (64 * 1024) + #else + #define WOLFSSL_STATIC_MEMORY_TEST_SZ (10 * 1024) + #endif #endif #define WOLFSSL_STATIC_MEMORY_LEAN @@ -929,7 +949,9 @@ extern int tolower(int c); /* User time functions provided */ #define WOLFSSL_USER_CURRTIME #define XTIME my_time + #ifndef __ASSEMBLER__ extern unsigned long my_time(unsigned long* timer); + #endif #endif #if !defined(WOLFCRYPT_TEST) && !defined(WOLFCRYPT_BENCHMARK) diff --git a/test-app/Makefile b/test-app/Makefile index 9b96d45d87..57057f1191 100644 --- a/test-app/Makefile +++ b/test-app/Makefile @@ -268,6 +268,18 @@ ifeq ($(SEC_QORIQ),1) APP_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/port/nxp/sec_qoriq_baremetal.o endif +# PolarFire SoC Athena F5200. options.mk already supplies the defines, the CAL +# include path and the archive in LIBS; the port objects are added here because +# the apps build their own wolfCrypt object list. +ifeq ($(MPFS_ATHENA),1) + ifeq ($(WOLFCRYPT_TEST)$(WOLFCRYPT_BENCHMARK),) + $(warning MPFS_ATHENA=1 without WOLFCRYPT_TEST or WOLFCRYPT_BENCHMARK only runs the engine self-test in the test-app) + endif + WOLFCRYPT_SUPPORT=1 + APP_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/cryptocb.o + APP_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/port/microchip/mpfs_athena.o +endif + ifeq ($(WOLFCRYPT_TEST),1) CFLAGS+=-DWOLFCRYPT_TEST WOLFCRYPT_SUPPORT=1 @@ -1530,6 +1542,13 @@ endif ifeq ($(TARGET),mpfs250) LSCRIPT_TEMPLATE:=RISCV64-mpfs250.ld CFLAGS+=-DRAMFUNCTION='__attribute__((used,section(".ramcode")))' + # The wolfCrypt test and benchmark need far more stack than the plain app, + # which the L2 scratchpad configurations have no room to reserve. + ifneq ($(filter 1,$(WOLFCRYPT_TEST) $(WOLFCRYPT_BENCHMARK)),) + TEST_APP_STACK_SIZE?=0x00010000 + else + TEST_APP_STACK_SIZE?=0x00002000 + endif endif # $(CYPRESS_PDL)/devices/templates/COMPONENT_MTB/COMPONENT_CM0P/system_psoc6_cm0plus.o @@ -1978,7 +1997,8 @@ endif sed -e "s/@WOLFBOOT_PARTITION_BOOT_ADDRESS@/$(WOLFBOOT_PARTITION_BOOT_ADDRESS)/g" | \ sed -e "s/@WOLFBOOT_PARTITION_SIZE@/$(WOLFBOOT_PARTITION_SIZE)/g" | \ sed -e "s/@WOLFBOOT_PARTITION_UPDATE_ADDRESS@/$(WOLFBOOT_PARTITION_UPDATE_ADDRESS)/g" | \ - sed -e "s/@WOLFBOOT_PARTITION_SWAP_ADDRESS@/$(WOLFBOOT_PARTITION_SWAP_ADDRESS)/g" \ + sed -e "s/@WOLFBOOT_PARTITION_SWAP_ADDRESS@/$(WOLFBOOT_PARTITION_SWAP_ADDRESS)/g" | \ + sed -e "s/@TEST_APP_STACK_SIZE@/$(TEST_APP_STACK_SIZE)/g" \ > $(@) $(Q)rm -f .app-size .entry-point .wolfboot-offset .partition-size .header-size diff --git a/test-app/RISCV64-mpfs250.ld b/test-app/RISCV64-mpfs250.ld index 0ec912bd5b..c61a303c07 100644 --- a/test-app/RISCV64-mpfs250.ld +++ b/test-app/RISCV64-mpfs250.ld @@ -4,7 +4,7 @@ /* TODO: Update with actual memory map for MPFS250 */ _Min_Heap_Size = 0x00002000; /* required amount of heap */ -_Min_Stack_Size = 0x00002000; /* required amount of stack */ +_Min_Stack_Size = @TEST_APP_STACK_SIZE@; /* required amount of stack */ OUTPUT_ARCH( "riscv" ) diff --git a/test-app/app_mpfs250.c b/test-app/app_mpfs250.c index b280bd9e8f..572ed5b43b 100644 --- a/test-app/app_mpfs250.c +++ b/test-app/app_mpfs250.c @@ -46,6 +46,84 @@ int wolfcrypt_test(void *args); int benchmark_test(void *args); #endif +#ifdef WOLFCRYPT_BENCHMARK +/* Buffer sizes the whole benchmark is run at, in turn: the Athena F5200 does + * one engine transfer per call, so a sector-sized call and a bulk one amortise + * the setup very differently, and the software build runs the same set as the + * baseline. Each needs twice the size from the static pool + * (WOLFSSL_STATIC_MEMORY_TEST_SZ). */ +#ifndef MPFS_BENCH_SIZES +#define MPFS_BENCH_SIZES { 1024, 512, 4096 } +#endif +#endif /* WOLFCRYPT_BENCHMARK */ + +#ifdef MPFS_ATHENA +#include +#include + +static void mpfs_athena_app_init(void) +{ + int ret; + + /* The engine is brought up here, not by the bootloader: a wolfBoot built + * without MPFS_ATHENA never touched it, and the port's own init handles + * one that did. wolfcrypt_test and benchmark_test register the device + * again, which the port accepts. */ + ret = wolfCrypt_Init(); + if (ret != 0) { + wolfBoot_printf("Athena: wolfCrypt_Init failed (%d)\r\n", ret); + return; + } + ret = wc_MpfsAthena_RegisterDevice(WC_MPFS_ATHENA_DEVID); + if (ret != 0) { + wolfBoot_printf("Athena: RegisterDevice failed (%d)\r\n", ret); + return; + } + ret = wc_MpfsAthena_SelfTest(); + if (ret != 0) { + wolfBoot_printf("Athena: self-test failed: %s (rc %d)\r\n", + wc_MpfsAthena_SelfTestDesc(), ret); + return; + } + wolfBoot_printf("Athena: self-test passed, engine served %u call(s)\r\n", + (unsigned int)wc_MpfsAthena_GetCbCount()); +} + +#endif /* MPFS_ATHENA */ + + +#ifdef WOLFBOOT_RISCV_MMODE +#include "../hal/riscv.h" +/* MTIME is not running in an M-mode boot: start it, then let the machine + * timer interrupt refresh the watchdogs every 5 s for as long as the app + * runs. */ +#define MPFS_WDT_PET_TICKS (5ULL * RTC_CLOCK_FREQ) + +/* The vector saved the caller's registers, so plain C: refresh the five MSS + * watchdogs, which the bitstream arms and which reset the chip well inside a + * test run, then rearm. */ +void isr_mtimer(void) +{ + mpfs_wdt_pet_all(); + CLINT_MTIMECMP_REG(0) = CLINT_MTIME_REG + MPFS_WDT_PET_TICKS; +} + +static void mpfs_wdt_timer_start(void) +{ + mpfs_enable_mtime(); + CLINT_MTIMECMP_REG(0) = CLINT_MTIME_REG + MPFS_WDT_PET_TICKS; + csr_set(mie, MIE_MTIE); + csr_set(mstatus, MSTATUS_MIE); +#ifdef MPFS_DDR_INIT + /* The bootloader raised the E51 to the Libero PLL rate; this copy of the + * HAL's clock variable, which times the benchmark, still holds the reset + * rate. Without DDR init the clock is never raised and that reset rate is + * the right one. */ + mpfs_cpu_freq_mhz = + (uint32_t)(LIBERO_SETTING_MSS_COREPLEX_CPU_CLK / 1000000UL); +#endif +} +#endif void main(void) { @@ -61,6 +139,14 @@ void main(void) wolfBoot_printf("GPL v3\r\n"); wolfBoot_printf("========================\r\n"); +#ifdef WOLFBOOT_RISCV_MMODE + mpfs_wdt_timer_start(); +#endif + +#ifdef MPFS_ATHENA + mpfs_athena_app_init(); +#endif + #ifdef WOLFCRYPT_TEST wolfBoot_printf("\r\nRunning wolfCrypt tests...\r\n"); wolfCrypt_Init(); @@ -72,11 +158,30 @@ void main(void) #ifdef WOLFCRYPT_BENCHMARK wolfBoot_printf("Running wolfCrypt benchmarks...\r\n"); wolfCrypt_Init(); - benchmark_test(NULL); + { + static const unsigned int sizes[] = MPFS_BENCH_SIZES; + unsigned int i; + /* benchmark_test() puts the block size back to its default on its + * first run, so take that run's setup first and configure after. */ + benchmark_init(); + benchmark_free(); + for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + wolfBoot_printf("\r\n--- %u-byte calls ---\r\n", sizes[i]); + benchmark_configure(sizes[i]); + benchmark_test(NULL); + } + } wolfCrypt_Cleanup(); wolfBoot_printf("Benchmarks complete.\r\n\r\n"); #endif +#ifdef MPFS_ATHENA + /* A passing test or a plausible figure proves nothing about offload on + * its own: every case the port declines runs in software. */ + wolfBoot_printf("Athena: engine served %u call(s) in total\r\n", + (unsigned int)wc_MpfsAthena_GetCbCount()); +#endif + while(1) { /* Main application loop */ /* TODO: Add watchdog feed if needed */ diff --git a/test-app/startup_riscv.c b/test-app/startup_riscv.c index 13afa5fcf8..0530184a25 100644 --- a/test-app/startup_riscv.c +++ b/test-app/startup_riscv.c @@ -21,6 +21,7 @@ #include #include "hal/riscv.h" +#include "printf.h" extern void trap_entry(void); extern void trap_exit(void); @@ -77,15 +78,35 @@ void do_boot(const uint32_t *app_offset) } static uint32_t synctrap_cause = 0; -void __attribute__((naked)) isr_synctrap(void) -{ #ifdef WOLFBOOT_RISCV_MMODE - asm volatile("csrr %0, mcause" : "=r"(synctrap_cause)); - asm volatile("mret"); +/* The vector saved the caller's registers, so this can be plain C. Report the + * fault and stop rather than resume at the faulting instruction, which would + * trap again for ever with nothing on the console. Hardware clears mstatus.MIE + * on trap entry, so the timer interrupt that pets the MSS watchdogs cannot run + * here and they reset the board a few seconds after the message. */ +void isr_synctrap(void) +{ + unsigned long cause, epc, tval; + asm volatile("csrr %0, mcause" : "=r"(cause)); + asm volatile("csrr %0, mepc" : "=r"(epc)); + asm volatile("csrr %0, mtval" : "=r"(tval)); + synctrap_cause = (uint32_t)cause; + wolfBoot_printf("TRAP: cause=%lx epc=%lx tval=%lx\r\n", cause, epc, tval); + while (1) { + /* halt */ + } +} #else +void __attribute__((naked)) isr_synctrap(void) +{ asm volatile("csrr %0, scause" : "=r"(synctrap_cause)); asm volatile("sret"); +} #endif + +/* Machine timer interrupt; a target that uses it defines its own. */ +void __attribute__((weak)) isr_mtimer(void) +{ } void isr_empty(void) diff --git a/test-app/vector_riscv.S b/test-app/vector_riscv.S index 5dbb06eb8e..fcc58a37ba 100644 --- a/test-app/vector_riscv.S +++ b/test-app/vector_riscv.S @@ -131,7 +131,7 @@ IV: .align 2 j trap_empty .align 2 - j trap_empty + j _mtimer .align 2 j trap_empty .align 2 @@ -187,5 +187,10 @@ _synctrap: jal isr_synctrap trap_exit +_mtimer: + trap_entry + jal isr_mtimer + trap_exit + trap_empty: nop diff --git a/test-app/wolfcrypt_support.c b/test-app/wolfcrypt_support.c index 143b71d2e6..9a0f75a692 100644 --- a/test-app/wolfcrypt_support.c +++ b/test-app/wolfcrypt_support.c @@ -179,10 +179,7 @@ * - M-mode (no HSS): the 'time'/'cycle' user CSRs trap on the E51; read * the machine 'mcycle' counter (CPU clock) instead. */ #include "../hal/riscv.h" - #ifndef RISCV_SMODE_TIMER_FREQ - /* M-mode: mcycle counts the CPU clock (MSS_CPU_CLK, 600 MHz). */ - #define RISCV_SMODE_TIMER_FREQ 600000000UL - #endif + #include "../hal/mpfs250.h" static uint64_t mpfs_start_ticks = 0; static uint64_t mpfs_get_ticks(void) {