diff --git a/Buildscripts/DevicetreeCompiler/source/generator.py b/Buildscripts/DevicetreeCompiler/source/generator.py index 7b60c6f9..49d88d3a 100644 --- a/Buildscripts/DevicetreeCompiler/source/generator.py +++ b/Buildscripts/DevicetreeCompiler/source/generator.py @@ -135,7 +135,7 @@ def write_device_init(file, device: Device, bindings: list[Binding], verbose: bo identifier = get_device_identifier_safe(device) device_variable = identifier # Write device struct - file.write(f"\tif (init_builtin_device(&{device_variable}, \"{compatible_property.value}\") != 0) return -1;\n") + file.write(f"\tif (device_construct_add_start(&{device_variable}, \"{compatible_property.value}\") != ERROR_NONE) return ERROR_RESOURCE;\n") # Write children for child_device in device.devices: write_device_init(file, child_device, bindings, verbose) @@ -145,7 +145,7 @@ def generate_devicetree_c(filename: str, items: list[object], bindings: list[Bin file.write(dedent('''\ // Default headers #include - #include + #include #include // DTS headers ''')) @@ -156,39 +156,17 @@ def generate_devicetree_c(filename: str, items: list[object], bindings: list[Bin write_include(file, item, verbose) file.write("\n") - file.write(dedent('''\ - #define TAG LOG_TAG(devicetree) - - static int init_builtin_device(struct Device* device, const char* compatible) { - struct Driver* driver = driver_find_compatible(compatible); - if (driver == NULL) { - LOG_E(TAG, "Can't find driver: %s", compatible); - return -1; - } - device_construct(device); - device_set_driver(device, driver); - device_add(device); - const int err = device_start(device); - if (err != 0) { - LOG_E(TAG, "Failed to start device %s with driver %s: error code %d", device->name, compatible, err); - return -1; - } - return 0; - } - - ''')) - # Then write all devices for item in items: if type(item) is Device: write_device_structs(file, item, None, bindings, verbose) # Init function body start - file.write("int devices_builtin_init() {\n") + file.write("error_t devices_builtin_init() {\n") # Init function body logic for item in items: if type(item) is Device: write_device_init(file, item, bindings, verbose) - file.write("\treturn 0;\n") + file.write("\treturn ERROR_NONE;\n") # Init function body end file.write("}\n") @@ -196,12 +174,13 @@ def generate_devicetree_h(filename: str): with open(filename, "w") as file: file.write(dedent('''\ #pragma once + #include #ifdef __cplusplus extern "C" { #endif - extern int devices_builtin_init(); + extern error_t devices_builtin_init(); #ifdef __cplusplus } diff --git a/CODING_STYLE_C.md b/CODING_STYLE_C.md index f48219db..3ef70188 100644 --- a/CODING_STYLE_C.md +++ b/CODING_STYLE_C.md @@ -1,6 +1,6 @@ # C coding Style -## Naming +## Files & Folders ### Files @@ -8,7 +8,7 @@ Files are lower snake case. - Files: `^[0-9a-z_]+$` - Directories: `^[0-9a-z_]+$` - + Example: ```c some_feature.c @@ -22,6 +22,8 @@ Project folders include: - `private` for private header files - `include` for projects that require separate header files +## C language + ### Macros and consts These are all upper snake case: @@ -94,3 +96,35 @@ Examples: ```c typedef uint32_t thread_id_t; ``` + +### Function comments + +```c +/** + * @brief Validates a number + * @param[in] number the integer to validate + * @return true if validation was succesful and there were no issues + */ +bool validate(int number); + +/** + * @brief Run the action. + * @param timeout[in] the maximum time the task should run + * @retval ERROR_TIMEOUT when the task couldn't be completed on time + * @retval ERROR_NONE when the task completed successfully + */ +error_t runAction(TickType_t timeout); + +/** + * @brief Increase a number. + * @param[inout] number + */ +void increase(int* number); + +/** + * A function with a longer description here. + * + * @brief short description + */ +void something(); +``` diff --git a/Devices/lilygo-tlora-pager/Source/Configuration.cpp b/Devices/lilygo-tlora-pager/Source/Configuration.cpp index a555e4b8..fd025898 100644 --- a/Devices/lilygo-tlora-pager/Source/Configuration.cpp +++ b/Devices/lilygo-tlora-pager/Source/Configuration.cpp @@ -38,25 +38,6 @@ static DeviceVector createDevices() { extern const Configuration hardwareConfiguration = { .initBoot = tpagerInit, .createDevices = createDevices, - .i2c = { - i2c::Configuration { - .name = "Internal", - .port = I2C_NUM_0, - .initMode = i2c::InitMode::ByTactility, - .isMutable = false, - .config = (i2c_config_t) { - .mode = I2C_MODE_MASTER, - .sda_io_num = GPIO_NUM_3, - .scl_io_num = GPIO_NUM_2, - .sda_pullup_en = false, - .scl_pullup_en = false, - .master = { - .clk_speed = 100'000 - }, - .clk_flags = 0 - } - } - }, .spi {spi::Configuration { .device = SPI2_HOST, .dma = SPI_DMA_CH_AUTO, diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts index 06aca8f8..0e43a8ab 100644 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts +++ b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts @@ -15,9 +15,9 @@ i2c0 { compatible = "espressif,esp32-i2c"; - clock-frequency = <100000>; - pin-sda = <&gpio0 3 GPIO_ACTIVE_HIGH>; - pin-scl = <&gpio0 2 GPIO_ACTIVE_HIGH>; port = ; + clock-frequency = <100000>; + pin-sda = <3>; + pin-scl = <2>; }; }; diff --git a/Platforms/PlatformEsp32/Bindings/espressif,esp32-i2c.yaml b/Platforms/PlatformEsp32/Bindings/espressif,esp32-i2c.yaml index 96968f3d..553fbf80 100644 --- a/Platforms/PlatformEsp32/Bindings/espressif,esp32-i2c.yaml +++ b/Platforms/PlatformEsp32/Bindings/espressif,esp32-i2c.yaml @@ -10,3 +10,16 @@ properties: description: | The port number, defined by i2c_port_t. Depending on the hardware, these values are available: I2C_NUM_0, I2C_NUM_1, LP_I2C_NUM_0 + clock-frequency: + type: int + description: Initial clock frequency in Hz + pin-sda: + type: int + pin-scl: + type: int + pin-sda-pull-up: + type: bool + description: enable internal pull-up resistor for SDA pin + pin-scl-pull-up: + type: bool + description: enable internal pull-up resistor for SCL pin diff --git a/Platforms/PlatformEsp32/Include/tactility/drivers/esp32_i2c.h b/Platforms/PlatformEsp32/Include/tactility/drivers/esp32_i2c.h index b57d51de..e210ddcd 100644 --- a/Platforms/PlatformEsp32/Include/tactility/drivers/esp32_i2c.h +++ b/Platforms/PlatformEsp32/Include/tactility/drivers/esp32_i2c.h @@ -9,12 +9,18 @@ extern "C" { #endif struct Esp32I2cConfig { + i2c_port_t port; uint32_t clockFrequency; - struct GpioPinConfig pinSda; - struct GpioPinConfig pinScl; - const i2c_port_t port; + gpio_pin_t pinSda; + gpio_pin_t pinScl; + bool pinSdaPullUp; + bool pinSclPullUp; }; +error_t esp32_i2c_get_port(struct Device* device, i2c_port_t* port); +void esp32_i2c_lock(struct Device* device); +void esp32_i2c_unlock(struct Device* device); + #ifdef __cplusplus } #endif diff --git a/Platforms/PlatformEsp32/Include/tactility/error_esp32.h b/Platforms/PlatformEsp32/Include/tactility/error_esp32.h index e0ab898c..d885efcd 100644 --- a/Platforms/PlatformEsp32/Include/tactility/error_esp32.h +++ b/Platforms/PlatformEsp32/Include/tactility/error_esp32.h @@ -4,4 +4,5 @@ #include +/** Convert an esp_err_t to an error_t */ error_t esp_err_to_error(esp_err_t error); diff --git a/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp b/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp index 081ed299..c78b824f 100644 --- a/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp +++ b/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp @@ -5,10 +5,12 @@ #include #include +#include #include #include #define TAG LOG_TAG(esp32_i2c) +#define ACK_CHECK_EN 1 struct InternalData { Mutex mutex { 0 }; @@ -30,46 +32,175 @@ struct InternalData { extern "C" { -static int read(Device* device, uint8_t address, uint8_t* data, size_t data_size, TickType_t timeout) { - vPortAssertIfInISR(); +static error_t read(Device* device, uint8_t address, uint8_t* data, size_t data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; auto* driver_data = GET_DATA(device); lock(driver_data); const esp_err_t esp_error = i2c_master_read_from_device(GET_CONFIG(device)->port, address, data, data_size, timeout); unlock(driver_data); - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_error); return esp_err_to_error(esp_error); } -static int write(Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { - vPortAssertIfInISR(); +static error_t write(Device* device, uint8_t address, const uint8_t* data, uint16_t data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; auto* driver_data = GET_DATA(device); lock(driver_data); - const esp_err_t esp_error = i2c_master_write_to_device(GET_CONFIG(device)->port, address, data, dataSize, timeout); + const esp_err_t esp_error = i2c_master_write_to_device(GET_CONFIG(device)->port, address, data, data_size, timeout); unlock(driver_data); - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_error); return esp_err_to_error(esp_error); } -static int write_read(Device* device, uint8_t address, const uint8_t* write_data, size_t write_data_size, uint8_t* read_data, size_t read_data_size, TickType_t timeout) { - vPortAssertIfInISR(); +static error_t write_read(Device* device, uint8_t address, const uint8_t* write_data, size_t write_data_size, uint8_t* read_data, size_t read_data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (write_data_size == 0 || read_data_size == 0) return ERROR_INVALID_ARGUMENT; auto* driver_data = GET_DATA(device); lock(driver_data); const esp_err_t esp_error = i2c_master_write_read_device(GET_CONFIG(device)->port, address, write_data, write_data_size, read_data, read_data_size, timeout); unlock(driver_data); - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_error); return esp_err_to_error(esp_error); } -static int start(Device* device) { +static error_t read_register(Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t data_size, TickType_t timeout) { + auto start_time = get_ticks(); + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; + auto* driver_data = GET_DATA(device); + + lock(driver_data); + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + esp_err_t error = ESP_OK; + if (cmd == nullptr) { + error = ESP_ERR_NO_MEM; + goto on_error; + } + // Set address pointer + error = i2c_master_start(cmd); + if (error != ESP_OK) goto on_error; + error = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN); + if (error != ESP_OK) goto on_error; + error = i2c_master_write(cmd, ®, 1, ACK_CHECK_EN); + if (error != ESP_OK) goto on_error; + // Read length of response from current pointer + error = i2c_master_start(cmd); + if (error != ESP_OK) goto on_error; + error = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, ACK_CHECK_EN); + if (error != ESP_OK) goto on_error; + if (data_size > 1) { + error = i2c_master_read(cmd, data, data_size - 1, I2C_MASTER_ACK); + if (error != ESP_OK) goto on_error; + } + error = i2c_master_read_byte(cmd, data + data_size - 1, I2C_MASTER_NACK); + if (error != ESP_OK) goto on_error; + error = i2c_master_stop(cmd); + if (error != ESP_OK) goto on_error; + error = i2c_master_cmd_begin(GET_CONFIG(device)->port, cmd, get_timeout_remaining_ticks(timeout, start_time)); + if (error != ESP_OK) goto on_error; + + i2c_cmd_link_delete(cmd); + unlock(driver_data); + return esp_err_to_error(error); + +on_error: + i2c_cmd_link_delete(cmd); + unlock(driver_data); + return esp_err_to_error(error); +} + +static error_t write_register(Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t data_size, TickType_t timeout) { + auto start_time = get_ticks(); + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; + auto* driver_data = GET_DATA(device); + + lock(driver_data); + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + esp_err_t error = ESP_OK; + if (cmd == nullptr) { + error = ESP_ERR_NO_MEM; + goto on_error; + } + error = i2c_master_start(cmd); + if (error != ESP_OK) goto on_error; + error = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN); + if (error != ESP_OK) goto on_error; + error = i2c_master_write_byte(cmd, reg, ACK_CHECK_EN); + if (error != ESP_OK) goto on_error; + error = i2c_master_write(cmd, (uint8_t*) data, data_size, ACK_CHECK_EN); + if (error != ESP_OK) goto on_error; + error = i2c_master_stop(cmd); + if (error != ESP_OK) goto on_error; + error = i2c_master_cmd_begin(GET_CONFIG(device)->port, cmd, get_timeout_remaining_ticks(timeout, start_time)); + if (error != ESP_OK) goto on_error; + + i2c_cmd_link_delete(cmd); + unlock(driver_data); + return esp_err_to_error(error); + +on_error: + i2c_cmd_link_delete(cmd); + unlock(driver_data); + return esp_err_to_error(error); +} + +error_t esp32_i2c_get_port(struct Device* device, i2c_port_t* port) { + auto* config = GET_CONFIG(device); + *port = config->port; + return ERROR_NONE; +} + +void esp32_i2c_lock(struct Device* device) { + mutex_lock(&GET_DATA(device)->mutex); +} + +void esp32_i2c_unlock(struct Device* device) { + mutex_unlock(&GET_DATA(device)->mutex); +} + +static error_t start(Device* device) { ESP_LOGI(TAG, "start %s", device->name); + auto dts_config = GET_CONFIG(device); + + i2c_config_t esp_config = { + .mode = I2C_MODE_MASTER, + .sda_io_num = dts_config->pinSda, + .scl_io_num = dts_config->pinScl, + .sda_pullup_en = dts_config->pinSdaPullUp, + .scl_pullup_en = dts_config->pinSclPullUp, + .master { + .clk_speed = dts_config->clockFrequency + }, + .clk_flags = 0 + }; + + esp_err_t error = i2c_param_config(dts_config->port, &esp_config); + if (error != ESP_OK) { + LOG_E(TAG, "Failed to configure port %d: %s", static_cast(dts_config->port), esp_err_to_name(error)); + return ERROR_RESOURCE; + } + + error = i2c_driver_install(dts_config->port, esp_config.mode, 0, 0, 0); + if (error != ESP_OK) { + LOG_E(TAG, "Failed to install driver at port %d: %s", static_cast(dts_config->port), esp_err_to_name(error)); + return ERROR_RESOURCE; + } auto* data = new InternalData(); device_set_driver_data(device, data); return ERROR_NONE; } -static int stop(Device* device) { +static error_t stop(Device* device) { ESP_LOGI(TAG, "stop %s", device->name); auto* driver_data = static_cast(device_get_driver_data(device)); + + i2c_port_t port = GET_CONFIG(device)->port; + esp_err_t result = i2c_driver_delete(port); + if (result != ESP_OK) { + LOG_E(TAG, "Failed to delete driver at port %d: %s", static_cast(port), esp_err_to_name(result)); + return ERROR_RESOURCE; + } + device_set_driver_data(device, nullptr); delete driver_data; return ERROR_NONE; @@ -78,7 +209,9 @@ static int stop(Device* device) { const static I2cControllerApi esp32_i2c_api = { .read = read, .write = write, - .write_read = write_read + .write_read = write_read, + .read_register = read_register, + .write_register = write_register }; Driver esp32_i2c_driver = { diff --git a/Platforms/PlatformEsp32/Source/error_esp32.cpp b/Platforms/PlatformEsp32/Source/error_esp32.cpp index 62f72dd5..da5d4a2c 100644 --- a/Platforms/PlatformEsp32/Source/error_esp32.cpp +++ b/Platforms/PlatformEsp32/Source/error_esp32.cpp @@ -11,6 +11,10 @@ error_t esp_err_to_error(esp_err_t error) { return ERROR_INVALID_STATE; case ESP_ERR_TIMEOUT: return ERROR_TIMEOUT; + case ESP_ERR_NO_MEM: + return ERROR_OUT_OF_MEMORY; + case ESP_ERR_NOT_SUPPORTED: + return ERROR_NOT_SUPPORTED; default: return ERROR_UNDEFINED; } diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index ba8c0430..540d8163 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -5,6 +5,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) list(APPEND REQUIRES_LIST TactilityKernel + PlatformEsp32 TactilityCore TactilityFreeRtos lvgl @@ -54,31 +55,27 @@ else() add_library(Tactility OBJECT) - target_sources(Tactility - PRIVATE ${SOURCES} - ) - - include_directories( - PRIVATE Private/ - ) + target_sources(Tactility PRIVATE ${SOURCES}) target_include_directories(Tactility + PRIVATE Private/ PUBLIC Include/ ) add_definitions(-D_Nullable=) add_definitions(-D_Nonnull=) - target_link_libraries(Tactility - PUBLIC cJSON - PUBLIC TactilityFreeRtos - PUBLIC TactilityCore - PUBLIC TactilityKernel - PUBLIC freertos_kernel - PUBLIC lvgl - PUBLIC lv_screenshot - PUBLIC minmea - PUBLIC minitar + target_link_libraries(Tactility PUBLIC + cJSON + TactilityFreeRtos + TactilityCore + TactilityKernel + PlatformPosix + freertos_kernel + lvgl + lv_screenshot + minmea + minitar ) endif() diff --git a/Tactility/Include/Tactility/hal/i2c/I2c.h b/Tactility/Include/Tactility/hal/i2c/I2c.h index a31c51b8..9e7ab4f5 100644 --- a/Tactility/Include/Tactility/hal/i2c/I2c.h +++ b/Tactility/Include/Tactility/hal/i2c/I2c.h @@ -14,7 +14,6 @@ constexpr TickType_t defaultTimeout = 10 / portTICK_PERIOD_MS; enum class InitMode { ByTactility, // Tactility will initialize it in the correct bootup phase - ByExternal, // The device is already initialized and Tactility should assume it works Disabled // Not initialized by default }; @@ -39,15 +38,6 @@ enum class Status { Unknown }; -/** - * Reconfigure a port with the provided settings. - * @warning This fails when the HAL Configuration is not mutable. - * @param[in] port the port to reconfigure - * @param[in] configuration the new configuration - * @return true on success - */ -bool configure(i2c_port_t port, const i2c_config_t& configuration); - /** * Start the bus for the specified port. * Devices might be started automatically at boot if their HAL configuration requires it. @@ -60,6 +50,9 @@ bool stop(i2c_port_t port); /** @return true if the bus is started */ bool isStarted(i2c_port_t port); +/** @return name or nullptr */ +const char* getName(i2c_port_t port); + /** Read bytes in master mode. */ bool masterRead(i2c_port_t port, uint8_t address, uint8_t* data, size_t dataSize, TickType_t timeout = defaultTimeout); diff --git a/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h b/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h index 37872ba1..a898468a 100644 --- a/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h +++ b/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h @@ -9,4 +9,6 @@ std::string getAddressText(uint8_t address); std::string getPortNamesForDropdown(); +bool getActivePortAtIndex(int32_t index, int32_t& out); + } diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 3293dc84..c262797d 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -74,16 +74,13 @@ namespace service { namespace app { namespace addgps { extern const AppManifest manifest; } + namespace alertdialog { extern const AppManifest manifest; } namespace apphub { extern const AppManifest manifest; } namespace apphubdetails { extern const AppManifest manifest; } - namespace alertdialog { extern const AppManifest manifest; } namespace appdetails { extern const AppManifest manifest; } namespace applist { extern const AppManifest manifest; } namespace appsettings { extern const AppManifest manifest; } namespace boot { extern const AppManifest manifest; } -#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED) - namespace chat { extern const AppManifest manifest; } -#endif namespace development { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; } namespace files { extern const AppManifest manifest; } @@ -94,9 +91,6 @@ namespace app { namespace imageviewer { extern const AppManifest manifest; } namespace inputdialog { extern const AppManifest manifest; } namespace launcher { extern const AppManifest manifest; } -#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) - namespace keyboardsettings { extern const AppManifest manifest; } -#endif namespace localesettings { extern const AppManifest manifest; } namespace notes { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; } @@ -105,21 +99,27 @@ namespace app { namespace systeminfo { extern const AppManifest manifest; } namespace timedatesettings { extern const AppManifest manifest; } namespace timezone { extern const AppManifest manifest; } -#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) - namespace trackballsettings { extern const AppManifest manifest; } -#endif namespace usbsettings { extern const AppManifest manifest; } namespace wifiapsettings { extern const AppManifest manifest; } namespace wificonnect { extern const AppManifest manifest; } namespace wifimanage { extern const AppManifest manifest; } + #ifdef ESP_PLATFORM + namespace crashdiagnostics { extern const AppManifest manifest; } namespace webserversettings { extern const AppManifest manifest; } #endif + +#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) + namespace keyboardsettings { extern const AppManifest manifest; } + namespace trackballsettings { extern const AppManifest manifest; } +#endif + #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const AppManifest manifest; } #endif -#ifdef ESP_PLATFORM - namespace crashdiagnostics { extern const AppManifest manifest; } + +#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED) + namespace chat { extern const AppManifest manifest; } #endif } @@ -138,12 +138,11 @@ static void registerInternalApps() { addAppManifest(app::display::manifest); addAppManifest(app::files::manifest); addAppManifest(app::fileselection::manifest); + addAppManifest(app::i2cscanner::manifest); + addAppManifest(app::i2csettings::manifest); addAppManifest(app::imageviewer::manifest); addAppManifest(app::inputdialog::manifest); addAppManifest(app::launcher::manifest); -#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) - addAppManifest(app::keyboardsettings::manifest); -#endif addAppManifest(app::localesettings::manifest); addAppManifest(app::notes::manifest); addAppManifest(app::settings::manifest); @@ -151,14 +150,19 @@ static void registerInternalApps() { addAppManifest(app::systeminfo::manifest); addAppManifest(app::timedatesettings::manifest); addAppManifest(app::timezone::manifest); -#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) - addAppManifest(app::trackballsettings::manifest); -#endif addAppManifest(app::wifiapsettings::manifest); addAppManifest(app::wificonnect::manifest); addAppManifest(app::wifimanage::manifest); + #ifdef ESP_PLATFORM addAppManifest(app::webserversettings::manifest); + addAppManifest(app::crashdiagnostics::manifest); + addAppManifest(app::development::manifest); +#endif + +#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) + addAppManifest(app::keyboardsettings::manifest); + addAppManifest(app::trackballsettings::manifest); #endif #if defined(CONFIG_TINYUSB_MSC_ENABLED) && CONFIG_TINYUSB_MSC_ENABLED @@ -173,16 +177,6 @@ static void registerInternalApps() { addAppManifest(app::chat::manifest); #endif -#ifdef ESP_PLATFORM - addAppManifest(app::crashdiagnostics::manifest); - addAppManifest(app::development::manifest); -#endif - - if (!hal::getConfiguration()->i2c.empty()) { - addAppManifest(app::i2cscanner::manifest); - addAppManifest(app::i2csettings::manifest); - } - if (!hal::getConfiguration()->uart.empty()) { addAppManifest(app::addgps::manifest); addAppManifest(app::gpssettings::manifest); diff --git a/Tactility/Source/app/i2cscanner/I2cHelpers.cpp b/Tactility/Source/app/i2cscanner/I2cHelpers.cpp index ade8058c..985f0240 100644 --- a/Tactility/Source/app/i2cscanner/I2cHelpers.cpp +++ b/Tactility/Source/app/i2cscanner/I2cHelpers.cpp @@ -19,18 +19,31 @@ std::string getAddressText(uint8_t address) { std::string getPortNamesForDropdown() { std::vector config_names; - size_t port_index = 0; - for (const auto& i2c_config: tt::getConfiguration()->hardware->i2c) { - if (!i2c_config.name.empty()) { - config_names.push_back(i2c_config.name); - } else { - std::stringstream stream; - stream << "Port " << std::to_string(port_index); - config_names.push_back(stream.str()); + for (int port = 0; port < I2C_NUM_MAX; ++port) { + auto native_port = static_cast(port); + if (hal::i2c::isStarted(native_port)) { + auto* name = hal::i2c::getName(native_port); + if (name != nullptr) { + config_names.push_back(name); + } } - port_index++; } return string::join(config_names, "\n"); } +bool getActivePortAtIndex(int32_t index, int32_t& out) { + int current_index = -1; + for (int port = 0; port < I2C_NUM_MAX; ++port) { + auto native_port = static_cast(port); + if (hal::i2c::isStarted(native_port)) { + current_index++; + if (current_index == index) { + out = port; + return true; + } + } + } + return false; +} + } diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index 101bca32..280142b5 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -141,11 +141,10 @@ void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN); scanListWidget = scan_list; - auto i2c_devices = getConfiguration()->hardware->i2c; - if (!i2c_devices.empty()) { - assert(selected_bus < i2c_devices.size()); - port = i2c_devices[selected_bus].port; - selectBus(selected_bus); + int32_t first_port; + if (getActivePortAtIndex(0, first_port)) { + lv_dropdown_set_selected(port_dropdown, 0); + selectBus(0); } } @@ -307,12 +306,14 @@ void I2cScannerApp::onSelectBus(lv_event_t* event) { } void I2cScannerApp::selectBus(int32_t selected) { - auto i2c_devices = getConfiguration()->hardware->i2c; - assert(selected < i2c_devices.size()); + int32_t found_port; + if (!getActivePortAtIndex(selected, found_port)) { + return; + } if (mutex.lock(100 / portTICK_PERIOD_MS)) { scannedAddresses.clear(); - port = i2c_devices[selected].port; + port = static_cast(found_port); scanState = ScanStateInitial; mutex.unlock(); } diff --git a/Tactility/Source/hal/i2c/I2c.cpp b/Tactility/Source/hal/i2c/I2c.cpp index 7f970382..8679b4b2 100644 --- a/Tactility/Source/hal/i2c/I2c.cpp +++ b/Tactility/Source/hal/i2c/I2c.cpp @@ -2,7 +2,14 @@ #include #include + #include +#include +#include + +#ifdef ESP_PLATFORM +#include +#endif namespace tt::hal::i2c { @@ -11,270 +18,250 @@ static const auto LOGGER = Logger("I2C"); struct Data { Mutex mutex; bool isConfigured = false; - bool isStarted = false; - Configuration configuration; + Device* device = nullptr; +#ifdef ESP_PLATFORM + Esp32I2cConfig config = { + .port = I2C_NUM_0, + .clockFrequency = 0, + .pinSda = 0, + .pinScl = 0, + .pinSdaPullUp = false, + .pinSclPullUp = false + }; +#endif }; -static const uint8_t ACK_CHECK_EN = 1; static Data dataArray[I2C_NUM_MAX]; +#ifdef ESP_PLATFORM +void registerDriver(Data& data, const Configuration& configuration) { + // Should only be called on init + check(data.device == nullptr); + + data.config.port = configuration.port; + data.config.clockFrequency = configuration.config.master.clk_speed; + data.config.pinSda = configuration.config.sda_io_num; + data.config.pinScl = configuration.config.scl_io_num; + data.config.pinSdaPullUp = configuration.config.sda_pullup_en; + data.config.pinSclPullUp = configuration.config.scl_pullup_en; + + data.device = new Device(); + data.device->name = configuration.name.c_str(); + data.device->config = &data.config; + data.device->parent = nullptr; + + if (device_construct_add(data.device, "espressif,esp32-i2c") == ERROR_NONE) { + data.isConfigured = true; + } +} + +Device* findExistingKernelDevice(i2c_port_t port) { + struct Params { + i2c_port_t port; + Device* device; + }; + + Params params = { + .port = port, + .device = nullptr + }; + + for_each_device_of_type(&I2C_CONTROLLER_TYPE, ¶ms, [](auto* device, auto* context) { + auto* params_ptr = (Params*)context; + auto* driver = device_get_driver(device); + if (driver == nullptr) return true; + if (!driver_is_compatible(driver, "espressif,esp32-i2c")) return true; + i2c_port_t port; + if (esp32_i2c_get_port(device, &port) != ERROR_NONE) return true; + if (port != params_ptr->port) return true; + // Found it, stop iterating + params_ptr->device = device; + return false; + }); + + return params.device; +} +#endif + bool init(const std::vector& configurations) { LOGGER.info("Init"); - for (const auto& configuration: configurations) { #ifdef ESP_PLATFORM - if (configuration.config.mode != I2C_MODE_MASTER) { - LOGGER.error("Currently only master mode is supported"); - return false; - } -#endif // ESP_PLATFORM - Data& data = dataArray[configuration.port]; - data.configuration = configuration; - data.isConfigured = true; - } - - for (const auto& config: configurations) { - if (config.initMode == InitMode::ByTactility) { - if (!start(config.port)) { - return false; - } - } else if (config.initMode == InitMode::ByExternal) { - dataArray[config.port].isStarted = true; - } - } - - return true; -} - -bool configure(i2c_port_t port, const i2c_config_t& configuration) { - auto lock = getLock(port).asScopedLock(); - lock.lock(); - - Data& data = dataArray[port]; - if (data.isStarted) { - LOGGER.error("({}) Cannot reconfigure while interface is started", static_cast(port)); - return false; - } else if (!data.configuration.isMutable) { - LOGGER.error("({}) Mutation not allowed because configuration is immutable", static_cast(port)); - return false; - } else { - data.configuration.config = configuration; - return true; + bool found_existing = false; + for (int port = 0; port < I2C_NUM_MAX; ++port) { + auto native_port = static_cast(port); + auto existing_device = findExistingKernelDevice(native_port); + if (existing_device != nullptr) { + LOGGER.info("Initialized port {} with existing kernel device", port); + auto& data = dataArray[port]; + data.device = existing_device; + data.isConfigured = true; + memcpy(&data.config, existing_device->config, sizeof(Esp32I2cConfig)); + // Ensure we don't initialize + found_existing = true; + } } + + // Nothing found in HAL, so try configuration + for (const auto& configuration: configurations) { + check(!found_existing, "hal::Configuration specifies I2C, but I2C was already initialized by devicetree. Remove the hal::Configuration I2C entries!"); + if (configuration.config.mode != I2C_MODE_MASTER) { + LOGGER.error("Currently only master mode is supported"); + return false; + } + Data& data = dataArray[configuration.port]; + registerDriver(data, configuration); + } + + if (!found_existing) { + for (const auto& config: configurations) { + if (config.initMode == InitMode::ByTactility) { + if (!start(config.port)) { + return false; + } + } + } + } + +#endif + return true; } bool start(i2c_port_t port) { +#ifdef ESP_PLATFORM auto lock = getLock(port).asScopedLock(); lock.lock(); Data& data = dataArray[port]; - Configuration& config = data.configuration; - - if (data.isStarted) { - LOGGER.error("({}) Starting: Already started", static_cast(port)); - return false; - } if (!data.isConfigured) { LOGGER.error("({}) Starting: Not configured", static_cast(port)); return false; } -#ifdef ESP_PLATFORM - esp_err_t result = i2c_param_config(port, &config.config); - if (result != ESP_OK) { - LOGGER.error("({}) Starting: Failed to configure: {}", static_cast(port), esp_err_to_name(result)); + check(data.device); + + error_t error = device_start(data.device); + if (error != ERROR_NONE) { + LOGGER.error("Failed to start device {}: {}", data.device->name, error_to_string(error)); return false; } - result = i2c_driver_install(port, config.config.mode, 0, 0, 0); - if (result != ESP_OK) { - LOGGER.error("({}) Starting: Failed to install driver: {}", static_cast(port), esp_err_to_name(result)); - return false; - } -#endif // ESP_PLATFORM - - data.isStarted = true; - - LOGGER.info("({}) Started", static_cast(port)); return true; +#else + return false; +#endif } bool stop(i2c_port_t port) { +#ifdef ESP_PLATFORM auto lock = getLock(port).asScopedLock(); lock.lock(); Data& data = dataArray[port]; - Configuration& config = data.configuration; - - if (!config.isMutable) { - LOGGER.error("({}) Stopping: Not allowed for immutable configuration", static_cast(port)); - return false; - } - - if (!data.isStarted) { - LOGGER.error("({}) Stopping: Not started", static_cast(port)); - return false; - } - -#ifdef ESP_PLATFORM - esp_err_t result = i2c_driver_delete(port); - if (result != ESP_OK) { - LOGGER.error("({}) Stopping: Failed to delete driver: {}", static_cast(port), esp_err_to_name(result)); - return false; - } -#endif // ESP_PLATFORM - - data.isStarted = false; - - LOGGER.info("({}) Stopped", static_cast(port)); - return true; + if (!dataArray[port].isConfigured) return false; + return device_stop(data.device) == ERROR_NONE; +#else + return false; +#endif } bool isStarted(i2c_port_t port) { +#ifdef ESP_PLATFORM auto lock = getLock(port).asScopedLock(); lock.lock(); - return dataArray[port].isStarted; + if (!dataArray[port].isConfigured) return false; + return device_is_ready(dataArray[port].device); +#else + return false; +#endif +} + +const char* getName(i2c_port_t port) { +#ifdef ESP_PLATFORM + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return nullptr; + return dataArray[port].device->name; +#else + return nullptr; +#endif } bool masterRead(i2c_port_t port, uint8_t address, uint8_t* data, size_t dataSize, TickType_t timeout) { - auto lock = getLock(port).asScopedLock(); - if (!lock.lock(timeout)) { - LOGGER.error("({}) Mutex timeout", static_cast(port)); - return false; - } - #ifdef ESP_PLATFORM - auto result = i2c_master_read_from_device(port, address, data, dataSize, timeout); - ESP_ERROR_CHECK_WITHOUT_ABORT(result); - return result == ESP_OK; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_read(dataArray[port].device, address, data, dataSize, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } bool masterReadRegister(i2c_port_t port, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout) { - auto lock = getLock(port).asScopedLock(); - if (!lock.lock(timeout)) { - LOGGER.error("({}) Mutex timeout", static_cast(port)); - return false; - } - #ifdef ESP_PLATFORM - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - // Set address pointer - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN); - i2c_master_write(cmd, ®, 1, ACK_CHECK_EN); - // Read length of response from current pointer - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, ACK_CHECK_EN); - if (dataSize > 1) { - i2c_master_read(cmd, data, dataSize - 1, I2C_MASTER_ACK); - } - i2c_master_read_byte(cmd, data + dataSize - 1, I2C_MASTER_NACK); - i2c_master_stop(cmd); - // TODO: We're passing an inaccurate timeout value as we already lost time with locking - esp_err_t result = i2c_master_cmd_begin(port, cmd, timeout); - i2c_cmd_link_delete(cmd); - - ESP_ERROR_CHECK_WITHOUT_ABORT(result); - - return result == ESP_OK; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_read_register(dataArray[port].device, address, reg, data, dataSize, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } bool masterWrite(i2c_port_t port, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { - auto lock = getLock(port).asScopedLock(); - if (!lock.lock(timeout)) { - LOGGER.error("({}) Mutex timeout", static_cast(port)); - return false; - } - #ifdef ESP_PLATFORM - auto result = i2c_master_write_to_device(port, address, data, dataSize, timeout); - ESP_ERROR_CHECK_WITHOUT_ABORT(result); - return result == ESP_OK; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_write(dataArray[port].device, address, data, dataSize, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } bool masterWriteRegister(i2c_port_t port, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { - check(reg != 0); - - auto lock = getLock(port).asScopedLock(); - if (!lock.lock(timeout)) { - LOGGER.error("({}) Mutex timeout", static_cast(port)); - return false; - } - #ifdef ESP_PLATFORM - - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - i2c_master_start(cmd); - i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN); - i2c_master_write_byte(cmd, reg, ACK_CHECK_EN); - i2c_master_write(cmd, (uint8_t*) data, dataSize, ACK_CHECK_EN); - i2c_master_stop(cmd); - // TODO: We're passing an inaccurate timeout value as we already lost time with locking - esp_err_t result = i2c_master_cmd_begin(port, cmd, timeout); - i2c_cmd_link_delete(cmd); - - ESP_ERROR_CHECK_WITHOUT_ABORT(result); - return result == ESP_OK; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_write_register(dataArray[port].device, address, reg, data, dataSize, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } bool masterWriteRegisterArray(i2c_port_t port, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { #ifdef ESP_PLATFORM - assert(dataSize % 2 == 0); - bool result = true; - for (int i = 0; i < dataSize; i += 2) { - // TODO: We're passing an inaccurate timeout value as we already lost time with locking and previous writes in this loop - if (!masterWriteRegister(port, address, data[i], &data[i + 1], 1, timeout)) { - result = false; - } - } - return result; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_write_register_array(dataArray[port].device, address, data, dataSize, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } bool masterWriteRead(i2c_port_t port, uint8_t address, const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout) { - auto lock = getLock(port).asScopedLock(); - if (!lock.lock(timeout)) { - LOGGER.error("({}) Mutex timeout", static_cast(port)); - return false; - } - #ifdef ESP_PLATFORM - esp_err_t result = i2c_master_write_read_device(port, address, writeData, writeDataSize, readData, readDataSize, timeout); - ESP_ERROR_CHECK_WITHOUT_ABORT(result); - return result == ESP_OK; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_write_read(dataArray[port].device, address, writeData, writeDataSize, readData, readDataSize, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } bool masterHasDeviceAtAddress(i2c_port_t port, uint8_t address, TickType_t timeout) { - auto lock = getLock(port).asScopedLock(); - if (!lock.lock(timeout)) { - LOGGER.error("({}) Mutex timeout", static_cast(port)); - return false; - } - #ifdef ESP_PLATFORM - uint8_t message[2] = { 0, 0 }; - // TODO: We're passing an inaccurate timeout value as we already lost time with locking - return i2c_master_write_to_device(port, address, message, 2, timeout) == ESP_OK; + auto lock = getLock(port).asScopedLock(); + lock.lock(); + if (!dataArray[port].isConfigured) return false; + return i2c_controller_has_device_at_address(dataArray[port].device, address, timeout) == ERROR_NONE; #else return false; -#endif // ESP_PLATFORM +#endif } Lock& getLock(i2c_port_t port) { diff --git a/TactilityKernel/Bindings/i2c-controller.yaml b/TactilityKernel/Bindings/i2c-controller.yaml index 5f14e180..58401eaa 100644 --- a/TactilityKernel/Bindings/i2c-controller.yaml +++ b/TactilityKernel/Bindings/i2c-controller.yaml @@ -1,10 +1 @@ bus: i2c - -properties: - clock-frequency: - type: int - description: Initial clock frequency in Hz - pin-sda: - type: phandle-array - pin-scl: - type: phandle-array diff --git a/TactilityKernel/CMakeLists.txt b/TactilityKernel/CMakeLists.txt index 5644a317..780a9782 100644 --- a/TactilityKernel/CMakeLists.txt +++ b/TactilityKernel/CMakeLists.txt @@ -7,6 +7,8 @@ if (DEFINED ENV{ESP_IDF_VERSION}) idf_component_register( SRCS ${SOURCES} INCLUDE_DIRS "Include/" + # TODO move the related logic for esp_time in Tactility/time.h into the Platform/ subproject + REQUIRES esp_timer ) else () diff --git a/TactilityKernel/Include/tactility/check.h b/TactilityKernel/Include/tactility/check.h index fcaf5b7f..38807b65 100644 --- a/TactilityKernel/Include/tactility/check.h +++ b/TactilityKernel/Include/tactility/check.h @@ -14,7 +14,7 @@ __attribute__((noreturn)) extern void __crash(void); #define CHECK_NO_MSG(condition) \ do { \ if (!(condition)) { \ - LOG_E("Error", "Check failed: %s at %s:%d", #condition, __FILE__, __LINE__); \ + LOG_E("Error", "Check failed: %s\n\tat %s:%d", #condition, __FILE__, __LINE__); \ __crash(); \ } \ } while (0) @@ -22,7 +22,7 @@ __attribute__((noreturn)) extern void __crash(void); #define CHECK_MSG(condition, message) \ do { \ if (!(condition)) { \ - LOG_E("Error", "Check failed: %s at %s:%d", message, __FILE__, __LINE__); \ + LOG_E("Error", "Check failed: %s\n\tat %s:%d", message, __FILE__, __LINE__); \ __crash(); \ } \ } while (0) diff --git a/TactilityKernel/Include/tactility/concurrent/thread.h b/TactilityKernel/Include/tactility/concurrent/thread.h new file mode 100644 index 00000000..b78a5714 --- /dev/null +++ b/TactilityKernel/Include/tactility/concurrent/thread.h @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "tactility/error.h" +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef ESP_PLATFORM +#include +#endif + +#include +#include + +#include +#include +#include + +typedef enum { + THREAD_STATE_STOPPED, + THREAD_STATE_STARTING, + THREAD_STATE_RUNNING, +} ThreadState; + +/** ThreadPriority */ +enum ThreadPriority { + THREAD_PRIORITY_NONE = 0U, + THREAD_PRIORITY_IDLE = 1U, + THREAD_PRIORITY_LOWER = 2U, + THREAD_PRIORITY_LOW = 3U, + THREAD_PRIORITY_NORMAL = 4U, + THREAD_PRIORITY_HIGH = 5U, + THREAD_PRIORITY_HIGHER = 6U, + THREAD_PRIORITY_CRITICAL = 7U +}; + +typedef int32_t (*thread_main_fn_t)(void* context); +typedef void (*thread_state_callback_t)(ThreadState state, void* context); + +struct Thread; +typedef struct Thread Thread; + +/** + * @brief Creates a new thread instance with default settings. + * @return A pointer to the created Thread instance, or NULL if allocation failed. + */ +Thread* thread_alloc(void); + +/** + * @brief Creates a new thread instance with specified parameters. + * @param[in] name The name of the thread. + * @param[in] stack_size The size of the thread stack in bytes. + * @param[in] function The main function to be executed by the thread. + * @param[in] function_context A pointer to the context to be passed to the main function. + * @param[in] affinity The CPU core affinity for the thread (e.g., tskNO_AFFINITY). + * @return A pointer to the created Thread instance, or NULL if allocation failed. + */ +Thread* thread_alloc_full( + const char* name, + configSTACK_DEPTH_TYPE stack_size, + thread_main_fn_t function, + void* function_context, + portBASE_TYPE affinity +); + +/** + * @brief Destroys a thread instance. + * @param[in] thread The thread instance to destroy. + * @note The thread must be in the STOPPED state. + */ +void thread_free(Thread* thread); + +/** + * @brief Sets the name of the thread. + * @param[in] thread The thread instance. + * @param[in] name The new name for the thread. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_name(Thread* thread, const char* name); + +/** + * @brief Sets the stack size for the thread. + * @param[in] thread The thread instance. + * @param[in] stack_size The stack size in bytes. Must be a multiple of 4. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_stack_size(Thread* thread, size_t stack_size); + +/** + * @brief Sets the CPU core affinity for the thread. + * @param[in] thread The thread instance. + * @param[in] affinity The CPU core affinity. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_affinity(Thread* thread, portBASE_TYPE affinity); + +/** + * @brief Sets the main function and context for the thread. + * @param[in] thread The thread instance. + * @param[in] function The main function to be executed. + * @param[in] context A pointer to the context to be passed to the main function. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_main_function(Thread* thread, thread_main_fn_t function, void* context); + +/** + * @brief Sets the priority for the thread. + * @param[in] thread The thread instance. + * @param[in] priority The thread priority. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_priority(Thread* thread, enum ThreadPriority priority); + +/** + * @brief Sets a callback to be invoked when the thread state changes. + * @param[in] thread The thread instance. + * @param[in] callback The callback function. + * @param[in] context A pointer to the context to be passed to the callback function. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_state_callback(Thread* thread, thread_state_callback_t callback, void* context); + +/** + * @brief Gets the current state of the thread. + * @param[in] thread The thread instance. + * @return The current ThreadState. + */ +ThreadState thread_get_state(Thread* thread); + +/** + * @brief Starts the thread execution. + * @param[in] thread The thread instance. + * @note The thread must be in the STOPPED state and have a main function set. + * @retval ERROR_NONE when the thread was started + * @retval ERROR_UNDEFINED when the thread failed to start + */ +error_t thread_start(Thread* thread); + +/** + * @brief Waits for the thread to finish execution. + * @param[in] thread The thread instance. + * @param[in] timeout The maximum time to wait in ticks. + * @param[in] poll_interval The interval between status checks in ticks. + * @retval ERROR_NONE when the thread was stopped + * @retval ERROR_TIMEOUT when the thread was not stopped because the timeout has passed + * @note Cannot be called from the thread being joined. + */ +error_t thread_join(Thread* thread, TickType_t timeout, TickType_t poll_interval); + +/** + * @brief Gets the FreeRTOS task handle associated with the thread. + * @param[in] thread The thread instance. + * @return The TaskHandle_t, or NULL if the thread is not running. + */ +TaskHandle_t thread_get_task_handle(Thread* thread); + +/** + * @brief Gets the return code from the thread's main function. + * @param[in] thread The thread instance. + * @return The return code of the thread's main function. + * @note The thread must be in the STOPPED state. + */ +int32_t thread_get_return_code(Thread* thread); + +/** + * @brief Gets the minimum remaining stack space for the thread since it started. + * @param[in] thread The thread instance. + * @return The minimum remaining stack space in bytes. + * @note The thread must be in the RUNNING state. + */ +uint32_t thread_get_stack_space(Thread* thread); + +/** + * @brief Gets the current thread instance. + * @return A pointer to the current Thread instance, or NULL if not called from a thread created by this module. + */ +Thread* thread_get_current(void); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/Include/tactility/concurrent/timer.h b/TactilityKernel/Include/tactility/concurrent/timer.h new file mode 100644 index 00000000..7a3e7c2a --- /dev/null +++ b/TactilityKernel/Include/tactility/concurrent/timer.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "tactility/error.h" +#include "tactility/freertos/timers.h" +#include "tactility/concurrent/thread.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum TimerType { + TIMER_TYPE_ONCE = 0, // Timer triggers once after time has passed + TIMER_TYPE_PERIODIC = 1 // Timer triggers repeatedly after time has passed +}; + +typedef void (*timer_callback_t)(void* context); +typedef void (*timer_pending_callback_t)(void* context, uint32_t arg); + +struct Timer; + +/** + * @brief Creates a new timer instance. + * @param[in] type The timer type. + * @param[in] ticks The timer period in ticks. + * @param[in] callback The callback function. + * @param[in] context The context to pass to the callback function. + * @return A pointer to the created timer instance, or NULL if allocation failed. + */ +struct Timer* timer_alloc(enum TimerType type, TickType_t ticks, timer_callback_t callback, void* context); + +/** + * @brief Destroys a timer instance. + * @param[in] timer The timer instance to destroy. + */ +void timer_free(struct Timer* timer); + +/** + * @brief Starts the timer. + * @param[in] timer The timer instance. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_start(struct Timer* timer); + +/** + * @brief Stops the timer. + * @param[in] timer The timer instance. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_stop(struct Timer* timer); + +/** + * @brief Set a new interval and reset the timer. + * @param[in] timer The timer instance. + * @param[in] interval The new timer interval in ticks. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_reset_with_interval(struct Timer* timer, TickType_t interval); + +/** + * @brief Reset the timer. + * @param[in] timer The timer instance. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_reset(struct Timer* timer); + +/** + * @brief Check if the timer is running. + * @param[in] timer The timer instance. + * @return true when the timer is running. + */ +bool timer_is_running(struct Timer* timer); + +/** + * @brief Gets the expiry time of the timer. + * @param[in] timer The timer instance. + * @return The expiry time in ticks. + */ +TickType_t timer_get_expiry_time(struct Timer* timer); + +/** + * @brief Calls xTimerPendFunctionCall internally. + * @param[in] timer The timer instance. + * @param[in] callback the function to call + * @param[in] context the first function argument + * @param[in] arg the second function argument + * @param[in] timeout the function timeout (must set to 0 in ISR mode) + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_set_pending_callback(struct Timer* timer, timer_pending_callback_t callback, void* context, uint32_t arg, TickType_t timeout); + +/** + * @brief Set callback priority (priority of the timer daemon task). + * @param[in] timer The timer instance. + * @param[in] priority The priority. + */ +void timer_set_callback_priority(struct Timer* timer, enum ThreadPriority priority); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/Include/tactility/defines.h b/TactilityKernel/Include/tactility/defines.h new file mode 100644 index 00000000..6b0e34f6 --- /dev/null +++ b/TactilityKernel/Include/tactility/defines.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +/** + * @brief Contains various unsorted defines + * @note Preprocessor defines with potentially clashing names implement an #ifdef check. + */ +#pragma once + +#ifndef MIN +/** @brief Get the minimum value of 2 values */ +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif + +#ifndef MAX +/** @brief Get the maximum value of 2 values */ +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +#ifndef CLAMP +/** @brief Clamp a value between the provided minimum and maximum */ +#define CLAMP(min, max, value) (((value) < (min)) ? (min) : (((value) > (max)) ? (max) : (value))) +#endif diff --git a/TactilityKernel/Include/tactility/device.h b/TactilityKernel/Include/tactility/device.h index 0fc77247..a9560972 100644 --- a/TactilityKernel/Include/tactility/device.h +++ b/TactilityKernel/Include/tactility/device.h @@ -133,6 +133,10 @@ error_t device_stop(struct Device* device); */ void device_set_parent(struct Device* device, struct Device* parent); +error_t device_construct_add(struct Device* device, const char* compatible); + +error_t device_construct_add_start(struct Device* device, const char* compatible); + static inline void device_set_driver(struct Device* device, struct Driver* driver) { device->internal.driver = driver; } diff --git a/TactilityKernel/Include/tactility/drivers/gpio_controller.h b/TactilityKernel/Include/tactility/drivers/gpio_controller.h index 15038dc8..ef6dfdd2 100644 --- a/TactilityKernel/Include/tactility/drivers/gpio_controller.h +++ b/TactilityKernel/Include/tactility/drivers/gpio_controller.h @@ -10,17 +10,85 @@ extern "C" { #include struct GpioControllerApi { + /** + * @brief Sets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] high true to set the pin high, false to set it low + * @return ERROR_NONE if successful + */ error_t (*set_level)(struct Device* device, gpio_pin_t pin, bool high); + + /** + * @brief Gets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] high pointer to store the pin level + * @return ERROR_NONE if successful + */ error_t (*get_level)(struct Device* device, gpio_pin_t pin, bool* high); + + /** + * @brief Configures the options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] options configuration flags (direction, pull-up/down, etc.) + * @return ERROR_NONE if successful + */ error_t (*set_options)(struct Device* device, gpio_pin_t pin, gpio_flags_t options); + + /** + * @brief Gets the configuration options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] options pointer to store the configuration flags + * @return ERROR_NONE if successful + */ error_t (*get_options)(struct Device* device, gpio_pin_t pin, gpio_flags_t* options); }; +/** + * @brief Sets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] high true to set the pin high, false to set it low + * @return ERROR_NONE if successful + */ error_t gpio_controller_set_level(struct Device* device, gpio_pin_t pin, bool high); + +/** + * @brief Gets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] high pointer to store the pin level + * @return ERROR_NONE if successful + */ error_t gpio_controller_get_level(struct Device* device, gpio_pin_t pin, bool* high); + +/** + * @brief Configures the options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] options configuration flags (direction, pull-up/down, etc.) + * @return ERROR_NONE if successful + */ error_t gpio_controller_set_options(struct Device* device, gpio_pin_t pin, gpio_flags_t options); + +/** + * @brief Gets the configuration options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] options pointer to store the configuration flags + * @return ERROR_NONE if successful + */ error_t gpio_controller_get_options(struct Device* device, gpio_pin_t pin, gpio_flags_t* options); +/** + * @brief Configures the options for a GPIO pin using a pin configuration structure. + * @param[in] device the GPIO controller device + * @param[in] config the pin configuration structure + * @return ERROR_NONE if successful + */ static inline error_t gpio_set_options_config(struct Device* device, const struct GpioPinConfig* config) { return gpio_controller_set_options(device, config->pin, config->flags); } diff --git a/TactilityKernel/Include/tactility/drivers/i2c_controller.h b/TactilityKernel/Include/tactility/drivers/i2c_controller.h index 2995002b..fe54628f 100644 --- a/TactilityKernel/Include/tactility/drivers/i2c_controller.h +++ b/TactilityKernel/Include/tactility/drivers/i2c_controller.h @@ -13,18 +13,154 @@ extern "C" { #include #include +/** + * @brief API for I2C controller drivers. + */ struct I2cControllerApi { + /** + * @brief Reads data from an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ error_t (*read)(struct Device* device, uint8_t address, uint8_t* data, size_t dataSize, TickType_t timeout); + + /** + * @brief Writes data to an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ error_t (*write)(struct Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout); + + /** + * @brief Writes data to then reads data from an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] writeData the buffer containing the data to write + * @param[in] writeDataSize the number of bytes to write + * @param[out] readData the buffer to store the read data + * @param[in] readDataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ error_t (*write_read)(struct Device* device, uint8_t address, const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout); + + /** + * @brief Reads data from a register of an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to read from + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*read_register)(struct Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout); + + /** + * @brief Writes data to a register of an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to write to + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*write_register)(struct Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout); }; +/** + * @brief Reads data from an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + */ error_t i2c_controller_read(struct Device* device, uint8_t address, uint8_t* data, size_t dataSize, TickType_t timeout); +/** + * @brief Writes data to an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + */ error_t i2c_controller_write(struct Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout); +/** + * @brief Writes data to then reads data from an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] writeData the buffer containing the data to write + * @param[in] writeDataSize the number of bytes to write + * @param[out] readData the buffer to store the read data + * @param[in] readDataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the operation was successful + */ error_t i2c_controller_write_read(struct Device* device, uint8_t address, const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout); +/** + * @brief Reads data from a register of an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to read from + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + */ +error_t i2c_controller_read_register(struct Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout); + +/** + * @brief Writes data to a register of an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to write to + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + */ +error_t i2c_controller_write_register(struct Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout); + +/** + * @brief Writes an array of register-value pairs to an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] data an array of bytes where even indices are register addresses and odd indices are values + * @param[in] dataSize the number of bytes in the data array (must be even) + * @param[in] timeout the maximum time to wait for each operation to complete + * @retval ERROR_NONE when all write operations were successful + */ +error_t i2c_controller_write_register_array(struct Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout); + +/** + * @brief Checks if an I2C device is present at the specified address. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address to check + * @param[in] timeout the maximum time to wait for the check to complete + * @retval ERROR_NONE when a device responded at the address + */ +error_t i2c_controller_has_device_at_address(struct Device* device, uint8_t address, TickType_t timeout); + extern const struct DeviceType I2C_CONTROLLER_TYPE; #ifdef __cplusplus diff --git a/TactilityKernel/Include/tactility/error.h b/TactilityKernel/Include/tactility/error.h index 000dba6c..00aae3bf 100644 --- a/TactilityKernel/Include/tactility/error.h +++ b/TactilityKernel/Include/tactility/error.h @@ -2,6 +2,10 @@ #pragma once +#ifdef __cplusplus +extern "C" { +#endif + // Avoid potential clash with bits/types/error_t.h #ifndef __error_t_defined typedef int error_t; @@ -17,3 +21,11 @@ typedef int error_t; #define ERROR_RESOURCE 7 // A problem with a resource/dependency #define ERROR_TIMEOUT 8 #define ERROR_OUT_OF_MEMORY 9 +#define ERROR_NOT_SUPPORTED 10 + +/** Convert an error_t to a human-readable text. Useful for logging. */ +const char* error_to_string(error_t error); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/Include/tactility/time.h b/TactilityKernel/Include/tactility/time.h index 0701ddeb..99964ed3 100644 --- a/TactilityKernel/Include/tactility/time.h +++ b/TactilityKernel/Include/tactility/time.h @@ -1,7 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +/** + * Time-keeping related functionality. + * This includes functionality for both ticks and seconds. + */ #pragma once #include +#include "defines.h" #include "tactility/freertos/task.h" #ifdef ESP_PLATFORM @@ -32,6 +38,14 @@ static inline size_t get_millis() { return get_ticks() * portTICK_PERIOD_MS; } +static inline TickType_t get_timeout_remaining_ticks(TickType_t timeout, TickType_t start_time) { + TickType_t ticks_passed = get_ticks() - start_time; + if (ticks_passed >= timeout) { + return 0; + } + return timeout - ticks_passed; +} + /** @return the frequency at which the kernel task schedulers operate */ uint32_t kernel_get_tick_frequency(); diff --git a/TactilityKernel/Source/concurrent/dispatcher.cpp b/TactilityKernel/Source/concurrent/dispatcher.cpp index 9fbc8f2a..41873c05 100644 --- a/TactilityKernel/Source/concurrent/dispatcher.cpp +++ b/TactilityKernel/Source/concurrent/dispatcher.cpp @@ -11,7 +11,7 @@ #include #include -#define TAG LOG_TAG("Dispatcher") +#define TAG LOG_TAG(Dispatcher) static constexpr EventBits_t BACKPRESSURE_WARNING_COUNT = 100U; static constexpr EventBits_t WAIT_FLAG = 1U; diff --git a/TactilityKernel/Source/concurrent/thread.cpp b/TactilityKernel/Source/concurrent/thread.cpp new file mode 100644 index 00000000..b57735e6 --- /dev/null +++ b/TactilityKernel/Source/concurrent/thread.cpp @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const size_t LOCAL_STORAGE_SELF_POINTER_INDEX = 0; +static const char* TAG = LOG_TAG(Thread); + +struct Thread { + TaskHandle_t taskHandle = nullptr; + ThreadState state = THREAD_STATE_STOPPED; + thread_main_fn_t mainFunction = nullptr; + void* mainFunctionContext = nullptr; + int32_t callbackResult = 0; + thread_state_callback_t stateCallback = nullptr; + void* stateCallbackContext = nullptr; + std::string name = "unnamed"; + enum ThreadPriority priority = THREAD_PRIORITY_NORMAL; + struct Mutex mutex = { 0 }; + configSTACK_DEPTH_TYPE stackSize = 4096; + portBASE_TYPE affinity = -1; + + Thread() { + mutex_construct(&mutex); + } + + ~Thread() { + mutex_destruct(&mutex); + } + + void lock() { mutex_lock(&mutex); } + + void unlock() { mutex_unlock(&mutex); } +}; + +static void thread_set_state_internal(Thread* thread, ThreadState newState) { + thread->lock(); + thread->state = newState; + auto cb = thread->stateCallback; + auto cb_ctx = thread->stateCallbackContext; + thread->unlock(); + if (cb) { + cb(newState, cb_ctx); + } +} + +static void thread_main_body(void* context) { + check(context != nullptr); + auto* thread = static_cast(context); + + // Save Thread instance pointer to task local storage + check(pvTaskGetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX) == nullptr); + vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, thread); + + LOG_I(TAG, "Starting %s", thread->name.c_str()); // No need to lock as we don't allow mutation after thread start + check(thread->state == THREAD_STATE_STARTING); + thread_set_state_internal(thread, THREAD_STATE_RUNNING); + + int32_t result = thread->mainFunction(thread->mainFunctionContext); + thread->lock(); + thread->callbackResult = result; + thread->unlock(); + + check(thread->state == THREAD_STATE_RUNNING); + thread_set_state_internal(thread, THREAD_STATE_STOPPED); + LOG_I(TAG, "Stopped %s", thread->name.c_str()); // No need to lock as we don't allow mutation after thread start + + vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr); + + thread->lock(); + thread->taskHandle = nullptr; + thread->unlock(); + + vTaskDelete(nullptr); +} + +extern "C" { + +Thread* thread_alloc(void) { + auto* thread = new(std::nothrow) Thread(); + if (thread == nullptr) { + return nullptr; + } + return thread; +} + +Thread* thread_alloc_full( + const char* name, + configSTACK_DEPTH_TYPE stack_size, + thread_main_fn_t function, + void* function_context, + portBASE_TYPE affinity +) { + auto* thread = new(std::nothrow) Thread(); + if (thread != nullptr) { + thread_set_name(thread, name); + thread_set_stack_size(thread, stack_size); + thread_set_main_function(thread, function, function_context); + thread_set_affinity(thread, affinity); + } + return thread; +} + +void thread_free(Thread* thread) { + check(thread); + check(thread->state == THREAD_STATE_STOPPED); + check(thread->taskHandle == nullptr); + delete thread; +} + +void thread_set_name(Thread* thread, const char* name) { + check(name != nullptr); + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->name = name; + thread->unlock(); +} + +void thread_set_stack_size(Thread* thread, size_t stack_size) { + thread->lock(); + check(stack_size > 0); + check(thread->state == THREAD_STATE_STOPPED); + thread->stackSize = stack_size; + thread->unlock(); +} + +void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->affinity = affinity; + thread->unlock(); +} + +void thread_set_main_function(Thread* thread, thread_main_fn_t function, void* context) { + thread->lock(); + check(function != nullptr); + check(thread->state == THREAD_STATE_STOPPED); + thread->mainFunction = function; + thread->mainFunctionContext = context; + thread->unlock(); +} + +void thread_set_priority(Thread* thread, enum ThreadPriority priority) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->priority = priority; + thread->unlock(); +} + +void thread_set_state_callback(Thread* thread, thread_state_callback_t callback, void* context) { + thread->lock(); + check(callback != nullptr); + check(thread->state == THREAD_STATE_STOPPED); + thread->stateCallback = callback; + thread->stateCallbackContext = context; + thread->unlock(); +} + +ThreadState thread_get_state(Thread* thread) { + check(xPortInIsrContext() == pdFALSE); + thread->lock(); + ThreadState state = thread->state; + thread->unlock(); + return state; +} + +error_t thread_start(Thread* thread) { + thread->lock(); + check(thread->mainFunction != nullptr); + check(thread->state == THREAD_STATE_STOPPED); + check(thread->stackSize); + thread->unlock(); + + thread_set_state_internal(thread, THREAD_STATE_STARTING); + + thread->lock(); + uint32_t stack_depth = thread->stackSize / sizeof(StackType_t); + enum ThreadPriority priority = thread->priority; + portBASE_TYPE affinity = thread->affinity; + thread->unlock(); + + BaseType_t result; + if (affinity != -1) { +#ifdef ESP_PLATFORM + result = xTaskCreatePinnedToCore( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle, + affinity + ); +#else + result = xTaskCreate( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle + ); +#endif + } else { + result = xTaskCreate( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle + ); + } + + if (result != pdPASS) { + thread_set_state_internal(thread, THREAD_STATE_STOPPED); + thread->lock(); + thread->taskHandle = nullptr; + thread->unlock(); + return ERROR_UNDEFINED; + } + + return ERROR_NONE; +} + +error_t thread_join(Thread* thread, TickType_t timeout, TickType_t poll_interval) { + check(thread_get_current() != thread); + + TickType_t start_ticks = get_ticks(); + while (thread_get_task_handle(thread)) { + delay_ticks(poll_interval); + if (get_ticks() - start_ticks > timeout) { + return ERROR_TIMEOUT; + } + } + + return ERROR_NONE; +} + +TaskHandle_t thread_get_task_handle(Thread* thread) { + thread->lock(); + auto* handle = thread->taskHandle; + thread->unlock(); + return handle; +} + +int32_t thread_get_return_code(Thread* thread) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + auto result = thread->callbackResult; + thread->unlock(); + return result; +} + +uint32_t thread_get_stack_space(Thread* thread) { + if (xPortInIsrContext() == pdTRUE) { + return 0; + } + thread->lock(); + check(thread->state == THREAD_STATE_RUNNING); + auto result = uxTaskGetStackHighWaterMark(thread->taskHandle) * sizeof(StackType_t); + thread->unlock(); + return result; +} + +Thread* thread_get_current(void) { + check(xPortInIsrContext() == pdFALSE); + return (Thread*)pvTaskGetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX); +} + +} diff --git a/TactilityKernel/Source/concurrent/timer.c b/TactilityKernel/Source/concurrent/timer.c new file mode 100644 index 00000000..6360ab2a --- /dev/null +++ b/TactilityKernel/Source/concurrent/timer.c @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +struct Timer { + TimerHandle_t handle; + timer_callback_t callback; + void* context; +}; + +static void timer_callback_internal(TimerHandle_t xTimer) { + struct Timer* timer = (struct Timer*)pvTimerGetTimerID(xTimer); + if (timer != NULL && timer->callback != NULL) { + timer->callback(timer->context); + } +} + +struct Timer* timer_alloc(enum TimerType type, TickType_t ticks, timer_callback_t callback, void* context) { + check(xPortInIsrContext() == pdFALSE); + check(callback != NULL); + + struct Timer* timer = (struct Timer*)malloc(sizeof(struct Timer)); + if (timer == NULL) { + return NULL; + } + + timer->callback = callback; + timer->context = context; + + BaseType_t auto_reload = (type == TIMER_TYPE_ONCE) ? pdFALSE : pdTRUE; + timer->handle = xTimerCreate(NULL, ticks, auto_reload, timer, timer_callback_internal); + + if (timer->handle == NULL) { + free(timer); + return NULL; + } + + return timer; +} + +void timer_free(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + // MAX_TICKS or a reasonable timeout for the timer command queue + xTimerDelete(timer->handle, portMAX_DELAY); + free(timer); +} + +error_t timer_start(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return (xTimerStart(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +error_t timer_stop(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return (xTimerStop(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +error_t timer_reset_with_interval(struct Timer* timer, TickType_t interval) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + if (xTimerChangePeriod(timer->handle, interval, portMAX_DELAY) != pdPASS) { + return ERROR_TIMEOUT; + } + return (xTimerReset(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +error_t timer_reset(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return (xTimerReset(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +bool timer_is_running(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return xTimerIsTimerActive(timer->handle) != pdFALSE; +} + +TickType_t timer_get_expiry_time(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return xTimerGetExpiryTime(timer->handle); +} + +error_t timer_set_pending_callback(struct Timer* timer, timer_pending_callback_t callback, void* context, uint32_t arg, TickType_t timeout) { + (void)timer; // Unused in this implementation but kept for API consistency if needed later + BaseType_t result; + if (xPortInIsrContext() == pdTRUE) { + check(timeout == 0); + result = xTimerPendFunctionCallFromISR(callback, context, arg, NULL); + } else { + result = xTimerPendFunctionCall(callback, context, arg, timeout); + } + return (result == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +void timer_set_callback_priority(struct Timer* timer, enum ThreadPriority priority) { + (void)timer; // Unused in this implementation but kept for API consistency if needed later + check(xPortInIsrContext() == pdFALSE); + + TaskHandle_t task_handle = xTimerGetTimerDaemonTaskHandle(); + check(task_handle != NULL); + + vTaskPrioritySet(task_handle, (UBaseType_t)priority); +} diff --git a/TactilityKernel/Source/crash.cpp b/TactilityKernel/Source/crash.cpp index ff728d9f..846383ca 100644 --- a/TactilityKernel/Source/crash.cpp +++ b/TactilityKernel/Source/crash.cpp @@ -1,7 +1,7 @@ #include #include -static const auto* TAG = LOG_TAG("Kernel"); +static const auto* TAG = LOG_TAG(Kernel); static void log_memory_info() { #ifdef ESP_PLATFORM diff --git a/TactilityKernel/Source/device.cpp b/TactilityKernel/Source/device.cpp index ffa42421..dcb3db86 100644 --- a/TactilityKernel/Source/device.cpp +++ b/TactilityKernel/Source/device.cpp @@ -146,6 +146,7 @@ failed_ledger_lookup: } error_t device_start(Device* device) { + LOG_I(TAG, "start %s", device->name); if (!device->internal.state.added) { return ERROR_INVALID_STATE; } @@ -166,6 +167,7 @@ error_t device_start(Device* device) { } error_t device_stop(struct Device* device) { + LOG_I(TAG, "stop %s", device->name); if (!device->internal.state.added) { return ERROR_INVALID_STATE; } @@ -183,6 +185,56 @@ error_t device_stop(struct Device* device) { return ERROR_NONE; } +error_t device_construct_add(struct Device* device, const char* compatible) { + struct Driver* driver = driver_find_compatible(compatible); + if (driver == nullptr) { + LOG_E(TAG, "Can't find driver '%s' for device '%s'", compatible, device->name); + return ERROR_RESOURCE; + } + + error_t error = device_construct(device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to construct device %s: %s", device->name, error_to_string(error)); + goto on_construct_error; + } + + device_set_driver(device, driver); + + error = device_add(device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add device %s: %s", device->name, error_to_string(error)); + goto on_add_error; + } + + return ERROR_NONE; + + on_add_error: + device_destruct(device); + on_construct_error: + return error; +} + +error_t device_construct_add_start(struct Device* device, const char* compatible) { + error_t error = device_construct_add(device, compatible); + if (error != ERROR_NONE) { + goto on_construct_add_error; + } + + error = device_start(device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to start device %s: %s", device->name, error_to_string(error)); + goto on_start_error; + } + + return ERROR_NONE; + +on_start_error: + device_remove(device); + device_destruct(device); +on_construct_add_error: + return error; +} + void device_set_parent(Device* device, Device* parent) { assert(!device->internal.state.started); device->parent = parent; diff --git a/TactilityKernel/Source/drivers/i2c_controller.cpp b/TactilityKernel/Source/drivers/i2c_controller.cpp index 1efaf256..ca08ef23 100644 --- a/TactilityKernel/Source/drivers/i2c_controller.cpp +++ b/TactilityKernel/Source/drivers/i2c_controller.cpp @@ -1,6 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 - -#include #include #include @@ -23,6 +21,34 @@ error_t i2c_controller_write_read(Device* device, uint8_t address, const uint8_t return I2C_DRIVER_API(driver)->write_read(device, address, writeData, writeDataSize, readData, readDataSize, timeout); } +error_t i2c_controller_read_register(Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout) { + const auto* driver = device_get_driver(device); + return I2C_DRIVER_API(driver)->read_register(device, address, reg, data, dataSize, timeout); +} + +error_t i2c_controller_write_register(Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + const auto* driver = device_get_driver(device); + return I2C_DRIVER_API(driver)->write_register(device, address, reg, data, dataSize, timeout); +} + +error_t i2c_controller_write_register_array(Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + const auto* driver = device_get_driver(device); + if (dataSize % 2 != 0) { + return ERROR_INVALID_ARGUMENT; + } + for (int i = 0; i < dataSize; i += 2) { + error_t error = I2C_DRIVER_API(driver)->write_register(device, address, data[i], &data[i + 1], 1, timeout); + if (error != ERROR_NONE) return error; + } + return ERROR_NONE; +} + +error_t i2c_controller_has_device_at_address(Device* device, uint8_t address, TickType_t timeout) { + const auto* driver = device_get_driver(device); + uint8_t message[2] = { 0, 0 }; + return I2C_DRIVER_API(driver)->write(device, address, message, 2, timeout); +} + const struct DeviceType I2C_CONTROLLER_TYPE { 0 }; } diff --git a/TactilityKernel/Source/error.cpp b/TactilityKernel/Source/error.cpp new file mode 100644 index 00000000..df469e89 --- /dev/null +++ b/TactilityKernel/Source/error.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +extern "C" { + +const char* error_to_string(error_t error) { + switch (error) { + case ERROR_NONE: + return "no error"; + case ERROR_UNDEFINED: + return "undefined"; + case ERROR_INVALID_STATE: + return "invalid state"; + case ERROR_INVALID_ARGUMENT: + return "invalid argument"; + case ERROR_MISSING_PARAMETER: + return "missing parameter"; + case ERROR_NOT_FOUND: + return "not found"; + case ERROR_ISR_STATUS: + return "ISR status"; + case ERROR_RESOURCE: + return "resource"; + case ERROR_TIMEOUT: + return "timeout"; + case ERROR_OUT_OF_MEMORY: + return "out of memory"; + case ERROR_NOT_SUPPORTED: + return "not supported"; + default: + return "unknown"; + } +} + +} diff --git a/Tests/TactilityKernel/ThreadTest.cpp b/Tests/TactilityKernel/ThreadTest.cpp new file mode 100644 index 00000000..1bc4e9f0 --- /dev/null +++ b/Tests/TactilityKernel/ThreadTest.cpp @@ -0,0 +1,112 @@ +#include "doctest.h" + +#include +#include + +TEST_CASE("when a thread is started then its callback should be called") { + bool has_called = false; + auto* thread = thread_alloc_full( + "immediate return task", + 4096, + [](void* context) { + auto* has_called_ptr = static_cast(context); + *has_called_ptr = true; + return 0; + }, + &has_called, + -1 + ); + + CHECK(!has_called); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); + CHECK(has_called); +} + +TEST_CASE("a thread can be started and stopped") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + ); + + CHECK(thread); + CHECK_EQ(thread_start(thread), ERROR_NONE); + interrupted = true; + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); +} + +TEST_CASE("thread id should only be set at when thread is started") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + ); + CHECK_EQ(thread_get_task_handle(thread), nullptr); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_NE(thread_get_task_handle(thread), nullptr); + interrupted = true; + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + CHECK_EQ(thread_get_task_handle(thread), nullptr); + thread_free(thread); +} + +TEST_CASE("thread state should be correct") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + + ); + CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED); + thread_start(thread); + auto state = thread_get_state(thread); + CHECK((state == THREAD_STATE_STARTING || state == THREAD_STATE_RUNNING)); + interrupted = true; + CHECK_EQ(thread_join(thread, 10, 1), ERROR_NONE); + CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED); + thread_free(thread); +} + +TEST_CASE("thread id should only be set at when thread is started") { + auto* thread = thread_alloc_full( + "return code", + 4096, + [](void* context) { return 123; }, + nullptr, + -1 + ); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(thread_join(thread, 1, 1), ERROR_NONE); + CHECK_EQ(thread_get_return_code(thread), 123); + thread_free(thread); +} diff --git a/Tests/TactilityKernel/TimerTest.cpp b/Tests/TactilityKernel/TimerTest.cpp new file mode 100644 index 00000000..c2d4e8c9 --- /dev/null +++ b/Tests/TactilityKernel/TimerTest.cpp @@ -0,0 +1,147 @@ +#include "doctest.h" + +#include + +#include +#include + +TEST_CASE("timer_alloc and timer_free should handle allocation and deallocation") { + auto callback = [](void* context) {}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, callback, nullptr); + CHECK_NE(timer, nullptr); + timer_free(timer); +} + +TEST_CASE("timer_start and timer_stop should change running state") { + auto callback = [](void* context) {}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, callback, nullptr); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_is_running(timer), false); + CHECK_EQ(timer_start(timer), ERROR_NONE); + CHECK_EQ(timer_is_running(timer), true); + CHECK_EQ(timer_stop(timer), ERROR_NONE); + CHECK_EQ(timer_is_running(timer), false); + + timer_free(timer); +} + +TEST_CASE("one-shot timer should fire callback once") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + delay_millis(20); + + CHECK_EQ(call_count.load(), 1); + CHECK_EQ(timer_is_running(timer), false); + + timer_free(timer); +} + +TEST_CASE("periodic timer should fire callback multiple times") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_PERIODIC, 10, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + delay_millis(35); // Should fire around 3 times + + CHECK_GE(call_count.load(), 3); + CHECK_EQ(timer_is_running(timer), true); + + timer_stop(timer); + timer_free(timer); +} + +TEST_CASE("timer_reset should restart the timer") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 20, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + delay_millis(10); + CHECK_EQ(call_count.load(), 0); + + // Resetting should push the expiry further + CHECK_EQ(timer_reset(timer), ERROR_NONE); + delay_millis(15); + CHECK_EQ(call_count.load(), 0); // Still shouldn't have fired if reset worked + + delay_millis(10); + CHECK_EQ(call_count.load(), 1); // Now it should have fired + + timer_free(timer); +} + +TEST_CASE("timer_reset_with_interval should change the period") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 40, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + // Change to a much shorter interval + CHECK_EQ(timer_reset_with_interval(timer, 10), ERROR_NONE); + + delay_millis(20); + CHECK_EQ(call_count.load(), 1); + + timer_free(timer); +} + +TEST_CASE("timer_get_expiry_time should return a valid time") { + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {}, nullptr); + REQUIRE_NE(timer, nullptr); + + timer_start(timer); + TickType_t expiry = timer_get_expiry_time(timer); + // Expiry should be in the future + CHECK_GT(expiry, xTaskGetTickCount()); + + timer_free(timer); +} + +TEST_CASE("timer_set_pending_callback should execute callback in timer task") { + std::atomic called{false}; + struct Context { + std::atomic* called; + uint32_t expected_arg; + uint32_t received_arg; + } context = { &called, 0x12345678, 0 }; + + auto pending_cb = [](void* ctx, uint32_t arg) { + auto* c = static_cast(ctx); + c->received_arg = arg; + c->called->store(true); + }; + + // timer_set_pending_callback doesn't actually use the timer object in current implementation + // but we need one for the API + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {}, nullptr); + + CHECK_EQ(timer_set_pending_callback(timer, pending_cb, &context, context.expected_arg, portMAX_DELAY), ERROR_NONE); + + // Wait for timer task to process the callback + int retries = 10; + while (!called.load() && retries-- > 0) { + delay_millis(10); + } + + CHECK(called.load()); + CHECK_EQ(context.received_arg, context.expected_arg); + + timer_free(timer); +}