diff --git a/Buildscripts/DevicetreeCompiler/source/generator.py b/Buildscripts/DevicetreeCompiler/source/generator.py index f40a1d2cc..b19e75da2 100644 --- a/Buildscripts/DevicetreeCompiler/source/generator.py +++ b/Buildscripts/DevicetreeCompiler/source/generator.py @@ -350,7 +350,7 @@ def generate_devicetree_c(filename: str, items: list[object], bindings: list[Bin for item in items: if type(item) is Device: write_device_structs(file, item, None, bindings, devices, verbose) - file.write("struct DtsDevice dts_devices[] = {\n") + file.write("const struct DtsDevice dts_devices[] = {\n") for item in items: if type(item) is Device: write_device_list_entry(file, item, bindings, verbose) @@ -379,7 +379,7 @@ def generate_devicetree_c(filename: str, items: list[object], bindings: list[Bin file.write(f"extern struct Module {symbol};\n") file.write("\n") # Create array of symbol variables - file.write("struct Module* dts_modules[] = {\n") + file.write("struct Module* const dts_modules[] = {\n") for symbol in module_symbol_names: file.write(f"\t&{symbol},\n") file.write("\tNULL\n") @@ -397,10 +397,10 @@ def generate_devicetree_h(filename: str): #endif // Array of device tree modules terminated with DTS_MODULE_TERMINATOR - extern struct DtsDevice dts_devices[]; + extern const struct DtsDevice dts_devices[]; // Array of module symbols terminated with NULL - extern struct Module* dts_modules[]; + extern struct Module* const dts_modules[]; #ifdef __cplusplus } diff --git a/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.c b/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.c index 1768f40ed..be2ac6084 100644 --- a/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.c +++ b/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.c @@ -50,7 +50,7 @@ static struct Device bool_test_device = { .internal = NULL }; -struct DtsDevice dts_devices[] = { +const struct DtsDevice dts_devices[] = { { &root, "test,root", DTS_DEVICE_STATUS_OKAY }, { &test_device, "test,generic-device", DTS_DEVICE_STATUS_OKAY }, { &bool_test_device, "test,bool-device", DTS_DEVICE_STATUS_OKAY }, @@ -59,7 +59,7 @@ struct DtsDevice dts_devices[] = { extern struct Module data_module; -struct Module* dts_modules[] = { +struct Module* const dts_modules[] = { &data_module, NULL }; diff --git a/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.h b/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.h index 7c56f34b0..c34c612e9 100644 --- a/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.h +++ b/Buildscripts/DevicetreeCompiler/tests/data/expected_devicetree.h @@ -7,10 +7,10 @@ extern "C" { #endif // Array of device tree modules terminated with DTS_MODULE_TERMINATOR -extern struct DtsDevice dts_devices[]; +extern const struct DtsDevice dts_devices[]; // Array of module symbols terminated with NULL -extern struct Module* dts_modules[]; +extern struct Module* const dts_modules[]; #ifdef __cplusplus } diff --git a/Tactility/Include/Tactility/Tactility.h b/Tactility/Include/Tactility/Tactility.h index 0e1b55b19..5f3b2d9b6 100644 --- a/Tactility/Include/Tactility/Tactility.h +++ b/Tactility/Include/Tactility/Tactility.h @@ -43,7 +43,7 @@ private: * @param dtsModules List of modules from devicetree, null-terminated, non-null parameter * @param dtsDevices Array that is terminated with DTS_DEVICE_TERMINATOR */ -void run(Module* dtsModules[], DtsDevice dtsDevices[]); +void run(Module* const dtsModules[], const DtsDevice dtsDevices[]); /** Provides access to the dispatcher that runs on the main task. * @warning This dispatcher is used for WiFi and might block for some time during WiFi connection. diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index a3ff036e9..a6f4ca9b6 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -371,7 +371,7 @@ static void onLvglStarted() { addService(service::screenshot::manifest); #endif - memory_trace(); + memory_print_stats(); } static void onLvglStopped() { @@ -388,10 +388,10 @@ static void onLvglStopped() { check(service::removeService(service::statusbar::manifest.id)); check(service::removeService(service::gui::manifest.id)); - memory_trace(); + memory_print_stats(); } -void run(Module* dtsModules[], DtsDevice dtsDevices[]) { +void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) { LOG_I(TAG, "Tactility v%s on %s (%s)", TT_VERSION, CONFIG_TT_DEVICE_NAME, CONFIG_TT_DEVICE_ID); LOG_I(TAG, "Initializing kernel"); diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index 078fbc5d4..2e55c45ec 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -69,7 +69,7 @@ void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launc transitionAppToState(new_app, app::State::Created); transitionAppToState(new_app, app::State::Showing); - memory_trace(); + memory_print_stats(); } void LoaderService::onStopTopAppMessage(const std::string& id) { @@ -162,7 +162,7 @@ void LoaderService::onStopTopAppMessage(const std::string& id) { } } - memory_trace(); + memory_print_stats(); } int LoaderService::findAppInStack(const std::string& id) const { diff --git a/TactilityKernel/include/tactility/kernel_init.h b/TactilityKernel/include/tactility/kernel_init.h index f75dcb579..540720f2d 100644 --- a/TactilityKernel/include/tactility/kernel_init.h +++ b/TactilityKernel/include/tactility/kernel_init.h @@ -14,7 +14,7 @@ extern "C" { * @param dts_devices The list of generated devices from the devicetree. The array must be terminated with DTS_DEVICE_TERMINATOR. Non-null parameter. * @return ERROR_NONE on success, otherwise an error code */ -error_t kernel_init(struct Module* dts_modules[], struct DtsDevice dts_devices[]); +error_t kernel_init(struct Module* const dts_modules[], const struct DtsDevice dts_devices[]); #ifdef __cplusplus } diff --git a/TactilityKernel/include/tactility/memory.h b/TactilityKernel/include/tactility/memory.h index c067640ae..bc51721d6 100644 --- a/TactilityKernel/include/tactility/memory.h +++ b/TactilityKernel/include/tactility/memory.h @@ -1,10 +1,114 @@ #pragma once +#include +#include + #ifdef __cplusplus extern "C" { #endif -void memory_trace(); +/** Capability flags that describe what a memory allocation needs or prefers. */ +enum MemoryCapability { + /** Internal memory (non-external/non-PSRAM) memory. */ + MEMORY_CAPABILITY_INTERNAL = 1u << 0, + /** External memory (e.g. PSRAM/SPIRAM). */ + MEMORY_CAPABILITY_EXTERNAL = 1u << 1, + /** Usable for code execution. */ + MEMORY_CAPABILITY_EXECUTABLE = 1u << 2, + /** Usable as a DMA source/destination. */ + MEMORY_CAPABILITY_DMA = 1u << 3, + /** Usable for SIMD instructions. */ + MEMORY_CAPABILITY_SIMD = 1u << 4, +}; + +/** + * @brief Describes the constraints an allocation must (or should) satisfy. + * + * `required` capabilities must all be satisfied or the allocation fails. `desired` + * capabilities are attempted alongside `required`, but implementations fall back to + * `required`-only if `required | desired` together can't be satisfied. + */ +struct MemoryPolicy { + /** A bitset of MemoryCapability flags that are required during allocation. */ + uint16_t required; + /** A bitset of MemoryCapability flags that are preferable (but optional) during allocation. */ + uint16_t desired; + /** Alignment (in bytes) of the returned pointer, or 0 for the platform default. Must be a power of 2. */ + size_t alignment; +}; + +/** The default policy: no required/desired capabilities, no alignment requirement. */ +extern const struct MemoryPolicy MEMORY_POLICY_DEFAULT; + +/** + * @brief Logs current heap usage (internal and external, when applicable). + * No-op on platforms without heap capability tracking. + */ +void memory_print_stats(); + +/** + * @brief Allocates memory that satisfies the given policy. + * @param[in] size number of bytes to allocate + * @param[in] policy the allocation constraints + * @return the allocated memory, or NULL on failure + */ +void* memory_alloc_with_policy(size_t size, const struct MemoryPolicy* policy); + +/** + * @brief Resizes a previous allocation, preserving its contents up to the smaller of the old and new size. + * @warning policy->alignment is not guaranteed to be preserved across a realloc - it is only + * honored on fresh allocations (memory_alloc_with_policy()/memory_calloc_with_policy()). + * @param[in] ptr memory previously returned by memory_alloc_with_policy(), memory_calloc_with_policy(), + * or memory_realloc_with_policy(), or NULL to allocate a new block + * @param[in] size new memory size in bytes + * @param[in] policy the policy for the new allocation + * @return the (possibly moved) allocated memory, or NULL on failure - in which case ptr is left untouched + */ +void* memory_realloc_with_policy(void* ptr, size_t size, const struct MemoryPolicy* policy); + +/** + * @brief Allocates zero-initialized memory that satisfies the given policy. + * @param[in] count number of elements + * @param[in] size size of each element in bytes + * @param[in] policy the allocation constraints + * @return the allocated memory, or NULL on failure + */ +void* memory_calloc_with_policy(size_t count, size_t size, const struct MemoryPolicy* policy); + +/** + * @brief Allocates memory using MEMORY_POLICY_DEFAULT. + * @param[in] size number of bytes to allocate + * @return the allocated memory, or NULL on failure + */ +inline void* memory_alloc(size_t size) { + return memory_alloc_with_policy(size, &MEMORY_POLICY_DEFAULT); +} + +/** + * @brief Allocates zero-initialized memory using MEMORY_POLICY_DEFAULT. + * @param[in] count number of elements + * @param[in] size size of each element in bytes + * @return the allocated memory, or NULL on failure + */ +inline void* memory_calloc(size_t count, size_t size) { + return memory_calloc_with_policy(count, size, &MEMORY_POLICY_DEFAULT); +} + +/** + * @brief Resizes a previous allocation using MEMORY_POLICY_DEFAULT. See memory_realloc_with_policy(). + * @param[in] ptr memory previously returned by one of the memory_* allocation functions, or NULL + * @param[in] size new memory size in bytes + * @return the (possibly moved) allocated memory, or NULL on failure - in which case ptr is left untouched + */ +inline void* memory_realloc(void* ptr, size_t size) { + return memory_realloc_with_policy(ptr, size, &MEMORY_POLICY_DEFAULT); +} + +/** + * @brief Frees memory previously returned by one of the memory_* allocation functions. + * @param[in] ptr the memory to free, or NULL (a no-op) + */ +void memory_free(void* ptr); #ifdef __cplusplus } diff --git a/TactilityKernel/source/kernel_init.cpp b/TactilityKernel/source/kernel_init.cpp index 6a84306d8..201645638 100644 --- a/TactilityKernel/source/kernel_init.cpp +++ b/TactilityKernel/source/kernel_init.cpp @@ -43,7 +43,7 @@ Module root_module = { .internal = nullptr }; -error_t kernel_init(Module* dts_modules[], DtsDevice dts_devices[]) { +error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[]) { LOG_I(TAG, "init"); if (module_construct_add_start(&root_module) != ERROR_NONE) { @@ -51,7 +51,7 @@ error_t kernel_init(Module* dts_modules[], DtsDevice dts_devices[]) { return ERROR_RESOURCE; } - Module** dts_module = dts_modules; + Module* const* dts_module = dts_modules; while (*dts_module != nullptr) { if (module_construct_add_start(*dts_module) != ERROR_NONE) { LOG_E(TAG, "dts module init failed: %s", (*dts_module)->name); @@ -60,7 +60,7 @@ error_t kernel_init(Module* dts_modules[], DtsDevice dts_devices[]) { dts_module++; } - DtsDevice* dts_device = dts_devices; + const DtsDevice* dts_device = dts_devices; while (dts_device->device != nullptr) { if (dts_device->status == DTS_DEVICE_STATUS_OKAY) { if (device_construct_add_start(dts_device->device, dts_device->compatible) != ERROR_NONE) { diff --git a/TactilityKernel/source/memory.cpp b/TactilityKernel/source/memory.cpp index d35b9e97d..c174df05b 100644 --- a/TactilityKernel/source/memory.cpp +++ b/TactilityKernel/source/memory.cpp @@ -9,7 +9,13 @@ constexpr auto* TAG = "memory"; extern "C" { -void memory_trace() { +const struct MemoryPolicy MEMORY_POLICY_DEFAULT = { + .required = 0, + .desired = 0, + .alignment = 0, +}; + +void memory_print_stats() { #ifdef ESP_PLATFORM size_t heap_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); size_t heap_total = heap_caps_get_total_size(MALLOC_CAP_INTERNAL); diff --git a/TactilityKernel/source/memory_esp32.cpp b/TactilityKernel/source/memory_esp32.cpp new file mode 100644 index 000000000..f52ab4620 --- /dev/null +++ b/TactilityKernel/source/memory_esp32.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifdef ESP_PLATFORM + +#include + +#include + +namespace { + +uint32_t toHeapCaps(uint16_t capabilityFlags) { + uint32_t caps = 0; + if (capabilityFlags & MEMORY_CAPABILITY_INTERNAL) caps |= MALLOC_CAP_INTERNAL; + if (capabilityFlags & MEMORY_CAPABILITY_EXTERNAL) caps |= MALLOC_CAP_SPIRAM; + if (capabilityFlags & MEMORY_CAPABILITY_EXECUTABLE) caps |= MALLOC_CAP_EXEC; + if (capabilityFlags & MEMORY_CAPABILITY_DMA) caps |= MALLOC_CAP_DMA; + if (capabilityFlags & MEMORY_CAPABILITY_SIMD) caps |= MALLOC_CAP_SIMD; + return caps; +} + +} // namespace + +extern "C" { + +void* memory_alloc_with_policy(size_t size, const struct MemoryPolicy* policy) { + uint32_t required_caps = toHeapCaps(policy->required); + uint32_t desired_caps = toHeapCaps(policy->desired); + + void* ptr; + if (policy->alignment > 0) { + ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps | desired_caps); + if (ptr == nullptr && desired_caps != 0) { + // Desired caps couldn't be satisfied alongside the required ones - retry with + // required only, since desired is explicitly optional. + ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps); + } + } else { + ptr = heap_caps_malloc(size, required_caps | desired_caps); + if (ptr == nullptr && desired_caps != 0) { + ptr = heap_caps_malloc(size, required_caps); + } + } + return ptr; +} + +void* memory_realloc_with_policy(void* ptr, size_t size, const struct MemoryPolicy* policy) { + uint32_t required_caps = toHeapCaps(policy->required); + uint32_t desired_caps = toHeapCaps(policy->desired); + + // No aligned-realloc counterpart in the heap_caps API - policy->alignment is only honored + // on fresh allocations (memory_alloc_with_policy/memory_calloc_with_policy). + void* result = heap_caps_realloc(ptr, size, required_caps | desired_caps); + if (result == nullptr && desired_caps != 0) { + result = heap_caps_realloc(ptr, size, required_caps); + } + return result; +} + +void* memory_calloc_with_policy(size_t count, size_t size, const struct MemoryPolicy* policy) { + uint32_t required_caps = toHeapCaps(policy->required); + uint32_t desired_caps = toHeapCaps(policy->desired); + + void* ptr; + if (policy->alignment > 0) { + ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps | desired_caps); + if (ptr == nullptr && desired_caps != 0) { + ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps); + } + } else { + ptr = heap_caps_calloc(count, size, required_caps | desired_caps); + if (ptr == nullptr && desired_caps != 0) { + ptr = heap_caps_calloc(count, size, required_caps); + } + } + return ptr; +} + +void memory_free(void* ptr) { + heap_caps_free(ptr); +} + +} // extern "C" + +#endif // ESP_PLATFORM diff --git a/TactilityKernel/source/memory_posix.cpp b/TactilityKernel/source/memory_posix.cpp new file mode 100644 index 000000000..0e0427476 --- /dev/null +++ b/TactilityKernel/source/memory_posix.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef ESP_PLATFORM + +#include + +#include +#include +#include + +namespace { + +// posix_memalign requires a power-of-2 alignment that's at least sizeof(void*). +size_t normalizeAlignment(uint8_t alignment) { + size_t result = alignment; + if (result < sizeof(void*)) { + result = sizeof(void*); + } + return result; +} + +} // namespace + +extern "C" { + +// MEMORY_CAP_* flags are meaningless on the desktop simulator (no capability-restricted memory regions) +// policy->required/desired are intentionally ignored here. +void* memory_alloc_with_policy(size_t size, const struct MemoryPolicy* policy) { + if (policy->alignment > 0) { + void* ptr = nullptr; + if (posix_memalign(&ptr, normalizeAlignment(policy->alignment), size) != 0) { + return nullptr; + } + return ptr; + } + return malloc(size); +} + +void* memory_realloc_with_policy(void* ptr, size_t size, const struct MemoryPolicy* policy) { + // Alignment can't be preserved across a POSIX realloc; only honored on fresh allocations + // (memory_alloc_with_policy/memory_calloc_with_policy). + return realloc(ptr, size); +} + +void* memory_calloc_with_policy(size_t count, size_t size, const struct MemoryPolicy* policy) { + if (policy->alignment > 0) { + size_t total_size = count * size; + if (count != 0 && total_size / count != size) { + // count * size overflowed - reject rather than under-allocating. + return nullptr; + } + + void* ptr = nullptr; + if (posix_memalign(&ptr, normalizeAlignment(policy->alignment), total_size) != 0) { + return nullptr; + } + memset(ptr, 0, total_size); + return ptr; + } + return calloc(count, size); +} + +void memory_free(void* ptr) { + free(ptr); +} + +} // extern "C" + +#endif // !ESP_PLATFORM diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index b64fdfd0c..22248b3ca 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -175,6 +176,13 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(file_system_unmount), DEFINE_MODULE_SYMBOL(file_system_is_mounted), DEFINE_MODULE_SYMBOL(file_system_get_path), + // memory + DEFINE_MODULE_SYMBOL(MEMORY_POLICY_DEFAULT), + DEFINE_MODULE_SYMBOL(memory_print_stats), + DEFINE_MODULE_SYMBOL(memory_alloc_with_policy), + DEFINE_MODULE_SYMBOL(memory_realloc_with_policy), + DEFINE_MODULE_SYMBOL(memory_calloc_with_policy), + DEFINE_MODULE_SYMBOL(memory_free), // drivers/gpio_controller DEFINE_MODULE_SYMBOL(gpio_descriptor_acquire), DEFINE_MODULE_SYMBOL(gpio_descriptor_release), diff --git a/Tests/TactilityKernel/Source/MemoryTest.cpp b/Tests/TactilityKernel/Source/MemoryTest.cpp new file mode 100644 index 000000000..eebb0c4ca --- /dev/null +++ b/Tests/TactilityKernel/Source/MemoryTest.cpp @@ -0,0 +1,83 @@ +#include "doctest.h" +#include + +#include +#include + +TEST_CASE("MEMORY_POLICY_DEFAULT should have no requirements") { + CHECK_EQ(MEMORY_POLICY_DEFAULT.required, 0); + CHECK_EQ(MEMORY_POLICY_DEFAULT.desired, 0); + CHECK_EQ(MEMORY_POLICY_DEFAULT.alignment, 0); +} + +TEST_CASE("memory_alloc should return usable memory") { + void* ptr = memory_alloc(64); + REQUIRE_NE(ptr, nullptr); + memset(ptr, 0xAB, 64); + CHECK_EQ(static_cast(ptr)[0], 0xAB); + CHECK_EQ(static_cast(ptr)[63], 0xAB); + memory_free(ptr); +} + +TEST_CASE("memory_calloc should zero-initialize memory") { + auto* ptr = static_cast(memory_calloc(16, sizeof(uint8_t))); + REQUIRE_NE(ptr, nullptr); + for (size_t i = 0; i < 16; i++) { + CHECK_EQ(ptr[i], 0); + } + memory_free(ptr); +} + +TEST_CASE("memory_realloc should preserve contents when growing") { + auto* ptr = static_cast(memory_alloc(8)); + REQUIRE_NE(ptr, nullptr); + for (uint8_t i = 0; i < 8; i++) { + ptr[i] = i; + } + + auto* grown = static_cast(memory_realloc(ptr, 32)); + REQUIRE_NE(grown, nullptr); + for (uint8_t i = 0; i < 8; i++) { + CHECK_EQ(grown[i], i); + } + + memory_free(grown); +} + +TEST_CASE("memory_realloc with a NULL pointer should behave like an allocation") { + void* ptr = memory_realloc(nullptr, 32); + REQUIRE_NE(ptr, nullptr); + memset(ptr, 0, 32); + memory_free(ptr); +} + +TEST_CASE("memory_free with a NULL pointer should be a no-op") { + memory_free(nullptr); +} + +TEST_CASE("memory_alloc_with_policy should honor a power-of-2 alignment") { + MemoryPolicy policy = MEMORY_POLICY_DEFAULT; + policy.alignment = 64; + + void* ptr = memory_alloc_with_policy(128, &policy); + REQUIRE_NE(ptr, nullptr); + CHECK_EQ(reinterpret_cast(ptr) % 64, 0); + memory_free(ptr); +} + +TEST_CASE("memory_calloc_with_policy should honor alignment and zero-initialize") { + MemoryPolicy policy = MEMORY_POLICY_DEFAULT; + policy.alignment = 32; + + auto* ptr = static_cast(memory_calloc_with_policy(8, sizeof(uint32_t), &policy)); + REQUIRE_NE(ptr, nullptr); + CHECK_EQ(reinterpret_cast(ptr) % 32, 0); + for (size_t i = 0; i < 8 * sizeof(uint32_t); i++) { + CHECK_EQ(ptr[i], 0); + } + memory_free(ptr); +} + +TEST_CASE("memory_print_stats should not crash") { + memory_print_stats(); +}