#include "board.h" #include #include #include "driver/i2c.h" #include "driver/ledc.h" #include "esp_check.h" #include "esp_heap_caps.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_rgb.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "lvgl.h" #include "radick_config.h" #define BOARD_I2C_PORT I2C_NUM_0 #define BOARD_I2C_FREQ_HZ 400000 #define BOARD_I2C_TIMEOUT_MS 30 #define GT911_READ_TIMEOUT_MS 5 #define GT911_REG_PRODUCT_ID 0x8140 #define GT911_REG_POINT_STATUS 0x814E #define GT911_REG_FIRST_POINT 0x814F #define LVGL_DRAW_LINES 48 #define LVGL_TICK_MS 2 static const char *TAG = "board"; static esp_lcd_panel_handle_t s_panel; static lv_disp_draw_buf_t s_draw_buf; static lv_disp_drv_t s_display_driver; static lv_indev_drv_t s_input_driver; static lv_color_t *s_buffer_a; static lv_color_t *s_buffer_b; static esp_timer_handle_t s_lvgl_tick_timer; static bool s_touch_available; static uint8_t s_touch_address; static bool s_touch_pressed; static uint16_t s_touch_x; static uint16_t s_touch_y; static int64_t s_touch_sample_us; static volatile uint32_t s_last_touch_ms; static uint8_t s_brightness; static esp_err_t i2c_write(uint8_t address, const uint8_t *data, size_t length) { return i2c_master_write_to_device(BOARD_I2C_PORT, address, data, length, pdMS_TO_TICKS(BOARD_I2C_TIMEOUT_MS)); } static esp_err_t pca9557_write(uint8_t reg, uint8_t value) { const uint8_t payload[2] = {reg, value}; return i2c_write(RADICK_PCA9557_ADDRESS, payload, sizeof(payload)); } static esp_err_t gt911_read(uint8_t address, uint16_t reg, uint8_t *data, size_t length) { const uint8_t command[2] = {(uint8_t)(reg >> 8), (uint8_t)reg}; return i2c_master_write_read_device(BOARD_I2C_PORT, address, command, sizeof(command), data, length, pdMS_TO_TICKS(GT911_READ_TIMEOUT_MS)); } static esp_err_t gt911_write_byte(uint8_t address, uint16_t reg, uint8_t value) { const uint8_t payload[3] = {(uint8_t)(reg >> 8), (uint8_t)reg, value}; return i2c_write(address, payload, sizeof(payload)); } static esp_err_t init_i2c(void) { const i2c_config_t config = { .mode = I2C_MODE_MASTER, .sda_io_num = RADICK_TOUCH_SDA_GPIO, .scl_io_num = RADICK_TOUCH_SCL_GPIO, .sda_pullup_en = GPIO_PULLUP_DISABLE, .scl_pullup_en = GPIO_PULLUP_DISABLE, .master.clk_speed = BOARD_I2C_FREQ_HZ, .clk_flags = 0, }; ESP_RETURN_ON_ERROR(i2c_param_config(BOARD_I2C_PORT, &config), TAG, "I2C configuration failed"); return i2c_driver_install(BOARD_I2C_PORT, config.mode, 0, 0, 0); } static esp_err_t reset_v3_touch(void) { // Vendor-required V3 sequence. IO0 is GT911 RESET and IO1 is INT. ESP_RETURN_ON_ERROR(pca9557_write(0x02, 0x00), TAG, "PCA9557 polarity reset failed"); ESP_RETURN_ON_ERROR(pca9557_write(0x01, 0xFC), TAG, "PCA9557 output setup failed"); ESP_RETURN_ON_ERROR(pca9557_write(0x03, 0xFC), TAG, "PCA9557 pin setup failed"); vTaskDelay(pdMS_TO_TICKS(20)); ESP_RETURN_ON_ERROR(pca9557_write(0x01, 0xFD), TAG, "GT911 reset release failed"); vTaskDelay(pdMS_TO_TICKS(100)); // Leave RESET driven high and release INT to an input. return pca9557_write(0x03, 0xFE); } static bool find_gt911(void) { const uint8_t addresses[] = {RADICK_GT911_ADDRESS, RADICK_GT911_ALT_ADDRESS}; uint8_t product_id[4]; for (size_t i = 0; i < sizeof(addresses); ++i) { if (gt911_read(addresses[i], GT911_REG_PRODUCT_ID, product_id, sizeof(product_id)) == ESP_OK) { s_touch_address = addresses[i]; ESP_LOGI(TAG, "GT911 at 0x%02x, product %.4s", s_touch_address, (const char *)product_id); return true; } } return false; } static bool poll_touch(uint16_t *x, uint16_t *y) { if (!s_touch_available) { return false; } uint8_t status = 0; const int64_t now_us = esp_timer_get_time(); if (gt911_read(s_touch_address, GT911_REG_POINT_STATUS, &status, 1) != ESP_OK) { if ((now_us - s_touch_sample_us) > 100000) { s_touch_pressed = false; } return s_touch_pressed; } if ((status & 0x80U) != 0U) { const uint8_t count = status & 0x0FU; if (count > 0U && count <= 5U) { uint8_t point[8]; if (gt911_read(s_touch_address, GT911_REG_FIRST_POINT, point, sizeof(point)) == ESP_OK) { // The vendor's ROTATION_NORMAL + map() path ultimately yields // these native landscape coordinates unchanged. s_touch_x = (uint16_t)point[1] | ((uint16_t)point[2] << 8); s_touch_y = (uint16_t)point[3] | ((uint16_t)point[4] << 8); if (s_touch_x >= RADICK_LCD_H_RES) { s_touch_x = RADICK_LCD_H_RES - 1; } if (s_touch_y >= RADICK_LCD_V_RES) { s_touch_y = RADICK_LCD_V_RES - 1; } s_touch_pressed = true; s_touch_sample_us = now_us; s_last_touch_ms = (uint32_t)(now_us / 1000); } } else { s_touch_pressed = false; s_touch_sample_us = now_us; } (void)gt911_write_byte(s_touch_address, GT911_REG_POINT_STATUS, 0); } else if ((now_us - s_touch_sample_us) > 100000) { s_touch_pressed = false; } *x = s_touch_x; *y = s_touch_y; return s_touch_pressed; } static void lvgl_touch_read(lv_indev_drv_t *driver, lv_indev_data_t *data) { (void)driver; uint16_t x = 0; uint16_t y = 0; const bool pressed = poll_touch(&x, &y); data->state = pressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL; if (pressed) { data->point.x = x; data->point.y = y; } } static void lvgl_flush(lv_disp_drv_t *driver, const lv_area_t *area, lv_color_t *pixels) { (void)driver; const esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, area->x1, area->y1, area->x2 + 1, area->y2 + 1, pixels); if (err != ESP_OK) { ESP_LOGE(TAG, "LCD flush failed: %s", esp_err_to_name(err)); } lv_disp_flush_ready(driver); } static void lvgl_tick(void *context) { (void)context; lv_tick_inc(LVGL_TICK_MS); } static esp_err_t init_backlight(void) { const ledc_timer_config_t timer = { .speed_mode = LEDC_LOW_SPEED_MODE, .duty_resolution = LEDC_TIMER_10_BIT, .timer_num = LEDC_TIMER_0, .freq_hz = 5000, .clk_cfg = LEDC_AUTO_CLK, }; const ledc_channel_config_t channel = { .gpio_num = RADICK_LCD_BACKLIGHT_GPIO, .speed_mode = LEDC_LOW_SPEED_MODE, .channel = LEDC_CHANNEL_0, .intr_type = LEDC_INTR_DISABLE, .timer_sel = LEDC_TIMER_0, .duty = 0, .hpoint = 0, }; ESP_RETURN_ON_ERROR(ledc_timer_config(&timer), TAG, "backlight timer init failed"); return ledc_channel_config(&channel); } static esp_err_t init_rgb_panel(void) { const esp_lcd_rgb_panel_config_t panel_config = { .clk_src = LCD_CLK_SRC_DEFAULT, .timings = { .pclk_hz = 15000000, .h_res = RADICK_LCD_H_RES, .v_res = RADICK_LCD_V_RES, .hsync_pulse_width = 48, .hsync_back_porch = 40, .hsync_front_porch = 40, .vsync_pulse_width = 31, .vsync_back_porch = 13, .vsync_front_porch = 1, .flags = { .pclk_active_neg = true, }, }, .data_width = 16, .bits_per_pixel = 16, .num_fbs = 1, .bounce_buffer_size_px = RADICK_LCD_H_RES * 10, .dma_burst_size = 64, .hsync_gpio_num = RADICK_LCD_HSYNC_GPIO, .vsync_gpio_num = RADICK_LCD_VSYNC_GPIO, .de_gpio_num = RADICK_LCD_DE_GPIO, .pclk_gpio_num = RADICK_LCD_PCLK_GPIO, .disp_gpio_num = GPIO_NUM_NC, .data_gpio_nums = { GPIO_NUM_15, GPIO_NUM_7, GPIO_NUM_6, GPIO_NUM_5, GPIO_NUM_4, GPIO_NUM_9, GPIO_NUM_46, GPIO_NUM_3, GPIO_NUM_8, GPIO_NUM_16, GPIO_NUM_1, GPIO_NUM_14, GPIO_NUM_21, GPIO_NUM_47, GPIO_NUM_48, GPIO_NUM_45, }, .flags = { .fb_in_psram = true, }, }; ESP_RETURN_ON_ERROR(esp_lcd_new_rgb_panel(&panel_config, &s_panel), TAG, "RGB panel allocation failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "RGB panel reset failed"); return esp_lcd_panel_init(s_panel); } static esp_err_t init_lvgl(void) { lv_init(); const size_t pixels = RADICK_LCD_H_RES * LVGL_DRAW_LINES; const size_t bytes = pixels * sizeof(lv_color_t); s_buffer_a = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); s_buffer_b = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (s_buffer_a == NULL || s_buffer_b == NULL) { return ESP_ERR_NO_MEM; } lv_disp_draw_buf_init(&s_draw_buf, s_buffer_a, s_buffer_b, pixels); lv_disp_drv_init(&s_display_driver); s_display_driver.hor_res = RADICK_LCD_H_RES; s_display_driver.ver_res = RADICK_LCD_V_RES; s_display_driver.flush_cb = lvgl_flush; s_display_driver.draw_buf = &s_draw_buf; lv_disp_drv_register(&s_display_driver); lv_indev_drv_init(&s_input_driver); s_input_driver.type = LV_INDEV_TYPE_POINTER; s_input_driver.read_cb = lvgl_touch_read; lv_indev_drv_register(&s_input_driver); const esp_timer_create_args_t timer_args = { .callback = lvgl_tick, .name = "lvgl_tick", }; ESP_RETURN_ON_ERROR(esp_timer_create(&timer_args, &s_lvgl_tick_timer), TAG, "LVGL tick timer create failed"); return esp_timer_start_periodic(s_lvgl_tick_timer, LVGL_TICK_MS * 1000); } esp_err_t board_init(void) { ESP_RETURN_ON_ERROR(init_backlight(), TAG, "backlight init failed"); ESP_RETURN_ON_ERROR(init_i2c(), TAG, "touch I2C init failed"); const esp_err_t reset_result = reset_v3_touch(); if (reset_result == ESP_OK) { s_touch_available = find_gt911(); } else { ESP_LOGW(TAG, "V3 touch reset failed: %s", esp_err_to_name(reset_result)); } if (!s_touch_available) { ESP_LOGW(TAG, "GT911 not found; display will remain usable without touch"); } ESP_RETURN_ON_ERROR(init_rgb_panel(), TAG, "LCD init failed"); ESP_RETURN_ON_ERROR(init_lvgl(), TAG, "LVGL init failed"); return ESP_OK; } void board_set_brightness(uint8_t percent) { if (percent > 100U) { percent = 100U; } const uint32_t max_duty = (1U << LEDC_TIMER_10_BIT) - 1U; const uint32_t duty = (max_duty * percent) / 100U; if (ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty) == ESP_OK) { (void)ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); s_brightness = percent; } } uint8_t board_get_brightness(void) { return s_brightness; } bool board_touch_available(void) { return s_touch_available; } uint32_t board_last_touch_ms(void) { return s_last_touch_ms; }