From 106fe3b5b03c5e1135e5f1ed3c1d58aa96185766 Mon Sep 17 00:00:00 2001 From: Bo Date: Mon, 9 Feb 2026 20:16:00 +0800 Subject: [PATCH] feat: add mimiclaw onboarding page. Signed-off-by: Bo --- .gitignore | 2 + assets/banner_172x320.rgb565 | Bin 0 -> 110080 bytes assets/banner_320x172.rgb565 | Bin 0 -> 110080 bytes main/CMakeLists.txt | 8 + main/buttons/button_driver.c | 70 +++++ main/buttons/button_driver.h | 16 + main/buttons/multi_button.c | 208 +++++++++++++ main/buttons/multi_button.h | 62 ++++ main/display/Vernon_ST7789T/Vernon_ST7789T.c | 307 +++++++++++++++++++ main/display/Vernon_ST7789T/Vernon_ST7789T.h | 47 +++ main/display/display.c | 199 ++++++++++++ main/display/display.h | 20 ++ main/idf_component.yml | 17 + main/mimi.c | 10 + main/rgb/rgb.c | 39 +++ main/rgb/rgb.h | 15 + main/tools/tool_files.c | 1 + 17 files changed, 1021 insertions(+) create mode 100644 assets/banner_172x320.rgb565 create mode 100644 assets/banner_320x172.rgb565 create mode 100644 main/buttons/button_driver.c create mode 100644 main/buttons/button_driver.h create mode 100644 main/buttons/multi_button.c create mode 100644 main/buttons/multi_button.h create mode 100644 main/display/Vernon_ST7789T/Vernon_ST7789T.c create mode 100644 main/display/Vernon_ST7789T/Vernon_ST7789T.h create mode 100644 main/display/display.c create mode 100644 main/display/display.h create mode 100644 main/idf_component.yml create mode 100644 main/rgb/rgb.c create mode 100644 main/rgb/rgb.h diff --git a/.gitignore b/.gitignore index cdeda01..c312ba3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ nanobot/ # OS .DS_Store Thumbs.db + +references/ \ No newline at end of file diff --git a/assets/banner_172x320.rgb565 b/assets/banner_172x320.rgb565 new file mode 100644 index 0000000000000000000000000000000000000000..c58e269c5499c176ab7d7617ce1c1c55a5ea17cc GIT binary patch literal 110080 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 KifI933;YX?epIUf literal 0 HcmV?d00001 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 490a733..60d6bf1 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,6 +1,11 @@ idf_component_register( SRCS "mimi.c" + "display/display.c" + "display/Vernon_ST7789T/Vernon_ST7789T.c" + "rgb/rgb.c" + "buttons/multi_button.c" + "buttons/button_driver.c" "bus/message_bus.c" "wifi/wifi_manager.c" "telegram/telegram_bot.c" @@ -19,7 +24,10 @@ idf_component_register( "tools/tool_files.c" INCLUDE_DIRS "." + EMBED_FILES + "../assets/banner_320x172.rgb565" REQUIRES nvs_flash esp_wifi esp_netif esp_http_client esp_http_server esp_https_ota esp_event json spiffs console vfs app_update esp-tls + driver esp_lcd esp_timer led_strip ) diff --git a/main/buttons/button_driver.c b/main/buttons/button_driver.c new file mode 100644 index 0000000..e50df3c --- /dev/null +++ b/main/buttons/button_driver.c @@ -0,0 +1,70 @@ +#include "buttons/button_driver.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "driver/gpio.h" +#include "display/display.h" + +void ESP32_Button_init(void){ + gpio_reset_pin(Button_PIN1); + gpio_set_direction(Button_PIN1, GPIO_MODE_INPUT); + gpio_set_pull_mode(Button_PIN1, GPIO_PULLUP_ONLY); +} +uint8_t Button_GPIO_Get_Level(int GPIO_PIN){ + return (uint8_t)(gpio_get_level(GPIO_PIN)); +} +void Timer_Callback(void *arg){ + button_ticks(); +} + + + +struct Button BUTTON1; +PressEvent BOOT_KEY_State,PWR_KEY_State; +uint8_t Read_Button_GPIO_Level(uint8_t button_id) +{ + if(!button_id) + return (uint8_t)(gpio_get_level(Button_PIN1)); + return 0; +} +void Button_SINGLE_CLICK_Callback(void* btn){ + struct Button *user_button = (struct Button *)btn; + if(user_button == &BUTTON1){ + BOOT_KEY_State = SINGLE_CLICK; + display_cycle_backlight(); + } +} +void Button_DOUBLE_CLICK_Callback(void* btn){ + struct Button *user_button = (struct Button *)btn; + if(user_button == &BUTTON1){ + BOOT_KEY_State = DOUBLE_CLICK; + } +} +void Button_LONG_PRESS_START_Callback(void* btn){ + struct Button *user_button = (struct Button *)btn; + if(user_button == &BUTTON1){ + BOOT_KEY_State= LONG_PRESS_START; + } +} +void button_Init(void) +{ + ESP32_Button_init(); + button_init(&BUTTON1, Read_Button_GPIO_Level, 0 , 0); + button_attach(&BUTTON1, SINGLE_CLICK, Button_SINGLE_CLICK_Callback); + button_attach(&BUTTON1, DOUBLE_CLICK, Button_DOUBLE_CLICK_Callback); + button_attach(&BUTTON1, LONG_PRESS_START, Button_LONG_PRESS_START_Callback); + + const esp_timer_create_args_t clock_tick_timer_args = + { + .callback = &Timer_Callback, + .name = "Timer_task", + .arg = NULL, + }; + esp_timer_handle_t clock_tick_timer = NULL; + ESP_ERROR_CHECK(esp_timer_create(&clock_tick_timer_args, &clock_tick_timer)); + ESP_ERROR_CHECK(esp_timer_start_periodic(clock_tick_timer, 1000 * 5)); + + BOOT_KEY_State = NONE_PRESS; + button_start(&BUTTON1); +} + diff --git a/main/buttons/button_driver.h b/main/buttons/button_driver.h new file mode 100644 index 0000000..17d896e --- /dev/null +++ b/main/buttons/button_driver.h @@ -0,0 +1,16 @@ +#ifndef BUTTON_BSP_H +#define BUTTON_BSP_H +#include +#include +#include "buttons/multi_button.h" + + +#define BOOT_KEY_PIN 0 + +#define Button_PIN1 BOOT_KEY_PIN + +extern PressEvent BOOT_KEY_State; + +void button_Init(void); + +#endif diff --git a/main/buttons/multi_button.c b/main/buttons/multi_button.c new file mode 100644 index 0000000..66d1020 --- /dev/null +++ b/main/buttons/multi_button.c @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2016 Zibin Zheng + * All rights reserved + */ + +#include "multi_button.h" + +#define EVENT_CB(ev) if(handle->cb[ev])handle->cb[ev]((void*)handle) +#define PRESS_REPEAT_MAX_NUM 15 /*!< The maximum value of the repeat counter */ + +//button handle list head. +static struct Button* head_handle = NULL; + +static void button_handler(struct Button* handle); + +/** + * @brief Initializes the button struct handle. + * @param handle: the button handle struct. + * @param pin_level: read the HAL GPIO of the connected button level. + * @param active_level: pressed GPIO level. + * @param button_id: the button id. + * @retval None + */ +void button_init(struct Button* handle, uint8_t(*pin_level)(uint8_t), uint8_t active_level, uint8_t button_id) +{ + memset(handle, 0, sizeof(struct Button)); + handle->event = (uint8_t)NONE_PRESS; + handle->hal_button_Level = pin_level; + handle->button_level = !active_level; + handle->active_level = active_level; + handle->button_id = button_id; +} + +/** + * @brief Attach the button event callback function. + * @param handle: the button handle struct. + * @param event: trigger event type. + * @param cb: callback function. + * @retval None + */ +void button_attach(struct Button* handle, PressEvent event, BtnCallback cb) +{ + handle->cb[event] = cb; +} + +/** + * @brief Inquire the button event happen. + * @param handle: the button handle struct. + * @retval button event. + */ +PressEvent get_button_event(struct Button* handle) +{ + return (PressEvent)(handle->event); +} + +/** + * @brief Button driver core function, driver state machine. + * @param handle: the button handle struct. + * @retval None + */ +static void button_handler(struct Button* handle) +{ + uint8_t read_gpio_level = handle->hal_button_Level(handle->button_id); + + //ticks counter working.. + if((handle->state) > 0) handle->ticks++; + + /*------------button debounce handle---------------*/ + if(read_gpio_level != handle->button_level) { //not equal to prev one + //continue read 3 times same new level change + if(++(handle->debounce_cnt) >= DEBOUNCE_TICKS) { + handle->button_level = read_gpio_level; + handle->debounce_cnt = 0; + } + } else { //level not change ,counter reset. + handle->debounce_cnt = 0; + } + + /*-----------------State machine-------------------*/ + switch (handle->state) { + case 0: + if(handle->button_level == handle->active_level) { //start press down + handle->event = (uint8_t)PRESS_DOWN; + EVENT_CB(PRESS_DOWN); + handle->ticks = 0; + handle->repeat = 1; + handle->state = 1; + } else { + handle->event = (uint8_t)NONE_PRESS; + } + break; + + case 1: + if(handle->button_level != handle->active_level) { //released press up + handle->event = (uint8_t)PRESS_UP; + EVENT_CB(PRESS_UP); + handle->ticks = 0; + handle->state = 2; + } else if(handle->ticks > LONG_TICKS) { + handle->event = (uint8_t)LONG_PRESS_START; + EVENT_CB(LONG_PRESS_START); + handle->state = 5; + } + break; + + case 2: + if(handle->button_level == handle->active_level) { //press down again + handle->event = (uint8_t)PRESS_DOWN; + EVENT_CB(PRESS_DOWN); + if(handle->repeat != PRESS_REPEAT_MAX_NUM) { + handle->repeat++; + } + EVENT_CB(PRESS_REPEAT); // repeat hit + handle->ticks = 0; + handle->state = 3; + } else if(handle->ticks > SHORT_TICKS) { //released timeout + if(handle->repeat == 1) { + handle->event = (uint8_t)SINGLE_CLICK; + EVENT_CB(SINGLE_CLICK); + } else if(handle->repeat == 2) { + handle->event = (uint8_t)DOUBLE_CLICK; + EVENT_CB(DOUBLE_CLICK); // repeat hit + } + handle->state = 0; + } + break; + + case 3: + if(handle->button_level != handle->active_level) { //released press up + handle->event = (uint8_t)PRESS_UP; + EVENT_CB(PRESS_UP); + if(handle->ticks < SHORT_TICKS) { + handle->ticks = 0; + handle->state = 2; //repeat press + } else { + handle->state = 0; + } + } else if(handle->ticks > SHORT_TICKS) { // SHORT_TICKS < press down hold time < LONG_TICKS + handle->state = 1; + } + break; + + case 5: + if(handle->button_level == handle->active_level) { + //continue hold trigger + handle->event = (uint8_t)LONG_PRESS_HOLD; + EVENT_CB(LONG_PRESS_HOLD); + } else { //released + handle->event = (uint8_t)PRESS_UP; + EVENT_CB(PRESS_UP); + handle->state = 0; //reset + } + break; + default: + handle->state = 0; //reset + break; + } +} + +/** + * @brief Start the button work, add the handle into work list. + * @param handle: target handle struct. + * @retval 0: succeed. -1: already exist. + */ +int button_start(struct Button* handle) +{ + struct Button* target = head_handle; + while(target) { + if(target == handle) return -1; //already exist. + target = target->next; + } + handle->next = head_handle; + head_handle = handle; + return 0; +} + +/** + * @brief Stop the button work, remove the handle off work list. + * @param handle: target handle struct. + * @retval None + */ +void button_stop(struct Button* handle) +{ + struct Button** curr; + for(curr = &head_handle; *curr; ) { + struct Button* entry = *curr; + if(entry == handle) { + *curr = entry->next; +// free(entry); + return;//glacier add 2021-8-18 + } else { + curr = &entry->next; + } + } +} + +/** + * @brief background ticks, timer repeat invoking interval 5ms. + * @param None. + * @retval None + */ +void button_ticks(void) +{ + struct Button* target; + for(target=head_handle; target; target=target->next) { + button_handler(target); + } +} diff --git a/main/buttons/multi_button.h b/main/buttons/multi_button.h new file mode 100644 index 0000000..2974168 --- /dev/null +++ b/main/buttons/multi_button.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016 Zibin Zheng + * All rights reserved + */ + +#ifndef _MULTI_BUTTON_H_ +#define _MULTI_BUTTON_H_ + +#include +#include + +//According to your need to modify the constants. +#define TICKS_INTERVAL 5 //ms +#define DEBOUNCE_TICKS 3 //MAX 7 (0 ~ 7) +#define SHORT_TICKS (300 /TICKS_INTERVAL) +#define LONG_TICKS (1000 /TICKS_INTERVAL) + + +typedef void (*BtnCallback)(void*); + +typedef enum { + PRESS_DOWN = 0, + PRESS_UP, + PRESS_REPEAT, + SINGLE_CLICK, + DOUBLE_CLICK, + LONG_PRESS_START, + LONG_PRESS_HOLD, + number_of_event, + NONE_PRESS +}PressEvent; + +typedef struct Button { + uint16_t ticks; + uint8_t repeat : 4; + uint8_t event : 4; + uint8_t state : 3; + uint8_t debounce_cnt : 3; + uint8_t active_level : 1; + uint8_t button_level : 1; + uint8_t button_id; + uint8_t (*hal_button_Level)(uint8_t button_id_); + BtnCallback cb[number_of_event]; + struct Button* next; +}Button; + +#ifdef __cplusplus +extern "C" { +#endif + +void button_init(struct Button* handle, uint8_t(*pin_level)(uint8_t), uint8_t active_level, uint8_t button_id); +void button_attach(struct Button* handle, PressEvent event, BtnCallback cb); +PressEvent get_button_event(struct Button* handle); +int button_start(struct Button* handle); +void button_stop(struct Button* handle); +void button_ticks(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/main/display/Vernon_ST7789T/Vernon_ST7789T.c b/main/display/Vernon_ST7789T/Vernon_ST7789T.c new file mode 100644 index 0000000..f6ef3fa --- /dev/null +++ b/main/display/Vernon_ST7789T/Vernon_ST7789T.c @@ -0,0 +1,307 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "sdkconfig.h" +#if CONFIG_LCD_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "display/Vernon_ST7789T/Vernon_ST7789T.h" + +static const char *TAG = "lcd_panel.st7789t"; + +static esp_err_t panel_st7789t_del(esp_lcd_panel_t *panel); +static esp_err_t panel_st7789t_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_st7789t_init(esp_lcd_panel_t *panel); +static esp_err_t panel_st7789t_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_st7789t_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_st7789t_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_st7789t_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_st7789t_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_st7789t_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_cal; // save surrent value of LCD_CMD_COLMOD register +} st7789t_panel_t; + +esp_err_t esp_lcd_new_panel_st7789t(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_st7789t_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ +#if CONFIG_LCD_ENABLE_DEBUG_LOG + esp_log_level_set(TAG, ESP_LOG_DEBUG); +#endif + esp_err_t ret = ESP_OK; + st7789t_panel_t *st7789t = NULL; + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + st7789t = calloc(1, sizeof(st7789t_panel_t)); + ESP_GOTO_ON_FALSE(st7789t, ESP_ERR_NO_MEM, err, TAG, "no mem for st7789t panel"); + + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_config_t io_conf = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + + switch (panel_dev_config->rgb_endian) { + case LCD_RGB_ENDIAN_RGB: + st7789t->madctl_val = 0; + break; + case LCD_RGB_ENDIAN_BGR: + st7789t->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } + + uint8_t fb_bits_per_pixel = 0; + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + st7789t->colmod_cal = 0x55; + fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + st7789t->colmod_cal = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + st7789t->io = io; + st7789t->fb_bits_per_pixel = fb_bits_per_pixel; + st7789t->reset_gpio_num = panel_dev_config->reset_gpio_num; + st7789t->reset_level = panel_dev_config->flags.reset_active_high; + st7789t->base.del = panel_st7789t_del; + st7789t->base.reset = panel_st7789t_reset; + st7789t->base.init = panel_st7789t_init; + st7789t->base.draw_bitmap = panel_st7789t_draw_bitmap; + st7789t->base.invert_color = panel_st7789t_invert_color; + st7789t->base.set_gap = panel_st7789t_set_gap; + st7789t->base.mirror = panel_st7789t_mirror; + st7789t->base.swap_xy = panel_st7789t_swap_xy; + st7789t->base.disp_on_off = panel_st7789t_disp_on_off; + *ret_panel = &(st7789t->base); + ESP_LOGD(TAG, "new st7789t panel @%p", st7789t); + // printf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"); + return ESP_OK; + +err: + if (st7789t) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(st7789t); + } + return ret; +} + +static esp_err_t panel_st7789t_del(esp_lcd_panel_t *panel) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + + if (st7789t->reset_gpio_num >= 0) { + gpio_reset_pin(st7789t->reset_gpio_num); + } + ESP_LOGD(TAG, "del st7789t panel @%p", st7789t); + free(st7789t); + return ESP_OK; +} + +static esp_err_t panel_st7789t_reset(esp_lcd_panel_t *panel) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + esp_lcd_panel_io_handle_t io = st7789t->io; + + // perform hardware reset + if (st7789t->reset_gpio_num >= 0) { + gpio_set_level(st7789t->reset_gpio_num, st7789t->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(st7789t->reset_gpio_num, !st7789t->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + } else { // perform software reset + esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(20)); // spec, wait at least 5m before sending new command + } + + return ESP_OK; +} + +static esp_err_t panel_st7789t_init(esp_lcd_panel_t *panel) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + esp_lcd_panel_io_handle_t io = st7789t->io; + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + // printf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"); + esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + // esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {st7789t->madctl_val,}, 1); + // esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) {st7789t->colmod_cal,}, 1); + + /* Memory Data Access Control, MX=MV=1, MY=ML=MH=0, RGB=0 */ + esp_lcd_panel_io_tx_param(io, 0x36, (uint8_t []){0x00}, 1); // 0x36: 接口像素格式 X镜像,Y镜像 + /* Interface Pixel Format, 16bits/pixel for RGB/MCU interface */ + esp_lcd_panel_io_tx_param(io, 0x3A, (uint8_t []){0x55}, 1); // 0x3A: Porch 设置 + + esp_lcd_panel_io_tx_param(io, 0xB0, (uint8_t []){0x00, 0xE8}, 2); + /* Porch Setting */ + esp_lcd_panel_io_tx_param(io, 0xB2, (uint8_t []){0x0c, 0x0c, 0x00, 0x33, 0x33}, 5); + /* Gate Control, Vgh=13.65V, Vgl=-10.43V */ + esp_lcd_panel_io_tx_param(io, 0xB7, (uint8_t []){0x75}, 1); + /* VCOM Setting, VCOM=1.175V */ + esp_lcd_panel_io_tx_param(io, 0xBB, (uint8_t []){0x1A}, 1); + /* LCM Control, XOR: BGR, MX, MH */ + esp_lcd_panel_io_tx_param(io, 0xC0, (uint8_t []){0x80}, 1); + /* VDV and VRH Command Enable, enable=1 */ + esp_lcd_panel_io_tx_param(io, 0xC2, (uint8_t []){0x01, 0xff}, 2); + /* VRH Set, Vap=4.4+... */ + esp_lcd_panel_io_tx_param(io, 0xC3, (uint8_t []){0x13}, 1); + /* VDV Set, VDV=0 */ + esp_lcd_panel_io_tx_param(io, 0xC4, (uint8_t []){0x20}, 1); + /* Frame Rate Control, 60Hz, inversion=0 */ + esp_lcd_panel_io_tx_param(io, 0xC6, (uint8_t []){0x0F}, 1); + /* Power Control 1, AVDD=6.8V, AVCL=-4.8V, VDDS=2.3V */ + esp_lcd_panel_io_tx_param(io, 0xD0, (uint8_t []){0xA4, 0xA1}, 1); + /* Positive Voltage Gamma Control */ + esp_lcd_panel_io_tx_param(io, 0xE0, (uint8_t []){0xD0, 0x0D, 0x14, 0x0D, 0x0D, 0x09, 0x38, 0x44, 0x4E, 0x3A, 0x17, 0x18, 0x2F, 0x30}, 14); + /* Negative Voltage Gamma Control */ + esp_lcd_panel_io_tx_param(io, 0xE1, (uint8_t []){0xD0, 0x09, 0x0F, 0x08, 0x07, 0x14, 0x37, 0x44, 0x4D, 0x38, 0x15, 0x16, 0x2C, 0x2E}, 14); + /* Sleep Out */ + esp_lcd_panel_io_tx_param(io, 0x21, NULL, 0); + /* Display On */ + esp_lcd_panel_io_tx_param(io, 0x29, NULL, 0); + + esp_lcd_panel_io_tx_param(io, 0x2C, NULL, 0); + + return ESP_OK; +} + +static esp_err_t panel_st7789t_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = st7789t->io; + + x_start += st7789t->x_gap; + x_end += st7789t->x_gap; + y_start += st7789t->y_gap; + y_end += st7789t->y_gap; + + // define an area of frame memory where MCU can access + esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4); + esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * st7789t->fb_bits_per_pixel / 8; + esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len); + + return ESP_OK; +} + +static esp_err_t panel_st7789t_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + esp_lcd_panel_io_handle_t io = st7789t->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + esp_lcd_panel_io_tx_param(io, command, NULL, 0); + return ESP_OK; +} + +static esp_err_t panel_st7789t_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + esp_lcd_panel_io_handle_t io = st7789t->io; + if (mirror_x) { + st7789t->madctl_val |= LCD_CMD_MX_BIT; + } else { + st7789t->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y) { + st7789t->madctl_val |= LCD_CMD_MY_BIT; + } else { + st7789t->madctl_val &= ~LCD_CMD_MY_BIT; + } + esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + st7789t->madctl_val + }, 1); + return ESP_OK; +} + +static esp_err_t panel_st7789t_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + esp_lcd_panel_io_handle_t io = st7789t->io; + if (swap_axes) { + st7789t->madctl_val |= LCD_CMD_MV_BIT; + } else { + st7789t->madctl_val &= ~LCD_CMD_MV_BIT; + } + esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + st7789t->madctl_val + }, 1); + return ESP_OK; +} + +static esp_err_t panel_st7789t_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + st7789t->x_gap = x_gap; + st7789t->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_st7789t_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + st7789t_panel_t *st7789t = __containerof(panel, st7789t_panel_t, base); + esp_lcd_panel_io_handle_t io = st7789t->io; + int command = 0; + if (on_off) { + command = LCD_CMD_DISPON; + } else { + command = LCD_CMD_DISPOFF; + } + esp_lcd_panel_io_tx_param(io, command, NULL, 0); + return ESP_OK; +} diff --git a/main/display/Vernon_ST7789T/Vernon_ST7789T.h b/main/display/Vernon_ST7789T/Vernon_ST7789T.h new file mode 100644 index 0000000..7472f57 --- /dev/null +++ b/main/display/Vernon_ST7789T/Vernon_ST7789T.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "esp_lcd_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Configuration structure for panel device + */ +typedef struct { + int reset_gpio_num; /*!< GPIO used to reset the LCD panel, set to -1 if it's not used */ + union { + lcd_color_rgb_endian_t color_space; /*!< @deprecated Set RGB color space, please use rgb_endian instead */ + lcd_color_rgb_endian_t rgb_endian; /*!< Set RGB data endian: RGB or BGR */ + }; + unsigned int bits_per_pixel; /*!< Color depth, in bpp */ + struct { + unsigned int reset_active_high: 1; /*!< Setting this if the panel reset is high level active */ + } flags; /*!< LCD panel config flags */ + void *vendor_config; /*!< vendor specific configuration, optional, left as NULL if not used */ +} esp_lcd_panel_dev_st7789t_config_t; + +/** + * @brief Create LCD panel for model ST7789T + * + * @param[in] io LCD panel IO handle + * @param[in] panel_dev_config general panel device configuration + * @param[out] ret_panel Returned LCD panel handle + * @return + * - ESP_ERR_INVALID_ARG if parameter is invalid + * - ESP_ERR_NO_MEM if out of memory + * - ESP_OK on success + */ +esp_err_t esp_lcd_new_panel_st7789t(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_st7789t_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel); + +#ifdef __cplusplus +} +#endif diff --git a/main/display/display.c b/main/display/display.c new file mode 100644 index 0000000..4c3b255 --- /dev/null +++ b/main/display/display.c @@ -0,0 +1,199 @@ +#include "display/display.h" + +#include +#include +#include "esp_check.h" +#include "esp_log.h" +#include "driver/ledc.h" +#include "driver/spi_master.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_ops.h" +#include "display/Vernon_ST7789T/Vernon_ST7789T.h" + +#define LCD_HOST SPI3_HOST + +#define LCD_PIXEL_CLOCK_HZ (12 * 1000 * 1000) +#define LCD_CMD_BITS 8 +#define LCD_PARAM_BITS 8 + +#define LCD_H_RES 172 +#define LCD_V_RES 320 + +#define BANNER_W 320 +#define BANNER_H 172 + +#define LCD_PIN_SCLK 40 +#define LCD_PIN_MOSI 45 +#define LCD_PIN_MISO -1 +#define LCD_PIN_DC 41 +#define LCD_PIN_RST 39 +#define LCD_PIN_CS 42 +#define LCD_PIN_BK_LIGHT 46 + +#define LCD_X_GAP 34 +#define LCD_Y_GAP 0 + +#define LEDC_TIMER LEDC_TIMER_0 +#define LEDC_MODE LEDC_LOW_SPEED_MODE +#define LEDC_CHANNEL LEDC_CHANNEL_0 +#define LEDC_DUTY_RES LEDC_TIMER_13_BIT +#define LEDC_FREQUENCY_HZ 4000 + +#define BACKLIGHT_MIN_PERCENT 10 +#define BACKLIGHT_MAX_PERCENT 100 +#define BACKLIGHT_STEP_PERCENT 10 + +static const char *TAG = "display"; + +static esp_lcd_panel_handle_t panel_handle = NULL; +static uint8_t backlight_percent = 50; + +extern const uint8_t _binary_banner_320x172_rgb565_start[]; +extern const uint8_t _binary_banner_320x172_rgb565_end[]; + +static void backlight_ledc_init(void) +{ + ledc_timer_config_t ledc_timer = { + .speed_mode = LEDC_MODE, + .timer_num = LEDC_TIMER, + .duty_resolution = LEDC_DUTY_RES, + .freq_hz = LEDC_FREQUENCY_HZ, + .clk_cfg = LEDC_AUTO_CLK, + }; + ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); + + ledc_channel_config_t ledc_channel = { + .speed_mode = LEDC_MODE, + .channel = LEDC_CHANNEL, + .timer_sel = LEDC_TIMER, + .intr_type = LEDC_INTR_DISABLE, + .gpio_num = LCD_PIN_BK_LIGHT, + .duty = 0, + .hpoint = 0, + }; + ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); +} + +void display_set_backlight_percent(uint8_t percent) +{ + if (percent > BACKLIGHT_MAX_PERCENT) { + percent = BACKLIGHT_MAX_PERCENT; + } + backlight_percent = percent; + + uint32_t duty_max = (1U << LEDC_DUTY_RES) - 1; + uint32_t duty = (duty_max * backlight_percent) / 100; + ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty)); + ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL)); +} + +uint8_t display_get_backlight_percent(void) +{ + return backlight_percent; +} + +void display_cycle_backlight(void) +{ + uint8_t next = backlight_percent + BACKLIGHT_STEP_PERCENT; + if (next > BACKLIGHT_MAX_PERCENT) { + next = BACKLIGHT_MIN_PERCENT; + } + display_set_backlight_percent(next); + ESP_LOGI(TAG, "Backlight -> %u%%", next); +} + +esp_err_t display_init(void) +{ + esp_err_t ret = ESP_OK; + + spi_bus_config_t buscfg = { + .sclk_io_num = LCD_PIN_SCLK, + .mosi_io_num = LCD_PIN_MOSI, + .miso_io_num = LCD_PIN_MISO, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = LCD_H_RES * LCD_V_RES * sizeof(uint16_t), + }; + ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi bus init failed"); + + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_spi_config_t io_config = { + .dc_gpio_num = LCD_PIN_DC, + .cs_gpio_num = LCD_PIN_CS, + .pclk_hz = LCD_PIXEL_CLOCK_HZ, + .lcd_cmd_bits = LCD_CMD_BITS, + .lcd_param_bits = LCD_PARAM_BITS, + .spi_mode = 0, + .trans_queue_depth = 10, + .on_color_trans_done = NULL, + .user_ctx = NULL, + }; + ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io_handle), TAG, "panel io init failed"); + + esp_lcd_panel_dev_st7789t_config_t panel_config = { + .reset_gpio_num = LCD_PIN_RST, + .rgb_endian = LCD_RGB_ENDIAN_BGR, + .bits_per_pixel = 16, + }; + + ESP_RETURN_ON_ERROR(esp_lcd_new_panel_st7789t(io_handle, &panel_config, &panel_handle), TAG, "panel init failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(panel_handle), TAG, "panel reset failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_init(panel_handle), TAG, "panel init failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_mirror(panel_handle, true, true), TAG, "panel mirror failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_swap_xy(panel_handle, true), TAG, "panel swap failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_set_gap(panel_handle, LCD_Y_GAP, LCD_X_GAP), TAG, "panel gap failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_disp_on_off(panel_handle, true), TAG, "panel on failed"); + + backlight_ledc_init(); + display_set_backlight_percent(backlight_percent); + + return ret; +} + +void display_show_banner(void) +{ + if (!panel_handle) { + ESP_LOGW(TAG, "display not initialized"); + return; + } + + const uint8_t *start = _binary_banner_320x172_rgb565_start; + const uint8_t *end = _binary_banner_320x172_rgb565_end; + size_t len = (size_t)(end - start); + size_t expected = (size_t)BANNER_W * (size_t)BANNER_H * 2; + if (len < expected) { + ESP_LOGW(TAG, "banner data too small (%u < %u)", (unsigned)len, (unsigned)expected); + return; + } + + ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, BANNER_W, BANNER_H, start)); +} + +bool display_get_banner_center_rgb(uint8_t *r, uint8_t *g, uint8_t *b) +{ + if (!r || !g || !b) { + return false; + } + + const uint8_t *start = _binary_banner_320x172_rgb565_start; + const uint8_t *end = _binary_banner_320x172_rgb565_end; + size_t len = (size_t)(end - start); + size_t expected = (size_t)BANNER_W * (size_t)BANNER_H * 2; + if (len < expected) { + return false; + } + + size_t cx = BANNER_W / 2; + size_t cy = BANNER_H / 2; + size_t idx = (cy * BANNER_W + cx) * 2; + uint16_t pixel = (uint16_t)start[idx] | ((uint16_t)start[idx + 1] << 8); + + uint8_t r5 = (pixel >> 11) & 0x1F; + uint8_t g6 = (pixel >> 5) & 0x3F; + uint8_t b5 = pixel & 0x1F; + + *r = (uint8_t)((r5 * 255) / 31); + *g = (uint8_t)((g6 * 255) / 63); + *b = (uint8_t)((b5 * 255) / 31); + return true; +} diff --git a/main/display/display.h b/main/display/display.h new file mode 100644 index 0000000..29dc315 --- /dev/null +++ b/main/display/display.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t display_init(void); +void display_show_banner(void); +void display_set_backlight_percent(uint8_t percent); +uint8_t display_get_backlight_percent(void); +void display_cycle_backlight(void); +bool display_get_banner_center_rgb(uint8_t *r, uint8_t *g, uint8_t *b); + +#ifdef __cplusplus +} +#endif diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..8093f0d --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.0' + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + espressif/led_strip: ^2.4.1 diff --git a/main/mimi.c b/main/mimi.c index 0c61150..ff0c176 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -21,6 +21,9 @@ #include "cli/serial_cli.h" #include "proxy/http_proxy.h" #include "tools/tool_registry.h" +#include "display/display.h" +#include "buttons/button_driver.h" +#include "rgb/rgb.h" static const char *TAG = "mimi"; @@ -95,6 +98,13 @@ void app_main(void) ESP_LOGI(TAG, "PSRAM free: %d bytes", (int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + /* Display + input */ + ESP_ERROR_CHECK(display_init()); + display_show_banner(); + ESP_ERROR_CHECK(rgb_init()); + rgb_set(255, 0, 0); + button_Init(); + /* Phase 1: Core infrastructure */ ESP_ERROR_CHECK(init_nvs()); ESP_ERROR_CHECK(esp_event_loop_create_default()); diff --git a/main/rgb/rgb.c b/main/rgb/rgb.c new file mode 100644 index 0000000..3b9f5e7 --- /dev/null +++ b/main/rgb/rgb.c @@ -0,0 +1,39 @@ +#include "rgb/rgb.h" + +#include "esp_check.h" +#include "led_strip.h" + +#define RGB_GPIO 38 + +static led_strip_handle_t s_strip = NULL; + +esp_err_t rgb_init(void) +{ + led_strip_config_t strip_config = { + .strip_gpio_num = RGB_GPIO, + .max_leds = 1, + .led_pixel_format = LED_PIXEL_FORMAT_GRB, + .led_model = LED_MODEL_WS2812, + .flags.invert_out = false, + }; + + led_strip_rmt_config_t rmt_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10 * 1000 * 1000, + .flags.with_dma = false, + }; + + ESP_RETURN_ON_ERROR(led_strip_new_rmt_device(&strip_config, &rmt_config, &s_strip), "rgb", "led_strip init failed"); + led_strip_clear(s_strip); + return ESP_OK; +} + +void rgb_set(uint8_t r, uint8_t g, uint8_t b) +{ + if (!s_strip) { + return; + } + /* Swap R/G for this board's LED ordering */ + led_strip_set_pixel(s_strip, 0, g, r, b); + led_strip_refresh(s_strip); +} diff --git a/main/rgb/rgb.h b/main/rgb/rgb.h new file mode 100644 index 0000000..fa59683 --- /dev/null +++ b/main/rgb/rgb.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t rgb_init(void); +void rgb_set(uint8_t r, uint8_t g, uint8_t b); + +#ifdef __cplusplus +} +#endif diff --git a/main/tools/tool_files.c b/main/tools/tool_files.c index 3fada9e..7bf16cc 100644 --- a/main/tools/tool_files.c +++ b/main/tools/tool_files.c @@ -8,6 +8,7 @@ #include #include "esp_log.h" #include "cJSON.h" +#include static const char *TAG = "tool_files";