Compare commits

...

5 Commits

Author SHA1 Message Date
rsgltzyd 92bead72b9 feat: 更新 display.c,添加反转输出标志并移除不必要的日志和 GPIO 设置 2026-07-08 22:46:44 +08:00
rsgltzyd 5ef6c20862 feat: 更新ui_Screen1.c、ui_Screen2.c 和 ui_Screen3.c ,引入 board_config.h 以获取屏幕分辨率 2026-07-08 22:38:53 +08:00
rsgltzyd b154c3fa20 feat: 增强运动手势的校准与检测逻辑 2026-07-08 22:18:42 +08:00
rsgltzyd efb3c8f304 docs: 更新 README.md,添加触摸控制芯片与 IMU 传感器的 I2C 总线共享信息
- 修改触摸控制芯片 CST816 和六轴惯性测量单元 QMI8658A 的描述,说明它们共用 I2C 总线(IO47/IO48)
- 更新触摸屏和 IMU 传感器的引脚信息,确保文档准确反映硬件配置
- 添加 I2C 设备与固件配置部分,提供设备地址和说明
2026-07-05 21:29:47 +08:00
rsgltzyd ff7d82286a feat: 实现摇一摇导航功能 2026-07-05 21:28:54 +08:00
21 changed files with 882 additions and 30 deletions
+19 -8
View File
@@ -13,12 +13,12 @@
| | 叠封 PSRAM | **8MB** | 用于运行代码,性能优异 |
| | 外接 Flash | **16MB** (W25Q128) | 存储程序和数据 |
| **显示与触摸** | LCD 屏幕 | 1.9英寸,170×320分辨率,262K彩色 | 驱动IC为ST7789V2 |
| | 触摸控制芯片 | **CST816** (电容式) | **触控版特有**通过I2C接口 |
| | 触摸控制芯片 | **CST816** (电容式) | **触控版特有**I2C 与 IMU 共用 IO47/IO48 |
| **无线通信** | Wi-Fi | 2.4GHz, 802.11 b/g/n | |
| | 蓝牙 | Bluetooth 5 (BLE) | |
| | 板载天线 | 贴片陶瓷天线 | |
| | 外部天线接口 | **IPEX-1 连接器** | **触控版特有**,需修改焊盘启用 |
| **传感器** | 六轴惯性测量单元 | **QMI8658** | 包含3轴加速度计和3轴陀螺仪 |
| **传感器** | 六轴惯性测量单元 | **QMI8658A** | 与 CST816 共用 I2C 总线(IO47/IO48 |
| **存储扩展** | Micro SD 卡槽 | 支持Micro SD卡 | 位于PCB背面 |
| **电源管理** | 电池接口 | MX1.25 连接器 | 支持3.7V锂电池**充放电** |
| | USB供电 | USB Type-C 接口 | 同时用于下载调试 |
@@ -37,18 +37,18 @@
| | | IO12 | LCD\_CS(片选) |
| | | IO13 | LCD\_DIN(数据输入) |
| | | IO14 | LCD\_BL(背光控制,PWM |
| **触摸屏** | I2C | IO47 | TP\_SDA(数据线) |
| | | IO48 | TP\_SCL(时钟线) |
| **触摸屏 / I2C 总线** | I2C | IO47 | TP\_SDA / IMU\_SDA(与 IMU 共用) |
| (CST816 + QMI8658) | | IO48 | TP\_SCL / IMU\_SCL(与 IMU 共用) |
| | GPIO | IO17 | TP\_RESET(复位) |
| | | IO21 | TP\_INT(中断) |
| **SD 卡** | SPI | IO5 | SD\_CS(片选) |
| | | IO39 | SD\_CLK(时钟) |
| | | IO40 | SD\_MISO(主入从出) |
| | | IO41 | SD\_MOSI(主出从入) |
| **IMU 传感器** | I2C | IO8 | IMU\_SDA数据线) |
| (QMI8658A) | | IO7 | IMU\_SCL时钟线) |
| | GPIO | IO6 | IMU\_INT1(中断1 |
| | | IO15 | IMU\_INT2(中断2 |
| **IMU 传感器** | I2C | IO47 | IMU\_SDA与 TP 共用,见上) |
| (QMI8658A) | | IO48 | IMU\_SCL与 TP 共用,见上) |
| | GPIO | IO8 | IMU\_INT1(中断1 |
| | | IO7 | IMU\_INT2(中断2 |
| **UART0 串口** | UART | IO43 | U0\_TX(发送) |
| | | IO44 | U0\_RX(接收) |
| **用户按键** | GPIO | IO0 | BOOT0(启动/下载模式) |
@@ -63,6 +63,17 @@
| **扩展排针 J3** | 混合 | IO0~IO14 | 20Pin 排针,引出主要 GPIO |
| **扩展排针 J4** | 混合 | IO14~IO48等 | 20Pin 排针,含 USB/3V3/地 |
### I2C 设备与固件配置
触控版 PCB 上 **CST816(触摸)****QMI8658AIMU** 挂载在同一条 I2C 总线(`I2C_NUM_0`SDA=IO47SCL=IO48)。固件在 `main/bsp/board_config.h` 中定义引脚,触摸与 IMU 驱动共用该总线句柄。
| 设备 | I2C 地址(7-bit | 说明 |
| :------- | :---------------- | :---------------------------- |
| CST816S | 由驱动自动探测 | 电容触摸 |
| QMI8658A | `0x6A``0x6B` | 由 SDO 电平决定;本板实测多为 `0x6B` |
IMU 中断引脚(IO8 / IO7)当前固件采用轮询采样,Phase 4 可改为中断驱动。
## Wi-Fi Station Example
(See the README.md file in the upper level 'examples' directory for more information about examples.)
+3
View File
@@ -1,10 +1,13 @@
idf_component_register(SRCS
"app/app_main.c"
"app/ui_handlers.c"
"app/ui_nav.c"
"app/motion_gesture.c"
"bsp/display.c"
"bsp/touch.c"
"bsp/lvgl_port.c"
"bsp/boot_button.c"
"bsp/imu.c"
"services/wifi_manager.c"
"services/ble_manager.c"
"services/ble_hid.c"
+19
View File
@@ -3,11 +3,15 @@
#include "board_config.h"
#include "display.h"
#include "imu.h"
#include "lvgl_port.h"
#include "motion_gesture.h"
#include "touch.h"
#include "ui.h"
#include "ui_handlers.h"
#include "ui_nav.h"
#include "ble_manager.h"
#include "settings/settings_store.h"
#if BOARD_WIFI_ENABLED
#include "wifi_manager.h"
@@ -38,10 +42,25 @@ void app_main(void)
lvgl_port_lock();
ui_init();
ui_nav_init();
lvgl_port_unlock();
settings_store_load();
settings_store_apply();
ui_handlers_init();
motion_gesture_init(NULL);
#if BOARD_IMU_ENABLED
ret = imu_init();
if (ret == ESP_OK) {
imu_start_task(motion_gesture_on_sample);
} else {
ESP_LOGW(TAG, "IMU init failed, shake navigation unavailable");
}
#endif
#if BOARD_WIFI_ENABLED
ESP_LOGI(TAG, "Starting WiFi");
wifi_manager_init();
+295
View File
@@ -0,0 +1,295 @@
#include "motion_gesture.h"
#include "ui_nav.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <assert.h>
#include <math.h>
static const char *TAG = "motion";
#define SAMPLE_PERIOD_MS 10
#define SAMPLE_RATE_HZ 100
/* Calibration: keep a sliding window of the last 500 ms and finish only when
* the window is stable (low variance). This avoids calibrating while the user
* is picking up or moving the device. */
#define CALIBRATION_MS 500
#define CALIBRATION_WINDOW_SAMPLES (CALIBRATION_MS / SAMPLE_PERIOD_MS)
#define CALIBRATION_MAX_VAR_G2 0.010f
/* Shake is detected on the horizontal plane (X/Y) so that picking the device
* up or putting it down (Z-axis jolt) does not trigger navigation. */
#define THRESHOLD_LOW_G 0.6f
#define THRESHOLD_MED_G 0.8f
#define THRESHOLD_HIGH_G 1.0f
/* A valid shake must produce enough strong samples within a short window. */
#define WINDOW_MS 300
#define MIN_SAMPLES 5
/* A real shake oscillates; require at least two zero-crossings on the
* dominant horizontal axis to distinguish it from a single jolt. */
#define MIN_ZERO_CROSSINGS 2
/* Cooldown prevents one physical shake from generating multiple events. */
#define COOLDOWN_MS 350
typedef enum {
GESTURE_IDLE = 0,
GESTURE_COOLDOWN,
} gesture_state_t;
static QueueHandle_t s_nav_queue;
static bool s_enabled = true;
static float s_threshold = THRESHOLD_MED_G;
static gesture_state_t s_state = GESTURE_IDLE;
/* Baseline calibration */
static float s_ref_x;
static float s_ref_y;
static float s_ref_z;
static bool s_calibrated;
static float s_cal_buf_x[CALIBRATION_WINDOW_SAMPLES];
static float s_cal_buf_y[CALIBRATION_WINDOW_SAMPLES];
static float s_cal_buf_z[CALIBRATION_WINDOW_SAMPLES];
static int s_cal_head;
static int s_cal_count;
/* Detection window state */
static int s_over_count;
static int64_t s_window_start_us;
static int s_zero_crossings;
static int s_last_x_sign;
static int s_last_y_sign;
static int64_t s_cooldown_until_us;
/* Last horizontal magnitude, exposed for UI/debug. */
static float s_last_horizontal_mag;
static void nav_handler_task(void *arg)
{
(void)arg;
motion_nav_event_t ev;
while (xQueueReceive(s_nav_queue, &ev, portMAX_DELAY) == pdTRUE) {
if (ev == MOTION_NAV_NEXT) {
ui_nav_next();
}
}
}
static void post_nav_next(void)
{
motion_nav_event_t ev = MOTION_NAV_NEXT;
if (xQueueSend(s_nav_queue, &ev, 0) != pdTRUE) {
ESP_LOGW(TAG, "nav queue full");
} else {
ESP_LOGI(TAG, "Shake detected -> next screen");
}
}
static void reset_window(void)
{
s_over_count = 0;
s_window_start_us = 0;
s_zero_crossings = 0;
s_last_x_sign = 0;
s_last_y_sign = 0;
}
static void run_calibration(float ax, float ay, float az)
{
s_cal_buf_x[s_cal_head] = ax;
s_cal_buf_y[s_cal_head] = ay;
s_cal_buf_z[s_cal_head] = az;
s_cal_head = (s_cal_head + 1) % CALIBRATION_WINDOW_SAMPLES;
if (s_cal_count < CALIBRATION_WINDOW_SAMPLES) {
s_cal_count++;
}
if (s_cal_count < CALIBRATION_WINDOW_SAMPLES) {
return;
}
float sum_x = 0.0f;
float sum_y = 0.0f;
float sum_z = 0.0f;
float sum_sq_x = 0.0f;
float sum_sq_y = 0.0f;
float sum_sq_z = 0.0f;
for (int i = 0; i < CALIBRATION_WINDOW_SAMPLES; i++) {
sum_x += s_cal_buf_x[i];
sum_y += s_cal_buf_y[i];
sum_z += s_cal_buf_z[i];
sum_sq_x += s_cal_buf_x[i] * s_cal_buf_x[i];
sum_sq_y += s_cal_buf_y[i] * s_cal_buf_y[i];
sum_sq_z += s_cal_buf_z[i] * s_cal_buf_z[i];
}
const int n = CALIBRATION_WINDOW_SAMPLES;
float var_x = (sum_sq_x / n) - (sum_x / n) * (sum_x / n);
float var_y = (sum_sq_y / n) - (sum_y / n) * (sum_y / n);
float var_z = (sum_sq_z / n) - (sum_z / n) * (sum_z / n);
float max_var = fmaxf(var_x, fmaxf(var_y, var_z));
if (max_var > CALIBRATION_MAX_VAR_G2) {
static int64_t s_last_cal_warn_us;
int64_t now_us = esp_timer_get_time();
if ((now_us - s_last_cal_warn_us) >= 1000 * 1000) {
s_last_cal_warn_us = now_us;
ESP_LOGW(TAG, "Calibration: motion detected (var=%.3f), waiting", max_var);
}
return;
}
s_ref_x = sum_x / n;
s_ref_y = sum_y / n;
s_ref_z = sum_z / n;
s_calibrated = true;
ESP_LOGI(TAG, "Baseline calibrated (ref=%.3f,%.3f,%.3f g)", s_ref_x, s_ref_y, s_ref_z);
}
static int axis_sign(float value)
{
if (value > 0.0f) {
return 1;
}
if (value < 0.0f) {
return -1;
}
return 0;
}
static void process_sample(float ax, float ay, float az)
{
int64_t now_us = esp_timer_get_time();
if (!s_calibrated) {
run_calibration(ax, ay, az);
return;
}
float dx = ax - s_ref_x;
float dy = ay - s_ref_y;
float dz = az - s_ref_z;
/* Use only horizontal magnitude for shake detection. Z-axis is dominated by
* gravity and is the main axis for pick-up/put-down false triggers. */
float horizontal_mag = sqrtf(dx * dx + dy * dy);
s_last_horizontal_mag = horizontal_mag;
if (s_state == GESTURE_COOLDOWN) {
if (now_us >= s_cooldown_until_us) {
s_state = GESTURE_IDLE;
reset_window();
}
return;
}
if (horizontal_mag > s_threshold) {
if (s_over_count == 0) {
s_window_start_us = now_us;
}
s_over_count++;
int x_sign = axis_sign(dx);
int y_sign = axis_sign(dy);
if (s_last_x_sign != 0 && x_sign != s_last_x_sign) {
s_zero_crossings++;
}
if (s_last_y_sign != 0 && y_sign != s_last_y_sign) {
s_zero_crossings++;
}
s_last_x_sign = x_sign;
s_last_y_sign = y_sign;
int64_t window_us = (int64_t)WINDOW_MS * 1000;
if ((now_us - s_window_start_us) > window_us) {
/* Window expired: start fresh with the current sample. */
reset_window();
s_window_start_us = now_us;
s_over_count = 1;
s_last_x_sign = axis_sign(dx);
s_last_y_sign = axis_sign(dy);
}
if (s_over_count >= MIN_SAMPLES && s_zero_crossings >= MIN_ZERO_CROSSINGS) {
if (s_enabled) {
post_nav_next();
}
s_state = GESTURE_COOLDOWN;
s_cooldown_until_us = now_us + (int64_t)COOLDOWN_MS * 1000;
reset_window();
}
} else if (s_over_count > 0) {
int64_t window_us = (int64_t)WINDOW_MS * 1000;
if ((now_us - s_window_start_us) > window_us) {
reset_window();
}
}
(void)dz;
}
void motion_gesture_init(motion_nav_cb_t cb)
{
(void)cb;
s_nav_queue = xQueueCreate(4, sizeof(motion_nav_event_t));
xTaskCreate(nav_handler_task, "nav_handler", 2048, NULL, 5, NULL);
s_calibrated = false;
s_cal_head = 0;
s_cal_count = 0;
ESP_LOGI(TAG, "Motion gesture ready (calibrating %d ms)", CALIBRATION_MS);
}
void motion_gesture_set_enabled(bool enabled)
{
s_enabled = enabled;
ESP_LOGI(TAG, "Shake navigation %s", enabled ? "enabled" : "disabled");
}
void motion_gesture_set_sensitivity(shake_sensitivity_t sens)
{
switch (sens) {
case SHAKE_SENS_LOW:
s_threshold = THRESHOLD_LOW_G;
break;
case SHAKE_SENS_MED:
s_threshold = THRESHOLD_MED_G;
break;
case SHAKE_SENS_HIGH:
s_threshold = THRESHOLD_HIGH_G;
break;
default:
assert(false && "unexpected shake_sensitivity_t");
break;
}
ESP_LOGI(TAG, "Sensitivity set (threshold=%.2f g)", s_threshold);
}
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g)
{
process_sample(ax_g, ay_g, az_g);
}
float motion_gesture_get_last_magnitude(void)
{
return s_last_horizontal_mag;
}
bool motion_gesture_is_calibrated(void)
{
return s_calibrated;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <stdbool.h>
#include "settings_types.h"
typedef enum {
MOTION_NAV_NEXT = 0,
} motion_nav_event_t;
typedef void (*motion_nav_cb_t)(motion_nav_event_t event);
void motion_gesture_init(motion_nav_cb_t cb);
void motion_gesture_set_enabled(bool enabled);
void motion_gesture_set_sensitivity(shake_sensitivity_t sens);
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g);
float motion_gesture_get_last_magnitude(void);
bool motion_gesture_is_calibrated(void);
+6
View File
@@ -105,6 +105,12 @@ void settings_on_value_changed(lv_event_t *e)
case SETTINGS_FIELD_MUTE:
buf.mute = lv_obj_has_state(target, LV_STATE_CHECKED);
break;
case SETTINGS_FIELD_SHAKE_NAV:
buf.shake_nav_enabled = lv_obj_has_state(target, LV_STATE_CHECKED);
break;
case SETTINGS_FIELD_SHAKE_SENS:
buf.shake_sensitivity = (shake_sensitivity_t)lv_dropdown_get_selected(target);
break;
default:
LV_ASSERT_MSG(false, "unexpected settings_field_t");
break;
+76
View File
@@ -0,0 +1,76 @@
#include "ui_nav.h"
#include "lvgl_port.h"
#include "screens/ui_Screen1.h"
#include "screens/ui_Screen2.h"
#include "screens/ui_Screen3.h"
#include "screens/ui_Settings.h"
#include "ui_helpers.h"
#include "esp_log.h"
static const char *TAG = "ui_nav";
static lv_obj_t *s_nav_screens[4];
static const char *const s_nav_names[] = {
"Settings",
"Screen1",
"Screen2",
"Screen3",
};
#define NAV_SCREEN_COUNT (sizeof(s_nav_screens) / sizeof(s_nav_screens[0]))
static uint8_t s_nav_index;
void ui_nav_init(void)
{
s_nav_screens[0] = ui_Settings;
s_nav_screens[1] = ui_Screen1;
s_nav_screens[2] = ui_Screen2;
s_nav_screens[3] = ui_Screen3;
s_nav_index = 0;
ESP_LOGI(TAG, "Navigation ready (%u screens, start=%s)", (unsigned)NAV_SCREEN_COUNT,
s_nav_names[s_nav_index]);
}
void ui_nav_goto(uint8_t index)
{
if (index >= NAV_SCREEN_COUNT) {
return;
}
lvgl_port_lock();
s_nav_index = index;
lv_obj_t **target = &s_nav_screens[s_nav_index];
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
lvgl_port_unlock();
ESP_LOGI(TAG, "Goto %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
}
void ui_nav_next(void)
{
lvgl_port_lock();
s_nav_index = (s_nav_index + 1) % NAV_SCREEN_COUNT;
lv_obj_t **target = &s_nav_screens[s_nav_index];
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
lvgl_port_unlock();
ESP_LOGI(TAG, "Next -> %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
}
lv_obj_t *ui_nav_current(void)
{
return s_nav_screens[s_nav_index];
}
uint8_t ui_nav_current_index(void)
{
return s_nav_index;
}
const char *ui_nav_current_name(void)
{
return s_nav_names[s_nav_index];
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "lvgl.h"
void ui_nav_init(void);
void ui_nav_next(void);
void ui_nav_goto(uint8_t index);
lv_obj_t *ui_nav_current(void);
uint8_t ui_nav_current_index(void);
const char *ui_nav_current_name(void);
+9
View File
@@ -49,6 +49,15 @@
#define BOARD_TOUCH_I2C_FREQ_HZ 400000
#define BOARD_TOUCH_ENABLED 1
/* IMU (QMI8658) — shares I2C bus with touch on IO47/IO48 */
#define BOARD_IMU_I2C_NUM I2C_NUM_0
#define BOARD_PIN_IMU_SDA 47
#define BOARD_PIN_IMU_SCL 48
#define BOARD_PIN_IMU_INT1 8
#define BOARD_PIN_IMU_INT2 7
#define BOARD_IMU_I2C_FREQ_HZ 400000
#define BOARD_IMU_ENABLED 1
/* User IO */
#define BOARD_PIN_BOOT 0
#define BOARD_PIN_KEY 4
+1 -4
View File
@@ -2,7 +2,6 @@
#include "board_config.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_lcd_panel_ops.h"
@@ -49,6 +48,7 @@ esp_err_t display_init(esp_lcd_panel_io_handle_t *io_handle_out,
.gpio_num = BOARD_PIN_BK_LIGHT,
.duty = 0,
.hpoint = 0,
.flags.output_invert = 1,
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
@@ -90,9 +90,6 @@ esp_err_t display_init(esp_lcd_panel_io_handle_t *io_handle_out,
ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, BOARD_LCD_GAP_X, BOARD_LCD_GAP_Y));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
ESP_LOGI(TAG, "Turn on LCD backlight");
gpio_set_level(BOARD_PIN_BK_LIGHT, BOARD_LCD_BK_LIGHT_ON_LEVEL);
*io_handle_out = io_handle;
*panel_handle_out = panel_handle;
return ESP_OK;
+308
View File
@@ -0,0 +1,308 @@
#include "imu.h"
#include "board_config.h"
#include "touch.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <math.h>
static const char *TAG = "imu";
#if BOARD_IMU_ENABLED
#define QMI8658_ADDR_SDO_LOW 0x6A
#define QMI8658_ADDR_SDO_HIGH 0x6B
#define QMI8658_WHO_AM_I 0x00
#define QMI8658_WHO_AM_I_ID 0x05
#define QMI8658_CTRL1 0x02
#define QMI8658_CTRL2 0x03
#define QMI8658_CTRL3 0x04
#define QMI8658_CTRL5 0x06
#define QMI8658_CTRL6 0x07
#define QMI8658_CTRL7 0x08
#define QMI8658_RESET 0x60
#define QMI8658_ACC_X_L 0x35
#define QMI8658_RESET_VALUE 0xB0
/* Little-endian + address auto-increment (matches int16 parse below) */
#define QMI8658_CTRL1_VALUE 0x40
/* ±8g, 1000 Hz ODR */
#define QMI8658_CTRL2_VALUE 0x23
/* Gyro ±512 dps, 1000 Hz — required even when only reading accel */
#define QMI8658_CTRL3_VALUE 0x53
#define QMI8658_CTRL5_VALUE 0x11
#define QMI8658_CTRL6_VALUE 0x00
#define QMI8658_CTRL7_VALUE 0x03
#define QMI8658_ACCEL_SENS_8G 4096.0f
#define IMU_SAMPLE_PERIOD_MS 10
#define IMU_DEBUG_LOG_INTERVAL_MS 1000
static i2c_master_bus_handle_t s_imu_bus;
static i2c_master_dev_handle_t s_imu_dev;
static imu_sample_cb_t s_sample_cb;
static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz);
static int16_t imu_raw16_le(const uint8_t *lo, const uint8_t *hi)
{
return (int16_t)((*hi << 8) | *lo);
}
static esp_err_t imu_write_u8(uint8_t reg, uint8_t val)
{
uint8_t buf[2] = {reg, val};
return i2c_master_transmit(s_imu_dev, buf, sizeof(buf), 100);
}
static esp_err_t imu_read_u8(uint8_t reg, uint8_t *val)
{
return i2c_master_transmit_receive(s_imu_dev, &reg, 1, val, 1, 100);
}
static esp_err_t imu_read_bytes(uint8_t reg, uint8_t *data, size_t len)
{
return i2c_master_transmit_receive(s_imu_dev, &reg, 1, data, len, 100);
}
static esp_err_t imu_probe_device(uint8_t addr)
{
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = addr,
.scl_speed_hz = BOARD_IMU_I2C_FREQ_HZ,
};
esp_err_t err = i2c_master_bus_add_device(s_imu_bus, &dev_cfg, &s_imu_dev);
if (err != ESP_OK) {
return err;
}
uint8_t who_am_i = 0;
err = imu_read_u8(QMI8658_WHO_AM_I, &who_am_i);
if (err != ESP_OK || who_am_i != QMI8658_WHO_AM_I_ID) {
i2c_master_bus_rm_device(s_imu_dev);
s_imu_dev = NULL;
if (err == ESP_OK) {
ESP_LOGW(TAG, "WHO_AM_I mismatch at 0x%02X: 0x%02X", addr, who_am_i);
return ESP_ERR_NOT_FOUND;
}
return err;
}
ESP_LOGI(TAG, "QMI8658 found at 0x%02X (WHO_AM_I=0x%02X)", addr, who_am_i);
return ESP_OK;
}
static esp_err_t imu_configure(void)
{
esp_err_t err = imu_write_u8(QMI8658_RESET, QMI8658_RESET_VALUE);
if (err != ESP_OK) {
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
err = imu_write_u8(QMI8658_CTRL1, QMI8658_CTRL1_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL2, QMI8658_CTRL2_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL3, QMI8658_CTRL3_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL5, QMI8658_CTRL5_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL6, QMI8658_CTRL6_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL7, QMI8658_CTRL7_VALUE);
if (err != ESP_OK) {
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
return ESP_OK;
}
static void imu_log_sample(const char *label, float ax, float ay, float az,
int16_t rx, int16_t ry, int16_t rz)
{
float mag = sqrtf(ax * ax + ay * ay + az * az);
ESP_LOGI(TAG, "%s raw=(%d,%d,%d) g=(%.3f,%.3f,%.3f) |mag|=%.3f",
label, rx, ry, rz, ax, ay, az, mag);
}
static void imu_task(void *arg)
{
(void)arg;
int64_t last_log_us = esp_timer_get_time();
while (1) {
float ax = 0.0f;
float ay = 0.0f;
float az = 0.0f;
int16_t rx = 0;
int16_t ry = 0;
int16_t rz = 0;
esp_err_t err = imu_read_raw(&rx, &ry, &rz);
if (err == ESP_OK) {
ax = (float)rx / QMI8658_ACCEL_SENS_8G;
ay = (float)ry / QMI8658_ACCEL_SENS_8G;
az = (float)rz / QMI8658_ACCEL_SENS_8G;
int64_t now_us = esp_timer_get_time();
if ((now_us - last_log_us) >= (int64_t)IMU_DEBUG_LOG_INTERVAL_MS * 1000) {
imu_log_sample("sample", ax, ay, az, rx, ry, rz);
last_log_us = now_us;
}
if (s_sample_cb != NULL) {
s_sample_cb(ax, ay, az);
}
}
vTaskDelay(pdMS_TO_TICKS(IMU_SAMPLE_PERIOD_MS));
}
}
esp_err_t imu_init(void)
{
#if BOARD_TOUCH_ENABLED
s_imu_bus = touch_get_i2c_bus();
if (s_imu_bus == NULL) {
ESP_LOGE(TAG, "Shared I2C bus not ready (touch not initialized?)");
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Using shared I2C bus (SDA=%d SCL=%d)", BOARD_PIN_IMU_SDA,
BOARD_PIN_IMU_SCL);
#else
ESP_LOGI(TAG, "Initialize dedicated I2C for IMU");
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = BOARD_IMU_I2C_NUM,
.sda_io_num = BOARD_PIN_IMU_SDA,
.scl_io_num = BOARD_PIN_IMU_SCL,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
esp_err_t bus_err = i2c_new_master_bus(&bus_config, &s_imu_bus);
if (bus_err != ESP_OK) {
return bus_err;
}
#endif
esp_err_t err = imu_probe_device(QMI8658_ADDR_SDO_LOW);
if (err != ESP_OK) {
err = imu_probe_device(QMI8658_ADDR_SDO_HIGH);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "QMI8658 not found");
return err;
}
err = imu_configure();
if (err != ESP_OK) {
ESP_LOGE(TAG, "IMU configure failed: %s", esp_err_to_name(err));
return err;
}
float ax = 0.0f;
float ay = 0.0f;
float az = 0.0f;
int16_t rx = 0;
int16_t ry = 0;
int16_t rz = 0;
if (imu_read_accel(&ax, &ay, &az) == ESP_OK) {
imu_read_raw(&rx, &ry, &rz);
imu_log_sample("init check", ax, ay, az, rx, ry, rz);
} else {
ESP_LOGW(TAG, "Init check read failed");
}
ESP_LOGI(TAG, "IMU initialized (±8g, 1000 Hz ODR)");
return ESP_OK;
}
static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz)
{
if (s_imu_dev == NULL || rx == NULL || ry == NULL || rz == NULL) {
return ESP_ERR_INVALID_STATE;
}
uint8_t raw[6];
esp_err_t err = imu_read_bytes(QMI8658_ACC_X_L, raw, sizeof(raw));
if (err != ESP_OK) {
return err;
}
*rx = imu_raw16_le(&raw[0], &raw[1]);
*ry = imu_raw16_le(&raw[2], &raw[3]);
*rz = imu_raw16_le(&raw[4], &raw[5]);
return ESP_OK;
}
esp_err_t imu_read_accel(float *ax, float *ay, float *az)
{
int16_t rx = 0;
int16_t ry = 0;
int16_t rz = 0;
esp_err_t err = imu_read_raw(&rx, &ry, &rz);
if (err != ESP_OK) {
return err;
}
*ax = (float)rx / QMI8658_ACCEL_SENS_8G;
*ay = (float)ry / QMI8658_ACCEL_SENS_8G;
*az = (float)rz / QMI8658_ACCEL_SENS_8G;
return ESP_OK;
}
void imu_start_task(imu_sample_cb_t cb)
{
s_sample_cb = cb;
xTaskCreate(imu_task, "imu_task", 3072, NULL, 4, NULL);
ESP_LOGI(TAG, "IMU sample task started");
}
#else
esp_err_t imu_init(void)
{
return ESP_OK;
}
esp_err_t imu_read_accel(float *ax, float *ay, float *az)
{
(void)ax;
(void)ay;
(void)az;
return ESP_ERR_NOT_SUPPORTED;
}
void imu_start_task(imu_sample_cb_t cb)
{
(void)cb;
}
#endif
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "esp_err.h"
typedef void (*imu_sample_cb_t)(float ax_g, float ay_g, float az_g);
esp_err_t imu_init(void);
esp_err_t imu_read_accel(float *ax, float *ay, float *az);
void imu_start_task(imu_sample_cb_t cb);
+10
View File
@@ -103,6 +103,11 @@ esp_err_t touch_init(lv_display_t *display)
return ESP_OK;
}
i2c_master_bus_handle_t touch_get_i2c_bus(void)
{
return s_touch_i2c_bus;
}
#else
esp_err_t touch_init(lv_display_t *display)
@@ -111,4 +116,9 @@ esp_err_t touch_init(lv_display_t *display)
return ESP_OK;
}
i2c_master_bus_handle_t touch_get_i2c_bus(void)
{
return NULL;
}
#endif
+2
View File
@@ -1,6 +1,8 @@
#pragma once
#include "driver/i2c_master.h"
#include "esp_err.h"
#include "lvgl.h"
esp_err_t touch_init(lv_display_t *display);
i2c_master_bus_handle_t touch_get_i2c_bus(void);
+4 -2
View File
@@ -4,6 +4,7 @@
// Project name: HeavenlyHeroStar
#include "../ui.h"
#include "board_config.h"
lv_obj_t * uic_Calendar1;
lv_obj_t * ui_Screen1 = NULL;
@@ -15,12 +16,13 @@ lv_obj_t * ui_Calendar1 = NULL;
void ui_Screen1_screen_init(void)
{
ui_Screen1 = lv_obj_create(NULL);
lv_obj_set_size(ui_Screen1, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); /// Flags
ui_Calendar1 = lv_calendar_create(ui_Screen1);
lv_obj_t * ui_Calendar1_header = lv_calendar_header_arrow_create(ui_Calendar1);
lv_obj_set_width(ui_Calendar1, 170);
lv_obj_set_height(ui_Calendar1, 320);
lv_obj_set_width(ui_Calendar1, BOARD_LCD_LOGIC_H_RES);
lv_obj_set_height(ui_Calendar1, BOARD_LCD_LOGIC_V_RES);
lv_obj_set_align(ui_Calendar1, LV_ALIGN_CENTER);
uic_Calendar1 = ui_Calendar1;
+12 -6
View File
@@ -4,6 +4,7 @@
// Project name: HeavenlyHeroStar
#include "../ui.h"
#include "board_config.h"
lv_obj_t * ui_Screen2 = NULL;
lv_obj_t * ui_Button1 = NULL;
@@ -45,11 +46,14 @@ void ui_event_Button3(lv_event_t * e)
void ui_Screen2_screen_init(void)
{
ui_Screen2 = lv_obj_create(NULL);
lv_obj_set_size(ui_Screen2, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Screen2, LV_OBJ_FLAG_SCROLLABLE); /// Flags
ui_Button1 = lv_button_create(ui_Screen2);
lv_obj_set_width(ui_Button1, 100);
lv_obj_set_height(ui_Button1, 80);
lv_obj_set_x(ui_Button1, -100);
lv_obj_set_y(ui_Button1, 0);
lv_obj_set_align(ui_Button1, LV_ALIGN_CENTER);
lv_obj_add_flag(ui_Button1, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
lv_obj_remove_flag(ui_Button1, LV_OBJ_FLAG_SCROLLABLE); /// Flags
@@ -58,7 +62,7 @@ void ui_Screen2_screen_init(void)
lv_obj_set_width(ui_Button2, 100);
lv_obj_set_height(ui_Button2, 80);
lv_obj_set_x(ui_Button2, 0);
lv_obj_set_y(ui_Button2, 100);
lv_obj_set_y(ui_Button2, 0);
lv_obj_set_align(ui_Button2, LV_ALIGN_CENTER);
lv_obj_add_flag(ui_Button2, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
lv_obj_remove_flag(ui_Button2, LV_OBJ_FLAG_SCROLLABLE); /// Flags
@@ -66,8 +70,8 @@ void ui_Screen2_screen_init(void)
ui_Button3 = lv_button_create(ui_Screen2);
lv_obj_set_width(ui_Button3, 100);
lv_obj_set_height(ui_Button3, 80);
lv_obj_set_x(ui_Button3, 0);
lv_obj_set_y(ui_Button3, -100);
lv_obj_set_x(ui_Button3, 100);
lv_obj_set_y(ui_Button3, 0);
lv_obj_set_align(ui_Button3, LV_ALIGN_CENTER);
lv_obj_add_flag(ui_Button3, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
lv_obj_remove_flag(ui_Button3, LV_OBJ_FLAG_SCROLLABLE); /// Flags
@@ -75,6 +79,8 @@ void ui_Screen2_screen_init(void)
ui_Label2 = lv_label_create(ui_Screen2);
lv_obj_set_width(ui_Label2, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_Label2, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_Label2, -100);
lv_obj_set_y(ui_Label2, 0);
lv_obj_set_align(ui_Label2, LV_ALIGN_CENTER);
lv_label_set_text(ui_Label2, "Ctrl+C");
@@ -82,15 +88,15 @@ void ui_Screen2_screen_init(void)
lv_obj_set_width(ui_Label4, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_Label4, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_Label4, 0);
lv_obj_set_y(ui_Label4, 100);
lv_obj_set_y(ui_Label4, 0);
lv_obj_set_align(ui_Label4, LV_ALIGN_CENTER);
lv_label_set_text(ui_Label4, "Ctrl+V");
ui_Label5 = lv_label_create(ui_Screen2);
lv_obj_set_width(ui_Label5, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_Label5, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_Label5, 0);
lv_obj_set_y(ui_Label5, -100);
lv_obj_set_x(ui_Label5, 100);
lv_obj_set_y(ui_Label5, 0);
lv_obj_set_align(ui_Label5, LV_ALIGN_CENTER);
lv_label_set_text(ui_Label5, "Ctrl+X");
+10 -8
View File
@@ -4,6 +4,7 @@
// Project name: HeavenlyHeroStar
#include "../ui.h"
#include "board_config.h"
lv_obj_t * ui_Screen3 = NULL;
lv_obj_t * ui_BtnVolDown = NULL;
@@ -23,8 +24,8 @@ static void ui_screen3_create_button(lv_obj_t **btn, lv_obj_t **label, int32_t x
const char *text)
{
*btn = lv_button_create(ui_Screen3);
lv_obj_set_width(*btn, 75);
lv_obj_set_height(*btn, 90);
lv_obj_set_width(*btn, 90);
lv_obj_set_height(*btn, 45);
lv_obj_set_x(*btn, x);
lv_obj_set_y(*btn, y);
lv_obj_set_align(*btn, LV_ALIGN_CENTER);
@@ -100,14 +101,15 @@ void ui_event_BtnNext(lv_event_t * e)
void ui_Screen3_screen_init(void)
{
ui_Screen3 = lv_obj_create(NULL);
lv_obj_set_size(ui_Screen3, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Screen3, LV_OBJ_FLAG_SCROLLABLE);
ui_screen3_create_button(&ui_BtnVolDown, &ui_LabelVolDown, -42, -107, "Vol-");
ui_screen3_create_button(&ui_BtnVolUp, &ui_LabelVolUp, 42, -107, "Vol+");
ui_screen3_create_button(&ui_BtnMute, &ui_LabelMute, -42, 0, "Mute");
ui_screen3_create_button(&ui_BtnPlayPause, &ui_LabelPlayPause, 42, 0, "Play");
ui_screen3_create_button(&ui_BtnPrev, &ui_LabelPrev, -42, 107, "Prev");
ui_screen3_create_button(&ui_BtnNext, &ui_LabelNext, 42, 107, "Next");
ui_screen3_create_button(&ui_BtnVolDown, &ui_LabelVolDown, -50, -55, "Vol-");
ui_screen3_create_button(&ui_BtnVolUp, &ui_LabelVolUp, 50, -55, "Vol+");
ui_screen3_create_button(&ui_BtnMute, &ui_LabelMute, -50, 0, "Mute");
ui_screen3_create_button(&ui_BtnPlayPause, &ui_LabelPlayPause, 50, 0, "Play");
ui_screen3_create_button(&ui_BtnPrev, &ui_LabelPrev, -50, 55, "Prev");
ui_screen3_create_button(&ui_BtnNext, &ui_LabelNext, 50, 55, "Next");
lv_obj_add_event_cb(ui_BtnVolDown, ui_event_BtnVolDown, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_BtnVolUp, ui_event_BtnVolUp, LV_EVENT_ALL, NULL);
+16
View File
@@ -1,6 +1,7 @@
#include "settings_store.h"
#include "display.h"
#include "motion_gesture.h"
#include "esp_log.h"
#include "nvs.h"
@@ -15,6 +16,8 @@ static const settings_data_t SETTINGS_DEFAULTS = {
.brightness_pct = 70,
.volume_pct = 50,
.mute = false,
.shake_nav_enabled = true,
.shake_sensitivity = SHAKE_SENS_MED,
};
static settings_data_t s_data;
@@ -48,6 +51,10 @@ void settings_store_load(void)
load_key_u8(handle, "vol", &s_data.volume_pct, SETTINGS_DEFAULTS.volume_pct);
load_key_u8(handle, "mute", &val, SETTINGS_DEFAULTS.mute);
s_data.mute = val != 0;
load_key_u8(handle, "shake_en", &val, SETTINGS_DEFAULTS.shake_nav_enabled);
s_data.shake_nav_enabled = val != 0;
load_key_u8(handle, "shake_sens", &val, SETTINGS_DEFAULTS.shake_sensitivity);
s_data.shake_sensitivity = (shake_sensitivity_t)val;
nvs_close(handle);
ESP_LOGI(TAG, "Settings loaded (bright=%u vol=%u)", s_data.brightness_pct,
@@ -83,6 +90,12 @@ esp_err_t settings_store_commit(void)
if (err == ESP_OK) {
err = nvs_set_u8(handle, "mute", s_data.mute ? 1 : 0);
}
if (err == ESP_OK) {
err = nvs_set_u8(handle, "shake_en", s_data.shake_nav_enabled ? 1 : 0);
}
if (err == ESP_OK) {
err = nvs_set_u8(handle, "shake_sens", (uint8_t)s_data.shake_sensitivity);
}
if (err == ESP_OK) {
err = nvs_commit(handle);
}
@@ -118,4 +131,7 @@ void settings_store_apply(void)
{
uint16_t duty = (uint16_t)((s_data.brightness_pct * 1023U) / 100U);
display_backlight_set_brightness(duty);
motion_gesture_set_enabled(s_data.shake_nav_enabled);
motion_gesture_set_sensitivity(s_data.shake_sensitivity);
}
+41
View File
@@ -3,6 +3,8 @@
#include "ui_handlers.h"
static lv_obj_t *s_notif_switch;
static lv_obj_t *s_shake_nav_switch;
static lv_obj_t *s_shake_sens_dropdown;
static lv_obj_t *s_brightness_slider;
static lv_obj_t *s_brightness_pct_label;
static lv_obj_t *s_volume_slider;
@@ -43,6 +45,8 @@ static lv_obj_t *create_settings_body(lv_obj_t *content)
void load_system_settings(lv_obj_t *content, const settings_data_t *data)
{
s_notif_switch = NULL;
s_shake_nav_switch = NULL;
s_shake_sens_dropdown = NULL;
create_status_bar(content, "System Settings");
lv_obj_t *body = create_settings_body(content);
@@ -62,6 +66,35 @@ void load_system_settings(lv_obj_t *content, const settings_data_t *data)
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_NOTIFICATION);
lv_obj_t *shake_nav_label = lv_label_create(body);
lv_label_set_text(shake_nav_label, "Shake Navigation");
lv_obj_set_pos(shake_nav_label, 4, 40);
lv_obj_set_size(shake_nav_label, 120, 28);
s_shake_nav_switch = lv_switch_create(body);
lv_obj_set_pos(s_shake_nav_switch, 180, 40);
lv_obj_set_size(s_shake_nav_switch, 40, 28);
if (data->shake_nav_enabled) {
lv_obj_add_state(s_shake_nav_switch, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(s_shake_nav_switch, settings_on_value_changed,
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_SHAKE_NAV);
lv_obj_t *shake_sens_label = lv_label_create(body);
lv_label_set_text(shake_sens_label, "Shake Sensitivity");
lv_obj_set_pos(shake_sens_label, 4, 76);
lv_obj_set_size(shake_sens_label, 120, 28);
s_shake_sens_dropdown = lv_dropdown_create(body);
lv_obj_set_pos(s_shake_sens_dropdown, 120, 76);
lv_obj_set_size(s_shake_sens_dropdown, 100, 28);
lv_dropdown_set_options(s_shake_sens_dropdown, "Low\nMed\nHigh");
lv_dropdown_set_selected(s_shake_sens_dropdown, data->shake_sensitivity);
lv_obj_add_event_cb(s_shake_sens_dropdown, settings_on_value_changed,
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_SHAKE_SENS);
lv_obj_t *reset_btn = lv_button_create(body);
lv_obj_set_pos(reset_btn, 4, 112);
lv_obj_set_size(reset_btn, 100, 28);
@@ -170,6 +203,14 @@ void settings_tabs_collect(settings_tab_t tab, settings_data_t *out)
out->notification_enabled =
lv_obj_has_state(s_notif_switch, LV_STATE_CHECKED);
}
if (s_shake_nav_switch != NULL) {
out->shake_nav_enabled =
lv_obj_has_state(s_shake_nav_switch, LV_STATE_CHECKED);
}
if (s_shake_sens_dropdown != NULL) {
out->shake_sensitivity =
(shake_sensitivity_t)lv_dropdown_get_selected(s_shake_sens_dropdown);
}
break;
case SETTINGS_TAB_DISPLAY:
if (s_brightness_slider != NULL) {
+2
View File
@@ -7,6 +7,8 @@ typedef enum {
SETTINGS_FIELD_NOTIFICATION = 0,
SETTINGS_FIELD_VOLUME,
SETTINGS_FIELD_MUTE,
SETTINGS_FIELD_SHAKE_NAV,
SETTINGS_FIELD_SHAKE_SENS,
} settings_field_t;
void load_system_settings(lv_obj_t *content, const settings_data_t *data);
+8
View File
@@ -10,9 +10,17 @@ typedef enum {
SETTINGS_TAB_NETWORK,
} settings_tab_t;
typedef enum {
SHAKE_SENS_LOW = 0,
SHAKE_SENS_MED,
SHAKE_SENS_HIGH,
} shake_sensitivity_t;
typedef struct {
bool notification_enabled;
uint8_t brightness_pct;
uint8_t volume_pct;
bool mute;
bool shake_nav_enabled;
shake_sensitivity_t shake_sensitivity;
} settings_data_t;