diff --git a/boards/ESP32/Kconfig b/boards/ESP32/Kconfig index 9d5f1de6a..312eb320f 100755 --- a/boards/ESP32/Kconfig +++ b/boards/ESP32/Kconfig @@ -102,6 +102,12 @@ choice rsource "./DNESP32S3_BOX2_WIFI/Kconfig" endif + config BOARD_CHOICE_M5STACK_STICKS3 + bool "M5STACK_STICKS3" + if (BOARD_CHOICE_M5STACK_STICKS3) + rsource "./M5STACK_STICKS3/Kconfig" + endif + config BOARD_CHOICE_WAVESHARE_ESP32C6_DEV_KIT_N16 bool "WAVESHARE_ESP32C6_DEV_KIT_N16" diff --git a/boards/ESP32/M5STACK_STICKS3/CMakeLists.txt b/boards/ESP32/M5STACK_STICKS3/CMakeLists.txt new file mode 100644 index 000000000..fecc4a9d0 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/CMakeLists.txt @@ -0,0 +1,42 @@ +## +# @file CMakeLists.txt +# @brief M5Stack StickS3 board build configuration +#/ + +# MODULE_PATH +set(MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) + +# MODULE_NAME +get_filename_component(MODULE_NAME ${MODULE_PATH} NAME) + +# LIB_SRCS +aux_source_directory(${MODULE_PATH} LIB_SRCS) + +# LIB_PUBLIC_INC +set(LIB_PUBLIC_INC ${MODULE_PATH}) + +######################################## +# Target Configure +######################################## +add_library(${MODULE_NAME}) + +target_sources(${MODULE_NAME} + PRIVATE + ${LIB_SRCS} + ) + +target_include_directories(${MODULE_NAME} + PRIVATE + ${LIB_PRIVATE_INC} + + PUBLIC + ${LIB_PUBLIC_INC} + ) + +######################################## +# Layer Configure +######################################## +list(APPEND COMPONENT_LIBS ${MODULE_NAME}) +set(COMPONENT_LIBS "${COMPONENT_LIBS}" PARENT_SCOPE) +list(APPEND COMPONENT_PUBINC ${LIB_PUBLIC_INC}) +set(COMPONENT_PUBINC "${COMPONENT_PUBINC}" PARENT_SCOPE) diff --git a/boards/ESP32/M5STACK_STICKS3/Kconfig b/boards/ESP32/M5STACK_STICKS3/Kconfig new file mode 100644 index 000000000..ce0f2e354 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/Kconfig @@ -0,0 +1,28 @@ +config CHIP_CHOICE + string + default "esp32s3" + +config BOARD_CHOICE + string + default "M5STACK_STICKS3" + +config BOARD_CONFIG + bool + default y + select ENABLE_AUDIO + select ENABLE_ESP_DISPLAY + select PLATFORM_FLASHSIZE_8M + select ENABLE_AUDIO_CODECS + select ENABLE_PMIC + select ENABLE_PMIC_M5PM1 + select ENABLE_BUTTON + select ENABLE_BUTTON_2 + +config BOARD_M5STACK_STICKS3_EXT_5V_OUTPUT + bool "Enable EXT_5V output on boot" + default y + depends on BOARD_CONFIG + help + Enable the Grove/Hat EXT_5V output rail during board initialization for + hardware bring-up. Disable this when the EXT_5V pin may be externally + powered. diff --git a/boards/ESP32/M5STACK_STICKS3/board_com_api.h b/boards/ESP32/M5STACK_STICKS3/board_com_api.h new file mode 100644 index 000000000..975e59c8a --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/board_com_api.h @@ -0,0 +1,29 @@ +/** + * @file board_com_api.h + * @brief Common board-level hardware registration APIs for M5Stack StickS3. + * @version 0.1 + * @date 2026-04-27 + * @copyright Copyright (c) Tuya Inc. All Rights Reserved. + */ +#ifndef __BOARD_COM_API_H__ +#define __BOARD_COM_API_H__ + +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Function declarations + * --------------------------------------------------------------------------- */ +/** + * @brief Register all board hardware peripherals. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_register_hardware(void); + +#ifdef __cplusplus +} +#endif +#endif /* __BOARD_COM_API_H__ */ diff --git a/boards/ESP32/M5STACK_STICKS3/board_config.h b/boards/ESP32/M5STACK_STICKS3/board_config.h new file mode 100644 index 000000000..cfed610f7 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/board_config.h @@ -0,0 +1,261 @@ +/** + * @file board_config.h + * @brief Board configuration for M5Stack StickS3. + * @version 0.1 + * @date 2026-04-27 + * @copyright Copyright (c) Tuya Inc. All Rights Reserved. + */ +#ifndef __BOARD_CONFIG_H__ +#define __BOARD_CONFIG_H__ + +#include "sdkconfig.h" +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Macros + * --------------------------------------------------------------------------- */ +#define I2S_INPUT_SAMPLE_RATE (16000) +#define I2S_OUTPUT_SAMPLE_RATE (16000) + +/* Shared internal I2C bus: ES8311, BMI270, M5PM1. */ +#define I2C_NUM TUYA_I2C_NUM_1 +#define I2C_SCL_IO TUYA_GPIO_NUM_48 +#define I2C_SDA_IO TUYA_GPIO_NUM_47 + +/* ES8311 audio codec. Speaker amp SHDN is controlled by M5PM1 PYG3, not ESP32 GPIO. */ +#define I2S_NUM TUYA_I2S_NUM_1 +#define I2S_MCK_IO TUYA_GPIO_NUM_18 +#define I2S_BCK_IO TUYA_GPIO_NUM_17 +#define I2S_WS_IO TUYA_GPIO_NUM_15 +#define I2S_DO_IO TUYA_GPIO_NUM_14 +#define I2S_DI_IO TUYA_GPIO_NUM_16 + +#define GPIO_OUTPUT_PA (-1) + +#define AUDIO_CODEC_DMA_DESC_NUM (6) +#define AUDIO_CODEC_DMA_FRAME_NUM (240) +#define AUDIO_CODEC_ES8311_ADDR_7BIT (0x18) +#define AUDIO_CODEC_ES8311_ADDR (AUDIO_CODEC_ES8311_ADDR_7BIT << 1) + +/* Display type. */ +#define DISPLAY_TYPE_UNKNOWN 0 +#define DISPLAY_TYPE_OLED_SSD1306 1 +#define DISPLAY_TYPE_LCD_SH8601 2 +#define DISPLAY_TYPE_LCD_ST7789_80 3 +#define DISPLAY_TYPE_LCD_ST7789_SPI 4 + +#define BOARD_DISPLAY_TYPE DISPLAY_TYPE_LCD_ST7789_SPI + +/* ST7789P3 LCD. */ +#define LCD_MOSI_PIN TUYA_GPIO_NUM_39 +#define LCD_SCLK_PIN TUYA_GPIO_NUM_40 +#define LCD_DC_PIN TUYA_GPIO_NUM_45 +#define LCD_CS_PIN TUYA_GPIO_NUM_41 +#define LCD_RST_PIN TUYA_GPIO_NUM_21 + +#define DISPLAY_BACKLIGHT_PIN TUYA_GPIO_NUM_38 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define DISPLAY_WIDTH (135) +#define DISPLAY_HEIGHT (240) +#define DISPLAY_OFFSET_X (52) +#define DISPLAY_OFFSET_Y (40) + +/* LVGL config. */ +#define DISPLAY_BUFFER_SIZE (DISPLAY_WIDTH * 20) + +#define DISPLAY_MONOCHROME false + +/* Rotation. */ +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false + +#define DISPLAY_COLOR_FORMAT LV_COLOR_FORMAT_RGB565 +#define DISPLAY_COLOR_INVERT true + +/* Only one of DISPLAY_BUFF_SPIRAM and DISPLAY_BUFF_DMA can be selected. */ +#define DISPLAY_BUFF_SPIRAM 0 +#define DISPLAY_BUFF_DMA 1 + +#define DISPLAY_SWAP_BYTES 1 + +/* M5PM1 power controller. + * StickS3 power levels are independent switches sourced from L0: + * L1 uses LDO3V3_EN_PP for the IMU, L2/L3A uses DCDC3V3_EN_PP for + * ESP32-S3-side power, EXT_5V uses BOOST5V_EN_PP, and L3B uses PYG2 for + * LCD backlight, microphone, and speaker peripheral power. + */ +#define M5PM1_I2C_PORT I2C_NUM +#define M5PM1_I2C_ADDR (0x6E) + +#define M5PM1_GPIO_CHARGE_STATUS (0) +#define M5PM1_GPIO_IRQ (1) +#define M5PM1_GPIO_L3B_POWER (2) +#define M5PM1_GPIO_SPK_AMP_SHDN (3) +#define M5PM1_GPIO_IMU_INT1 (4) + +#define M5PM1_L3B_POWER_ENABLE_LEVEL true +#define M5PM1_L3B_POWER_DISABLE_LEVEL false +#define M5PM1_SPK_AMP_ENABLE_LEVEL true +#define M5PM1_SPK_AMP_DISABLE_LEVEL false + +/* Buttons. */ +#define BOARD_BUTTON_PIN TUYA_GPIO_NUM_11 +#define BOARD_BUTTON_2_PIN TUYA_GPIO_NUM_12 +#define BOARD_BUTTON_ACTIVE_LV TUYA_GPIO_LEVEL_LOW +#define BOARD_BUTTON_2_ACTIVE_LV TUYA_GPIO_LEVEL_LOW + +#ifndef BUTTON_NAME_2 +#define BUTTON_NAME_2 "ai_chat_button_2" +#endif + +/* --------------------------------------------------------------------------- + * Function declarations + * --------------------------------------------------------------------------- */ +/** + * @brief Initialize the board display. + * @return 0 on success, error code on failure. + */ +int board_display_init(void); + +/** + * @brief Show a simple centered red box on the board display. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_display_show_red_box(void); + +/** + * @brief Get the ESP LCD panel IO handle. + * @return Panel IO handle, or NULL if not initialized. + */ +void *board_display_get_panel_io_handle(void); + +/** + * @brief Get the ESP LCD panel handle. + * @return Panel handle, or NULL if not initialized. + */ +void *board_display_get_panel_handle(void); + +/** + * @brief Enable or disable StickS3 L1 IMU power. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l1(bool enable); + +/** + * @brief Enable or disable StickS3 L2/L3A ESP32-S3-side power switch. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l2_l3a(bool enable); + +/** + * @brief Enable or disable StickS3 EXT_5V output mode. + * @param[in] enable true for 5V output mode, false for 5V input mode. + * @return OPRT_OK on success, error code on failure. + * @attention Only enable output mode when external 5V is not supplied through Grove/Hat EXT_5V/5VIN. + */ +OPERATE_RET board_sticks3_power_set_ext_5v_output(bool enable); + +/** + * @brief Enable or disable StickS3 L3B peripheral power. + * @param[in] enable true to power LCD backlight, microphone, and speaker peripherals. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l3b(bool enable); + +/** + * @brief Enable or disable StickS3 speaker amplifier. + * @param[in] enable true to enable amplifier, false to disable amplifier. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_speaker_amp(bool enable); + +/** + * @brief Enable or disable StickS3 L1 IMU power hold while M5PM1 sleeps. + * @param[in] enable true to keep L1 held during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l1_hold(bool enable); + +/** + * @brief Enable or disable StickS3 EXT_5V power hold while M5PM1 sleeps. + * @param[in] enable true to keep EXT_5V held during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + * @attention Only hold EXT_5V output when external 5V is not supplied through Grove/Hat EXT_5V/5VIN. + */ +OPERATE_RET board_sticks3_power_set_ext_5v_hold(bool enable); + +/** + * @brief Enable or disable StickS3 L3B peripheral power hold while M5PM1 sleeps. + * @param[in] enable true to hold L3B state during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l3b_hold(bool enable); + +/** + * @brief Enable or disable StickS3 speaker amplifier hold while M5PM1 sleeps. + * @param[in] enable true to hold speaker amplifier state during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_speaker_amp_hold(bool enable); + +/** + * @brief Read and optionally clear StickS3 M5PM1 wake source flags. + * @param[out] wake_source wake source bitmask from M5PM1. + * @param[in] clear_after_read true to clear the returned flags. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_get_wake_source(uint8_t *wake_source, bool clear_after_read); + +/** + * @brief Configure a timer wake before entering M5PM1 shutdown. + * @param[in] seconds wake timer in seconds. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_timer_wake(uint32_t seconds); + +/** + * @brief Clear the M5PM1 timer wake configuration. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_clear_timer_wake(void); + +/** + * @brief Configure BMI270 INT1 via M5PM1 PYG4 as a PMIC wake source. + * @param[in] enable true to enable IMU wake source, false to disable it. + * @param[in] rising_edge true for rising-edge wake, false for falling-edge wake. + * @return OPRT_OK on success, error code on failure. + * @note The BMI270 itself must be configured separately before this wake source can fire. + */ +OPERATE_RET board_sticks3_power_set_imu_wake(bool enable, bool rising_edge); + +/** + * @brief Request StickS3 PMIC shutdown. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_shutdown(void); + +/** + * @brief Request StickS3 PMIC reboot. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_reboot(void); + +/** + * @brief Enable all currently supported StickS3 board power rails. + * @return OPRT_OK on success, error code on failure. + * @attention This enables EXT_5V output mode for bring-up. Do not feed external 5V into output interfaces. + */ +OPERATE_RET board_sticks3_power_enable_all(void); + +#ifdef __cplusplus +} +#endif +#endif /* __BOARD_CONFIG_H__ */ diff --git a/boards/ESP32/M5STACK_STICKS3/example/CMakeLists.txt b/boards/ESP32/M5STACK_STICKS3/example/CMakeLists.txt new file mode 100644 index 000000000..94da3dc31 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/example/CMakeLists.txt @@ -0,0 +1,19 @@ +## +# @file CMakeLists.txt +# @brief M5Stack StickS3 hardware bring-up example +#/ + +if (CONFIG_BOARD_CHOICE_M5STACK_STICKS3 STREQUAL "y") +set(APP_PATH ${CMAKE_CURRENT_LIST_DIR}) +get_filename_component(APP_NAME ${APP_PATH} NAME) +aux_source_directory(${APP_PATH}/src APP_SRCS) + +add_library(${EXAMPLE_LIB}) + +target_sources(${EXAMPLE_LIB} + PRIVATE + ${APP_SRCS} + ) +else() +message(FATAL_ERROR "m5stack_sticks3_hardware example requires BOARD_CHOICE_M5STACK_STICKS3") +endif() diff --git a/boards/ESP32/M5STACK_STICKS3/example/app_default.config b/boards/ESP32/M5STACK_STICKS3/example/app_default.config new file mode 100644 index 000000000..e75b29676 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/example/app_default.config @@ -0,0 +1,7 @@ +CONFIG_BOARD_CHOICE_ESP32=y +CONFIG_BOARD_CHOICE_M5STACK_STICKS3=y +CONFIG_BOARD_M5STACK_STICKS3_EXT_5V_OUTPUT=y +CONFIG_ENABLE_LIBLVGL=y +CONFIG_ENABLE_BUTTON=y +CONFIG_BUTTON_NAME="ai_chat_button" +CONFIG_BUTTON_NAME_2="ai_chat_button_2" diff --git a/boards/ESP32/M5STACK_STICKS3/example/config/M5STACK_STICKS3.config b/boards/ESP32/M5STACK_STICKS3/example/config/M5STACK_STICKS3.config new file mode 100644 index 000000000..e75b29676 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/example/config/M5STACK_STICKS3.config @@ -0,0 +1,7 @@ +CONFIG_BOARD_CHOICE_ESP32=y +CONFIG_BOARD_CHOICE_M5STACK_STICKS3=y +CONFIG_BOARD_M5STACK_STICKS3_EXT_5V_OUTPUT=y +CONFIG_ENABLE_LIBLVGL=y +CONFIG_ENABLE_BUTTON=y +CONFIG_BUTTON_NAME="ai_chat_button" +CONFIG_BUTTON_NAME_2="ai_chat_button_2" diff --git a/boards/ESP32/M5STACK_STICKS3/example/src/example_m5stack_sticks3_hw.c b/boards/ESP32/M5STACK_STICKS3/example/src/example_m5stack_sticks3_hw.c new file mode 100644 index 000000000..386017b77 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/example/src/example_m5stack_sticks3_hw.c @@ -0,0 +1,341 @@ +/** + * @file example_m5stack_sticks3_hw.c + * @brief M5Stack StickS3 board hardware bring-up example. + * @version 0.1 + * @date 2026-04-27 + * @copyright Copyright (c) 2026 Tuya Inc. All Rights Reserved. + */ +#include "tuya_cloud_types.h" + +#include "board_com_api.h" +#include "board_config.h" +#include "tal_api.h" +#include "tdl_audio_manage.h" +#include "tdl_button_manage.h" +#include "tkl_output.h" + +#include + +/* --------------------------------------------------------------------------- + * Macros + * --------------------------------------------------------------------------- */ +#define EXAMPLE_BOOT_DELAY_MS (5000U) +#define EXAMPLE_SAMPLE_RATE (16000U) +#define EXAMPLE_CHIRP_DURATION_MS (250U) +#define EXAMPLE_CHIRP_GAP_MS (1000U) +#define EXAMPLE_CHIRP_COUNT (3U) +#define EXAMPLE_CHUNK_SAMPLES (512U) + +/* --------------------------------------------------------------------------- + * File-scope variables + * --------------------------------------------------------------------------- */ +static THREAD_HANDLE s_app_thread = NULL; +static const int16_t s_sine_1khz[16] = { + 0, 10716, 19800, 25868, 28000, 25868, 19800, 10716, 0, -10716, -19800, -25868, -28000, -25868, -19800, -10716, +}; + +/* --------------------------------------------------------------------------- + * Function implementations + * --------------------------------------------------------------------------- */ +/** + * @brief Get a text name for a button event. + * @param[in] event button event. + * @return event name string. + */ +static const char *__button_event_name(TDL_BUTTON_TOUCH_EVENT_E event) +{ + switch (event) { + case TDL_BUTTON_PRESS_DOWN: + return "PRESS_DOWN"; + case TDL_BUTTON_PRESS_UP: + return "PRESS_UP"; + case TDL_BUTTON_PRESS_SINGLE_CLICK: + return "SINGLE_CLICK"; + case TDL_BUTTON_PRESS_DOUBLE_CLICK: + return "DOUBLE_CLICK"; + case TDL_BUTTON_PRESS_REPEAT: + return "REPEAT"; + case TDL_BUTTON_LONG_PRESS_START: + return "LONG_PRESS_START"; + case TDL_BUTTON_LONG_PRESS_HOLD: + return "LONG_PRESS_HOLD"; + case TDL_BUTTON_RECOVER_PRESS_UP: + return "RECOVER_PRESS_UP"; + default: + return "UNKNOWN"; + } +} + +/** + * @brief Log a StickS3 button event. + * @param[in] name button name. + * @param[in] event button event. + * @param[in] argc event argument. + * @return none + */ +static void __button_cb(char *name, TDL_BUTTON_TOUCH_EVENT_E event, void *argc) +{ + PR_NOTICE("StickS3 key event: name=%s event=%s arg=%u", name, __button_event_name(event), + (uint32_t)(uintptr_t)argc); +} + +/** + * @brief Register common button events for one button handle. + * @param[in] handle button handle. + * @return none + */ +static void __register_button_events(TDL_BUTTON_HANDLE handle) +{ + tdl_button_event_register(handle, TDL_BUTTON_PRESS_DOWN, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_PRESS_UP, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_PRESS_SINGLE_CLICK, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_PRESS_DOUBLE_CLICK, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_PRESS_REPEAT, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_LONG_PRESS_START, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_LONG_PRESS_HOLD, __button_cb); + tdl_button_event_register(handle, TDL_BUTTON_RECOVER_PRESS_UP, __button_cb); +} + +/** + * @brief Open StickS3 user buttons and log events. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __open_buttons(void) +{ + OPERATE_RET rt = OPRT_OK; + TDL_BUTTON_HANDLE button_hdl = NULL; + TDL_BUTTON_CFG_T button_cfg = { + .long_start_valid_time = 800, + .long_keep_timer = 1000, + .button_debounce_time = 50, + .button_repeat_valid_count = 3, + .button_repeat_valid_time = 500, + }; + + tdl_button_set_task_stack_size(4096); + +#if defined(BUTTON_NAME) + TUYA_CALL_ERR_RETURN(tdl_button_create(BUTTON_NAME, &button_cfg, &button_hdl)); + __register_button_events(button_hdl); + TUYA_CALL_ERR_LOG(tdl_button_set_ready_flag(BUTTON_NAME, TRUE)); + PR_NOTICE("StickS3 button 1 ready: %s", BUTTON_NAME); +#endif + +#if defined(BUTTON_NAME_2) + button_hdl = NULL; + TUYA_CALL_ERR_RETURN(tdl_button_create(BUTTON_NAME_2, &button_cfg, &button_hdl)); + __register_button_events(button_hdl); + TUYA_CALL_ERR_LOG(tdl_button_set_ready_flag(BUTTON_NAME_2, TRUE)); + PR_NOTICE("StickS3 button 2 ready: %s", BUTTON_NAME_2); +#endif + + return rt; +} + +/** + * @brief Log the first microphone frame to prove the codec is receiving. + * @param[in] type audio frame type. + * @param[in] status audio status. + * @param[in] data audio buffer. + * @param[in] len audio length in bytes. + * @return none + */ +static void __mic_cb(TDL_AUDIO_FRAME_FORMAT_E type, TDL_AUDIO_STATUS_E status, uint8_t *data, uint32_t len) +{ + static bool s_logged = false; + (void)type; + (void)status; + (void)data; + + if (!s_logged && (len > 0)) { + s_logged = true; + PR_NOTICE("StickS3 mic stream active, first frame len=%u", len); + } +} + +/** + * @brief Stream a tone or silence segment. + * @param[in] audio_hdl audio handle. + * @param[in,out] pcm temporary PCM buffer. + * @param[in] total_samples number of samples to stream. + * @param[in] tone true to stream 1 kHz tone, false to stream silence. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __stream_audio_segment(TDL_AUDIO_HANDLE_T audio_hdl, int16_t *pcm, uint32_t total_samples, bool tone) +{ + OPERATE_RET rt = OPRT_OK; + uint32_t written = 0; + + while (written < total_samples) { + uint32_t chunk_samples = total_samples - written; + if (chunk_samples > EXAMPLE_CHUNK_SAMPLES) { + chunk_samples = EXAMPLE_CHUNK_SAMPLES; + } + + if (tone) { + for (uint32_t i = 0; i < chunk_samples; i++) { + pcm[i] = s_sine_1khz[(written + i) % CNTSOF(s_sine_1khz)]; + } + } else { + memset(pcm, 0, chunk_samples * sizeof(int16_t)); + } + + rt = tdl_audio_play(audio_hdl, (uint8_t *)pcm, chunk_samples * sizeof(int16_t)); + if (rt != OPRT_OK) { + PR_ERR("StickS3 audio segment play failed rt:%d", rt); + return rt; + } + written += chunk_samples; + } + + return OPRT_OK; +} + +/** + * @brief Open ES8311 audio and play 1 kHz chirps with one second gaps. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __play_chirps(void) +{ + OPERATE_RET rt = OPRT_OK; + TDL_AUDIO_HANDLE_T audio_hdl = NULL; + int16_t *pcm = NULL; + uint32_t chirp_samples = EXAMPLE_SAMPLE_RATE * EXAMPLE_CHIRP_DURATION_MS / 1000U; + uint32_t gap_samples = EXAMPLE_SAMPLE_RATE * EXAMPLE_CHIRP_GAP_MS / 1000U; + + rt = tdl_audio_find(AUDIO_CODEC_NAME, &audio_hdl); + if (rt != OPRT_OK) { + PR_NOTICE("StickS3 audio test skipped: %s not registered", AUDIO_CODEC_NAME); + return OPRT_OK; + } + + rt = tdl_audio_open(audio_hdl, __mic_cb); + if (rt != OPRT_OK) { + PR_NOTICE("StickS3 audio test skipped: codec open failed rt:%d", rt); + return OPRT_OK; + } + TUYA_CALL_ERR_LOG(tdl_audio_volume_set(audio_hdl, 90)); + + pcm = (int16_t *)tal_malloc(EXAMPLE_CHUNK_SAMPLES * sizeof(int16_t)); + if (pcm == NULL) { + return OPRT_MALLOC_FAILED; + } + + PR_NOTICE("StickS3 audio test: playing %u x 1kHz chirps, %ums gap", EXAMPLE_CHIRP_COUNT, EXAMPLE_CHIRP_GAP_MS); + for (uint32_t chirp = 0; chirp < EXAMPLE_CHIRP_COUNT; chirp++) { + rt = __stream_audio_segment(audio_hdl, pcm, chirp_samples, true); + if (rt != OPRT_OK) { + break; + } + if (chirp + 1U < EXAMPLE_CHIRP_COUNT) { + rt = __stream_audio_segment(audio_hdl, pcm, gap_samples, false); + if (rt != OPRT_OK) { + break; + } + } + } + + tal_free(pcm); + PR_NOTICE("StickS3 audio test done"); + + return rt; +} + +/** + * @brief Print common application information. + * @return none + */ +static void __print_app_info(void) +{ + PR_NOTICE("Application information:"); + PR_NOTICE("Project name: %s", PROJECT_NAME); + PR_NOTICE("App version: %s", PROJECT_VERSION); + PR_NOTICE("Compile time: %s", __DATE__); + PR_NOTICE("TuyaOpen version: %s", OPEN_VERSION); + PR_NOTICE("TuyaOpen commit-id: %s", OPEN_COMMIT); + PR_NOTICE("Platform chip: %s", PLATFORM_CHIP); + PR_NOTICE("Platform board: %s", PLATFORM_BOARD); + PR_NOTICE("Platform commit-id: %s", PLATFORM_COMMIT); +} + +/** + * @brief Run the StickS3 hardware bring-up example. + * @return none + */ +void user_main(void) +{ + OPERATE_RET rt = OPRT_OK; + + tal_system_sleep(EXAMPLE_BOOT_DELAY_MS); + tal_log_init(TAL_LOG_LEVEL_DEBUG, 1024, (TAL_LOG_OUTPUT_CB)tkl_log_output); + __print_app_info(); + + PR_NOTICE("StickS3 board_register_hardware begin"); + rt = board_register_hardware(); + if (rt != OPRT_OK) { + PR_ERR("StickS3 board_register_hardware failed rt:%d", rt); + } else { + PR_NOTICE("StickS3 board_register_hardware success"); + } + + PR_NOTICE("StickS3 board_display_init begin"); + rt = board_display_init(); + if (rt != OPRT_OK) { + PR_ERR("StickS3 board_display_init failed rt:%d", rt); + } else { + PR_NOTICE("StickS3 board_display_init success"); + } + TUYA_CALL_ERR_LOG(board_display_show_red_box()); + + TUYA_CALL_ERR_LOG(__open_buttons()); + TUYA_CALL_ERR_LOG(__play_chirps()); + + PR_NOTICE("StickS3 hardware example idle; press KEY1/KEY2 to log events"); + for (;;) { + PR_NOTICE("StickS3 hardware example heartbeat, heap=%u", tal_system_get_free_heap_size()); + tal_system_sleep(10000); + } +} + +#if OPERATING_SYSTEM == SYSTEM_LINUX +/** + * @brief Linux entry point. + * @param[in] argc argument count. + * @param[in] argv argument vector. + * @return none + */ +void main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + user_main(); +} +#else +/** + * @brief TuyaOpen application task. + * @param[in] arg unused task argument. + * @return none + */ +static void __app_thread(void *arg) +{ + (void)arg; + user_main(); + tal_thread_delete(s_app_thread); + s_app_thread = NULL; +} + +/** + * @brief TuyaOpen ESP application entry. + * @return none + */ +void tuya_app_main(void) +{ + THREAD_CFG_T thrd_param = { + .stackDepth = 1024 * 6, + .priority = THREAD_PRIO_1, + .thrdname = "sticks3_hw", + }; + + tal_thread_create_and_start(&s_app_thread, NULL, NULL, __app_thread, NULL, &thrd_param); +} +#endif diff --git a/boards/ESP32/M5STACK_STICKS3/m5stack_sticks3.c b/boards/ESP32/M5STACK_STICKS3/m5stack_sticks3.c new file mode 100644 index 000000000..ed4afef49 --- /dev/null +++ b/boards/ESP32/M5STACK_STICKS3/m5stack_sticks3.c @@ -0,0 +1,486 @@ +/** + * @file m5stack_sticks3.c + * @brief Board-level hardware registration for M5Stack StickS3. + * @version 0.1 + * @date 2026-04-27 + * @copyright Copyright (c) Tuya Inc. All Rights Reserved. + */ +#include "tuya_cloud_types.h" + +#include "board_com_api.h" +#include "board_config.h" +#include "lcd_st7789_spi.h" +#include "m5pm1_driver.h" +#include "tal_api.h" +#include "tdd_audio_8311_codec.h" +#include "tkl_gpio.h" +#include "tkl_i2c.h" +#include "tkl_pinmux.h" + +#if defined(ENABLE_BUTTON) && (ENABLE_BUTTON == 1) +#include "tdd_button_gpio.h" +#endif + +/* --------------------------------------------------------------------------- + * Function implementations + * --------------------------------------------------------------------------- */ +/** + * @brief Configure the TuyaOpen TKL I2C pinmux for the StickS3 internal bus. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __board_i2c_pinmux_config(void) +{ + OPERATE_RET rt = OPRT_OK; + TUYA_PIN_FUNC_E scl_func = TUYA_IIC1_SCL; + TUYA_PIN_FUNC_E sda_func = TUYA_IIC1_SDA; + + switch (I2C_NUM) { + case TUYA_I2C_NUM_0: + scl_func = TUYA_IIC0_SCL; + sda_func = TUYA_IIC0_SDA; + break; + case TUYA_I2C_NUM_1: + scl_func = TUYA_IIC1_SCL; + sda_func = TUYA_IIC1_SDA; + break; + default: + PR_ERR("StickS3 unsupported TKL I2C port:%d", I2C_NUM); + return OPRT_INVALID_PARM; + } + + TUYA_CALL_ERR_RETURN(tkl_io_pinmux_config((TUYA_PIN_NAME_E)I2C_SCL_IO, scl_func)); + TUYA_CALL_ERR_RETURN(tkl_io_pinmux_config((TUYA_PIN_NAME_E)I2C_SDA_IO, sda_func)); + + PR_NOTICE("StickS3 I2C pinmux: port:%d SCL:%d SDA:%d", I2C_NUM, I2C_SCL_IO, I2C_SDA_IO); + + return rt; +} + +/** + * @brief Configure one M5PM1 GPIO as push-pull output. + * @param[in] pin M5PM1 GPIO number. + * @param[in] high true for high level, false for low level. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_gpio_output(M5PM1_GPIO_NUM_E pin, bool high) +{ + OPERATE_RET rt = OPRT_OK; + + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_wake_enable(pin, false)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_func(pin, M5PM1_GPIO_FUNC_GPIO)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_mode(pin, M5PM1_GPIO_MODE_OUTPUT)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_drive(pin, M5PM1_GPIO_DRIVE_PUSHPULL)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_pull(pin, M5PM1_GPIO_PULL_NONE)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_output(pin, high)); + + return OPRT_OK; +} + +/** + * @brief Configure one M5PM1 GPIO as input. + * @param[in] pin M5PM1 GPIO number. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_gpio_input(M5PM1_GPIO_NUM_E pin) +{ + OPERATE_RET rt = OPRT_OK; + + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_func(pin, M5PM1_GPIO_FUNC_GPIO)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_mode(pin, M5PM1_GPIO_MODE_INPUT)); + + return OPRT_OK; +} + +/** + * @brief Enable or disable StickS3 L1 IMU power. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l1(bool enable) +{ + return m5pm1_set_ldo_enable(enable); +} + +/** + * @brief Enable or disable StickS3 L2/L3A ESP32-S3-side power switch. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l2_l3a(bool enable) +{ + return m5pm1_set_dcdc_enable(enable); +} + +/** + * @brief Enable or disable StickS3 EXT_5V output mode. + * @param[in] enable true for 5V output mode, false for 5V input mode. + * @return OPRT_OK on success, error code on failure. + * @attention Only enable output mode when external 5V is not supplied through Grove/Hat EXT_5V/5VIN. + */ +OPERATE_RET board_sticks3_power_set_ext_5v_output(bool enable) +{ + return m5pm1_set_boost_enable(enable); +} + +/** + * @brief Enable or disable StickS3 L3B peripheral power. + * @param[in] enable true to power LCD backlight, microphone, and speaker peripherals. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l3b(bool enable) +{ + bool level = enable ? M5PM1_L3B_POWER_ENABLE_LEVEL : M5PM1_L3B_POWER_DISABLE_LEVEL; + + return __m5pm1_gpio_output((M5PM1_GPIO_NUM_E)M5PM1_GPIO_L3B_POWER, level); +} + +/** + * @brief Enable or disable StickS3 speaker amplifier. + * @param[in] enable true to enable amplifier, false to disable amplifier. + * @return OPRT_OK on success, error code on failure. + * @note StickS3 routes the amplifier SHDN signal through M5PM1 PYG3 + * (G3_WAKEin/IRQout/PWM13 alternate function). This API forces it to + * GPIO push-pull output and drives high to enable the amplifier. + */ +OPERATE_RET board_sticks3_power_set_speaker_amp(bool enable) +{ + bool level = enable ? M5PM1_SPK_AMP_ENABLE_LEVEL : M5PM1_SPK_AMP_DISABLE_LEVEL; + + return __m5pm1_gpio_output((M5PM1_GPIO_NUM_E)M5PM1_GPIO_SPK_AMP_SHDN, level); +} + +/** + * @brief Enable or disable StickS3 L1 IMU power hold while M5PM1 sleeps. + * @param[in] enable true to keep L1 held during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l1_hold(bool enable) +{ + return m5pm1_ldo_set_power_hold(enable); +} + +/** + * @brief Enable or disable StickS3 EXT_5V power hold while M5PM1 sleeps. + * @param[in] enable true to keep EXT_5V held during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + * @attention Only hold EXT_5V output when external 5V is not supplied through Grove/Hat EXT_5V/5VIN. + */ +OPERATE_RET board_sticks3_power_set_ext_5v_hold(bool enable) +{ + return m5pm1_boost_set_power_hold(enable); +} + +/** + * @brief Enable or disable StickS3 L3B peripheral power hold while M5PM1 sleeps. + * @param[in] enable true to hold L3B state during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_l3b_hold(bool enable) +{ + return m5pm1_gpio_set_power_hold((M5PM1_GPIO_NUM_E)M5PM1_GPIO_L3B_POWER, enable); +} + +/** + * @brief Enable or disable StickS3 speaker amplifier hold while M5PM1 sleeps. + * @param[in] enable true to hold speaker amplifier state during PMIC sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_speaker_amp_hold(bool enable) +{ + return m5pm1_gpio_set_power_hold((M5PM1_GPIO_NUM_E)M5PM1_GPIO_SPK_AMP_SHDN, enable); +} + +/** + * @brief Read and optionally clear StickS3 M5PM1 wake source flags. + * @param[out] wake_source wake source bitmask from M5PM1. + * @param[in] clear_after_read true to clear the returned flags. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_get_wake_source(uint8_t *wake_source, bool clear_after_read) +{ + M5PM1_CLEAN_E clean = clear_after_read ? M5PM1_CLEAN_ONCE : M5PM1_CLEAN_NONE; + + return m5pm1_get_wake_source(wake_source, clean); +} + +/** + * @brief Configure a timer wake before entering M5PM1 shutdown. + * @param[in] seconds wake timer in seconds. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_set_timer_wake(uint32_t seconds) +{ + return m5pm1_timer_set(seconds, M5PM1_TIM_ACTION_POWERON); +} + +/** + * @brief Clear the M5PM1 timer wake configuration. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_clear_timer_wake(void) +{ + return m5pm1_timer_clear(); +} + +/** + * @brief Configure BMI270 INT1 via M5PM1 PYG4 as a PMIC wake source. + * @param[in] enable true to enable IMU wake source, false to disable it. + * @param[in] rising_edge true for rising-edge wake, false for falling-edge wake. + * @return OPRT_OK on success, error code on failure. + * @note The BMI270 itself must be configured separately before this wake source can fire. + */ +OPERATE_RET board_sticks3_power_set_imu_wake(bool enable, bool rising_edge) +{ + OPERATE_RET rt = OPRT_OK; + M5PM1_GPIO_WAKE_EDGE_E edge = rising_edge ? M5PM1_GPIO_WAKE_RISING : M5PM1_GPIO_WAKE_FALLING; + + if (!enable) { + return m5pm1_gpio_set_wake_enable((M5PM1_GPIO_NUM_E)M5PM1_GPIO_IMU_INT1, false); + } + + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_func((M5PM1_GPIO_NUM_E)M5PM1_GPIO_IMU_INT1, M5PM1_GPIO_FUNC_WAKE)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_mode((M5PM1_GPIO_NUM_E)M5PM1_GPIO_IMU_INT1, M5PM1_GPIO_MODE_INPUT)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_wake_edge((M5PM1_GPIO_NUM_E)M5PM1_GPIO_IMU_INT1, edge)); + TUYA_CALL_ERR_RETURN(m5pm1_gpio_set_wake_enable((M5PM1_GPIO_NUM_E)M5PM1_GPIO_IMU_INT1, true)); + + return OPRT_OK; +} + +/** + * @brief Request StickS3 PMIC shutdown. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_shutdown(void) +{ + return m5pm1_shutdown(); +} + +/** + * @brief Request StickS3 PMIC reboot. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_sticks3_power_reboot(void) +{ + return m5pm1_reboot(); +} + +/** + * @brief Enable all currently supported StickS3 board power rails. + * @return OPRT_OK on success, error code on failure. + * @attention This enables EXT_5V output mode for bring-up. Do not feed external 5V into output interfaces. + */ +OPERATE_RET board_sticks3_power_enable_all(void) +{ + OPERATE_RET rt = OPRT_OK; + + /* L0 is the always-on battery/PMIC domain. The rest are independent switches sourced from L0. */ + TUYA_CALL_ERR_LOG(m5pm1_set_charge_enable(true)); + TUYA_CALL_ERR_RETURN(board_sticks3_power_set_l1(true)); + TUYA_CALL_ERR_RETURN(board_sticks3_power_set_l2_l3a(true)); + TUYA_CALL_ERR_RETURN(board_sticks3_power_set_ext_5v_output(true)); + TUYA_CALL_ERR_RETURN(board_sticks3_power_set_l3b(true)); + + return OPRT_OK; +} + +/** + * @brief Initialize StickS3 PMIC and board power rails. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __board_register_power(void) +{ + OPERATE_RET rt = OPRT_OK; + M5PM1_PWR_SRC_E power_src = M5PM1_PWR_SRC_UNKNOWN; + uint8_t wake_src = 0; + M5PM1_CFG_T cfg = { + .i2c_port = M5PM1_I2C_PORT, + .i2c_addr = M5PM1_I2C_ADDR, + }; + + TUYA_CALL_ERR_RETURN(__board_i2c_pinmux_config()); + TUYA_CALL_ERR_RETURN(m5pm1_init(&cfg)); + TUYA_CALL_ERR_LOG(m5pm1_get_power_source(&power_src)); + TUYA_CALL_ERR_LOG(m5pm1_get_wake_source(&wake_src, M5PM1_CLEAN_ONCE)); + TUYA_CALL_ERR_LOG(m5pm1_set_i2c_sleep_time(0)); + + /* PYG0 is the PMIC charge-status input in the official StickS3 power setup. */ + TUYA_CALL_ERR_RETURN(__m5pm1_gpio_input((M5PM1_GPIO_NUM_E)M5PM1_GPIO_CHARGE_STATUS)); + + /* PYG4 is connected to the BMI270 INT1 signal for later wake-up/power-save work. */ + TUYA_CALL_ERR_RETURN(__m5pm1_gpio_input((M5PM1_GPIO_NUM_E)M5PM1_GPIO_IMU_INT1)); + + TUYA_CALL_ERR_RETURN(board_sticks3_power_enable_all()); + + /* Keep the speaker amplifier SHDN low until the audio output path opens. */ + TUYA_CALL_ERR_RETURN(board_sticks3_power_set_speaker_amp(false)); + + tal_system_sleep(100); + + PR_NOTICE("StickS3 M5PM1 power initialized: src:%d wake:0x%02x L1 on, L2/L3A on, EXT_5V output on, " + "3V3_L3B_AU on, PYG3_SPK_SHDN off", + power_src, wake_src); + + return OPRT_OK; +} + +/** + * @brief Enable or disable the StickS3 speaker amplifier. + * @param[in] enable true to enable amplifier, false to disable amplifier. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __board_audio_pa_enable(bool enable) +{ + return board_sticks3_power_set_speaker_amp(enable); +} + +/** + * @brief Probe the optional StickS3 ES8311 codec on the shared internal I2C bus. + * @return true if the codec ACKs its 7-bit I2C address, false otherwise. + */ +static bool __board_audio_codec_present(void) +{ + return (tkl_i2c_master_send(I2C_NUM, AUDIO_CODEC_ES8311_ADDR_7BIT, NULL, 0, FALSE) == OPRT_OK); +} + +/** + * @brief Register StickS3 ES8311 audio codec. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __board_register_audio(void) +{ + OPERATE_RET rt = OPRT_OK; + +#if defined(AUDIO_CODEC_NAME) + TDD_AUDIO_8311_CODEC_T cfg = {0}; + + if (!__board_audio_codec_present()) { + PR_NOTICE("StickS3 ES8311 not detected on shared I2C addr7:0x%02x; audio demo disabled", + AUDIO_CODEC_ES8311_ADDR_7BIT); + return OPRT_OK; + } + + cfg.i2c_id = I2C_NUM; + cfg.i2c_scl_io = I2C_SCL_IO; + cfg.i2c_sda_io = I2C_SDA_IO; + cfg.mic_sample_rate = I2S_INPUT_SAMPLE_RATE; + cfg.spk_sample_rate = I2S_OUTPUT_SAMPLE_RATE; + cfg.i2s_id = I2S_NUM; + cfg.i2s_mck_io = I2S_MCK_IO; + cfg.i2s_bck_io = I2S_BCK_IO; + cfg.i2s_ws_io = I2S_WS_IO; + cfg.i2s_do_io = I2S_DO_IO; + cfg.i2s_di_io = I2S_DI_IO; + cfg.gpio_output_pa = GPIO_OUTPUT_PA; + cfg.pa_enable_cb = __board_audio_pa_enable; + cfg.es8311_addr = AUDIO_CODEC_ES8311_ADDR; + cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM; + cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM; + cfg.default_volume = 70; + + TUYA_CALL_ERR_RETURN(tdd_audio_8311_codec_register(AUDIO_CODEC_NAME, cfg)); +#endif + + return rt; +} + +/** + * @brief Register StickS3 hardware buttons. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __board_register_button(void) +{ +#if !defined(ENABLE_BUTTON) || (ENABLE_BUTTON != 1) + return OPRT_OK; +#else + OPERATE_RET rt = OPRT_OK; + +#if defined(BUTTON_NAME) + BUTTON_GPIO_CFG_T button_hw_cfg = { + .pin = BOARD_BUTTON_PIN, + .level = BOARD_BUTTON_ACTIVE_LV, + .mode = BUTTON_TIMER_SCAN_MODE, + .pin_type.gpio_pull = TUYA_GPIO_PULLUP, + }; + + TUYA_CALL_ERR_RETURN(tdd_gpio_button_register(BUTTON_NAME, &button_hw_cfg)); +#endif + +#if ((defined(ENABLE_BUTTON_2) && (ENABLE_BUTTON_2 == 1)) || defined(BOARD_CHOICE_M5STACK_STICKS3)) && defined(BUTTON_NAME_2) + BUTTON_GPIO_CFG_T button_2_hw_cfg = { + .pin = BOARD_BUTTON_2_PIN, + .level = BOARD_BUTTON_2_ACTIVE_LV, + .mode = BUTTON_TIMER_SCAN_MODE, + .pin_type.gpio_pull = TUYA_GPIO_PULLUP, + }; + + TUYA_CALL_ERR_RETURN(tdd_gpio_button_register(BUTTON_NAME_2, &button_2_hw_cfg)); +#endif + + return rt; +#endif +} + +/** + * @brief Register all board hardware peripherals. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET board_register_hardware(void) +{ + OPERATE_RET rt = OPRT_OK; + + TUYA_CALL_ERR_RETURN(__board_register_power()); + TUYA_CALL_ERR_LOG(__board_register_button()); + TUYA_CALL_ERR_LOG(__board_register_audio()); + + return rt; +} + +/** + * @brief Initialize the StickS3 display. + * @return 0 on success, error code on failure. + */ +int board_display_init(void) +{ + int rt = lcd_st7789_spi_init(); + TUYA_GPIO_LEVEL_E backlight_level = TUYA_GPIO_LEVEL_HIGH; + + if (rt != OPRT_OK) { + PR_ERR("lcd_st7789_spi_init failed:%d", rt); + return rt; + } + + TUYA_GPIO_BASE_CFG_T out_pin_cfg = { + .mode = TUYA_GPIO_PULLUP, + .direct = TUYA_GPIO_OUTPUT, + .level = TUYA_GPIO_LEVEL_LOW, + }; + + TUYA_CALL_ERR_LOG(tkl_gpio_init(DISPLAY_BACKLIGHT_PIN, &out_pin_cfg)); + if (DISPLAY_BACKLIGHT_OUTPUT_INVERT) { + backlight_level = TUYA_GPIO_LEVEL_LOW; + } + tkl_gpio_write(DISPLAY_BACKLIGHT_PIN, backlight_level); + + PR_NOTICE("StickS3 LCD initialized: ST7789P3 %dx%d, BL gpio:%d level:%d", DISPLAY_WIDTH, DISPLAY_HEIGHT, + DISPLAY_BACKLIGHT_PIN, backlight_level); + + return OPRT_OK; +} + +/** + * @brief Get the ESP LCD panel IO handle. + * @return Panel IO handle, or NULL if not initialized. + */ +void *board_display_get_panel_io_handle(void) +{ + return lcd_st7789_spi_get_panel_io_handle(); +} + +/** + * @brief Get the ESP LCD panel handle. + * @return Panel handle, or NULL if not initialized. + */ +void *board_display_get_panel_handle(void) +{ + return lcd_st7789_spi_get_panel_handle(); +} diff --git a/boards/ESP32/common/audio/tdd_audio_8311_codec.c b/boards/ESP32/common/audio/tdd_audio_8311_codec.c index bf148f94e..361138174 100644 --- a/boards/ESP32/common/audio/tdd_audio_8311_codec.c +++ b/boards/ESP32/common/audio/tdd_audio_8311_codec.c @@ -55,6 +55,7 @@ static int input_sample_rate_ = 0; static int output_sample_rate_ = 0; static int output_volume_ = 0; static gpio_num_t pa_pin_ = 0; +static TDD_AUDIO_PA_ENABLE_CB pa_enable_cb_ = NULL; static i2c_master_bus_handle_t codec_i2c_bus_ = NULL; static const audio_codec_data_if_t *data_if_ = NULL; static const audio_codec_ctrl_if_t *ctrl_if_ = NULL; @@ -127,6 +128,8 @@ static void SetOutputVolume(int volume) static void EnableOutput(bool enable) { + OPERATE_RET rt = OPRT_OK; + if (enable) { // Play 16bit 1 channel esp_codec_dev_sample_info_t fs = { @@ -138,22 +141,32 @@ static void EnableOutput(bool enable) }; ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); - if (pa_pin_ != GPIO_NUM_NC) { + if (pa_enable_cb_ != NULL) { + rt = pa_enable_cb_(true); + if (rt != OPRT_OK) { + PR_ERR("PA enable callback failed:%d", rt); + } + } else if (pa_pin_ != GPIO_NUM_NC) { gpio_set_level(pa_pin_, 1); } } else { ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); - if (pa_pin_ != GPIO_NUM_NC) { + if (pa_enable_cb_ != NULL) { + rt = pa_enable_cb_(false); + if (rt != OPRT_OK) { + PR_ERR("PA disable callback failed:%d", rt); + } + } else if (pa_pin_ != GPIO_NUM_NC) { gpio_set_level(pa_pin_, 0); } } } -static void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, - uint32_t dma_desc_num, uint32_t dma_frame_num) +static void CreateDuplexChannels(TUYA_I2S_NUM_E i2s_id, gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, + gpio_num_t dout, gpio_num_t din, uint32_t dma_desc_num, uint32_t dma_frame_num) { i2s_chan_config_t chan_cfg = { - .id = I2S_NUM_0, + .id = (i2s_port_t)i2s_id, .role = I2S_ROLE_MASTER, .dma_desc_num = dma_desc_num, .dma_frame_num = dma_frame_num, @@ -201,9 +214,15 @@ OPERATE_RET codec_8311_init(TUYA_I2S_NUM_E i2s_num, const TDD_AUDIO_8311_CODEC_T { OPERATE_RET rt = OPRT_OK; void *i2c_master_handle = NULL; - i2c_port_t i2c_port = (i2c_port_t)(i2s_config->i2c_id); - uint8_t es8311_addr = i2s_config->es8311_addr; + i2c_port_t i2c_port = I2C_NUM_0; + uint8_t es8311_addr = 0; + + TUYA_CHECK_NULL_RETURN(i2s_config, OPRT_INVALID_PARM); + + i2c_port = (i2c_port_t)(i2s_config->i2c_id); + es8311_addr = i2s_config->es8311_addr; pa_pin_ = i2s_config->gpio_output_pa; + pa_enable_cb_ = i2s_config->pa_enable_cb; input_sample_rate_ = i2s_config->mic_sample_rate; output_sample_rate_ = i2s_config->spk_sample_rate; output_volume_ = i2s_config->default_volume; @@ -219,18 +238,27 @@ OPERATE_RET codec_8311_init(TUYA_I2S_NUM_E i2s_num, const TDD_AUDIO_8311_CODEC_T // i2s_config->dma_desc_num, i2s_config->dma_frame_num); codec_i2c_bus_ = __i2c_init(i2s_config->i2c_id, i2s_config->i2c_scl_io, i2s_config->i2c_sda_io); + if (codec_i2c_bus_ == NULL) { + PR_ERR("ES8311 I2C bus init failed, port:%d scl:%d sda:%d", i2c_port, i2s_config->i2c_scl_io, + i2s_config->i2c_sda_io); + return OPRT_COM_ERROR; + } i2c_master_handle = (void *)codec_i2c_bus_; - CreateDuplexChannels(i2s_config->i2s_mck_io, i2s_config->i2s_bck_io, i2s_config->i2s_ws_io, i2s_config->i2s_do_io, - i2s_config->i2s_di_io, i2s_config->dma_desc_num, i2s_config->dma_frame_num); + CreateDuplexChannels(i2s_num, i2s_config->i2s_mck_io, i2s_config->i2s_bck_io, i2s_config->i2s_ws_io, + i2s_config->i2s_do_io, i2s_config->i2s_di_io, i2s_config->dma_desc_num, + i2s_config->dma_frame_num); // Do initialize of related interface: data_if, ctrl_if and gpio_if audio_codec_i2s_cfg_t i2s_cfg = { - .port = i2s_config->i2s_id, + .port = i2s_num, .rx_handle = rx_handle_, .tx_handle = tx_handle_, }; data_if_ = audio_codec_new_i2s_data(&i2s_cfg); - assert(data_if_ != NULL); + if (data_if_ == NULL) { + PR_ERR("ES8311 I2S data interface create failed"); + return OPRT_COM_ERROR; + } // Output audio_codec_i2c_cfg_t i2c_cfg = { @@ -239,10 +267,16 @@ OPERATE_RET codec_8311_init(TUYA_I2S_NUM_E i2s_num, const TDD_AUDIO_8311_CODEC_T .bus_handle = i2c_master_handle, }; ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); - assert(ctrl_if_ != NULL); + if (ctrl_if_ == NULL) { + PR_ERR("ES8311 I2C control interface create failed"); + return OPRT_COM_ERROR; + } gpio_if_ = audio_codec_new_gpio(); - assert(gpio_if_ != NULL); + if (gpio_if_ == NULL) { + PR_ERR("ES8311 GPIO interface create failed"); + return OPRT_COM_ERROR; + } es8311_codec_cfg_t es8311_cfg = {}; es8311_cfg.ctrl_if = ctrl_if_; @@ -253,7 +287,10 @@ OPERATE_RET codec_8311_init(TUYA_I2S_NUM_E i2s_num, const TDD_AUDIO_8311_CODEC_T es8311_cfg.hw_gain.pa_voltage = 5.0; es8311_cfg.hw_gain.codec_dac_voltage = 3.3; codec_if_ = es8311_codec_new(&es8311_cfg); - assert(codec_if_ != NULL); + if (codec_if_ == NULL) { + PR_ERR("ES8311 codec interface create failed, addr:0x%02x port:%d", es8311_addr, i2c_port); + return OPRT_COM_ERROR; + } esp_codec_dev_cfg_t dev_cfg = { .dev_type = ESP_CODEC_DEV_TYPE_OUT, @@ -261,10 +298,16 @@ OPERATE_RET codec_8311_init(TUYA_I2S_NUM_E i2s_num, const TDD_AUDIO_8311_CODEC_T .data_if = data_if_, }; output_dev_ = esp_codec_dev_new(&dev_cfg); - assert(output_dev_ != NULL); + if (output_dev_ == NULL) { + PR_ERR("ES8311 output device create failed"); + return OPRT_COM_ERROR; + } dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; input_dev_ = esp_codec_dev_new(&dev_cfg); - assert(input_dev_ != NULL); + if (input_dev_ == NULL) { + PR_ERR("ES8311 input device create failed"); + return OPRT_COM_ERROR; + } esp_codec_set_disable_when_closed(output_dev_, false); esp_codec_set_disable_when_closed(input_dev_, false); ESP_LOGI(TAG, "Es8311AudioCodec initialized"); @@ -344,9 +387,13 @@ static OPERATE_RET __tdd_audio_esp_i2s_8311_open(TDD_AUDIO_HANDLE_T handle, TDL_ TDD_AUDIO_8311_CODEC_T *tdd_i2s_cfg = &hdl->cfg; - hdl->i2s_id = TUYA_I2S_NUM_0; + hdl->i2s_id = (TUYA_I2S_NUM_E)tdd_i2s_cfg->i2s_id; - codec_8311_init(hdl->i2s_id, tdd_i2s_cfg); + rt = codec_8311_init(hdl->i2s_id, tdd_i2s_cfg); + if (rt != OPRT_OK) { + PR_ERR("I2S 8311 codec init failed rt:%d", rt); + return rt; + } PR_NOTICE("I2S 8311 channels created"); diff --git a/boards/ESP32/common/audio/tdd_audio_8311_codec.h b/boards/ESP32/common/audio/tdd_audio_8311_codec.h index 822200ae0..c0fe9343f 100644 --- a/boards/ESP32/common/audio/tdd_audio_8311_codec.h +++ b/boards/ESP32/common/audio/tdd_audio_8311_codec.h @@ -19,6 +19,8 @@ extern "C" { /*********************************************************** ************************macro define************************ ***********************************************************/ +typedef OPERATE_RET (*TDD_AUDIO_PA_ENABLE_CB)(bool enable); + typedef struct { uint8_t i2c_id; TUYA_GPIO_NUM_E i2c_sda_io; /*!< GPIO number of I2C SDA signal, pulled-up internally */ @@ -32,6 +34,7 @@ typedef struct { TUYA_GPIO_NUM_E i2s_do_io; TUYA_GPIO_NUM_E i2s_di_io; TUYA_GPIO_NUM_E gpio_output_pa; + TDD_AUDIO_PA_ENABLE_CB pa_enable_cb; uint8_t es8311_addr; uint32_t dma_desc_num; uint32_t dma_frame_num; diff --git a/boards/ESP32/common/lcd/lcd_st7789_spi.c b/boards/ESP32/common/lcd/lcd_st7789_spi.c index 2dd24d29d..fbb1eea1d 100644 --- a/boards/ESP32/common/lcd/lcd_st7789_spi.c +++ b/boards/ESP32/common/lcd/lcd_st7789_spi.c @@ -27,6 +27,22 @@ ***********************************************************/ #define TAG "LCD_ST7789_SPI" +#ifndef LCD_RST_PIN +#define LCD_RST_PIN GPIO_NUM_NC +#endif + +#ifndef DISPLAY_COLOR_INVERT +#define DISPLAY_COLOR_INVERT false +#endif + +#ifndef DISPLAY_OFFSET_X +#define DISPLAY_OFFSET_X (0) +#endif + +#ifndef DISPLAY_OFFSET_Y +#define DISPLAY_OFFSET_Y (0) +#endif + /*********************************************************** ***********************typedef define*********************** ***********************************************************/ @@ -88,18 +104,20 @@ int lcd_st7789_spi_init(void) ESP_LOGD(TAG, "Install LCD driver"); esp_lcd_panel_dev_config_t panel_config = {}; - panel_config.reset_gpio_num = GPIO_NUM_NC; + panel_config.reset_gpio_num = LCD_RST_PIN; panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; panel_config.bits_per_pixel = 16; - panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG, + panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG; esp_lcd_new_panel_st7789(lcd_config.panel_io, &panel_config, &lcd_config.panel); esp_lcd_panel_reset(lcd_config.panel); esp_lcd_panel_init(lcd_config.panel); - esp_lcd_panel_invert_color(lcd_config.panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + esp_lcd_panel_invert_color(lcd_config.panel, DISPLAY_COLOR_INVERT); + esp_lcd_panel_set_gap(lcd_config.panel, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y); esp_lcd_panel_swap_xy(lcd_config.panel, DISPLAY_SWAP_XY); esp_lcd_panel_mirror(lcd_config.panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + esp_lcd_panel_disp_on_off(lcd_config.panel, true); return 0; } diff --git a/src/peripherals/pmic/CMakeLists.txt b/src/peripherals/pmic/CMakeLists.txt index 0b4a6221d..0cffad3be 100755 --- a/src/peripherals/pmic/CMakeLists.txt +++ b/src/peripherals/pmic/CMakeLists.txt @@ -20,6 +20,11 @@ if (CONFIG_ENABLE_PMIC_AXP2101 STREQUAL "y") list(APPEND LIB_PUBLIC_INC ${MODULE_PATH}/axp2101/include) endif() +if (CONFIG_ENABLE_PMIC_M5PM1 STREQUAL "y") + list(APPEND LIB_SRCS ${MODULE_PATH}/m5pm1/src/m5pm1_driver.c) + list(APPEND LIB_PUBLIC_INC ${MODULE_PATH}/m5pm1/include) +endif() + ######################################## # Target Configure ######################################## diff --git a/src/peripherals/pmic/Kconfig b/src/peripherals/pmic/Kconfig index 79caba8ed..3bce4d8a5 100644 --- a/src/peripherals/pmic/Kconfig +++ b/src/peripherals/pmic/Kconfig @@ -6,4 +6,8 @@ if (ENABLE_PMIC) config ENABLE_PMIC_AXP2101 bool "Enable axp2101" default y + + config ENABLE_PMIC_M5PM1 + bool "Enable M5PM1" + default n endif \ No newline at end of file diff --git a/src/peripherals/pmic/m5pm1/include/m5pm1_driver.h b/src/peripherals/pmic/m5pm1/include/m5pm1_driver.h new file mode 100644 index 000000000..67a0378b5 --- /dev/null +++ b/src/peripherals/pmic/m5pm1/include/m5pm1_driver.h @@ -0,0 +1,424 @@ +/** + * @file m5pm1_driver.h + * @brief M5PM1 power management IC driver + * @version 0.1 + * @date 2026-04-27 + * @copyright Copyright (c) Tuya Inc. All Rights Reserved. + */ +#ifndef __M5PM1_DRIVER_H__ +#define __M5PM1_DRIVER_H__ + +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Macros + * --------------------------------------------------------------------------- */ +#define M5PM1_DEFAULT_ADDR (0x6E) + +/* --------------------------------------------------------------------------- + * Type definitions + * --------------------------------------------------------------------------- */ +typedef enum { + M5PM1_GPIO_NUM_0 = 0, + M5PM1_GPIO_NUM_1, + M5PM1_GPIO_NUM_2, + M5PM1_GPIO_NUM_3, + M5PM1_GPIO_NUM_4, + M5PM1_GPIO_NUM_MAX, +} M5PM1_GPIO_NUM_E; + +typedef enum { + M5PM1_GPIO_FUNC_GPIO = 0, + M5PM1_GPIO_FUNC_IRQ, + M5PM1_GPIO_FUNC_WAKE, + M5PM1_GPIO_FUNC_SPECIAL, +} M5PM1_GPIO_FUNC_E; + +typedef enum { + M5PM1_GPIO_MODE_INPUT = 0, + M5PM1_GPIO_MODE_OUTPUT, +} M5PM1_GPIO_MODE_E; + +typedef enum { + M5PM1_GPIO_DRIVE_PUSHPULL = 0, + M5PM1_GPIO_DRIVE_OPENDRAIN, +} M5PM1_GPIO_DRIVE_E; + +typedef enum { + M5PM1_GPIO_PULL_NONE = 0, + M5PM1_GPIO_PULL_UP, + M5PM1_GPIO_PULL_DOWN, +} M5PM1_GPIO_PULL_E; + +typedef enum { + M5PM1_GPIO_WAKE_FALLING = 0, + M5PM1_GPIO_WAKE_RISING, +} M5PM1_GPIO_WAKE_EDGE_E; + +typedef enum { + M5PM1_PWR_SRC_5VIN = 0, + M5PM1_PWR_SRC_5VINOUT, + M5PM1_PWR_SRC_BAT, + M5PM1_PWR_SRC_UNKNOWN, +} M5PM1_PWR_SRC_E; + +typedef enum { + M5PM1_WAKE_SRC_TIMER = (1U << 0), + M5PM1_WAKE_SRC_VIN = (1U << 1), + M5PM1_WAKE_SRC_PWRBTN = (1U << 2), + M5PM1_WAKE_SRC_RSTBTN = (1U << 3), + M5PM1_WAKE_SRC_CMD_RESET = (1U << 4), + M5PM1_WAKE_SRC_EXT_GPIO = (1U << 5), + M5PM1_WAKE_SRC_5VINOUT = (1U << 6), + M5PM1_WAKE_SRC_ALL = 0x7F, +} M5PM1_WAKE_SRC_E; + +typedef enum { + M5PM1_CLEAN_NONE = 0, + M5PM1_CLEAN_ONCE, + M5PM1_CLEAN_ALL, +} M5PM1_CLEAN_E; + +typedef enum { + M5PM1_TIM_ACTION_STOP = 0, + M5PM1_TIM_ACTION_FLAG, + M5PM1_TIM_ACTION_REBOOT, + M5PM1_TIM_ACTION_POWERON, + M5PM1_TIM_ACTION_POWEROFF, +} M5PM1_TIM_ACTION_E; + +typedef enum { + M5PM1_IRQ_MASK_DISABLE = 0, + M5PM1_IRQ_MASK_ENABLE, +} M5PM1_IRQ_MASK_E; + +typedef enum { + M5PM1_IRQ_DOMAIN_GPIO = 0, + M5PM1_IRQ_DOMAIN_SYS, + M5PM1_IRQ_DOMAIN_BTN, +} M5PM1_IRQ_DOMAIN_E; + +typedef struct { + TUYA_I2C_NUM_E i2c_port; + uint8_t i2c_addr; +} M5PM1_CFG_T; + +/* --------------------------------------------------------------------------- + * Function declarations + * --------------------------------------------------------------------------- */ +/** + * @brief Initialize M5PM1. + * @param[in] cfg M5PM1 bus configuration. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_init(const M5PM1_CFG_T *cfg); + +/** + * @brief Enable or disable M5PM1 battery charging. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_charge_enable(bool enable); + +/** + * @brief Enable or disable M5PM1 5V DCDC output. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_dcdc_enable(bool enable); + +/** + * @brief Enable or disable M5PM1 3.3V LDO output. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_ldo_enable(bool enable); + +/** + * @brief Enable or disable M5PM1 BOOST/Grove 5V output. + * @param[in] enable true to enable output mode, false to leave the port as input mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_boost_enable(bool enable); + +/** + * @brief Set M5PM1 LED_EN default level. + * @param[in] high true for high level, false for low level. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_led_en_level(bool high); + +/** + * @brief Get current M5PM1 power source. + * @param[out] src power source. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_power_source(M5PM1_PWR_SRC_E *src); + +/** + * @brief Read M5PM1 wake source flags. + * @param[out] src wake source bitmask. + * @param[in] clean read-and-clear behavior. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_wake_source(uint8_t *src, M5PM1_CLEAN_E clean); + +/** + * @brief Clear selected M5PM1 wake source flags. + * @param[in] mask wake source bits to clear. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_clear_wake_source(uint8_t mask); + +/** + * @brief Configure raw M5PM1 power configuration bits. + * @param[in] mask bit mask to update. + * @param[in] value masked value to write. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_power_config(uint8_t mask, uint8_t value); + +/** + * @brief Read raw M5PM1 power configuration bits. + * @param[out] config power configuration register value. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_power_config(uint8_t *config); + +/** + * @brief Clear selected M5PM1 power configuration bits. + * @param[in] mask bit mask to clear. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_clear_power_config(uint8_t mask); + +/** + * @brief Set M5PM1 battery low-voltage protection threshold. + * @param[in] mv threshold in millivolts, 2000 to 4000. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_battery_lvp(uint16_t mv); + +/** + * @brief Configure an M5PM1 GPIO function. + * @param[in] pin GPIO number. + * @param[in] func GPIO function. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_func(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_FUNC_E func); + +/** + * @brief Configure an M5PM1 GPIO direction. + * @param[in] pin GPIO number. + * @param[in] mode GPIO direction. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_mode(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_MODE_E mode); + +/** + * @brief Configure an M5PM1 GPIO drive mode. + * @param[in] pin GPIO number. + * @param[in] drive GPIO drive mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_drive(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_DRIVE_E drive); + +/** + * @brief Set an M5PM1 GPIO output level. + * @param[in] pin GPIO number. + * @param[in] high true for high level, false for low level. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_output(M5PM1_GPIO_NUM_E pin, bool high); + +/** + * @brief Get an M5PM1 GPIO input level. + * @param[in] pin GPIO number. + * @param[out] high true when input is high, false when input is low. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_get_input(M5PM1_GPIO_NUM_E pin, bool *high); + +/** + * @brief Configure an M5PM1 GPIO pull resistor. + * @param[in] pin GPIO number. + * @param[in] pull pull mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_pull(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_PULL_E pull); + +/** + * @brief Enable or disable an M5PM1 GPIO wake source. + * @param[in] pin GPIO number. + * @param[in] enable true to enable wake, false to disable wake. + * @return OPRT_OK on success, error code on failure. + * @note GPIO1 does not support wake. GPIO0/GPIO2 and GPIO3/GPIO4 are mutually exclusive wake pairs. + */ +OPERATE_RET m5pm1_gpio_set_wake_enable(M5PM1_GPIO_NUM_E pin, bool enable); + +/** + * @brief Configure M5PM1 GPIO wake edge. + * @param[in] pin GPIO number. + * @param[in] edge wake edge. + * @return OPRT_OK on success, error code on failure. + * @note GPIO1 does not support wake edge configuration. + */ +OPERATE_RET m5pm1_gpio_set_wake_edge(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_WAKE_EDGE_E edge); + +/** + * @brief Enable or disable M5PM1 GPIO power hold during sleep. + * @param[in] pin GPIO number. + * @param[in] enable true to hold, false to release hold. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_power_hold(M5PM1_GPIO_NUM_E pin, bool enable); + +/** + * @brief Read M5PM1 GPIO power hold state. + * @param[in] pin GPIO number. + * @param[out] enable true when hold is enabled. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_get_power_hold(M5PM1_GPIO_NUM_E pin, bool *enable); + +/** + * @brief Enable or disable M5PM1 LDO power hold during sleep. + * @param[in] enable true to hold, false to release hold. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_ldo_set_power_hold(bool enable); + +/** + * @brief Read M5PM1 LDO power hold state. + * @param[out] enable true when hold is enabled. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_ldo_get_power_hold(bool *enable); + +/** + * @brief Enable or disable M5PM1 BOOST power hold during sleep. + * @param[in] enable true to hold, false to release hold. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_boost_set_power_hold(bool enable); + +/** + * @brief Read M5PM1 BOOST power hold state. + * @param[out] enable true when hold is enabled. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_boost_get_power_hold(bool *enable); + +/** + * @brief Set M5PM1 watchdog timeout. + * @param[in] timeout_sec timeout in seconds, 0 disables watchdog. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_wdt_set(uint8_t timeout_sec); + +/** + * @brief Feed M5PM1 watchdog. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_wdt_feed(void); + +/** + * @brief Read M5PM1 watchdog countdown. + * @param[out] count countdown in seconds. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_wdt_get_count(uint8_t *count); + +/** + * @brief Configure M5PM1 timer. + * @param[in] seconds timer count in seconds. + * @param[in] action timer action. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_timer_set(uint32_t seconds, M5PM1_TIM_ACTION_E action); + +/** + * @brief Stop and clear M5PM1 timer. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_timer_clear(void); + +/** + * @brief Run an M5PM1 system command. + * @param[in] cmd command value. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_sys_cmd(uint8_t cmd); + +/** + * @brief Request M5PM1 shutdown. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_shutdown(void); + +/** + * @brief Request M5PM1 reboot. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_reboot(void); + +/** + * @brief Request M5PM1 download mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_enter_download_mode(void); + +/** + * @brief Read M5PM1 IRQ status. + * @param[in] domain IRQ domain. + * @param[out] status IRQ status bitmask. + * @param[in] clean read-and-clear behavior. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_get_status(M5PM1_IRQ_DOMAIN_E domain, uint8_t *status, M5PM1_CLEAN_E clean); + +/** + * @brief Clear all IRQ status bits in an M5PM1 IRQ domain. + * @param[in] domain IRQ domain. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_clear_all(M5PM1_IRQ_DOMAIN_E domain); + +/** + * @brief Set all IRQ mask bits in an M5PM1 IRQ domain. + * @param[in] domain IRQ domain. + * @param[in] mask mask control. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_set_mask_all(M5PM1_IRQ_DOMAIN_E domain, M5PM1_IRQ_MASK_E mask); + +/** + * @brief Read raw IRQ mask bits in an M5PM1 IRQ domain. + * @param[in] domain IRQ domain. + * @param[out] mask IRQ mask bits. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_get_mask_bits(M5PM1_IRQ_DOMAIN_E domain, uint8_t *mask); + +/** + * @brief Set M5PM1 I2C idle sleep timeout. + * @param[in] seconds timeout in seconds, 0 disables sleep, 1 to 15 enables sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_i2c_sleep_time(uint8_t seconds); + +/** + * @brief Read M5PM1 I2C idle sleep timeout. + * @param[out] seconds timeout in seconds. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_i2c_sleep_time(uint8_t *seconds); + +#ifdef __cplusplus +} +#endif +#endif /* __M5PM1_DRIVER_H__ */ diff --git a/src/peripherals/pmic/m5pm1/src/m5pm1_driver.c b/src/peripherals/pmic/m5pm1/src/m5pm1_driver.c new file mode 100644 index 000000000..1a2e41984 --- /dev/null +++ b/src/peripherals/pmic/m5pm1/src/m5pm1_driver.c @@ -0,0 +1,1157 @@ +/** + * @file m5pm1_driver.c + * @brief M5PM1 power management IC driver implementation + * @version 0.1 + * @date 2026-04-27 + * @copyright Copyright (c) Tuya Inc. All Rights Reserved. + */ +#include "m5pm1_driver.h" + +#include "tal_log.h" +#include "tal_system.h" +#include "tkl_i2c.h" +#include "tuya_error_code.h" + +/* --------------------------------------------------------------------------- + * Macros + * --------------------------------------------------------------------------- */ +#define M5PM1_REG_DEVICE_ID (0x00) +#define M5PM1_REG_PWR_SRC (0x04) +#define M5PM1_REG_WAKE_SRC (0x05) +#define M5PM1_REG_PWR_CFG (0x06) +#define M5PM1_REG_HOLD_CFG (0x07) +#define M5PM1_REG_BATT_LVP (0x08) +#define M5PM1_REG_I2C_CFG (0x09) +#define M5PM1_REG_WDT_CNT (0x0A) +#define M5PM1_REG_WDT_KEY (0x0B) +#define M5PM1_REG_SYS_CMD (0x0C) +#define M5PM1_REG_GPIO_MODE (0x10) +#define M5PM1_REG_GPIO_OUT (0x11) +#define M5PM1_REG_GPIO_IN (0x12) +#define M5PM1_REG_GPIO_DRV (0x13) +#define M5PM1_REG_GPIO_PUPD0 (0x14) +#define M5PM1_REG_GPIO_PUPD1 (0x15) +#define M5PM1_REG_GPIO_FUNC0 (0x16) +#define M5PM1_REG_GPIO_FUNC1 (0x17) +#define M5PM1_REG_GPIO_WAKE_EN (0x18) +#define M5PM1_REG_GPIO_WAKE_CFG (0x19) +#define M5PM1_REG_TIM_CNT_0 (0x38) +#define M5PM1_REG_TIM_CFG (0x3C) +#define M5PM1_REG_TIM_KEY (0x3D) +#define M5PM1_REG_IRQ_STATUS1 (0x40) +#define M5PM1_REG_IRQ_STATUS2 (0x41) +#define M5PM1_REG_IRQ_STATUS3 (0x42) +#define M5PM1_REG_IRQ_MASK1 (0x43) +#define M5PM1_REG_IRQ_MASK2 (0x44) +#define M5PM1_REG_IRQ_MASK3 (0x45) + +#define M5PM1_PWR_CFG_CHG_EN (1U << 0) +#define M5PM1_PWR_CFG_DCDC_EN (1U << 1) +#define M5PM1_PWR_CFG_LDO_EN (1U << 2) +#define M5PM1_PWR_CFG_BOOST_EN (1U << 3) +#define M5PM1_PWR_CFG_LED_EN (1U << 4) + +#define M5PM1_HOLD_CFG_LDO (1U << 5) +#define M5PM1_HOLD_CFG_BOOST (1U << 6) + +#define M5PM1_I2C_SLEEP_MASK (0x0F) +#define M5PM1_WDT_FEED_KEY (0xA5) +#define M5PM1_SYS_CMD_KEY (0xA0) +#define M5PM1_SYS_CMD_OFF (0x01) +#define M5PM1_SYS_CMD_RESET (0x02) +#define M5PM1_SYS_CMD_DL (0x03) +#define M5PM1_TIM_RELOAD_KEY (0xA5) +#define M5PM1_TIM_ARM (1U << 3) +#define M5PM1_TIM_MAX_SECONDS (214748364UL) + +#define M5PM1_IRQ_GPIO_VALID_MASK (0x1F) +#define M5PM1_IRQ_SYS_VALID_MASK (0x3F) +#define M5PM1_IRQ_BTN_VALID_MASK (0x07) +#define M5PM1_SYS_CMD_DELAY_MS (120) + +/* --------------------------------------------------------------------------- + * Type definitions + * --------------------------------------------------------------------------- */ +typedef struct { + M5PM1_CFG_T cfg; + bool initialized; +} M5PM1_DEV_T; + +/* --------------------------------------------------------------------------- + * File-scope variables + * --------------------------------------------------------------------------- */ +static M5PM1_DEV_T s_m5pm1 = { + .cfg = { + .i2c_port = TUYA_I2C_NUM_0, + .i2c_addr = M5PM1_DEFAULT_ADDR, + }, + .initialized = false, +}; + +/* --------------------------------------------------------------------------- + * Function implementations + * --------------------------------------------------------------------------- */ +/** + * @brief Read one M5PM1 register. + * @param[in] reg register address. + * @param[out] data register value. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_read_reg(uint8_t reg, uint8_t *data) +{ + OPERATE_RET rt = OPRT_OK; + + if (data == NULL) { + return OPRT_INVALID_PARM; + } + + rt = tkl_i2c_master_send(s_m5pm1.cfg.i2c_port, s_m5pm1.cfg.i2c_addr, ®, 1, FALSE); + if (rt != OPRT_OK) { + return rt; + } + + rt = tkl_i2c_master_receive(s_m5pm1.cfg.i2c_port, s_m5pm1.cfg.i2c_addr, data, 1, FALSE); + if (rt != OPRT_OK) { + return rt; + } + + return OPRT_OK; +} + +/** + * @brief Write one M5PM1 register. + * @param[in] reg register address. + * @param[in] data register value. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_write_reg(uint8_t reg, uint8_t data) +{ + uint8_t buf[2] = {reg, data}; + + return tkl_i2c_master_send(s_m5pm1.cfg.i2c_port, s_m5pm1.cfg.i2c_addr, buf, sizeof(buf), FALSE); +} + +/** + * @brief Write a contiguous M5PM1 register block. + * @param[in] reg first register address. + * @param[in] data register data. + * @param[in] len register data length. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_write_bytes(uint8_t reg, const uint8_t *data, uint8_t len) +{ + uint8_t buf[5] = {0}; + uint8_t i = 0; + + if ((data == NULL) || (len == 0) || (len > (sizeof(buf) - 1U))) { + return OPRT_INVALID_PARM; + } + + buf[0] = reg; + for (i = 0; i < len; i++) { + buf[i + 1U] = data[i]; + } + + return tkl_i2c_master_send(s_m5pm1.cfg.i2c_port, s_m5pm1.cfg.i2c_addr, buf, (uint16_t)(len + 1U), FALSE); +} + +/** + * @brief Check whether M5PM1 is initialized. + * @return OPRT_OK when initialized, OPRT_COM_ERROR otherwise. + */ +static OPERATE_RET __m5pm1_check_init(void) +{ + if (!s_m5pm1.initialized) { + return OPRT_COM_ERROR; + } + + return OPRT_OK; +} + +/** + * @brief Update selected bits in one M5PM1 register. + * @param[in] reg register address. + * @param[in] mask bit mask to update. + * @param[in] value masked value to write. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_update_bits(uint8_t reg, uint8_t mask, uint8_t value) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(reg, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + reg_val &= (uint8_t)~mask; + reg_val |= (uint8_t)(value & mask); + + return __m5pm1_write_reg(reg, reg_val); +} + +/** + * @brief Validate an M5PM1 GPIO number. + * @param[in] pin GPIO number. + * @return true when valid, false otherwise. + */ +static bool __m5pm1_is_valid_gpio(M5PM1_GPIO_NUM_E pin) +{ + return ((pin >= M5PM1_GPIO_NUM_0) && (pin < M5PM1_GPIO_NUM_MAX)); +} + +/** + * @brief Check whether a GPIO wake source is enabled. + * @param[in] pin GPIO number. + * @param[out] enabled true when wake is enabled. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_gpio_get_wake_enabled(M5PM1_GPIO_NUM_E pin, bool *enabled) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (!__m5pm1_is_valid_gpio(pin) || (enabled == NULL)) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_read_reg(M5PM1_REG_GPIO_WAKE_EN, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *enabled = ((reg_val & (uint8_t)(1U << pin)) != 0); + return OPRT_OK; +} + +/** + * @brief Convert clean behavior into write-0-to-clear register write. + * @param[in] reg status register address. + * @param[in] status current status value. + * @param[in] clean clean behavior. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_clean_status(uint8_t reg, uint8_t status, M5PM1_CLEAN_E clean) +{ + if (clean == M5PM1_CLEAN_NONE) { + return OPRT_OK; + } + + if (clean == M5PM1_CLEAN_ONCE) { + if (status == 0) { + return OPRT_OK; + } + + return __m5pm1_write_reg(reg, (uint8_t)~status); + } + + if (clean == M5PM1_CLEAN_ALL) { + return __m5pm1_write_reg(reg, 0x00); + } + + return OPRT_INVALID_PARM; +} + +/** + * @brief Get M5PM1 IRQ status register for a domain. + * @param[in] domain IRQ domain. + * @param[out] status_reg status register. + * @param[out] mask_reg mask register. + * @param[out] valid_mask valid bits mask. + * @return OPRT_OK on success, error code on failure. + */ +static OPERATE_RET __m5pm1_get_irq_regs(M5PM1_IRQ_DOMAIN_E domain, uint8_t *status_reg, uint8_t *mask_reg, + uint8_t *valid_mask) +{ + if ((status_reg == NULL) || (mask_reg == NULL) || (valid_mask == NULL)) { + return OPRT_INVALID_PARM; + } + + switch (domain) { + case M5PM1_IRQ_DOMAIN_GPIO: + *status_reg = M5PM1_REG_IRQ_STATUS1; + *mask_reg = M5PM1_REG_IRQ_MASK1; + *valid_mask = M5PM1_IRQ_GPIO_VALID_MASK; + break; + case M5PM1_IRQ_DOMAIN_SYS: + *status_reg = M5PM1_REG_IRQ_STATUS2; + *mask_reg = M5PM1_REG_IRQ_MASK2; + *valid_mask = M5PM1_IRQ_SYS_VALID_MASK; + break; + case M5PM1_IRQ_DOMAIN_BTN: + *status_reg = M5PM1_REG_IRQ_STATUS3; + *mask_reg = M5PM1_REG_IRQ_MASK3; + *valid_mask = M5PM1_IRQ_BTN_VALID_MASK; + break; + default: + return OPRT_INVALID_PARM; + } + + return OPRT_OK; +} + +/** + * @brief Initialize M5PM1. + * @param[in] cfg M5PM1 bus configuration. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_init(const M5PM1_CFG_T *cfg) +{ + OPERATE_RET rt = OPRT_OK; + TUYA_IIC_BASE_CFG_T i2c_cfg = {0}; + uint8_t device_id = 0; + + if (cfg == NULL) { + return OPRT_INVALID_PARM; + } + + s_m5pm1.cfg = *cfg; + + i2c_cfg.role = TUYA_IIC_MODE_MASTER; + i2c_cfg.speed = TUYA_IIC_BUS_SPEED_100K; + i2c_cfg.addr_width = TUYA_IIC_ADDRESS_7BIT; + rt = tkl_i2c_init(s_m5pm1.cfg.i2c_port, &i2c_cfg); + if (rt != OPRT_OK) { + PR_ERR("m5pm1 i2c init failed:%d", rt); + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_DEVICE_ID, &device_id); + if (rt != OPRT_OK) { + PR_ERR("m5pm1 read device id failed:%d", rt); + return rt; + } + + s_m5pm1.initialized = true; + PR_INFO("m5pm1 initialized, device id:0x%02x", device_id); + + return OPRT_OK; +} + +/** + * @brief Enable or disable M5PM1 battery charging. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_charge_enable(bool enable) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_CHG_EN, + enable ? M5PM1_PWR_CFG_CHG_EN : 0); +} + +/** + * @brief Enable or disable M5PM1 5V DCDC output. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_dcdc_enable(bool enable) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_DCDC_EN, + enable ? M5PM1_PWR_CFG_DCDC_EN : 0); +} + +/** + * @brief Enable or disable M5PM1 3.3V LDO output. + * @param[in] enable true to enable, false to disable. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_ldo_enable(bool enable) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_LDO_EN, + enable ? M5PM1_PWR_CFG_LDO_EN : 0); +} + +/** + * @brief Enable or disable M5PM1 BOOST/Grove 5V output. + * @param[in] enable true to enable output mode, false to leave the port as input mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_boost_enable(bool enable) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_BOOST_EN, + enable ? M5PM1_PWR_CFG_BOOST_EN : 0); +} + +/** + * @brief Set M5PM1 LED_EN default level. + * @param[in] high true for high level, false for low level. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_led_en_level(bool high) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_LED_EN, high ? M5PM1_PWR_CFG_LED_EN : 0); +} + +/** + * @brief Get current M5PM1 power source. + * @param[out] src power source. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_power_source(M5PM1_PWR_SRC_E *src) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (src == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_PWR_SRC, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *src = (M5PM1_PWR_SRC_E)(reg_val & 0x07U); + return OPRT_OK; +} + +/** + * @brief Read M5PM1 wake source flags. + * @param[out] src wake source bitmask. + * @param[in] clean read-and-clear behavior. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_wake_source(uint8_t *src, M5PM1_CLEAN_E clean) +{ + OPERATE_RET rt = OPRT_OK; + + if (src == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_WAKE_SRC, src); + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_clean_status(M5PM1_REG_WAKE_SRC, *src, clean); +} + +/** + * @brief Clear selected M5PM1 wake source flags. + * @param[in] mask wake source bits to clear. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_clear_wake_source(uint8_t mask) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_WAKE_SRC, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_write_reg(M5PM1_REG_WAKE_SRC, (uint8_t)(reg_val & (uint8_t)~mask)); +} + +/** + * @brief Configure raw M5PM1 power configuration bits. + * @param[in] mask bit mask to update. + * @param[in] value masked value to write. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_power_config(uint8_t mask, uint8_t value) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, mask, value); +} + +/** + * @brief Read raw M5PM1 power configuration bits. + * @param[out] config power configuration register value. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_power_config(uint8_t *config) +{ + OPERATE_RET rt = OPRT_OK; + + if (config == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_read_reg(M5PM1_REG_PWR_CFG, config); +} + +/** + * @brief Clear selected M5PM1 power configuration bits. + * @param[in] mask bit mask to clear. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_clear_power_config(uint8_t mask) +{ + return __m5pm1_update_bits(M5PM1_REG_PWR_CFG, mask, 0); +} + +/** + * @brief Set M5PM1 battery low-voltage protection threshold. + * @param[in] mv threshold in millivolts, 2000 to 4000. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_battery_lvp(uint16_t mv) +{ + uint32_t value = 0; + + if ((mv < 2000U) || (mv > 4000U)) { + return OPRT_INVALID_PARM; + } + + if (__m5pm1_check_init() != OPRT_OK) { + return OPRT_COM_ERROR; + } + + value = ((uint32_t)(mv - 2000U) * 100U) / 781U; + return __m5pm1_write_reg(M5PM1_REG_BATT_LVP, (uint8_t)value); +} + +/** + * @brief Configure an M5PM1 GPIO function. + * @param[in] pin GPIO number. + * @param[in] func GPIO function. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_func(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_FUNC_E func) +{ + uint8_t reg = 0; + uint8_t shift = 0; + + if (!__m5pm1_is_valid_gpio(pin) || (func > M5PM1_GPIO_FUNC_SPECIAL)) { + return OPRT_INVALID_PARM; + } + + if (pin < M5PM1_GPIO_NUM_4) { + reg = M5PM1_REG_GPIO_FUNC0; + shift = (uint8_t)pin * 2; + } else { + reg = M5PM1_REG_GPIO_FUNC1; + shift = 0; + } + + return __m5pm1_update_bits(reg, (uint8_t)(0x03U << shift), (uint8_t)((uint8_t)func << shift)); +} + +/** + * @brief Configure an M5PM1 GPIO direction. + * @param[in] pin GPIO number. + * @param[in] mode GPIO direction. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_mode(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_MODE_E mode) +{ + if (!__m5pm1_is_valid_gpio(pin) || (mode > M5PM1_GPIO_MODE_OUTPUT)) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_update_bits(M5PM1_REG_GPIO_MODE, (uint8_t)(1U << pin), + (mode == M5PM1_GPIO_MODE_OUTPUT) ? (uint8_t)(1U << pin) : 0); +} + +/** + * @brief Configure an M5PM1 GPIO drive mode. + * @param[in] pin GPIO number. + * @param[in] drive GPIO drive mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_drive(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_DRIVE_E drive) +{ + if (!__m5pm1_is_valid_gpio(pin) || (drive > M5PM1_GPIO_DRIVE_OPENDRAIN)) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_update_bits(M5PM1_REG_GPIO_DRV, (uint8_t)(1U << pin), + (drive == M5PM1_GPIO_DRIVE_OPENDRAIN) ? (uint8_t)(1U << pin) : 0); +} + +/** + * @brief Set an M5PM1 GPIO output level. + * @param[in] pin GPIO number. + * @param[in] high true for high level, false for low level. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_output(M5PM1_GPIO_NUM_E pin, bool high) +{ + if (!__m5pm1_is_valid_gpio(pin)) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_update_bits(M5PM1_REG_GPIO_OUT, (uint8_t)(1U << pin), high ? (uint8_t)(1U << pin) : 0); +} + +/** + * @brief Get an M5PM1 GPIO input level. + * @param[in] pin GPIO number. + * @param[out] high true when input is high, false when input is low. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_get_input(M5PM1_GPIO_NUM_E pin, bool *high) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (!__m5pm1_is_valid_gpio(pin) || (high == NULL)) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_GPIO_IN, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *high = ((reg_val & (uint8_t)(1U << pin)) != 0); + return OPRT_OK; +} + +/** + * @brief Configure an M5PM1 GPIO pull resistor. + * @param[in] pin GPIO number. + * @param[in] pull pull mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_pull(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_PULL_E pull) +{ + uint8_t reg = 0; + uint8_t shift = 0; + + if (!__m5pm1_is_valid_gpio(pin) || (pull > M5PM1_GPIO_PULL_DOWN)) { + return OPRT_INVALID_PARM; + } + + if (pin < M5PM1_GPIO_NUM_4) { + reg = M5PM1_REG_GPIO_PUPD0; + shift = (uint8_t)pin * 2U; + } else { + reg = M5PM1_REG_GPIO_PUPD1; + shift = 0; + } + + return __m5pm1_update_bits(reg, (uint8_t)(0x03U << shift), (uint8_t)((uint8_t)pull << shift)); +} + +/** + * @brief Enable or disable an M5PM1 GPIO wake source. + * @param[in] pin GPIO number. + * @param[in] enable true to enable wake, false to disable wake. + * @return OPRT_OK on success, error code on failure. + * @note GPIO1 does not support wake. GPIO0/GPIO2 and GPIO3/GPIO4 are mutually exclusive wake pairs. + */ +OPERATE_RET m5pm1_gpio_set_wake_enable(M5PM1_GPIO_NUM_E pin, bool enable) +{ + OPERATE_RET rt = OPRT_OK; + bool pair_enabled = false; + + if (!__m5pm1_is_valid_gpio(pin)) { + return OPRT_INVALID_PARM; + } + + if (!enable) { + return __m5pm1_update_bits(M5PM1_REG_GPIO_WAKE_EN, (uint8_t)(1U << pin), 0); + } + + if (pin == M5PM1_GPIO_NUM_1) { + return OPRT_INVALID_PARM; + } + + if (pin == M5PM1_GPIO_NUM_0) { + rt = __m5pm1_gpio_get_wake_enabled(M5PM1_GPIO_NUM_2, &pair_enabled); + } else if (pin == M5PM1_GPIO_NUM_2) { + rt = __m5pm1_gpio_get_wake_enabled(M5PM1_GPIO_NUM_0, &pair_enabled); + } else if (pin == M5PM1_GPIO_NUM_3) { + rt = __m5pm1_gpio_get_wake_enabled(M5PM1_GPIO_NUM_4, &pair_enabled); + } else { + rt = __m5pm1_gpio_get_wake_enabled(M5PM1_GPIO_NUM_3, &pair_enabled); + } + if (rt != OPRT_OK) { + return rt; + } + if (pair_enabled) { + PR_ERR("m5pm1 wake source conflict on GPIO%d", pin); + return OPRT_COM_ERROR; + } + + return __m5pm1_update_bits(M5PM1_REG_GPIO_WAKE_EN, (uint8_t)(1U << pin), (uint8_t)(1U << pin)); +} + +/** + * @brief Configure M5PM1 GPIO wake edge. + * @param[in] pin GPIO number. + * @param[in] edge wake edge. + * @return OPRT_OK on success, error code on failure. + * @note GPIO1 does not support wake edge configuration. + */ +OPERATE_RET m5pm1_gpio_set_wake_edge(M5PM1_GPIO_NUM_E pin, M5PM1_GPIO_WAKE_EDGE_E edge) +{ + if (!__m5pm1_is_valid_gpio(pin) || (edge > M5PM1_GPIO_WAKE_RISING) || (pin == M5PM1_GPIO_NUM_1)) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_update_bits(M5PM1_REG_GPIO_WAKE_CFG, (uint8_t)(1U << pin), + (edge == M5PM1_GPIO_WAKE_RISING) ? (uint8_t)(1U << pin) : 0); +} + +/** + * @brief Enable or disable M5PM1 GPIO power hold during sleep. + * @param[in] pin GPIO number. + * @param[in] enable true to hold, false to release hold. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_set_power_hold(M5PM1_GPIO_NUM_E pin, bool enable) +{ + if (!__m5pm1_is_valid_gpio(pin)) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_update_bits(M5PM1_REG_HOLD_CFG, (uint8_t)(1U << pin), enable ? (uint8_t)(1U << pin) : 0); +} + +/** + * @brief Read M5PM1 GPIO power hold state. + * @param[in] pin GPIO number. + * @param[out] enable true when hold is enabled. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_gpio_get_power_hold(M5PM1_GPIO_NUM_E pin, bool *enable) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (!__m5pm1_is_valid_gpio(pin) || (enable == NULL)) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_HOLD_CFG, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *enable = ((reg_val & (uint8_t)(1U << pin)) != 0); + return OPRT_OK; +} + +/** + * @brief Enable or disable M5PM1 LDO power hold during sleep. + * @param[in] enable true to hold, false to release hold. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_ldo_set_power_hold(bool enable) +{ + return __m5pm1_update_bits(M5PM1_REG_HOLD_CFG, M5PM1_HOLD_CFG_LDO, enable ? M5PM1_HOLD_CFG_LDO : 0); +} + +/** + * @brief Read M5PM1 LDO power hold state. + * @param[out] enable true when hold is enabled. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_ldo_get_power_hold(bool *enable) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (enable == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_HOLD_CFG, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *enable = ((reg_val & M5PM1_HOLD_CFG_LDO) != 0); + return OPRT_OK; +} + +/** + * @brief Enable or disable M5PM1 BOOST power hold during sleep. + * @param[in] enable true to hold, false to release hold. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_boost_set_power_hold(bool enable) +{ + return __m5pm1_update_bits(M5PM1_REG_HOLD_CFG, M5PM1_HOLD_CFG_BOOST, enable ? M5PM1_HOLD_CFG_BOOST : 0); +} + +/** + * @brief Read M5PM1 BOOST power hold state. + * @param[out] enable true when hold is enabled. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_boost_get_power_hold(bool *enable) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (enable == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_HOLD_CFG, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *enable = ((reg_val & M5PM1_HOLD_CFG_BOOST) != 0); + return OPRT_OK; +} + +/** + * @brief Set M5PM1 watchdog timeout. + * @param[in] timeout_sec timeout in seconds, 0 disables watchdog. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_wdt_set(uint8_t timeout_sec) +{ + OPERATE_RET rt = __m5pm1_check_init(); + + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_write_reg(M5PM1_REG_WDT_CNT, timeout_sec); +} + +/** + * @brief Feed M5PM1 watchdog. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_wdt_feed(void) +{ + OPERATE_RET rt = __m5pm1_check_init(); + + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_write_reg(M5PM1_REG_WDT_KEY, M5PM1_WDT_FEED_KEY); +} + +/** + * @brief Read M5PM1 watchdog countdown. + * @param[out] count countdown in seconds. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_wdt_get_count(uint8_t *count) +{ + OPERATE_RET rt = OPRT_OK; + + if (count == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_read_reg(M5PM1_REG_WDT_CNT, count); +} + +/** + * @brief Configure M5PM1 timer. + * @param[in] seconds timer count in seconds. + * @param[in] action timer action. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_timer_set(uint32_t seconds, M5PM1_TIM_ACTION_E action) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t data[4] = {0}; + + if ((seconds > M5PM1_TIM_MAX_SECONDS) || (action > M5PM1_TIM_ACTION_POWEROFF)) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + data[0] = (uint8_t)(seconds & 0xFFU); + data[1] = (uint8_t)((seconds >> 8) & 0xFFU); + data[2] = (uint8_t)((seconds >> 16) & 0xFFU); + data[3] = (uint8_t)((seconds >> 24) & 0x7FU); + + rt = __m5pm1_write_bytes(M5PM1_REG_TIM_CNT_0, data, sizeof(data)); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_write_reg(M5PM1_REG_TIM_CFG, (uint8_t)(M5PM1_TIM_ARM | (uint8_t)action)); + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_write_reg(M5PM1_REG_TIM_KEY, M5PM1_TIM_RELOAD_KEY); +} + +/** + * @brief Stop and clear M5PM1 timer. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_timer_clear(void) +{ + OPERATE_RET rt = __m5pm1_check_init(); + + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_write_reg(M5PM1_REG_TIM_CFG, 0); + if (rt != OPRT_OK) { + return rt; + } + + return __m5pm1_write_reg(M5PM1_REG_TIM_KEY, M5PM1_TIM_RELOAD_KEY); +} + +/** + * @brief Run an M5PM1 system command. + * @param[in] cmd command value. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_sys_cmd(uint8_t cmd) +{ + OPERATE_RET rt = __m5pm1_check_init(); + + if (rt != OPRT_OK) { + return rt; + } + if (cmd > M5PM1_SYS_CMD_DL) { + return OPRT_INVALID_PARM; + } + + tal_system_sleep(M5PM1_SYS_CMD_DELAY_MS); + return __m5pm1_write_reg(M5PM1_REG_SYS_CMD, (uint8_t)(M5PM1_SYS_CMD_KEY | cmd)); +} + +/** + * @brief Request M5PM1 shutdown. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_shutdown(void) +{ + return m5pm1_sys_cmd(M5PM1_SYS_CMD_OFF); +} + +/** + * @brief Request M5PM1 reboot. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_reboot(void) +{ + return m5pm1_sys_cmd(M5PM1_SYS_CMD_RESET); +} + +/** + * @brief Request M5PM1 download mode. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_enter_download_mode(void) +{ + return m5pm1_sys_cmd(M5PM1_SYS_CMD_DL); +} + +/** + * @brief Read M5PM1 IRQ status. + * @param[in] domain IRQ domain. + * @param[out] status IRQ status bitmask. + * @param[in] clean read-and-clear behavior. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_get_status(M5PM1_IRQ_DOMAIN_E domain, uint8_t *status, M5PM1_CLEAN_E clean) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t status_reg = 0; + uint8_t mask_reg = 0; + uint8_t valid_mask = 0; + + if (status == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_get_irq_regs(domain, &status_reg, &mask_reg, &valid_mask); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(status_reg, status); + if (rt != OPRT_OK) { + return rt; + } + *status &= valid_mask; + + return __m5pm1_clean_status(status_reg, *status, clean); +} + +/** + * @brief Clear all IRQ status bits in an M5PM1 IRQ domain. + * @param[in] domain IRQ domain. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_clear_all(M5PM1_IRQ_DOMAIN_E domain) +{ + uint8_t status_reg = 0; + uint8_t mask_reg = 0; + uint8_t valid_mask = 0; + + if (__m5pm1_check_init() != OPRT_OK) { + return OPRT_COM_ERROR; + } + if (__m5pm1_get_irq_regs(domain, &status_reg, &mask_reg, &valid_mask) != OPRT_OK) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_write_reg(status_reg, 0x00); +} + +/** + * @brief Set all IRQ mask bits in an M5PM1 IRQ domain. + * @param[in] domain IRQ domain. + * @param[in] mask mask control. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_set_mask_all(M5PM1_IRQ_DOMAIN_E domain, M5PM1_IRQ_MASK_E mask) +{ + uint8_t status_reg = 0; + uint8_t mask_reg = 0; + uint8_t valid_mask = 0; + + if (mask > M5PM1_IRQ_MASK_ENABLE) { + return OPRT_INVALID_PARM; + } + if (__m5pm1_check_init() != OPRT_OK) { + return OPRT_COM_ERROR; + } + if (__m5pm1_get_irq_regs(domain, &status_reg, &mask_reg, &valid_mask) != OPRT_OK) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_write_reg(mask_reg, (mask == M5PM1_IRQ_MASK_ENABLE) ? valid_mask : 0); +} + +/** + * @brief Read raw IRQ mask bits in an M5PM1 IRQ domain. + * @param[in] domain IRQ domain. + * @param[out] mask IRQ mask bits. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_irq_get_mask_bits(M5PM1_IRQ_DOMAIN_E domain, uint8_t *mask) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t status_reg = 0; + uint8_t mask_reg = 0; + uint8_t valid_mask = 0; + + if (mask == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_get_irq_regs(domain, &status_reg, &mask_reg, &valid_mask); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(mask_reg, mask); + if (rt != OPRT_OK) { + return rt; + } + + *mask &= valid_mask; + return OPRT_OK; +} + +/** + * @brief Set M5PM1 I2C idle sleep timeout. + * @param[in] seconds timeout in seconds, 0 disables sleep, 1 to 15 enables sleep. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_set_i2c_sleep_time(uint8_t seconds) +{ + if (seconds > M5PM1_I2C_SLEEP_MASK) { + return OPRT_INVALID_PARM; + } + + return __m5pm1_update_bits(M5PM1_REG_I2C_CFG, M5PM1_I2C_SLEEP_MASK, seconds); +} + +/** + * @brief Read M5PM1 I2C idle sleep timeout. + * @param[out] seconds timeout in seconds. + * @return OPRT_OK on success, error code on failure. + */ +OPERATE_RET m5pm1_get_i2c_sleep_time(uint8_t *seconds) +{ + OPERATE_RET rt = OPRT_OK; + uint8_t reg_val = 0; + + if (seconds == NULL) { + return OPRT_INVALID_PARM; + } + + rt = __m5pm1_check_init(); + if (rt != OPRT_OK) { + return rt; + } + + rt = __m5pm1_read_reg(M5PM1_REG_I2C_CFG, ®_val); + if (rt != OPRT_OK) { + return rt; + } + + *seconds = reg_val & M5PM1_I2C_SLEEP_MASK; + return OPRT_OK; +}