From e227ec066c3493dae1747383d074b34cbb0d9769 Mon Sep 17 00:00:00 2001 From: Nicolas Pitre Date: Wed, 29 Jul 2026 01:26:22 -0400 Subject: [PATCH 1/3] Add bare-metal AArch64 platform support Detect ISA features on AArch64 targets that run without an OS, that is under an RTOS or on bare metal, by reading the ID registers directly. This is not an alternative spelling of the Linux path, the two cannot share detection logic: there is no kernel to publish HWCAP here, and conversely the ID registers are inaccessible from Linux userspace. Reading them requires EL1, which is where an RTOS application runs. Nothing in the new file is tied to a particular RTOS. The single piece of external information needed is the core count, which comes from get_num_cpus(): Zephyr provides arch_num_cpus(), anything else falls back to a single core. Supporting another RTOS means adding a case there. cpuinfo_initialize() guards this path with a plain flag rather than pthread_once(), following the Emscripten-without-pthreads precedent, so that cpuinfo still builds where POSIX threads are unavailable or deliberately disabled to save space. Topology is reported as one package with one cluster. Cache geometry uses conservative Cortex-A defaults; reading CCSIDR_EL1 for the real values is left as a follow-up. --- src/arm/baremetal/init.c | 337 +++++++++++++++++++++++++++++++++++++ src/cpuinfo/internal-api.h | 1 + src/init.c | 14 ++ 3 files changed, 352 insertions(+) create mode 100644 src/arm/baremetal/init.c diff --git a/src/arm/baremetal/init.c b/src/arm/baremetal/init.c new file mode 100644 index 00000000..9599bffe --- /dev/null +++ b/src/arm/baremetal/init.c @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2026 BayLibre SAS + * SPDX-License-Identifier: BSD-2-Clause + * + * cpuinfo initialization for bare-metal AArch64 targets, i.e. an RTOS or no + * OS at all, where the code runs at EL1. + * + * ISA features are read straight out of the AArch64 ID registers rather than + * from /proc/cpuinfo or HWCAP. That is not merely an alternative to the Linux + * path, it is the only option here: there is no kernel publishing HWCAP, and + * conversely the ID registers are inaccessible from Linux userspace. Reading + * them requires EL1, which is exactly where an RTOS application runs. + * + * Nothing below is specific to one RTOS. The only external input is the core + * count, obtained through get_num_cpus() below; add a case there to support + * another RTOS. + * + * Topology is simplified to a single package with a single cluster. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#if defined(__ZEPHYR__) + +#include + +static inline uint32_t get_num_cpus(void) { + return (uint32_t)arch_num_cpus(); +} + +#else + +/* + * No RTOS-specific way to ask: assume a single core. SMP-capable ports should + * add a case above rather than change this default. + */ +#define get_num_cpus() 1u + +#endif + +struct cpuinfo_arm_isa cpuinfo_isa = {0}; + +static struct cpuinfo_package static_package = {{0}}; + +/* + * ID_AA64ISAR0_EL1 field definitions (4-bit fields). + */ +#define ISAR0_AES_SHIFT 4 +#define ISAR0_SHA1_SHIFT 8 +#define ISAR0_SHA2_SHIFT 12 +#define ISAR0_CRC32_SHIFT 16 +#define ISAR0_ATOMIC_SHIFT 20 +#define ISAR0_RDM_SHIFT 28 +#define ISAR0_DP_SHIFT 44 /* DotProduct (UDOT/SDOT) */ +#define ISAR0_FHM_SHIFT 48 + +/* + * ID_AA64ISAR1_EL1 field definitions. + */ +#define ISAR1_JSCVT_SHIFT 12 +#define ISAR1_FCMA_SHIFT 16 +#define ISAR1_I8MM_SHIFT 52 +#define ISAR1_BF16_SHIFT 44 + +/* + * ID_AA64PFR0_EL1 field definitions. + */ +#define PFR0_FP_SHIFT 16 +#define PFR0_ADVSIMD_SHIFT 20 +#define PFR0_SVE_SHIFT 32 + +/* + * ID_AA64PFR1_EL1 field definitions. + */ +#define PFR1_SME_SHIFT 24 + +/* + * ID_AA64SMFR0_EL1 field definitions (for SME sub-features). + */ +#define SMFR0_I16I32_BIT (1ULL << 52) +#define SMFR0_BI32I32_BIT (1ULL << 48) +#define SMFR0_B16B16_BIT (1ULL << 60) +#define SMFR0_F16F16_BIT (1ULL << 42) + +static inline uint64_t read_id_aa64isar0(void) { + uint64_t val; + __asm__ volatile("mrs %0, id_aa64isar0_el1" : "=r"(val)); + return val; +} + +static inline uint64_t read_id_aa64isar1(void) { + uint64_t val; + __asm__ volatile("mrs %0, id_aa64isar1_el1" : "=r"(val)); + return val; +} + +static inline uint64_t read_id_aa64pfr0(void) { + uint64_t val; + __asm__ volatile("mrs %0, id_aa64pfr0_el1" : "=r"(val)); + return val; +} + +static inline uint64_t read_id_aa64pfr1(void) { + uint64_t val; + __asm__ volatile("mrs %0, id_aa64pfr1_el1" : "=r"(val)); + return val; +} + +static inline uint64_t read_midr(void) { + uint64_t val; + __asm__ volatile("mrs %0, midr_el1" : "=r"(val)); + return val; +} + +#define FIELD(reg, shift) (((reg) >> (shift)) & 0xf) + +static void detect_isa_features(void) { + const uint64_t isar0 = read_id_aa64isar0(); + const uint64_t isar1 = read_id_aa64isar1(); + const uint64_t pfr0 = read_id_aa64pfr0(); + const uint64_t pfr1 = read_id_aa64pfr1(); + + /* ID_AA64ISAR0_EL1 */ + cpuinfo_isa.aes = FIELD(isar0, ISAR0_AES_SHIFT) >= 1; + cpuinfo_isa.pmull = FIELD(isar0, ISAR0_AES_SHIFT) >= 2; + cpuinfo_isa.sha1 = FIELD(isar0, ISAR0_SHA1_SHIFT) >= 1; + cpuinfo_isa.sha2 = FIELD(isar0, ISAR0_SHA2_SHIFT) >= 1; + cpuinfo_isa.crc32 = FIELD(isar0, ISAR0_CRC32_SHIFT) >= 1; + cpuinfo_isa.atomics = FIELD(isar0, ISAR0_ATOMIC_SHIFT) >= 2; + cpuinfo_isa.rdm = FIELD(isar0, ISAR0_RDM_SHIFT) >= 1; + cpuinfo_isa.dot = FIELD(isar0, ISAR0_DP_SHIFT) >= 1; + cpuinfo_isa.fhm = FIELD(isar0, ISAR0_FHM_SHIFT) >= 1; + + /* ID_AA64ISAR1_EL1 */ + cpuinfo_isa.jscvt = FIELD(isar1, ISAR1_JSCVT_SHIFT) >= 1; + cpuinfo_isa.fcma = FIELD(isar1, ISAR1_FCMA_SHIFT) >= 1; + cpuinfo_isa.i8mm = FIELD(isar1, ISAR1_I8MM_SHIFT) >= 1; + cpuinfo_isa.bf16 = FIELD(isar1, ISAR1_BF16_SHIFT) >= 1; + + /* ID_AA64PFR0_EL1: FP16 arithmetic requires both FP and AdvSIMD + * fields to report half-precision support (value >= 1). */ + const uint32_t fp_field = FIELD(pfr0, PFR0_FP_SHIFT); + const uint32_t advsimd_field = FIELD(pfr0, PFR0_ADVSIMD_SHIFT); + cpuinfo_isa.fp16arith = (fp_field >= 1 && fp_field != 0xf) && (advsimd_field >= 1 && advsimd_field != 0xf); + + /* SVE */ + cpuinfo_isa.sve = FIELD(pfr0, PFR0_SVE_SHIFT) >= 1; + if (cpuinfo_isa.sve) { + /* SVE2: indicated by ID_AA64ZFR0_EL1.SVEver >= 1 */ + uint64_t zfr0; + __asm__ volatile("mrs %0, s3_0_c0_c4_4" : "=r"(zfr0)); + cpuinfo_isa.sve2 = (zfr0 & 0xf) >= 1; + + /* SVE vector length in bytes. */ + uint64_t vl_bytes; + __asm__ volatile("rdvl %0, #1" : "=r"(vl_bytes)); + cpuinfo_isa.svelen = (uint32_t)vl_bytes; + } + + /* ID_AA64PFR1_EL1: SME */ + const uint32_t sme_field = FIELD(pfr1, PFR1_SME_SHIFT); + cpuinfo_isa.sme = sme_field >= 1; + cpuinfo_isa.sme2 = sme_field >= 2; + cpuinfo_isa.sme2p1 = sme_field >= 3; + + if (cpuinfo_isa.sme) { + uint64_t smfr0; + __asm__ volatile("mrs %0, s3_0_c0_c4_5" : "=r"(smfr0)); + cpuinfo_isa.sme_i16i32 = !!(smfr0 & SMFR0_I16I32_BIT); + cpuinfo_isa.sme_bi32i32 = !!(smfr0 & SMFR0_BI32I32_BIT); + cpuinfo_isa.sme_b16b16 = !!(smfr0 & SMFR0_B16B16_BIT); + cpuinfo_isa.sme_f16f16 = !!(smfr0 & SMFR0_F16F16_BIT); + + /* + * SME streaming vector length may differ from SVE vector + * length but reading it requires entering streaming mode + * (SMSTART/SMSTOP) which has side effects. Default to + * SVE length as a reasonable approximation. + */ + cpuinfo_isa.smelen = cpuinfo_isa.svelen; + } +} + +void cpuinfo_arm_baremetal_init(void) { + struct cpuinfo_processor* processors = NULL; + struct cpuinfo_core* cores = NULL; + struct cpuinfo_cluster* clusters = NULL; + struct cpuinfo_uarch_info* uarchs = NULL; + struct cpuinfo_cache* l1i = NULL; + struct cpuinfo_cache* l1d = NULL; + struct cpuinfo_cache* l2 = NULL; + + const uint32_t core_count = get_num_cpus(); + const uint32_t cluster_count = 1; + + detect_isa_features(); + + processors = calloc(core_count, sizeof(struct cpuinfo_processor)); + cores = calloc(core_count, sizeof(struct cpuinfo_core)); + clusters = calloc(cluster_count, sizeof(struct cpuinfo_cluster)); + uarchs = calloc(cluster_count, sizeof(struct cpuinfo_uarch_info)); + l1i = calloc(core_count, sizeof(struct cpuinfo_cache)); + l1d = calloc(core_count, sizeof(struct cpuinfo_cache)); + l2 = calloc(cluster_count, sizeof(struct cpuinfo_cache)); + + if (!processors || !cores || !clusters || !uarchs || !l1i || !l1d || !l2) { + cpuinfo_log_error("failed to allocate cpuinfo structures"); + goto cleanup; + } + + const uint64_t midr = read_midr(); + enum cpuinfo_vendor vendor = cpuinfo_vendor_unknown; + enum cpuinfo_uarch uarch = cpuinfo_uarch_unknown; + cpuinfo_arm_decode_vendor_uarch((uint32_t)midr, &vendor, &uarch); + + strncpy(static_package.name, "ARM AArch64", CPUINFO_PACKAGE_NAME_MAX); + static_package.processor_count = core_count; + static_package.core_count = core_count; + static_package.cluster_count = cluster_count; + + clusters[0] = (struct cpuinfo_cluster){ + .processor_start = 0, + .processor_count = core_count, + .core_start = 0, + .core_count = core_count, + .cluster_id = 0, + .package = &static_package, + .vendor = vendor, + .uarch = uarch, + }; + + for (uint32_t i = 0; i < core_count; i++) { + processors[i] = (struct cpuinfo_processor){ + .smt_id = 0, + .core = cores + i, + .cluster = clusters, + .package = &static_package, + .cache.l1i = l1i + i, + .cache.l1d = l1d + i, + .cache.l2 = l2, + }; + + cores[i] = (struct cpuinfo_core){ + .processor_start = i, + .processor_count = 1, + .core_id = i, + .cluster = clusters, + .package = &static_package, + .vendor = vendor, + .uarch = uarch, + .midr = (uint32_t)midr, + }; + + /* + * TODO: read CCSIDR_EL1 (via CSSELR_EL1 level select) for + * actual cache geometry. These are conservative defaults + * typical for Cortex-A class cores. + */ + l1i[i] = (struct cpuinfo_cache){ + .size = 64 * 1024, + .associativity = 4, + .line_size = 64, + .processor_start = i, + .processor_count = 1, + }; + + l1d[i] = (struct cpuinfo_cache){ + .size = 64 * 1024, + .associativity = 4, + .line_size = 64, + .processor_start = i, + .processor_count = 1, + }; + } + + l2[0] = (struct cpuinfo_cache){ + .size = 512 * 1024, + .associativity = 8, + .line_size = 64, + .processor_start = 0, + .processor_count = core_count, + }; + + /* Commit */ + cpuinfo_processors = processors; + cpuinfo_cores = cores; + cpuinfo_clusters = clusters; + cpuinfo_packages = &static_package; + + cpuinfo_cache[cpuinfo_cache_level_1i] = l1i; + cpuinfo_cache[cpuinfo_cache_level_1d] = l1d; + cpuinfo_cache[cpuinfo_cache_level_2] = l2; + + cpuinfo_cache_count[cpuinfo_cache_level_1i] = core_count; + cpuinfo_cache_count[cpuinfo_cache_level_1d] = core_count; + cpuinfo_cache_count[cpuinfo_cache_level_2] = cluster_count; + + uarchs[0] = (struct cpuinfo_uarch_info){ + .uarch = uarch, + .processor_count = core_count, + .core_count = core_count, + }; + cpuinfo_uarchs = uarchs; + cpuinfo_uarchs_count = cluster_count; + + cpuinfo_processors_count = core_count; + cpuinfo_cores_count = core_count; + cpuinfo_clusters_count = cluster_count; + cpuinfo_packages_count = 1; + + cpuinfo_max_cache_size = 8 * 1024 * 1024; + + cpuinfo_is_initialized = true; + + processors = NULL; + cores = NULL; + clusters = NULL; + uarchs = NULL; + l1i = l1d = l2 = NULL; + +cleanup: + free(processors); + free(cores); + free(clusters); + free(uarchs); + free(l1i); + free(l1d); + free(l2); +} diff --git a/src/cpuinfo/internal-api.h b/src/cpuinfo/internal-api.h index d84b26a8..df72c41b 100644 --- a/src/cpuinfo/internal-api.h +++ b/src/cpuinfo/internal-api.h @@ -59,6 +59,7 @@ CPUINFO_PRIVATE BOOL CALLBACK cpuinfo_x86_windows_init(PINIT_ONCE init_once, PVO #endif CPUINFO_PRIVATE void cpuinfo_arm_mach_init(void); CPUINFO_PRIVATE void cpuinfo_arm_linux_init(void); +CPUINFO_PRIVATE void cpuinfo_arm_baremetal_init(void); CPUINFO_PRIVATE void cpuinfo_riscv_linux_init(void); CPUINFO_PRIVATE void cpuinfo_emscripten_init(void); diff --git a/src/init.c b/src/init.c index 81d5721c..158b7fc6 100644 --- a/src/init.c +++ b/src/init.c @@ -1,5 +1,7 @@ #if defined(_WIN32) || defined(__CYGWIN__) #include +#elif defined(CPUINFO_BAREMETAL) +/* No threading library assumed, see the init guard below. */ #elif !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) #include #endif @@ -14,6 +16,13 @@ #if defined(_WIN32) || defined(__CYGWIN__) static INIT_ONCE init_guard = INIT_ONCE_STATIC_INIT; +#elif defined(CPUINFO_BAREMETAL) +/* + * Bare-metal targets need not have POSIX threads: initialization happens + * before secondary cores are released, so a plain flag is enough and this + * keeps cpuinfo usable in configurations built without pthreads. + */ +static bool init_guard = false; #elif !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) static pthread_once_t init_guard = PTHREAD_ONCE_INIT; #else @@ -40,6 +49,11 @@ bool CPUINFO_ABI cpuinfo_initialize(void) { pthread_once(&init_guard, &cpuinfo_arm_mach_init); #elif defined(_WIN32) InitOnceExecuteOnce(&init_guard, &cpuinfo_arm_windows_init, NULL, NULL); +#elif defined(CPUINFO_BAREMETAL) + if (!init_guard) { + cpuinfo_arm_baremetal_init(); + init_guard = true; + } #else cpuinfo_log_error("operating system is not supported in cpuinfo"); #endif From f2e8ddb09b5857c7b27522a26b4e21e23ebfa5a3 Mon Sep 17 00:00:00 2001 From: Nicolas Pitre Date: Wed, 29 Jul 2026 01:26:22 -0400 Subject: [PATCH 2/3] CMake: add bare-metal AArch64 platform support Select the bare-metal init when CMAKE_SYSTEM_NAME is "Generic", CMake's name for a target without an operating system, which is what both Zephyr and FreeRTOS set. "Generic" therefore also joins the list of recognised platforms so that cpuinfo_initialize() is not stubbed out for them. The source is added only for AArch64 targets: the ID register layout it reads does not apply to AArch32, so an ARM32 build must not pick it up. CPUINFO_BAREMETAL is defined for the library targets, selecting both that init path and its non-POSIX init guard. --- CMakeLists.txt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 072c9873..15ce1ec6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,7 +108,7 @@ IF(NOT CMAKE_SYSTEM_NAME) "Target operating system is not specified. " "cpuinfo will compile, but cpuinfo_initialize() will always fail.") SET(CPUINFO_SUPPORTED_PLATFORM FALSE) -ELSEIF(NOT CMAKE_SYSTEM_NAME MATCHES "^(Windows|WindowsStore|CYGWIN|MSYS|Darwin|Linux|Android|FreeBSD|Emscripten)$") +ELSEIF(NOT CMAKE_SYSTEM_NAME MATCHES "^(Windows|WindowsStore|CYGWIN|MSYS|Darwin|Linux|Android|FreeBSD|Emscripten|Generic)$") IF(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.14" AND NOT IS_APPLE_OS) MESSAGE(WARNING "Target operating system \"${CMAKE_SYSTEM_NAME}\" is not supported in cpuinfo. " @@ -211,6 +211,13 @@ IF(CPUINFO_SUPPORTED_PLATFORM) ENDIF() ELSEIF(IS_APPLE_OS AND CPUINFO_TARGET_PROCESSOR MATCHES "arm64.*") LIST(APPEND CPUINFO_SRCS src/arm/mach/init.c) + ELSEIF(CMAKE_SYSTEM_NAME STREQUAL "Generic" AND CPUINFO_TARGET_PROCESSOR MATCHES "^(aarch64|arm64.*)$") + # "Generic" is CMake's name for a target without an OS, which covers both + # an RTOS (Zephyr, FreeRTOS) and no OS at all. Such a target runs at EL1 + # and reads the AArch64 ID registers directly. AArch64 only: the ID + # register layout below does not apply to AArch32. + LIST(APPEND CPUINFO_SRCS src/arm/baremetal/init.c) + SET(CPUINFO_BAREMETAL_INIT TRUE) ENDIF() IF(CMAKE_SYSTEM_NAME STREQUAL "Android") LIST(APPEND CPUINFO_SRCS @@ -264,6 +271,12 @@ ADD_LIBRARY(cpuinfo_internals STATIC ${CPUINFO_SRCS}) CPUINFO_TARGET_ENABLE_C99(cpuinfo) CPUINFO_TARGET_ENABLE_C99(cpuinfo_internals) CPUINFO_TARGET_RUNTIME_LIBRARY(cpuinfo) +IF(CPUINFO_BAREMETAL_INIT) + # Selects the bare-metal init path and its non-POSIX init guard, so cpuinfo + # builds in configurations without pthreads. + TARGET_COMPILE_DEFINITIONS(cpuinfo PRIVATE CPUINFO_BAREMETAL=1) + TARGET_COMPILE_DEFINITIONS(cpuinfo_internals PRIVATE CPUINFO_BAREMETAL=1) +ENDIF() IF(CMAKE_SYSTEM_NAME MATCHES "^(Windows|WindowsStore|CYGWIN|MSYS)$") # Target Windows 7+ API TARGET_COMPILE_DEFINITIONS(cpuinfo PRIVATE _WIN32_WINNT=0x0601 _CRT_SECURE_NO_WARNINGS) From e232356c7d60ca5e8e53b0eebc890ddd09488e46 Mon Sep 17 00:00:00 2001 From: Nicolas Pitre Date: Wed, 29 Jul 2026 01:26:22 -0400 Subject: [PATCH 3/3] log: support platforms without POSIX write() Bare-metal targets have no file descriptors to write() to, so route logging through printf() there, the way the Hexagon path already does. Keep Hexagon on qurt_printf(): its RTOS does not necessarily redirect the standard printf(), which is why that path exists in the first place. --- src/log.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/log.c b/src/log.c index 2f85128d..e6eabe28 100644 --- a/src/log.c +++ b/src/log.c @@ -37,6 +37,12 @@ #elif defined(__hexagon__) #define CPUINFO_LOG_NEWLINE_LENGTH 1 +#define CPUINFO_LOG_STDERR 0 +#define CPUINFO_LOG_STDOUT 0 +#elif defined(CPUINFO_BAREMETAL) +/* No file descriptors: output goes through printf(), see below. */ +#define CPUINFO_LOG_NEWLINE_LENGTH 1 + #define CPUINFO_LOG_STDERR 0 #define CPUINFO_LOG_STDOUT 0 #else @@ -112,6 +118,10 @@ static void cpuinfo_vlog( NULL); #elif defined(__hexagon__) qurt_printf("%s", out_buffer); +#elif defined(CPUINFO_BAREMETAL) + out_buffer[prefix_length + format_length] = '\n'; + out_buffer[prefix_length + format_length + 1] = '\0'; + printf("%s", out_buffer); #else out_buffer[prefix_length + format_length] = '\n';